learn-customer-retention-with-phoebe / Analyst session 6 of 8
Learn Customer Retention with Phoebe · Analyst track · Session 6 of 8

CLV and buy-till-you-die

A shop customer never cancels - they just stop showing up, and a four-month silence means completely different things for a weekly buyer and a quarterly one. Buy-till-you-die models turn that intuition into a per-customer P(alive) from nothing but frequency, recency and age. Then the second half takes on the most abused number in growth decks: LTV - why the simple formula overstates it, and the cumulative cohort curve from a2 that tells the truth.

🟠 Analyst track Data analysts · Python Build-along: lifetimes 45 min
0-3 · Welcome 3-20 · P(alive) 20-40 · CLV honestly 40-45 · Q&A
Part 0

Where this session sits

a5 ended on an honest admission: shops and games only ever infer churn from silence. This session replaces "pick a 90-day window and hope" with a model that gives every customer their own probability of still being alive. The RFM summary you built in a4 is literally the model's input, a2's counts table returns as the ground truth for LTV, and a7 will inherit the "who is worth saving" question. Work along in the companion notebook notebooks/a6_clv.ipynb.

Live - presented in session Self-study - read after class ★ Build-along - run the code yourself Sources covered
★ What you walk out with today The BG/NBD intuition well enough to explain P(alive) to a stakeholder in one sentence, a fitted BetaGeoFitter with per-customer P(alive) and 90-day purchase forecasts, a working list of the four failure modes that break these models in the wild, and an honest LTV: the cumulative cohort curve that exposes what the ARPU x margin / churn formula overstates.
Part 1 · covers the BG/NBD model + P(alive)

Customers who never say goodbye 17 min live

Non-contractual settings have no churn event, so BG/NBD (Fader, Hardie and Lee, 2005) tells a story instead: while alive, each customer makes purchases at their own Poisson rate - rates vary across customers following a Gamma distribution - and after each purchase they flip a Beta-distributed coin that decides whether they silently die. Feed it three RFM summary numbers per customer - frequency (repeat purchases), recency (customer age at their LAST purchase) and T (total customer age) - and it returns P(alive) and expected future purchases. The magic intuition: high frequency + long silence = probably dead; low frequency + the same silence = probably fine, because that gap is simply their normal rhythm.

Same four months of silence, opposite verdicts month 0 month 8 month 12 Customer A - the regular · 13 purchases in 8 months 4 months of silence Usual gap ~18 days. This silence is ~7x their rhythm. P(alive) ~ 0.09 - probably gone. Customer B - the occasional · 3 purchases in 8 months the same 4 months Usual gap ~3.5 months. This silence is normal. P(alive) ~ 0.75 - probably fine. The BG/NBD story (Fader-Hardie-Lee 2005): While alive, each customer buys at their own Poisson rate (rates Gamma-distributed across customers); after every purchase, a Beta-weighted coin flip decides death. In: frequency, recency, T. Out: P(alive). High frequency + long silence = probably dead. Low frequency + the same silence = probably fine.
🔍 Click to zoom - P(alive) judges silence against each customer's own purchase rhythm
Live★ Build-along: BetaGeoFitter and per-customer P(alive)9 min▶

Two customers, the same four quiet months. The regular's silence is an anomaly against their own history; the occasional's is not - and the model prices exactly that. It needs no browsing data, no demographics: one summary row per customer, straight from the transaction log.

★ Transactions to RFM summary, then fit BG/NBDfrom lifetimes import BetaGeoFitter from lifetimes.utils import summary_data_from_transaction_data summary = summary_data_from_transaction_data( orders, customer_id_col="customer_id", datetime_col="order_date", observation_period_end=orders["order_date"].max()) # columns: frequency (repeat purchases), recency (age at LAST # purchase), T (customer age at the end of the data) bgf = BetaGeoFitter(penalizer_coef=0.001) bgf.fit(summary["frequency"], summary["recency"], summary["T"])
★ P(alive) + expected purchases in the next 90 dayssummary["p_alive"] = bgf.conditional_probability_alive( summary["frequency"], summary["recency"], summary["T"]) summary["pred_90d"] = bgf.conditional_expected_number_of_purchases_up_to_time( 90, summary["frequency"], summary["recency"], summary["T"]) summary.sort_values("p_alive").head(10) # the "probably gone" list
Honesty check on the tooling The lifetimes library is archived - maintenance mode. We still teach with it because its API maps one-to-one onto the concepts and reads beautifully. For production, the maintained successor is PyMC-Marketing's CLV module: the same BG/NBD family, fit Bayesian, so every P(alive) comes with honest uncertainty instead of a bare point estimate. Learn here, ship there.
Part 2 · covers where BTYD assumptions break

