Causal inference, explained

Double Machine Learning.

Two prediction models and two residuals can pin down an effect very precisely. Which effect you get depends on a causal graph the data can't show you. This page explains what those methods buy, what they assume, and why experiments and independent lines of evidence have to carry the rest.

Graph 1 of 6

Demand drives both

Effect of one more dollar
0.49

Each of these six graphs links every pair of variables. So none of them predicts a pattern the data could rule out. Statisticians call a group like this a Markov equivalence class. Any data on demand, spend and revenue fits all six equally well, so picking one is an assumption you make. The plot on the right shows the same customers the way the selected graph says to estimate the effect. For graph 1, that means removing demand from both spend and revenue first. For the other graphs, it means using spend and revenue as recorded.

6causal graphs
1observed dataset
0testable differences
3different answers
Part I · Identification

Before any model: what makes it causal.

Every estimator on this page answers a question about the data. Assumptions decide whether that answer is the causal effect. The data can't check those assumptions.

1 · Both outcomes2 · What you see3 · Identification

left half: with adsright half: without adsconverteddid notnever observedsaw the campaign
Dana's two questions

Two questions. One move.

Dana runs measurement for an online retailer. This year Finance wants two answers from her. She can only get either one by making assumptions she has no way to test.

Identification is the first arrow. Everything after Part I is about the second. In this page's simulations we built the world ourselves and left nothing out of X, so the first arrow holds. With real data, it holds only if the assumptions do.

Part II · weekly spend

What does one more dollar of paid social return?

Y=θD+g(X)+ε,  E[ε | D, X] = 0

Assumes two things. Once you account for demand X, what's left of each week's budget has nothing to do with revenue. And the effect is the same every week.

θ=E[(D − E[D | X])(Y − E[Y | X])]E[(D − E[D | X])²]

Gives the effect as a ratio of averages Dana can measure. Nothing in it depends on an outcome she can't see.

Part III · ad exposure

What did the win-back campaign do for customers who saw it?

Y(0) ⫫ D | X,  e(X) < 1,  Y = Y(D)

Assumes three things. The platform targeted only on data Dana also has. Every kind of exposed customer has similar customers who weren't exposed. And seeing the ads means the same thing for everyone, with no spillover between customers.

ATT=E[Y | D = 1]−E[μ0(X) | D = 1]

Gives the missing piece, E[Y(0) | D = 1], as the conversion rate of unexposed customers, reweighted to match the exposed group.

What the data can and cannot check.

Untestable

No unmeasured confounding

Spend: once you account for demand, the rest of each week's budget is as good as random. Ads: the platform used nothing to target that Dana can't see. You can run a placebo check, such as conversions in the weeks before launch, which should show no lift. That check carries an assumption too. It only works if the hidden signal would also have moved those earlier conversions.

Design

One version of exposure, no spillover

The exposed group mixes people who saw one ad with people who saw thirty. People in the same household also share screens. Both make it less clear what the effect measures and who it applies to.

Partly checkable

Overlap

The propensity is the chance a customer was exposed. You can spot estimated propensities near 0 or 1. You only see them for the features you have, though, and only through a model. A customer type the platform never skips looks the same in the data as bad luck.

Modeling choice

What the target means

The spend model assumes one θ for every week. Double ML lets the shapes of g and m bend freely. The effect itself stays a single number. If the effect really varies from week to week, θ becomes a weighted average, and you should know which weeks count most.

Both results come down to one instruction. Compare customers, or weeks, that look alike on X. Neither result says how to do that. When X has 140 columns and curved relationships, simple models get the comparison wrong, and Parts II and III cover how. Before that, look at how much the assumptions are carrying. Nothing in the data can confirm them.

What assumptions buy Partial identification

Identification is bought with assumptions.

Start with what this campaign's data say about the effect when you assume nothing. Then add one assumption at a time and watch the range shrink.

Data only

No assumptions

The exposed group's rate without ads could be anything from 0% to 100%, so the effect could be anything from −66 to +34 points.

Untestable

+ Ads never reduce conversion

Called monotone response, this rules out ads that hurt sales. That's reasonable for most ads. It's wrong for the ones people find annoying.

Untestable

+ The platform picked people at least as likely to convert

Called monotone selection, this says hidden targeting leans the same way as the targeting Dana can see. If so, her estimate becomes an upper limit.

Untestable

+ Nothing hidden

Nothing outside X drives both who saw the ads and who bought. Every estimator in Parts II and III needs this to give a single number.

Effect on conversion among exposed customers, in percentage points. The rows follow Manski (1990) and Manski & Pepper (2000) and use only this campaign's data. Each range is estimated from the sample. The last row also shows its 95% interval.

Read the ladder from the top. On their own, the data can't even tell Dana whether the campaign helped. The first assumption rules out harm. The second caps the effect at her estimate. Only the last gives a single number. Each step down is a claim about customers she can't observe.

The temptation Throw in every column

More controls. Worse answer.

