Contents/ Part IV Β· Preparing Data for Analysis/ Chapter 20

Handling Missing Data

Almost every real dataset has gaps. How you fill or drop them can quietly bias your results, and the right choice depends on why the data is missing. This chapter gives you the mechanisms, the methods, and the pitfalls.

⏱️ ~13 min read
🐍 Notebook included
πŸ“Š Chapter 20

The Data-Cleaning Mindset chapter warned about hidden missing codes like -99, and the Finding & Removing Duplicates and Inconsistencies chapter showed that coercing bad values to numbers creates NaN. Now the gaps are real and visible. The question is what to do with them, and the honest answer starts with a question of your own: why are they missing?

βˆ…
Missing data are absent values in a dataset. How to handle them, delete or impute, depends on the missingness mechanism: whether the gaps are random or tied to the very values you cannot see.
1

Why Missingness Has a Mechanism

It is tempting to just drop the blanks and move on. But whether that is safe depends entirely on why they are blank. Rubin's classic taxonomy names three mechanisms, and the mechanism decides the method.

MCAR Missing Completely At Random Depends on nothing, pure chance. e.g. a sensor randomly drops a reading deletion is unbiased MAR Missing At Random Depends only on data you DID observe. e.g. men skip an item, and sex is recorded imputation can work MNAR Missing Not At Random Depends on the value you CANNOT see. e.g. high earners refuse to report income hardest: model it The three missingness mechanisms (Rubin)
🧭
You can't read the mechanism off the data