Where the models break 8 min live

BG/NBD is a story, and stories have fine print. Dropout is irreversible in the model - once the coin lands on death, that customer can never buy again. Real customers resurrect, binge on promo weekends, and hibernate seasonally, and every one of those behaviors violates the story. Know the four classic breaks before you present a single P(alive).

Four ways reality breaks the story 1 · Resurrection model: dead Dropout is irreversible in the model - a comeback after "death" should not exist. Seasonality and promo spikes violate the story the same way. 2 · One-time buyers look immortal nothing, forever The BG/NBD coin is only flipped AFTER a purchase. Zero repeats = zero death opportunities, so the model keeps them suspiciously alive. Quirk, not bug. 3 · History too short T barely covers one gap If T is short relative to the inter-purchase time, slow-and-alive vs fast-and-dead are indistinguishable: the parameters are unidentifiable. Wait for data. 4 · Spend tied to frequency correlated Gamma-Gamma (the monetary add-on) assumes spend is independent of frequency. Often violated - test the correlation BEFORE fitting it. Every failure mode is checkable before you present: plot comebacks, count one-timers, compare T to gaps, test corr.
🔍 Click to zoom - resurrection, immortal one-timers, short history, spend-frequency correlation
LiveThe fine print, spelled out6 min▶
  • Irreversible dropout: the model has no resurrection state. Winback campaigns, seasonal shoppers and promo-spike bingers all generate purchase patterns the story says cannot happen - fit quality degrades quietly, so inspect holdout calibration, not just the fit summary.
  • The one-time-buyer quirk: customers can only die immediately AFTER a purchase, so a buyer with zero repeats has never faced the coin - BG/NBD keeps their P(alive) high indefinitely. With a large one-time-buyer base (most shops), read cohort-level outputs with that skew in mind.
  • Unidentifiability on short histories: when T is short relative to typical inter-purchase time, the data cannot separate "slow but alive" from "fast but dead". No fitter setting fixes missing time.
  • Gamma-Gamma's independence assumption: the monetary model assumes spend per transaction is independent of frequency. Check summary[["frequency", "monetary_value"]].corr() first; a material correlation means the CLV ranking inherits a bias.
Self-studyWhen to just use the cohort table instead3 min read▶

BTYD earns its keep when you need per-customer lifetime estimates in a non-contractual business with enough history. Skip it when:

  • Small data: a few thousand customers or a short observation window - the a2 cohort table plus a5's KM curve answer the aggregate question with far fewer assumptions.
  • Weekly reporting: the frozen cohort table is the reporting artifact; a probabilistic model that restates every refit has no business on a weekly dashboard.
  • Contractual business: churn is observed, so you do not need to infer it - survival analysis (a5) is the right tool, not BTYD.
Part 3 · covers honest LTV + cohort-informed budgeting

LTV without lying 12 min live