The CRM has dozens of columns, and someone always suggests giving all of them to a flexible learner. The learner can't tell good columns from bad. Whether a column belongs in X depends on where it sits in the causal graph. How well it predicts tells you nothing about that.

The opening animation showed two of these cases. Graph 2 treated demand as a mediator, and graph 5 treated it as a collider.

What the CRM holdstap a column to add it to both models
Doubly robust estimate · 120 campaigns·
Mean estimate
…
Truth
…

Confounder

Purchase intent

Intent drives both exposure and conversion, which makes it a confounder. It belongs in the model. Leave it out, and part of the platform's targeting gets counted as lift.

Leave it out: …
Confounder

Weeks since last order

The platform targets both recent and lapsed buyers, so recency shapes who sees the ads. Leave it out, and the platform's choices get counted as lift.

Leave it out: …
Mediator

Site sessions during the flight

Ads drive visits, and visits drive orders. That makes visits a mediator. Adjust for them, and you remove the part of the effect that runs through visits. Here that's most of it.

Add it: …
Collider

Loyalty activity after the flight

Both exposure and conversion drive loyalty activity, which makes it a collider. Adjusting for it links the two through a path that isn't causal. The ads end up looking harmful.

Add it: …
Instrument

Ad inventory in the customer's region

Inventory changes who gets exposed and nothing else, which makes it an instrument. It adds no bias here. It does push propensities toward 0 and 1, so the estimates spread out. If a hidden confounder exists, it also makes that bias bigger.

Add it: …
Noise

Five unrelated fields

These fields connect to nothing, and they do little harm in this model. A flexible learner would pay for them with some extra noise and more room to overfit.

Add them: …

Sessions and loyalty activity predict conversion better than any other column in the file. A feature-importance plot would rank them first. Any variable selection that scores prediction would keep them. Yet the ads cause both, so including them wrecks the estimate. You only know to drop them if you know the order of events and the causal graph.

Suppose you have the right columns and the other assumptions hold. You still have to estimate the effect from a finite, messy dataset. That sounds like the easy part. The next two parts show how simple models get it wrong, and why.

Part II · Estimating a continuous effect

Grant the assumptions. Now estimate.

Part I turned the spend question into a formula the data can answer. Now we have to compute it. When demand X is curved and has many columns, the obvious methods fail, and each fails for a reason you can name.

Raw data · fitted slope
1.42
Spend and revenue, as recorded.
θ = 0.50the true effect
500weeks per dataset
300datasets simulated in your browser
5folds
The question

One number. A lot riding on it.

Start with the first question. Every week the media team sets a paid social budget, and every week revenue moves. Finance is about to lock next year's plan. They want one figure from Dana: how much revenue does one more dollar of spend bring in?

If she overstates it, the company spends money on a channel that won't pay it back. If she understates it, they cut something that works. Nobody gets to rerun the year.

The problem is how the budget gets set. The team spends more in weeks they expect to be busy, like holidays, paydays and warm weekends. Those weeks bring in more revenue anyway. We call these conditions demand, written X.

Y=θD+g(X)+ε
D=m(X)+v
This is called the partially linear model. θ is the effect of spend. g and m are nuisance functions. We need to learn them well, but we don't care about them for their own sake. Part I covered when θ is causal. From here on we assume it is and ask how to estimate it.
The truth, set by usθ = 0.50

Each extra dollar of spend brings in fifty cents of revenue. We simulate the data, so we can grade every method against this number.

How demand drives spendm(X) = 1.4 sin X + 0.3X

Plus noise v ~ N(0, 0.8²). This is the part of each week's budget that demand doesn't explain.

How demand drives revenueg(X) = 2.5 sin X + 0.6 cos 2X

It's curved, and it peaks where spend peaks. That overlap causes all the trouble below.

Attempt 1 Regress revenue on spend

1.41. Nearly three times the truth.

500 weeks · one datasetrevenue vs spend
OLS fit, slope 1.42True effect, slope 0.50colored by demand

Dana starts where most people start. She fits a straight line of revenue against spend. The slope comes back steep, with a standard error of 0.04. It looks precise, and it's wrong.

Color each week by demand and you can see why. Slow weeks sit at the bottom left and busy weeks at the top right. Most of the slope comes from demand, and the line credits it to spend.

plim θOLS=θ+Cov(m(X), g(X))Var(D)
The second term is confounding: demand moving spend and revenue together. Here it adds about 0.91 to the answer.
Attempt 2 Let machine learning control for demand

Smarter model. Still wrong.

The fix seems clear: control for demand X. But its effect is curved, and in the real data it has 140 columns.

A straight-line control would miss the shape of g. So Dana uses a flexible machine learning model, a penalized regression on many demand features, tuned to predict well. She fits one model of revenue on spend plus demand. Then she reads θ off the spend coefficient.

The answer drops to 0.64. That's better. To check, she runs the same steps on 300 new datasets. The estimates cluster around 0.64, not 0.50. Her 95% intervals contain the truth only 4% of the time.

