Собесов
12 августа 2026 г. · 2,4 тыс. просмотров

Пет-проект: когортный анализ и retention — матрица, heatmap, выводы бизнес-языком

Пошаговая сборка пет-проекта по когортному анализу: журнал заказов, когорты по месяцу первой покупки, retention-матрица через pivot_table, heatmap в matplotlib и выводы с реальными цифрами. Плюс вопросы с собеседования по проекту.

Это вторая инструкция серии про пет-проекты аналитика. Общий контекст — зачем нужен проект, где брать данные, как упаковать — в хабе серии. Остальные инструкции: A/B-тест с нуля, дашборд продаж в DataLens, воронка продукта. Код — в репозитории серии, папка 02-cohort-retention.

Что собираем

Маркетинг приводит новых клиентов, выручка растёт. Вопрос проекта: растёт ли база — или мы наливаем воду в дырявое ведро? Суммарная выручка на этот вопрос не отвечает: рост новых клиентов может маскировать отток старых годами. Отвечает когортный анализ: группируем клиентов по месяцу первой покупки и смотрим, какая доля каждой когорты возвращается через 1, 3, 6 месяцев.

Этот проект — про продуктовые метрики, а не про технику. Понимание retention на собеседовании аналитика ценят дороже, чем умение написать красивый цикл: retention напрямую связан с экономикой продукта, и вопросы о нём есть почти в каждом продуктовом интервью.

Данные: синтетический журнал заказов интернет-магазина, np.random.seed(7), 8 000 клиентов, 20 680 заказов за 14 месяцев (2024-01 — 2025-02). Три колонки: customer_id, order_date, revenue. В генератор зашита реалистичная механика: у каждого клиента своя «липкость», а вероятность повторной покупки затухает экспоненциально с каждым месяцем после привлечения. Стек: pandas, numpy, matplotlib. Время: 2–3 часа. Все цифры ниже — из реального прогона.

Шаг 1. Взять журнал заказов

Зачем: определить минимальную модель данных. Для когорт достаточно трёх полей — кто купил, когда и на сколько. Ни каталог товаров, ни профили пользователей не нужны; умение выделить минимум из данных — само по себе навык, который проверяют вопросом «какие данные вам понадобятся».

import pandas as pd

df = pd.read_csv("data/orders.csv", parse_dates=["order_date"])
# columns: customer_id, order_date, revenue

Что должно получиться: DataFrame на 20 680 строк — по строке на заказ.

Шаг 2. Найти дату первого заказа клиента

Зачем: месяц первой покупки определяет когорту — «день рождения» клиента в магазине. Всё дальнейшее отсчитывается от него.

first = df.groupby("customer_id")["order_date"].min().rename("first_date")
df = df.join(first, on="customer_id")

Что должно получиться: у каждой строки заказа появляется дата привлечения его клиента.

Шаг 3. Свести даты к месяцам

Зачем: retention считают в дискретных периодах. Месяц — стандартная гранулярность для e-commerce; для мобильных приложений те же расчёты делают по дням и неделям.

df["order_month"] = df["order_date"].dt.to_period("M")
df["cohort"] = df["first_date"].dt.to_period("M")

Что должно получиться: колонка cohort — месяц первой покупки, колонка order_month — месяц заказа. to_period("M") удобнее строковых срезов: периоды корректно вычитаются друг из друга.

Шаг 4. Посчитать номер месяца каждого заказа

Зачем: перевести календарные даты в «возраст» клиента. 0 — месяц первой покупки, 1 — следующий и так далее. Это ось X будущей матрицы.

df["period_index"] = (
    (df["order_month"].dt.year - df["cohort"].dt.year) * 12
    + (df["order_month"].dt.month - df["cohort"].dt.month)
)

Что должно получиться: целое число месяцев с момента привлечения у каждого заказа.

Шаг 5. Собрать таблицу «когорта × номер месяца»

