learn-experimentation-with-phoebe / Builder session 10 of 10
Learn Experimentation with Phoebe · Builder session 10 of 10 · capstone

Observational causal inference, when you cannot run the experiment

The capstone. You will not always be allowed to randomize - the treatment already happened, or it is unethical, or nobody will hold out the budget. This session builds the honest toolkit for causal claims from observational data: DAGs and the backdoor criterion to decide what to adjust for, propensity scores and IPW to de-confound, and doubly robust estimators for a second line of defence. We de-confound Lumen's email-opt-in problem in Python with DoWhy, stress-test it with refutation checks, and close the whole course with a decision table: which method for which question, and why.

🔴 Hardest Builders · DA / DE / DS Python · DoWhy / EconML 45 min
0-3 · Setup 3-16 · DAGs + propensity 16-40 · De-confound in Python 40-45 · Capstone wrap
Part 0

The honest way to answer an unrandomized question

Lumen's email opt-in users convert far better than everyone else, and the growth team wants to declare email the hero channel. But opt-in was never randomized - users self-selected on intent. The confounders are obvious once you name them: prior engagement, account tenure, acquisition channel. All three drive both who opts in and who buys. Session 10 is about doing the best you honestly can when the coin flip was not available: draw the causal graph, adjust for the right variables (and never the wrong ones), estimate with propensity methods, and then try hard to break your own answer. It ends the course by mapping every method you have learned to the questions it actually answers.

Live - built in session Self-study - read after class ★ Build-along - everyone codes it The data: Lumen Skincare
★ What you walk out with today A notebook that (1) reproduces the inflated naive email "effect" - the b1 confounding lesson, now on real-shaped data, (2) estimates a propensity model and recovers a credible effect with IPW and matching via DoWhy's identify-estimate-refute flow, and (3) runs refutation and sensitivity checks and states honestly what an unmeasured confounder could still do. Plus the capstone decision table for the whole course.
Part 1 · what to adjust for

DAGs and the backdoor criterion 6 min live

Before you adjust for anything, you draw the assumed causal graph - a directed acyclic graph (DAG). Pearl's backdoor criterion tells you what to condition on: block every "backdoor" path from treatment T to outcome Y (paths that start with an arrow into T), and condition on no descendants of T. Confounders open backdoor paths, so you adjust for them. But two kinds of variable must be left alone - conditioning on them creates bias where there was none: colliders and mediators.

Backdoor path open (biased) Engagement (X) Opt-in T Buys Y T ← X → Y stays open naive effect = real effect + confounding Backdoor blocked (adjusted) Engagement (X) Opt-in T Buys Y adjust X condition on X → path closed estimate ≈ real effect (observed confounders only) ⚠ collider / mediator A collider (T → C ← Y) or a mediator (T → M → Y) must NOT be conditioned on - doing so OPENS a spurious path and creates bias. The backdoor criterion: block all backdoor paths (arrows into T), condition on no descendants of T. Adjust confounders; leave colliders and mediators alone.
🔍 Click to zoom - a confounder opens a backdoor path; adjusting for it closes the path (but never condition on a collider or mediator)
LiveWhy colliders are the trap3 min

A confounder is a common cause of T and Y - conditioning on it helps. A collider is a common effect of two variables (arrows point into it) - conditioning on it opens a path that was closed, injecting bias. A mediator sits on the causal path (T → M → Y); conditioning on it removes part of the effect you are trying to measure. The lesson: "control for more variables" is not a virtue. You control for the right variables, which only the DAG can tell you.

Real world

Classic collider: if Lumen only analyzes shoppers who reached checkout, and both "saw a promo" and "high intent" independently raise the odds of reaching checkout, then conditioning on "reached checkout" makes promo and intent look negatively related among that group - a correlation that exists only because you filtered on their common effect. Selection bias is collider bias in disguise.

Part 2 · how to adjust

Propensity scores, IPW, and doubly robust 5 min live

Once the DAG says which confounders X to adjust for, the propensity score gives you a clean way to do it. The propensity is e(X) = P(T=1 | X) - each unit's modelled probability of being treated given its covariates (Rosenbaum & Rubin 1983). Two ways to use it: matching (pair treated and control units with similar scores) and inverse-probability weighting (IPW - weight each unit by 1/e(X) so the treated and control groups become comparable in aggregate). Doubly robust estimators (AIPW, TMLE) combine a propensity model and an outcome model and stay consistent if either one is correct - two shots on goal.

Before: imbalanced on engagement (X) control (low engagement) treated (high engagement) groups not comparable - naive diff is confounded 1/e(X) After IPW: distributions overlap reweighted groups share the same X distribution now the difference estimates the effect IPW weights each unit by 1/e(X) so treated and control become balanced on the confounders. Check covariate balance (standardized mean differences) after weighting. Extreme propensity scores → huge weights → unstable estimates. Trim or clip weights, and check positivity: every unit needs 0 < e(X) < 1.
🔍 Click to zoom - inverse-probability weighting reshapes two imbalanced groups into balance on the confounder
Self-studyThe honest ceiling on any observational estimate3 min read

