learn-causal-inference-with-phoebe / Practitioner session 8 of 10
Learn Causal Inference with Phoebe · Practitioner session 8 of 10

Qini curves and who to treat

Session 7 ended with a number per customer: a predicted effect of the re-engagement email on that person's spend. A number per customer is not yet a decision. Kestrelmark can send the email to everybody, to nobody, or to a chosen share, and the choice is worth real money: on a held-out set of 1,500 customers, treating everybody earns 9,729 units of incremental spend, while stopping at the ninth decile of an uplift ranking earns 10,698. Tonight you build the curve that finds the stopping point, and you watch a ranking by likelihood to buy climb all the way to the same endpoint without ever beating it.

🔴 Hardest Data scientists and analysts Whoever owns the campaign budget 45 min
0-3 · Welcome 3-22 · Ranking by uplift, not by likelihood 22-40 · Build-along: build a Qini curve 40-45 · Q&A
Part 0

What a per-customer effect is for

A predicted effect per customer only earns its keep once it changes who gets treated. Between the model and the send list sits one question that has a right answer and is almost never asked out loud: how far down the ranked list should the campaign go? The Qini curve answers it from data rather than from a round number somebody liked.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. A Qini curve built from a held-out split and a real ranked list, read the way an ROC curve is read. A single number, the Qini coefficient, that says how much of the available gain your ranking captures: 0.94 for Kestrelmark's uplift ranking against 0.53 for a likelihood-to-buy ranking of the same customers. And the finding that actually changes the campaign, which is not that number at all: only the uplift curve has a peak before 100 percent, and stopping there is worth about 1,000 units against a blanket send.

LiveThe number session 7 produced, and the shape of the data underneath it3 min▶

The S-, T- and X-learners of session 7 each return the same object: an estimated individual effect, one row per customer, in the units of the outcome. For Kestrelmark that is extra spend caused by receiving the email. Across all 5,000 customers the true average effect is 6.23, and the distribution around that average is the whole point, because 6.23 is an average of numbers that are mostly not 6.23.

Leader session 5 splits the same population four ways, and in this data the split has a name in the columns: recency. Sure things, still active, are 22.4 percent of the base and the email is worth +2.10 to them. Persuadables, lapsed 30 to 90 days, are 20.4 percent and worth +29.09. Lost causes, dormant a year, are 49.3 percent and worth +1.04. Sleeping dogs, customers who cancelled once already, are 7.9 percent, and the email costs them 8.87 units each.

Session 7 measured how far that goes: recency alone explains the individual effect with an R-squared of 0.986. Which is to say the group is recoverable and the person is not, because inside every group there is still a 3.0 coin flip that nobody can predict. Tonight only needs the group, which is fortunate, because the group is all any model can give you.

Real world

Kestrelmark's campaign tool ships with one targeting control: a slider labelled "propensity to purchase", top N percent. Nobody at the company chose that control, it came with the platform, and it ranks on the wrong quantity in a way that is invisible from inside the tool. The first hour of tonight is about seeing it from outside.

Self-studyThe held-out split, and why the curve needs one3 min▶
  • 3,500 customers to fit, 1,500 to score. The meta-learner from session 7 never sees the 1,500. Every number tonight comes from the held-out block.
  • Both arms live in the held-out block. The curve compares treated against untreated customers inside each slice of the ranking, so a slice with no controls in it produces nothing.
  • The ranking is predicted, the response is actual. Order the held-out customers by predicted uplift; measure the gain from what those customers actually did. A curve built from predictions on both axes is a picture of the model agreeing with itself.
  • The truth is available here and nowhere else. Kestrelmark comes from a written structural model, so tonight's curve can be checked against a known answer. Your own curve cannot be, which is the reason the held-out discipline is not optional.
Part 1 · covers the two rankings and why they disagree

The most common targeting mistake is ranking by the wrong thing

10 min live

A likelihood-to-buy model answers "who is going to spend". An uplift model answers "whose spend does the email change". Those are different questions, they produce different orderings of the same customers, and a campaign budget spent against the first one buys mostly things that were going to happen anyway.

