Собесов

Karpov ДЗ: Прогноз CLTV для подписочного сервиса

PythonCLTV / CohortСложнаяMiddle

Условие

Сервис стримов берёт 499 ₽/мес с подписчика. Маркетинг хочет знать LTV нового пользователя за 24 месяца, чтобы понять допустимый CAC. У вас есть retention curve по 12 месяцам:

m1=0.65, m2=0.50, m3=0.42, m4=0.37, m5=0.34, m6=0.31,
m7=0.29, m8=0.27, m9=0.26, m10=0.25, m11=0.24, m12=0.23

(доля от месяца 0, который = 100% по построению — все, кто оплатил первый месяц).

  1. Прогнозируйте retention на месяцы 13-24 (экстраполяция).
  2. Посчитайте 12m / 24m LTV (gross, без discount rate).
  3. Посчитайте 24m discounted LTV при ставке 10% годовых.

Решение

1) Экстраполяция retention

Retention обычно затухает как power law или exponential decay. Подгоняем оба:

import numpy as np
import pandas as pd
from scipy.optimize import curve_fit
 
months = np.arange(1, 13)
ret = np.array([0.65, 0.50, 0.42, 0.37, 0.34, 0.31,
                0.29, 0.27, 0.26, 0.25, 0.24, 0.23])
 
# Power law: r(t) = a * t^(-b)
def power_law(t, a, b):
    return a * t**(-b)
 
# Exponential: r(t) = a * exp(-b*t)
def exp_decay(t, a, b):
    return a * np.exp(-b * t)
 
popt_pow, _ = curve_fit(power_law, months, ret, p0=(0.7, 0.4))
popt_exp, _ = curve_fit(exp_decay, months, ret, p0=(0.7, 0.1))
 
future = np.arange(13, 25)
ret_pow_future = power_law(future, *popt_pow)
ret_exp_future = exp_decay(future, *popt_exp)
 
# Сравним MAPE на исторических
mape_pow = np.mean(np.abs(power_law(months, *popt_pow) - ret) / ret) * 100
mape_exp = np.mean(np.abs(exp_decay(months, *popt_exp) - ret) / ret) * 100
print(f"power MAPE: {mape_pow:.2f}%   exp MAPE: {mape_exp:.2f}%")

Для подписочных сервисов power law обычно лучше — «хвост» дольше. Выбираем модель с меньшим MAPE на in-sample (и проверяем на out-of-time, если данных хватит).

2) LTV gross

LTV_12m = price × (m0 + Σ retention_t) = price × Σ_{t=0..12} r_t,  r_0 = 1

Месяц 0 — все 100% оплачивают (первый платёж). Дальше — m1, m2, ....

PRICE = 499
 
# месяц 0 = 1.0, месяцы 1..12 — реальные данные, 13..24 — прогноз
ret_full = np.concatenate(([1.0], ret, ret_pow_future))
 
ltv_12 = PRICE * ret_full[:13].sum()
ltv_24 = PRICE * ret_full.sum()
print(f"LTV 12m = {ltv_12:.0f} ₽")     # ≈ 499 * (1+0.65+0.50+...+0.23) = ~2580
print(f"LTV 24m = {ltv_24:.0f} ₽")     # ≈ ~3700

3) Discounted LTV (NPV)

Ставка 10% годовых → месячная ≈ (1+0.10)^(1/12) − 1 ≈ 0.00797. Или часто упрощают: 0.10 / 12 ≈ 0.00833.

ANNUAL = 0.10
m_rate = (1 + ANNUAL)**(1/12) - 1     # ≈ 0.00797
 
t = np.arange(0, 25)                  # 0..24
df_factor = 1 / (1 + m_rate)**t
ltv_24_disc = PRICE * (ret_full * df_factor).sum()
print(f"LTV 24m discounted = {ltv_24_disc:.0f} ₽")

Дисконт «съест» 5-10% за 24 месяца — типичный эффект.

CAC threshold

Если CAC = 1500 ₽:

  • 12m LTV ≈ 2580 ₽ → ROI = 72%, окупаемость за ~6 мес — ок.
  • Если CAC = 3000 ₽ → не окупается за 24 мес → канал в минусе.

Менеджеры обычно требуют LTV / CAC ≥ 3 на горизонте 24 мес.

Альтернатива: BG/NBD + Gamma-Gamma

Для non-subscription моделей (e-commerce) используют статистические модели lifetimes из библиотеки lifetimes:

from lifetimes import BetaGeoFitter, GammaGammaFitter
# ... подбирают на RFM-данных

Здесь подписка → проще: retention curve достаточно.

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

  1. Месяц 0 vs месяц 1: в задаче m1 = месяц 1 (доля, оставшаяся после первого месяца). Месяц 0 — все, кто оплатил первый платёж (100%).
  2. Экстраполяция power law на 100 месяцев даёт бесконечный LTV — опасно. Обрезать на 24-36 мес или использовать модели с плато.
  3. Линейная экстраполяция retention → негативные значения за 24 мес.
  4. Inflation vs discount rate: 10% — часто номинальный; для реального NPV учитывают инфляцию (3-7%/год).
  5. Cohort heterogeneity: новые когорты могут иметь разный shape. Считать LTV на актуальной cohort, не на агрегате 5 лет назад.
  6. Refund / churn-during-month: если есть возвраты — revenue ≠ price × retention.
  7. «Predict 24m по 12m» требует допущения о стабильности retention curve. Если продукт меняется — экстраполяция врёт.

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

ret_full = np.concatenate(([1.0], ret, power_law(np.arange(13,25), *popt_pow)))
LTV_24    = 499 * ret_full.sum()                           # gross
LTV_24_NPV = 499 * (ret_full / (1 + (1.1)**(1/12) - 1) ** np.arange(25)).sum()

Шаги: (1) подогнать power law на 12 месяцев retention; (2) экстраполировать до 24; (3) LTV = price × sum(retention) с month=0..24 (где r_0 = 1); (4) дисконт = делим на (1+m_rate)^t. Подвох — power law даёт бесконечность при extrapolation, надо обрезать.

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

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

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