Собесов

EasyBrain — кумулятивные показы рекламы по дням жизни когорты

PythonCohort / cumulative metricsСредняяMiddle

Условие

Дан csv test_analyst.csv с поведенческими событиями игроков:

  • us_id — user id
  • event_namead_start_session (заход), ad_banner_impression (показ баннера), ad_interstitial_impression (показ inter-рекламы)
  • created_at — время события (unix-time)
  • installed_at — время установки приложения (unix-time)

Задачи:

  1. Рассчитать DAU на каждую дату периода 18.05–01.06.
  2. Кумулятивные показы на пользователя по типу рекламы до 12 дня жизни когорты. Таблица: строки = дата установки, столбцы = день жизни (0..12), вторая колонка — размер когорты.
  3. Прогноз кумулятивных показов баннера на пользователя на 180 дней для когорт с датой установки 01.05–07.05 (с графиком).
  4. 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.

Подводные камни

  1. Знаменатель когорты — фиксированный (users at install date), а не изменяющийся каждый день. Иначе retention будет 100% по определению.
  2. lt_day через дни календаря, а не разницу секунд — иначе LT_day = 0 для тех, кто играл в течение 24h.
  3. Прогноз 180 дней — экстраполяция за пределы данных. curve_fit на 12 точках для 180 — большое доверительное окно. Сообщайте интервалы.
  4. Cohort selection bias: ранние когорты (01.05) уже прожили дольше — для них факт известен; поздние (07.05) — нет. Не сравнивайте D7 ранних и поздних на одинаковый срок без выравнивания.
  5. Период DAU vs данные: если в CSV нет дат до 18.05, DAU будет начинаться позже.

Эталонный ответ

Pivot-таблица на (cohort_date × lifetime_day) с per-user impressions, cumsum по строкам — для (2). Для (3) — curve_fit лог/экспоненты на 12 точках, экстраполяция на 180. Retention — pivot активных пользователей / размер когорты.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти