learn-causal-inference-with-phoebe / Practitioner session 5 of 10
Learn Causal Inference with Phoebe · Practitioner session 5 of 10

Simpson's paradox

A renewal offer beats no offer among routine renewals. It beats no offer among at-risk renewals too. Put the two groups together and it loses, badly, 35.6 percent against 69.4 percent. Every one of those four numbers is correct and any spreadsheet will reproduce them. What no spreadsheet will tell you is which of the two conclusions to act on, because that depends on why the groups have different sizes, and the answer to that is a claim about causes rather than a fact in the table.

🟠 Advanced Data scientists and analysts 45 min
0-3 · Welcome 3-22 · A sign that flips 22-40 · Build-along: reproduce it, then resolve it 40-45 · Q&A
Part 0

What is actually strange here

Nothing about the arithmetic. Sums of fractions are allowed to behave this way and there is no error to find, no rounding to blame, no bug in the query. What is strange is that two correct summaries of the same rows recommend opposite actions, and that the information needed to choose between them is not present anywhere in the rows.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. A reversal you built by hand from nine hundred rows a side, so it stops feeling like a trick. The rule that decides which number to act on: the stratifier's role in the graph, confounder against mediator against collider. And the reason the two most common pieces of advice in analytics, "always segment" and "never trust the aggregate", are each wrong about half the time.

LiveThe same numbers, two opposite conclusions3 min▶

The shape has a precise definition. An association between two variables holds in every level of a third variable, and reverses when that third variable is ignored. It also runs the other way round: no association in the aggregate, a strong one in every subgroup. Both are the same phenomenon and both are ordinary arithmetic.

What makes it worth a session is the frequency. Any time a treatment is allocated unevenly across groups that differ in baseline outcome, the ingredients are present. Uneven allocation is the normal state of affairs outside an experiment, because somebody chose who to treat and they chose for a reason.

Real world

The famous instance is the 1973 Berkeley graduate admissions figures, where the university-wide rate favoured men and almost every individual department favoured women. No department was doing what the aggregate suggested. Women applied in larger numbers to departments that admitted fewer of everybody.

Self-studyThe vocabulary for tonight3 min▶
  • Stratum - one level of the variable you split on. Two strata tonight; the effect is identical with twenty.
  • Aggregate - the comparison with the stratifier ignored, which is the same thing as pooling the strata weighted by their own row counts.
  • Reversal - the sign of the comparison differs between every stratum and the pool.
  • Unequal allocation - the treated and untreated groups have different proportions in each stratum. This is the engine of the reversal; with equal allocation it cannot happen.
  • Berkson's paradox - the collider-driven cousin, where selecting on a common effect creates an association between two unrelated things. Session 2.
  • Stratifier role - confounder, mediator, collider, or bystander. The whole resolution turns on this one word, and the word comes from the graph.
Part 1 · covers the shape and why it is not a paradox

The same numbers, two opposite conclusions

10 min live

Here is a reversal small enough to check on paper. A subscription business offers a renewal discount to 900 of its 1,800 accounts due for renewal, and afterwards counts who renewed. Two strata: routine accounts, and accounts the churn model had already flagged as at risk.

StratumGroupAccountsRenewedRenewal rate
RoutineOffer1008080.0%
RoutineNo offer80060075.0%
At riskOffer80024030.0%
At riskNo offer1002525.0%
BothOffer90032035.6%
BothNo offer90062569.4%

The offer wins by five points among routine accounts and by five points among at-risk accounts. Pooled, it loses by nearly thirty-four. The row counts are where the reversal lives: 800 of the 900 offers went to at-risk accounts, whose renewal rate is low whatever you do to them, while the no-offer group is mostly made of routine accounts, whose renewal rate is high whatever you do to them.