The penalty causes the bias. The penalty, called regularization, helps the model predict well by shrinking its estimates. So ĝ flattens the peaks of g. Those peaks fall in the weeks when spend is high. The spend coefficient picks up the revenue that ĝ missed.

What the learner seesone dataset
true g(X)learned ĝ(X)shrinkage gapm(X), spend
Estimates across 300 datasetsBiased
mean 0.6495% CI coverage 4%
√n(θ−θ)≈ (1/√n) Σ DiεiE[D²]noise, well behaved + (1/√n) Σ m(Xi)(g − ĝ)(Xi)E[D²]regularization bias
Flexible models learn more slowly than 1/√n, so the second sum grows as the sample grows. With more data, this method becomes more confident about the wrong number.
The idea Partial out demand from both sides

Subtract what demand explains. Twice.

Frisch and Waugh used this idea in 1933, and Robinson extended it to flexible models in 1988. You give machine learning two prediction tasks it's good at. You never ask it for θ directly.

Fitted slope
1.42
x: D   y: Y
Step 1 of 5

Start from the raw weeks.

Spend runs across and revenue runs up. The fitted slope is 1.42, the same wrong answer as before.

Step 2 of 5

Color each week by demand.

Indigo marks a slow week and yellow a busy one. Demand sorts the weeks along both axes at once, which creates most of the diagonal.

Step 3 of 5

Predict revenue from demand. Subtract.

Ỹ=Y−ℓ(X),  ℓ(X) = E[Y | X]

The cloud flattens vertically, and the slope now falls too far, to 0.17. Spend still moves with demand, but revenue no longer does. So the two stop lining up.

Step 4 of 5

Predict spend from demand. Subtract.

D̃=D−m(X),  m(X) = E[D | X]

Now the cloud narrows sideways. What's left of spend is the part demand can't predict, the budget surprises. The colors mix because demand no longer sorts the weeks.

Step 5 of 5

Regress residual on residual.

θ=Σ D̃iỸiΣ D̃i²

The slope settles at 0.49. This uses the same model and penalty as Attempt 2. The difference is how it's used. It makes two predictions and never estimates the spend effect itself.

Estimates across 300 datasetsCentered
partial out bothplug-in, from Attempt 2
mean 0.5095% CI coverage 94%

Across 300 datasets, the estimates center on the truth. The 95% intervals contain it about 95% of the time, as they should.

The model still uses the same penalty. So ℓ and m are shrunk and wrong in the same way ĝ was. This time the errors don't reach the estimate.

Statisticians call this property Neyman orthogonality. An error in one model only matters where it lines up with an error in the other, so the bias depends on the two errors multiplied together. Two small errors make a much smaller product. Part III draws this as a picture.

Attempt 3 Upgrade to a sharper learner

A better fit. A worse answer.

The method works in simulation, so Dana tries it on real data. There the ridge model looks too stiff. She swaps the revenue model for a much more flexible one.

The new model fits very well. Its R² on the data it trained on climbs from 0.84 to 0.92. Then the estimate drops to 0.25, half the truth. Finance would conclude that paid social barely works and cut it.

The new model averages each week with its nearest neighbour. On its training data, each week counts as one of its own two neighbours. Deep random forests and long boosting runs do something similar. Any model that nearly memorizes its training data has this problem to some degree.

Take one week. Its revenue has three parts: what demand predicts, the effect of that week's unplanned spend θv, and noise. A model trained on that same week has already seen its revenue. So its prediction absorbs part of the effect. When you subtract the prediction, you subtract part of the effect you wanted to measure.

Anatomy of one weekrevenue Yi
Yi
ℓ(Xi)
Ỹi
ℓ(Xi): what demand predicts θ·vi: the effect of this week's surprise spend εi: noise
100%
of the green effect is left in the residual Ỹi. The regression only sees what sits above the orange line.
(1/√n) Σ vi(ℓ − ℓ)(Xi)mean zero only if ℓ never saw row i
If the model trained on row i, then ℓ(Xᵢ) holds part of θvᵢ. This term then averages about √n·θ·σ²ᵥ·wᵢᵢ, where wᵢᵢ is the weight the model gives a row's own outcome. Here wᵢᵢ = ½, so the estimate lands at almost exactly half the truth.
Estimates across 300 datasetsBiased
mean 0.2595% CI coverage 0%
The second fix Cross-fitting

Never grade a week with a model that saw it.

Split the weeks into five groups, called folds. Train both helper models on four folds and compute residuals on the fifth. Rotate until every week has been scored by a model that never saw it.

500 weeks, 5 foldsrotation
Fold 1: train on 400 weeks, score 100.
training the modelsbeing scored, out of fold

Each residual now comes from a model that never saw that week's noise. So the problem term averages to zero again. No data goes to waste. Every week helps train the models four times and gets scored once. All 500 residuals then go into one residual-on-residual regression.

This is the same memorizing model as in Attempt 3, on the same data. The only change is which weeks each prediction could learn from.

