datarekha

Cohort analysis

The retention triangle every PM asks for — assign users to a signup cohort, measure their activity month by month, and pivot it into the analyst flagship.

8 min read Advanced SQL Lesson 17 of 27

What you'll learn

  • Assigning each user a cohort (their signup month)
  • Computing per-cohort activity in each later month
  • Normalising counts into retention percentages
  • Pivoting the long form into the wide 'triangle'

Before you start

Every PM asks for it eventually: “How sticky are our users? What’s month-1 retention by signup month?” The answer is a cohort retention table — a triangle where each row is a cohort (everyone who started in the same month), each column is a month-since-signup, and each cell is the share of that cohort still active then.

It is the analyst’s flagship deliverable, and it is entirely SQL once you break it into four named steps: assign each user a cohort, flag their active months, join activity against tenure, and pivot into the wide shape. CTEs are made for this — one per step.

The build

Cohort and activity month come from truncating dates to the month (SQLite’s strftime('%Y-%m', …)). Tenure is the gap between them, and retention is active users over cohort size:

WITH cohorts AS (
  SELECT id AS user_id, strftime('%Y-%m', signup_date) AS cohort_month
  FROM   users
),
sizes AS (
  SELECT cohort_month, COUNT(*) AS cohort_size FROM cohorts GROUP BY cohort_month
),
activity AS (
  SELECT DISTINCT user_id, strftime('%Y-%m', order_date) AS active_month
  FROM   orders
),
retention AS (
  SELECT c.cohort_month,
         CAST((julianday(a.active_month || '-01') - julianday(c.cohort_month || '-01')) / 30 AS INT) AS m,
         COUNT(DISTINCT c.user_id) AS active_users
  FROM   cohorts c
  JOIN   activity a ON a.user_id = c.user_id
  GROUP BY c.cohort_month, m
)
SELECT r.cohort_month, s.cohort_size,
       ROUND(100.0 * MAX(CASE WHEN m = 0 THEN active_users END) / s.cohort_size, 1) AS m0,
       ROUND(100.0 * MAX(CASE WHEN m = 1 THEN active_users END) / s.cohort_size, 1) AS m1
FROM   retention r
JOIN   sizes s ON s.cohort_month = r.cohort_month
GROUP BY r.cohort_month, s.cohort_size
ORDER BY r.cohort_month;

The one new idiom is MAX(CASE WHEN m = N THEN active_users END)conditional aggregation, which pulls each tenure month into its own column (SQLite has no PIVOT). On a tiny dataset — a 3-user January cohort and a 4-user February cohort — it produces the triangle:

month 0month 12024-01 (n=3)2024-02 (n=4)66.7%33.3%100%
Two of three January users returned in month 0, one in month 1. The February cohort is too young to have a month-1 number yet — hence the empty cell.

Two things make this a real artifact rather than a pile of counts. Dividing by cohort_size turns raw active-user counts into percentages, which is the only way to compare cohorts of different sizes — 50 actives means nothing until you know the cohort was 100 or 10,000. And the triangle shape is not a bug: a cohort that signed up last month simply cannot have a month-1 number yet, so the bottom-right is empty.

Practice

Quick check

0/3
Q1What does a 'cohort' usually correspond to?
Q2Why divide active_users by cohort_size?
Q3Why does the cohort table form a triangle, with empty cells bottom-right?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
30-day retention dropped from 42 % to 31 % over the last two months. How do you diagnose the root cause?

A retention drop investigation requires distinguishing between an acquisition-mix shift (newer cohorts are lower quality) and a genuine product regression (existing cohorts are performing worse). The two look identical in aggregate retention but have completely different fixes. Cohort analysis — plotting the D30 survival curve for each weekly acquisition cohort — is the first move.

How would you measure user engagement for a mobile app — what metrics would you use and how would you structure them?

Engagement is multi-dimensional: breadth (how many users engage), depth (how much they do per session), and frequency (how often they return). A robust engagement framework stacks these three layers into a metric hierarchy and links them to retention curves, because engagement that does not predict long-term retention is usually noise.

What is the difference between leading and lagging indicators, and how do you use both when building a metrics system?

Lagging indicators (revenue, annual retention, NPS) measure outcomes after they have occurred — they are accurate but slow. Leading indicators (D1 retention, feature adoption rate, time-to-value) correlate with future outcomes and are available faster, making them suitable for early experiment decisions. A robust metrics system pairs both, with the leading metric as the experiment signal and the lagging metric as the validation gate.

Walk me through the full ML lifecycle from problem definition to model retirement.

The ML lifecycle spans eight phases: problem framing, data collection and validation, feature engineering, training and experimentation, offline evaluation, deployment, production monitoring, and retirement or retraining. Each phase has distinct owners, artefacts, and failure modes that an MLOps practice must systematise.

Related lessons

Explore further

Skip to content