ml-models / Specialized learning / Causal inference advanced

Causal inference

A model that predicts well can still be useless for deciding what to do: predicting who recovers is not the same as knowing whether the drug made them recover. This page sets out potential outcomes, confounding and causal graphs, why randomized experiments work, how propensity scores and inverse weighting rescue observational data, difference-in-differences, and uplift modeling, with every number worked by hand.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Potential outcomes
  5. Confounding and causal graphs
  6. Randomized experiments
  7. Propensity scores and inverse weighting
  8. Difference-in-differences
  9. Uplift modeling
  10. Worked example
  11. Eight units with both futures visible
  12. Adjusting the hospital data
  13. Difference-in-differences on four periods
  14. Response model against uplift model
  15. Implementation
  16. Tradeoffs
  17. Connections
  18. Read first
  19. Read next
  20. References

Problem

A hospital records 1200 patients. Of those who got a new treatment, 66.0% recovered; of those who didn't, 74.3%. The treatment looks harmful, by 8.3 points. Yet within the mild cases it raises recovery from 80% to 90%, and within the severe cases from 40% to 50%. Both groups gain ten points, so how can the total lose eight?

Because doctors gave the treatment mostly to severe cases: 75% of severe patients got it, against 25% of mild ones, and severe patients recover less whatever they receive. The treated group is sicker, and the comparison mixes the drug's effect with the difference in who got it. This is Simpson's paradox, and it is the everyday condition of observational data.

Supervised learning answers "given what I see, what will happen?" Decisions need "what would happen if I did this?" A classifier trained on this hospital's records would correctly learn that treated patients recover less often, and a policy built on it would withhold a treatment that helps everyone. Causal inference is the set of assumptions and estimators that get from data to the second question.

Intuition

Imagine each patient has two possible futures: the outcome if treated, Y(1), and the outcome if not, Y(0). The effect of the treatment on that patient is Y(1) − Y(0). We only ever see one of the two, the one matching what actually happened. Every causal question is a missing-data problem: fill in the futures that didn't happen.

Comparing treated with untreated people fills each group's missing future with the other group's observed one. That is only fair if the groups were alike before treatment. A confounder is something that affects both who gets treated and the outcome, here severity, and it makes the groups unlike. There are two ways out. Make the groups alike by assigning treatment at random (an experiment). Or measure the confounders and compare like with like: within each severity level, or by reweighting people so that the treated and untreated groups have the same mix of severity.

The confounded treatment as a causal graph, and three basic shapes Left: severity Z points to treatment T and to recovery Y, and T points to Y. The path T to Y is the causal effect; the path T, back through Z, to Y is a backdoor path that carries confounding. Adjusting for Z blocks it. Right, three small graphs: a confounder Z with arrows into X and Y, which should be adjusted for; a mediator M on the path X to M to Y, which should not be adjusted for when estimating the total effect; and a collider C with arrows from X and Y into it, which must not be adjusted for, because conditioning on it creates a spurious association. Z severity T treatment Y recovery causal effect backdoor: T ← Z → Y confounder: adjust mediator: don't, for the total effect collider: never Z X Y X M Y X Y C arrows point from cause to effect

Mechanics

Potential outcomes

For unit i with treatment Ti ∈ {0, 1}, the potential outcomes are Yi(0) and Yi(1) and we observe Yi = TiYi(1) + (1 − Ti)Yi(0). The quantities of interest are averages, because individual effects are never observed:

ATE = E[Y(1) − Y(0)], ATT = E[Y(1) − Y(0) | T = 1]

The naive comparison splits exactly into the effect on the treated plus a bias term:

E[Y | T=1] − E[Y | T=0] = ATT + (E[Y(0) | T=1] − E[Y(0) | T=0])

The bracket is selection bias: how different the treated were from the untreated before treatment. It vanishes when treatment is independent of the potential outcomes. This framework, with the assumption that one unit's treatment doesn't change another's outcome (no interference), is usually credited to Neyman and Rubin.

Confounding and causal graphs

A causal graph (a DAG) draws an arrow from each variable to the variables it directly affects. Pearl's backdoor criterion says which variables to adjust for: a set Z is enough if it blocks every path from T to Y that starts with an arrow into T, and contains nothing caused by T. Then, within levels of Z, treatment is as good as random, and

E[Y | do(T = t)] = Σz P(Z = z) · E[Y | T = t, Z = z]

The do means "set T to t by intervention", as opposed to "observe T = t". The graph also says what not to adjust for. Conditioning on a mediator removes part of the effect you want to measure; conditioning on a collider (a common effect of T and Y, like "was admitted to hospital") opens a path that wasn't there and manufactures an association. More variables is not automatically safer.

