top of page
 the Uplift Targeting wordmark, the line “From randomised data to a priced who-to-treat policy”, four stat tiles read from the repository’s artifacts (1,064,000 rows across two randomised experiments; 8 models on one cross-validated harness; Criteo Qini +0.0905 ± 0.0067 for the uplift forest against the response model’s +0.0877 ± 0.0074; 0 of 6 uplift models clearing the Hillstrom bar), and an inset of the Criteo leaderboard against the dashed baseline.

Uplift Targeting

A model that predicts who will respond also picks customers who would have responded anyway. This project ranks customers by how much a contact changes their behaviour, prices a budget against that ranking, and reports when the simpler model wins.

Executive Summary


Bottom line: The simpler model held at 64,000 customers, drew level with uplift at a million rows, and lost, narrowly, at fourteen million: the deployment decision.

  • Business problem: contact budgets should go to customers a contact moves, but no customer shows both outcomes, so that effect is never labelled per person.

  • What was built: six uplift models against two naive baselines, eight in all, on one cross-validated harness, a written selection rule, and a priced budget step.

  • Result, stated honestly: no uplift model beat the response model on 64,000 customers; on a million rows the forest edged it inside both fold bands and trailed it on the hold-out; on all fourteen million both uplift models cleared it on every fold, by four to six percent of its Qini.

  • Reading time: about 33 minutes, counting every visible word including tables and captions, or 41 with the four deep-dive sections opened.

  • GitHub: https://github.com/i-hridaysaha/uplift-targeting

  • Live demo: https://uplift-targeting-zpatkwkjfwqvnqdxpu9rzc.streamlit.app, a Streamlit app that scores the untouched hold-out live and reprices the policy from a budget slider.


Project Snapshot


Dimension

Detail

Scale and complexity

1.06M rows, eight models

Core technique

Uplift meta-learners and forests

Engineering challenge

No per-row ground truth

Differentiator

Honest negative, priced policy


Project Metrics Card


Six tiles: 1,064,000 rows across two randomised experiments; 8 models on one cross-validated harness; the headline tile, Criteo cross-validated Qini +0.0905 ± 0.0067 for the uplift forest with the response model’s +0.0877 ± 0.0074 printed beside it; 0 of 6 uplift models clearing the Hillstrom baseline on the cross-validated mean; a net value band of −$8,811 to $2,567 for the top 10 percent of the Criteo hold-out; and 141 unit tests.
Figure 2. The numbers, with the baseline printed beside the headline: the band is the result, and the tile that says 0 of 6 is a default to the simpler model on data that cannot rank them, as much a finding as the one that says +0.0905.
Public, anonymised, randomised email and ad data; never deployed. Qini is normalised and reported as mean ± std over five folds; the dollar band sweeps $50 to $150 per conversion against $0.05 to $0.50 per contact.

Key Insight


The most important lesson was that uplift is never labelled per customer, so models had to be judged as rankings, in cross-validated bands, against a cheap baseline the system was built to let win.


Jump links



Architecture Diagram


Four numbered stages, top to bottom. Data preparation loads both experiments, maps them to one binary treatment, splits them into five stratified folds plus a hold-out, and checks balance; one trunk from the splits fans out to the three model families. The model bakeoff fits two baselines, three meta-learners and three direct models on the same folds. Model evaluation runs the audited harness, keeps the bands, applies the selection rule and scores the hold-out once. One connector carries the chosen model into serving, where the policy bundle, the only emphasised block, is loaded by a FastAPI service and a Streamlit demo, and both feed the priced who-to-treat policy.
Figure 3. The policy bundle is the one artifact that ships: the harness selects it, the service and the demo both load it, and every score, curve, decile and priced band downstream is read off the bundle; nothing downstream refits a model or re-selects one.

The Problem


A churn model names who will leave and a forecast names next quarter. Neither answers the question a budget owner asks: what should we do, and to whom. The customers a model flags and the customers an action changes are not the same people. Someone who would have visited anyway is wasted budget, someone a nudge tips over is real gain, and a few people a nudge pushes away are worse than doing nothing. A model that ranks by predicted response spends most of the budget on the first group, the people who inflate its accuracy because they were going to act regardless.


Both experiments show an effect that is real and uneven. In the Hillstrom email test 16.71 percent of treated customers visited against 10.62 percent of control, a lift of 6.09 points; in the Criteo ad test the lift is 1.08 points. Sliced, it moves: across Hillstrom's prior-spend deciles it climbs from 5.04 points to 7.84, and in Criteo the lowest decile of one anonymised feature carries 6.38 points against roughly zero elsewhere. An average hides that; targeting is the act of finding it.


The quantity that finds it is causal: a customer's outcome if contacted minus the same customer's outcome if not. Nobody observes both. Each customer received the contact or did not, and the other outcome never happened, so unlike a forecast, which the future eventually grades, uplift is never labelled for any single person. There is no answer key, not late, not ever.


The obvious build models that effect directly and ranks by it. Six such models were built. The question this page answers is not whether they work but whether any of them earns a budget over the plain classifier it was meant to replace.


Whom to contact is the visible problem; the real one is an effect with no per-row label, so every model is judged in aggregate.


Requirements and Constraints


Functional Requirements

  • Estimate and rank: a per-customer uplift score from randomised treatment and control data, and a population ranked by it.

  • Compare on one footing: several uplift models against two naive baselines, treating everyone (the zero line) and a plain response classifier (the bar), on identical folds, with the winner chosen by a rule written before the numbers.

  • Price the ranking: a budget, a count or a fraction becomes a who-to-treat policy with a projected incremental value.


