Simpson's Paradox

โฑ๏ธ 40 sec read ๐Ÿ“ˆ Data Analysis

Simpson's Paradox is when a trend that appears in every subgroup reverses or disappears once the groups are combined. It happens because a lurking variable is distributed unevenly across groups, so the aggregate mixes apples and oranges.

How Can a Trend Reverse When You Aggregate Data?

Aggregation weights each subgroup by its size, so if one group is concentrated in a "hard" segment and the other in an "easy" one, the overall rates reflect the mix, not the underlying performance.

Treatment A beats B in small stones AND large stones,
yet B wins overall โ€” because A got mostly hard cases:

              A               B
Small     81/87  (93%)    234/270 (87%)
Large    192/263 (73%)     55/80  (69%)
Overall  273/350 (78%)    289/350 (83%)  โ† reversal

What Is the Berkeley Admissions Example?

In 1973, UC Berkeley's aggregate graduate admissions looked biased against women (44% of men admitted vs. 35% of women), yet within most departments women were admitted at equal or higher rates.

Dept   Men: applied/admitted   Women: applied/admitted
A          825 โ†’ 62%                108 โ†’ 82%
B          560 โ†’ 63%                 25 โ†’ 68%
C          325 โ†’ 37%                593 โ†’ 34%
E          191 โ†’ 28%                393 โ†’ 24%
F          373 โ†’  6%                341 โ†’  7%

Aggregate: Men 44%  vs  Women 35%

Women applied disproportionately to competitive departments (C, E, F) with single-digit-to-30% admit rates, while men flooded departments A and B where most applicants got in. The department was the lurking variable; the aggregate reversed the department-level picture.

How Do You Guard Against Simpson's Paradox?

Segment by every plausible confounder before drawing a conclusion from an aggregate rate, and check whether the group mix differs between the populations you're comparing.

import pandas as pd

# Always look at both levels before concluding:
df.groupby("variant")["converted"].mean()             # aggregate
df.groupby(["segment", "variant"])["converted"].mean() # per segment

# Red flag: the segment mix differs by variant
pd.crosstab(df["segment"], df["variant"], normalize="columns")

If the per-segment story and the aggregate story disagree, report the segmented numbers and explain the mix shift. Which level is "true" depends on the causal question โ€” this is a close cousin of the correlation vs. causation problem.

Common Pitfalls

Pro Tip: Any time two groups you're comparing could differ in composition, run the comparison twice โ€” aggregated and segmented by the suspected confounder. If the direction flips, you've caught Simpson's Paradox before it shipped in a report.

โ† Back to Data Analysis Tips