Effect of the re-engagement email on spend, by recency segment One number per group, 5,000 customers. The average of all four is 6.23. 0 7.00, the fully loaded cost of one send Sure thing still active · 22.4% of the base +2.10 Persuadable lapsed 30-90 days · 20.4% +29.09 Lost cause dormant 12 months · 49.3% +1.04 Sleeping dog cancelled once · 7.9% -8.87 One group in four clears the cost of a send. One is harmed by the campaign outright.
🔍 Click to zoom - four groups, four very different answers to the same email.
LiveTwo models, two questions, two lists4 min▶

A likelihood-to-buy model is fit on outcomes. It learns that customers with high tenure, recent activity and a full basket tend to spend, and it ranks them first. Nothing in that fitting procedure ever sees a comparison between a treated customer and an untreated one, so nothing in the resulting score is about the email at all. It is a good model of who spends and it is silent on the question being asked.

An uplift model is fit on the difference between arms. Whatever the meta-learner does internally, the quantity it targets is the change in spend caused by the email for a customer with those characteristics. The two scores are related only through whatever accidental overlap exists between "spends a lot" and "responds to a nudge", and at Kestrelmark that overlap is weak enough to be worth 0.94 against 0.53 on the scale Part 2 builds.

A naming collision worth clearing up. The word propensity is used for two unrelated things in this area. A campaign tool's "propensity to purchase" score is the likelihood-to-buy model above. A propensity score in the causal literature is the probability of being treated, used inside estimators that correct an observational comparison, and that family of methods lives in learn-experimentation-with-phoebe. Neither one ranks customers by uplift, and the shared word has cost more than one campaign its budget.

LiveWhat a likelihood ranking buys, and what it misses4 min▶

Send down a likelihood-to-buy list and the money goes to sure things. These are customers who were going to purchase whether or not the email arrived, so the discount, the send cost and the attention are spent to produce an outcome that was already coming. The campaign report will look excellent, because the emailed group really did spend a lot, and every unit of that spend belongs to the customer rather than to the campaign. Session 1 priced exactly this confusion at Kestrelmark: 10.54 reported against 6.23 real.

  • Sure things get the budget. 22.4 percent of the population, high baseline spend, an effect of +2.10. They are at the top of the likelihood list by construction.
  • Persuadables carry the business case. 20.4 percent of the population at +29.09 each, an order of magnitude above every other group. Many of them are low-baseline customers precisely because they are drifting, which is what puts them low on a likelihood ranking.
  • Lost causes waste less than they look. Half the population, 49.3 percent, at +1.04. They are cheap mistakes: the send cost, and almost nothing else.

The arithmetic is unforgiving, and Part 2 measures it. At the ninth decile the uplift ranking has banked 10,698 units of incremental spend on the held-out block. At the same send volume the likelihood ranking has 9,644, and it has nowhere left to go but the blanket total.

LiveThe fourth group, and why a likelihood model targets it happily3 min▶

Sleeping dogs are the customers a campaign actively drives away. 7.9 percent of Kestrelmark's base, at an effect of -8.87 each: the email reminds a semi-dormant subscriber that the subscription exists, and they cancel, downgrade, or simply stop drifting pleasantly along. Their existence is not exotic. Any contact that carries a cancel link, a price, or a reminder of a commitment has a group like this.

The dangerous part is that sleeping dogs look engaged. They have history, they have spend, they open things. A likelihood-to-buy model reads that history, scores them high, and puts them near the top of the send list, which is the worst place they could be. On the held-out block, 111 of the 1,500 customers are genuinely harmed by the email, and no report built on outcomes alone will ever surface them.

Real world

The trap is that the average survives. Kestrelmark's true average effect is +6.23, comfortably positive, and on that number alone the campaign is worth running. A positive average is fully compatible with a subgroup being harmed, and the average is the only number most campaign reports contain.

Self-studyWhen the two rankings do agree3 min▶

The two lists are not always almost disjoint, and knowing when they converge saves the argument. They agree when the treatment effect is roughly proportional to baseline outcome, which happens with percentage-style interventions: a ten percent discount on a customer who buys more is worth more in absolute units. In that world, ranking by predicted spend is a decent proxy for ranking by uplift and the Qini curves of the two rankings sit close together.