Non-Functional Requirements

  • Seeded end to end: every headline reproduces at seed 42 from the persisted splits, one command per phase; Criteo's raw archive has to be supplied locally, because its public download is dead.

  • Free and small: public datasets, open-source libraries, free hosting, and dependencies added only when a phase first needs them.

  • The ruler audited first: the evaluation harness is tested against a reference implementation before any model is judged by it.


Hidden Constraints

The target has no per-row label, and every proxy for it is aggregate and noisy. Three consequences follow, and each bends a decision below.

  • Evaluation is a ranking metric, not a per-row error: it has to be read over groups, which is what Qini, the incremental outcome a ranking captures above random targeting, and AUUC, the area under the uplift curve, are. Decision 1 builds that.

  • Every number is a band: those metrics are high-variance and depend by construction on the treated-to-control balance, 2 to 1 on Hillstrom and 85 to 15 on Criteo, so a single split can rank a lucky model first. Decision 3 makes the band the unit.

  • The price is a swept range: what a conversion is worth and what a contact costs are inputs no dataset carries. Decision 6 carries them as ranges.


Figure 4 is the constraint made visible. On Hillstrom's 51,200 development rows every uplift model's fold-to-fold spread is wider than its own mean, so none can be told from zero, while the response model sits at +0.0197 ± 0.0170. A six-point effect on a 14.68 percent base rate is, on this many rows, too thin to rank.


A bar chart of normalised cross-validated Qini for eight models on Hillstrom. Treat-everyone sits at zero. The response model is the tallest at +0.0197 with a dashed line at its height. Every uplift model bar, from the S-learner at +0.0088 to the uplift forest at +0.0129, carries an error bar that reaches below zero, so all six straddle the axis.
Figure 4. The result most portfolios would bury: on 51,200 development rows no uplift model's band clears zero, and the response model is the tallest bar.

No label, noisy proxies, and a price built on assumptions: so the system reports rankings in bands against a baseline it must let win.


High-Level System Design


The pipeline in Figure 3 runs top to bottom, every stage pure logic kept apart from the I/O around it. Both datasets load through the scikit-uplift loader, Criteo from a local archive because its public downloads stopped answering. They land in one schema with a single binary treatment: Hillstrom's two email arms collapse to one treated flag, and Criteo's randomised flag is used as it comes, its post-treatment exposure flag ignored. Splits stratify on treatment by outcome so every fold and the 20 percent hold-out keep the arm balance and the base rate. Before any model runs, a standardised-mean-difference check per covariate confirms the randomisation on both datasets.


Eight models then fit on the same folds: treat-everyone and a response model as baselines; three meta-learners over a shared LightGBM base, S, one model with treatment as a feature, T, one model per arm, and X, T's two models blended by propensity; and three direct models, a propensity-corrected class transformation, an uplift tree and a bagged uplift forest. Each is scored out of fold on Qini, AUUC and uplift by decile as mean and standard deviation across folds, and the five per-fold values are kept beside the band.


A selection rule applies the beat-the-bar test on the cross-validated mean, breaks ties among the survivors on the tighter band, and reads the hold-out once, as confirmation rather than as a vote. The shipped ranking and the baseline are then read at the top 10 percent of the hold-out, where the policy spends. Permutation importance names the winner's drivers.


The chosen model, its feature schema and a slim hold-out reference freeze into one pickled policy bundle, stamped with the lightgbm and numpy versions that built it so a later dependency bump warns instead of moving a score silently. A FastAPI service reads it for /score, /policy and /health; the Streamlit demo loads the same bundle in process, so the hosted app needs no running API and no processed data, and on Hillstrom it adds readable segment filters over the reference rows. Each stage embeds one of the six decisions that follow.


The measuring stick is built before the models it judges, and the bundle it selects is the only artifact that ships.


Engineering Decisions


Decision 1: Evaluate the ranking, not a per-row error


Context

There is no per-customer uplift label to compute an error against, and the two datasets differ in base rate, 14.68 against 4.72 percent, and in arm ratio, so their scores also have to sit on one scale.


Alternatives Considered

Option

Advantages

Disadvantages

Per-row uplift error

Familiar, one number per model

Impossible: the label does not exist for any customer

Response classifier AUC

Easy, well understood

Measures who responds, not who is moved, which is the wrong question

Normalised Qini and AUUC with uplift by decile

Scores the ranking the policy will use; the 0 to 1 scale compares datasets with different base rates and arm ratios

Aggregate and high-variance, so it needs a band to read


Final Decision

  • Why this option won: Qini asks what the data can answer: sorted by predicted uplift and treated from the top, do incremental visits accumulate faster than random targeting? The size-corrected curve, which scales the control arm up to the treated arm's size, handles Criteo's 85 to 15 arms; the decile view is the readable companion.

  • Which trade-offs were accepted: no single per-model error; a model is a curve and a band. A whole-curve coefficient can also rank two models by the part of the curve no budget ever reaches, so Results reads the curve at the operating point as well.

  • Which assumptions were made: the coefficients are defined to equal scikit-uplift's qini_auc_score and uplift_auc_score, and the from-scratch implementation is tested equal to them to machine precision; the reference library is the audit, not the engine.


Engineering Principle

When the quantity has no label, measure the decision it informs: the ranking, not a point estimate that cannot be graded.


The harness scores the ranking the policy will act on, and it was tested equal to an independent implementation before it judged anything.


Decision 2: Make the naive baseline the adversary


Context

A ranking metric needs a reference. Uplift is a difference of two noisy rates, often small next to the base rate, so a sophisticated model can lose to a plain classifier without anyone noticing. Two references are cheap: treat-everyone, a constant score whose Qini is the diagonal at exactly 0.0000, and a response model, one LightGBM classifier of the outcome fitted on the whole population with the treatment column dropped.


