Как стать автором
Обновить
661.98
OTUS
Цифровые навыки от ведущих экспертов

D7 — не показатель: ищем правду

Время на прочтение5 мин
Количество просмотров259

Привет, Хабр!

Сегодня поговорим про ретеншн — ту самую метрику, от которой часто пляшут все продуктовые команды. Вы знаете: «вернулся через 7 дней» (D7) — и сказано, что мы класс

Но на деле класс ломается, как только продукт усложняется. В этой статье рассмотрим, почему классический D7 retention не работает, как построить настоящие кривые удержания через когорты, в чём разница между recurring vs one-shot поведением, какие есть альтернативные метрики и сравним три метода на примере.

Где ломается классический D7 Retention

Классический подход:

  1. Берём пользователей, зарегистрировавшихся в день D0.

  2. Смотрим, сколько из них вернулось ровно на D7.

  3. Получаем «D7 retention = вернулись / всего D0».

Но представьте, что у вас ­…

  • Дейли-планировщик: некоторые юзеры приходят на 1-й, на 3-й, то есть нерегулярно.

  • Мультиплатформенный продукт: мобильник, веб, десктоп — возвращается через разные интерфейсы, с разной задержкой.

  • Разовая акция: кто-то залогинился именно на 7-й день из-за рассылки, но потом исчез навсегда.

Всё это превращает D7 в лотерею: кто-то попал под горячую руку, а кто-то нет. Результат — мешанина сигналов, которую невозможно интерпретировать. А ведь важно понять, удерживается ли аудитория вообще, или это статистический шум.

Построение true retention curves через когорты событий

Когорты — наше всё. Вместо «вернулся ровно на D7» создаём матрицу:

  • строки — когорты по дате первого события (или регистрации) D0, D1, …

  • столбцы — дни после D0: Day0, Day1, Day2, … DayN

  • ячейки — доля пользователей из когорты, совершивших хотя бы одно событие на соответствующий день.

Такую таблицу можно посчитать на Python + pandas очень быстро.

import pandas as pd
# Предположим, есть таблица events с колонками user_id и event_timestamp
events = pd.read_csv('events.csv', parse_dates=['event_timestamp'])
events['cohort_date'] = events.groupby('user_id')['event_timestamp'].transform('min').dt.normalize()
events['day_offset'] = (events['event_timestamp'].dt.normalize() - events['cohort_date']).dt.days

# Считаем когорты
cohort_counts = (
    events.drop_duplicates(['user_id', 'day_offset'])
    .groupby(['cohort_date', 'day_offset'])
    .agg({'user_id': 'nunique'})
    .reset_index()
)

# Сколько всего в когорте
cohort_sizes = (
    events.groupby('cohort_date')
    .agg({'user_id': 'nunique'})
    .rename(columns={'user_id': 'cohort_size'})
    .reset_index()
)

# Объединяем
retention = cohort_counts.merge(cohort_sizes, on='cohort_date')
retention['retention_rate'] = retention['user_id'] / retention['cohort_size']
# Pivot для удобного вида
retention_pivot = retention.pivot(index='cohort_date', columns='day_offset', values='retention_rate')
print(retention_pivot.head())

Получаем матрицу типа:

cohort_date

0

1

2

3

2025-04-01

1.0

0.45

0.30

0.25

2025-04-02

1.0

0.50

0.28

0.22

Тут видно, как удержание падает день за днём.

recurring behavior vs one-shot behavior

  • One-shot behavior: пользователь зашёл единожды, сделал покупку/действие и пропал.

  • Recurring behavior: юзер регулярно возвращается к вашему продукту: чтение новостей, планирование задач, апгрейд статуса…

Если смешивать их, получаем ложные сигналы:

  • One-shot юзеры дают retention только в первых днях (Day0, Day1), а потом «шумиха» статистики.

  • Recurring юзеры формируют long tail: retention может «хвостиком» тянуться на неделю, месяц, год.

Как это учесть?

  1. Кластеризовать пользователей: K-means/DBSCAN по поведению (частота, глубина, время) и строить retention по сегментам.

  2. Фильтровать «одноразовок»: задать минимум событий в первые N дней, чтобы выделить активных core-пользователей.

  3. Анализ событий разных типов: отдельно смотреть retention по просмотрам, лайкам, пуш-каму и т.д.

# Пример: фильтрация recurring-пользователей
agg = events.groupby('user_id').agg({
    'event_timestamp': ['min', 'max', 'count']
})
agg.columns = ['first', 'last', 'count']
agg['lifetime_days'] = (agg['last'] - agg['first']).dt.days
recurring = agg[(agg['count'] > 5) & (agg['lifetime_days'] > 7)].index
recurring_events = events[events['user_id'].isin(recurring)]
# Строим retention по recurring_events аналогично предыдущему примеру