They diverge when the effect is roughly flat in absolute units, when the treatment works by removing a barrier that high-baseline customers never had, or when a harmed subgroup is concentrated among the engaged. Kestrelmark is the second kind: the email is a reminder, reminders do nothing for people already active, and the reminder itself irritates a slice of the customers who have cancelled before. Check which world you are in before assuming either result.

Part 2 · covers the curve, the coefficient and the peak

The Qini curve, read like an ROC curve

9 min live

The Qini curve plots cumulative incremental gain against the share of the population targeted, walking down a ranking. It is read the way an ROC curve is read: the further the line sits above the diagonal, the better the targeting, and the diagonal represents random targeting. The area between the curve and the diagonal is summarised as the Qini coefficient, also written AUUC. Two rankings of the same 1,500 customers are plotted below, and the difference that matters is not the area.

Cumulative incremental gain, 1,500 held-out customers 0 3,000 6,000 9,000 12,000 0% 20% 40% 60% 80% 100% ranked by predicted uplift ranked by likelihood to buy random targeting peak at the ninth decile, 10,698 about 1,000 above a blanket send both end at 9,729 the last decile: the sleeping dogs share of the population targeted, walking down the ranking Only one of these two curves ever tells anybody to stop. Both are built from estimated uplift.
🔍 Click to zoom - two rankings, one endpoint, one peak.
LiveHow the curve is constructed, slice by slice3 min▶

Sort the held-out customers by predicted uplift, highest first. Walk down the list in deciles. At each stopping point, take everyone above it and ask what the treated ones actually earned compared with the untreated ones in the same slice, scaling the control group up to the size of the treated group so the two are comparable. That is the cumulative incremental gain at that share, and it is measured from what people did, not from what the model predicted they would do.

  • The x axis is share of the population, not a threshold. Reading it as a budget is the point: at the fourth decile you are sending to four rows in every ten of the held-out block.
  • The y axis is cumulative and in outcome units. Units of spend here, so the height is directly comparable to the cost of sending.
  • The right-hand endpoint is fixed. Target everybody and the ranking is irrelevant, so every curve on the same data must end at the same value. Kestrelmark's is 9,729 units, and both rankings arrive there, because treating everybody is the same campaign whatever order you did it in. The endpoint is a property of the campaign, not of the model.
  • The diagonal is random targeting. A random s percent of the population captures s percent of the total gain, in expectation, which is a straight line from the origin to that fixed endpoint.
LiveThe Qini coefficient, and what a peak means4 min▶

The area between the curve and the diagonal is the Qini coefficient. Larger is better, zero means the ranking is no better than shuffling the list, and negative means the ranking is actively inverted. On Kestrelmark's held-out block the uplift ranking scores 4,061 and the likelihood-to-buy ranking scores 2,290. Divided by the 4,298 a perfect ranking would achieve on the same data, those become 0.94 and 0.53, numbers between 0 and 1 that can be compared across models.

A curve that peaks and then falls is saying something specific and easy to miss. Gain accumulates while the customers being added have positive actual response. It flattens where the added customers do nothing. It falls where the added customers are harmed. So the peak is not a modelling artifact to be smoothed away: it is the recommendation. Kestrelmark's uplift curve peaks at the ninth decile, 10,698 units, and treating the last decile as well gives about 1,000 of them back, arriving at the 9,729 a blanket send would have produced. On a finer grid than deciles the true optimum is the top 92.5 percent at 10,731, which is 1,002 units above the blanket send.

The coefficient is not the finding. The peak is. 0.94 against 0.53 says the uplift ranking gets to the money sooner, and that is worth knowing. What changes the campaign is that only the uplift curve has a maximum below 100 percent. The likelihood-to-buy curve climbs the whole way and finishes at 9,729, exactly where a blanket send finishes, so it can never recommend stopping and therefore can never beat treating everybody. Ranking on the wrong quantity does not always look like failure. Here it looks like a curve that rises the whole way.

LiveOnce a send costs money, the case sharpens3 min▶