Alternatives Considered

Option

Advantages

Disadvantages

One uplift model, its Qini reported

A simple story

No reference; a mediocre number looks fine alone

Uplift models compared only to each other

A winner emerges

The winner may still be worse than doing something trivial

Every model against treat-everyone and the response model on the same folds

A real bar, and the negative case is exposed

More to build, and a negative headline is possible


Final Decision

  • Why this option won: treat-everyone is the zero line and the response model is the bar. An uplift model must clear both on the cross-validated mean to be considered at all; the band rule in Decision 3 decides among those that do. The asymmetry is deliberate: the baseline is the default, not a competitor, so a lead on the mean is the entry ticket, not the verdict, and if none clears the bar the response model wins by default.

  • Which trade-offs were accepted: the bakeoff can end in a negative, and on Hillstrom it did. A point test at the bar also admits a lead inside the noise, and on Criteo it did, which is why every Criteo claim here carries its hold-out qualifier and the deployment case is made at the operating point, not on the coefficient.

  • Which assumptions were made: the response model shares its base-learner parameters with the meta-learners, so the comparison is between learners, not tuning budgets.


Engineering Principle

A model is only as good as the cheapest thing it beats; build that first and make it the thing to defeat.


Treat-everyone is the zero line, the response model is the bar, and the rule lets the bar win.


Decision 3: Report bands, score the hold-out once


Context

With the bar set, the question is how many samples of the metric to trust. Qini moves with the split; on both datasets the single hold-out disagreed with the cross-validated order.


Alternatives Considered

Option

Advantages

Disadvantages

One train and test split

Standard, one number per model

One draw of a high-variance metric can rank a lucky model first

Cross-validation, the mean reported

Averages some noise

A lone mean hides how wide the spread is

Five stratified folds, mean ± std, hold-out scored once to confirm

Shows the uncertainty; a single draw cannot overturn it

Five fits per model, and a two-column result to explain


Final Decision

  • Why this option won: every headline is five-fold stratified cross-validation at seed 42, mean and standard deviation across folds, with the standard deviation the unit, and the five per-fold values persisted beside it. The 20 percent hold-out, 12,800 rows on Hillstrom and 200,000 on Criteo, is scored once per model and read as confirmation of the pick; it never sets the headline, and as coded it is not a vote in the rule.

  • Which trade-offs were accepted: five fits per model, and a headline that is a band rather than a number.

  • Which assumptions were made: a gap smaller than the larger of the two bands is a tie. That is a one-standard-deviation heuristic, not a test, and the paired test in the Results deep-dive agrees with it: no pair on either dataset separates.


Engineering Principle

On a high-variance metric the width of the band is the result, and a single split is a rumour.


Every Qini on this page is a band across five folds; the hold-out is scored once, read as confirmation, never promoted to the headline.


Decision 4: Hand-roll the methods over LightGBM and NumPy


Context

Three meta-learners, an uplift tree and forest, and a class transformation were needed. scikit-uplift 0.5.1 ships no tree or forest, and the causal libraries that do carry heavy builds on Python 3.12.


Alternatives Considered

Option

Advantages

Disadvantages

causalml or econml wholesale

Every method ready

Heavy dependency trees with build risk on Python 3.12, and the method being judged is a black box

scikit-uplift's own models

Already installed

No tree or forest at all, and its class transformation assumes 50/50 arms, which is biased on 2 to 1 and 85 to 15 data

Hand-roll every method over LightGBM and NumPy

A small dependency set, and every method readable and unit-tested

More code, and the tree needed its own proof before it could join the board


Final Decision

  • Why this option won: the meta-learners are thin wrappers over the LightGBM already installed. The class transformation regresses the propensity-corrected outcome Y·(T − p)/(p·(1 − p)), tested unbiased against the plain variant on unequal arms. The tree splits histogram-binned features on a Euclidean divergence criterion with a per-arm minimum in every leaf; the forest bags 50 such trees with a random feature subset per split. Both joined the leaderboard only after recovering an effect set in synthetic data.

  • Which trade-offs were accepted: no TreeSHAP for the forest, which the second challenge pays for, and a 150,000-row bootstrap cap per tree on Criteo to keep pure-NumPy fits fast.

  • Which assumptions were made: on randomised data the propensity is the constant treated fraction, which the X-learner blend and the transformation both use.


Engineering Principle

Own the logic you are judged on; a library method encodes assumptions, and the one checked here encoded the wrong arm ratio.


Every method on the leaderboard is code that can be read and tested, and the library shortcut checked would have been biased on both datasets.


Decision 5: Break the tie by the rule written first, not the higher mean


Context

On Criteo two models cleared the bar on the cross-validated mean: the S-learner at +0.0927 ± 0.0110 and the uplift forest at +0.0905 ± 0.0067. The gap, 0.0022, is a fraction of either band.


Alternatives Considered

Option

Advantages

Disadvantages

The higher cross-validated mean, the S-learner

The simplest rule, and the interpretable model, readable by native TreeSHAP

The gap sits inside the noise, so the mean is not a lead

Stability first, then interpretability, the forest

Rewards the model that survives a new sample

Loses the easier model to explain


