Index Ai Academy
Phase 3 · Machine learning · Foundations

Preparing data

Clean, encode and scale data before a model sees it.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

A model only accepts complete tables of numbers. Real data is rarely like that. Some customers never subscribed to your emails, so they have no open rate. Regions are words, not numbers. Order values run from about $20 to almost $200, while a discount share sits between 0 and 1. Getting data ready usually takes more time than building the model. Small mistakes here can quietly make your results look better than they are. This lesson shows the three everyday fixes, and the one rule that keeps them honest.

What you'll be able to do

  • Find and handle missing values: drop, fill, and flag
  • Turn categories into numbers with one-hot encoding
  • Scale numeric features with standardization
  • Fit every preparation step on training data only, and reuse it

What it covers

  1. Missing values
  2. Categories: one-hot encoding
  3. Scaling numbers
  4. Worked example: scaling with training numbers
  5. Fit on training data, apply everywhere

Missing values

Think of a paper sign-up form. Some people skip a box. Maybe they had no answer, or maybe they didn't want to say. Customer data has the same gaps. A model can't work with a blank, so you have to decide what goes there.

The code below loads 600 customers from the course's practice shop. One line turns empty region text into a proper "missing" marker. The last line counts the missing values in each column and shows only the columns that have some.

import pandas as pd
import requests

df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"])
df["region"] = df["region"].mask(df["region"] == "")      # empty string → missing
print(df.isna().sum()[lambda s: s > 0])
It runs right here in your browser, with nothing to install.

44 customers have no email open rate, because they never subscribed. 23 have no region. You have three standard options.

  1. Drop the rows. This is the simplest fix, but you lose data. Worse, the missing rows may be different from the rest. Here they're the people who never subscribed. Drop them and the data left behind no longer looks like your real customers.
  2. Fill the gap with a typical value. Filling in a value is also called imputing. For numbers, use the median. For categories, use the most common value or an honest "unknown".
  3. Flag it. Add a new column that holds 1 where the value was missing and 0 where it wasn't. The fact that a value is missing often tells you something. Never subscribing to email might predict that a customer leaves.

The median is the middle value once you sort the numbers. For open rates of 0.2, 0.4 and 0.6, the median is 0.4. It's a good choice because one extreme value can't drag it far. With 0.2, 0.4 and an odd 0.99, the median is still 0.4. The average would jump to about 0.53. An extreme value like that 0.99 is called an outlier.

Filling plus flagging is a good default. The fill lets the model run. The flag lets it learn whether "missing" matters.

Quick check
Three customers have open rates 0.2, 0.5 and 0.9. A fourth has none. You fill the gap with the median. What does this print?
import statistics
rates = [0.2, 0.5, 0.9]   # the customer with no rate is left out
print(statistics.median(rates))
  1. 0.9
  2. 0
  3. 0.5
  4. 0.53
Show the answer

0.5

Sorted, the rates are 0.2, 0.5, 0.9. The middle one is 0.5. The missing value is skipped, not counted as zero. (0.53 would be the average.)

Quick check
All 44 customers with no open rate never subscribed to email. What goes wrong if you drop those rows?
  1. Nothing, missing rows are always random
  2. The data left over no longer represents all your customers, and you lose a possible warning sign
  3. The model gets more data to learn from
  4. pandas refuses to drop rows
Show the answer

The data left over no longer represents all your customers, and you lose a possible warning sign

The missing rows share something (no subscription). Dropping them skews what remains, and throws away a pattern that might predict churn.

Categories: one-hot encoding

A model works by multiplying numbers by weights. It can't multiply a weight by "west". So you need to turn words into numbers.

Think of a tick-box question on a form: "How did you find us? Tick one." Each answer gets its own box. One-hot encoding does exactly that. It turns one category column into one 0/1 column per category. Each row has a 1 in the box that matches, and 0 everywhere else:

channel→channel_emailchannel_referralchannel_searchchannel_social
search0010
social0001

Why not number the categories 1, 2, 3 instead? Because numbers carry meaning that categories don't. If search is 1 and social is 2, the model reads social as "twice search". That's meaningless. The boxes keep each category separate, with no order and no size.

pandas does one-hot encoding with pd.get_dummies:

import pandas as pd

df = pd.DataFrame({"channel": ["search", "social", "email", "search"]})
print(pd.get_dummies(df, columns=["channel"], dtype=int))

You get three columns, because only three channels appear in this small table. They come in alphabetical order. Every row has exactly one 1. The setting dtype=int asks for 0 and 1 instead of True and False.

Quick check
A color column holds red, green, blue, green. You one-hot encode it. How many new columns do you get?
  1. 1
  2. 4
  3. 3
  4. 2
Show the answer

3

One column per distinct category: blue, green and red. Repeats of green don't add a column.

Scaling numbers

Picture comparing two shoppers. Their order values differ by $10. Their discount shares differ by 0.7, which is a huge gap: one rarely uses a discount and the other usually does. A model that adds up differences sees 10 and 0.7. The dollar gap wins, only because dollars are bigger numbers.

Columns on very different scales cause trouble for models that measure distances between rows. They also slow down gradient descent, the step-by-step training method from Phase 2. The fix is to put every number column on the same ruler.

Standardization rescales a column so its average is 0 and its standard deviation is 1. If you met z-scores in Phase 2, this is the same calculation:

z=x−μtrainσtrainz = \frac{x - \mu_{\text{train}}}{\sigma_{\text{train}}}