Everything above is gross incremental gain. Leader session 5 prices a Kestrelmark send at 7.00 fully loaded, and once that bar is on the page the four groups sort themselves without a curve at all.

  • Persuadables clear it, and only persuadables. +29.09 against 7.00.
  • Everybody else loses money on every send. Sure things return 2.10, lost causes 1.04, sleeping dogs -8.87. All three sit below the cost line in the figure above.
  • The blanket campaign is net negative. Run at that cost across the base it is -3,848. Sending only to the 20.4 percent persuadable segment is +22,578.

That is the same lesson as the peak, stated in money rather than in units of spend. The average effect of 6.23 is positive, the gross gain of 9,729 is positive, and the programme as it is actually run destroys value. Nothing in a campaign report built on the average can see it.

LiveWhat the curve cannot do3 min▶

The honest limits are worth more than the curve, because they are what stops it being quoted as proof.

  • It is built from estimated uplift. Every identification assumption from sessions 2 and 3 travels straight into it. If a back-door path was left open, the per-customer effects are biased and so is the ordering built from them. A beautiful Qini curve computed on a mis-specified adjustment set is a confident picture of the wrong ranking.
  • Fit and score on the same rows and it flatters itself. The model that produced the ranking has already seen those outcomes, so the top slices look better than they are. Held-out, always, and a curve reported without a stated split should be assumed to be in-sample.
  • Both arms must be present in every slice. Squeeze the deciles too fine, or rank in a way that correlates with who was treated, and some slices lose their control group. The gain in those slices is noise wearing a number.
  • The peak is estimated too. It moves between resamples, and on 1,500 rows it can move by a decile. The decile grid puts it at 90 percent and the finer grid at 92.5 percent, which is a useful reminder that it is a region rather than a point. Say so when you hand over the share.
  • Correcting a contaminated comparison is a different job. Reweighting or modelling a treated-against-control comparison to remove the remaining imbalance belongs to learn-experimentation-with-phoebe. What the graph decides is whether the comparison is correctable at all.
Self-studyReading it against an ROC curve, and where the analogy breaks3 min▶

The similarity is real and it is what makes the plot legible on sight: cumulative capture against cumulative population, a diagonal for chance, an area for a summary. An analyst who reads ROC curves needs no training to read a Qini curve, which is most of the reason the format won.

  • An ROC curve cannot go down. Every extra case adds a non-negative amount of capture. A Qini curve can, and the descent carries the most actionable information on the page.
  • The y axis is in outcome units, not a rate. That is a feature: it converts directly against a send cost, so a cost line drawn on the same axes gives a net-value optimum rather than a gross one.
  • There is no ground truth per row. An ROC curve is scored against labels that exist. No customer's individual effect is ever observed, so the curve is scored against group differences and inherits their noise.
Build-along

Build a Qini curve, then break it on purpose

★ 18 min · everyone builds

Start from the per-customer predicted effects session 7 produced. Everything below runs on the 1,500 held-out rows the meta-learner never saw.

Load the held-out block and attach the predictions. One row per customer, one column of predicted uplift from the X-learner, plus the columns that record what actually happened: whether the email was sent, and what the customer spent.

Sort descending by predicted uplift. This ordering is the model's entire contribution. Nothing about the curve depends on the magnitude of the predictions, only on the order they impose, which is worth knowing when somebody objects that the effects look poorly calibrated.

Compute cumulative actual incremental response at each decile. Inside the top k percent, sum the spend of the treated, sum the spend of the untreated, scale the second by the ratio of the group sizes, and difference. Ten stopping points, ten numbers.

Plot it against the diagonal. The diagonal runs from the origin to the last point of your own curve. Drawing it from anything else, such as a total computed on the full sample, is the most common way this plot is wrong.

Compute the Qini coefficient. Trapezoidal area under the curve, minus the area under the diagonal. Divide by the area a perfect ranking would reach if you want the normalised version that survives being compared across models.

Find the peak and read off the share. The decile with the largest cumulative gain is the recommendation. Report the share, the gain at that share, and the gain at 100 percent, so the budget owner sees what stopping early is worth.

