Раздел 42 · Аналитика данных с нуля

Retention: когортная таблица

middle~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Мы возвращаемся в Пограничье. В боссе блока C мы считали деньги магазина, потом были магазин настолок, доставка и стратегия. Теперь у нас в руках JOIN, даты и окна, и пора собрать главные продуктовые метрики игры. На этот раз ставка выше одного заказчика: тот же отчёт прочтёт Фрея, и по нему «Монетка» решит, брать ли агентство на свой тест. Начнём с метрики, о которой спрашивают первой: возвращаются ли новички.

Мерный стакан для новичков

Рост числа игроков ничего не говорит о том, держит ли игра людей. Его может давать реклама: каждую неделю приходят сотни новых и тут же уходят. Это как ведро с дырой: если лить воду быстро, уровень растёт, хотя вода утекает.

Дыру в ведре меряет retention. Определение мы дали в уроке про метрики: retention Dn это доля когорты, которая зашла в игру ровно на n-й день после регистрации, день регистрации нулевой. Когорты мы уже строили в уроке про «Гонца», теперь применим их к игре.

Шаг 1: день жизни игрока

Для каждой сессии считаем, на какой день жизни игрока она пришлась. Разность двух дат в PostgreSQL это целое число дней:

SELECT p.id,
       s.started_at::date - p.registered_at::date AS day_no
FROM players p
JOIN sessions s ON s.player_id = p.id
ORDER BY p.id, day_no
LIMIT 4;
 id | day_no
----+--------
  1 |      0
  2 |      0
  2 |      6
  2 |      8

Игрок 1 сыграл только в день регистрации. Игрок 2 вернулся на шестой и восьмой день. Даты берём в UTC, как они лежат в базе: регистрация и сессии должны жить в одном календаре.

Шаг 2: D1, D7, D30 по когортам

Игрок «вернулся на день n», если у него есть хотя бы одна сессия с day_no = n. Это проверка существования, поэтому подходит EXISTS. Тестовые аккаунты студии с почтой @studio.dev выбрасываем, как в уроке про чистку строк.

WITH reg AS (
  SELECT id, registered_at::date AS d0,
         date_trunc('month', registered_at)::date AS cohort
  FROM players
  WHERE email NOT LIKE '%@studio.dev'
    AND registered_at < '2026-06-01'
),
act AS (
  SELECT DISTINCT player_id, started_at::date AS d FROM sessions
)
SELECT cohort,
       count(*) AS players,
       round(100.0 * count(*) FILTER (WHERE EXISTS (
         SELECT 1 FROM act WHERE act.player_id = reg.id AND act.d = reg.d0 + 1)) / count(*), 1) AS d1,
       round(100.0 * count(*) FILTER (WHERE EXISTS (
         SELECT 1 FROM act WHERE act.player_id = reg.id AND act.d = reg.d0 + 7)) / count(*), 1) AS d7,
       round(100.0 * count(*) FILTER (WHERE EXISTS (
         SELECT 1 FROM act WHERE act.player_id = reg.id AND act.d = reg.d0 + 30)) / count(*), 1) AS d30
FROM reg
GROUP BY cohort
ORDER BY cohort;
   cohort   | players |  d1  |  d7  | d30
------------+---------+------+------+-----
 2026-01-01 |     539 | 23.7 | 18.4 | 8.9
 2026-02-01 |     587 | 23.5 | 21.3 | 8.0
 2026-03-01 |     842 | 21.7 | 18.4 | 6.8
 2026-04-01 |     694 | 22.3 | 17.4 | 8.9
 2026-05-01 |     703 | 21.5 | 16.4 | 6.7

Почему только до 1 июня? Игрок, пришедший 20 июня, физически не мог зайти на 30-й день: данные кончаются 30 июня. Если оставить июнь, его D30 будет нулём, и график покажет обвал, которого нет. Мы уже встречали это в уроке про когорты: недозревшая когорта не годится для сравнения.

Мартовская когорта самая большая, 842 игрока: в марте шла кампания у блогеров. Если разложить игроков по каналу, у пришедших от блогеров D30 всего 3,6%, а у остальных каналов 8,4-9%. Блогеры приводят много людей, которые быстро уходят. Это ещё всплывёт в юнит-экономике.

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.