A test (Little's MCAR test) can give evidence against MCAR, but MAR and MNAR are impossible to tell apart from the observed data alone, because the deciding information is exactly the values you don't have. The mechanism is an assumption you justify with domain knowledge, and you should state it openly.

2

Detect First, Then Decide on Deletion

Before any fix: convert hidden codes (-99, "N/A") to real NaN, then count. df.isna().sum() and df.isna().mean()*100 tell you how much is missing and where; a missingness heatmap shows whether columns go blank together (a clue toward MAR).

The simplest response is deletion, and it is the most common mistake:

MethodHowGoodRisk
Listwise (complete-case)df.dropna() drops any row with a gapSimple; unbiased under MCARCan discard a LOT of rows; biased under MAR/MNAR
PairwiseUse rows present for each calculation (e.g. df.corr())Keeps more dataDifferent stats on different subsamples
Drop the columnRemove a feature that is mostly empty (rule of thumb >50–60%)Sheds a near-useless featureThreshold is arbitrary; missingness may itself be informative
πŸ—‘οΈ
"Just drop the NaNs" compounds fast

With three columns each only 10% missing, only about 0.9Β³ β‰ˆ 73% of rows survive listwise deletion, you can lose a quarter of the data while no single column looks bad. And deletion is unbiased only under MCAR; under MAR or MNAR it systematically distorts your results.

3

Imputation: Filling the Gaps

Rather than drop, you can impute, estimate the missing values. The methods run from trivial to sophisticated, and each has a catch worth knowing.

MethodHow it worksThe catchscikit-learn
Mean / median / modeFill with the column's average (median for skewed, mode for categorical)Shrinks variance, biases correlations toward 0; ignores other columnsSimpleImputer(strategy=...)
Constant + indicatorFill a fixed value and add a "was-missing" flag columnKeeps the missingness signal; constant fill alone still distortsadd_indicator=True
RegressionPredict the gap from other columnsDeterministic version inflates correlations; add a random residual (stochastic)IterativeImputer
KNNFill from the k most similar rowsMust standardize first; curse of dimensionalityKNNImputer
MICE (iterative)Model each column from all others, cycle to convergenceThe serious default under MAR; sklearn's is experimental & singleIterativeImputer
πŸ“‰
The killer caveat: mean imputation lies

Replace every gap with the column mean and the standard deviation drops, a fake spike piles up at the mean, and relationships with other variables wash out. It is a fine quick baseline, but never a basis for careful inference. The notebook shows the variance shrinking before your eyes.

🎲
Single imputation understates uncertainty

A filled-in value is a guess, but the analysis treats it as if it were truly observed, so standard errors come out too small and confidence intervals too narrow. Multiple imputation fixes this: generate several complete datasets, analyze each, and pool the results (Rubin's rules) so the uncertainty of guessing is carried through. scikit-learn's IterativeImputer is MICE-style but does single imputation; full multiple imputation lives in R's mice or statsmodels.

4

A Decision Framework

Put it together into a repeatable flow. The mechanism drives the method; there is no universal best.

Convert hidden codes to NaN, then quantify & visualize Reason about the mechanism MCAR + little missing deletion is fine MAR + substantial impute (KNN / MICE) MNAR model it / indicator / sensitivity Column >50–60% empty? Consider dropping it. Always document the assumption. For modeling: impute inside a Pipeline fit on TRAIN only.
🚰
Fit the imputer on training data only

Same discipline as scaling in the Standardization & Z-Scores chapter: learn the fill values (mean, neighbors, MICE model) from the training set, then apply them to the test set. Imputing over the whole dataset before the split leaks test information into training and inflates your scores. A scikit-learn Pipeline handles this automatically inside cross-validation.

πŸ€–
Why this matters for data science

Most estimators refuse to run on NaN, so you must deal with gaps before modeling, and how you deal with them shapes every result that follows. Mishandled missingness is one of the quietest sources of biased conclusions in all of analysis. Choosing deliberately, and documenting the assumption, is what separates a defensible analysis from a lucky one. With gaps handled, the next chapter turns to the values that are present but extreme: detecting and treating outliers.

5

Missing Data in Machine Learning & AI

Most models refuse to train on missing values, so handling them is a required step, and doing it carelessly leaks information or bakes in bias. The reason a value is missing decides the right fix.

ApproachThe scikit-learn toolWhen to reach for it
Simple imputationSimpleImputer (mean, median, most-frequent)A fast, robust default, median for skewed features
Model-based imputationKNNImputer, IterativeImputerWhen other features can predict the missing one
Missing-indicatoradd_indicator=True, or a manual flag columnWhen the fact that a value is missing is itself informative (MNAR)
πŸ€–
Impute inside the pipeline, fit on train only

Compute the imputation value (the mean, say) on the training set, then apply it to the test set, exactly as with scaling. Impute before you split and you leak test information into training. And when data is missing not at random, add a missing-indicator, because the missingness carries signal a plain fill would erase.

6

Real-World Example: A Survey with Gaps

In this survey, income is missing not at random: higher earners are the ones who leave it blank. The result is a trap, the average of the incomes people did report is biased low (about 57,000 dollars against a true 61,000), and mean-imputing that value would spread the bias to everyone. The companion notebook maps the missingness and compares imputation strategies.

πŸ“‚ Dataset Β· handling-missing-data--survey_missing.xlsx

One row per respondent: respondent_id, age, region, hours_online, and income. income is missing not at random (high earners skip it), while age and hours_online are missing roughly at random, so the mechanism differs by column.

🐍

Bring it to life in Python

The companion notebook converts sentinels to NaN and charts the gaps, measures how many rows deletion really costs, shows mean imputation shrinking the variance with a fake spike, imputes with KNN (scaled) and MICE, and fits an imputer on train only to avoid leakage.

πŸ““ View Notebook (code & outputs) β–Ά Open in Colab ⬇ View / Download on GitHub

View opens the rendered notebook instantly (no setup). Open in Colab runs & edits it live in your browser. To run locally, install numpy, pandas, scikit-learn, matplotlib (and optionally missingno) and launch jupyter notebook.

πŸŽ“ Key Takeaways

  • βœ“The mechanism drives the method: MCAR (random), MAR (depends on observed data), MNAR (depends on the missing value itself).
  • βœ“You can't prove the mechanism from data; MAR vs MNAR needs domain knowledge, so state your assumption.
  • βœ“Deletion is unbiased only under MCAR and compounds fast across columns; under MAR/MNAR it biases results.
  • βœ“Mean imputation shrinks variance; single imputation understates uncertainty (multiple imputation fixes it); KNN/MICE use other columns.
  • βœ“Fit imputers on train only to avoid leakage, and keep a missingness indicator when the gap is informative.
7

Practice Challenges

Five short challenges, beginner to intermediate. Try them on paper or in Python before checking the solutions.

1

Name the mechanism

Classify each: (a) a scale randomly drops every 50th reading from a glitch; (b) high earners skip the income question; (c) men skip an item more often, and sex is recorded.

Hint: depends on nothing β†’ MCAR; on the missing value β†’ MNAR; on an observed variable β†’ MAR.
2

Count the gaps

A frame uses -1 for missing age. Convert it to NaN and report the count and percent missing per column.

Hint: replace(-1, np.nan) first, then isna().sum() / isna().mean()*100.
3

Deletion cost

For a 500-row frame where each of 3 columns is independently 10% missing, how many complete rows survive listwise deletion? Compute it.

Hint: len(df.dropna()); compare with 0.9Β³.
4

Mean imputation cost

Take 300 values ~N(100, 20), blank out 40%, then mean-impute. Compare the standard deviation before and after, and explain what happened.

Hint: SimpleImputer(strategy="mean"); the SD shrinks.
5

No leakage

Split a frame with missing values into train/test, then impute without leaking. Show the test set is filled with the value learned from the training data.

Hint: SimpleImputer().fit(X_train) then transform(X_test).
βœ…
Check your work

A fully-worked solutions notebook walks through all five challenges in the same visual style. Try them yourself first, then compare.

πŸ““ View Solutions β–Ά Open Solutions in Colab ⬇ View / Download on GitHub
8

Quiz: Test Yourself

Eight quick questions on missing data. Answer them, hit Check Answers, and keep refining until you score 100%. Your progress is saved, so you can hop back to the chapter and return anytime.