Зачем: каркас retention-матрицы. В каждой ячейке — сколько уникальных клиентов когорты были активны в тот месяц жизни. Ключевое слово «уникальных»: клиент с тремя заказами за месяц считается один раз, поэтому nunique, а не count.

grp = (df.groupby(["cohort", "period_index"])["customer_id"]
         .nunique()
         .reset_index())
counts = grp.pivot(index="cohort", columns="period_index",
                   values="customer_id")

Что должно получиться: таблица 12 когорт × номера месяцев; правый верхний угол заполнен, левый нижний — NaN (у молодых когорт поздние месяцы ещё не наступили). Этот «треугольник» — нормальная форма когортной таблицы, а не ошибка.

Шаг 6. Перевести в доли

Зачем: когорты разного размера нельзя сравнивать по абсолютам. Делим каждую строку на её нулевую колонку — размер когорты.

cohort_sizes = counts[0]
retention = counts.divide(cohort_sizes, axis=0)

Что должно получиться: retention-матрица. В нулевом месяце везде 100%, дальше — затухание. Средний размер когорты в прогоне — около 666 клиентов.

Шаг 7. Нарисовать heatmap

Зачем: матрица из ста ячеек читается глазами плохо, тепловая карта — мгновенно. Строки — когорты, столбцы — месяцы жизни, цвет — доля активных.

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(12, 7))
im = ax.imshow(retention.values.astype(float),
               aspect="auto", cmap="YlGnBu", vmin=0, vmax=1)
ax.set_xlabel("Месяцев с момента первого заказа")
ax.set_ylabel("Когорта (месяц привлечения)")
# подписи процентов в ячейках — циклом по матрице, с белым текстом на тёмном
fig.colorbar(im, ax=ax, label="доля активных")
fig.savefig("outputs/retention_heatmap.png", dpi=120)

Что должно получиться: outputs/retention_heatmap.png — треугольная тепловая карта. На ней сразу видны две вещи: резкий перепад цвета между месяцем 0 и месяцем 1 и то, что строки почти идентичны — когорты ведут себя одинаково.

Шаг 8. Посчитать средние ориентиры

Зачем: одна цифра на контрольную точку удобнее матрицы — и для сравнения с бенчмарками, и для разговора с бизнесом. Усредняем колонки 1, 3 и 6 по всем когортам, у которых эта точка уже наблюдаема.

avg = lambda n: retention[n].dropna().mean()
r1, r3, r6 = avg(1), avg(3), avg(6)

Что должно получиться:

Клиентов: 8000   Заказов: 20680   Когорт: 12
Доля клиентов хотя бы с одним повтором: 72.3%

Средняя кривая удержания:
  Месяц 0:  100.0%
  Месяц 1:   34.8%   <- самый резкий обрыв
  Месяц 3:   20.4%
  Месяц 6:    9.0%

Шаг 9. Сделать вывод бизнес-языком

Зачем: «retention к M1 = 35%» — не вывод, а показание прибора. Вывод отвечает на вопрос, что делать.

Что должно получиться — четыре наблюдения из прогона:

  • Самый резкий обрыв — сразу после первой покупки: к первому месяцу активны 34,8% когорты. Бороться надо за второй заказ, а не за десятый: к шестому месяцу (9,0%) спасать уже почти некого.
  • Падение после первого месяца плавное и предсказуемое: 20,4% к третьему месяцу, 9,0% к шестому, без аномальных провалов в отдельных когортах. Значит проблема системная — продукт или онбординг, а не разовый сбой.
  • При этом 72,3% клиентов делают хотя бы один повтор — люди в принципе готовы возвращаться. Теряем их рано, а не насовсем; потенциал удержания есть.
  • Строки heatmap почти идентичны: качество привлекаемого трафика стабильно из месяца в месяц, дело не в «плохих» когортах.

Обратите внимание, как уживаются 34,8% retention к M1 и 72,3% повторных покупателей: первый показатель — про конкретный месяц, второй — про всю жизнь клиента в окне наблюдения. Клиент мог пропустить месяц 1 и вернуться в месяце 2 — в retention M1 он не попал, в долю повторных попал. Путаница между этими метриками — классическая ловушка, и вопрос о ней на собеседовании звучит регулярно.

