pandas·Lesson 9·12 min·0/4 exercises

Missing data

Dropping and filling are different claims — and they give different answers.

.isna().dropna().fillna().ffill()subset=thresh=pd.NA

Watch it happen

Play it through, or step back and forth yourself.

orders
date
city
item
cups
revenue
rating
0
2026-03-02
Delhi
masala
120
2400.0
4.5
1
2026-03-02
Mumbai
ginger
80
2000.0
3.9
2
2026-03-03
Delhi
Ginger
95
1852.5
NaN
3
2026-03-03
Pune
masala
150
3000.0
4.8
4
2026-03-04
Mumbai
masala
60
NaN
3.2
5
2026-03-04
Delhi
elaichi
110
2530.0
4.1
6
2026-03-04
Delhi
ginger
85
1700.0
4.0
7
2026-03-05
Pune
ginger
70
1400.0
NaN
orders.isna().sum().sum()3

orders.isna() gives a frame of booleans the same shape as the original — True wherever a value is missing. Three gaps here: two in rating, and one in revenue.

The idea

Our orders table has three gaps — two missing ratings and one missing revenue. That's deliberate — real data always has gaps, and how you handle them changes your results more than almost anything else you'll do.

Find them first

orders.isna()                    # a frame of booleans, same shape
orders.isna().sum()              # how many per column
orders.isna().mean()             # what FRACTION per column
orders.isna().sum().sum()        # 3 — the total
orders[orders["rating"].isna()]  # the offending rows themselves

.isna() and .isnull() are the same function with two names, as are .notna() and .notnull(). Use whichever reads better; most code uses isna.

The fraction is usually the more useful number. Two missing values out of twelve is a different problem from two out of two million.

Dropping

orders.dropna()                      # ANY gap in ANY column — 10 of 13 rows
orders.dropna(subset=["rating"])     # 11 rows — only the rating gaps
orders.dropna(how="all")             # only rows that are entirely empty
orders.dropna(thresh=5)              # keep rows with 5+ real values
orders.dropna(axis=1)                # drop the COLUMN instead of the rows

A bare dropna() is more aggressive than people expect — one gap anywhere on the row and it's gone. On a wide table with scattered gaps it can remove nearly everything. Always pass subset= unless you genuinely mean "any column".

And check what you lost: len(before) - len(after). If that number is large, the gaps are telling you something and deleting them is throwing the message away.

Filling

orders.fillna(0)                              # a constant
orders.fillna({"rating": 0, "cups": 0})       # per column
orders["rating"].fillna(orders["rating"].mean())   # the column's mean
orders["rating"].ffill()                      # carry the last value forward
orders["rating"].bfill()                      # pull the next one back

ffill and bfill only make sense when the rows are ordered — a time series, a sorted log. On unordered rows, "the previous value" is meaningless and you're inventing data from whatever order the file happened to be in.

Filling within groups is often what you actually want: orders.groupby("city")["rating"].transform("mean") gives each city's own average rather than one global number. That's Module 4.

The part that matters

The same column, three defensible treatments, three different answers:

orders["rating"].mean()               # 4.24  — skips the gaps (the default)
orders["rating"].fillna(0).mean()     # 3.58  — treats missing as zero
orders["rating"].ffill().mean()       # 4.19  — carries the previous forward

None of those is wrong. But only one matches what you meant, and the difference between 4.24 and 3.58 is the difference between a good month and a bad one. Decide deliberately, and leave a comment saying why.

Worth noting: pandas' default is to skip missing values in aggregations. That's a choice too — it's why mean() gives 4.24 rather than NaN, unlike NumPy.

NaN, None and pd.NA

Three spellings of "missing", for historical reasons. np.nan is a float and lives in float columns. None is Python's null, and pandas converts it on the way in. pd.NA is the newer one used by the nullable dtypes from lesson 3.

You rarely need to care which you have, as long as you use .isna() to test rather than == None or == np.nan — neither of which works.

See it run

The lesson's code, ready to run and to fiddle with.

Putting the kettle on…

Starting up…

Worked example

not graded

Already written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.

tryorders.dropna() and see how many rows it removes compared with subset=["rating"].

Press Run — the output appears here.

Your turn

4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

How many values are missing in each column of orders? Return the counts.

your answer

Return the rows of orders where rating is missing.

your answer

Drop only the rows whose rating is missing, leaving everything else. The starter drops on any column — restrict it.

your answer

Return the rating column with its gaps filled by the column's own mean.

your answer