Когорта — группа пользователей объединённая по дате первой активности (регистрации, первого заказа, установки app). Обычно — месячные когорты (январская когорта = все кто зарегался в январе).
Без когорт ты видишь усреднённую картину. С когортами — видишь паттерн:
Retention(t) для когорты C = доля юзеров когорты C активных в период t (с момента acquisition):
Retention(cohort=C, month_since=t) =
(active_users_in_cohort_C_at_month_t) /
(cohort_size_C)
* 100%
Пример:
Январская когорта = 1000 юзеров (cohort_size)
Через 1 месяц активны = 400 → Retention_M1 = 40%
Через 3 месяца активны = 200 → Retention_M3 = 20%
Через 6 месяцев активны = 120 → Retention_M6 = 12%
Самый частый SQL-вопрос с собеса. Считаем retention для каждой когорты по месяцам:
WITH cohorts AS (
-- Cohort month = месяц первой активности
SELECT
user_id,
DATE_TRUNC('month', MIN(event_date)) AS cohort_month
FROM events
GROUP BY user_id
),
activity AS (
-- Все активные месяцы каждого юзера
SELECT
e.user_id,
DATE_TRUNC('month', e.event_date) AS event_month,
c.cohort_month,
EXTRACT(MONTH FROM AGE(
DATE_TRUNC('month', e.event_date),
c.cohort_month
)) AS months_since_acq
FROM events e
JOIN cohorts c USING (user_id)
)
SELECT
cohort_month,
months_since_acq,
COUNT(DISTINCT user_id) AS active_users,
ROUND(
COUNT(DISTINCT user_id) * 100.0 /
FIRST_VALUE(COUNT(DISTINCT user_id))
OVER (PARTITION BY cohort_month ORDER BY months_since_acq),
2
) AS retention_pct
FROM activity
WHERE months_since_acq <= 6
GROUP BY cohort_month, months_since_acq
ORDER BY cohort_month, months_since_acq;
Аналог на pandas — через groupby + transform + pivot:
import pandas as pd
def cohort_retention(events: pd.DataFrame) -> pd.DataFrame:
# 1. Cohort month для каждого юзера
events['cohort_month'] = events.groupby('user_id')['event_date'].transform('min').dt.to_period('M')
events['event_month'] = events['event_date'].dt.to_period('M')
# 2. Месяцев с момента acquisition
events['months_since'] = (events['event_month'] - events['cohort_month']).apply(lambda x: x.n)
# 3. Pivot: cohort × months_since → unique users
cohort_table = events.groupby(['cohort_month', 'months_since'])['user_id'] \
.nunique() \
.unstack(fill_value=0)
# 4. Retention % от cohort size (M0)
cohort_sizes = cohort_table.iloc[:, 0]
retention = cohort_table.divide(cohort_sizes, axis=0) * 100
return retention.round(2)
Что новички делают не так:
Стандартное представление — cohort heatmap: строки = когорты, колонки = месяцы с момента acquisition, цвет = retention %. Темнее = лучше. Видишь паттерны: одни когорты лучше, другие хуже.
Обычный retention — усреднённый по всем юзерам. Когорты показывают паттерн во времени: стало лучше или хуже для новых юзеров.
Месячные — стандарт. Недельные — для быстрорастущих стартапов или анализа фич. Дневные — для коротких циклов (новости, app drop).
Retention на N-й день / неделю / месяц после первой активности. N1 = retention на 1-й день после регистрации (стандарт mobile-приложений).
Нет, применяется везде где есть пользователи и время: маркетинг (cohort LTV по каналам), кредиты (vintage analysis — когорты выдачи), HR (когорты сотрудников по месяцу найма).
Зависит от definition. Чаще всего: активность в конкретном месяце N после acquisition. Returning user после паузы — считается активным в месяц возврата.