The potential-outcomes version of the same assumption is unconfoundedness: (Y(0), Y(1)) is independent of T given Z. It comes with overlap (positivity): every kind of unit must have some chance of each treatment, 0 < P(T = 1 | Z = z) < 1. No data can test unconfoundedness; it is a claim that every confounder was measured, defended with domain knowledge.

Randomized experiments

Random assignment makes T independent of everything about the unit, measured or not, so selection bias is zero in expectation and the difference in means estimates the ATE without any model. Any one randomization can still be unlucky; the estimate has variance, which is why experiments report confidence intervals and why larger samples help. An A/B test is the same design with a product change as the treatment. Stratifying the randomization on known strong predictors (block randomization) reduces the unlucky draws.

Propensity scores and inverse weighting

When there are many confounders, comparing within every combination of their values runs out of data. Rosenbaum and Rubin (1983) showed that the propensity score e(z) = P(T = 1 | Z = z) is enough: if treatment is unconfounded given Z, it is unconfounded given e(Z), a single number. In practice e is estimated with a classifier, very often logistic regression.

Inverse propensity weighting (IPW) weights each treated unit by 1/e and each untreated unit by 1/(1 − e). A treated patient from a group where treatment was rare stands in for the many similar patients who weren't treated. The weighted sample is a pseudo-population in which treatment no longer depends on Z:

ATEIPW = (1/N) Σi [ TiYi / e(Zi) − (1 − Ti)Yi / (1 − e(Zi)) ]

This is the Horvitz–Thompson form. The Hájek form divides each weighted sum by the sum of its weights instead of N, which is more stable when weights are large. Propensities near 0 or 1 give huge weights and a noisy estimate; that is the overlap assumption failing in practice, and common responses are trimming units with extreme scores or clipping the weights. A propensity model is judged by whether it balances the covariates after weighting, not by its accuracy: a propensity model that predicts treatment perfectly is a sign of no overlap, not a good model.

TermShape/ParamsMeaningSource
Y(0), Y(1)two numbers per unit, one observedPotential outcomes without and with treatmentRubin 1974; Pearl 2009
ATE / ATTscalarAverage effect over everyone / over the treatedPearl 2009
e(z)P(T=1 | Z=z) ∈ (0, 1)Propensity score; fitted by a classifierRosenbaum and Rubin 1983
LogisticRegressionC=1.0, l1_ratio=0.0, solver="lbfgs"A common propensity model; L2-penalized by default_logistic.py
Cfloat, np.inf = no penaltyInverse regularization strength; the penalty shrinks propensities toward the base rate_logistic.py
fit(X, y, sample_weight)sample_weight of shape (n_samples,)Fits the propensity model; also how IPW weights enter a weighted outcome model_logistic.py
predict_proba(X)[:, 1](n_samples,)The estimated propensity of each unit_logistic.py
1/e, 1/(1−e)one weight per unitIPW weights; huge when e is near 0 or 1Horvitz and Thompson 1952
DiD(post − pre)treated − (post − pre)controlEffect under parallel trendsCard and Krueger 1994
uplift(x)P(Y | T=1, x) − P(Y | T=0, x)Conditional effect, used to choose whom to treatRadcliffe and Surry 2011

Difference-in-differences

Sometimes treatment hits a whole group at once: a policy in one city, a feature launched in one country. Comparing the group before and after mixes the effect with whatever else changed over time; comparing it with another group after mixes the effect with the groups' standing difference. Difference-in-differences subtracts both:

DiD = (Ȳtreated, post − Ȳtreated, pre) − (Ȳcontrol, post − Ȳcontrol, pre)

The assumption is parallel trends: without treatment, the treated group would have changed by the same amount as the control group. It can't be checked after treatment, but several pre-treatment periods that move in parallel make it believable, and diverging pre-trends are a warning. Card and Krueger's study of a minimum-wage rise in New Jersey, with neighbouring Pennsylvania as the control, is the classic example.

Uplift modeling

Marketing and retention teams ask a per-person question: whose behaviour will the action change? A response model predicts who buys if contacted, which picks people who would buy anyway ("sure things") and even people the contact puts off ("sleeping dogs"). An uplift model predicts the conditional effect P(Y | T=1, x) − P(Y | T=0, x) and targets the "persuadables". It is trained on randomized data: the simplest version (a T-learner) fits one model on the treated group and one on the control group and subtracts their predictions. It is evaluated with uplift or Qini curves: sort people by predicted uplift and plot the incremental outcome as more of them are treated.

Worked example

Eight units with both futures visible

