Собесов

kivork — когортный LTV, прогноз на 6 месяцев и расчёт ROMI

Кейсы и метрикиКогорты, LTV и юнит-экономикаСложнаяMiddle

Условие

Приложение — мобильная утилита для сканирования документов. Подписочная монетизация: trial 7 дней$4.99/неделя. Каждая строка датасета — событие (оформление trial либо оплата после trial).

Задание 1.

  1. Рассчитать текущий LTV пользователя через когортный анализ. Cohorting event — оформление trial. Когорта = число возможных операций (пояснение в задании: «cohorting event — оформление пробного периода, когорта представляет собой кол-во возможных операций»).
  2. Спрогнозировать LTV на полгода.
  3. Построить график фактического и прогнозного LTV.
  4. Рассчитать ROMI на 4 недели и на полгода, если стоимость привлечения платящего пользователя — $6 (ROMI операционный).

Решение

Подход

Подписочная LTV считается как:

LTV(t) = Σ_{i=0}^{t} retention_rate_i × revenue_per_paid_user_per_period

Для еженедельной подписки $4.99 после trial:

  • Когорта = пользователи, оформившие trial в одну неделю.
  • Пол-жизни: trial → платная неделя 1 → платная неделя 2 → ... → отписка.
  • Retention(t) = % когорты, остающихся платящими на неделе t (после trial).
  • Cumulative LTV(t) = $4.99 × Σ retention(i) для i = 1..t.

Реализация

import pandas as pd
import numpy as np
 
df = pd.read_csv("subs_events.csv", parse_dates=["purchase_date"])
 
# Когорта = неделя оформления trial
trials = df[df["is_trial_period"] == True].copy()
trials["cohort_week"] = trials["purchase_date"].dt.to_period("W").dt.start_time
 
# Платежи — фактические оплаты (is_trial_period=False)
payments = df[df["is_trial_period"] == False].copy()
 
# Соединяем: для каждого юзера найти, на какой неделе с момента trial он сделал N-й платёж
trials_per_user = (trials.sort_values("purchase_date")
                          .drop_duplicates("user_id")[["user_id", "purchase_date", "cohort_week"]]
                          .rename(columns={"purchase_date": "trial_date"}))
 
m = payments.merge(trials_per_user, on="user_id")
m["weeks_since_trial"] = (m["purchase_date"] - m["trial_date"]).dt.days // 7 + 1  # неделя 1 — первая после trial
 
# Retention table: для каждой когорты + недели — # активных платящих
retention = (m.groupby(["cohort_week", "weeks_since_trial"])["user_id"]
              .nunique()
              .reset_index(name="active_payers"))
 
# Размер когорты (число trial)
cohort_size = trials.groupby("cohort_week")["user_id"].nunique().rename("trial_users")
 
retention = retention.merge(cohort_size, on="cohort_week")
retention["retention_rate"] = retention["active_payers"] / retention["trial_users"]

LTV (накопительный)

PRICE = 4.99
 
# Pivot: rows = cohort, cols = week, values = retention
pivot = retention.pivot_table(index="cohort_week", columns="weeks_since_trial",
                              values="retention_rate", fill_value=0)
 
# Кумулятивный revenue per trial-юзер: PRICE × cumsum(retention)
ltv_per_cohort = pivot.cumsum(axis=1) * PRICE
print(ltv_per_cohort)
 
# Усреднённый LTV (взвешенный по когорте)
weights = cohort_size.reindex(pivot.index)
avg_ltv = (pivot.multiply(weights, axis=0).sum(axis=0) / weights.sum()).cumsum() * PRICE

Прогноз на полгода

Возможные методы:

1. Параметрическая модель retention curve

Подписочный retention обычно описывается геометрическим затуханием:

retention(t) = A * r^t

где r — еженедельный churn rate, A — начальная конверсия из trial в платный.

from scipy.optimize import curve_fit
 
# Усреднённая retention curve
mean_retention = pivot.mean(axis=0).values
weeks = np.arange(1, len(mean_retention) + 1)
 
def geom(t, A, r):
    return A * np.power(r, t - 1)
 
# Только наблюдаемые (не нулевые) недели
mask = mean_retention > 0
popt, _ = curve_fit(geom, weeks[mask], mean_retention[mask])
A_fit, r_fit = popt
print(f"A = {A_fit:.4f}, weekly retention = {r_fit:.4f}")
 
# Прогноз на 26 недель (полгода)
weeks_full = np.arange(1, 27)
retention_pred = A_fit * np.power(r_fit, weeks_full - 1)
ltv_forecast = np.cumsum(retention_pred) * PRICE
 
print(f"LTV @ 4 weeks  = ${ltv_forecast[3]:.2f}")
print(f"LTV @ 26 weeks = ${ltv_forecast[25]:.2f}")