1,800 renewals, 900 offers, three correct summaries Routine renewals Offer: 80 of 100 = 80.0% No offer: 600 of 800 = 75.0% Offer wins gap: 5.0 points up At-risk renewals Offer: 240 of 800 = 30.0% No offer: 25 of 100 = 25.0% Offer wins gap: 5.0 points up Both strata pooled Offer: 320 of 900 = 35.6% No offer: 625 of 900 = 69.4% Offer loses gap: 33.8 points down Why it flips: 800 of the 900 offers went to at-risk accounts, and only 100 to routine ones. Every figure is correct. Which one to act on is not a question the table can answer.
🔍 Click to zoom - the offer wins twice and loses once, on the same 1,800 rows.
LiveCheck the arithmetic by hand, because it is the point3 min▶

Four divisions, each exact. Routine offers, 80 of 100, is 80.0 percent. Routine no-offers, 600 of 800, is 75.0 percent. At-risk offers, 240 of 800, is 30.0 percent. At-risk no-offers, 25 of 100, is 25.0 percent. The offer is ahead by exactly five points in both strata.

Now pool. The offer column: 80 plus 240 renewals is 320, out of 100 plus 800 accounts, which is 900. That is 35.6 percent. The no-offer column: 600 plus 25 renewals is 625, out of 800 plus 100 accounts, also 900. That is 69.4 percent. Same 1,800 accounts, same 945 renewals, and the winner has changed.

The reason is visible in the counts and nowhere else. The offer group is 89 percent at-risk accounts, whose ceiling is low. The no-offer group is 89 percent routine accounts, whose floor is high. Pooling compares a mostly-hard sample against a mostly-easy one, and the five-point advantage inside each stratum is buried by a forty-five-point difference in stratum composition.

Equal allocation kills the paradox. Had 450 offers gone to each stratum and 450 no-offers to each, the pooled rates would be the plain averages of the strata rates and the offer would win by five points overall. Reversal needs both a baseline difference and an allocation difference; remove either one and it cannot occur.

LiveIt is not a paradox, and calling it one hides the real question3 min▶

A paradox is a contradiction. There is no contradiction here: two different quantities were computed and they came out different, which is what different quantities do. The stratified comparison answers "among accounts like this one, does the offer help". The pooled comparison answers "of the accounts that got the offer and the accounts that did not, which group renewed more". Those are separate questions with separate answers and both answers are right.

The surprise comes from an unstated assumption that one of them is the effect of the offer. Neither is, automatically. A comparison becomes an effect only when the groups being compared differ in nothing but the treatment, and whether that holds is a claim about how accounts came to be treated, which lives outside the table.

  • The arithmetic is never in dispute. Do not go looking for the bug. There is no bug, and hunting for one wastes the afternoon that should have been spent on the graph.
  • The dispute is about which comparison is the causal one. That is a modelling question, and it has an answer, and the answer is not in the data.
  • Both numbers can be reported honestly. What cannot be done honestly is reporting one of them as the effect without saying why the other was rejected.
Self-studyWhere the ingredients show up in ordinary work4 min▶
  • Any targeted campaign. Targeting is deliberate unequal allocation, so the ingredients are present by design in every campaign readout that was not randomised.
  • Any triage. Treatments given preferentially to severe cases, support tickets routed by difficulty, remedial teaching given to struggling students. The treated group is worse off before anything happens.
  • Any comparison across time. If the mix of customers changed between two periods, the aggregate can move in the opposite direction to every segment within it.
  • Any weighted average that somebody unweighted. Pooling raw counts silently reweights by group size, and group size is rarely a quantity anybody chose on purpose.
  • The tell is always the same. Look at the allocation, not the outcome. If the treated and untreated groups have very different compositions, the aggregate is a comparison of compositions wearing a treatment label.
Part 2 · covers the resolution and Kestrelmark's own version

The graph decides, not the table

8 min live

Take the identical table and hang it under two different graphs. Under the first, the stratified numbers are the ones to believe. Under the second, stratifying is the mistake and the aggregate is closer to right. Nothing in the rows changed. Everything about the recommendation did.