Rebuild the same curve ranking on predicted spend, and overlay it. Identical function, different sort column. The two curves end at the same point. Check whether the second one has a maximum anywhere before that point, because if it does not, it has no recommendation in it.

Then break it. Rerun the whole thing on the rows the model was fit on. The curve rises, the coefficient rises, and nothing about the targeting got better. Keep that number next to the honest one for the rest of the week.

★ Try it now - the Qini curve, from session 7's predictions import numpy as np import pandas as pd held = pd.read_csv("kestrelmark_holdout.csv") # 1,500 rows the learner never saw held["tau_hat"] = xlearner.predict(held[FEATURES]) # per-customer effect, session 7 held["spend_hat"] = outcome_model.predict(held[FEATURES]) # likelihood-to-buy ranking def qini_points(df, score, bins=10): """Cumulative ACTUAL incremental response, walking down a PREDICTED ranking.""" d = df.sort_values(score, ascending=False).reset_index(drop=True) n, out = len(d), [] for k in range(1, bins + 1): top = d.iloc[: int(round(n * k / bins))] nt = top.email.sum() nc = len(top) - nt yt = top.loc[top.email == 1, "spend"].sum() yc = top.loc[top.email == 0, "spend"].sum() out.append((k / bins, yt - yc * (nt / nc))) # scale controls to treated size return pd.DataFrame(out, columns=["share", "gain"]) q = qini_points(held, "tau_hat") total = q.gain.iloc[-1] # 9,729 units: everybody treated, ranking irrelevant q["random"] = q.share * total # the diagonal, from YOUR endpoint q["lift"] = q.gain - q.random
★ Try it now - the coefficient, the peak, and the ranking that never peaks def qini_coef(q, total): x = np.concatenate([[0.0], q.share.values]) y = np.concatenate([[0.0], q.gain.values]) return np.trapz(y, x) - total / 2.0 # area between the curve and the diagonal perfect = 4298 # same data, effects ordered by their true value print(round(qini_coef(q, total))) # 4,061 raw print(round(qini_coef(q, total) / perfect, 2)) # 0.94 normalised peak = q.loc[q.gain.idxmax()] print(f"treat the top {peak.share:.0%}: {peak.gain:,.0f} units") # top 90%: 10,698 units print(f"blanket send: {total:,.0f} units") # 9,729 units qs = qini_points(held, "spend_hat") # the ranking most campaign tools ship with print(round(qini_coef(qs, total) / perfect, 2)) # 0.53 print(qs.loc[qs.share == 0.9, "gain"].item()) # 9,644 at the same send volume print(qs.gain.idxmax() == len(qs) - 1) # True: its maximum IS the blanket send

The ten stopping points, both rankings, on the same held-out block:

Share targetedRanked by predicted upliftRanked by likelihood to buy
10%4,3684,245
20%8,7155,792
30%9,5436,364
40%9,8497,082
50%10,0627,667
60%10,2398,069
70%10,3838,725
80%10,5309,096
90%10,6989,644
100%9,7299,729
Real world

Three readings the table gives up that the headline coefficient does not. Both columns end at 9,729, because treating everybody is the same campaign whatever order you did it in: the endpoint is a property of the campaign, not of the model, and a business that sends to everybody can never tell whether its targeting is any good. Only the uplift column has a maximum before the last row, 10,698 at 90 percent, worth about 1,000 units against the blanket send, and the reason it turns down there is that the last decile is where the sleeping dogs are. The likelihood-to-buy column rises the whole way and stops, which is what ranking on the wrong quantity looks like when it is not obviously failing: a respectable-looking climb whose best available recommendation is "send to everyone".

Homework

Before session 9

30 min
  1. Find out what your production campaign actually ranks on. Ask for the column name, not the label in the interface. If it is an outcome model, you now know what the budget is buying.
  2. Build the Qini curve for one live campaign on a held-out split, and write down the share at which it peaks. If it never peaks, say so: a monotone curve means the ranking found nobody it believes is harmed, which is either a claim about your population or a sign you are ranking on the wrong quantity, and the two are worth telling apart.
  3. Report the peak as three numbers to whoever owns the budget: the share, the gain at that share, and the gain at a blanket send. Watch which of the three they ask about first.
  4. Put your cost per contact on the same axes and find out which segments clear it. A curve that peaks late can still be a programme that loses money at every stopping point.
  5. Rebuild the same curve in-sample and record how much the coefficient inflates. That gap is what a Qini curve with no stated split is hiding, and it is the number to ask a vendor for.