The formula every pitch deck loves: LTV = ARPU x margin / churn. It assumes one constant churn rate forever, and that assumption overstates - churn falls with tenure because the survivors are the durable ones (a5's lesson), so a single blended rate flatters the average customer. The honest ground truth needs no model at all: cumulative cohort gross profit over 12-36 months, read straight off a2's counts table times ARPU times margin. Against that curve, apply the classic LTV:CAC 3:1 benchmark (David Skok) - and set channel-level CAC ceilings from channel-level cohort LTV curves, not from one blended number.

The naive formula vs the cohort truth: constant churn overstates naive LTV = ARPU x margin / churn honest: cumulative cohort gross profit a2 counts x ARPU x margin, per member what constant churn implies the gap $0 0 6 12 18 24 36 mo Churn falls with tenure - the survivors are durable (a5) - so one blended rate flatters every new customer.
🔍 Click to zoom - the honest curve flattens where the constant-churn line keeps climbing
Live★ Build-along: the cumulative cohort LTV curve vs the formula8 min▶

You already built the hard part in a2. The counts pivot (distinct buyers per cohort x period) becomes an LTV curve in three lines: cumulate across periods, convert to money, divide by cohort size.

★ Honest LTV from a2's counts pivot, vs the naive formula# counts = pivot from a2: distinct active buyers per (cohort, period) arpu, margin, churn_rate = 42.0, 0.30, 0.08 # monthly, illustrative cum_gp = (counts * arpu * margin).cumsum(axis=1) # cohort gross profit ltv_curve = cum_gp.divide(counts.iloc[:, 0], axis=0) # per original member ltv_curve.T.plot(legend=False, color="#C2410C", alpha=0.4) naive = arpu * margin / churn_rate # the deck formula print(f"naive formula: {naive:.0f}") print(f"12-month cohort truth: {ltv_curve[12].mean():.0f}") # the naive answer overshoots what any cohort actually delivered
  • Read the curve at fixed horizons - 12, 24, 36 months - and compare like with like. "LTV" without a horizon is not a number, it is a mood.
  • LTV:CAC 3:1 (Skok's classic benchmark) only means something against the honest curve at a stated horizon, with margin in the numerator.
  • Cohort-informed budgeting: compute the curve per acquisition channel, and each channel gets its own CAC ceiling. A channel whose 12-month cohort LTV is half the blended average is quietly subsidized by every other channel.
Real world

How ecommerce teams actually spend winback budgets. BG/NBD plus Gamma-Gamma is the standard ranking machine: score every lapsed customer by P(alive) and expected value, then aim the winback offer at high-value customers whose P(alive) is decaying - not at the definitely-dead (wasted send) and not at the comfortably-alive (wasted discount). One caveat travels with this: a7 will show that predicting who churns is not the same as knowing who can be persuaded - hold that thought until the uplift discussion.

Where the winback budget goes: value x P(alive), offers only to the decaying middle as silence lengthens, P(alive) drifts left - timing is the whole game definitely dead winback = wasted send decaying + high value aim the offer here low value: the discount costs more than they would return comfortably alive discount = wasted margin high value low value 0.0 0.3 0.7 1.0 P(alive) from BG/NBD · value axis from Gamma-Gamma Winback list = high value x decaying P(alive) - the dead waste the send, the alive waste the discount.
🔍 Click to zoom - the winback map: P(alive) across, expected value up, budget only for the decaying middle
Homework

Try it yourself - this week ◐ 30 min total

Sources covered

What this session draws on

Buy-till-you-die is one of the rare corners of retention analytics with an actual academic spine. This page teaches the working core of:

✓Fader, Hardie & Lee (2005) - the BG/NBD "counting your customers" modelPart 1 · the story, inputs and P(alive); Part 2 · its assumptions
✓lifetimes library API (archived - flagged in session)Part 1 · BetaGeoFitter, summary utils; teach here, do not ship here
◐PyMC-Marketing CLV module - the maintained successorPart 1 · named as the production path; Bayesian fitting not covered
◐David Skok - LTV:CAC 3:1 benchmarkPart 3 · the benchmark, applied against honest cohort curves
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Two customers have both been silent for 4 months. A bought 13 times in their first 8 months; B bought 3 times in the same span. Who does BG/NBD call probably dead?

P(alive) judges silence against each customer's own purchase rate. The regular's gap is many times their usual rhythm; the occasional buyer's identical gap is unremarkable. This is the whole point of modeling heterogeneous rates.

2 · Why do one-time buyers look immortal to BG/NBD?

In the BG/NBD story a customer can only die immediately after a purchase. A buyer with no repeat purchases has never been exposed to the dropout coin, so the model holds their P(alive) high indefinitely - a quirk to keep in mind wherever one-time buyers dominate.

3 · Why does LTV = ARPU x margin / churn overstate?

The survivors are the durable ones (a5's lesson), so churn is high early and falls with tenure. Plugging one blended rate into 1/churn projects the durable minority's behavior onto every new customer. The honest check is cumulative cohort gross profit over 12-36 months.

Analyst session 6 cheat sheet · pin this

BG/NBD in one breathWhile alive: Poisson purchasing, rates Gamma-distributed across customers. After each purchase: a Beta-weighted coin flip of dying. Fader-Hardie-Lee 2005.
The three inputsfrequency = repeat purchases · recency = customer age at LAST purchase · T = total customer age. Straight from a4's RFM summary.
The P(alive) intuitionSilence is judged against the customer's own rhythm: high frequency + long silence = probably dead; low frequency + same silence = probably fine.
Tooling honestylifetimes is archived (maintenance mode) - best API to learn on. Production successor: PyMC-Marketing CLV, Bayesian BG/NBD with uncertainty.
The four breaksIrreversible dropout (resurrection/seasonality/promos violate it) · one-time buyers look immortal · short T = unidentifiable · Gamma-Gamma needs spend independent of frequency.
When to skip BTYDSmall data, weekly reporting, or a contractual business - use the a2 cohort table or a5 survival instead. Models restate; the reported table never does.
Honest LTVCumulative cohort gross profit over 12-36 months = a2 counts x ARPU x margin / cohort size. The ARPU x margin / churn formula OVERSTATES - churn falls with tenure.
LTV:CAC discipline3:1 classic benchmark (Skok), judged at a stated horizon on the honest curve. Channel-level cohort curves set channel-level CAC ceilings.