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

Retention metrics, precisely

Ask three teams for "D7 retention" and you will get three different numbers computed from the same event log - and all three are honest. Before anyone builds a cohort table, a model, or a dashboard, this session pins the vocabulary down: the three counting variants of day-N retention, the frozen-history discipline every trustworthy retention number obeys, the day-boundary conventions that make your SQL disagree with the analytics tool, and what DAU/MAU can and cannot tell you.

🟢 Analyst track Data analysts · Python ▶ Live variant switcher Start here
0-3 · Welcome 3-20 · Three D7s 20-40 · Windows & stickiness 40-45 · Q&A
Part 0

Where this track goes

Eight analyst sessions, one running artifact: a retention report your stakeholders can act on, shipped in the capstone. Today (a1) nails the definitions. a2 builds the cohort table and heatmap - the artifact everything else reads from. a3 grows it into the full viz gallery, a4 segments who is retained (RFM, behavioral cohorts), a5 brings survival analysis, a6 CLV and buy-till-you-die models, a7 churn prediction that actually helps, and a8 assembles the report. Examples run on ecommerce and mobile-game data throughout. Work along in the companion notebook notebooks/a1_metrics.ipynb.

Live - presented in session Self-study - read after class ▶ Switcher - drive it yourself Sources covered
★ What you walk out with today The three counting variants of "D7 retention" and which tools default to which, the frozen-history discipline that makes a retention number immutable once its window closes, the day-boundary conventions behind tool-vs-SQL disagreements, checkpoint diagnostics for D1/D7/D14/D30, and a four-line metric contract you can write for your own product tonight.
Part 1 · covers classic vs rolling vs bracket (Amplitude + Mixpanel docs)

Three different D7s 17 min live

Below is a real computation, not a picture: 400 fixed simulated users with a full event log, and three honest ways to count "day 7 retention" on them. Same users, same events - and the answers range from 22% to 94%. Nothing is broken. The variants answer different questions, and every analytics tool quietly picks one as its default. "Which D7 is this?" is the first question of every retention conversation you will ever have.

LiveThe three definitions, one at a time8 min▶

Each variant fixes the same base - all 400 users, anchored at their first-seen day - and differs only in which return events count:

  • Classic / N-day (22% here): the user returned exactly on day 7. Amplitude calls it "N-Day", Mixpanel calls it "On", and it is GameAnalytics' default - which makes it the benchmark standard: every published D1/D7/D30 figure you will compare against is classic. Once day 7 has passed for a cohort, the number is frozen. It never moves again.
  • Rolling / unbounded (94% here): the user returned on day 7 or any later day. Mixpanel's default. It is always the biggest of the three by construction, and it equals 1 minus churn-by-day-7. Its fatal flaw: a dormant user returning on day 45 flips their day-7 answer from no to yes, so historical values keep rising retroactively. A useful concept - but never put it on a dashboard: a trend line that rewrites its own past cannot be trended.
  • Bracket / range (88% for week 1 here): the user returned within a window, days 1-7. Each bracket is independent and freezes once its window closes. This is the right shape for products with a weekly or repurchase cadence, where "came back on exactly day 7" is an arbitrary target but "came back within the first week" is the real habit question.
The 3.5x gap is normal, not a bug Amplitude's trillion-event study found N-day retention underestimates users-who-ever-return by roughly 3.5x. That is exactly the classic-vs-rolling spread you see in the switcher: 22% vs 94%. Neither number is wrong; they answer "returned on day 7" vs "ever returned after day 6". Report a number without naming its variant and your reader will pick whichever question flatters the chart.

Start on Classic D7: 22%. Read which return events light up - only the ones landing exactly on day 7. This is the number benchmarks mean.