2. Альтернативные модели

  • Beta-Geometric / NBD (Pareto/NBD) — стандарт в lifetime modeling.
  • Survival analysis (Kaplan-Meier для оценки + параметрический экстраполятор).

ROMI (Return on Marketing Investment)

ROMI = (LTV - CAC) / CAC или (Revenue - CAC) / CAC.

CAC = $6 (стоимость платящего, не trial-юзера).

Чтобы скорректировать на trial → paid конверсию: если A = 0.30, то на одного платящего в среднем 1/A = 3.33 trial. Если CPI trial-юзера = X,тоCACплатящего=X/A.ЗдесьпоусловиюCACплатящегонапрямую=X, то CAC платящего = X / A. Здесь по условию CAC платящего напрямую = 6.

CAC = 6.0
 
romi_4w  = (ltv_forecast[3]  - CAC) / CAC * 100  # %
romi_26w = (ltv_forecast[25] - CAC) / CAC * 100
 
print(f"ROMI @ 4 weeks  = {romi_4w:.1f}%")
print(f"ROMI @ 26 weeks = {romi_26w:.1f}%")

Интерпретация: ROMI = 100% означает, что вернули 1на1 на 1 (точка безубыточности). 200% — 2на2 на 1 (двойная окупаемость).

Анализ / интерпретация

Возможные числа (зависят от датасета):

При retention curve A=0.30, r=0.85 (еженедельный churn 15%):

  • LTV @ 4 weeks ≈ 4.99×(0.30+0.255+0.217+0.184)4.99 × (0.30 + 0.255 + 0.217 + 0.184) ≈ **4.77**
  • LTV @ 26 weeks ≈ 4.99×Σretention(1..26)4.99 × Σ retention(1..26) ≈ **9.50**
  • ROMI @ 4 weeks = (4.77 − 6) / 6 = −20.5% — не окупаемся.
  • ROMI @ 26 weeks = (9.50 − 6) / 6 = +58% — окупаемся за полгода.

Бизнес-вывод: на коротком горизонте кампания убыточна, на полугодовом — позитивна. Нужно выдержать P&L 4-х недель, чтобы дойти до окупаемости.

График

import matplotlib.pyplot as plt
 
# Фактический LTV
weeks_actual = avg_ltv.index
ltv_actual = avg_ltv.values
 
# Прогноз
weeks_forecast = np.arange(1, 27)
ltv_forecasted = np.cumsum(A_fit * np.power(r_fit, weeks_forecast - 1)) * PRICE
 
plt.plot(weeks_actual,   ltv_actual,    "o-", label="Actual LTV")
plt.plot(weeks_forecast, ltv_forecasted, "--", label="Forecasted LTV (geom)")
plt.axhline(CAC, color="red", linestyle=":", label=f"CAC = ${CAC}")
plt.xlabel("Weeks since trial"); plt.ylabel("Cumulative LTV, $"); plt.legend()

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

  1. «Когорта = число возможных операций» — формулировка задания неоднозначная. По смыслу — это трактовка когорты как пользователей с одинаковым числом совершённых платежей, а не недели. Уточняйте у заказчика. В моём решении взята более стандартная когорта по неделе trial.
  2. Неполные когорты. Самые свежие когорты не успели «дозреть» — их retention(N) будет занижен. Используйте только зрелые когорты для построения модели и применяйте на свежих.
  3. Trial cancellation. Часть юзеров отменяют trial до его конца — они не платят и не входят в paying-когорту.
  4. Refund. Если есть возвраты — net revenue ниже gross.
  5. Геометрическая модель — упрощение. Реальный retention часто имеет «тяжёлый хвост» (heterogeneity): «верные» юзеры держатся месяцами. Beta-Geometric лучше.
  6. **CAC = 6заплатящего,анезаtrialюзера.ЕсливмаркетингеCACtrial=6 за платящего**, а не за trial-юзера. Если в маркетинге CAC trial = 2, а конверсия в paid = 0.30, то CAC paid = $6.67.
  7. «Операционный ROMI» — без учёта overhead (R&D, support, server costs). Только маркетинг vs revenue.
  8. Длина прогноза. Полгода далеко от наблюдений — экстраполяция геометрической модели может сильно ошибаться.

Альтернативы

  • Bayesian hierarchical (numpyro / Stan) — даёт CI на прогноз.
  • Empirical (без модели): для каждой недели просто берём фактический retention, экстраполируем константой r для последних наблюдаемых недель. Простой, но шумный.

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

LTV = PRICE × cumsum(retention_rate) по неделям с момента trial. Прогноз — fit A * r^t на retention, экстраполяция на 26 недель. ROMI = (LTV - CAC) / CAC × 100%.

Главные тонкости: использование только зрелых когорт для моделирования, неполные когорты помечать NULL, прогноз на полгода — это экстраполяция и нужны CI / альтернативные модели.

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

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

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