Check yourself

Three questions

A campaign platform ranks customers by predicted likelihood to purchase. A colleague argues that this is fine, because the customers most likely to buy are also the ones the email will work best on. What is wrong with the argument?

A likelihood model is fit on outcomes and never sees a comparison between arms, so nothing in its score is about the treatment. It puts sure things at the top, misses low-baseline persuadables, and happily targets sleeping dogs, who look engaged and lose 8.87 units each when contacted.

A Qini curve rises to 10,698 units at the ninth decile and then falls to 9,729 at 100 percent. What should the recommendation be?

The descent is information, not noise to be removed. Stopping at the peak earns about 1,000 more units than a blanket send and pays for one decile fewer emails. Report the peak as a region, though: the decile grid puts it at 90 percent and a finer grid at 92.5 percent.

Two Qini curves are presented for the same campaign. One was computed on the rows the uplift model was fit on; one was computed on a held-out block and scores 0.94. The in-sample curve scores higher. Which is the honest number, and why?

The ranking and the response have to come from different rows or the plot is a picture of the model agreeing with itself. A Qini curve reported with no stated split should be assumed to be in-sample.

Sources covered

What tonight maps to

✓ Targeting by predicted uplift against targeting at random The full construction on a held-out block, the diagonal as random targeting, and the measured gap between two rankings of the same 1,500 customers: a Qini of 4,061 against 2,290, or 0.94 against 0.53 once normalised by a perfect ranking's 4,298.
✓ Qini curve and AUUC, read like an ROC curve Distance above the diagonal as the gain from uplift ranking, the area summarised as one coefficient, and the three places the ROC analogy breaks.
✓ The peak as a budget recommendation Why the curve falls, what the falling section is made of, and why only the uplift ranking has a peak at all: 10,698 at the ninth decile against the 9,729 a blanket send produces, with the true optimum at the top 92.5 percent and 10,731.
◐ S-, T- and X-learners and their documented weaknesses Session 7. Tonight consumes the per-customer effects those produce and never refits them.
◐ Persuadables, sure things, lost causes, sleeping dogs, and the 7.00 cost of a send Leader session 5 owns the four-group split, the cost bar and the policy argument. Tonight uses them to explain why the two rankings disagree and why the curve turns down.
· Propensity scores, IPW, doubly robust, difference-in-differences, DoWhy Not covered by design, in any session. Correcting an observational treated-against-control comparison is the experimentation course's territory; ranking who to treat, and knowing what the ranking inherits, is this one's.

Session 8 cheat sheet

Likelihood is not upliftOne ranks how likely the outcome is, the other how much the treatment changes it. The lists can be almost disjoint.
Sure things eat the budget22.4 percent of the base at +2.10. A likelihood ranking puts them first by construction.
Persuadables are the business case20.4 percent at +29.09, an order of magnitude above every other group, and often low baseline because they are drifting.
Sleeping dogs look engaged7.9 percent at -8.87. History and opens score them high, which is the worst place for them.
Read it like an ROC curveCumulative gain against share targeted. Further above the diagonal is better; the diagonal is random.
Qini coefficientThe area between curve and diagonal, also written AUUC. 4,061 on uplift and 2,290 on likelihood to buy, against a perfect 4,298: 0.94 and 0.53.
Only the uplift curve peaksNinth decile, 10,698, about 1,000 units above a blanket send. A ranking with no peak can never recommend stopping.
Every curve ends at the same pointTreat everybody and the ranking stops mattering. Kestrelmark's endpoint is 9,729 for both rankings.
Cost moves the answer againAt 7.00 a send only persuadables clear the bar. Blanket is net -3,848; persuadables only is +22,578.
Held out, or it flatters itselfAnd the ordering inherits every identification assumption from sessions 2 and 3.