One table, two possible graphs, two opposite verdicts Stratifier S is a confounder S X Y common cause: hold it fixed Believe the strata: 80.0 against 75.0, 30.0 against 25.0. The pooled figure compares two different populations. Stratifier S is a mediator S X Y on the causal path: leave it alone Believe the pool: 35.6 against 69.4 is nearer the total. Splitting on S removes part of the real effect. Same rows, same arithmetic. The deciding fact sits in the graph and never in the data.
🔍 Click to zoom - the stratifier's role, not the table, decides which number is the effect.
LiveThree roles, three verdicts4 min▶

Session 2 named the shapes. Applied to a stratifier, each one gives a different answer to "should I split on this".

  • Confounder, a common cause of treatment and outcome. Split. The stratified numbers are the causal ones and the pooled figure is contaminated by the back-door path. In the renewal table, if the churn risk both determined who got the offer and predicted renewal on its own, this is the case: the offer helps, by five points, in both strata.
  • Mediator, on the causal path from treatment to outcome. Do not split. Holding it fixed removes part of the effect you are trying to measure, so the stratified numbers understate and the pooled figure is closer to the total effect. Session 10 measures exactly this on Kestrelmark's opened variable, at 5.48 against a total of 6.23.
  • Collider, a common effect of treatment and outcome or of their causes. Do not split. Conditioning on it manufactures an association that was not there, so the stratified numbers are worse than the pooled ones. Session 10 prices Kestrelmark's two colliders at 0.97 for the loyalty score, which precedes the email, and 3.64 for the ticket flag, which the email helps cause: the downstream one is by far the more expensive of the pair.
  • Bystander, connected to neither. Splitting changes nothing at all. Kestrelmark's device variable moves the estimate by nothing to two decimal places, which is the cleanest possible reminder that harmful variables cannot be spotted by feel.

The uncomfortable part is that the table looks identical in all four cases. There is no column of the data, no correlation, no fit statistic that distinguishes them. The distinguishing information is the direction of the arrows, and arrows come from knowing the business.

LiveKestrelmark's own version4 min▶

The re-engagement email has the same ingredients. The winter campaign calendar sends more email, and people buy more in winter regardless of email, so season both allocates the treatment and drives the outcome. Long-tenured accounts sit on more lists and spend more anyway, so tenure does the same. Two confounders, two back doors, unequal allocation across both.

The aggregate comparison of emailed against not emailed gives 10.54. Holding season and tenure fixed gives 6.56, against a true effect of 6.23 known by construction from the model that generated the customers. The aggregate overstates by 4.31 units, which is two thirds of the true effect again.

Both roles are confounders here, so the verdict is unambiguous: 6.56 is the number to act on and 10.54 is the number to explain. Note how little the size of the gap tells you on its own. A gap of that size under a mediator would have meant the opposite, and no amount of staring at 10.54 and 6.56 would reveal which case you were in.

Real world

The board deck version reads "the campaign drove 10.54 units of incremental spend". Nobody in that room can check it, because checking it requires the graph and the graph was never written down. The analyst who asks "what did you hold fixed, and why those variables" converts a slide into a conversation.

Self-studyWhy "always segment" and "never segment" are both wrong3 min▶

Both rules of thumb are attempts to replace a causal judgement with a procedure, and neither can work, because the two cases they are trying to cover recommend opposite actions on identical tables.

  • "Always segment" fails on mediators and colliders. Segmenting by something the treatment caused answers a different question at best and manufactures bias at worst. The habit is popular because segmenting almost always produces an interesting-looking difference.
  • "Never trust the aggregate" fails on mediators too, and on any case where the pooled figure was the total effect all along. It also has no stopping rule: there is always one more variable to segment by, and segmenting far enough reaches subgroups of one.
  • The replacement rule is short. Name the role of the variable in the graph before splitting on it. If nobody can name the role, that is the finding, and the split waits.
  • Berkson's paradox is the same lesson in the collider case. Selecting on a common effect creates an association between causes that were independent. Session 2 built it; here it is one of the four verdicts.