Final Decision

  • Why this option won: the rule written before the numbers breaks ties on the tighter band first, then on interpretability. On the seed the rule ran on, the forest's standard deviation is 0.0067 against 0.0110, and the hold-out confirms the pick: the forest held +0.0936 while the S-learner fell to +0.0806, a drop of 0.0121 that only the X-learner's 0.0211 exceeds.

  • Which trade-offs were accepted: the band-width reason did not survive more draws. Two standard deviations from five folds are themselves noisy, and on twenty folds across four seeds the forest's band widens to ± 0.0081 while the S-learner leads it on 14 of 20, so the 200,000-row hold-out is what makes the pick credible after the fact, and the band is at most corroboration. The S-learner is recorded as the interpretable near-tie and the stronger cross-validated model; serving kept the forest for consistency with the recorded verdict and its hold-out, and the next re-tag should revisit that.

  • Which assumptions were made: the tie rule of Decision 3.


Engineering Principle

When two results are a statistical tie, the higher mean is not a winner; break the tie on the property that survives a new sample.


Among the uplift models the forest ships because its hold-out held; its tighter band was the rule's reason and did not survive more draws.


Decision 6: Price the policy as a range


Context

A ranking is half a decision. The other half is money, and money depends on two inputs no dataset carries.


Alternatives Considered

Option

Advantages

Disadvantages

One dollar figure

Quotable

Fabricates precision the inputs do not have

Incremental conversions only

Fully measured

Stops short of the decision a budget owner needs

Measured conversions plus a swept value band

Separates the measured from the assumed

Two numbers to explain instead of one


Final Decision

  • Why this option won: incremental conversions come off the conversion Qini curve with no assumption. Value is swept over $50 to $150 per conversion, anchored on the $116.36 mean spend among Hillstrom's 578 converters, against $0.05 to $0.50 per contact, and every figure ships beside those inputs.

  • Which trade-offs were accepted: the contact cost is notional, chosen so a budget bites; real email is near free, which would make treat-everyone win by default. The sign of the value band follows from that choice: at near-free contact the policy is positive throughout and the ranking is moot, which is the point, since ranking only matters when a contact costs something, as an impression does on Criteo.

  • Which assumptions were made: the two ranges themselves, kept as documented constants rather than buried in a function.


Engineering Principle

Separate the measured from the assumed, and let the assumption travel as a range.


The conversions are measured, the dollars are a swept band, and the assumptions print beside every figure.


Implementation Highlights


The decisions above set the rules; this is where each is enforced, and where the pipeline was checked against itself.


Go deeper: the audited harness, bundle contract and two live bugs

  • The ruler audited first: Qini and AUUC are a from-scratch implementation asserted equal to scikit-uplift's to machine precision, and the 141 tests across 16 files lead with the evaluation math and the data transforms, because a metric bug corrupts every decision at once where a model bug corrupts one score. They run on every push in continuous integration.

  • Per-fold scores kept: the leaderboard CSV carries each model's five fold-level Qini values beside the mean and standard deviation, so two models can be compared on the same five draws rather than through two summary bands.

  • Randomisation checked, not trusted: a standardised mean difference per covariate, categoricals one level at a time, 18 checks on Hillstrom and 12 on Criteo; the worst is 0.0153 on Hillstrom and 0.0518 on Criteo against a flag at 0.1, so nothing flagged.

  • Stratified where it matters: folds and the hold-out stratify on treatment by outcome; the Criteo subsample stratifies on treatment by conversion so the rare positives survive the draw.

  • Baselines reproduced to the digit: the two baselines were re-scored in each modelling phase and matched their earlier numbers exactly, a machinery check that the folds and the metric had not moved.

  • Dependencies staged: the three modelling phases added nothing; serving added fastapi, uvicorn and httpx; the demo added streamlit; shap was never added.

  • The bundle fits on dev and prices on hold-out: the shipped model is fitted on the development split and the value band is projected on the untouched 20 percent, so the reference the dollars are read off was never trained on. The sanity check reproduces the harness to the digit: a $100 Hillstrom budget prices at $836, band −$98 to $1,157.

  • One policy input at a time: /policy takes exactly one of budget, count or fraction, with optional value and cost overrides, and the value band always sweeps the documented ranges. The Hillstrom score is labelled response_probability with honest_negative set true, so the product does not pretend uplift where uplift lost.

  • Two bugs found in the browser: matplotlib's GUI backend segfaulted, exit 139, on Streamlit's worker thread under rapid slider reruns, fixed by forcing Agg before pyplot loads; a dollar caption rendered as LaTeX until the $ was escaped.

  • The bundle rides in the repository: 6.2 MB of public data, committed as a documented gitignore exception, because Criteo's source download is dead and the cloud cannot rebuild it. Each bundle is stamped with the uplift, lightgbm and numpy versions that built it, and loading under different versions prints a warning naming both, so a dependency bump cannot move a score silently.

  • Wall time: the full selection board runs in about 88 seconds on Hillstrom and 198 on Criteo, the operating-point read in 4 and 14; persisting the bundles takes about 1.5 and 13 seconds, as measured when the serving layer was built.


The baselines reproduced to the digit across phases, every per-fold score is kept, and the priced reference was never trained on.


Challenges and Debugging


Challenge: the hold-out contradicted the cross-validation

  • What failed: on both datasets the single hold-out reversed the cross-validated order. On Hillstrom the uplift forest posted +0.0661 against the response model's +0.0187; on Criteo the response model posted +0.0973, above the forest's +0.0936 and the S-learner's +0.0806.

  • Lesson: when a single draw disagrees with a cross-validated band, the band wins; the disagreement is the reason the band exists.


Challenge: the planned explainer would not build, and would not fit

  • What failed: SHAP was planned. Its numba dependency cannot build against numpy 2.5.1 under Python 3.12, and the chosen model is a NumPy forest that no tree explainer reads.

  • Lesson: choose the explainer that fits the model chosen, not the one with the familiar name.

