33 — Data Analytics

Cohort & Retention Analysis#

AnalyticsStatistical 1 min read
◆ The PatternGroup users by when they arrived, track them over time — the retention heatmap

Averages hide everything in user analytics: growth pumps new users into the numerator and masks that older users are quietly leaving. Cohort analysis fixes this by grouping users by their start month and tracking each cohort separately. The classic output is the retention triangle — rows are cohorts, columns are months since signup, cells are the share still active.

retention(c, t) = active(c, t) / size(c)
Retention = share of cohort c still active t periods after joining. Read down a column to compare cohorts at the same age.
churn = 1 − retention  ·  LTV ≈ ARPU / churn
Churn is retention's complement — and the denominator of the standard lifetime-value approximation.
// Interactive retention triangle — set churn, then let newer cohorts improve
Monthly churn25%
Newer cohorts improve0%
Month-3 retention (latest)—
Read the triangleMeaning
Down a columnSame-age comparison — are newer cohorts retaining better? (Product is improving)
Along a rowOne cohort's decay curve — where is the steepest drop?
Flattening rowsA retained core exists — the curve's plateau is your product-market-fit signal
Rows hitting zeroLeaky bucket — growth is refilling, not compounding
Define "active" first. Logged in? Performed the core action? Paid? Retention numbers are meaningless without a stated activity definition and period — and comparisons across products even more so.
Pattern bridge: Averaging metrics over current users only is survivorship bias — cohort analysis is the antidote, because the churned users stay in their cohort's denominator.
When to use this
✓ Use when: Any subscription or repeat-use product — separating growth from stickiness, judging whether product changes moved long-term behaviour, computing honest LTV.
✗ Skip when: One-shot transactions with no expected repeat behaviour, or cohorts too small for stable percentages (a 20-user cohort moves 5 points per person).
Try it on real data
UCI: Online Retail (540K transactions — build real purchase cohorts)
Assign each customer to their first-invoice month, pivot by months-since, and you have a genuine retention triangle from real e-commerce data.
Quick start — copy to notebook
pip install pandas numpy
# ────────────────────────────────────────
import numpy as np
import pandas as pd

rng = np.random.default_rng(5)
rows = []
for cohort in range(6):                      # signup month
    months_active = rng.geometric(0.25, 500).clip(max=8 - cohort)
    for uid, life in enumerate(months_active):
        rows += [(f"{cohort}-{uid}", cohort, cohort + m)
                 for m in range(life)]

ev = pd.DataFrame(rows, columns=['user_id', 'cohort', 'month'])
ev['age'] = ev['month'] - ev['cohort']
counts = ev.pivot_table(index='cohort', columns='age',
                        values='user_id', aggfunc='nunique')
print((counts.div(counts[0], axis=0) * 100).round(1))
Worked example

From January to June, retention improved in both channels: search from 50.0% to 53.3%, ads from 30.0% to 32.0%. The overall rate fell from 46.0% to 38.4%, because the June cohort came mostly from ads. Reading retention without breaking it down by acquisition mix can report a decline that is really a change in who arrived.

Where it misleads
  • Recent cohorts have not yet had time to churn; averaging curves of different ages biases them.
  • Retention definitions (active in month k, or in any month since) change the curve’s shape.
  • Extrapolating early churn forward understates lifetime when the curve flattens (Fader & Hardie 2007).
In code
cohorts = pd.DataFrame({'channel': ['search', 'ads'] * 2, 'month': ['Jan', 'Jan', 'Jun', 'Jun'],
                        'users': [800, 200, 300, 700], 'retained': [400, 60, 160, 224]})
cohorts['rate'] = cohorts.retained / cohorts.users
overall = cohorts.groupby('month')[['users', 'retained']].sum()
overall['rate'] = overall.retained / overall.users
Python, in your browser — edit the code and run it again

Assumes import numpy as np and import pandas as pd (and SciPy where imported). Each snippet simulates or makes up its own data, as the comments say.

The same idea with a library
# Python — retention triangle in pandas
import pandas as pd

# events: user_id, event_date
events['month'] = events['event_date'].dt.to_period('M')
events['cohort'] = events.groupby('user_id')['month'].transform('min')
events['age'] = (events['month'] - events['cohort']).apply(lambda p: p.n)

counts = (events.drop_duplicates(['user_id', 'month'])
                .pivot_table(index='cohort', columns='age',
                             values='user_id', aggfunc='nunique'))

retention = counts.div(counts[0], axis=0)   # normalize by cohort size
print((retention * 100).round(1))
Sources
  • P. S. Fader & B. G. S. Hardie, “How to Project Customer Retention”, Journal of Interactive Marketing 21(1), 2007
  • E. H. Simpson, “The Interpretation of Interaction in Contingency Tables”, Journal of the Royal Statistical Society B 13(2), 1951
  • P. J. Bickel, E. A. Hammel & J. W. O’Connell, “Sex Bias in Graduate Admissions: Data from Berkeley”, Science 187(4175), 1975

Last reviewed October 2026 · How this site checks its content · Report a mistake

Linked from 2
← Previous
GroupBy, Pivot & Aggregation
Open in the full reader, with the topic sidebar