Unit 02.02: Complete-case analysis is a decision
pandas skips missing values silently in almost every aggregation, so each statistic in your report can rest on a different number of rows.
Complete-case analysis is a decision, not a default
mean(), std() and sum() all drop NaN without saying so. The effective sample changes and nothing in the output mentions it — count() reports what was actually used, which is why it belongs in every summary.
The question that decides whether it matters is whether missingness relates to the outcome. If the slowest deliveries are the ones that failed to record, complete cases give you the fast ones and every estimate is biased. The data cannot settle this, because the missing values are exactly what you cannot see.
What the data *can* show is whether missingness is balanced across groups. An imbalance is evidence that it is not random.
This block counts missingness by group before dropping anything.
import numpy as np, pandas as pd
d = pd.DataFrame({
"depot": ["north"] * 5 + ["south"] * 5,
"minutes": [34, 41, np.nan, 29, 38, 44, np.nan, np.nan, 31, 36],
})
print(f"Rows: {len(d)} missing: {d.minutes.isna().sum()}")
print(d.groupby("depot", observed=True)["minutes"]
.agg(n="size", missing=lambda s: s.isna().sum()).to_string())
print("\nMissingness is not balanced: 1 in north, 2 in south.\n")
complete = d.dropna(subset=["minutes"])
print(f"Complete-case analysis keeps {len(complete)} of {len(d)} rows")
print(complete.groupby("depot", observed=True)["minutes"]
.agg(n="size", mean="mean").round(2).to_string())
print()
# The pandas trap: aggregation silently skips NaN, so each statistic can
# rest on a different number of rows and nothing says so.
print("mean() skips NaN silently:", round(d.minutes.mean(), 2),
"computed from", d.minutes.notna().sum(), "of", len(d), "rows")
print("count() reports what it used:", d.minutes.count())
print("\nComplete-case analysis is valid for the deliveries that were recorded.")
print("It describes all deliveries only if missingness is unrelated to time --")
print("which the data cannot show, and the imbalance above argues against.")
Three of ten values are missing, and they are not balanced: 1 in north and 2 in south. Complete cases keep 7 rows, giving means of 35.5 from 4 rows and 37.0 from 3. The pandas point comes next — mean() returns 36.14 computed from 7 of 10 rows and says nothing about it, while count() reports 7.
The mistake this prevents
The mistake is calling .mean() on several columns and comparing the results. Each may have skipped a different set of rows, so the summaries in one report can describe different samples.
Takeaway
Count missing values by group before excluding anything, and put count in every aggregation so the effective n is visible. State that complete-case analysis assumes missingness is unrelated to the outcome, and flag any imbalance as evidence against it.
