EDA and Feature Engineeringpandas 3.0 · scikit-learn 1.9 · seaborn 0.13 · Python 3.12+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
72 small wins to finish your pathNext lesson →

Capturing NaN with an indicator column

An indicator column is a new column holding 1 where a value was missing and 0 where it was present, added before the gap is filled, so the model keeps the fact that the value was absent as well as a number to work with.

Last updated: 12 Oct, 2026 · pandas 3.0

Random sample imputation kept the shape of the column and lost the same thing median imputation lost: which rows were filled. This technique keeps it, and it is the answer whenever the gaps are not completely at random.

One column with gaps becomes two columns. The indicator column is built first and holds 1 for every row whose value is missing and 0 for the rest. The original column is then filled with the median, so the frame carries both a usable number and the record that the number was invented.

Syntax: np.where(condition, 1, 0) builds the flag from isnull(), and the fill follows. The order matters: build the flag first, because after the fill there is nothing left to flag.

python
# the flag first: after the fill, isnull() is False everywhere
df['Age_was_missing'] = np.where(df['Age'].isnull(), 1, 0)
df['Age'] = df['Age'].fillna(df['Age'].median())    # then fill

Loading the Titanic data

The Titanic table is 891 passengers. Age is missing for 177 of them, Cabin for 687 and Embarked for 2, which makes it the right table to learn these techniques on. It loads from its GitHub URL, so there is nothing to download. Run this block first, then the example under it.

python
import pandas as pd, numpy as np
import matplotlib
matplotlib.use('svg')
import matplotlib.pyplot as plt

url = ("https://raw.githubusercontent.com/krishnaik06/"
       "Feature-Engineering-Live-sessions/master/titanic.csv")
df = pd.read_csv(url, usecols=['Survived', 'Age', 'Fare'])

Building the flag, then filling the column

Two lines in that order give a frame with no missing ages and one extra column. Row 5 is the first passenger with no recorded age: its age reads 28.0, the median, and its flag reads 1.

ExampleRow 5 carries the median and the flag
df['Age_was_missing'] = np.where(df['Age'].isnull(), 1, 0)
df['Age'] = df['Age'].fillna(df['Age'].median())
print(df.head(7).to_string())
print()
print("rows flagged:", int(df['Age_was_missing'].sum()))
print("nulls left:", int(df['Age'].isnull().sum()))

Showing that the flag carries something

The test is the one from Why values go missing: compare the target between the flagged rows and the rest. If the two differ, the column that was about to be thrown away predicts the thing you are modelling.

ExampleThe flag against the target
df['Age_was_missing'] = np.where(df['Age'].isnull(), 1, 0)
df['Age'] = df['Age'].fillna(df['Age'].median())
rates = df.groupby('Age_was_missing')['Survived'].mean()
rates.index = ['age recorded', 'age missing']
print(rates.round(4).to_string())
print("flag correlated with Survived:",
      round(df['Age_was_missing'].corr(df['Survived']), 6))
print("filled Age correlated with Survived:",
      round(df['Age'].corr(df['Survived']), 6))

fig, ax = plt.subplots(figsize=(6, 4))
rates.plot.bar(ax=ax, color=['#2e9e5b', '#d64541'], rot=0)
ax.set_ylabel('share who survived')
ax.set_title('Survival rate against a missing age')
fig.savefig('chart.svg')
A bar chart of survival rate by whether the age was recorded: passengers with a recorded age survived at 0.4062 and passengers whose age was missing at 0.2938.
  • 0.4062 against 0.2938. A passenger with no recorded age was about a third less likely to survive than one with an age.
  • The flag correlates with the target at -0.092197. The filled Age column manages -0.064910, so the fact that an age was missing predicts survival more strongly than the age itself.
  • 177 rows carry the flag. That is 19.87 percent of the table, enough for a model to learn from and small enough that the extra column costs little.
  • Both columns go into the model. The point is not to choose between them.

The pattern people get half right

The technique has two halves and the second is easy to lose: build the flag, then still fill the original column. A frame left with the flag and the gaps is no better off, because the model cannot read NaN either way. The order matters as much: a flag built after the fill is a column of zeros, because nothing is missing any more.

python
# wrong: nothing is missing by the time the flag is built
df['Age'] = df['Age'].fillna(df['Age'].median())
df['Age_was_missing'] = np.where(df['Age'].isnull(), 1, 0)   # all zeros

Indicator plus fill vs filling alone

Fill aloneIndicator plus fill
Columns afterthe same numberone more per filled column
Keeps the missingness signalnoyes
Use when the gaps arecompletely at randomrelated to anything else
Costnoneone column each, which adds up over many columns
Done by scikit-learn withSimpleImputer(strategy='median')SimpleImputer(strategy='median', add_indicator=True)

Where the indicator earns its column

  • Any column where the gap might mean something. A blank income, an unreported symptom, a cabin number nobody wrote down.
  • Columns with a large share missing. The more rows are filled, the more of the column is invention and the more the flag is worth.
  • Before a model you cannot inspect. Give it both columns and let it decide, rather than deciding for it with a fill.
Watch out. One indicator per column with gaps is one more feature each. A table with 200 columns that all have missing values becomes a table with 400, and the extra ones are mostly zeros. Add indicators where the test above shows a difference in the target, not to every column by habit.
Try it yourself
  • Build the flag after the fill and print its sum to see the column of zeros.
  • Add a second indicator for Fare and check whether it correlates with Survived at all.
  • Fill with the random sample technique instead of the median and check that the flag's correlation with the target does not move.

Little by little, you're building something great.