Data cleaning and preparation skill 01 of 7
Handling missing data
Missing values are a question, not a defect to paper over. This skill makes an AI agent find every gap, including the ones disguised as -999 or “N/A”, work out why the data is missing, then drop, impute or flag it on purpose, with the method matched to the column and no test data leaked along the way.
handling-missing-data/SKILL.md, version 1.0.0, about 2,900 words.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 31 October 2026. Updated 31 October 2026. Free to use under CC BY 4.0.
When to use this, and when not to
Use this skill when a dataset has gaps and you need to decide what to do with them before analysis or modelling. The triggers are a request to fill or remove missing values, columns full of NaN, NA, NULL or blanks, a call to dropna, fillna or an imputer, a row count that fell without explanation, or placeholder values like -999, 9999, "N/A" or empty strings standing in for real numbers. The work is to find every missing value including the disguised ones, decide why it is missing, then drop, impute or flag it on purpose.
Do not use it for values that are present but extreme or impossible (that is outlier-detection), for rows that repeat (that is duplicate-detection), for reshaping rows into columns, or for deciding which schema columns should allow NULL. Those are separate jobs, and mixing them is how a cleaning pass quietly changes numbers it was never asked to touch.
The core idea: a missing value is a question, not a defect to paper over. Fill it only once you know why it is missing and what the fill will do to the distribution, and keep the fact that it was missing, because that fact is often information in its own right (Rubin, "Inference and missing data", 1976).
Inputs the agent must have before starting
- The tool and version. pandas 3.0 changed how in-place writes behave, SQL has no built-in imputation, and scikit-learn's
IterativeImputeris still experimental. The right code depends on which one you are in (see the appendix). - What each column means and its type, so you know whether a gap is numeric, categorical, a date, or free text.
- The real meaning of "missing" in this data. Ask whether a blank means unknown, not applicable, or a true zero, and whether placeholders like
-999,0,"N/A","Unknown"or an empty string are being used as stand-ins. A legitimate zero must never be imputed. - Where the data is going. Imputation for a one-off report is low stakes; imputation that feeds a model needs a train and test split, so the method must be fitted without leaking test information.
- Whether rows or columns may be removed at all. Dropping data is irreversible, so confirm it is allowed before doing it, and default to working on a copy rather than the raw source.
If any of these is unknown, ask before touching the data. Guessing that a 0 is missing, or that an empty string is a true blank, changes the answer silently.
Raw SKILL.md file
This is the exact file agents read, including the credit header and licence notice. Save it as handling-missing-data/SKILL.md.
---
name: handling-missing-data
description: >
Find, diagnose and handle missing values in a dataset so that gaps are filled or removed on purpose, not by accident, across pandas, scikit-learn, SQL and R. Covers disguised sentinels (-999, "N/A", "", blanks), the MCAR, MAR and MNAR mechanisms, the drop versus impute versus flag decision, mean, median, mode, forward and backward fill, interpolation, KNN, iterative and multiple imputation, the missingness indicator, and fitting the imputer on training data only to avoid leakage. Use it when someone says "handle missing data", "fill the NaNs", "impute", "deal with nulls", "dropna", "fillna", "my column has blanks", "replace missing values", or "why did my row count drop". Do NOT use it for extreme but present values (see outlier-detection), for repeated rows (see duplicate-detection), for turning rows into columns, or for designing a schema's nullability.
license: Free to use and adapt under CC BY 4.0, with credit to Afeez Adebayo. Link afeezadebayo.com, https://www.linkedin.com/in/afeez-adebayo/ and this skill's page at https://afeezadebayo.com/skills-md/data/data-cleaning-preparation/handling-missing-data/.
---
# Handling missing data
## When to use this, and when not to
Use this skill when a dataset has gaps and you need to decide what to do with them before analysis or modelling. The triggers are a request to fill or remove missing values, columns full of `NaN`, `NA`, `NULL` or blanks, a call to `dropna`, `fillna` or an imputer, a row count that fell without explanation, or placeholder values like `-999`, `9999`, `"N/A"` or empty strings standing in for real numbers. The work is to find every missing value including the disguised ones, decide why it is missing, then drop, impute or flag it on purpose.
Do not use it for values that are present but extreme or impossible (that is `outlier-detection`), for rows that repeat (that is `duplicate-detection`), for reshaping rows into columns, or for deciding which schema columns should allow NULL. Those are separate jobs, and mixing them is how a cleaning pass quietly changes numbers it was never asked to touch.
The core idea: a missing value is a question, not a defect to paper over. Fill it only once you know why it is missing and what the fill will do to the distribution, and keep the fact that it was missing, because that fact is often information in its own right (Rubin, "Inference and missing data", 1976).
## Inputs the agent must have before starting
1. The tool and version. pandas 3.0 changed how in-place writes behave, SQL has no built-in imputation, and scikit-learn's `IterativeImputer` is still experimental. The right code depends on which one you are in (see the appendix).
2. What each column means and its type, so you know whether a gap is numeric, categorical, a date, or free text.
3. The real meaning of "missing" in this data. Ask whether a blank means unknown, not applicable, or a true zero, and whether placeholders like `-999`, `0`, `"N/A"`, `"Unknown"` or an empty string are being used as stand-ins. A legitimate zero must never be imputed.
4. Where the data is going. Imputation for a one-off report is low stakes; imputation that feeds a model needs a train and test split, so the method must be fitted without leaking test information.
5. Whether rows or columns may be removed at all. Dropping data is irreversible, so confirm it is allowed before doing it, and default to working on a copy rather than the raw source.
If any of these is unknown, ask before touching the data. Guessing that a `0` is missing, or that an empty string is a true blank, changes the answer silently.
## The workflow, step by step
### 1. Surface the disguised missing values first
Before counting anything, find the gaps that `isna` cannot see. Teams encode missing data as `-999`, `9999`, `0`, `"N/A"`, `"NA"`, `"null"`, `"none"`, `"unknown"`, a single space or an empty string. A standard null check reports zero missing while the column is full of them. Run a value frequency count on each column, look for suspicious spikes at round sentinels, then convert those to the tool's real missing marker (`pandas` `replace`, SQL `NULLIF`) so every later step sees them. Decision rule: if a value is a placeholder for "no data", it is missing, whatever it looks like, and must be converted before you measure.
### 2. Measure the missingness, per column and overall
Count missing values per column as a percentage, and count how many rows have any gap at all. This single table drives every later decision: a column that is 2% missing, a column that is 60% missing and a column that is always present each need different treatment, and you cannot tell which is which until you have measured.
### 3. Classify the mechanism (MCAR, MAR, MNAR)
The mechanism decides what handling is safe (Rubin, 1976; Little and Rubin, *Statistical Analysis with Missing Data*).
- **MCAR, missing completely at random.** The gap is unrelated to anything, observed or not, for example a sample lost in transit. Listwise deletion is unbiased here, though it still costs sample size.
- **MAR, missing at random.** The gap depends on other observed columns but not on the missing value itself, for example younger users skipping an income field more often when age is recorded. Imputation that uses the other columns is appropriate.
- **MNAR, missing not at random.** The gap depends on the value that is missing, for example high earners refusing to state income. No method fully corrects this from the data alone; it needs a missingness indicator, domain knowledge and a sensitivity analysis.
To probe the mechanism, compare the rows where a column is missing against the rows where it is present, across the other observed columns. If missingness is predicted by observed columns, treat it as MAR. If no observed column predicts it, it is consistent with MCAR. A non-significant Little's MCAR test is consistent with MCAR but cannot rule out MNAR (metricgate, "How to handle missing data"). MNAR cannot be confirmed from the data; it is a judgement from how the data was collected.
### 4. Decide drop, impute or flag, with thresholds
- **Drop rows** only when under about 5% of rows are incomplete and MCAR is plausible; above that, deletion loses power and can bias results (metricgate). Row deletion is irreversible, so confirm it first.
- **Drop a column** only when it is almost entirely missing (a common cut is above 50 to 60%) and carries no usable signal. Even then, a near-empty column can be worth keeping as a presence or absence flag.
- **Flag and impute** is the default for everything in between: add an indicator (step 5), then impute (step 6).
- **MNAR or informative missingness** is never silently imputed. Add the indicator, impute if you must, and run the analysis with and without the imputed values to see how much they move the result.
### 5. Add a missingness indicator before you impute
For any column where the fact of being missing could carry meaning, add a boolean column (`income_missing`) before imputing, so the signal survives the fill (scikit-learn `MissingIndicator`, or `add_indicator=True` on the imputer). This is cheap insurance: if missingness turns out to be informative, the model can still use it; if not, the indicator does no harm.
### 6. Choose the imputation method by type, distribution and purpose
- **Categorical:** fill with the most frequent value, or better, with an explicit `"Unknown"` category that preserves the signal rather than inventing a majority label.
- **Numeric, roughly symmetric:** the mean is acceptable for a quick fill.
- **Numeric, skewed:** use the median, which is not dragged by the tail.
- **Ordered or time series:** forward fill, backward fill or interpolation, but only within a meaningful order and never across unrelated groups. Interpolate by time, not position, when the index is a timestamp with uneven gaps (pandas `interpolate(method="time")`).
- **Values that depend on other columns:** `KNNImputer` uses similar rows, and `IterativeImputer` (MICE) models each column from the others.
- **Statistical inference:** single imputation understates uncertainty. Use multiple imputation (MICE with 20 or more imputations) and pool with Rubin's rules, not one filled dataset (wentorai, "missing-data-handling"; scikit-learn impute guide).
Decision rule: mean and median single imputation shrink variance and weaken correlations, so they are fine for a rough report and wrong for inference. The more the result depends on the spread of the data, the more you move towards KNN, iterative or multiple imputation.
### 7. Fit the imputer on training data only
When the data feeds a model, compute the fill statistics (mean, median, neighbours, model) on the training split alone, then apply those same statistics to both the training and test sets. Fitting the imputer on the full dataset before the split leaks test information into training and makes the model look better than it is (scikit-learn impute guide). For a time series, fit on past data only. This is the single most common correctness bug in cleaning code found in the wild, including in published cleaning skills that call `fit_transform` on the whole frame.
### 8. Write the fill correctly for your tool
In pandas 3.0, copy-on-write means `df[col].fillna(x, inplace=True)` operates on a temporary and silently leaves the original unchanged. Assign the result back instead: `df[col] = df[col].fillna(x)`, or `df.fillna({col: x})`. The same applies to `ffill`, `bfill` and `replace`. Any cleaning code that relies on `inplace=True` on a column should be rewritten (pandas, "Working with missing data"; pandas Copy-on-Write notes).
### 9. Verify
Re-run the missing count and confirm it is zero where you intended, and still non-zero where you chose to leave gaps. Compare the mean and standard deviation of each imputed column before and after: a large variance drop is the signature of over-eager mean imputation. Confirm the indicators are present, confirm the imputer was fitted on training data only, and log the before and after counts so the decision is auditable.
## Worked example, end to end
A customer table has `income` with gaps, and a quick null check reports 4% missing. A value count shows a spike of `-999` and a scatter of `"N/A"` strings, so the true missingness is 18% once those are converted to real nulls. Probing shows that missing `income` is more common for younger customers and one acquisition channel, both recorded, so the mechanism is MAR, not MCAR.
The wrong approach fills the whole column with the mean, in place, on the full dataset before splitting:
```python
df["income"].fillna(df["income"].mean(), inplace=True) # three bugs at once
```
This leaks across the train and test split, it shrinks the variance of a skewed column, and under pandas 3.0 the `inplace=True` may leave the column unchanged, so the gaps quietly remain. The corrected version converts sentinels, adds an indicator, uses the median because income is skewed, and fits on the training split only:
```python
import numpy as np
train["income"] = train["income"].replace([-999, "N/A"], np.nan)
test["income"] = test["income"].replace([-999, "N/A"], np.nan)
train["income_missing"] = train["income"].isna().astype(int)
test["income_missing"] = test["income"].isna().astype(int)
fill = train["income"].median() # fitted on train only
train["income"] = train["income"].fillna(fill)
test["income"] = test["income"].fillna(fill)
```
Verifying, the missing count is zero, the median fill moved the mean only slightly, the `income_missing` flag is kept for the model to use, and the test set was filled with the training median, so no test information leaked. The report states that 18% of income was imputed with the training median under a MAR assumption, so no reader mistakes the filled values for observed ones.
## Variations you will meet in practice
- **Legitimate zeros.** A `0` in a purchases column may mean "bought nothing", not "unknown". Confirm before converting zeros to nulls; the opposite error, imputing over a real zero, inflates every total.
- **Whole rows empty.** Rows that are blank except for an identifier are usually structural debris; drop them after confirming they carry no data.
- **Nullable dtypes.** With pandas `Int64`, `boolean` or `string` columns, the marker is `pd.NA`, and `bool(pd.NA)` raises `TypeError`. Use `isna`, or `&`, `|`, `~`, not `and`, `or`, `not` (pandas missing-data guide).
- **Silent reductions.** `pd.Series([np.nan]).sum()` returns `0.0`, and reductions skip `NA` by default (`skipna=True`). Pass `skipna=False` when a missing value should make the whole result missing (pandas missing-data guide).
- **Categorical "Unknown" beats mode.** Filling a categorical gap with the most frequent label can invent a pattern; an explicit `"Unknown"` category keeps the truth and lets the model learn from it.
## Templates you can use verbatim
```python
# 1. Surface disguised missing values and measure (pandas)
import numpy as np, pandas as pd
SENTINELS = [-999, 9999, "N/A", "NA", "null", "none", "unknown", "", " "]
df = df.replace(SENTINELS, np.nan)
missing = (df.isna().mean() * 100).round(1).sort_values(ascending=False)
print(missing) # percentage missing per column
# 2. Flag then impute, fitted on train only (no leakage, no inplace)
for col in ["income", "age"]:
med = train[col].median()
for part in (train, test):
part[f"{col}_missing"] = part[col].isna().astype(int)
part[col] = part[col].fillna(med)
# 3. Multivariate / inference-grade imputation
from sklearn.experimental import enable_iterative_imputer # required import
from sklearn.impute import IterativeImputer, KNNImputer
imp = KNNImputer(n_neighbors=5).fit(train[num_cols]) # fit on train
train[num_cols] = imp.transform(train[num_cols])
test[num_cols] = imp.transform(test[num_cols])
```
```sql
-- SQL: treat a sentinel as NULL, then fill with a chosen value
SELECT id,
COALESCE(NULLIF(income, -999), (SELECT median_income FROM params)) AS income
FROM customers; -- SQL has no built-in mean/median impute; compute the fill yourself
```
## Failure modes and anti-patterns
1. **Missing values hidden as sentinels.** A null check reports clean while the column is full of `-999` or `"N/A"`. Spot it with a value count and a suspicious spike at a round number or placeholder string. Fix: convert sentinels to real nulls before measuring.
2. **Mean imputation presented as fact.** The standard deviation falls and correlations weaken after filling, then the report quotes confidence intervals as if the values were observed. Spot it by comparing variance before and after. Fix: median or multiple imputation, add an indicator, and label imputed values.
3. **Data leakage.** The imputer is fitted on the whole dataset before the split, so test scores look too good. Spot it when offline metrics beat production by a wide margin. Fix: fit on the training split only, transform both.
4. **`inplace=True` that does nothing.** Under pandas 3.0 copy-on-write, `df[col].fillna(x, inplace=True)` leaves the column unchanged. Spot it when nulls remain after a fill. Fix: assign the result back.
5. **Silent dropping.** `dropna()` removes rows or columns and the sample shrinks or an informative column disappears. Spot it from a fallen row count or a MNAR pattern. Fix: flag and confirm before dropping, and run the analysis with and without the dropped data.
## Quality bar
- Every disguised sentinel is converted to a real missing marker before anything is measured.
- Missingness is quantified per column, and the assumed mechanism (MCAR, MAR or MNAR) is stated with a reason.
- A missingness indicator is added wherever the gap could be informative.
- The imputation method matches the column type, its distribution and whether the output is a report or an inference.
- Any imputer that feeds a model is fitted on training data only, and pandas writes are assigned back rather than relying on `inplace`.
- No row or column is dropped without explicit confirmation, and imputed values are labelled as estimates, never presented as observed.
- A before-and-after count and a variance check are recorded.
## Data handling and safety
Missing values often sit in records with personal or sensitive fields (income, health, contact details). Do not log raw values, paste them into prompts or send them to a third-party service while imputing; work on the data in place with the tool you already have. Dropping rows or columns, and overwriting the original file, are irreversible, so require explicit human confirmation and default to writing a new cleaned copy rather than editing the raw source. Imputed values are estimates: label them as such, keep the indicator column, and never let a filled number be read as a measured one. When the data affects health, money, legal standing or employment, an imputed value can change a decision about a real person, so add a clear "this is estimated, not observed" note and a human review step, and do not present imputed figures as fact.
## Appendix: tool-specific notes (verify before relying on these)
- **pandas:** `isna`/`notna` detect `NaN`, `NaT` and `pd.NA`; equality does not (`np.nan == np.nan` is `False`, `pd.NA == pd.NA` is `<NA>`). `dropna`, `fillna`, `ffill`, `bfill`, `interpolate`, `replace`. `fillna(None)` does not fill a numeric column. Reductions skip `NA` by default; `sum` of all-`NA` is `0`. Under copy-on-write, assign results back rather than using `inplace=True`.
- **scikit-learn:** `SimpleImputer` (`strategy` of `mean`, `median`, `most_frequent`, `constant`); `KNNImputer` (`n_neighbors`, `nan_euclidean_distances`); `IterativeImputer`, which is experimental and needs `from sklearn.experimental import enable_iterative_imputer`; `MissingIndicator` or `add_indicator=True`. Fit on training data only. `SimpleImputer` drops all-missing columns unless `keep_empty_features=True`.
- **SQL:** missing is `NULL`; test with `IS NULL`, not `=`. `NULLIF(x, -999)` turns a sentinel into `NULL`; `COALESCE` fills with a fallback. There is no native mean or median imputation, so compute the fill value in a subquery or application layer, and never fill with a magic number that later reads as real.
- **R:** missing is `NA`; test with `is.na`. The `mice` package does multiple imputation and pooling; `naniar` and `VIM` visualise and summarise missingness.
## Sources
- pandas documentation, "Working with missing data": the sentinels per dtype (`NaN`, `NaT`, `pd.NA`), that equality does not detect missing values, `dropna`/`fillna`/`ffill`/`bfill`/`interpolate`/`replace` behaviour, that `fillna(None)` does not fill numeric columns, that reductions skip `NA` by default and `sum` of all-`NA` is `0`, and the `pd.NA` boolean-context error. Verified 31 October 2026.
- scikit-learn User Guide, "Imputation of missing values" (section 8.4, v1.9.1): `SimpleImputer` strategies, `KNNImputer` with `nan_euclidean_distances`, `IterativeImputer` as experimental with the required enabling import, `MissingIndicator` and `add_indicator`, and the list of estimators that handle `NaN` natively. Verified 31 October 2026.
- Rubin, "Inference and missing data" (Biometrika, 1976), and Little and Rubin, *Statistical Analysis with Missing Data*: the MCAR, MAR and MNAR mechanisms and the principle that the mechanism determines valid handling. Verified 31 October 2026.
- metricgate, "How to handle missing data: MCAR, MAR, and MNAR": definitions and examples of each mechanism, that Little's MCAR test non-significance is consistent with MCAR but cannot separate MAR from MNAR, that complete-case analysis is often acceptable when under 5% of rows are incomplete and MCAR is plausible, and that multiple imputation is the standard for MAR. Verified 31 October 2026.
- wentorai research-plugins, "missing-data-handling" and "data-cleaning-pipeline" SKILL.md: a worked MICE and Rubin's-rules pooling implementation, the recommendation of 20 or more imputations, and a reproducible cleaning order. Noted as an existing implementation that this skill improves on by adding the leakage rule, the copy-on-write fix and the sentinel-first step. Verified 31 October 2026.
The workflow, step by step
1. Surface the disguised missing values first
Before counting anything, find the gaps that isna cannot see. Teams encode missing data as -999, 9999, 0, "N/A", "NA", "null", "none", "unknown", a single space or an empty string. A standard null check reports zero missing while the column is full of them. Run a value frequency count on each column, look for suspicious spikes at round sentinels, then convert those to the tool's real missing marker (pandas replace, SQL NULLIF) so every later step sees them. Decision rule: if a value is a placeholder for "no data", it is missing, whatever it looks like, and must be converted before you measure.
2. Measure the missingness, per column and overall
Count missing values per column as a percentage, and count how many rows have any gap at all. This single table drives every later decision: a column that is 2% missing, a column that is 60% missing and a column that is always present each need different treatment, and you cannot tell which is which until you have measured.
3. Classify the mechanism (MCAR, MAR, MNAR)
The mechanism decides what handling is safe (Rubin, 1976; Little and Rubin, *Statistical Analysis with Missing Data*).
- MCAR, missing completely at random. The gap is unrelated to anything, observed or not, for example a sample lost in transit. Listwise deletion is unbiased here, though it still costs sample size.
- MAR, missing at random. The gap depends on other observed columns but not on the missing value itself, for example younger users skipping an income field more often when age is recorded. Imputation that uses the other columns is appropriate.
- MNAR, missing not at random. The gap depends on the value that is missing, for example high earners refusing to state income. No method fully corrects this from the data alone; it needs a missingness indicator, domain knowledge and a sensitivity analysis.
To probe the mechanism, compare the rows where a column is missing against the rows where it is present, across the other observed columns. If missingness is predicted by observed columns, treat it as MAR. If no observed column predicts it, it is consistent with MCAR. A non-significant Little's MCAR test is consistent with MCAR but cannot rule out MNAR (metricgate, "How to handle missing data"). MNAR cannot be confirmed from the data; it is a judgement from how the data was collected.
4. Decide drop, impute or flag, with thresholds
- Drop rows only when under about 5% of rows are incomplete and MCAR is plausible; above that, deletion loses power and can bias results (metricgate). Row deletion is irreversible, so confirm it first.
- Drop a column only when it is almost entirely missing (a common cut is above 50 to 60%) and carries no usable signal. Even then, a near-empty column can be worth keeping as a presence or absence flag.
- Flag and impute is the default for everything in between: add an indicator (step 5), then impute (step 6).
- MNAR or informative missingness is never silently imputed. Add the indicator, impute if you must, and run the analysis with and without the imputed values to see how much they move the result.
5. Add a missingness indicator before you impute
For any column where the fact of being missing could carry meaning, add a boolean column (income_missing) before imputing, so the signal survives the fill (scikit-learn MissingIndicator, or add_indicator=True on the imputer). This is cheap insurance: if missingness turns out to be informative, the model can still use it; if not, the indicator does no harm.
6. Choose the imputation method by type, distribution and purpose
- Categorical: fill with the most frequent value, or better, with an explicit
"Unknown"category that preserves the signal rather than inventing a majority label. - Numeric, roughly symmetric: the mean is acceptable for a quick fill.
- Numeric, skewed: use the median, which is not dragged by the tail.
- Ordered or time series: forward fill, backward fill or interpolation, but only within a meaningful order and never across unrelated groups. Interpolate by time, not position, when the index is a timestamp with uneven gaps (pandas
interpolate(method="time")). - Values that depend on other columns:
KNNImputeruses similar rows, andIterativeImputer(MICE) models each column from the others. - Statistical inference: single imputation understates uncertainty. Use multiple imputation (MICE with 20 or more imputations) and pool with Rubin's rules, not one filled dataset (wentorai, "missing-data-handling"; scikit-learn impute guide).
Decision rule: mean and median single imputation shrink variance and weaken correlations, so they are fine for a rough report and wrong for inference. The more the result depends on the spread of the data, the more you move towards KNN, iterative or multiple imputation.
7. Fit the imputer on training data only
When the data feeds a model, compute the fill statistics (mean, median, neighbours, model) on the training split alone, then apply those same statistics to both the training and test sets. Fitting the imputer on the full dataset before the split leaks test information into training and makes the model look better than it is (scikit-learn impute guide). For a time series, fit on past data only. This is the single most common correctness bug in cleaning code found in the wild, including in published cleaning skills that call fit_transform on the whole frame.
8. Write the fill correctly for your tool
In pandas 3.0, copy-on-write means df[col].fillna(x, inplace=True) operates on a temporary and silently leaves the original unchanged. Assign the result back instead: df[col] = df[col].fillna(x), or df.fillna({col: x}). The same applies to ffill, bfill and replace. Any cleaning code that relies on inplace=True on a column should be rewritten (pandas, "Working with missing data"; pandas Copy-on-Write notes).
9. Verify
Re-run the missing count and confirm it is zero where you intended, and still non-zero where you chose to leave gaps. Compare the mean and standard deviation of each imputed column before and after: a large variance drop is the signature of over-eager mean imputation. Confirm the indicators are present, confirm the imputer was fitted on training data only, and log the before and after counts so the decision is auditable.
Worked example, end to end
A customer table has income with gaps, and a quick null check reports 4% missing. A value count shows a spike of -999 and a scatter of "N/A" strings, so the true missingness is 18% once those are converted to real nulls. Probing shows that missing income is more common for younger customers and one acquisition channel, both recorded, so the mechanism is MAR, not MCAR.
The wrong approach fills the whole column with the mean, in place, on the full dataset before splitting:
df["income"].fillna(df["income"].mean(), inplace=True) # three bugs at onceThis leaks across the train and test split, it shrinks the variance of a skewed column, and under pandas 3.0 the inplace=True may leave the column unchanged, so the gaps quietly remain. The corrected version converts sentinels, adds an indicator, uses the median because income is skewed, and fits on the training split only:
import numpy as np
train["income"] = train["income"].replace([-999, "N/A"], np.nan)
test["income"] = test["income"].replace([-999, "N/A"], np.nan)
train["income_missing"] = train["income"].isna().astype(int)
test["income_missing"] = test["income"].isna().astype(int)
fill = train["income"].median() # fitted on train only
train["income"] = train["income"].fillna(fill)
test["income"] = test["income"].fillna(fill)Verifying, the missing count is zero, the median fill moved the mean only slightly, the income_missing flag is kept for the model to use, and the test set was filled with the training median, so no test information leaked. The report states that 18% of income was imputed with the training median under a MAR assumption, so no reader mistakes the filled values for observed ones.
Variations you will meet in practice
- Legitimate zeros. A
0in a purchases column may mean "bought nothing", not "unknown". Confirm before converting zeros to nulls; the opposite error, imputing over a real zero, inflates every total. - Whole rows empty. Rows that are blank except for an identifier are usually structural debris; drop them after confirming they carry no data.
- Nullable dtypes. With pandas
Int64,booleanorstringcolumns, the marker ispd.NA, andbool(pd.NA)raisesTypeError. Useisna, or&,|,~, notand,or,not(pandas missing-data guide). - Silent reductions.
pd.Series([np.nan]).sum()returns0.0, and reductions skipNAby default (skipna=True). Passskipna=Falsewhen a missing value should make the whole result missing (pandas missing-data guide). - Categorical "Unknown" beats mode. Filling a categorical gap with the most frequent label can invent a pattern; an explicit
"Unknown"category keeps the truth and lets the model learn from it.
Templates you can use verbatim
# 1. Surface disguised missing values and measure (pandas)
import numpy as np, pandas as pd
SENTINELS = [-999, 9999, "N/A", "NA", "null", "none", "unknown", "", " "]
df = df.replace(SENTINELS, np.nan)
missing = (df.isna().mean() * 100).round(1).sort_values(ascending=False)
print(missing) # percentage missing per column
# 2. Flag then impute, fitted on train only (no leakage, no inplace)
for col in ["income", "age"]:
med = train[col].median()
for part in (train, test):
part[f"{col}_missing"] = part[col].isna().astype(int)
part[col] = part[col].fillna(med)
# 3. Multivariate / inference-grade imputation
from sklearn.experimental import enable_iterative_imputer # required import
from sklearn.impute import IterativeImputer, KNNImputer
imp = KNNImputer(n_neighbors=5).fit(train[num_cols]) # fit on train
train[num_cols] = imp.transform(train[num_cols])
test[num_cols] = imp.transform(test[num_cols])-- SQL: treat a sentinel as NULL, then fill with a chosen value
SELECT id,
COALESCE(NULLIF(income, -999), (SELECT median_income FROM params)) AS income
FROM customers; -- SQL has no built-in mean/median impute; compute the fill yourselfFailure modes and anti-patterns
- Missing values hidden as sentinels. A null check reports clean while the column is full of
-999or"N/A". Spot it with a value count and a suspicious spike at a round number or placeholder string. Fix: convert sentinels to real nulls before measuring. - Mean imputation presented as fact. The standard deviation falls and correlations weaken after filling, then the report quotes confidence intervals as if the values were observed. Spot it by comparing variance before and after. Fix: median or multiple imputation, add an indicator, and label imputed values.
- Data leakage. The imputer is fitted on the whole dataset before the split, so test scores look too good. Spot it when offline metrics beat production by a wide margin. Fix: fit on the training split only, transform both.
inplace=Truethat does nothing. Under pandas 3.0 copy-on-write,df[col].fillna(x, inplace=True)leaves the column unchanged. Spot it when nulls remain after a fill. Fix: assign the result back.- Silent dropping.
dropna()removes rows or columns and the sample shrinks or an informative column disappears. Spot it from a fallen row count or a MNAR pattern. Fix: flag and confirm before dropping, and run the analysis with and without the dropped data.
Quality bar
- Every disguised sentinel is converted to a real missing marker before anything is measured.
- Missingness is quantified per column, and the assumed mechanism (MCAR, MAR or MNAR) is stated with a reason.
- A missingness indicator is added wherever the gap could be informative.
- The imputation method matches the column type, its distribution and whether the output is a report or an inference.
- Any imputer that feeds a model is fitted on training data only, and pandas writes are assigned back rather than relying on
inplace. - No row or column is dropped without explicit confirmation, and imputed values are labelled as estimates, never presented as observed.
- A before-and-after count and a variance check are recorded.
Data handling and safety
Missing values often sit in records with personal or sensitive fields (income, health, contact details). Do not log raw values, paste them into prompts or send them to a third-party service while imputing; work on the data in place with the tool you already have. Dropping rows or columns, and overwriting the original file, are irreversible, so require explicit human confirmation and default to writing a new cleaned copy rather than editing the raw source. Imputed values are estimates: label them as such, keep the indicator column, and never let a filled number be read as a measured one. When the data affects health, money, legal standing or employment, an imputed value can change a decision about a real person, so add a clear "this is estimated, not observed" note and a human review step, and do not present imputed figures as fact.
Appendix: tool-specific notes (verify before relying on these)
- pandas:
isna/notnadetectNaN,NaTandpd.NA; equality does not (np.nan == np.nanisFalse,pd.NA == pd.NAis<NA>).dropna,fillna,ffill,bfill,interpolate,replace.fillna(None)does not fill a numeric column. Reductions skipNAby default;sumof all-NAis0. Under copy-on-write, assign results back rather than usinginplace=True. - scikit-learn:
SimpleImputer(strategyofmean,median,most_frequent,constant);KNNImputer(n_neighbors,nan_euclidean_distances);IterativeImputer, which is experimental and needsfrom sklearn.experimental import enable_iterative_imputer;MissingIndicatororadd_indicator=True. Fit on training data only.SimpleImputerdrops all-missing columns unlesskeep_empty_features=True. - SQL: missing is
NULL; test withIS NULL, not=.NULLIF(x, -999)turns a sentinel intoNULL;COALESCEfills with a fallback. There is no native mean or median imputation, so compute the fill value in a subquery or application layer, and never fill with a magic number that later reads as real. - R: missing is
NA; test withis.na. Themicepackage does multiple imputation and pooling;naniarandVIMvisualise and summarise missingness.
Sources
- pandas documentation, "Working with missing data": the sentinels per dtype (
NaN,NaT,pd.NA), that equality does not detect missing values,dropna/fillna/ffill/bfill/interpolate/replacebehaviour, thatfillna(None)does not fill numeric columns, that reductions skipNAby default andsumof all-NAis0, and thepd.NAboolean-context error. Verified 31 October 2026. - scikit-learn User Guide, "Imputation of missing values" (section 8.4, v1.9.1):
SimpleImputerstrategies,KNNImputerwithnan_euclidean_distances,IterativeImputeras experimental with the required enabling import,MissingIndicatorandadd_indicator, and the list of estimators that handleNaNnatively. Verified 31 October 2026. - Rubin, "Inference and missing data" (Biometrika, 1976), and Little and Rubin, *Statistical Analysis with Missing Data*: the MCAR, MAR and MNAR mechanisms and the principle that the mechanism determines valid handling. Verified 31 October 2026.
- metricgate, "How to handle missing data: MCAR, MAR, and MNAR": definitions and examples of each mechanism, that Little's MCAR test non-significance is consistent with MCAR but cannot separate MAR from MNAR, that complete-case analysis is often acceptable when under 5% of rows are incomplete and MCAR is plausible, and that multiple imputation is the standard for MAR. Verified 31 October 2026.
- wentorai research-plugins, "missing-data-handling" and "data-cleaning-pipeline" SKILL.md: a worked MICE and Rubin's-rules pooling implementation, the recommendation of 20 or more imputations, and a reproducible cleaning order. Noted as an existing implementation that this skill improves on by adding the leakage rule, the copy-on-write fix and the sentinel-first step. Verified 31 October 2026.
On this page
- When to use this skill, and when not to
- Who it is for
- What is inside
- Example prompts
- Example output
- What the agent needs from you
- How to install and use it
- How to handle missing data step by step
- Worked example
- Situations this skill handles
- What makes this skill different
- Common mistakes
- Related skills
- Frequently asked questions
- Changelog
When to use this skill, and when not to
Use this skill when a dataset has gaps and you need to decide what to do with them before analysis or modelling. It fires on requests to fill or remove missing values, columns full of NaN, NA, NULL or blanks, a call to dropna, fillna or an imputer, or placeholder values like -999 and “N/A” standing in for real numbers. Do not use it for values that are present but extreme, which is outlier detection, for rows that repeat, which is duplicate detection, or for deciding which schema columns should allow NULL.
Who it is for
Analysts, data scientists and engineers, and anyone using an AI agent to prepare a dataset before a report, a dashboard or a model. You do not need to be a statistician, because the skill carries the decision rules, chooses the right treatment and explains why it made that choice.
What is inside
A nine-step workflow with decision rules, not vague advice. It covers surfacing disguised missing values, measuring the gaps, classifying the mechanism as MCAR, MAR or MNAR, the drop versus impute versus flag decision with thresholds, the missingness indicator, choosing an imputation method by column type and distribution, avoiding data leakage, the pandas copy-on-write trap, and a verification step. It ends with a quality bar, a safety section and an appendix for pandas, scikit-learn, SQL and R.
Example prompts
“Fill the missing values in this sales file.” “Why did my row count drop after dropna?” “Impute income without leaking into my test set.” “My column shows -999 everywhere, is that missing?”
Example output
The skill returns a treatment with the reasoning stated, not just code. A shortened version: income is 18% missing once -999 and “N/A” are converted to real nulls; the gaps track age and acquisition channel, so this is MAR; I add an income_missing flag, impute with the training-set median because income is skewed, and transform the test set with that same median so nothing leaks.
What the agent needs from you
The tool and version, what each column means, the real meaning of a blank (unknown, not applicable, or a true zero), where the data is going (a one-off report or a model), and whether rows or columns may be removed at all. The skill asks for these rather than guessing, because treating a legitimate zero as missing changes the numbers.
How to install and use it
In the Claude apps, download the SKILL.md from this page, put it in a folder named handling-missing-data, and upload that folder. In Claude Code, Codex and other Agent Skills tools, copy the folder into your skills directory. Then ask the agent to handle the missing data in your file and it loads the skill on its own.
How to handle missing data step by step
Install the skill as handling-missing-data/SKILL.md. Give the agent the dataset, say what each column means and where the data is going, and confirm whether rows may be dropped. The agent surfaces disguised sentinels, measures the gaps, classifies the mechanism, adds an indicator where the gap could matter, imputes with a method matched to the column, fits on the training split only, and verifies the fill against the distribution before and after.
Worked example
Asked to fill a customer table, the obvious move fills income with the mean, in place, on the whole dataset before splitting. That leaks across the split, shrinks the variance of a skewed column, and under pandas 3.0 the in-place call can leave the gaps in place. The skill instead converts the -999 and “N/A” sentinels, adds an income_missing flag, uses the training median because income is skewed, and reports that 18% of income was imputed so no reader mistakes it for observed data.
Situations this skill handles
| Situation | What the skill does |
|---|---|
| Blanks and NaN before a model | Flags the gap, then imputes with a method fitted on the training split only |
| -999 or “N/A” placeholders | Converts them to real nulls before anything is measured |
| Skewed numeric column | Imputes with the median, not the mean |
| Missingness that may be informative | Adds an indicator and runs the analysis with and without it |
| Statistical inference | Uses multiple imputation and pooled results, not a single fill |
What makes this skill different
Most guides stop at fillna with the mean. This skill adds the three things that go wrong in practice: it fits the imputer on the training data only so nothing leaks, it warns that pandas 3.0 copy-on-write makes inplace=True silently do nothing, and it converts disguised sentinels before measuring so the count is real. It also keeps the fact that a value was missing, because that fact is often a signal in its own right.
Common mistakes
Mean-imputing a skewed column and quoting the narrowed variance as if it were real. Fitting the imputer on the whole dataset before the train and test split. Relying on inplace=True under copy-on-write. Converting a legitimate zero to missing. Dropping rows silently and losing an informative pattern. The skill names each one with how to spot it and what to do instead.
Related skills
Pair it with outlier-detection, since an impossible value is an error to handle before imputing, and run duplicate-detection earlier in the cleaning order. For what to do once the data is clean, see the data-analysis skills: descriptive-statistics, correlation-analysis and exploratory-data-analysis.
Frequently asked questions
Should I drop rows with missing data or fill them?
Drop rows only when under about 5% are incomplete and the gaps look completely random; above that you lose power and risk bias. Otherwise flag the gap and impute, and never drop a row or column without confirming it, because the removal cannot be undone.
Is mean imputation ever wrong?
Mean imputation is fine for a quick report on a roughly symmetric column, but it shrinks the variance and weakens correlations, so it understates uncertainty for any statistical inference. Use the median for skewed data, and multiple imputation when you need valid confidence intervals.
What are MCAR, MAR and MNAR?
They are the three ways data goes missing. MCAR means the gap is unrelated to anything; MAR means it depends on other observed columns; MNAR means it depends on the missing value itself. The mechanism decides what handling is safe, and MNAR cannot be corrected from the data alone.
How do I avoid data leakage when imputing?
Compute the fill statistic on the training split only, then apply that same statistic to both the training and test sets. Fitting the imputer on the full dataset before the split leaks test information and makes the model look better than it really is.
Why does fillna with inplace=True do nothing in new pandas?
Under pandas 3.0 copy-on-write, df[col].fillna(x, inplace=True) acts on a temporary copy and leaves the original unchanged. Assign the result back instead, with df[col] = df[col].fillna(x).
My column has -999 everywhere, is that missing?
Often yes. Teams encode missing data as -999, 9999, 0, N/A or an empty string, and a standard null check cannot see them. Run a value count, look for a suspicious spike, and convert those placeholders to real nulls before you measure or impute.
Changelog
1.0.0, 31 October 2026: first release. Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.
All Data Cleaning and Preparation skills
