Lab NotebookEntry 01Models that decide

How to make money with a propensity model

Spoiler: the model alone makes no money. The threshold does.

A propensity model with a good AUC tells you who is most likely to buy. The question that pays the bills is a different one: who do I call tomorrow, with the team I have? That answer doesn't come out of the model. It comes from a ratio of cost to value, from how well the score is calibrated, and from the team's capacity. Plenty of people think they have this down; few have actually done the math.

The model tells you who likes you most. Who you call is up to your cost spreadsheet.

Origin
A problem brought to a mentoring session, anonymized
Data
10,000 synthetic customers, generated on this page with a fixed seed
Two voices
In the notebook, for the people who build the model. On a napkin, for the people who hold the budget
Benches
Two, interactive. Play with them

Same model, three decisions

Results before theory. Same 10,000 customers, same model, same AUC of —. Three ways to use the score, with a call costing €8 and a margin of €120 per sale:

StrategyCallsSalesProfit− 0 +

—

The rest of this entry explains that math, shows where it works and, above all, where it breaks.

Where this came from

A mentee came to me with a project that, on paper, was finished. It had the stages every course teaches (data, preparation, model, deployment) plus one that almost nobody draws on the diagram: a folder called POLICY.

The model estimated each customer's propensity to buy. The policy folder turned that estimate into action: propensity bands, a threshold, the list of who gets the offer. The business decision lived there, not in the model. His numbers don't appear here; the problem is the same, rebuilt with synthetic data.

The physicist's version of the problem