Estimates across 300 datasetsCentered
cross-fittedin-sample, from Attempt 3
mean 0.5095% CI coverage 94%
The scorecard

Same data. Five answers.

Each card shows where one method landed across 300 datasets, compared with the truth of 0.50. It also shows how often its 95% interval actually contained the truth.

Dana's number
0.51

95% interval 0.37 to 0.65. Each extra dollar of paid social brings back about fifty cents in revenue.

The first regression would have led Finance to nearly triple the budget for a channel that loses money on each extra dollar. The flexible model without cross-fitting would have led them to cut it. The number they get now is the one the data support, as long as the assumption from Part I holds.

The scorecard also shows something less flattering. With the stiff ridge model, partialling out worked fine without cross-fitting. Cross-fitting is what lets you use any model you like on 140 messy columns. That freedom is why you would reach for machine learning in the first place.

Part III · Estimating ad lift

The holdout you never ran.

The campaign is over. Some customers saw the ads, most didn't, and the platform picked who. Matching, weighting and outcome models each get one try at guessing what would have happened without the ads. A doubly robust estimate gets two.

Dashboard · exposed vs unexposed
+213%
Lift in conversion rate, as the platform reports it.
saw the campaigndid notmatched on Dana's scorematched on the flexible score
+24%the true lift
5,000customers per campaign
2supporting models
1,150campaigns simulated in your browser
The question

Did the win-back campaign work?

A few months after the paid social work, Dana gets a harder question. The lifecycle team ran a six-week win-back campaign on connected TV and paid social, aimed at existing customers. Nobody held back a control group, because the media plan had no room for one. The platform decided on its own which customers saw the ads.

The platform's dashboard says customers who saw the ads converted at 34.3%, against 11.0% for those who didn't. That's a lift of +213%. The brand team wants to triple the next flight because of it. Before the money moves, Dana has to tell the CMO what the campaign really did.

For each of 5,000 customers, she has two fields from the CRM. One is a purchase-intent score from browsing. The other is the number of weeks since their last order. She doesn't have the platform's targeting rule, and nobody outside the platform does.

How the platform targetslogit e = −0.95 + 0.8·int + 0.6·(rec² − 1)

Higher intent means more ads. Recency has a U-shape, because the platform retargets recent buyers and chases lapsed ones.

How customers convert anywaylogit μ0 = −2 + 0.8·int − 0.3·rec + 0.7·(rec² − 1)

Customers at both ends of the recency range buy more on their own. That's the same U the platform targets.

The truth, set by usATT = 6.8 pp

The ads add 0.35 + 0.15·intent to the log-odds of buying. For the customers who saw them, that's a lift of +24%.

Attempt 1 Read the dashboard

+213%. The ads get credit for who they reached.

Conversion rate · one campaign1,578 exposed of 5,000

The dashboard compares everyone who saw an ad with everyone who didn't. But the platform didn't pick at random. It went after high-intent customers and customers at both ends of the recency range. Those people buy a lot whether or not they see an ad.

This is a simulation, so we know what these customers would have done without the campaign. They would have converted at 28.5%. The campaign added 5.9 points to that. The dashboard gives it credit for 23.4 points.

Dana knows the dashboard number is too high. She needs to know by how much, and she needs an answer she can defend.

Attempt 2 Match each exposed customer to an unexposed twin

Perfect balance. On paper.

Matching on the propensity score is the standard fix, and Dana does it by the book.

She fits a logistic model for the chance of seeing the ads, using intent and recency. Then she pairs each of the 1,578 exposed customers with the unexposed customer whose score is closest. Some unexposed customers get used more than once. The balance table looks perfect. The gap in intent drops from 0.74 to 0.00, and recency was already balanced.

The estimate falls to 17.9 points, a lift of +109%. That's half the dashboard's number, backed by a clean balance table. She's tempted to send it.

Then she plots the matched pairs. They're the red lines in the animation above. Many pairs join someone who ordered last week with someone who hasn't ordered in a year. They have the same intent and the same score, but opposite recency. Her model treats recency as a straight line, so customers at either extreme look average to it. The balance table only checks average recency, and the two extremes cancel out.

Balance before and after matching|standardized mean difference|
beforeafter, balancedafter, still imbalanced|SMD| < 0.1
A balance check only covers what you put in the model. Recency² was never in the propensity model, so nothing kept it balanced. It got slightly worse.
How often each control was reusedcontrols used at least once
Only 1,039 of the 3,422 unexposed customers end up in the matched sample. One of them stands in for 11 different exposed customers.

Matching is a form of weighting. Write out the matched estimate and the pairs drop away.

θmatch=1n1ΣD=1Yi−ΣD=0Kjn1Yj
Kj counts how many exposed customers were matched to unexposed customer j. Those counts are a rough estimate of the odds of exposure, e/(1 − e). So any mistake in the propensity model carries straight into the weights.
Matched estimate across 200 campaignsBiased
mean … pptruth 6.8 pp95% CI coverage …
The idea Use both models, and let each check the other

Two ways to be right.

