Условие
Приложение — мобильная утилита для сканирования документов. Подписочная монетизация: trial 7 дней → $4.99/неделя. Каждая строка датасета — событие (оформление trial либо оплата после trial).
Задание 1.
- Рассчитать текущий LTV пользователя через когортный анализ. Cohorting event — оформление trial. Когорта = число возможных операций (пояснение в задании: «cohorting event — оформление пробного периода, когорта представляет собой кол-во возможных операций»).
- Спрогнозировать LTV на полгода.
- Построить график фактического и прогнозного LTV.
- Рассчитать 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-юзера = 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 (точка безубыточности). 200% — 1 (двойная окупаемость).
Анализ / интерпретация
Возможные числа (зависят от датасета):
При retention curve
A=0.30, r=0.85(еженедельный churn 15%):
- LTV @ 4 weeks ≈ 4.77**
- LTV @ 26 weeks ≈ 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()Подводные камни
- «Когорта = число возможных операций» — формулировка задания неоднозначная. По смыслу — это трактовка когорты как пользователей с одинаковым числом совершённых платежей, а не недели. Уточняйте у заказчика. В моём решении взята более стандартная когорта по неделе trial.
- Неполные когорты. Самые свежие когорты не успели «дозреть» — их retention(N) будет занижен. Используйте только зрелые когорты для построения модели и применяйте на свежих.
- Trial cancellation. Часть юзеров отменяют trial до его конца — они не платят и не входят в paying-когорту.
- Refund. Если есть возвраты — net revenue ниже gross.
- Геометрическая модель — упрощение. Реальный retention часто имеет «тяжёлый хвост» (heterogeneity): «верные» юзеры держатся месяцами. Beta-Geometric лучше.
- **CAC = 2, а конверсия в paid = 0.30, то CAC paid = $6.67.
- «Операционный ROMI» — без учёта overhead (R&D, support, server costs). Только маркетинг vs revenue.
- Длина прогноза. Полгода далеко от наблюдений — экстраполяция геометрической модели может сильно ошибаться.
Альтернативы
- 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 / альтернативные модели.