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.
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.
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.
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.
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.
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."
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:
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:
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.
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:
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:
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.
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.
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:
# 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?
# 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.
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 question | Can you randomize? | Method | Why |
|---|---|---|---|
| New product-page layout lifts checkout? | Yes - per user | A/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 user | Multivariate / factorial (b5) | Estimates main effects and interactions in one test. |
| Stop early without inflating error? | Yes - per user | Sequential / mSPRT (b6) | Always-valid inference lets you peek honestly. |
| CTV / TV brand burst drives revenue? | Region only | Geo + synthetic control (b8) | Cannot randomize a person's TV; build a synthetic twin from donor regions. |
| Loyalty program rolled out by region? | No - staggered rollout | Difference-in-differences (b9) | Natural experiment; parallel trends + event study, mind staggered-adoption bias. |
| Does email opt-in cause conversion? | No - self-selected | Propensity / IPW / DR (b10) | De-confound on observed confounders; state the untestable-unconfoundedness caveat. |
Take it further ◐ 40 min total
- Add a hidden confounder and hide it. Simulate an unobserved driver of both opt-in and conversion, drop it from the adjustment set, and show the de-confounded estimate is still biased - the honest ceiling, demonstrated.
- Condition on a collider on purpose. Filter to shoppers who reached checkout and watch a spurious relationship appear. This is why "add more controls" is wrong.
- Compare estimators. Put IPW, matching, and an EconML doubly-robust learner side by side on the same data; explain where they agree and diverge.
- Capstone: take one real question from your own work, place it in the decision table, and write the one-paragraph analysis plan - method, assumptions, and the residual risk you would name.
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.
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.