Preparing data
Clean, encode and scale data before a model sees it.
Create a free account Try part of it here
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
- Missing values
- Categories: one-hot encoding
- Scaling numbers
- Worked example: scaling with training numbers
- 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])
44 customers have no email open rate, because they never subscribed. 23 have no region. You have three standard options.
- 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.
- 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". - 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.
import statistics rates = [0.2, 0.5, 0.9] # the customer with no rate is left out print(statistics.median(rates))
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.)
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_email | channel_referral | channel_search | channel_social |
|---|---|---|---|---|---|
| search | 0 | 0 | 1 | 0 | |
| social | 0 | 0 | 0 | 1 |
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.
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:
Here is the original value, such as an order value in dollars. The answer, , is the scaled value. (mu, pronounced "mew") is the column's average over the training rows. (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 . An order of $20 gives .
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, . The other divides by , which gives a slightly bigger answer for small samples.
pandas .std() and Python's statistics.stdev divide by . NumPy's np.std divides by unless you pass ddof=1. For $20, $40 and $60, dividing by gives
- Dividing by gives about 16.3. With hundreds of rows the two barely differ. Pick one
and use it everywhere.
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)
$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.
mean, std = 40, 20 # learned from the training rows print((50 - mean) / std)
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.
The order matters: fitting any step on all the data leaks the test set into 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
Try a quiz question
Why add an 'open_missing' column instead of only filling the missing values?