Every look-back analysis like this needs a model. Weighting models who saw the ads. An outcome model predicts who buys. If you use only one and it's wrong, so is your answer.

Unexposed conversion, reweighted
11.0%
from 11.0% to 27.6%
Exposed, had they not seen ads
28.5%
known only in simulation
Exposed, as observed
34.3%
the dashboard's starting point

Each unexposed customer is scaled by ê/(1 − ê), the odds that a customer like them saw the ads. As the weights grow, the unexposed group takes on the exposed group's U-shape in recency (side panel). Their conversion rate climbs toward what exposed customers would have done without ads.

Where the weights blow up.

Estimated propensity by groupshare of each group
exposedunexposedê > 0.9

Even correct weights can cause trouble. Some exposed customers had a better than 95% chance of seeing the campaign. The platform almost never skipped people like them. So their unexposed look-alikes are rare, and each one gets a weight of 19 or more.

In this file, 81 exposed customers have a propensity above ê = 0.9. Only 4 unexposed customers do.

Effective number of controls
540 of 3,422
Largest single control's share of weight
2.8%

Show this overlap plot with every lift estimate. You can drop customers above a propensity cutoff, or only accept close matches. Both steady the estimate, but they change the question. You're now measuring lift only among exposed customers who had real look-alikes, so say that when you report it. The doubly robust estimator softens extreme weights a little, because the weights only multiply model errors. No method can create comparisons the data never had.

Weighting · IPW
θ=Ȳexposed−ΣD=0 wjYjΣD=0 wj,  w = e1 − e

This gives unexposed customers more or less weight until they look like the exposed ones. It works when the propensity ê is right. It ignores everything about who buys. Matching is a version of this.

Outcome model · regression
θ=1n1ΣD=1(Yi−μ0(Xi))

This learns who buys from unexposed customers, then predicts the same for exposed ones. It works when μ̂₀ is right. Where there are no unexposed look-alikes, it guesses, and it won't warn you.

θDR=Ȳexposed−[ 1n1ΣD=1μ0(Xi)the outcome model's counterfactual + ΣD=0 wj(Yj − μ0(Xj))ΣD=0 wjits average miss on look-alike controls ]
Start with the outcome model's guess at what exposed customers would have done without ads. Then check how far off that model is on unexposed customers weighted to look exposed, and add the gap back. If μ̂₀ is right, its errors average to zero whatever the weights. If ŵ is right, the weighted group matches the exposed group, so its average error equals the outcome model's error on the exposed.

Four worlds, 200 campaigns each.

A model is right here if it includes the recency² term. Each dot is one campaign's estimate. Green rows center on the truth. Red rows miss it.

Propensity wrong · Outcome wrongDana's models
Propensity right · Outcome wrong
Propensity wrong · Outcome right
Propensity right · Outcome right

In three of the four worlds, the doubly robust estimate lands on the truth. The fourth is Dana's world. Both her models treat recency as a straight line, and both errors push the same way. Her estimate comes back at 17.1 points. Doubly robust gives you two chances to get a model right. If both are wrong, it can't save you.

Why it works Bias as a product

Two errors, multiplied.

Work out how far the doubly robust estimate can be off, and you get the propensity error multiplied by the outcome error.

Bias(θDR)=E[ (1 − e) · (w − w) · (μ0 − μ0) ]P(D = 1)
With w = e/(1 − e). If either error is zero, the product is zero. The formula is for the version with raw weights. This page rescales the weights to sum to one, and that version is zero in the same places.
Weighting (IPW)…
Outcome model…
Doubly robust…
overstates liftunderstates liftbias under 0.4 ppAcross: how much of the recency² term the propensity model misses. Up: how much the outcome model misses. 0 means correct, 1 means the term is left out. Tap or drag on any map.

The weighting map has vertical stripes because its error depends only on the propensity model. The outcome model map has horizontal stripes for the same reason. The doubly robust map is clear along both axes. Near the corner where both models are right, it stays small in every direction, because it grows with the product of the two errors.

This is the same orthogonality as in Part II, now with a yes-or-no treatment. The doubly robust formula for the effect on exposed customers (the ATT) is Neyman orthogonal. So the two models' errors only matter through their product. Two flexible models that each improve at a rate of n−1/4 are enough. Cross-fit them, and you get a √n-accurate estimate with an interval you can trust.

The fix Learn both models, cross-fit both

Stop guessing the targeting rule.

Dana can't see the platform's rule, so she stops trying to write it down. She swaps both logistic models for flexible ones and cross-fits them, just as in Part II.

Each model is now a flexible logistic regression that can bend with each feature, using nine smooth curves per feature. A gradient-boosted classifier would work just as well. Nobody tells either model about the U-shape in recency. Both find it in the data. Each customer's ê and μ̂₀ come from models trained on the other four folds. That way, no customer's own conversion leaks into their own residual.

She reruns the check on simulated campaigns. This time the estimates land on the truth. The 95% intervals contain the true effect 95% of the time, just as they should.

Her matching work isn't wasted. She can keep the pairs and subtract each pair's predicted difference, μ̂₀(Xᵢ) − μ̂₀(Xⱼ). This is Abadie and Imbens's bias-corrected matching. It's also doubly robust, with the match counts acting as weights. With the flexible models, it gives 6.8 points on this campaign.

Doubly robust estimate across 150 campaignsCentered
flexible, cross-fitDana's linear models
mean … pptruth 6.8 ppcoverage …

Same question. Six answers.

Dana's number
+26%

95% interval +9% to +42%. That's 7.0 extra conversions for every 100 exposed customers.

The interval is wide. A ratio needs a lot of data, and 1,578 exposed customers isn't much. Even so, the whole interval sits far below the dashboard's +213%. It also sits well below the +109% that the matching analysis almost reported.

Dana takes this number to the CMO, with the overlap plot attached. The campaign works, at a fraction of what the dashboard claimed. That holds as long as the platform didn't target on something Dana can't see. That condition is Part I's assumption, and the last section tests how much it matters.

The whole method

The recipe for ad lift.

  1. Define the trial you wish you had run.

    Write down who was eligible, when you measure exposure, and what counts as a conversion over what window. Measure every feature in X before anyone saw an ad.
  2. Cross-fit two models.

    Train the propensity model ê(X), the chance each customer was exposed, on everyone. Train the outcome model μ̂₀(X) on unexposed customers only, then predict for everyone. Use flexible models and split the data into K folds.
  3. Look at overlap before looking at lift.

    Plot ê for each group. If you plan to drop customers with no good comparison, decide now. Then write down which customers your result still describes.
  4. Compute the doubly robust ATT.

    θ = meanD=1(Y − μ0) − ΣD=0w(Y − μ0)ΣD=0w
  5. Get intervals from the influence function.

    Each customer's score gives you the standard error. Relative lift divides by an estimated rate, so use the delta method to carry the uncertainty from both parts.
  6. Try to break it.

    Run a placebo test on conversions from the weeks before the campaign. It should show zero lift. Check that the weighted groups match on squares and curves of each feature, too. Then run a sensitivity analysis for a confounder you couldn't measure.
import numpy as np
from sklearn.model_selection import KFold
from sklearn.ensemble import HistGradientBoostingClassifier as HGBC

# X: (n, p) pre-exposure customer features   d: 1 if exposed   y: 1 if converted
n = len(y); e_hat = np.zeros(n); m0_hat = np.zeros(n)
for train, test in KFold(5, shuffle=True, random_state=0).split(X):
    e_hat[test] = HGBC().fit(X[train], d[train]).predict_proba(X[test])[:, 1]
    ctl = train[d[train] == 0]                          # outcome model: unexposed only
    m0_hat[test] = HGBC().fit(X[ctl], y[ctl]).predict_proba(X[test])[:, 1]

e_hat = np.clip(e_hat, 0.005, 0.995)
w, r, t, c = e_hat / (1 - e_hat), y - m0_hat, d == 1, d == 0
t1 = r[t].mean()
t2 = np.sum(w[c] * r[c]) / np.sum(w[c])
att = t1 - t2                                           # doubly robust ATT

p, q = t.mean(), np.mean(w * c)
phi = np.where(t, (r - t1) / p, -w * (r - t2) / q)      # influence function
base = y[t].mean() - att                                # exposed, had they not seen ads
phi_c = np.where(t, (y - y[t].mean()) / p, 0) - phi
phi_l = phi / base - att * phi_c / base**2             # delta method for the ratio
se, lift_se = phi.std() / np.sqrt(n), phi_l.std() / np.sqrt(n)
print(f"ATT {att:.3f} ± {1.96*se:.3f}   lift {att/base:+.0%} ± {1.96*lift_se:.0%}")

Off the shelf

DoubleML's DoubleMLIRM(score="ATTE") runs this estimator with cross-fitting and repeated splits. EconML's DRLearner fits the lift-by-segment curve.

ATT or ATE

The ATT asks what the campaign did for the people who saw it. It needs only μ₀ and comparable unexposed customers for each exposed one. The ATE asks what would happen if everyone saw the ads. It needs both outcome models and comparable customers on both sides.

Playground

Pick your models. Run 100 campaigns.

Choose how each model handles recency and how sharply the platform targets. Then see which estimates hold up.

Flexible models use five-fold cross-fitting, so they take a few seconds longer.

1.0×

Changes how strongly the platform targets. Sharper targeting leaves fewer comparable customers and makes the weights swing more.

5,000
truth 6.8 pp
Matching·
mean …sd …
Weighting (IPW)·
mean …sd …
Outcome model·
mean …sd …
Doubly robust·
mean …sd …
Fix only the propensity model. Matching, weighting and the doubly robust estimate all recover. The outcome model alone stays off.
Fix only the outcome model. The outcome model and the doubly robust estimate recover. Matching and weighting stay biased.
Make both flexible, then sharpen targeting. The estimate stays on target but gets wider. That's what running out of comparable customers looks like.
Back to Part I The question no data can answer

Same data. Different truths.

Take the ad question. The CFO asks how Dana knows the unexposed customers are a fair comparison. She shows balance tables and overlap plots. She can estimate the number as precisely as she likes. He asks again. What if the platform knew something she doesn't, like which customers had been browsing its app that week?

She can't rule it out. No amount of data from this campaign would let her. She needs to know how exposed customers would have converted without ads, and that is missing for every one of them. The table shows ten customers. Drag the slider to give exposed customers a hidden head start. Their hidden column changes. Every column Dana can see stays exactly the same.

This is what untestable means. Two worlds can produce the same (X, D, Y) and still have different effects. Any test you run on (X, D, Y) gives the same result in both. More customers won't help, and neither will a better model.

Ten customers from the campaigndashed = never observed
IntentRecencySaw adsConvertedWould have converted without ads
0.0 pp
Observed data
Identical
1,578 exposed · 542 converted
Best estimate of the identified quantity
7.0 pp
the same in every world
True ATT in this world
5.9 pp
known only in simulation
What to report Sensitivity analysis

Say how wrong it would have to be.

No one can prove the key assumption. So once you have the estimate, show how far that assumption would have to break before the decision changes.

Effect if the hidden head start is δpercentage points
effect = ψ̂ − δ95% intervalhead start created by a measured feature

Call δ the hidden head start. It measures how much more likely exposed customers were to buy anyway, compared with unexposed customers who share their intent and recency. If nothing is hidden, δ = 0. For any other value, subtract δ from the estimate to get the effect.

The interval touches zero at a head start of 3.3 points. The estimate itself hits zero at 7.0. To judge whether those numbers are big, compare them with signals Dana did measure. Dropping intent from both models moves the estimate by 8.0 points. Dropping recency moves it by 11.3.

That gives Dana one sentence for the deck. The campaign lifted conversions by about +26%. It stays clear of zero unless the platform's hidden targeting created more than about 40% of the head start that intent did. The CFO may doubt that premise, but now he can see it and argue with it.

We kept this dial simple on purpose. It assumes the same gap in conversion rate for every customer. More formal tools work the same way. Examples are Rosenbaum bounds for matched designs, Cinelli and Hazlett's partial-R² benchmarks, and the omitted-variable bounds for debiased ML by Chernozhukov and coauthors. Each one names the assumption, turns it into a dial, and compares the dial with what you measured. That comparison is still a judgment call. Nothing guarantees a hidden signal behaves like a measured one.

These tools make the most of one dataset. They cannot tell you whether the dataset holds the causal answer. For that you need evidence from outside the sample, which is the subject of Part IV.

Part IV · Evidence beyond the sample

You can't test the assumption. You can replace it.

Everything so far drew a causal answer out of one dataset, and that always needs an assumption the dataset can't check. An experiment removes the need for it. Hold out a random slice of the customers the platform picked, and the comparison is fair by construction.

1 · The platform picks2 · A coin flip holds some back3 · Compare like with like
Customers the platform skipped
…
a different kind of customer
Picked, then held out at random
…
same kind of customer, no ads
Picked and shown the ads
…
…
Why it works Randomization

The coin flip does the identifying.

In Part I, six causal graphs fit the same data. A coin flip leaves one. Nothing about a customer can cause a coin to land heads, so nothing can confound the comparison.

What a holdout gives you

The held-out customers are the missing half from Part I, made visible. The platform picked them for the same reasons it picked everyone else, hidden reasons included. Then chance kept the ads away.

So their conversion rate is a direct measurement of what exposed customers would have done without ads. You need no propensity model, no outcome model and no list of confounders. Because the holdout comes from the customers the platform picked, it measures the same thing as Part III: the effect on exposed customers.

What it does not give you

An experiment answers one question, about one campaign, for one audience, at one point in time. And it costs something: every held-out customer is a sale you may have given up.

Small holdouts give wide ranges. Experiments can also break in practice. Held-out customers see the ads on another device, or the platform delivers differently when a test is running. An experiment replaces an untestable assumption with ones you can inspect. It does not remove the need to think.

Triangulation Several lines of evidence

Four answers. Four different ways to be wrong.

No single method earns full trust. What earns it is agreement between methods that would fail for different reasons. Turn up the platform's hidden signal and watch which lines move.

1.0

Something the platform uses to pick customers that also predicts who buys. It is not in the CRM, so no model can adjust for it.

10%

Share of the picked customers who are randomly kept from seeing the ads.

The placebo test only works if whatever is hidden also moved sales before the ads ran.

Estimated effect on exposed customers · 100 campaigns eachpercentage points
true effectmiddle 95% of campaignsaverage

Model-based

Doubly robust estimate

Uses every customer, so it is precise.

Fails if: the platform targets on something you don't have.
Check

Placebo test

Runs the same analysis on sales from before the campaign. Ads can't change the past, so the answer should be zero.

Fails if: the hidden signal is new, so it left no trace in the past.
Model-based

Placebo-corrected estimate

Subtracts the placebo result from the estimate, on the logic that the same bias shows up in both.

Fails if: the bias before the campaign differs from the bias during it.
Design-based

Randomized holdout

Compares picked customers who saw the ads with picked customers who were randomly held back.

Fails if: the holdout is too small to be precise, or it leaks.

Read the chart by asking which lines agree. When the model-based estimate and the experiment agree, you have two independent reasons to believe the number, and the tighter one tells you how big it is. When they disagree, the pattern of disagreement points at the assumption that broke.

The lines also help each other. A small holdout is honest but vague. A doubly robust estimate is sharp but may be off. A holdout that confirms the sharp estimate lets you keep its precision. A holdout that contradicts it tells you how far off the model is, and that correction carries over to the next campaign's model.

The same trap, upstream Audience building

The best-looking audience can be the worst one to advertise to.

Most audience work looks for customers who are likely to buy. Those customers are easy to find and they make every dashboard glow. Many of them were going to buy anyway.

20,000 customers · each dot is one customeraudience highlighted
Dashboard: conversion rate
…
…
Would have bought anyway
…
invisible without a holdout
Sales the ads caused
…
per 1,000 customers reached
Cost per sale the ads caused
…
at $3 to reach each customer

Dana's team builds a model of who is likely to buy and hands the top fifth to the platform. The launch looks like a triumph. The audience converts at 57%, far above the file average of 22%, and the cost per order is the lowest the team has seen.

Now look at what the ads did. Without any ads, this audience would have converted at 53%. The campaign added 4 points. Each sale the ads caused cost $73, more than twice what it would cost to advertise to customers picked at random.

No dashboard metric after launch can catch this. Conversion rate, cost per order and return on ad spend all count sales that sit next to the ads. A likely-buyer audience is built to put sales next to the ads. The better the audience model predicts purchases, the better these numbers look, whether or not the ads change anything.

This is Part III's problem moved one step earlier. There, the platform's targeting made exposed customers look good. Here, your own targeting does it, and it is harder to see because the selection was the goal.

The audience worth paying for is the one the ads change. Those customers sit at the top of the chart: a modest chance of buying without ads, and a much higher chance with them. Finding them takes a model of the effect, like the DR-learner below, trained on data where exposure varied for reasons you understand.

And only a holdout can confirm it. Hold back a random slice of any audience before launch. The gap between the slice that saw ads and the slice that didn't is the one number on this chart that a dashboard can't produce. Judge audiences by that gap.

Finding the customers the ads change.

Back to the win-back campaign from Part III. The same two models that measured its lift can also show which customers it moved.

Effect of exposure by purchase intentpercentage points
true effectDR-learner, mean of 150 campaigns80% of campaignsdashboard segment cut

Give every customer a score that estimates their own lift. Then average those scores within each segment you care about. Kennedy's DR-learner builds that score from the same two models:

φ=μ1−μ0+D(Y − μ1)e−(1 − D)(Y − μ0)1 − e

Any one customer's score is noisy. Averaged over similar customers, it gives the true effect, as long as either the propensity model or the outcome models are right. This still relies on the platform not targeting on anything hidden. Smoothing the scores across intent gives the curve on the left.

The dashboard compares exposed and unexposed customers within each intent band. By that view, the campaign adds 14 to 23 points for everyone. The DR-learner tells a different story. Lift is close to zero for low-intent customers and climbs to about 14 points for the highest. That curve tells the team who to target in the next flight.

Putting it together

One estimate is a claim. Several make a case.

A causal claim from one sample always rests on something the sample can't show. So treat each analysis as one piece of evidence, and plan for more than one.

  1. Design before you estimate.

    Ask for a random holdout, a staggered launch or a geo test before the campaign runs. A small experiment beats a clever model, and the two together beat either.
  2. Estimate with methods that forgive.

    When you must work from data that already exists, use doubly robust estimation with cross-fitting, and adjust only for things measured before exposure.
  3. Look for evidence that could embarrass you.

    Run placebo tests on past sales and on outcomes the ads can't affect. A method that finds an effect where none can exist is telling you about its bias.
  4. Say how wrong you would have to be.

    Report the tipping point from the sensitivity analysis next to the estimate, with a benchmark the reader can judge.
  5. Judge audiences by what the ads change.

    A high conversion rate says the audience buys. Only a holdout inside the audience says the ads made them buy.
  6. Compare lines that fail differently.

    Put the model-based number next to an experiment, a different data source or a different method. Agreement between methods that share an assumption proves little.
  7. Keep the disagreements.

    When an experiment contradicts a model, record the gap and use it to correct the next model. Over time the experiments calibrate the models, and the models extend the experiments.

Dana's final deck has three numbers on one page. The doubly robust estimate says the campaign lifted conversions by about a quarter. The sensitivity analysis says how much hidden targeting it would take to erase that. And the plan for the next flight includes a ten percent holdout, so that next quarter she can replace the assumption with a measurement.