Go deeper: how each was traced and what was done

  • Hold-out, investigation: instead of asking which split is right, the swings were compared with the fold-to-fold spread. On Hillstrom every uplift model's standard deviation already exceeded its mean, so a 12,800-row draw of that metric was expected to swing either way.

  • Hold-out, root cause: not a bug. Qini is high-variance, and one split is one sample from a wide distribution.

  • Hold-out, resolution: the band was the arbiter by design: negative on Hillstrom, a slim and split-sensitive positive on Criteo, and the disagreement recorded as evidence for banding rather than a result to reconcile away.

  • Explainer, investigation: LightGBM computes exact TreeSHAP itself through pred_contrib, the algorithm shap.TreeExplainer delegates to, so a LightGBM winner needs no package; for the S-learner that is the T equals 1 minus T equals 0 contribution difference with the treatment column dropped. None of it reads the forest.

  • Explainer, root cause: the explainer was chosen before the winner was.

  • Explainer, resolution: permutation importance: shuffle a feature five times, measure the mean Qini drop and its spread. f0 costs 0.0049 ± 0.0050, f3 0.0022 ± 0.0037; f1 is exactly unused; f5 and f11 are slightly negative, f11 inside its own noise and f5 just outside it. f0 leads on the mean and the ordering below it is inside the bands. The features are anonymised, so the reading is which signal, not what it means.


Neither challenge was a bug; both were the metric and the model behaving as their variance and their construction predict.


Results and Evaluation


Every number here, and every tile in Figure 2, comes from one configuration. The board is five-fold stratified cross-validation at seed 42 on the development split, 51,200 rows on Hillstrom and 800,000 on Criteo, with normalised Qini on the primary outcome visit as mean and standard deviation across folds. The hold-out, 12,800 and 200,000 rows, is scored once. AUUC tracks Qini throughout and sits in the deep-dive.


Does uplift beat the classifier on 64,000 customers? No

Model

Qini, five-fold CV

Qini, hold-out

treat_everyone

+0.0000 ± 0.0000

+0.0000

response_model, shipped

+0.0197 ± 0.0170

+0.0187

s_learner

+0.0088 ± 0.0174

+0.0410

t_learner

+0.0134 ± 0.0214

+0.0282

x_learner

+0.0085 ± 0.0168

+0.0505

class_transformation

+0.0061 ± 0.0167

+0.0469

uplift_tree

+0.0017 ± 0.0221

+0.0417

uplift_forest

+0.0129 ± 0.0155

+0.0661


No uplift model clears the bar on the mean, and every one has a standard deviation larger than its mean (Figure 4); the baseline's own band overlaps all six, so nothing here is distinguishable from anything, and the rule defaults to the response model rather than crowning it.


The hold-out column flips the order, and at the operating point the disagreement is sharper than the whole-curve numbers suggest: in the top 10 percent of the 12,800-row draw the forest's decile carries +0.151 visit uplift against the baseline's +0.074, with standard errors near 0.02. One draw does not overturn five, and the rule was written before any of them, so the chosen model is the response model, labelled a negative: on this much data, do not deploy an uplift model, and do not read the hold-out as proof that one would work. The disagreement is why Limitations asks for more draws.


On a million rows? A tie

Model

Qini, five-fold CV

Qini, hold-out

treat_everyone

+0.0000 ± 0.0000

+0.0000

response_model, the baseline

+0.0877 ± 0.0074

+0.0973

s_learner

+0.0927 ± 0.0110

+0.0806

t_learner

+0.0703 ± 0.0168

+0.0626

x_learner

+0.0729 ± 0.0145

+0.0518

class_transformation

+0.0776 ± 0.0049

+0.0810

uplift_tree

+0.0784 ± 0.0055

+0.0876

uplift_forest, shipped

+0.0905 ± 0.0067

+0.0936


A bar chart of normalised cross-validated Qini for eight models on Criteo, with a dashed line at the response model’s +0.0877. The S-learner at +0.0927 and the uplift forest at +0.0905 stand just above the line with error bars that cross it; the T-learner, X-learner, class transformation and uplift tree sit below it, the first two with wide bars.
Figure 5. On a million rows uplift draws level and no further: two of six models clear the bar on the mean, both bands overlap it, and the forest is chosen on the hold-out and the band it had on this seed, not on a lead. On fourteen million rows, below, the same two pull clear.

Two of six uplift models clear the bar on the mean, by 0.0050 and 0.0028, and both bands overlap it (Figure 5). T and X, the two learners that fit a separate model on the 150,000-row control arm, land under the bar, while the S-learner pools both arms and the forest splits on uplift directly; the base-learner swap in the deep-dive supports that reading indirectly. On the single hold-out draw the baseline scores highest at +0.0973, so the edge cross-validation finds does not reproduce out of sample on the whole curve. The honest reading is a tie, with the forest its most stable member on the seed the rule was applied to; where a budget would break the tie is the operating point, below.


On all fourteen million? Narrowly, yes

The subsample is a choice, so the three models the verdict turns on were re-run on the whole Criteo file under the same protocol: 11,183,673 development rows in five stratified folds, a 2,795,919-row hold-out scored once, balance still passing with a worst standardised mean difference of 0.049.

Model

Qini, five-fold CV, all rows

Qini, hold-out

Folds above the bar

response_model, the baseline

+0.0846 ± 0.0013

+0.0844

not applicable

s_learner

+0.0899 ± 0.0021

+0.0893

5 of 5

uplift_forest, shipped

+0.0881 ± 0.0016

+0.0887

5 of 5