Here xx is the original value, such as an order value in dollars. The answer, zz, is the scaled value. μtrain\mu_{\text{train}} (mu, pronounced "mew") is the column's average over the training rows. σtrain\sigma_{\text{train}} (sigma) is the column's standard deviation over the same rows. The standard deviation measures how spread out the values are.

In plain words: subtract the training average, then divide by the training spread. The answer says how many standard deviations a value sits above (positive) or below (negative) the training average.

A small example. Your training rows have order values of $20, $40 and $60. The average is $40. The standard deviation is $20. A new order of $80 sits $40 above average. Divide by 20 and you get z=2z = 2. An order of $20 gives z=−1z = -1.

Not every model needs this. Tree-based models (later in this phase) don't need scaling. Linear models, nearest neighbors and neural networks do.

Go deeper Two ways to compute a standard deviation

There are two common formulas for the standard deviation. One divides by the number of values, nn. The other divides by n−1n - 1, which gives a slightly bigger answer for small samples.

pandas .std() and Python's statistics.stdev divide by n−1n - 1. NumPy's np.std divides by nn unless you pass ddof=1. For $20, $40 and $60, dividing by n−1n - 1 gives

  1. Dividing by nn gives about 16.3. With hundreds of rows the two barely differ. Pick one

and use it everywhere.

Worked example

Worked example: scaling with training numbers

The training rows have order values of $20, $40 and $60. Standardize three values: two from training ($20 and $60) and a new customer's $80.

import statistics

train = [20, 40, 60]                 # order values in the training rows
mean = statistics.mean(train)        # 40
std = statistics.stdev(train)        # 20.0 (divides by n - 1, like pandas)

for x in [20, 60, 80]:
    print(x, "->", (x - mean) / std)
In the lesson you can change this and run it.

$20 becomes −1.0 and $60 becomes 1.0: one standard deviation below and above the average. The new customer's $80 becomes 2.0. It's scaled with the training average and spread, not with numbers that include the new value. A scaled value can land outside the range seen in training. That's fine.

Quick check
The training average is 40 and the standard deviation is 20. What does this print for a new value of 50?
mean, std = 40, 20        # learned from the training rows
print((50 - mean) / std)
  1. 10
  2. 0.5
  3. 2.5
  4. -0.5
Show the answer

0.5

50 − 40 = 10, and 10 / 20 = 0.5. The value is half a standard deviation above the training average.

Fit on training data, apply everywhere

To check a model fairly, you keep some rows aside. These rows are the test set. The model never learns from them. The other rows are the training data. Later, real new customers arrive that nobody has seen yet.

Every preparation step learns something from data:

  • filling learns the median;
  • one-hot encoding learns the list of categories;
  • scaling learns an average and a standard deviation.

The rule: learn these numbers from the training data only. Then reuse the same numbers on the test set and on all future data.

Here's why. Think of a student preparing for an exam. If they peek at the exam while studying, their score looks better than it would on a fresh exam. If you compute the scaling average over all the data, the test rows sneak into training in the same way. It's a mild version of the leakage from the last lesson. Your test score comes out a little too good.

There's a second reason. Once the model is in use, new customers arrive one at a time. You'll only ever have the training numbers, so the model must be built around those.

The code below uses the first 450 customers for training and keeps the last 150 as the test set.

import numpy as np
import pandas as pd
import requests

df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"])
train, test = df.iloc[:450], df.iloc[450:]

median_open = train["email_open_rate"].median()          # learned from training rows only
mean, std = train["avg_order_value"].mean(), train["avg_order_value"].std()

def prepare(part):
    out = pd.DataFrame(index=part.index)
    out["open_missing"] = part["email_open_rate"].isna().astype(int)
    out["email_open_rate"] = part["email_open_rate"].fillna(median_open)
    out["aov_z"] = (part["avg_order_value"] - mean) / std
    return out

print(prepare(train).describe().round(2).loc[["mean", "std"]])
print(prepare(test).head(3).round(2))

The code learns three numbers from the training rows. The median open rate is 0.35. The average order value is about $69.89, with a standard deviation of about $35. Over all 600 customers the average would be about $70.49. That small gap is exactly the test-set information you keep out.

The prepare function then uses those three numbers on any group of rows. The first printout shows the training rows: the scaled order value aov_z has an average of 0 and a standard deviation of 1, as standardization promises. The second printout shows three test customers. Their order values are around $30 below the training average, so their aov_z values are negative, around −0.9.

In scikit-learn (the workflow lesson later in this phase), each step becomes a transformer. A transformer has a fit method, which learns from training data, and a transform method, which applies what it learned anywhere. You chain transformers in a pipeline, so you can't get the order wrong.

Watch out: one-hot encoding must produce the same columns in training and in use. A category that only appears later, such as a new channel, needs a plan. You could add an "other" column, or ignore it.

Try it now

The order matters: fitting any step on all the data leaks the test set into training.

Quick check
You standardize features using the mean and standard deviation of the whole dataset, then split into train and test. What's wrong?
  1. The mean should be computed on the test set
  2. Nothing, standardization doesn't learn anything
  3. Test-set information leaked into training, so the test score is slightly optimistic
  4. Standardization must happen after training
Show the answer

Test-set information leaked into training, so the test score is slightly optimistic

Fit preparation steps on training data only, then apply them to the test set.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

Why add an 'open_missing' column instead of only filling the missing values?

  1. Being missing can itself be informative (never subscribing may predict churn)
  2. Models can't handle filled values unless a second column marks which ones they are
  3. It makes the dataset smaller, because rows with a flag can be dropped during training
  4. It's required by pandas, which raises an error if you fill a column with no record of it

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson