Causal Inference 01: Confounding & Adjustment

In Causal Inference 00, Veranda Home's dashboard — and a genuinely good Bayesian MMM — over-credited the two channels whose budgets chase demand. The disease was an open back-door: a common cause driving both spend and sales. The classic prescription is adjustment: find a variable that measures the common cause, condition on it, and the spurious correlation is blocked. This page mirrors nbs/causal/causal_01_confounding_and_adjustment.ipynb, which tests that prescription the only way that settles anything — against sealed answer keys — across three possible hidden histories of the same brand.

+43% → +25%
Search's bias, demand hidden → demand-index control added (History A → B)
+38% → +18%
Social's bias under the same one-column change
40% → 22%
mean |bias| over the demand-chasers — roughly half the door, not all of it
+15%
Search over-read under budget pacing (History C) — no control variable can fix it

Three rules to take away, each demonstrated below with measured numbers: (1) conditioning on a real confounder closes the door — most of the bias disappears; (2) the door only closes as far as the proxy is good — a noisy measurement of demand leaves residual bias, and the model won't warn you; (3) adjustment is powerless against simultaneity — when spend responds to sales, there is no third variable to condition on.

Three DAGs, one dashboard

The dashboards these worlds generate are statistically similar — positive spend–sales correlations everywhere. The graphs are what differ, and the graph is what decides whether adjustment can work:

historyhidden structurethe open pathcan a control close it?
A — demand hidden
unobserved_confounding
Latent demand drives both spend and sales; nothing in the warehouse measures it spend ← demand → sales No — the confounder is unobserved
B — demand measured
confounding_controlled
Same world, but a syndicated category demand index — a noisy proxy — lands in the warehouse spend ← demand → sales Partly — as far as the proxy is good
C — budgets paced to sales
reverse_causality
No hidden third cause; finance sets next week's budgets as a share of recent revenue sales (last week) → spend (this week) No — the "confounder" is the KPI itself

Adjustment's textbook logic, for the world where it applies: condition on the common cause \( d \) and the interventional quantity becomes estimable from observational strata,

\[ P\bigl(y \mid \operatorname{do}(s)\bigr) \;=\; \sum_{d} P(y \mid s, d)\, P(d). \]

The formula's fine print is the whole notebook: it requires \( d \) to be measured, measured well, and to actually be a common cause rather than the outcome wearing a disguise.

Fit all three histories

Same model family for each — Bayesian MMM with geometric adstock, saturating response, linear trend, NumPyro NUTS — graded against each world's sealed key. In History B the demand index enters marked as a CONFOUNDER: the framework routes it to a wide, un-shrunk coefficient prior, because shrinking a confounder's coefficient re-opens the very door you're trying to close.

from mmm_framework.config import CausalControlRole

# Declaring the control's causal role is part of the model spec:
# CONFOUNDER -> wide, un-shrunk prior (closing a back-door);
# unmarked   -> default precision-control prior (shrunk toward zero).
role = CausalControlRole.CONFOUNDER
channelA — demand hiddenB — demand measuredC — budget pacing
TV−18% (covered)−8% (covered)−15% (covered)
Search+43% (truth outside CI)+25% (truth outside CI)+15% (covered)
Social+38% (truth outside CI)+18% (covered)−10% (covered)
Display−27% (covered)−14% (covered)+0% (covered)

Relative error of estimated counterfactual contribution vs each world's sealed key (nbs/artifacts/causal_01_adjustment_scorecard.json and the notebook's grade tables). Histories A and B are the same world — the only difference is one column in the analyst's dataset.

Grading: what the control bought

Because Histories A and B share the same latent demand, the same chasing budgets, and the same true ROAS, the comparison is surgical: any improvement is attributable to adjustment and nothing else.

Measured — per-channel bias before and after the demand-index control. The chasers' bias roughly halves: Search +43% → +25%, Social +38% → +18%; Social's 90% interval now covers the truth (it didn't in History A). But Search is still over-read by a quarter — the index is a noisy proxy for true demand, so residual confounding survives, and nothing inside the model announces it. A proxy closes the door only as far as it is a good proxy.

Aside — does the CONFOUNDER marking matter?

The framework asks you to declare a control's causal role, because unmarked controls get the default precision-control prior (narrower, shrunk toward zero), and shrinking a confounder can leave the door half-open. Here is the same History B fit with the index left unmarked:

channelA — no controlB — marked CONFOUNDERB — unmarked (shrunk prior)
TV−18%−8%−8%
Search+43%+25%+25%
Social+38%+18%+18%
Display−27%−14%−15%

The honest aside: in this world the index's signal is strong enough to survive the default shrinkage, so both variants close the door about equally (mean chaser bias 21.8% marked vs 21.5% unmarked). Marking the role is cheap insurance, not magic — it exists for the worlds where the proxy is weaker and shrinkage genuinely bites.

Measured — mean |relative error| over the demand-chasing channels (Search & Social), the channels confounding actually attacks: 40% with demand hidden, 22% with the index marked as a confounder, 21% unmarked. Adjustment is real, cheap, and the first thing to reach for — and it stops halfway because the proxy is noisy, not because the method is wrong.

History C: the disease adjustment can't touch

In History C there is no hidden demand at all. Budgets are paced: finance sets next week's spend partly from recent revenue, so spend and sales are jointly determined — the arrow into spend starts at the KPI itself.

No control variable can fix this. Conditioning works by blocking a path through a third variable; here the offending path runs through last week's outcome. And conditioning on lagged sales does not rescue you either — lagged sales carries the adstocked effect of last week's media, so it is a bad control sitting on the causal path. The honest fixes live further up the ladder: model the feedback structurally, or experiment — randomization severs the arrow into spend by construction.

Measured — Veranda paces gently (a fraction of each budget tracks revenue: TV 0.6, Search 1.0, Social 0.8, Display 0.5), and the media calendar keeps its identifying pulses, so the damage is moderate. But the pattern is diagnostic: the hardest-paced channel (Search, pacing 1.0) is the most over-read at +15% — the bias follows the pacing intensities, not the media plan.

⚠️ What the diagnostics said — in all three histories

History A: r-hat max 1.02 ✓ History B: r-hat max 1.01 ✓ History C: r-hat max 1.02 ✓

Every fit converged cleanly. The unmeasured confounder, the residual proxy bias, and the simultaneity bias are all invisible from inside the model — R-hat, fit quality, and intervals look healthy in every history. Both failure modes share a signature: the model is confident and wrong.

What this rung bought — and what it can't

Bought: with a measured confounder, most of the chasers' bias disappears. Adjustment is real, cheap, and the first thing to reach for. Declare causal roles when you do it.

Can't:

The rest of the series is about earning confidence honestly — first by modeling more of the structure we believe in (03 · structural mediation and the latent-confounder notebook after it), then by buying ground truth with experiments (notebooks 05–07).

Check your understanding

Commit to an answer before opening each one.

The demand index cut Search's bias from +43% to +25%. Why didn't it go to zero — and what in the model's output warns you about the remainder?

The index is a noisy proxy for latent demand, not demand itself. Conditioning on it blocks the back-door only to the extent the proxy captures the confounder; the unexplained slice of demand keeps leaking correlation into the demand-chasing channels. And nothing warns you: the History B fit converges cleanly (r-hat 1.01), the intervals look reasonable, and Search's 90% interval still excludes the truth. The residual is measurable here only because the answer key is unsealed — in production you'd need an experiment to see it.

History C's budgets are paced to last week's revenue. Why can't you fix that by adding lagged sales as a control?

Adjustment blocks paths through third variables; in History C the path into spend starts at the outcome itself. Worse, lagged sales is a bad control: it carries the adstocked effect of last week's media, so conditioning on it blocks part of the very causal effect you're estimating (the same amputation mechanism the mediator trap demonstrates in Causal 02). The honest fixes are structural modeling of the feedback, or randomization — an experiment severs the arrow into spend by construction.

💡 Run it yourself

This page mirrors nbs/causal/causal_01_confounding_and_adjustment.ipynb on GitHub. All numbers from nbs/artifacts/causal_01_adjustment_scorecard.json, produced by nbs/causal/causal_01_confounding_and_adjustment.ipynb. Previous: Causal Inference 00 — The Ladder of Evidence. Next: Causal Inference 02 — The MMM as a Causal Model: the realistic seven-channel world, what an MMM's numbers actually claim, the bad-control trap, and the model's most underrated skill — saying "I don't know."