← Back to portfolio Causal / Uplift — CUPED and Individual Treatment Effects preview
Stack
PythonLightGBMscikit-learnpandas / NumPymatplotlibpytestuv
Impact
  • CUPED keeps the ATE (0.270 → 0.276) and cuts the standard error ~26%
  • Same power with ~5.6k users/arm instead of 10k (95% CI narrows 1.35x)
  • Uplift models recover that new users respond ~10x more than returning users
  • Implemented from scratch on LightGBM — no causalml/econml dependency
Source View on GitHub → Updated: Sep 18, 2026

Causal / Uplift — CUPED and Individual Treatment Effects

Context

A standard two-sample t-test answers only “does the treatment work on average” and ignores the pre-period. Experiments therefore ask for more traffic than they need, and retention offers go to everyone while only part of the audience responds.

The project closes both gaps with two methods:

  • CUPED — variance reduction using a pre-period covariate: the same ATE with a narrower CI, so fewer users are needed for the same power.
  • Uplift modeling — estimation of the individual treatment effect (ITE), so offers target the users who actually respond rather than everyone.

Both run on a synthetic randomized experiment with a known heterogeneous effect, so the estimates can be checked against ground truth.

Data & Method

Synthetic, deterministic data (seed = 42), 20,000 users in a randomized experiment:

  • segmentnew (30%) / returning (70%) → heterogeneous treatment effect;
  • x_pre — a pre-period covariate correlated with the outcome (ρ ≈ 0.67);
  • treatment — 50/50, independent of everything (clean randomization);
  • tau_true — the ground-truth individual treatment effect (eval only);
  • y_cont — continuous outcome; y_bin — binary conversion.

CUPED — same ATE estimate, less variance:

Method ATE SE 95% CI
Naive 0.270 0.019 [0.232, 0.308]
CUPED 0.276 0.014 [0.247, 0.304]

The point estimate is unchanged (0.270 → 0.276, within noise). The standard error shrinks by ~26% (×0.74) and the CI narrows 1.35x. Variance reduction is ~45% actual vs 55% theoretical (the gap is the covariate being a pre-period proxy, not the outcome itself).

Uplift — individual effects, T- and S-learners:

Model AUUC QINI uplift@20% corr(τ)
T-learner 0.0043 0.0014 0.072 0.50
S-learner 0.0057 0.0029 0.041 0.66
Random 0.0014 −0.0014 −0.015 0.007

A note on honesty: the latent ground-truth τ is 0.60 (new) / 0.08 (returning), but the conversion uplift is ~0.11 / 0.01 because the sigmoid at a high baseline conversion (~71%) damps large latent effects. Comparing predicted binary uplift to latent τ would be a scale mismatch; we report rank correlation (scale-free) and per-segment empirical recovery (same scale).

Run

uv run --with pandas --with numpy python data/generate_data.py
uv run --with pandas --with numpy --with scikit-learn --with lightgbm --with matplotlib python run.py
uv run --with pandas --with numpy --with scikit-learn --with lightgbm --with matplotlib --with pytest pytest -q

Outputs: reports/metrics.json + reports/uplift.png (QINI curves + segment uplift vs ground truth).

Findings

CUPED buys power for free: no new experiment design is needed, only a better estimator on data you already collected — provided a pre-period covariate exists.

Uplift answers a different question than A/B testing: not “does the treatment work on average” but “who does it work on”. The two are complementary, not substitutes.

The model recovered the shape of the effect: “new” users respond ~10x more than “returning” users — the targeting signal a discount campaign would act on.

Segment Truth (binary uplift) T-learner S-learner
new 0.110 0.126 0.120
returning 0.010 0.018 0.019

Synthetic data with a true heterogeneous effect is the only reason recovery can be checked at all: on real data the ITE is never observed — the fundamental problem of causal inference.

Impact

  • CUPED — same ATE estimate, standard error −26%, 95% CI 1.35x narrower.
  • Traffic savings — 10k → ~5.6k users per arm at the same power.
  • Uplift — T- and S-learners beat random on AUUC, Qini and uplift@20%.
  • Targeting — the recovered “new ~10x returning” effect turns discounts from “everyone” into addressed actions.

Documentation

Case study

Problem

A standard two-sample t-test answers only 'does the treatment work on average' and ignores the pre-period. As a result, experiments need more traffic than necessary, and retention offers go to everyone while only part of the users respond. The project needs methods that reduce variance and estimate the effect at the individual-user level.

Approach

CUPED: Y_adj = Y − θ·(X − mean(X)), with θ = Cov(Y,X)/Var(X) — the same expected ATE with variance ~(1 − ρ²). Two LightGBM uplift learners: a T-learner (one model per arm) and an S-learner (treatment as a feature), uplift = P(t=1) − P(t=0). Evaluation: AUUC, Qini, uplift@20%, rank correlation with the latent τ, and per-segment recovery; all implemented from scratch on LightGBM, without causalml/econml.

Result

CUPED leaves the point estimate unchanged (0.270 -> 0.276, within noise) but shrinks the standard error by ~26% and narrows the 95% CI by 1.35x — an experiment that needed 10k users per arm now needs ~5.6k. Both learners beat random on every metric; the model recovers that new users respond ~10x more than returning users, the signal a discount campaign would act on.

−26% SE reduction
1.35x 95% CI narrowing
10k → 5.6k Users per arm
0.66 corr(τ), S-learner

Charts

Source: github.com/NikitaBoyarkin/causal-uplift: metrics from run.py (reports/metrics.json) on the seeded synthetic randomized experiment (20,000 users, seed=42); regenerate with `uv run --with pandas --with numpy --with scikit-learn --with lightgbm --with matplotlib python run.py`

CUPED: same ATE, narrower interval

ATE point estimate and 95% CI width for the naive estimator vs CUPED on a synthetic randomized experiment (20,000 users, seed=42).

0 0.1 0.2 0.3 Naive — ATE: 0.27 0.27 CUPED — ATE: 0.28 0.28 Naive — 95% CI width: 0.08 0.08 CUPED — 95% CI width: 0.06 0.06 Naive CUPED Method Value ATE 95% CI width
Key takeaways
  • The point estimate is unchanged: ATE 0.270 -> 0.276 (within noise) — CUPED reduces variance, not the effect.
  • The 95% CI width falls from 0.076 to 0.057 (1.35x): the experiment needs ~5.6k users per arm instead of 10k at the same power.

Segment-level uplift recovery

True binary uplift vs the T- and S-learner predictions per segment. The latent tau is 0.60 (new) and 0.08 (returning); the sigmoid at a high baseline conversion damps the effect to ~0.11 / 0.01.

0 0.05 0.1 0.15 new — Truth: 0.11 0.11 returning — Truth: 0.01 0.01 new — T-learner: 0.13 0.13 returning — T-learner: 0.02 0.02 new — S-learner: 0.12 0.12 returning — S-learner: 0.02 0.02 new returning Segment Uplift Truth T-learner S-learner
Key takeaways
  • The model recovers that new users respond ~10x more than returning users (0.110 vs 0.010) — the signal a discount campaign acts on.
  • Predictions are close to ground truth in both segments: 0.120 (S-learner) vs 0.110 (truth) for new.

See also

Connection map

Projects, posts and topics connected to this one. Hover a node to see its name; click to open.