Contents
Figure 1: Every dataset has holes — the mechanism that made them decides which fix is safe
Four imputers, four ways of losing data, one honest experiment
Your dataset has holes. Every real one does. The question is what you do about them, and the default answer — fill with the mean and move on — is sometimes fine and sometimes quietly wrong.
The same experiment was run on one dataset with four imputers, four ways of losing data, and a few downstream models. The results are more nuanced than "use the fancy one," and one of them surprised us.
First, Ask Why the Data Is Missing
Statisticians sort missingness into three types, and the type decides what's safe:
- MCAR (missing completely at random): a cell vanishes by pure chance. A sensor glitch is the usual example.
- MAR (missing at random): missingness depends on other columns you can see. Older patients skip a question more often, say.
- MNAR (missing not at random): the value itself causes the gap. High earners leave the income field blank.
You can't prove which one you have from the data alone. That's the uncomfortable part. You decide using what you know about how the data was collected.
The Experiment
We used the breast cancer dataset that ships with scikit-learn (569 rows, 30 numeric features) and hid about 15% of the cells four different ways: MCAR, MAR, MNAR, and a "target-linked" version where benign cases lose values more often than malignant ones. Then we tested these approaches:
- Mean and median imputation (
SimpleImputer) - KNN imputation (
KNNImputer,k=5) - Iterative imputation (
IterativeImputer) - Mean plus a missing-indicator column (
add_indicator=True) HistGradientBoosting, which accepts NaN directly
One practical footnote: scikit-learn still marks IterativeImputer as experimental, so you must from sklearn.experimental import enable_iterative_imputer before importing it.
Why "Just Drop the Rows" Fails Here
Dropping incomplete rows sounds clean. With 30 columns, it destroys your data. At 15% missing per cell, the theoretical share of fully complete rows is 0.85**30 — about 0.8%. In the MCAR draw, 0.53% of rows survived, which is three out of 569.
Complete-case analysis only works when you have few columns, few holes, or both. Otherwise you throw away nearly everything.
Round One: How Close Do the Guesses Get?
We hid known values, imputed them, and measured the error on those cells. Data was standardized, so an RMSE of 1.0 means "no better than guessing the average." Lower is better. Each figure averages five random masks.
| Mechanism | Mean | Median | KNN (k=5) | Iterative |
|---|---|---|---|---|
| MCAR | 1.000 | 1.026 | 0.574 | 0.405 |
| MAR | 1.133 | 1.216 | 0.655 | 0.459 |
| MNAR | 1.228 | 1.349 | 0.720 | 0.469 |
| Target-linked | 0.889 | 0.826 | 0.532 | 0.368 |
Two things stand out:
- Multivariate methods win by a wide margin. Iterative imputation cut the error to well under half of mean imputation's. That makes sense for this dataset: its features are highly correlated, so other columns say a lot about a missing one.
- Mean and median can't beat "guess the average" under MAR or MNAR. Their error rose above 1.0.
The Bias Problem
Error size isn't the whole story. Direction matters too. This table shows the average signed error, where zero means unbiased and a negative number means imputing too low:
| Mechanism | Mean | Median | KNN | Iterative |
|---|---|---|---|---|
| MCAR | 0.005 | -0.214 | -0.066 | 0.000 |
| MAR | -0.313 | -0.534 | -0.119 | -0.008 |
| MNAR | -0.609 | -0.826 | -0.276 | -0.055 |
Under MNAR, big values go missing, so filling with the column average pulls everything downward — mean imputation was off by about 0.6 standard deviations on average. Median did worse here because these features are skewed, so the median sits below the mean.
IMO, this is the strongest reason to avoid the mean by habit. It preserves the average only when data goes missing for harmless reasons. One caveat: the iterative imputer's bias was small here, but no method fixes MNAR in general — it was just less wrong on this dataset.
Round Two: Does It Matter for the Final Model?
Here's the honest twist. A logistic regression was scored with each imputer inside the pipeline, using ROC AUC across 5-fold, 3-repeat cross-validation. With no missing data, it scored 0.9950.
| Mechanism | Mean | Mean + indicator | KNN | Iterative | HistGB (native NaN) |
|---|---|---|---|---|---|
| MCAR | 0.9920 | 0.9871 | 0.9911 | 0.9940 | 0.9885 |
| MAR | 0.9934 | 0.9869 | 0.9936 | 0.9948 | 0.9914 |
| MNAR | 0.9941 | 0.9913 | 0.9946 | 0.9948 | 0.9908 |
| Target-linked | 0.9906 | 0.9998 | 0.9949 | 0.9952 | 0.9959 |
The spread is tiny. This dataset is easy, so almost any method lands near 0.99, and differences of 0.001 to 0.005 are within what you'd expect from noise. One mask per mechanism and no significance test means you shouldn't read a winner into the middle rows.
Better imputation accuracy doesn't automatically become a better model. When the model already has strong signal, it shrugs off sloppy fills. A harder dataset could behave differently, and this experiment didn't test one.
When the Missing Flag Is the Signal
The last row is the interesting one. In the target-linked scenario, mean plus an indicator scored 0.9998 — higher than the 0.9950 obtained with no missing data at all.
That isn't magic. The scenario was built so benign cases lose values far more often, which means the pattern of holes reveals the answer, and the indicator column let the model read it. Real data sometimes works this way: a doctor skips a test because the patient looks healthy. But it's also a warning — if missingness leaks the label in your training data and not in production, your model will look great and then fail.
A personal example makes the point concrete: in one churn project, a "contract_notes" column was empty for almost everyone who churned, because support stopped logging notes once an account was clearly leaving. A model that saw the blank learned churn fast — and then couldn't reproduce it in production, where the blanks appeared for a completely different reason. If a gap looks predictive, ask what human process created it before you trust it.
Add an indicator when you suspect the gap itself carries information. When the indicator did not help in the other scenarios, it cost a little AUC (0.9871 vs 0.9920 under MCAR). Don't add it blindly.
Leakage: A Smaller Deal Than the Warnings Suggest
You'll hear "always impute inside your cross-validation folds." That's correct advice, and the cost of ignoring it is cheap to measure. Fitting imputers on all rows before splitting and comparing:
- Mean: leaky 0.9920, proper 0.9920, difference 0.0000.
- Iterative: leaky 0.9937, proper 0.9940, difference -0.0002.
The gap was negligible here. That's one dataset with simple imputers, so it doesn't prove leakage is harmless. Still, the pipeline version costs you nothing, so use it — as the preprocessing guide shows, it's just a scikit-learn Pipeline:
from sklearn.pipeline import Pipeline
from sklearn.impute import IterativeImputer
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
("impute", IterativeImputer(max_iter=50, random_state=0)),
("model", LogisticRegression(max_iter=1000)),
])
# fit happens on the training fold only
pipe.fit(X_train, y_train)
It also prevents the bigger problems that appear with target-aware methods.
A Practical Decision Guide
- Few missing values, MCAR: mean or median is fine. Keep it simple.
- Correlated columns, moderate gaps: try
IterativeImputerorKNNImputer. They were far more accurate in these tests. - Tree-based models: try native NaN support, such as
HistGradientBoostingClassifier, before building any imputation at all. - Missingness might carry meaning: add an indicator and check that it helps in validation.
- Anything MNAR: be suspicious. No imputer removes the bias for sure. Talk to whoever collected the data.
One practical note: IterativeImputer hit its 10-iteration limit in all 20 imputation runs of round one and raised convergence warnings. Raise max_iter or check the results before trusting it in production. And run validation on the cleaned output either way, as covered in the data validation guide.
Common Pitfalls
- Filling with the mean by habit without asking why the values are missing.
- Dropping rows when many columns are involved — you may lose almost everything.
- Trusting a higher validation score from a missingness indicator without checking whether that pattern of holes will exist in production.
- Imputing before the train/test split — costs little here, but target-aware methods make it expensive.
- Ignoring convergence warnings from
IterativeImputerand shipping stale iterations. - Reading a winner into noise-level AUC differences on an easy dataset.
Best Book to Read
| Cover | Book | Description | Get it |
|---|---|---|---|
![]() |
Python for Data Analysis | the pandas reference for finding, masking, and reshaping missing data before an imputer ever runs. | View on Amazon |
![]() |
Python Feature Engineering Cookbook | recipe-style coverage of imputation methods, missing indicators, and the pipelines that keep them leak-free. | View on Amazon |
![]() |
Statistics | a clear, no-nonsense treatment of why MCAR, MAR, and MNAR change what conclusions your numbers can support. | View on Amazon |
![]() |
Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow | end-to-end projects showing where imputation sits inside a real training pipeline. | View on Amazon |
As an Amazon Associate I earn from qualifying purchases. Links on this page are affiliate links, and I may earn a commission at no extra cost to you.
Unlock AI That Actually Works
Get lifetime access to GPT-6 Astra, Claude Fable 5.1, Gemini 3.5, Grok 4.5, and more — all in one platform. Build websites, apps, videos, content, and digital products from a single command. No monthly fees. No tool-hopping.
Click here to get GPTAstra Max now — one-time payment, lifetime access.
Frequently Asked Questions
What is the best imputation method in Python?
It depends on why the data is missing and what model consumes it. In this experiment, IterativeImputer and KNNImputer recovered hidden values far more accurately than mean or median on a correlated dataset — Iterative cut error to well under half of mean imputation. But the downstream logistic regression barely cared: differences of 0.001–0.005 AUC were within noise. Try iterative or KNN for accuracy, native NaN handling for tree models, and measure the effect on your own data instead of assuming a winner.
Is mean imputation bad?
Not always — it's fine when data is missing completely at random and you have few holes. It becomes quietly wrong when missingness depends on the value itself (MNAR): in this experiment mean imputation was biased by about 0.6 standard deviations, pulling large values downward, because it preserves the average only when data vanishes for harmless reasons. Median did even worse on these skewed features. If the gap might carry meaning, be suspicious of the default.
Should I drop rows with missing values?
Rarely, when you have many columns. With 30 features and 15% of cells hidden, the theoretical share of fully complete rows is 0.85**30 ≈ 0.8% — in practice only three of 569 rows survived as complete cases. Complete-case analysis only works when you have few columns, few holes, or both. Otherwise you throw away nearly everything, which is why imputation (or a model that accepts NaN natively) is the default.
Does imputing before cross-validation cause data leakage?
Yes, in principle: fit the imputer only on training folds, inside a scikit-learn Pipeline. In this experiment the measured cost was negligible (mean: 0.0000 difference; iterative: -0.0002), because simple imputers learn little from the data. That doesn't prove leakage is harmless — target-aware methods leak far more. The pipeline version costs nothing, so use it and you're protected against the bigger failures.
Wrapping This Up
Dropping rows fails when you have many columns. Mean and median imputation are quick, but they biased the results under MAR and MNAR. KNN and iterative imputation recovered hidden values much more accurately on this correlated dataset. The downstream model barely cared, which is its own lesson: measure on your data, not ours.
Open your own dataset, pick the column with the most gaps, and check whether the missing rows differ from the rest before you choose a method.