Fourteen times the rows shrink the bands about five-fold, and the picture resolves. Both uplift models clear the baseline on every fold, by 0.0035 for the forest and 0.0053 for the S-learner, p below 0.001 for each on a paired test, and by about the same on the hold-out. The edge is small, four to six percent of the baseline's Qini. It is no longer inside the noise. The S-learner against the forest stays a tie (gap 0.0018, p 0.07), and the rule re-applied returns the forest on the tighter band. The full field, the bundle and the demo remain on the one-million-row subsample; the scale check is what turns "draws level" into "pulls clear, narrowly".


The ordering of the designs is the base learner's, not the data's: re-run over a linear base on the same folds, the T-learner, last under LightGBM, is the best design on Criteo and the S-learner falls to the bar, while the small edge over the response model survives both learners. The full swap is in the deep-dive.


Does the top of the ranking respond more?


Paired bars of observed visit uplift, treated rate minus control rate, per decile of predicted score on the 200,000-row Criteo hold-out, the forest’s ranking beside the response model’s, each bar with a standard-error whisker. In the first decile the forest stands near 0.072 and the response model near 0.063, whiskers overlapping; in decile two the response model’s bar is the taller at about 0.012; decile three is near 0.008 for the forest; the rest sit at zero, with a small forest bar of about 0.003 returning at decile ten.
Figure 6. The ranking puts the effect where the budget goes: about seven points of observed visit uplift in the forest's top decile against a 1.08-point population average, roughly sixfold, with the response model's top decile at 6.3 points and inside the same whisker, and near zero below either.

Figure 6 matters more than the aggregate Qini, because the policy spends from the top of the list. The profile is not perfectly monotone; the small return at decile ten is the noise of estimating a one-point effect on a 4.72 percent base rate. The response model's top decile sits inside the same whisker, which is why the operating point, not this chart, decides anything.


Where the budget spends, which ranking wins?

The coefficient scores the whole curve; a budget acts on its head. Figure 7 reads the two hold-out curves there: through the first tenth they run together, the forest ahead by 159 visits at the cut, and the baseline's higher coefficient is earned past 40 percent, where no budget goes.


Two hold-out Qini curves on Criteo, cumulative incremental visits against the fraction targeted, with the random diagonal dotted and a dashed vertical at 10 percent. The forest and the response model run together through the first 40 percent, the forest ahead at the 10 percent mark with 1,240 incremental visits against 1,081; past 40 percent the response model stays a little above until both meet at the right edge. The legend carries each curve’s hold-out Qini, +0.0936 and +0.0973.
Figure 7. The coefficient is earned where no budget goes: the two rankings run together through the first tenth, and the baseline's lead lives in the tail.

A committed script reads both rankings at that operating point on the same 200,000-row draw:

Ranking, top 10 percent of the Criteo hold-out

Top-decile visit uplift

Incremental visits, share of treat-everyone

Incremental conversions

Net value, point

Net value, band

uplift forest, shipped

+0.072 ± 0.008

1,240, 68 percent

+23.8 ± 52

$767

−$8,811 to $2,567

response model, the baseline

+0.063 ± 0.010

1,081, 59 percent

+44.2 ± 54

$3,147

−$7,788 to $5,636


The ± here is a standard error on one draw, not a fold band. On visits, the outcome the forest was fitted on, it captures more at the operating point: 1,240 against 1,081 incremental visits, 68 against 59 percent of what treating everyone captures, and a top decile of 7.2 against 6.3 points, a lead inside one standard error. On the priced outcome the point favours the baseline and neither number carries much: +23.8 is 389 treated conversions minus 57 control conversions scaled by the 6.4 to 1 arm ratio inside the top 20,000, so one control conversion moves it by about six, and both standard errors exceed their estimates.


So the forest ships for two reasons that survive the tie, and neither is that it beat the baseline. At the operating point it is level or ahead on visits, the outcome it was fitted on. And its score has the units of the decision: an uplift can be thresholded at zero, and the serving checks show the near-zero and negative scores as the customers the forest would not contact, which a response probability cannot express. On this data the response model would spend the same 10 percent nearly as well, and the page says so.


What drives the ranking

Figure 8 names the forest's drivers by permutation: shuffle one feature five times and read the mean drop in hold-out Qini with its spread.


A horizontal bar chart of permutation importance for the Criteo uplift forest, measured as the mean drop in Qini over five shuffles of one feature, each bar with its five-shuffle error bar. f0 leads at 0.0049 with a bar of 0.0050, then f3 at 0.0022, f6 at 0.0019, f10 at 0.0017 and f8 at 0.0016, their bars overlapping; f9, f4, f2 and f7 are small; f1 is exactly zero; f11 and f5 sit slightly below zero, f11’s bar crossing it and f5’s not.
Figure 8. Which signal, not what it means: f0 leads on the mean and only on the mean, f1 is unused, and every ordering below the top is inside the five-shuffle bands.

What is the policy worth?

Figure 9 is that policy live: targeting the top 10 percent of the Criteo hold-out by predicted uplift spends $2,000 at $0.10 a contact on 20,000 customers for +23.8 incremental conversions, $767 at $116.36 per conversion, inside the value band of the table above. The value band crosses zero for two reasons a budget owner should see side by side: the contact cost is notional and chosen so the budget bites, and the conversion count behind the point is thinner than its own sampling error. That range, not one reassuring figure, is the message.


The live demo’s Targeting Policy block on Criteo. A slider at 10 percent of customers drives four tiles: 20,000 targeted, $2,000 spent, +23.8 incremental conversions, and $767 net with the value band −8,811 to 2,567 beside it; a footnote names the swept and point assumptions; below, a net-value curve against the fraction targeted is positive over roughly the first third and negative beyond.
Figure 9. The policy, repriced from a slider: the count, the spend, the conversions and the value band are computed live off the bundle's untouched hold-out reference, and the value band moves with the inputs.

