Where this session sits
Eight analyst sessions, one running artifact: a retention report your stakeholders can act on. Session a1 nailed down what "day-7 retention" even means (three different things, it turns out). Today builds the table everything else reads from. The viz gallery (a3), segmentation (a4), survival analysis (a5), CLV models (a6), and churn prediction (a7) all start from the cohort table you build here, and the capstone (a8) ships it. Work along in the companion notebook notebooks/a2_cohort_heatmap.ipynb on the repo's synthetic data, then re-run on the real Online Retail II dataset.
One cohort, one row, frozen forever 6 min live
A registration cohort is everyone who joined in the same month. Month 0 is their join month, so month 0 retention is 100% by construction. Month 1 is the percent who came back the next calendar month, and so on. Each cohort gets one row, each age gets one column - and because young cohorts have not lived long enough to fill their late columns, the table is a triangle. The empty corner is not missing data. It is the future.
LiveFrozen history: why a completed cohort never restates4 min▶
A retention number you can trend, compare, and set targets against is computed one way: fix the base, look forward. The base is a cohort anchored to a fixed date - everyone who registered in January, or everyone active on Aug 1. Then you look forward 1, 7, 14, 30 days (or 1 to 10 months) and count who came back. Because the base never changes and the window eventually closes, a completed cell is immutable. January's month-3 retention is the same number today, next quarter, and forever.
- Forward-looking, cohort-based: the question is always "of the people who started at time T, how many returned within x days?" The denominator is locked at time T.
- Frozen once complete: when the window has fully elapsed, the number is final. Your January row will never restate. A dashboard whose history rewrites itself cannot be trusted, compared, or targeted against.
- Only the current window updates: the newest diagonal of the triangle is still filling. Mark it as partial - an outline, an asterisk, a lighter shade - so nobody reads a half-filled month as a collapse.
Self-studyWhy month 0 is 100% and other denominator fine print3 min read▶
The cohort table hides three denominator decisions. Make them explicitly, write them on the dashboard, and never change them silently mid-year.
- Who counts as joined? All signups, or only activated users (completed a first core action)? Activated-base retention reads far higher and answers a different question: product stickiness versus funnel-plus-product combined. Both are legitimate. Mixing them is not.
- What counts as active? For a game: opened a session. For ecommerce: placed an order - login-based retention is nearly meaningless for shops, since people browse without buying. Purchase-based cohorts are the ecommerce standard.
- Which calendar? Monthly periods for a 10-month view like ours; weekly for fast products. Games use day-based windows (D1/D7/D30) since play cadence is daily - a1 covers how day boundaries and timezones can make two tools disagree on the same data.
Five lines of pandas 8 min live
The whole construction is one idea: stamp every order with the customer's first-ever month, then count distinct customers per (cohort, age) pair. Everything else is bookkeeping. We build it on the repo's synthetic orders file first - where the answer is known because we generated the churn - then you re-run the identical cells on Online Retail II.
Live★ Build-along: the recipe, line by line8 min▶
Load the repo's synthetic ecommerce orders and run each stage. Every line below is in the companion notebook with its printed output.
Read the result before you plot it. Column 0 is all 1.00 - everyone is active in their join month, by construction. Each row steps down as the cohort ages. The bottom-right is NaN, and it must stay NaN: those windows have not completed. NaN here means "not yet", never "zero".
The same five lines run a game studio's dashboard. Swap orders for session logs, month for day, and the identical stamp-age-count-divide pipeline produces D1/D7/D30 tables. Mobile publishers gate soft-launch decisions on exactly this artifact - Supercell killed Smash Land in 2015 off its soft-launch cohort tables even though player feedback was positive. The table you just built is a kill-or-scale decision instrument.
Self-studyOnline Retail II: what real data does to your clean recipe5 min read▶
The notebook's second half re-runs the pipeline on the UCI Online Retail II dataset (1,067,371 rows, a UK gift retailer, Dec 2009 to Dec 2011). It is deliberately messy, and every mess is a lesson:
- Cancellations: invoices starting with "C" are cancellations carrying negative quantities. Keep them and your cohort sizes are wrong. Filter first.
- Missing customers: around 135k rows have no CustomerID (guest checkouts) - even though the dataset's own documentation claims no missing values. Never trust metadata over a
df.isna().sum(). - The inflated first cohort: the earliest month sweeps in every pre-existing customer as if they were new - their real first purchase happened before the data starts. Left-censoring. Either drop the first cohort or annotate it.
- Wholesalers: a handful of trade buyers order weekly and never churn. They are real, but they will dominate any average - segment them out in a4.
Render the heatmap properly ★ 12 min · everyone builds
A cohort table full of decimals is analyst food. The heatmap is what everyone else reads. Four choices separate a professional one from a misleading one: annotate every cell, mask the future, anchor the color scale, and use a colorblind-safe single-hue ramp.
Live★ Build-along: the seaborn call and its four deliberate choices6 min▶
- annot + fmt=".0%": the number is the data; color is only the index that guides the eye to it.
- mask: an unmasked NaN renders at the colormap floor - the darkest cell on the chart - and every executive will read the future as a catastrophe. This is the single most common cohort-chart bug in the wild.
- vmin/vmax: without anchors, seaborn rescales to the data every refresh, so this month's chart is not comparable to last month's. Frozen history deserves a frozen color scale.
- cmap: sequential single-hue (Oranges, Blues) or perceptually uniform (viridis, cividis). Never jet or rainbow - they invent boundaries that are not in the data - and never red-green encodings, which about 8% of men cannot separate.
Run the heatmap cell on the synthetic data. Find the month-1 column and read it downward: is the product improving for newer cohorts?
Delete the mask argument and re-run. Watch the empty corner turn into the darkest cells on the chart. Put it back. Now you have seen the bug you will spend your career catching in other people's decks.
Delete vmin/vmax and re-run. The colors shift even though not one number changed. Anchors back in.
Re-run the whole pipeline on Online Retail II after the clean-up gate. Say out loud what the first cohort's row means, knowing it is left-censored.
The flattening row that bought a company. Dollar Shave Club's cohort rows flattened at roughly 50% six-month retention - transaction data later confirmed by Second Measure showed it led its whole category. Investors read that flattening as durable revenue, and the story ended in a $1B acquisition by Unilever. Same artifact you just built, rendered the same way.
Try it yourself - this week ◐ 30 min total
- Run the companion notebook end to end on the synthetic data, then on Online Retail II. Both heatmaps go in your running retention report - the capstone (a8) assembles it.
- Build the same table on YOUR product's data: pick the anchor event (registration or first purchase), the active event, and the period size. Write all three choices at the top of the notebook as comments.
- Find one existing retention chart at your company and check it against the four choices: annotated? masked? anchored? safe colormap? Bring the worst offender to the next session.
- Answer in one sentence: why can your January cohort's month-3 number never change after April ends?
What this session draws on
Cohort analysis has no single certificate - it is a craft standardized by the analytics tools and a handful of durable essays. This page teaches the working core of:
Three questions before you go 🎯 ◐ 90 seconds
1 · Your January cohort's month-2 retention was 34% when you reported it in April. It is now August. What is January's month-2 retention?
Forward-looking cohort retention locks the base (January joiners) and the window (month 2). Once March ended, every fact the number depends on is in the past. "Came back later" is rolling retention - the metric that rewrites its own history.
2 · You forget the mask argument and NaN cells render at the colormap floor. What does your VP see?
Unmasked NaN takes the darkest end of the scale, so windows that simply have not happened yet read as 0% retention. Mask with retention.isnull() so immature cells stay blank - "not yet" is not "zero".
3 · Month-1 retention read down the column: Jan 46%, Feb 48%, Mar 51%, Apr 55%. What is this telling you?
Down a column every cohort is compared at the SAME age, so cohort-over-cohort improvement is the cleanest read of whether changes since January actually work. (One cohort recovering would show across its row; a calendar effect would show on a diagonal.)