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 →

Missing as its own category

Treating missing as its own category means replacing the gaps in a text column with a label such as Missing, so the absence becomes a value the model can learn from instead of a hole that has to be patched.

Last updated: 12 Oct, 2026 · pandas 3.0

Frequent category imputation filled a column that was 47.26 percent missing with its mode and the median price of the Gd grade fell from 206,950 to 149,525. This lesson does the same column properly, and the change is one line.

Syntax: Series.fillna('Missing'). The label is arbitrary and any string that cannot collide with a real category works. The column stays text, so every encoder that follows treats the new label as one more level.

python
df['FireplaceQu'] = df['FireplaceQu'].fillna('Missing')   # one more level
df['GarageType'] = df['GarageType'].fillna('Missing')     # the same everywhere

Loading the house price data

These examples use the house price table and three of its columns: two text columns with missing values and the sale price to read the result against. Run this block first, then the example under it.

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

url = ("https://raw.githubusercontent.com/krishnaik06/"
       "Advanced-House-Price-Prediction-/master/train.csv")
df = pd.read_csv(url, usecols=['FireplaceQu', 'GarageType', 'SalePrice'])

Turning 690 gaps into a level

The fill is one call, and the result is a column with six levels instead of five. The new level is the largest one, which is honest: no fireplace is the most common thing to know about a house in this table.

ExampleSix levels, none of them invented
df['FireplaceQu'] = df['FireplaceQu'].fillna('Missing')
print(df['FireplaceQu'].value_counts().to_string())
print()
print("levels now:", df['FireplaceQu'].nunique())
print("nulls left:", int(df['FireplaceQu'].isnull().sum()))
print("dtype:", df['FireplaceQu'].dtype)

Reading the new level against the target

The test of whether the label deserves to exist is whether it behaves differently from the real categories. Grouping the sale price by the six levels answers it.

ExampleMedian sale price per level, Missing among them
df['FireplaceQu'] = df['FireplaceQu'].fillna('Missing')
summary = (df.groupby('FireplaceQu')['SalePrice']
             .agg(['size', 'median']).sort_values('median'))
print(summary.round(0).to_string())

fig, ax = plt.subplots(figsize=(7, 4))
summary['median'].plot.bar(ax=ax, rot=0,
                           color=['#d64541' if i == 'Missing' else '#3a6fd8'
                                  for i in summary.index])
ax.set_ylabel('median SalePrice')
ax.set_title('Missing is a level of its own, and nearly the cheapest')
fig.savefig('chart.svg')
A bar chart of median sale price for each fireplace grade, sorted from cheapest: Po at 131,500, then the red Missing bar at 135,000, Fa at 158,000, TA at 187,500, Gd at 206,950 and Ex at 314,250.
  • Missing is 690 houses with a median price of 135,000. That is nearly 72,000 below the Gd grade and within 3,500 of Po, the cheapest real grade.
  • The five real grades keep their prices. Nothing was poured into them, so Gd is still 206,950 and the ordering of the grades still makes sense.
  • The absence turns out to mean something concrete. A missing fireplace grade in this table means the house has no fireplace, and houses without one are cheaper.
  • The column is still text. It will be encoded like any other categorical column, with one extra level.

When a label beats a fill

Fill with the modeA category of its own
Share missing it suitsa few percentany, and especially a large one
What it assumesthe gaps are ordinary rowsthe gap means something
Effect on the real categoriesthe modal one is dilutednone
Columns afterthe samethe same, with one more level
Cost at encoding timenoneone more dummy column per encoded column
Reads asa repaired columna column that records what was unknown

Where this is the first thing to reach for

  • A column whose gap has a meaning in the domain. No fireplace, no garage, no second phone number: absence is a fact, not a loss.
  • A column with a large share missing. No fill value is honest over half a column, and a label costs nothing.
  • Before any categorical encoder. One hot encoding and the target based encoders need a complete column, and this is the cheapest way to give them one, as One hot encoding assumes.
Watch out. The label has to be one that cannot already be in the column. fillna('NA') on a column that already uses NA for North America merges two different meanings into one level and nothing complains. Check with 'Missing' in df[col].unique() before choosing it.
Try it yourself
  • Apply the same fill to GarageType and read the median price of its Missing level.
  • Change the label to 'unknown' and check that nothing downstream depends on the spelling.
  • Count the dummy columns pd.get_dummies(df['FireplaceQu'], dtype=int) produces with and without the Missing level.

Every expert started right here.