Go deeper: AUUC, the base-learner swap, drivers and serving checks

Dataset

Model

AUUC, five-fold CV

AUUC, hold-out

Hillstrom

response_model

+0.0111 ± 0.0099

+0.0112

Hillstrom

uplift_forest

+0.0072 ± 0.0090

+0.0395

Criteo

response_model

+0.0345 ± 0.0030

+0.0383

Criteo

s_learner

+0.0364 ± 0.0044

+0.0316

Criteo

uplift_forest

+0.0355 ± 0.0027

+0.0367

Feature

Qini drop when shuffled, mean ± std over five shuffles

f0

0.0049 ± 0.0050

f3

0.0022 ± 0.0037

f6

0.0019 ± 0.0042

f10

0.0017 ± 0.0012

f8

0.0016 ± 0.0018

f9, f4, f2

0.0008 ± 0.0013, 0.0007 ± 0.0005, 0.0005 ± 0.0011

f7

0.0001 ± 0.0003

f1

0.0000 ± 0.0000, unused

f11, f5

−0.0007 ± 0.0009, −0.0009 ± 0.0006


The base-learner swap. Every meta-learner and the response model share one LightGBM parameter set, so the same four designs were re-run over a deliberately simple base, a linear model over one-hot categoricals and scaled numerics, on the same folds. On Criteo the T-learner, the worst design under LightGBM at +0.0703 ± 0.0168, is the best under the linear base at +0.0960 ± 0.0123 against a linear response bar of +0.0840 ± 0.0074, and the S-learner falls to that bar. On Hillstrom nothing separates under either base. The ranking of designs is a fact about the learner; what survives both learners is that uplift's edge over the response model is small on a million rows. The swap also says something about why T and X lose under LightGBM: a twelve-parameter linear model fitted on the 150,000-row control arm does not overfit it, and a 200-tree booster can.


The two baselines reproduced their earlier phase numbers to the digit on the final board, and the whole board reproduced again when it was re-run to persist the per-fold values. Read fold by fold on Criteo, the forest beats the response model on 3 of 5 folds (by −0.0005, −0.0056, +0.0136, +0.0056 and +0.0010), the S-learner beats it on 4 of 5, and the S-learner beats the forest on 4 of 5; on Hillstrom the forest beats the response model on 2 of 5 and the S-learner on 1 of 5. Read as paired t-tests on those five folds, no pair separates: forest against response p 0.43 on Criteo and 0.38 on Hillstrom, S-learner against response 0.37 and 0.40, S-learner against forest 0.36 and 0.51. Re-splitting the development rows under three more seeds and pooling with seed 42 gives twenty folds per model: on Criteo the response model sits at +0.0876 ± 0.0055, the S-learner at +0.0922 ± 0.0099 and the forest at +0.0889 ± 0.0081, the forest ahead of the bar on 13 of 20 folds and the S-learner ahead of the forest on 14 of 20; on Hillstrom the response model sits at +0.0194 ± 0.0166, the forest at +0.0118 ± 0.0126 and the S-learner at +0.0098 ± 0.0141, the baseline ahead of the forest on 14 of 20. The rule re-applied to the pooled folds returns the same two verdicts, by a narrower margin. The paired read says what the bands say: nothing on Hillstrom, a slim and unsteady lead on Criteo, and a tie-break on band width that the hold-out, not the folds, makes credible. Serving was verified live: /health lists both models; on 2,000 Criteo hold-out rows /score returned 847 distinct values in the range −0.004 to 0.147, the near-zero and slightly negative scores being the customers the forest would not contact; /policy at a $100 budget priced Criteo at +19.2 incremental conversions and $2,138 with a band of $462 to $2,835, and Hillstrom at $836 with a band of −$98 to $1,157, carrying the honest-negative note. The hosted demo reproduced the phase 8 hold-out Qini on both datasets, +0.0936 on Criteo and +0.0187 on Hillstrom.


The fold band overlaps the baseline at a million rows, the simpler model wins at 64,000, and the forest ships on the operating point.


Business Impact


Never deployed, on anonymised public experiments, so no return is booked; the product is a decision framework and a measured boundary on when to pay for it.

  • The decision it enables: rank by incremental effect and cut at a budget, so spend goes to the customers a contact moves rather than the ones a response model rewards for converting anyway; on Criteo the forest's top decile carries about seven points of observed uplift against a 1.08-point average, with the response model's top decile inside the same whisker.

  • What the money looks like: a value band under stated inputs, never a figure: the top 10 percent of the Criteo hold-out prices at $767 inside −$8,811 to $2,567, with a sampling error on the conversions larger than the point, and the response model prices higher on the same draw inside the same noise.

  • What the negative is worth: on 64,000 customers nothing is distinguishable from the simpler model, so the right call is to skip the uplift model and target likely responders until more data says otherwise; knowing that before the build is the cheapest outcome such a project can have.

  • Where the edge is: on a million rows the forest is level with the baseline inside the fold bands and trails it on the hold-out; on all fourteen million it clears the baseline on every fold, by four to six percent of its Qini. The fundable claim is "level at a million rows, clear narrowly at fourteen", never "beats".


The product is a rule for when a complex model earns deployment: no at 64,000 customers, barely at a million, narrowly yes at fourteen million.


Limitations


An independent build on two public randomised experiments; the table names what that data cannot establish.

Category

What remains unsolved

Why

What would be required

