Условие
Дан events(user_id, ts). Построить когортный retention по неделям регистрации: процент пользователей, вернувшихся в неделю 1, 2, …, 12 после первой активности.
Решение
Подход
- Для каждого юзера — неделя его первой активности (cohort).
- Для каждой активности — её неделя.
- Разница недель = «возраст когорты».
- Pivot:
cohort × week_offset → unique users / cohort_size.
Реализация
import pandas as pd
events['week'] = events['ts'].dt.to_period('W').dt.start_time
events['cohort'] = events.groupby('user_id')['week'].transform('min')
events['offset'] = ((events['week'] - events['cohort']).dt.days // 7).astype(int)
cohort_users = (
events.groupby(['cohort', 'offset'])['user_id']
.nunique()
.reset_index(name='active')
)
cohort_size = cohort_users[cohort_users['offset'] == 0][['cohort', 'active']] \
.rename(columns={'active': 'cohort_size'})
cohort_users = cohort_users.merge(cohort_size, on='cohort')
cohort_users['retention'] = cohort_users['active'] / cohort_users['cohort_size']
retention_matrix = cohort_users.pivot(index='cohort', columns='offset', values='retention')Визуализация
import seaborn as sns
sns.heatmap(retention_matrix, annot=True, fmt='.0%', cmap='YlGnBu')Подводные камни
- Cohort = неделя первой активности; если в первой неделе у юзера 5 событий, retention в
offset=0всё равно 100% (он там по определению). - Survivorship bias: для свежих когорт ещё мало недель пройдено — нижний треугольник пуст, но это не «упал retention», это «не дожило».
- Граница недели зависит от tz и от
dt.to_period('W')(по умолчанию W-SUN). Зафиксируйтеto_period('W-MON')для понедельной. nuniqueважно — пользователь, заглянувший дважды, считается раз.- Для очень больших данных pivot может взорвать память — используйте polars или уменьшайте до интересующего слайса.
Эталонный ответ
Cohort = неделя первого ивента, offset = (week − cohort)/7. Pivot cohort × offset с nunique(user_id), делить на cohort_size.