Как рассказывать про проект на собеседовании

Заготовка на 90 секунд: «Я собрала когортный анализ удержания на журнале заказов: 8 000 клиентов, 20 680 заказов, 14 месяцев. Сгруппировала клиентов по месяцу первой покупки, построила retention-матрицу через pivot_table и heatmap. Главный вывод — обрыв сосредоточен между первой и второй покупкой: к первому месяцу остаётся 34,8%, дальше кривая затухает плавно. При этом 72% клиентов хотя бы раз возвращаются — то есть теряем людей рано, а не навсегда, и точка приложения усилий — онбординг и стимулирование второго заказа, а не реанимация старых клиентов».

Акценты: выбор когортной гранулярности, nunique вместо count, треугольная форма матрицы как норма, вывод в терминах действий.

Вопросы, которые зададут по этому проекту

— Что значит «retention к месяцу 1 = 34,8%»? Из клиентов, сделавших первую покупку в некотором месяце, 34,8% совершили хотя бы один заказ в следующем календарном месяце их жизни. Усреднено по 12 когортам. Это month-N retention: активность именно в месяце N, а не «дожитие до месяца N».

— Почему retention M1 = 34,8%, а повторные покупки у 72,3%? Числа противоречат друг другу? Нет, это разные метрики. 72,3% — доля клиентов хотя бы с одним повтором за всё окно наблюдения; 34,8% — активность в конкретный первый месяц. Клиент, вернувшийся через два месяца, входит в 72,3%, но не входит в 34,8%. Разрыв между этими числами сам по себе информативен: возвраты растянуты во времени.

— Почему когорты месячные, а не недельные? Гранулярность подбирается под цикл покупки. В e-commerce с межпокупочным интервалом в недели месяц — стандарт: недельные когорты были бы малы и шумны. Для мобильного приложения с ежедневным использованием я бы взяла day-1/day-7/day-30.

— В матрице половина ячеек пустая. Это ошибка? Это устройство данных: у когорты, пришедшей в последнем месяце окна, наблюдаем только месяц 0. Поэтому матрица треугольная, а средние по колонкам я считаю только по когортам, у которых точка наблюдаема, — dropna().mean(). Смешивать наблюдаемые и ненаблюдаемые точки нельзя, это занизило бы retention.

— Какие действия предложите по результатам? Обрыв сконцентрирован между первой и второй покупкой, значит гипотезы — вокруг раннего опыта: welcome-цепочка, стимул второго заказа в первые 2–4 недели, проверка качества первого заказа (сроки доставки, соответствие ожиданиям). Эффект мерила бы по retention M1 целевых когорт против прошлых — а лучше A/B-тестом, как в первом проекте серии.

— Как бы вы посчитали это на SQL? Та же логика: CTE с минимальной датой заказа по клиенту, датировка месяцев через DATE_TRUNC, разница месяцев — номер периода, затем GROUP BY когорта и период с COUNT(DISTINCT customer_id) и деление на размер когорты через оконную функцию или join. Pandas-версия отображается в SQL один в один — pivot здесь только форма выдачи.

— Данные синтетические. Что изменится на реальных? Появится грязь: возвраты и отмены заказов (считать ли их активностью — решение, которое надо зафиксировать), дубли клиентов с разными id, сезонность, промо-всплески, ломающие сравнимость когорт. И вырастет цена вопроса об определении «активности»: заказ, визит или любое событие — три разных retention.

Что дальше

Следующий проект серии — дашборд продаж в Yandex DataLens: от аналитики в коде к витрине, которую открывают по ссылке. Обзор серии и источники данных — в хабе.

Источники


Проект закрывает «покажите, что понимаете продуктовые метрики». Смежные вопросы собеседований — определения retention, разбор метрик, кейсы про удержание — тренируются на задачах каталога; часть открыта бесплатно.

Другие статьи