Build-along

Reproduce it, then resolve it

★ 18 min · everyone builds

Build the small table in SQL first, because a reversal you typed yourself is a reversal you believe. Then run the same two comparisons on the Kestrelmark customers, write both numbers side by side, and classify the stratifier before saying which one is the effect.

Step 1. Type the four cells and let the database pool them. Two queries against the same rows: one grouped by stratum and offer, one grouped by offer alone. Nothing else. The reversal appears without any modelling at all.

Step 2. Check the pooled row counts against your own addition. 100 plus 800 is 900 on each side. If the two sides had come out unequal you would be looking at a different phenomenon and it would be worth stopping to find out which.

Step 3. Run the naive comparison on Kestrelmark and write down 10.54. This is the aggregate, and it is what a dashboard reports.

Step 4. Hold season and tenure fixed and write down 6.56 beside it. Two numbers, one line apart, from one dataset. Both correct as arithmetic.

Step 5. Classify the stratifier with the session 2 rules and commit to one number in writing. Season is a common cause of email and spend, so it is a confounder, so the held-fixed number is the causal one. Write the sentence, not just the figure.

Step 6. Now do it wrong on purpose. Hold opened fixed instead and watch the estimate fall to 5.48. Same procedure, same code, a stratifier with a different role, and a number that answers a question nobody asked.

★ Try it now - the reversal in SQL, checkable by hand -- renewals.sql - four cells, two queries, one reversal with renewals as ( select 'routine' as stratum, 1 as offered, 100 as accounts, 80 as renewed union all select 'routine', 0, 800, 600 union all select 'at_risk', 1, 800, 240 union all select 'at_risk', 0, 100, 25 ) -- stratified: the offer wins in both strata select stratum, offered, accounts, renewed, round(100.0 * renewed / accounts, 1) as renewal_pct from renewals order by stratum, offered desc; -- routine 1 100 80 80.0 -- routine 0 800 600 75.0 -- at_risk 1 800 240 30.0 -- at_risk 0 100 25 25.0 -- pooled: the offer loses by nearly 34 points select offered, sum(accounts) as accounts, sum(renewed) as renewed, round(100.0 * sum(renewed) / sum(accounts), 1) as renewal_pct from renewals group by offered order by offered desc; -- 1 900 320 35.6 -- 0 900 625 69.4

Keep the allocation column in the output. The reversal is invisible in the rates and obvious in the counts. An output that shows only percentages hides its own explanation, which is why a readout with rates and no denominators should always be sent back.

★ Try it now - the same two comparisons on Kestrelmark # simpson_kestrelmark.py - the aggregate, then the strata, then the verdict import numpy as np import pandas as pd df = pd.read_csv("kestrelmark.csv") # 5,000 customers from the session 10 model # the aggregate: emailed against not emailed, nothing held fixed naive = (df.loc[df.email == 1, "spend"].mean() - df.loc[df.email == 0, "spend"].mean()) # the same difference computed inside each season, for the shape of it by_season = df.groupby("season").apply( lambda g: (g.loc[g.email == 1, "spend"].mean() - g.loc[g.email == 0, "spend"].mean()) ) def held_fixed(frame, controls): """the arithmetic the session 10 bench runs: least squares, no inference""" design = np.column_stack([np.ones(len(frame)), frame.email] + [frame[c] for c in controls]) beta, *_ = np.linalg.lstsq(design, frame.spend, rcond=None) return beta[1] adjusted = held_fixed(df, ["season", "tenure"]) print(f"aggregate, nothing held fixed {naive:.2f}") # 10.54 print(f"season and tenure held fixed {adjusted:.2f}") # 6.56 print(by_season) # read your own run # truth from the generating model: 6.23 # season is a common cause of email and spend, so it is a CONFOUNDER, # so 6.56 is the causal number and 10.54 is the one to explain.
★ Try it now - the same code with a stratifier whose role nobody checked # opened is caused by the email. It is a MEDIATOR, not a confounder. mediator_split = held_fixed(df, ["season", "tenure", "opened"]) print(f"opened held fixed too {mediator_split:.2f}") # 5.48 # Nothing errored. Nothing looked wrong. The estimate now answers # "what does the email do for people who would not have opened it", # which is the direct effect and not the total the spec asked for.

