A propensity model example: the p(book) score on my home page, explained
My site scores whoever reads it with a small propensity model, on every page, and the home page prints the arithmetic as it goes. This note takes it apart: the signals, the hand-set weights, one score worked by hand, and what a production model does differently.
ALEXANDER ANDERSSON ·
What a propensity model does
A propensity model estimates how likely someone is to do one thing: buy, renew, churn, book. My site runs one on whoever reads it, on every page, this one included. It scores p(book), the chance you book a call, and the home page shows the maths signal by signal while you read. This note uses it as a propensity model example: what goes in, how the weights were set, one score worked by hand, and what a production propensity to buy model does differently.
A logistic propensity model, the simplest kind, has three parts: signals about what a person has done, a weight for how much each counts, and a function that turns the weighted sum into a probability between 0 and 1. A propensity to buy score is that probability for a purchase, and CRM teams use it to decide who gets which message, offer or channel, and who gets left alone.
The home page’s model has the shape of logistic regression, with the weights set by hand instead of fitted. Each signal is multiplied by its weight, the products are added to an intercept, and the total (the logit, or log-odds) goes through the logistic function, σ(z) = 1 / (1 + e^−z).
The intercept is −3.2. σ(−3.2) = 1 / (1 + 24.533) = 0.039, or 4% before any signal counts. The first screen already puts scroll_depth above zero, so a new visitor starts a little higher.
Why the weights are set by hand
A trained model learns its weights from outcomes: past visitors, each labelled booked or didn’t. This one has none. The tab keeps no record of who read what and then booked, and nothing from the analytics feeds back into the weights. Each score belongs to one visitor in one tab. The comment above the model in the code puts it in one line: “Same shape as a trained model; nothing from the analytics feeds back into it, so there is nothing to train it on.”
So the weights are judgement, printed from the code in the table below. Read as a set:
- Looking inside an offer counts for more than reading proof: +0.85 per program viewed, +0.55 per receipt opened.
- Each page beyond the home page you read for 15 seconds adds +0.35.
- A click toward booking adds +0.6. Getting the whole page on screen adds +0.9.
- Idle time costs 0.25 a minute after a 30-second grace. Heading for the tab bar costs 0.6.
- Every signal has a cap, so no single habit runs away with the score.
Three exceptions sit outside the sum: the score shows at most 92% until the calendar confirms a booking, 100% after that, and 0% with tracking off. Clicking Book doesn’t jump the score; it adds +0.6, like any other book_intent click.
The twelve signals
All are computed in the browser, from this tab alone, across every page of the site you open in it:
- receipts_opened: receipt cards you’ve opened, up to four.
- programs_viewed: programs you’ve looked inside, by opening the card or visiting the program’s own page, up to three.
- pages_read: pages beyond the home page that have been on screen for 15 seconds or more, this one included, up to four.
- opened_match: 1 if you opened the program flagged as matching the topic you’d shown most interest in (lifecycle, growth or AI).
- book_intent: clicks toward booking before you book: a Book button, a time picked in the booking panel, or a contact link. Up to two.
- read_min: minutes reading: a receipt or program card open, or a page beyond the home page on screen, added up. Up to three.
- active_min: minutes on screen with some input (a mouse move, scroll, key, click or touch) in the last 30 seconds. Up to five.
- scroll_depth: how far down the home page the bottom of your screen has reached, from 0 to 1.
- came_back: returns to the tab after 10 seconds or more away. Up to two.
- copied_text: 1 if you copied any text. The page logs the length, never the text.
- idle_min: minutes since your last input, after a 30-second grace, because reading without touching anything looks the same as being gone. Up to four.
- exit_intent: 1 once your pointer heads for the tab bar, or you press leave.
Each value is clipped to 0…its cap and rounded to two decimals before weighting.
A propensity model example, worked by hand
Take a reader three minutes in. They have opened two receipts, then the program flagged as their match. A card has been open for 90 seconds in total, they’ve been active all three minutes, and 80% of the page has been on screen. No clicks toward booking, no returns, nothing copied, and they touched the page in the last 30 seconds, so idle_min is 0.
Weight × signal, in log-odds:
- receipts_opened: 0.55 × 2 = 1.10
- programs_viewed: 0.85 × 1 = 0.85
- opened_match: 0.40 × 1 = 0.40
- read_min: 0.30 × 1.50 = 0.45
- active_min: 0.20 × 3.00 = 0.60
- scroll_depth: 0.90 × 0.80 = 0.72
- the other six: 0
The contributions sum to 4.12. With the intercept, the logit is −3.20 + 4.12 = 0.92. Then σ(0.92) = 1 / (1 + e^−0.92) = 1 / (1 + 0.3985) = 0.715. The scoring card prints p = σ(0.92) = 0.715 → 72%.
Now the pointer heads for the tab bar. exit_intent becomes 1, the logit drops by 0.60 to 0.32, and σ(0.32) = 1 / (1 + 0.7261) = 0.579, or 58%. The home page’s exit message, a draft win-back email, reads p from just before the exit counted, 0.72, and uses its band to pick the segment and the template’s second line: cold under 0.2, warm from 0.2, hot from 0.5.
A 72% chance of booking after three minutes is far too high, and the model can’t know that. Nobody has checked these weights against real bookings. The score puts readers in order of how much they’ve done; it doesn’t mean 72 in 100 such readers book. Calibration, further down, is how a production model closes that gap.
How to build a propensity model that runs in production
The shape survives, signals in and a probability out. Almost everything around it changes.
- Start from outcomes. Pick the event (a purchase, a renewal, a cancellation) and a window. Each training row is a customer at a past point in time, with their signals as they were then and whether the event happened in the window after. A signal recorded after that point leaks the answer and flatters the model.
- Fit the model. Logistic regression is the baseline. Gradient-boosted trees such as XGBoost often do better on behavioural data, because they find interactions, such as two receipts plus a return visit, without anyone writing them in.
- Track every run. Log each run’s data, parameters and metrics, and register the model you deploy, so you know which version is scoring customers today and can roll it back. MLflow does both, with the data and jobs in a warehouse such as Databricks.
- Calibrate. On data the model hasn’t seen, check that customers scored 0.7 convert about 70% of the time, and correct the scores if they don’t. A ranking needs only the order. A threshold or a budget needs probabilities you can take at face value.
- Score on a schedule, into the CRM. A recurring job writes the score onto each profile, for example as a custom attribute in Braze, where segments and Canvas steps can read it.
- Act on it with a holdout. Keep a random share of each scored group out of the message, so the difference between the two groups is what the message added.
I build propensity scoring with Databricks, XGBoost and MLflow; the home page’s formula is the toy version of that. If you’re weighing whether a score is worth building into your CRM, start with the AI marketing audit. If it should decide who gets what inside one program, see lifecycle marketing.
Propensity model vs uplift model
A propensity model asks who is likely to act. An uplift model asks whose behaviour a message changes: the difference between a person’s chance of acting if they get the message and if they don’t.
The top of a propensity list includes people who would have bought anyway, and messaging them costs a send, or a discount, for nothing. An uplift model looks for the people a message moves, and flags the ones it puts off.
Uplift needs data a propensity model can do without: outcomes from people randomly held out of the message, set against people who got it. The home page’s model is propensity only; a website that remembers nothing has no control group.
See it score you
The model runs on every page of the site, this one included, and the home page shows it. Open How this page works and the scoring card lists every signal with its weight, your value and its log-odds, then the logit and σ, updating as you read. The site stores nothing in the browser and sets no cookies. Only three things about you can leave the tab. One is a batch of events for my own analytics, sent while tracking is on: what you open, click and how far you scroll. Another is a note request: the numbers on your scoring card and the names of the cards you opened go to Claude, which rewrites one message. The third is a booking you make.
The weights, from the code.
Read at build time from MODEL in src/lib/program.ts, the same object the home page scores you with.
| Signal | Weight (log-odds per unit) | Counted up to |
|---|---|---|
| intercept | −3.2 | — |
| receipts_opened | +0.55 | 4 |
| programs_viewed | +0.85 | 3 |
| pages_read | +0.35 | 4 |
| opened_match | +0.4 | 1 |
| book_intent | +0.6 | 2 |
| read_min | +0.3 | 3 |
| active_min | +0.2 | 5 |
| scroll_depth | +0.9 | 1 |
| came_back | +0.35 | 2 |
| copied_text | +0.3 | 1 |
| idle_min | −0.25 | 4 |
| exit_intent | −0.6 | 1 |
p(book) = σ(intercept + Σ weight × signal), each signal clipped to 0…its cap. Shown at most 92% until a booking is confirmed, then 100%; 0% with tracking off.
Questions.
What is a propensity model?
A propensity model estimates the probability that a person takes a specific action, such as buying, renewing, churning or booking, from signals about what they have done. CRM teams use the score to decide who gets which message, and who gets none. The home page of this site runs a small one on its reader.
What is a propensity to buy score?
It's the output of a propensity to buy model: a number between 0 and 1 for how likely a customer is to purchase within a set window. In production it's usually recomputed on a schedule and written to each customer profile in the CRM, where segments and customer journeys can read it.
How do you build a propensity model?
Define the outcome and its window. Build a table of customers at past points in time, with their signals as they were then and what happened afterwards. Train on it: logistic regression as a baseline, gradient-boosted trees such as XGBoost as the usual next step. Check calibration on data the model hasn't seen, score on a schedule into the CRM, and measure with a holdout.
What is the difference between a propensity model and an uplift model?
A propensity model predicts who is likely to act. An uplift model predicts whose behaviour a message changes: the difference between their chance of acting with the message and without it. Uplift needs outcomes from a randomly held-out group to learn from.
Is the model on the home page trained on data?
No. It's a logistic model with hand-set weights. The site stores nothing in the browser and sets no cookies, so there is no history of visitors and bookings to train on. The weights are printed on this page, read from the same code the home page runs.
Book 20 minutes on your CRM.
If one flow leaks, we look at that. If it’s the whole program, we go from onboarding. You leave with what I’d fix first. — Alex