Adjustment - propensity, IPW, matching, doubly robust - only fixes the confounders you observed and included. The identifying assumption, unconfoundedness (no unmeasured confounders), is untestable: nothing in the data can confirm it. That is the permanent gap between observational and experimental evidence. Randomization balances even the confounders you never measured or imagined; adjustment cannot. So an observational estimate is always a conditional claim - "the effect is X, assuming we captured the confounders that matter."

Doubly robust is insurance, not a cure AIPW/TMLE give you two chances - consistent if either the propensity or the outcome model is right. That protects against modelling error. It does nothing about an unmeasured confounder. Two shots on goal, same goalposts.
Build-along 1 of 3

Watch the naive email effect inflate ★ 7 min · everyone

Straight callback to b1, now on Lumen's email data. Opt-in users self-select on intent, so the raw opt-in-minus-non-opt-in conversion gap is confounded. Quantify how wrong the naive number is before we fix it.

Load the opt-in dataset with the confounders the DAG named - prior engagement, tenure, acquisition channel:

demo1_naive_effect.py
import pandas as pd, numpy as np

df = pd.read_parquet("lumen_email_optin.parquet")
# columns: opt_in (T), converted (Y), prior_engagement, tenure_days, acq_channel
confounders = ["prior_engagement", "tenure_days", "acq_channel"]

naive = df.loc[df.opt_in==1, "converted"].mean() - df.loc[df.opt_in==0, "converted"].mean()
print(f"Naive opt-in 'effect' = {naive:.4f}")   # large - looks like email is magic

Show the groups are not comparable to begin with - the opt-in group was already more engaged:

imbalance.py
print(df.groupby("opt_in")[["prior_engagement", "tenure_days"]].mean())
# opt-in users start higher on engagement AND tenure -> confounded comparison
# this is the b1 lesson: naive diff = real effect + selection on intent

The naive gap credits email for conversions that high-intent users would have made anyway. Just like b1's self-selection demo - only now we cannot rerun it as a coin flip, so we have to adjust our way to an honest number.

Build-along 2 of 3

De-confound with propensity, IPW, and DoWhy ★ 9 min · everyone

Fit a propensity model, use it to weight or match, and recover a credible effect. We drive it through DoWhy's identify-estimate-refute flow so the causal assumptions are explicit, not buried in a regression call.

Estimate the propensity e(X) and de-confound by hand with IPW:

demo2a_ipw.py
from sklearn.linear_model import LogisticRegression
X = pd.get_dummies(df[confounders], drop_first=True)

e = LogisticRegression(max_iter=1000).fit(X, df.opt_in).predict_proba(X)[:, 1]
e = np.clip(e, 0.02, 0.98)                         # trim extremes -> stable weights

w = np.where(df.opt_in==1, 1/e, 1/(1-e))           # inverse-probability weights
ipw = (np.average(df.converted[df.opt_in==1], weights=w[df.opt_in==1])
       - np.average(df.converted[df.opt_in==0], weights=w[df.opt_in==0]))
print(f"IPW effect = {ipw:.4f}   vs naive {naive:.4f}")   # much smaller, credible

Do it properly with DoWhy - identify, estimate, and keep the model for refutation:

demo2b_dowhy.py
from dowhy import CausalModel

model = CausalModel(data=df, treatment="opt_in", outcome="converted",
                    common_causes=confounders)
est_and = model.identify_effect(proceed_when_unidentifiable=False)   # backdoor set
estimate = model.estimate_effect(
    est_and, method_name="backdoor.propensity_score_weighting")
print("DoWhy ATE:", estimate.value)
# EconML alt: LinearDML / DRLearner for a doubly robust estimate

Check covariate balance after weighting (standardized mean differences should shrink toward zero), and confirm positivity - no unit pinned at e(X)≈0 or 1. The de-confounded effect will be a fraction of the naive one; that shrinkage is the confounding you just removed.

Real world

This is the fix for "our email opt-in users convert 3x better, so email is our best channel." After de-confounding, the honest lift is a fraction of the raw gap - most of the 3x was who those users already were. That single correction can redirect a real slice of Lumen's $4M budget away from a phantom.

Build-along 3 of 3

Try to break it - refutation and sensitivity ★ 8 min · everyone

A causal estimate you have not attacked is a guess. DoWhy's refuters stress-test whether your number survives deliberate sabotage - and then you state, out loud, what an unmeasured confounder could still do.

Run the standard refuters - a good estimate barely moves under placebo, and holds under a random common cause:

demo3a_refute.py
# placebo treatment: replace opt_in with random noise -> effect should collapse to ~0
placebo = model.refute_estimate(est_and, estimate,
            method_name="placebo_treatment_refuter", placebo_type="permute")