In a simulation we can write down both potential outcomes. Z = 1 marks a severe case; the treatment raises everyone's score, by 1 to 3 points.

UnitZY(0)Y(1)EffectTObserved Y
108(9)108
207(9)207
309(10)109
40(8)102110
51(3)6316
61(2)5315
71(4)6216
813(5)203

Bracketed values are the ones nobody would ever see. The true ATE is 2.0. Three of the four treated units are severe cases, and the naive difference in observed means is 0.0: the treatment looks useless. The decomposition shows why: the effect on the treated is 2.5, and selection bias, E[Y(0) | T=1] − E[Y(0) | T=0], is -2.5, cancelling it exactly.

Now assign treatment at random instead: choose 4 of the 8 units to treat, every choice equally likely. There are 70 possible choices. Their difference-in-means estimates range from -2.5 to 6.5 (standard deviation 1.75), but their average is exactly 2.0, the true ATE. That is what "unbiased" means: no single experiment is guaranteed right, but the design has no systematic error.

Adjusting the hospital data

SeverityPatientsTreatedRecovered if treatedRecovered if notEffecte(z)
mild800200180/200 = 90%480/600 = 80%+10.0 points0.25
severe400300150/300 = 50%40/100 = 40%+10.0 points0.75
All120050066.0%74.3%-8.3 points

Adjustment. Average the within-stratum effects, weighted by how common each stratum is: 0.667 × 0.10 + 0.333 × 0.10 = 0.10, a gain of ten points, against the naive loss of 8.3.

Inverse weighting. Mild patients were treated with probability 0.25, so each treated mild patient gets weight 1/0.25 = 4.00 and each untreated one 1/0.75 = 1.33; severe patients the other way round. The 200 treated mild patients now count as 800, and the 600 untreated as 800: in the pseudo-population, both arms have the full 800 mild and 400 severe patients, so severity no longer predicts treatment. The Horvitz–Thompson estimate is

(180·4 + 150·1.333 − 480·1.333 − 40·4) / 1200 = 0.10

the same 0.10 as adjustment, and so is the Hájek estimate. With a single discrete confounder, IPW with stratum frequencies as propensities and stratified adjustment are algebraically the same estimator; they differ when the propensity comes from a model over many covariates.

