Условие
Дан csv test_analyst.csv с поведенческими событиями игроков:
us_id— user idevent_name—ad_start_session(заход),ad_banner_impression(показ баннера),ad_interstitial_impression(показ inter-рекламы)created_at— время события (unix-time)installed_at— время установки приложения (unix-time)
Задачи:
- Рассчитать DAU на каждую дату периода 18.05–01.06.
- Кумулятивные показы на пользователя по типу рекламы до 12 дня жизни когорты. Таблица: строки = дата установки, столбцы = день жизни (0..12), вторая колонка — размер когорты.
- Прогноз кумулятивных показов баннера на пользователя на 180 дней для когорт с датой установки 01.05–07.05 (с графиком).
- Retention rate до максимального дня жизни когорты, аналогичная таблица.
Решение
Подход
- Если
installed_atизвестен по каждому событию — берёмcohort_date = date(installed_at),lifetime_day = date(created_at) − cohort_date. - Кумулятивные показы =
cumsum(impressions per user per lifetime_day). На пользователя в когорте — делим на размер когорты (фикс. знаменатель =users_in_cohort_at_install). - Retention =
unique_users_with_session_on_lifetime_day / cohort_size. - Прогноз 180 дней — экстраполяция кривой кумулятива функцией вида
a · log(1+t)илиa · (1−e^{−bt})·c + …. На практике используюscipy.optimize.curve_fitили более простуюPower Law/Weibull-like.
Реализация
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
df = pd.read_csv('test_analyst.csv')
df['ts'] = pd.to_datetime(df['created_at'], unit='s')
df['install_d'] = pd.to_datetime(df['installed_at'], unit='s').dt.date
df['day'] = df['ts'].dt.date
df['lt_day'] = (pd.to_datetime(df['day']) - pd.to_datetime(df['install_d'])).dt.days
# 1. DAU
dau = (df[df['event_name'] == 'ad_start_session']
.query('"2024-05-18" <= day <= "2024-06-01"') # подставить реальный год
.groupby('day')['us_id'].nunique()
.reset_index(name='DAU'))
# 2. Кумулятивные показы баннера/интерстишала на пользователя
def cumulative_impressions(df, ad_event):
cohort_size = df.groupby('install_d')['us_id'].nunique().rename('users')
imps = (df[df['event_name'] == ad_event]
.groupby(['install_d', 'lt_day']).size()
.rename('imps').reset_index())
imps = imps[imps['lt_day'].between(0, 12)]
imps = imps.merge(cohort_size, left_on='install_d', right_index=True)
imps['per_user'] = imps['imps'] / imps['users']
pivot = imps.pivot_table(index='install_d', columns='lt_day',
values='per_user', fill_value=0)
cum = pivot.cumsum(axis=1)
cum.insert(0, 'users_in_cohort', cohort_size.loc[cum.index].values)
return cum
banner = cumulative_impressions(df, 'ad_banner_impression')
inter = cumulative_impressions(df, 'ad_interstitial_impression')
# 3. Прогноз 180 дней для когорт 01.05–07.05
# модель: f(t) = a * (1 - exp(-b*t)) * t**c (можно проще)
def model(t, a, b):
return a * np.log(1 + b * t)
cohorts = pd.date_range('2024-05-01', '2024-05-07', freq='D').date
fig, ax = plt.subplots(figsize=(10, 6))
for c in cohorts:
if c not in banner.index: continue
series = banner.loc[c].drop('users_in_cohort')
series = series[series.index <= 12].astype(float)
t = np.arange(1, len(series) + 1)
popt, _ = curve_fit(model, t, series.values, p0=[1, 0.1], maxfev=5000)
forecast_t = np.arange(1, 181)
ax.plot(forecast_t, model(forecast_t, *popt), label=str(c))
ax.set_xlabel('Lifetime day'); ax.set_ylabel('Cumulative banner imp / user')
ax.legend(); plt.tight_layout()
# 4. Retention
def retention(df):
cohort_size = df.groupby('install_d')['us_id'].nunique().rename('users')
sess = df[df['event_name'] == 'ad_start_session']
rt = (sess.groupby(['install_d', 'lt_day'])['us_id'].nunique()
.rename('alive').reset_index())
rt = rt.merge(cohort_size, left_on='install_d', right_index=True)
rt['rate'] = rt['alive'] / rt['users']
pivot = rt.pivot_table(index='install_d', columns='lt_day',
values='rate', fill_value=0)
pivot.insert(0, 'users_in_cohort', cohort_size.loc[pivot.index].values)
return pivotИнтерпретация
- DAU — sanity, должен быть стабилен в выбранный период.
- Кумулятивные показы на пользователя — растут монотонно. Если
interstitial_per_user_day_0 ≪ banner_per_user_day_0— поведение «постепенно прогревается». - Прогноз 180 дней через лог-функцию даёт оценку. Реальные данные часто ведут себя как насыщающаяся кривая
a · ln(1+b·t); для ARPU используют чащеPower LawилиWeibull-LTV. - Retention падает: типичная кривая Day-1 ≈ 30–50%, Day-7 ≈ 10–20%, Day-30 ≈ 5–10% для casual head-puzzle.
Подводные камни
- Знаменатель когорты — фиксированный (
users at install date), а не изменяющийся каждый день. Иначе retention будет 100% по определению. lt_dayчерез дни календаря, а не разницу секунд — иначе LT_day = 0 для тех, кто играл в течение 24h.- Прогноз 180 дней — экстраполяция за пределы данных.
curve_fitна 12 точках для 180 — большое доверительное окно. Сообщайте интервалы. - Cohort selection bias: ранние когорты (01.05) уже прожили дольше — для них факт известен; поздние (07.05) — нет. Не сравнивайте D7 ранних и поздних на одинаковый срок без выравнивания.
- Период DAU vs данные: если в CSV нет дат до 18.05, DAU будет начинаться позже.
Эталонный ответ
Pivot-таблица на (cohort_date × lifetime_day) с per-user impressions, cumsum по строкам — для (2). Для (3) — curve_fit лог/экспоненты на 12 точках, экстраполяция на 180. Retention — pivot активных пользователей / размер когорты.