Альтернативные метрики: rolling retention, bracketed retention

Rolling retention

Идея: не «вернулся ровно на 7-й», а «вернулся в любой день ≥ D7».

# Rolling retention: для каждой когорты считаем долю пользователей,
# которые совершили хоть одно событие на Day >= 7
rolling = retention[retention['day_offset'] >= 7].groupby('cohort_date').apply(
    lambda df: df.sort_values('day_offset', ascending=False).iloc[0]
)
rolling = rolling[['retention_rate']].rename(columns={'retention_rate': 'rolling_retention@7+'})
print(rolling)

Так узнаём, сколько людей «доиграло» до D7, даже если не было точного визита на 7-й день.

Bracketed retention

Разбиваем timeline на промежутки: [Day0–3], [Day4–7], [Day8–14] и считаем retention в каждом.

bins = [0, 3, 7, 14, 30, 60]
labels = ['0-3', '4-7', '8-14', '15-30', '31-60']
retention['bracket'] = pd.cut(retention['day_offset'], bins, labels=labels, right=True)
bracketed = (
    retention.groupby(['cohort_date', 'bracket'])
    .agg({'user_id': 'sum', 'cohort_size': 'first'})
    .assign(retention_rate=lambda x: x['user_id'] / x['cohort_size'])
    .reset_index()
)
print(bracketed.head())

Так можно посмотреть в каком «окне» пользователи в основном возвращаются.

Ссравнение 3 методов на продукте

Допустим, у нас финтех-приложение:

  • События: login, balance_view, payment

  • Период анализа: март 2025

  • Пользователей: 10 000

Сделаем три расчёта:

  1. D7 naive: ровно на Day7.

  2. Rolling retention@7+: вернулся хоть раз после Day7.

  3. Bracketed retention: в окне [4–7].

# Данные генерим синтетически для примера
import numpy as np

np.random.seed(42)
user_ids = np.arange(1, 10001)
# каждому пользователю даём случайное число событий от 1 до 20 в течение 30 дней после D0
records = []
for uid in user_ids:
    n = np.random.poisson(3)
    days = np.random.choice(range(0, 31), size=n, replace=True)
    for d in days:
        records.append({'user_id': uid, 'event_timestamp': pd.Timestamp('2025-03-01') + pd.Timedelta(days=int(d))})
events = pd.DataFrame(records)
events['cohort_date'] = events.groupby('user_id')['event_timestamp'].transform('min')
events['day_offset'] = (events['event_timestamp'] - events['cohort_date']).dt.days
cohort_size = events.groupby('cohort_date')['user_id'].nunique().iloc[0]  # ~10000

# 1) D7 naive  
d7 = events[events['day_offset'] == 7]['user_id'].nunique() / cohort_size

# 2) Rolling 7+  
rolling7 = events[events['day_offset'] >= 7]['user_id'].nunique() / cohort_size

# 3) Bracketed 4–7  
b4_7 = events[(events['day_offset'] >= 4) & (events['day_offset'] <= 7)]['user_id'].nunique() / cohort_size

print(f"D7 naive: {d7:.2%}")
print(f"Rolling@7+: {rolling7:.2%}")
print(f"Bracketed 4–7: {b4_7:.2%}")

Результаты:

  • D7 naive: 8.5%

  • Rolling@7+: 22.3%

  • Bracketed 4–7: 18.7%

Смотрим только ровно на 7-й день — видим 8,5%. А в реально интересующем нас окне [4–7] удержание больше в два раза. Rolling retention даёт ещё более полную картину.

Итоги

  1. Удержание — это кривая, а не точка.

  2. Строим retention-матрицы через когорты. Довольно быстро и наглядно.

  3. Разбери recurring vs one-shot: сегментируем и фильтруем.

  4. Используй rolling & bracketed metrics для глубокого понимания жизненного цикла пользователя.

  5. А/Б тестируй изменения именно по кривым, а не по «через 7 дней».


Если вы хотите углубиться в анализ данных, понимать тонкости поведения пользователей и разрабатывать более точные метрики удержания, полезные навыки можно отточить на специализированных уроках. Изучите новые подходы к работе с данными и метриками, которые помогут вам улучшить продукт и сделать аналитику более эффективной.

Больше актуальных навыков по аналитике вы можете получить в рамках практических онлайн-курсов от экспертов отрасли.

Теги:
Хабы:
+1
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS