← Kalyan Venkatesh

GroundTruth

An A/B analysis that runs its validity checks before it computes the effect, on a 13.9M-row Criteo uplift log.

Try it

control-arm conversion, the thing the lift is measured against
the relative lift you want to be able to catch
users in each of the 2 arms
correlation of the CUPED covariate with the outcome

Statistical power · two-proportion z-test

0%

95% confidence interval · naive vs CUPED

naive
CUPED
effect
variance cut interval cut

Sequential decision · if you peeked

always-valid interval is wider than fixed-horizon

How it works

01System design, the stages in run order

Read it top to bottom. On the left are the raw log and the config that fixes alpha, power, the MDE and the ship bar before any effect is seen. The centre spine runs in order: ingest to per-arm sums, then the 6 guardrails (SRM, balance, the specification curve), then power, then the effect (z-test, CUPED, Lin, CACE), then the sequential and uplift stages. The right shows what each stage writes. The amber path is a guardrail firing.

system.blueprint
hover a chip to spotlight its stage
Sums & statistics, in flight
Guardrail fires
Cleared, adjusted, shipped
Stage · artifact · result

The stats layer never sees a row

Spark reduces 13.9M rows to per-arm sums and the driver receives a 68 KB parquet file. Every test here (z-test, CUPED, Hotelling T²) is additive, so it runs off those sums. Full run: about 80 seconds.

Guardrails first, on purpose

3 of 6 fire: the split is under-dispersed (ratio is exactly 17:3), the file is not shuffled (13% to 100% treatment by block), and all 12 features differ. The effect is only computed after they run.

CUPED earns its place on robustness

The unadjusted lift spans 1.44x across 3 duplicate-handling specs, the adjusted one spans 1.12x. Adjustment cuts specification sensitivity 3.5x. That stability, more than the 10.7% variance reduction, is why CUPED earns its place here.

Notes

The SRM pass is too clean.

The 85/15 split misses by 2 users of 13.98M (p=0.9989), because control was downsampled to hit exactly 15%. It proves the check runs.

Duplicate rows set the magnitude.

2.22M byte-identical rows carry zero conversions, so all are kept. Raw lift then reads +59.45% to +85.37% by choice; the CUPED lift barely moves.

A review caught 3 wrong numbers.

The first headline was +50.76%, an adjusted numerator over an unadjusted control mean; the real figure is +47.36%. A test in tests/test_stats.py locks each one.