★ What just happened. Three numbers came out of one dataset with one line of difference between them: 10.54, 6.56, 5.48. All three are correct arithmetic and only one answers the question on the spec. The thing that separates them is a single word about each stratifier, and the word is not in the data.

Homework

Before session 6

30 min
  1. Find a reversal in your own data. Take any treated-against-untreated comparison your team reports and split it by the variable that most influenced who got treated. Record whether the sign flips, narrows, or holds.
  2. For that stratifier, write one sentence naming its role: common cause, on the causal path, common effect, or unrelated. Bring the sentence, not the number.
  3. Take one segmented readout your team publishes regularly and check whether any segmenting variable is caused by the thing being measured. Those splits are producing a different quantity every week.
  4. Rewrite one dashboard tile so it shows denominators next to rates. Half of the reversals in a business are hiding behind a missing count.
Check yourself

Three questions

You have the renewal table and nothing else: 80.0 against 75.0 in one stratum, 30.0 against 25.0 in the other, 35.6 against 69.4 pooled. Which number is the effect of the offer?

The table is identical under both graphs, so no property of the data can separate them. The deciding information is the direction of the arrows, which comes from knowing how accounts were selected for the offer.

Kestrelmark's aggregate gives 10.54 and holding season and tenure fixed gives 6.56, against a true effect of 6.23. What made the aggregate wrong?

Two confounders, two open back doors, and unequal allocation across both. Confounding is bias rather than noise, so more customers would have produced 10.54 with tighter error bars.

A colleague proposes a standing rule: "always segment, because aggregates lie". What is wrong with it?

"Never segment" fails the same way in the confounder case. Both rules of thumb try to replace a judgement about the graph with a procedure, and the two cases they cover recommend opposite actions on the same table.

Sources covered

What tonight maps to

✓ Simpson's paradox reproduced from real numbers A reversal built by hand from 1,800 accounts, with the unequal allocation that produces it shown in the counts.
✓ Why the aggregate and the strata disagree, and which to believe The stratifier's role decides: confounder means split, mediator and collider mean do not, bystander means it makes no difference.
✓ Kestrelmark's own version 10.54 aggregate against 6.56 with season and tenure held fixed, and a true effect of 6.23 known from the generating model.
◐ Berkson's paradox and the three shapes Session 2 built confounder, collider and mediator. Tonight applies them to the choice of which number to report.
◐ The cost of each wrong stratifier, measured Session 10 prices them on one dataset: 0.97 for the collider that precedes the email and 3.64 for the one downstream of it, 0.75 for the mediator, nothing for the bystander.
· Propensity scores, IPW, doubly robust, difference-in-differences, synthetic control, DoWhy Not covered by design, in any session. Choosing which comparison is causal is the question here; fitting an estimator to it belongs to learn-experimentation-with-phoebe.

Session 5 cheat sheet

The shapeHolds in every stratum, reverses in the pool. Or the reverse of that.
No bug to find80.0 and 30.0 beat 75.0 and 25.0, and 35.6 loses to 69.4. All correct.
Allocation is the engine800 of 900 offers went to at-risk accounts. Equal allocation cannot reverse.
Stratifier is a confounderSplit. The strata are causal, the pool carries the back door.
Stratifier is a mediatorDo not split. Holding it fixed deletes part of the real effect.
Stratifier is a colliderDo not split. Conditioning manufactures an association that was not there.
Kestrelmark: 10.54 or 6.56Season and tenure are confounders, so 6.56 against a truth of 6.23.
Both slogans are wrongAlways segment and never segment. Name the role first, then split or do not.