print(placebo)   # new effect ~0 => your pipeline is not inventing effects

# random common cause: add an irrelevant covariate -> estimate should be stable
rcc = model.refute_estimate(est_and, estimate,
            method_name="random_common_cause")
print(rcc)       # estimate unchanged => robust to a spurious added confounder

Sensitivity to an unobserved confounder - how strong would one need to be to erase your effect?

demo3b_sensitivity.py
# subset (data) refuter: effect stable on random subsamples?
subset = model.refute_estimate(est_and, estimate,
            method_name="data_subset_refuter", subset_fraction=0.8)
print(subset)

# then the honest sentence: name a plausible unmeasured confounder (e.g. purchase
# intent we never logged) and state that unconfoundedness is UNTESTABLE - this
# estimate holds only if we captured the confounders that matter.

Passing the refuters is necessary, not sufficient. They catch pipeline errors and spurious signal; they cannot rule out a confounder you never measured. Report the estimate, the refutation results, and the named residual risk - that trio is what makes an observational claim defensible.

State the residual risk by name "The de-confounded lift is X; it survives placebo and random-common-cause refutation; it would be overturned only by an unmeasured confounder at least as strong as prior engagement - for example, latent purchase intent we do not log." That sentence is the difference between honest observational work and a number pretending to be an experiment.
Course capstone

The decision table - which method for which question wrap

Ten sessions, one skill: match the method to the question and the constraints. Here is the whole course as a decision table for Lumen's real questions. The rule that ties it together is at the bottom.

Lumen questionCan you randomize?MethodWhy
New product-page layout lifts checkout?Yes - per userA/B test (b1-b7)Coin-flip assignment; cleanest evidence. Power it, guard SRM, use CUPED to cut variance.
Two changes at once, do they interact?Yes - per userMultivariate / factorial (b5)Estimates main effects and interactions in one test.
Stop early without inflating error?Yes - per userSequential / mSPRT (b6)Always-valid inference lets you peek honestly.
CTV / TV brand burst drives revenue?Region onlyGeo + synthetic control (b8)Cannot randomize a person's TV; build a synthetic twin from donor regions.
Loyalty program rolled out by region?No - staggered rolloutDifference-in-differences (b9)Natural experiment; parallel trends + event study, mind staggered-adoption bias.
Does email opt-in cause conversion?No - self-selectedPropensity / IPW / DR (b10)De-confound on observed confounders; state the untestable-unconfoundedness caveat.
The whole course in one line Randomize when you can, adjust honestly when you cannot. An experiment answers the question; an observational method answers it conditionally, and the mark of a good analyst is naming that condition out loud.
After the course

Take it further ◐ 40 min total

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · The backdoor criterion says you should adjust for a variable when it...

Confounders are common causes that open backdoor paths - block them by conditioning. Conditioning on a mediator removes part of the real effect; conditioning on a collider opens a spurious path and creates bias.

2 · Inverse-probability weighting de-confounds by...

IPW reweights by the inverse propensity so the two groups share the same covariate distribution. Watch for extreme scores (huge, unstable weights) - trim them and check positivity.

3 · Why is an observational causal estimate weaker than a randomized one, even after doubly robust adjustment?

Doubly robust protects against modelling error (consistent if either model is right) but does nothing about an unmeasured confounder. Only randomization balances confounders you never observed - the permanent gap.

Source material

What this session covers

The capstone teaches the working core of observational causal inference and the discipline of stating your assumptions. The deep identification theory and the graphical formalism live in the sources below.

Confounding, backdoor/DAGs, propensity, IPW, doubly robustParts 1-2 + all three build-alongs on Lumen's email opt-in
Refutation, sensitivity, and when to trust observational workbuild-along 3 + the course decision table
Crash Course in Causality (UPenn, Roy)DAGs, propensity, IPTW in depth - the formal treatment
Brady Neal - Intro to Causal Inferencedo-calculus and the graphical framing - concept-level here
Causal Data Science with DAGs (Hunermund, Udemy)the Pearlian half - extends Part 1

Builder Session 10 cheat sheet · pin this

DAG firstDraw the assumed causal graph before adjusting. It decides what to condition on - intuition does not.
Backdoor criterionBlock all backdoor paths (arrows into T); condition on no descendants of T. Adjust confounders only.
Never a collider/mediatorConditioning on a common effect (collider) or a mediator CREATES bias. More controls ≠ better.
Propensity + IPWe(X)=P(T=1|X). Weight by 1/e(X) to balance groups; trim extreme weights; check positivity.
Doubly robustAIPW/TMLE consistent if propensity OR outcome model is right. Insurance against modelling error only.
The honest ceilingAdjustment fixes observed confounders; unconfoundedness is untestable. Randomize when you can, adjust honestly when you cannot.