The question every experiment is secretly asking
Lumen's CMO wants to know one thing: "If we ship the new product-page layout, will more people check out?" That sounds like a reporting question, but it is a causal one. It compares the world where we ship the change to the world where we do not - for the same shoppers, at the same time. Only one of those worlds is ever real. Session 1 gives you the framework that makes this precise (potential outcomes), names the thing you are actually estimating (the estimand), and shows in code why a plain before/after comparison quietly lies while a randomized one does not.
Two potential outcomes, one observed 6 min live
The Neyman-Rubin framework says: for each unit i there are two potential outcomes - Y_i(1), what happens if it gets the treatment, and Y_i(0), what happens if it does not. The causal effect for that unit is Y_i(1) - Y_i(0). The catch is brutal and permanent: you can only ever observe one of the two, because a shopper either sees the new layout or the old one, never both. The other is the missing counterfactual.
LiveThe fundamental problem, stated plainly3 min▶
Holland (1986) named it: you can never observe both potential outcomes for the same unit. Causal inference is therefore a missing-data problem, not a fancier regression. Ava saw the new layout and bought; we will never know whether she would have bought under the old one. That single missing cell is why "she bought after we shipped it, so it worked" is not evidence of anything.
- Y_i(1) - the outcome if unit i is treated.
- Y_i(0) - the outcome if unit i is control.
- Y_i(1) - Y_i(0) - the individual treatment effect. Unknowable for any real unit.
A PM says "conversion went up 8% the week after we launched the new page - ship it everywhere." That compares this week's shoppers to last week's - different people, different weather, a payday, a promo. It never touches the counterfactual (what these shoppers would have done under the old page). It is a before/after story, and before/after is where careers quietly go wrong.
LiveThe estimands: ATE, ATT, CATE3 min▶
Since individual effects are hidden, we target an average. Which average is the "estimand" - the precise quantity you are trying to estimate, chosen before you touch data.
| Estimand | Definition | Answers |
|---|---|---|
| ATE | E[Y(1) - Y(0)] over everyone | "If we rolled the new page to all shoppers?" |
| ATT | E[Y(1) - Y(0) | treated] | "Did it help the people who actually got it?" |
| CATE | E[Y(1) - Y(0) | X = x] | "Did it help mobile users specifically?" |
Why randomization is the one move that works 4 min live
If you let shoppers self-select into the new layout (say, only logged-in loyalty members see it), then treatment and outcome share a common cause - engagement. Engaged people convert more and are likelier to be treated. That common cause is a confounder, and it opens a "backdoor path" that makes the naive difference a blend of real effect plus selection. Randomization severs that path by making assignment independent of everything - observed and unobserved.
Self-studyIgnorability, SUTVA, positivity - the fine print3 min read▶
For any estimate to be causal, three assumptions must hold. Randomization hands you the first two for free; the third you still watch.
- Ignorability / unconfoundedness - assignment is independent of the potential outcomes (given covariates). Randomization makes this true by construction.
- SUTVA - no interference (one shopper's treatment does not affect another's outcome) and no hidden versions of the treatment. Marketplaces and social features break this; we return to it in b3.
- Positivity / overlap - every unit has a non-zero chance of either arm (0 < P(T=1) < 1). A 50/50 split trivially satisfies it; observational data (b10) often does not.
The tool you will build by hand in b2 try it
Every experiment in this course starts by deciding how many users it needs. Here is the finished planner - drag the dials and feel how baseline rate, the lift you want to detect, and your error tolerances trade off against sample size and runtime. In Builder Session 2 you rebuild this math from scratch in statsmodels.
Create a world where you know both outcomes ★ 8 min · everyone
In real life the counterfactual is missing. In a simulation, we play god: we generate Y(1) and Y(0) for every synthetic Lumen shopper, so we know the true effect and can check whether our estimators recover it.
Build a population of 20,000 shoppers with an engagement score. Engagement lifts baseline conversion - it is our confounder.
import numpy as np, pandas as pd
rng = np.random.default_rng(42)
N = 20_000
engagement = rng.beta(2, 5, N) # skewed 0..1, most shoppers low
# TRUE potential outcomes (we are omniscient here):
# baseline buy-prob rises with engagement; the new layout adds a flat +2pp lift
base = 0.02 + 0.10 * engagement # Y(0) probability
p0 = base
p1 = np.clip(base + 0.02, 0, 1) # Y(1) probability = +2pp TRUE effect
Y0 = (rng.random(N) < p0).astype(int) # outcome if OLD layout
Y1 = (rng.random(N) < p1).astype(int) # outcome if NEW layout
df = pd.DataFrame({"engagement": engagement, "Y0": Y0, "Y1": Y1})
Compute the estimand you are chasing - the true ATE - directly from the two columns you would never both have in reality:
true_ate = (df.Y1 - df.Y0).mean()
print(f"True ATE = {true_ate:.4f}") # ~0.020 by construction (the +2pp)
This true_ate is the target. Every method in the course - t-tests, CUPED, DiD, propensity scores - is a different attempt to recover this number when one of Y0 / Y1 is hidden. Keep it on screen.
Watch the naive estimate lie under self-selection ★ 8 min · everyone
Now assign treatment the way it usually happens in the wild: the most engaged shoppers opt into the new experience. Reveal only the observed outcome for each, and compare group means. The naive number will miss the true ATE - on purpose.
Confounded assignment - probability of treatment rises with engagement:
# self-selection: engaged shoppers are far likelier to get the new layout
p_treat = 0.15 + 0.70 * df.engagement
T = (rng.random(N) < p_treat).astype(int)
# reveal only the OBSERVED outcome (the fundamental problem in code)
Y_obs = np.where(T == 1, df.Y1, df.Y0)
df["T"], df["Y"] = T, Y_obs
naive = df.loc[df.T==1, "Y"].mean() - df.loc[df.T==0, "Y"].mean()
print(f"Naive diff = {naive:.4f} vs true ATE {true_ate:.4f}")
# naive is INFLATED - treated group was more engaged to begin with
Run it. The naive difference comes out well above 0.020 - often 2-4x too big. The extra is pure selection bias: you credited the layout for conversions that engaged shoppers would have made anyway.
This is the before/after PM story from Part 1, now quantified. The number is not noise - it is confidently wrong, which is worse.
This is exactly how "our email opt-in users convert 3x better, so email is our best channel" happens. Opt-in users self-selected on intent. The 3x is mostly who they already were, not what email did. Builder Session 10 de-confounds this properly with propensity scores.
Randomize, and watch the bias vanish ★ 7 min · everyone
Change one line - assign by coin flip instead of by engagement - and the same naive difference now lands on the true ATE. Nothing else changed. That is the entire argument for controlled experiments in three lines of Python.
Replace self-selection with a fair coin:
T_rand = (rng.random(N) < 0.5).astype(int) # 🎲 independent of everything
Y_rand = np.where(T_rand == 1, df.Y1, df.Y0)
est = Y_rand[T_rand==1].mean() - Y_rand[T_rand==0].mean()
print(f"Randomized estimate = {est:.4f} vs true ATE {true_ate:.4f}")
# now they match within sampling noise - bias is gone
Run it a few times (change the seed). The randomized estimate wobbles around 0.020 from sampling noise but is unbiased - no systematic gap. The confounded one was biased every single time.
Confirm balance: df.groupby(T_rand)["engagement"].mean() shows the two arms have near-identical engagement. Randomization balanced the confounder without anyone measuring it. That is the magic - and why we spend b3 protecting it.
This week ◐ 40 min total
- Run all three build-alongs and keep the notebook - b2 through b10 reuse this simulate-and-check pattern to sanity-test every estimator against a known truth.
- Sweep the confounding strength. Change the
0.70 * engagementcoefficient in demo 2 to 0.1, 0.4, 0.9 and plot naive-bias vs confounding. See the bias grow smoothly with selection. - Write the ATT and a CATE. Compute the effect among the treated, and the effect for high-engagement (engagement > 0.5) shoppers. Confirm ATE = ATT under randomization but not under self-selection.
- Optional: add a second, unobserved confounder and show that dropping the observed one from an adjustment still leaves bias - motivating why randomization beats "just control for it".
Three questions before you go 🎯 ◐ 90 seconds
1 · The "fundamental problem of causal inference" is that...
Each unit has Y(1) and Y(0), but only one is ever realized - the counterfactual is missing. That is why causal inference is a missing-data problem and why per-unit effects are unknowable.
2 · A confounder biases the naive treated-minus-control difference because it...
Engagement drives both who gets treated and who converts, so the naive gap mixes the real effect with selection. Randomization cuts the arrow into treatment and removes the backdoor.
3 · Why does a coin-flip assignment give an unbiased estimate even for confounders you never measured?
Assignment independent of the potential outcomes means the two arms are comparable on everything in expectation - the one guarantee no observational adjustment can make.
What this session covers
The potential-outcomes framework is the shared foundation of every causal course. We teach ~80% of the working core here; the deep proofs and the graphical (Pearlian) treatment live in the sources below.