Switch to Rolling D7: 94%. Same users. Every return from day 7 onward now counts - including ones that happened weeks later. Ask yourself: if a user returns tomorrow, which historical cells change? (In rolling: last month's.)

Switch to Bracket W1: 88%. Returns within days 1-7 count. Close to rolling here because most returners come back early - but unlike rolling, this number froze forever when day 7 ended.

Say the three numbers out loud as one sentence: "22% came back on day 7, 88% came back within week 1, 94% eventually came back." All true. Only one belongs on a trend line.

Part 2 · covers frozen history, the reporting discipline

The frozen-history law 8 min live

A retention number you can trust is computed one way: fix the base, look forward. Anchor a cohort to a fixed date - registration date, or everyone active on a fixed date like Aug 1 - then look forward 1, 7, 14, 30 days and count who came back. Because the base never moves and the window eventually closes, a completed number is immutable. Only the current, still-filling window updates, and it is always visibly marked partial.

One user, one anchor, four forward windows - and a day-45 return that changes nothing Day 0 anchor: first seen D1 D7 - frozen once day 7 passes D14 D30 day 7 day 14 day 30 day 45 a return event This return lands OUTSIDE every closed window: Classic and bracket ignore the day-45 return - their windows closed. Rolling would flip D7 to "yes", retroactively rewriting a five-week-old number. That is why rolling never belongs on a dashboard.
🔍 Click to zoom - fix the base at day 0, look forward; closed windows are history and history does not move
Live★ Build-along: forward-looking D1/D7/D14/D30 in pandas6 min▶

The whole law is four lines of logic: anchor each user at their first-seen day, age every event against that anchor, then flag the ages you care about. Run it on the repo's synthetic game telemetry - the churn is generated, so the right answers are known.

★ Anchor, age, and count - classic and bracket variants side by sideimport pandas as pd events = pd.read_csv("data/game_telemetry.csv", parse_dates=["ts"]) events["day"] = events["ts"].dt.normalize() anchor = events.groupby("user_id")["day"].min().rename("anchor") # the fixed base ev = events.join(anchor, on="user_id") ev["age"] = (ev["day"] - ev["anchor"]).dt.days base = ev["user_id"].nunique() returns = ev[ev["age"] > 0] # day 0 is the anchor itself classic = {f"D{n}": returns.loc[returns["age"] == n, "user_id"].nunique() / base for n in [1, 7, 14, 30]} # exactly day n bracket_w1 = returns.loc[returns["age"].between(1, 7), "user_id"].nunique() / base # within days 1-7 # Freeze rule: only publish a window once it has fully elapsed for the base. snapshot = ev["day"].max() d30_mature = anchor + pd.Timedelta(days=30) <= snapshot # else: mark PARTIAL print(classic, round(bracket_w1, 3), d30_mature.mean())

Note what is NOT here: no "last 7 days as of today", no "day 7 or later". The anchor is per-user and fixed; the window looks forward from it; a completed window never recomputes.

Why two popular metrics restate their own history Trailing look-back ("of today's actives, how many were here 7 days ago?") re-anchors to today, so the denominator moves every single day - yesterday's number is already different, and no two days of the trend line describe the same population. Rolling retention keeps the anchor but never closes the window, so a day-45 return rewrites day-7 history. Both are worth understanding, and neither ever belongs on a dashboard.
Part 3 · covers day boundaries, timezones, checkpoints, DAU/MAU

Day boundaries, checkpoints, stickiness 12 min live

Even with the variant fixed, two tools can still disagree on the same data - because "day 1" itself has two definitions. Amplitude's default counts rolling 24-hour windows from each user's start event (timezone-independent); Mixpanel uses strict calendar dates in the project timezone. A 23:50 install that returns at 00:10 is D1-retained on a calendar - twenty minutes later. This one mechanic explains most "my SQL disagrees with the tool" tickets you will ever file.

Same install at 23:50, same return at 00:10 - two conventions, two verdicts Rolling 24h windows (Amplitude default) 0-24h = day 0 24-48h = day 1 install 23:50 return 00:10 = 20 min in: still inside day 0 Verdict: NOT D1-retained (it is day-0 activity) Calendar days, project timezone (Mixpanel) Aug 1 = day 0 Aug 2 = day 1 install Aug 1, 23:50 return Aug 2, 00:10: a new calendar date Verdict: D1-retained, 20 minutes later Neither convention is wrong - but your SQL must copy the tool's convention before you compare numbers.
🔍 Click to zoom - the 23:50 install: not retained under 24h windows, D1-retained on a calendar
LiveCheckpoints and stickiness: what each number diagnoses7 min▶

The standard checkpoints are not arbitrary - each one has a diagnostic job:

  • D1 - onboarding. Did the first session deliver value? A weak D1 is a first-session problem, nothing later can fix it.
  • D7 - habit. One full weekly cycle has passed, so weekday effects cancel. This is the habit-formation read.
  • D14 - trial boundary and content depth. Free trials end, content runways run out. A cliff here is a depth problem.
  • D30 - established habit and the monetization horizon. Games often report D28 instead: exactly four weeks, so the weekday mix cancels perfectly.

Stickiness = DAU/MAU. Around 20% is good for a consumer app; 50%+ is daily-habit legend territory (the Facebook number). Two traps: it is meaningless for products with a non-daily cadence - use WAU/MAU for weekly-cadence products - and the ratio can RISE while the product declines, whenever MAU shrinks faster than DAU. A shrinking casual base concentrates the loyal core and flatters the ratio. It also hides the whole distribution: the fix is the L28 power-user curve, previewed here and built properly in a4.

Self-studyDenominators: all signups or activated users?3 min read▶

One more axis before your metric is fully specified: who is in the base? Retention over all signups measures the funnel and the product together; retention over activated users (completed a first core action) isolates product stickiness and reads far higher. Both are legitimate, they answer different questions, and mixing them silently is how two dashboards "disagree" for a quarter before anyone notices. State the denominator on the chart, every time.

For ecommerce, activity-based retention is nearly meaningless - people browse without buying - so the standard is purchase-based cohorts: the anchor is the first purchase, the active event is another purchase. a2 builds exactly that table.

The gap between those two denominators is the activation funnel - and the biggest leak in it is where retention is really won or lost:

The activation funnel: where the two denominators diverge Install 100% Signup 64% First core action 36% Second visit 26% -36% never sign up -44% never do the core action the biggest leak -28% never come back Every user lost before the first core action was never retainable - activation is the biggest retention lever. Illustrative stages - no install/signup events in the repo; each loss is % of the previous stage.
🔍 Click to zoom - install to second visit: the biggest leak sits before the first core action
The four checkpoints and what each one diagnoses D1 Onboarding Did the first session deliver value? Game Tutorial landed; median 22-27% (2025) Ecommerce Post-purchase flow opened again D7 Habit One full weekly cycle; weekday effects cancel Game Core loop stuck; median 3.4-3.9% (2025) Ecommerce Browse-again cadence forming D14 Trial / depth Trials end, content runways run out Game Content depth cliff shows here first Ecommerce Replenishment check-in point D30 Monetization Established habit; revenue horizon opens Game Often D28 = 4 exact weeks, no weekday mix Ecommerce Second-order window opens A weak D1 is an onboarding problem; a D14 cliff is a depth problem. Diagnose at the checkpoint, not on averages.
🔍 Click to zoom - D1 onboarding, D7 habit, D14 depth, D30 monetization: each checkpoint has a diagnostic job

Scored against those checkpoints, here are this course's own numbers - classic D1/D7/D14/D30 from the repo's 8,000-player game telemetry, drawn over the GameAnalytics 2025 benchmark bands:

The decay ladder: this dataset's D1/D7/D14/D30 vs the 2025 benchmark bands GA 2025 median band top-quartile band legacy 40/20/10 folklore (D1/D7/D30) 40% 30% 20% 10% 0% 40 20 10 27.9% 13.6% 9.1% 6.9% D1 D7 D14 D30 median 22-27 · topQ 30%+ median 3.4-3.9 · topQ 7-8 no published 2025 band 75% of games under 3% (D28) Read: this dataset beats the GA 2025 median at every checkpoint and clears the top quartile at D7 and D30. At D1 it sits above the median band but below the 30%+ top quartile - all four numbers are classic counting.
🔍 Click to zoom - 27.9 / 13.6 / 9.1 / 6.9: above the median everywhere, top-quartile at D7 and D30
Real world

Benchmarks with a vintage, not folklore. The legacy game rule of thumb - 40/20/10 for D1/D7/D30 - is real and long-standing, but today it marks a top-quartile-to-top-decile game, not an average one. GameAnalytics' 2025 study (11,600 games) puts the medians at D1 22-27% and D7 3.4-3.9%, and 75% of games hold under 3% at D28. Ecommerce speaks a different dialect entirely: purchase-based retention, where repeat purchase rate averages ~28% across industries, ~76% of second orders arrive within 90 days, and the median inter-purchase interval of 73-120 days sets how wide your churn window should be.

Homework

Try it yourself - this week ◐ 30 min total

The metric contract - four lines that fully specify a retention number (ecommerce example) 1 · Anchor event first completed purchase who is in the base, and when each user's clock starts 2 · Active event any later purchase what counts as coming back - a visit is not a retained buyer 3 · Window type classic D30, frozen at close which question it answers; brackets for weekly cadence 4 · Timezone + boundary calendar days, shop timezone the 23:50 install problem - copy the tool's convention Change any one line and it is a different metric - pin the four lines to the top of every retention notebook.
🔍 Click to zoom - anchor, active event, window, timezone: change any line and it is a different metric
Sources covered

What this session draws on

Retention definitions are standardized by the analytics tools' own documentation and a few durable industry studies. This page teaches the working core of:

✓Amplitude retention docs - N-Day vs unbounded, 24h window mechanics, the 3.5x studyParts 1 + 3 · variants and boundaries
✓Mixpanel retention docs - "On" vs "on or after", calendar-day conventionParts 1 + 3 · the other default
✓GameAnalytics 2025 benchmarks (11,600 games, 1.48B MAU)Part 3 · medians with vintage
◐devtodev - rolling vs classic retention comparisonPart 1 · why rolling restates; the full trending case in a2
◐Lenny's Newsletter - "Measuring cohort retention" (Olga Berezovsky)Part 3 · denominator discipline; cohort build in a2
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Which D7 variant restates history, and why?

Rolling/unbounded retention keeps the window open forever, so every dormant user who resurfaces rewrites the historical value upward. Classic freezes when day N passes; each bracket freezes when its window closes. A metric whose past keeps moving cannot be trended - keep it off dashboards.

2 · A user installs at 23:50 and returns at 00:10, twenty minutes later. Which convention counts them D1-retained?

Under calendar-day counting (Mixpanel-style) the return lands on a new date, so it is D1 retention twenty minutes after install. Under rolling 24h windows from the start event (Amplitude default) it is still inside hour 0-24, which is day 0. Copy the tool's convention in your SQL before comparing numbers.

3 · DAU/MAU has been rising for three months while every other signal says the product is declining. How can both be true?

Stickiness is a ratio, and a ratio rises whenever its denominator falls faster than its numerator. A product bleeding casual users concentrates down to its daily core and posts its best-ever DAU/MAU on the way down. Check the absolute DAU and MAU trends, and look at the L28 distribution (a4) instead of the average.

Analyst session 1 cheat sheet · pin this

Classic / N-dayReturned exactly day N. Amplitude "N-Day", Mixpanel "On", GameAnalytics default. The benchmark standard. Freezes when day N passes.
Rolling / unboundedReturned day N or later. Mixpanel default; always the biggest; = 1 - churn-by-day-N. History rises retroactively - NEVER for reporting.
Bracket / rangeReturned within a window (days 1-7). Each bracket independent, frozen once closed. Right for weekly-cadence and repurchase products.
The frozen-history lawFix the base at a fixed anchor date, look forward 1/7/14/30 days. Completed windows immutable; only the current window updates, marked partial.
Day boundariesRolling 24h windows from the start event (Amplitude) vs calendar dates in project timezone (Mixpanel). The 23:50 install decides which you have.
CheckpointsD1 = onboarding, D7 = habit (full weekly cycle), D14 = trial/content depth, D30 = monetization horizon. Games often D28 = 4 exact weeks.
StickinessDAU/MAU: ~20% good consumer, 50%+ daily-habit legend. Can RISE during decline (MAU shrinking faster). WAU/MAU for weekly products; L28 for the distribution.
The metric contractFour lines on every notebook: anchor event, active event, window type, timezone convention. All signups vs activated = different question - state which.