Data

Two datasets that differ in size, domain, base rate and effect size at once, and no clear sleeping-dog signal

Everything varies between the two experiments, so "uplift needs scale" is the leading reading, not a proven cause; only 4 of Criteo's 120 feature-decile cells dip below zero

More experiments that vary one thing at a time, and data where a treatment measurably harms some customers

Evaluation

No per-row label, and a metric that disagreed with itself across splits

Every number is aggregate; on both datasets the single hold-out reversed the cross-validated order, and twenty folds across four seeds did not separate any pair

A larger hold-out, or a second hold-out drawn under another seed, since more folds did not settle it

Selection

A tie rule that is a heuristic, and a tie-break that did not survive more draws

The tie rule is a one-standard-deviation heuristic, and on twenty folds the forest's band is no longer the tighter one, so the Criteo pick rests on one hold-out draw and a verdict recorded first

Re-apply the rule at the next re-tag with the S-learner, the stronger cross-validated and the interpretable model, as the candidate

Modelling

The ranking of designs depends on the base learner

Under LightGBM the S-learner leads and the T-learner is last; under a linear base the order reverses; only the size of the edge over the baseline survives both

A board that crosses designs with base learners, and a tuned linear base rather than a default one

Deployment

Never deployed; the projection is offline

Acting on the policy changes who is targeted, so realised value is not measured by this harness; the bundles are stamped and warn on a version mismatch, but they remain pickles tied to those versions

The online loop, which is the next system rather than this one

Scale

The full field, the bundle and the demo sit on a one-million-row subsample of 13,979,592

The subsample kept each phase tractable, but the edge resolved only on all rows, so the shipped bundle is fitted on the split where the edge is still inside the noise

The full field, the bundle and the demo refitted on all rows

Assumptions

Value per conversion and cost per contact

$116.36 rests on 578 right-skewed orders with a median of $80.79; the contact cost is notional so the budget bites

Real cost and value inputs from the business

Explainability

Which feature, not what it means

Criteo's twelve features are anonymised floats, and the forest is read by permutation importance because no tree explainer parses it

Named features, or the S-learner near-tie with LightGBM's native TreeSHAP


Every number holds for these two experiments and these exact treatments; the ones a real deployment would change are named here, not hidden.


Lessons Learned


Lesson 1: Measure the decision, not the metric the data can afford

  • Context: the familiar metric, a per-row error, does not exist here, and the next, response AUC, answers the wrong question.

  • Engineering principle: choose the metric that mirrors the operational decision, here the ranking the budget follows, even when it is noisier and harder to summarise.

  • Generalization: a metric chosen out of habit measures what is convenient; one chosen from the decision measures what will be acted on.


Lesson 2: A model is only as good as the cheapest thing it beats

  • Context: making the naive baselines first-class competitors turned "uplift did not help on 64,000 customers" from a footnote into the headline.

  • Engineering principle: build the trivial baseline first and make it the thing to defeat; if the complex model cannot, that is the finding.

  • Generalization: the credibility of a positive result is set by how visibly the negative one would have been reported.


Lesson 3: On a noisy metric, the band is the result

  • Context: every disagreement between the folds and the hold-out resolved the same way, and the S-learner and forest tie was broken on the hold-out and the band, not on a decimal inside it. A standard error turned the dollar point into a number smaller than its own noise, and fourteen times the rows shrank the band five-fold, which is what let the Criteo edge be read at all.

  • Engineering principle: report the uncertainty as the primary quantity, score the hold-out once, and put a standard error on any number a single draw produces.

  • Generalization: a point estimate on a high-variance metric is a headline waiting to be wrong.


Lesson 4: Score the curve where the budget spends

  • Context: the response model's higher hold-out coefficient on Criteo was earned past 40 percent of the population, where no budget goes; through the first tenth the two rankings coincide, and on the fitted outcome the forest is ahead there.

  • Engineering principle: when a policy acts on the head of a ranking, evaluate the head; a whole-range summary can rank models by the part of the range nobody uses.

  • Generalization: the metric that decides deployment is the one read at the operating point, and it can disagree with the metric that decides the leaderboard.


Future Improvements


Go deeper: the five roadmap extensions, and the stack

  • Observational causal inference: propensity weighting and backdoor adjustment, to lift the randomised-data restriction.

  • Multi-arm and continuous treatments: including the R-learner left optional in v1, to move past one binary arm.

  • The full field on all rows: the scale check covers three models on the full Criteo file; the other five, the bundle and the demo still sit on the subsample.

  • Uplift on the conversion outcome: the rarer signal, where the value machinery matters more and the bands will be wider.

  • Designs crossed with base learners: the linear swap showed the ordering of designs is the learner's; a tuned linear base and the direct models under a second learner would show how much. The T/X control-arm reading, supported indirectly by the swap, still wants a direct test.


Technology Stack


Category

Technologies

Machine learning

LightGBM base learner; hand-rolled S, T and X meta-learners, uplift tree and forest, and class transformation over NumPy; scikit-uplift for loaders and reference metrics

Evaluation

From-scratch Qini, AUUC, decile and incremental-value harness; permutation importance

Backend

FastAPI and Uvicorn, /score, /policy, /health; stdlib pickle bundles

Demo

Streamlit and matplotlib, in process over the library

Environment

Python 3.12, uv with a lockfile, Ruff, pytest

Deployment

Streamlit Community Cloud, free tier



References


Resource

Link

GitHub

Live demo

Documentation

Datasets

Hillstrom MineThatData Email Challenge; Criteo Uplift Prediction; both under their own terms, Hillstrom loaded through scikit-uplift


bottom of page