The question before any imputation 8 min live
Before you type SimpleImputer, answer a question nobody asks in the sprint: why is this cell empty? Three columns can each be 30% blank and look identical on a data-quality dashboard, and the correct handling for each is different. Get the cause wrong and you do not just impute badly - you delete a real feature and skew the distribution you are training on.
LiveThe three mechanisms, in plain language5 min▶
Strip away the statistics vocabulary and there are three stories a blank cell can be telling you:
- Missing completely at random. The blankness has nothing to do with the row. A connection dropped, a batch job half-failed, a device rebooted mid-form. The rows with gaps look like the rows without them, so filling the gap with a central value distorts very little.
- Missing at random, given other columns. The blankness is not random, but it is fully explained by columns you already have. Lumen's mobile checkout never rendered the optional field, so
device = "mobile"predicts the gap perfectly. You cannot ignore it, but you can model it from the columns that explain it. - Missing NOT at random. The blankness depends on the missing value itself, or on the outcome.
incomeis left blank disproportionately by high earners.support_noteis empty precisely because the order went perfectly - which is exactly the kind of order you may be trying to predict. Here the missingness is the information.
You cannot prove which mechanism you are in from the data alone. You get there by asking the person who owns the form, the app screen, or the upstream job - which is why this session is as much an interview technique as a coding one.
LiveWhat mean-imputing a not-at-random column actually costs3 min▶
Two separate damages, and teams usually only notice the second one much later:
- You destroy real signal. If blank means "this order was uneventful", then every blank row carried a usable fact about the target. Replace them all with the column mean and every one of those rows now says the same neutral thing. The feature that was there is gone, and no amount of model tuning brings it back.
- You bias the distribution. Filling 30% of a column with its own mean piles a third of the mass onto a single point. Variance shrinks, correlations get diluted toward zero, and any downstream scaler or binner you fit afterwards is fitting a shape the real world never had.
Both damages are silent. Nothing raises an error, the pipeline runs, and the score is merely a little worse than it should have been - the hardest kind of problem to notice.
The toolkit, and the move that costs nothing 9 min live
sklearn gives you three imputers and one small utility, and the utility is the one that wins arguments. Read the table for what each imputer decides, then look hard at the last row: the missingness indicator does not fill anything at all, it simply lets the model see that the cell was blank.
| Tool | What it does | Use it when |
|---|---|---|
SimpleImputer | Fills with mean, median, most_frequent or a constant you supply | The default. Median for anything skewed, most_frequent or constant for categoricals |
IterativeImputer | Models each column with gaps as a function of the other columns, round-robin | Missing-at-random cases where other columns genuinely explain the gap. Still an experimental feature - you must opt in explicitly |
KNNImputer | Fills from the values of the nearest complete rows | Rows have close neighbours and the data is small enough to afford the distance computation |
MissingIndicator / add_indicator=True | Adds a boolean column saying "this was blank", alongside the filled value | Almost always. It is one argument, it costs one column, and it is the only way the model can learn from absence directly |
LiveMedian over mean, and a sentinel for categoricals4 min▶
Two defaults worth making automatic:
- Median, not mean, for money columns. Lumen's
prior_30d_spendis right-skewed like every spend column you will ever meet - a long tail of big spenders drags the mean well above the typical customer. Impute with the mean and every blank row is quietly labelled a slightly-above-average spender. The median puts them where most customers actually are. - A constant plus an explicit sentinel for categoricals.
SimpleImputer(strategy="constant", fill_value="unknown")makes "we did not have this" a named level rather than a silent merge into whatever happened to be most frequent. The most_frequent strategy has its uses, but it hides the gap inside a real category, and you can never separate them again downstream.
OneHotEncoder, np.nan and None are treated as separate categories. That means NaN can simply be a category when blankness is meaningful - no imputation step at all, just a column in the one-hot output that says "this was missing". For a not-at-random categorical, that is often the cleanest thing you can do.
LiveThe build-along pipeline5 min▶
Numerics get a median plus an indicator; categoricals get a named sentinel and then one-hot encoding that tolerates unseen levels at prediction time. Note that nothing is fitted outside the Pipeline:
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
numeric = Pipeline([
("impute", SimpleImputer(strategy="median", add_indicator=True)),
("scale", StandardScaler()),
])
categorical = Pipeline([
("impute", SimpleImputer(strategy="constant", fill_value="unknown")),
("encode", OneHotEncoder(handle_unknown="ignore")),
])
prep = ColumnTransformer([
("num", numeric, ["prior_30d_spend", "pages_viewed"]),
("cat", categorical, ["channel", "device", "product_category"]),
])
pipe = Pipeline([
("prep", prep),
("model", LogisticRegression(max_iter=1000)),
])
pipe.fit(X_train, y_train) # every median learned from train rows only
One argument, add_indicator=True, is the whole missingness-as-signal lesson in code. The imputed value keeps the row usable; the indicator keeps the fact that it was blank.
Self-studyIterativeImputer, and why it comes with a warning label3 min read▶
IterativeImputer models each column with gaps from the other columns and cycles until the estimates settle. It is genuinely useful in the missing-at-random case, and it is still an experimental feature in sklearn - you must import the experimental enabler before the class is available at all, and the API can change between releases.
Two practical cautions before you reach for it: it is far more expensive than a median, and it makes the imputed values a function of the whole feature set, which means a single upstream column changing shape now moves values in a column you were not touching. Start simple, and let a measured gain justify the complexity - not the other way round.
An imputer is a model, not a cleaning step ★ 9 min · everyone runs it
Here is the sentence that puts this session on the leakage rail: imputation statistics are learned parameters. A median is not a fact about your column, it is a number estimated from a specific set of rows. Compute it on the full dataset before splitting and you have committed preprocessing leakage - family 2 from b5, full stop, no nuance, no "but it is only a median".
What goes wrong. The global median is computed from rows that include your test set. The test rows now have their gaps filled with a number that was informed by the test rows themselves. Your held-out score is measuring something production will never reproduce.
Why "it is only a median" is not a defence. sklearn's rule is written without an exemption: "Never include test data when using the fit and fit_transform methods." A median is a fit. A most-frequent category is a fit. A KNN imputer is unambiguously a fit.
The fix is structural, not disciplinary. Put the imputer inside the Pipeline, so each cross-validation fold learns its own fill values from its own training rows. sklearn states the purpose plainly: pipelines "help avoid leaking statistics from your test data into the trained model in cross-validation, by ensuring that the same samples are used to train the transformers and predictors."
The habit to build. If a preprocessing step has a number in it that came from data, it belongs inside the Pipeline. Imputation, scaling, encoding, selection - all of them. The Pipeline is not tidiness, it is the thing that makes the wrong version hard to type.
The month the column went 90% blank. A team imputed a key numeric feature with the global median, cross-validated cleanly, and shipped. In their historical data the column was about 8% missing every month, so the fill value touched almost nothing and nobody thought about it again. Then an upstream form change made the field optional, the column arrived 90% missing, and nearly every prediction collapsed toward the same value - because for nine rows in ten the model's strongest feature had become a constant. The model had never once been evaluated under the missingness rate it actually faced. Nothing errored, no alert fired, and the first signal was a business metric drifting a week later.
Missingness moves read after class
Everything above assumed the missingness rate is a property of the column. It is not. It is a property of the systems upstream of you, and those systems change without telling you.
Self-studyThree ways a stable column starts going blank4 min read▶
- A column gets deprecated upstream. The producing service stops writing it, or writes it only for a subset of traffic. The column still exists, the schema still validates, and the values simply stop arriving.
- A form field is made optional. The most common cause and the most invisible one, because it is a product decision made in a different team's sprint, for perfectly good conversion reasons. Overnight, a column that was 2% missing becomes 60% missing.
- The mechanism itself changes. Worse than a rate change: a column that was missing completely at random becomes missing not at random, or the reverse. The correct handling flips, and your pipeline carries on doing the old thing.
Self-studyMonitor the missingness rate as a first-class signal3 min read▶
The missingness rate of every feature is worth monitoring in its own right, on the same dashboard as your model's score. It is cheap to compute, it needs no labels, and it moves before your metrics do - a rate that jumps from 8% to 90% is visible on day one, while the business impact shows up weeks later.
Two thresholds worth setting per feature: an absolute one ("alert if more than 40% blank") and a relative one ("alert if the rate doubles versus the training window"). The relative one catches the slow drift the absolute one sleeps through.
This is where the practitioner track meets the leader track: in a3, missingness is framed as part of the feature budget - a data-quality signal that decides which features you can responsibly build at all. If a column cannot be relied on to arrive, its predictive power on historical data is a promise you cannot keep.
Try it yourself - this week ◐ 25-35 min total
- Take the three most-missing columns in a table you actually work with and write one sentence each on why the cells are blank. If you cannot write the sentence, you have found this week's conversation to have with the upstream owner.
- For each of those columns, label the mechanism: completely at random, at random given other columns, or not at random. Note which handling your current pipeline uses and whether it matches.
- Add
add_indicator=Trueto one numeric imputer and re-score. Note the change - it is often small, occasionally large, and never costs more than one column. - Find one imputation statistic in your codebase that is computed outside a Pipeline. Move it inside and re-run cross-validation. If the score drops, you just found leakage.
- Blank one important column at 90% in a copy of your held-out set and re-score. Write down the number. That is your model's answer to the question "what if this feed breaks?"
Official sources covered
This session works through sklearn's imputation classes and the part of the common-pitfalls chapter that governs them, plus the categorical NaN behaviour carried over from b3 and Google's data-scrubbing framing. Certificates and graded exercises stay with the official providers (Kaggle Learn, Google MLCC). This page covers:
Three questions before you go 🎯 ◐ 90 seconds
1 · support_note is empty on 70% of Lumen's orders, because nothing went wrong on those orders. Which mechanism is that, and what follows?
The gap depends on what happened to the order, which is close to what you are predicting. Fill it with a central value and every one of those rows now says the same neutral thing. Keep the blank, either as an indicator column or as its own one-hot category.
2 · What does add_indicator=True actually buy you?
It fills nothing and estimates nothing. It preserves the one piece of information that every imputation strategy otherwise destroys - that the cell was empty. One argument, one extra column, and the not-at-random signal survives.
3 · Why must the imputer live inside the Pipeline rather than being applied to the dataframe first?
A median is a fit. sklearn's rule has no exemption for it: never include test data in fit or fit_transform. Pipelines exist precisely to "avoid leaking statistics from your test data into the trained model in cross-validation."