Fitting the propensity. A logistic regression of T on Z with no penalty (Newton's method on all 1200 rows) converges to intercept -1.0986 = log(1/3) and slope 2.1972, which give back exactly 0.25 and 0.75, and the same estimate, 0.10. With scikit-learn's default penalty, C = 1, the slope shrinks to 2.1544, the propensities move toward each other (0.2527 and 0.7446), and the estimate drops to 0.0957. Small here, but a penalty tuned for prediction is a bias in a propensity model; fit it with C=np.inf or check balance after weighting.

The hospital data before and after inverse propensity weighting Bars show how many patients each arm holds per severity level. Before weighting, the treated arm is 200 mild and 300 severe, and the untreated arm 600 mild and 100 severe, so the treated arm is mostly severe. After weighting, both arms are 800 mild and 400 severe, the same mix as the whole population. observed treated 200 300 untreated 600 100 weighted by 1/e and 1/(1−e) treated 800 400 untreated 800 400 mild severe

Difference-in-differences on four periods

A city launches a programme after period 2; a similar city does not. Weekly averages of the outcome:

Period123 (post)4 (post)Pre meanPost meanChange
Treated city485062644963+14
Control city384046483947+8

Before-after in the treated city says +14; comparing the cities after says +16. Both are wrong. The control city rose by 8 without the programme, so DiD = 14 − 8 = 6. Before the launch both cities rose by the same 2 per period (pre-trend gap 0), which is the evidence for parallel trends. The counterfactual treated series, what the city would have shown without the programme, is 48, 50, 56, 58.

Difference-in-differences: observed and counterfactual trends Outcome against period for both cities. The control city goes 38, 40, 46, 48. The treated city goes 48, 50, 62, 64, running parallel to the control before the launch line and jumping after it. A dashed line continues the treated city's path with the control city's changes, 48, 50, 56, 58; the vertical gap between the observed and the dashed line after the launch is the estimated effect, 6. programme starts period 1 period 2 period 3 period 4 treated control counterfactual effect 6

Response model against uplift model

A randomized campaign test gives purchase rates with and without a discount in four customer segments of 1000 each. The budget covers 2 segments.

SegmentBuys if contactedBuys anywayUpliftExtra purchases
A30%10%+20.0 points+200
B60%58%+2.0 points+20
C5%4%+1.0 points+10
D35%45%-10.0 points-100

A response model ranks by "buys if contacted" and picks B and D: segment B are sure things who mostly buy anyway, and segment D are sleeping dogs whom the contact puts off. Net effect: -80 purchases. Ranking by uplift picks A and B for +220, more than contacting all four segments (+130).

Implementation

Inverse propensity weighting with scikit-learn, with the checks that matter: an unpenalized propensity model, overlap, and covariate balance after weighting. X holds the measured confounders, t the 0/1 treatment and y the outcome.

import numpy as np
from sklearn.linear_model import LogisticRegression

def ipw_ate(X, t, y, clip=0.01):
    model = LogisticRegression(C=np.inf, max_iter=1000).fit(X, t)
    e = model.predict_proba(X)[:, 1]
    print("propensity range", e[t == 1].min(), e[t == 0].max())   # overlap check
    e = np.clip(e, clip, 1 - clip)
    w = np.where(t == 1, 1 / e, 1 / (1 - e))

    treated = np.sum(w * t * y) / np.sum(w * t)                   # Hajek form
    control = np.sum(w * (1 - t) * y) / np.sum(w * (1 - t))

    def smd(weights):                                             # standardized mean difference
        m1 = np.average(X[t == 1], axis=0, weights=weights[t == 1])
        m0 = np.average(X[t == 0], axis=0, weights=weights[t == 0])
        return (m1 - m0) / np.sqrt((X[t == 1].var(axis=0) + X[t == 0].var(axis=0)) / 2)

    return treated - control, smd(np.ones_like(w)), smd(w)        # balance before / after

A common rule of thumb treats an absolute standardized mean difference under about 0.1 after weighting as balanced. Clipping trades a little bias for much lower variance. For better estimates, combine an outcome model with the propensity model (doubly robust estimation), which stays consistent if either model is right. A T-learner for uplift is two classifiers: fit one on X[t == 1], one on X[t == 0], and subtract their predict_proba outputs.

Tradeoffs

  • Experiment vs observational study. Randomization removes confounding, measured or not, but costs time and money and is sometimes unethical or impossible. Observational methods use existing data but rest on an untestable claim that every confounder was measured.
  • Adjustment vs weighting. Stratifying is transparent but runs out of data as covariates multiply. Propensity methods compress the covariates into one number but depend on that model, and extreme weights make them noisy.
  • What to adjust for. Adjusting for confounders removes bias; adjusting for mediators or colliders adds it. Throwing every available column into the model is not safe.
  • DiD. Handles unmeasured confounders that are constant over time, but only under parallel trends, and only with comparable control groups.
  • Uplift vs response. Uplift models answer the right question but need randomized data, and the difference of two noisy predictions is noisier than either.
Treated patients recover less often overall, yet better in every severity group. Which number should drive the decision?

The within-group comparison, here, because severity is a confounder: it causes both treatment and recovery. The answer depends on the graph, not the numbers; if the grouping variable were caused by the treatment (a mediator), the overall comparison would be the right one.

Why can't you check unconfoundedness from the data?

It is a statement about potential outcomes, half of which are never observed. An unmeasured confounder leaves no trace in the measured columns. Sensitivity analysis asks how strong such a confounder would have to be to change the conclusion.

A propensity model reaches 0.99 AUC at predicting treatment. Is that good?

It is a warning. It means treated and untreated units barely overlap, so weights explode and the effect is estimated from a handful of units, or from extrapolation.

In the DiD example, what would a +4 per period pre-trend in the treated city (against +2 in the control) do?

It would break parallel trends. DiD would credit the programme with the treated city's faster underlying growth, overstating the effect.

Connections

Read first

Read next

No page builds on this one yet.

References

  • Pearl. Causal Inference in Statistics: An Overview. Statistics Surveys 3, 96–146, 2009. doi:10.1214/09-SS057
  • Rubin. Estimating Causal Effects of Treatments in Randomized and Nonrandomized Studies. Journal of Educational Psychology, 1974. (Potential outcomes.)
  • Rosenbaum and Rubin. The Central Role of the Propensity Score in Observational Studies for Causal Effects. Biometrika, 1983.
  • Horvitz and Thompson. A Generalization of Sampling Without Replacement from a Finite Universe. Journal of the American Statistical Association, 1952. (The inverse-probability estimator.)
  • Card and Krueger. Minimum Wages and Employment: A Case Study of the Fast-Food Industry in New Jersey and Pennsylvania. American Economic Review, 1994.
  • Radcliffe and Surry. Real-World Uplift Modelling with Significance-Based Uplift Trees. Stochastic Solutions white paper, 2011. PDF
  • scikit-learn 1.9.1: LogisticRegression, fit, predict_proba.