Before any ROC curve, an order of magnitude. Ten thousand customers, a purchase rate of 6%: some 600 people who will buy, scattered across the customer base. Each contact costs C; each sale leaves a margin of V (what's left after product cost, taxes, commission and discount, not the price).

Calling a customer with probability p of buying has an expected value of p·V − C. It pays off when that is positive:

p · V > C ⟹ t* = C / V
(1)

This is classical decision theory, in the form Elkan (2001) made the standard reference in machine learning: if not calling costs nothing, the optimal threshold is the ratio C/V. With €8 and €120, t* ≈ 0.067. Not 0.5. Nor is it, in general, the Youden point or any other "pretty" point on the ROC curve. With a calibrated score, the Youden point amounts to cutting at the base rate: it bakes in a cost ratio that matches yours only by coincidence. Here, by coincidence, it almost does (the base rate is 6.4% and C/V is 6.7%); change V on the bench and watch the two drift apart. (The cost-weighted ROC point, on the other hand, does lead to the same t*.)

Call anyone whose chance of buying beats the cost of a call divided by what a sale brings in. Calls cost €8, a sale brings in €120: call anyone with better than a 1-in-15 chance.

The math hides two conditions. First: p has to be a real probability. Second: people who don't get a call don't buy on their own. Keep both in mind, because that's where the money leaks.

Bench 1 · Pick the threshold

calibrated score · AUC —
0.500
€8
€120
Team capacity (calls)
—
Calls—
Sales—
Precision—
Buyers reached—
Profit—
Campaign profit by threshold (€) maximum-profit zonelossalmost nobody gets a call

—

Would buy if calledWouldn't buy
Called—a sale that paid for the call—wasted call
Not called—buyer left below the line (visible only because the data is synthetic)—call rightly skipped
Show the numbers as a table
What matters to whoever pays

Where the money is

1. Stop paying for calls that don't pay for themselves

Going from the "call everyone" strategy to the C/V rule, the campaign makes — fewer calls and profit goes from — to —. The calls avoided save —; along the way, — sales are lost, sales that together cost more in calls than they brought in. No new sale was invented: the gain comes from no longer paying for customers below C/V.

The default 0.5 threshold errs in the other direction. With this customer base, —. By equation (1), 0.5 would only be optimal if a sale were worth exactly twice a call (V = 2C); here it's worth 15 times as much. The error is a cost error, inherited from the library default. The scikit-learn documentation itself warns that this fixed threshold is almost never the right one.

The first money a model brings in is the money you stop spending. The second is the money you stop losing by being too cautious.

2. Same team, more sales

With fixed capacity, the question shifts from "who is worth calling" to "who is most worth calling". The threshold becomes a position in the queue, and what decides who gets in is the quality of the ranking at the top of the list (precision in the first 1,000 positions, which is what sets the lift), not the AUC over the whole customer base. Calibration still tells you whether the queue is worth filling.

at random
—
model's list
—

Sales with 1,000 calls. Lift: —. Change the capacity in Bench 1 and these bars follow.

—

3. The bar is set by the business, not by IT

The optimal threshold is a function of C and V, not of the model. Move the C and V sliders in Bench 1 and watch the green zone shift while the model stays exactly the same. When the product margin changes, or the team's cost changes, the list has to change too. Try C = 40 and V = 80, or any pair with V = 2C: it's the only kind of world where equation (1) puts the threshold at 0.5. And notice what the bench shows in that case: in this customer base almost nobody scores above 0.5, so not even the right threshold can save a campaign with nobody to call.

If nobody in the company owns this threshold, nobody is managing the model. It's just running.

Mandatory section

Where this breaks (and the money leaks)

A score is not a probability

Equation (1) assumes that a score of 0.10 means a 10% chance. Not every model delivers that. Niculescu-Mizil and Caruana showed that maximum-margin methods, such as AdaBoost-style boosted trees and SVMs, push probabilities away from 0 and 1, in a sigmoid-shaped distortion. (Today's gradient boosting, trained on log-loss, usually comes out less distorted.) In practice, what shifts the scale most is rebalancing the classes in training, by resampling or by class weights. In Bench 2, the miscalibrated model has the same ranking, the same AUC, and the same maximum achievable profit, and yet the rule applied to it lands in the wrong place.

A physicist's detour

A thermoscope is not a thermometer

In the late 16th and early 17th centuries, Galileo and others built thermoscopes: instruments that showed whether one thing was hotter than another, but not by how many degrees. The thermoscope only became a true thermometer, one whose readings could be compared from instrument to instrument, once its scale was tied to reproducible fixed points, like Rømer's in 1701 and, later, Fahrenheit's.

A model with a high AUC and no calibration is a thermoscope. AUC is invariant under any strictly increasing transformation of the score: raise the score to the power 0.6, pass it through a sigmoid, take the square root, and it doesn't budge. Equation (1), by contrast, gives a completely different answer. Ranking and scale are different quantities, and the decision depends on the scale.

The fix is well known: calibrate on a separate, labeled set before applying any expected-value rule. Platt scaling works well when the distortion is sigmoid-shaped. Isotonic regression corrects any monotonic distortion, but it needs more data and overfits when positives are few. Neither fixes drift. There's also a shortcut: with C and V fixed, you can pick the threshold directly on the raw score by maximizing profit on a separate labeled sample (that's what scikit-learn's TunedThresholdClassifierCV does). Calibration becomes indispensable when V varies from customer to customer or when the bar changes often.

The model knows who's warmer, but not by how many degrees. To decide whether a call is worth it, you need the degrees.

The world changes after deployment

If the purchase rate drops 40% the next month because of seasonality, the score stays the same, but a score of 0.10 now means a 6% chance. The model is no longer calibrated, and not a single line of it has changed. The threshold on probability is still C/V; the threshold on the score should have gone up, but it stayed frozen at its day-0 value. For this specific case, a shift in the base rate, there is a classic adjustment: correct the odds for the new prevalence (Saerens and colleagues, 2002). It holds when only the share of buyers changes while each group's profile stays the same, and the new rate has to be measured: the random sample in the next section is how you get it. See the "Drift" scenario in Bench 2.

In December everybody buys; in February, they don't. If you're still using December's bar in February, you're paying to call people who won't buy.

You only see what you chose to see

After deployment, you only see responses from the people who got a call. At the current threshold in Bench 1, — customers who would have bought if called were left below the line, and in the production data nobody knows how they would have responded. Retraining on that data means learning about a slice that the threshold itself picked, and even calibration monitoring inherits that bias. The literature has a name for it: the selective labels problem.

A physicist's detour

The LHC is short-staffed too

At the LHC, the beams cross 40 million times per second, and each crossing brings dozens of collisions. There's no way to keep them all. At ATLAS, the first trigger level cuts the stream to 100,000 events per second, and the second leaves about 3,000 per second for analysis. It's a threshold with fixed capacity, just like your sales team's, and with the same problem: whatever the trigger throws away simply doesn't exist as far as the analysis is concerned.

One way physicists get around it is to deliberately record a fraction of what the main filter would throw away: randomly selected bunch crossings, and looser triggers with a prescale that keep 1 event in every N. That sample becomes the control that measures what the filter is missing. ATLAS, for example, used randomly selected bunch crossings as a control to measure the efficiency of its minimum bias trigger.

The translation to marketing is direct: also call a small random sample of customers below the threshold. It costs little and keeps the model honest. Randomize in both directions, leaving some customers above the threshold uncalled as well, and the same design measures the real effect of the call.

Not even the world's largest particle accelerator can keep everything it sees. That's why it sets aside a random sample, to find out what it's missing. Your campaign should too.

Propensity is not the effect of the call

The model predicts who buys, not who buys because of the call. The customer who would have bought anyway has a high score and uses up a contact for nothing. This is the second condition behind equation (1): the right p there is the difference between the chance of buying with a call and without one. That question needs a different tool: uplift models, which estimate the incremental impact of the action and need randomized contact data to be trained reliably.

Spending a call, or a discount, on someone who was going to buy anyway isn't a sale, it's a donation. The customer worth their weight in gold is the one who only buys if you call.

Bench 2 · Where it breaks

uses C and V from Bench 1 · no capacity limit
—
Profit by threshold applied to the score (€)

—

Show the numbers as a table

Reproduce it yourself

The same steps as Bench 1, in Python. The numbers come out different from the page's because the random generator is different (they move by a few thousand euros from one seed to the next); the conclusions don't.

import numpy as np

rng = np.random.default_rng(7)
N, C, V = 10_000, 8, 120            # customers, cost per call, value per sale

score = rng.beta(0.8, 12, N)        # score calibrated by construction
buyer = rng.random(N) < score       # who actually buys

def profit(t, capacity=None):
    order = np.argsort(-score)
    chosen = order[score[order] >= t][:capacity]
    return buyer[chosen].sum() * V - len(chosen) * C

grid = np.arange(0, 0.5, 0.005)
best = grid[np.argmax([profit(t) for t in grid])]
print(f"C/V rule = {C/V:.3f} | empirical best = {best:.3f}")
print(f"everyone = {profit(0)} | threshold 0.5 = {profit(0.5)} | rule = {profit(C/V)}")
print(f"rule, capped at 1,000 calls = {profit(C/V, 1000)}")

References

Decision and cost

  1. Elkan, C. (2001). The Foundations of Cost-Sensitive Learning. IJCAI.The classic ML formulation of equation (1): the optimal threshold as a ratio of costs.
  2. Provost, F. & Fawcett, T. (2013). Data Science for Business. O'Reilly.The expected-value framework for marketing decisions, written for managers too.
  3. scikit-learn: Tuning the decision threshold for class prediction.The documentation that warns a fixed 0.5 threshold is almost never the ideal one.
  4. Wikipedia: Youden's J statistic.The "pretty" ROC point, which bakes in a cost ratio that isn't yours.

Calibration and drift

  1. Niculescu-Mizil, A. & Caruana, R. (2005). Predicting Good Probabilities With Supervised Learning. ICML.Which models come out miscalibrated, and when to use Platt or isotonic.
  2. scikit-learn: Probability calibration.Calibration curves and both methods, in practice.
  3. Saerens, M., Latinne, P. & Decaestecker, C. (2002). Adjusting the Outputs of a Classifier to New a Priori Probabilities: A Simple Procedure. Neural Computation, 14(1).How to correct probabilities when the base rate changes, assuming each class's profile doesn't.

What the threshold hides

  1. Lakkaraju, H. et al. (2017). The Selective Labels Problem: Evaluating Algorithmic Predictions in the Presence of Unobservables. KDD.Why judging decisions only by the cases that made it through the filter is misleading. The main case is judges' bail decisions; the mechanism is the same.
  2. Gutierrez, P. & Gérardy, J.-Y. (2017). Causal Inference and Uplift Modelling: A Review of the Literature. PMLR 67.Modeling the incremental impact of the action, not the propensity.

Detours

  1. ATLAS Experiment (2022). ATLAS Event Selection System Readies for LHC Run 3.The trigger figures quoted above.
  2. Tompkins, L., for the ATLAS Collaboration (2010). Performance of the ATLAS Minimum Bias Trigger in p-p collisions at the LHC. HCP2010.Conference proceedings. Randomly selected bunch crossings as a trigger control.
  3. Wikipedia: Thermoscope.From thermoscope to thermometer.

Got a problem like this at your company?

For companies, I do this through Vettore Data, my consultancy: we look at your model, the cost of your errors and your team's capacity, and tell you which threshold to use and how much money is on the table. The version for decision-makers is in Vettore's Insights.

Read the decision-maker versionor get in touch