- def get_ltv(
- profiles, # Шаг 1. Получить профили и данные о покупках
- purchases,
- observation_date,
- horizon_days,
- dimensions=[],
- ignore_horizon=False,
- ):
- # исключаем пользователей, не «доживших» до горизонта анализа
- last_suitable_acquisition_date = observation_date
- if not ignore_horizon:
- last_suitable_acquisition_date = observation_date - timedelta(
- days=horizon_days - 1
- )
- result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
- # Шаг 2. Добавить данные о покупках в профили
- result_raw = result_raw.merge(
- # добавляем в профили время совершения покупок и выручку
- purchases[['user_id', 'event_dt', 'revenue']],
- on='user_id',
- how='left',
- )
- # Шаг 3. Рассчитать лайфтайм пользователя для каждой покупки
- result_raw['lifetime'] = (
- result_raw['event_dt'] - result_raw['first_ts']
- ).dt.days
- # группируем по cohort, если в dimensions ничего нет
- if len(dimensions) == 0:
- result_raw['cohort'] = 'All users'
- dimensions = dimensions + ['cohort']
- # функция для группировки таблицы по желаемым признакам
- def group_by_dimensions(df, dims, horizon_days):
- # Шаг 4. Построить таблицу выручки
- # строим «треугольную» таблицу
- result = df.pivot_table(
- index=dims,
- columns='lifetime',
- values='revenue', # в ячейках — выручка за каждый лайфтайм
- aggfunc='sum',
- )
- # Шаг 5. Посчитать сумму выручки с накоплением
- result = result.fillna(0).cumsum(axis=1)
- # Шаг 6. Вычислить размеры когорт
- cohort_sizes = (
- df.groupby(dims)
- .agg({'user_id': 'nunique'})
- .rename(columns={'user_id': 'cohort_size'})
- )
- # Шаг 7. Объединить размеры когорт и таблицу выручки
- result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
- # Шаг 8. Посчитать LTV
- # делим каждую «ячейку» в строке на размер когорты
- result = result.div(result['cohort_size'], axis=0)
- # исключаем все лайфтаймы, превышающие горизонт анализа
- result = result[['cohort_size'] + list(range(horizon_days))]
- # восстанавливаем размеры когорт
- result['cohort_size'] = cohort_sizes
- return result
- # получаем таблицу LTV
- result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
- # для таблицы динамики LTV убираем 'cohort' из dimensions
- if 'cohort' in dimensions:
- dimensions = []
- # получаем таблицу динамики LTV
- result_in_time = group_by_dimensions(
- result_raw, dimensions + ['dt'], horizon_days
- )
- # возвращаем обе таблицы LTV и сырые данные
- return result_raw, result_grouped, result_in_time
- # функция для визуализации LTV и ROI
- def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=14):
- # задаём сетку отрисовки графиков
- plt.figure(figsize=(20, 10))
- # из таблицы ltv исключаем размеры когорт
- ltv = ltv.drop(columns=['cohort_size'])
- # в таблице динамики ltv оставляем только нужный лайфтайм
- ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]
- # стоимость привлечения запишем в отдельный фрейм
- cac_history = roi_history[['cac']]
- # из таблицы roi исключаем размеры когорт и cac
- roi = roi.drop(columns=['cohort_size', 'cac'])
- # в таблице динамики roi оставляем только нужный лайфтайм
- roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
- [horizon - 1]
- ]
- # первый график — кривые ltv
- ax1 = plt.subplot(2, 3, 1)
- ltv.T.plot(grid=True, ax=ax1)
- plt.legend()
- plt.xlabel('Лайфтайм')
- plt.title('LTV')
- # второй график — динамика ltv
- ax2 = plt.subplot(2, 3, 2, sharey=ax1)
- # столбцами сводной таблицы станут все столбцы индекса, кроме даты
- columns = [name for name in ltv_history.index.names if name not in ['dt']]
- filtered_data = ltv_history.pivot_table(
- index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
- )
- filter_data(filtered_data, window).plot(grid=True, ax=ax2)
- plt.xlabel('Дата привлечения')
- plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))
- # третий график — динамика cac
- ax3 = plt.subplot(2, 3, 3, sharey=ax1)
- # столбцами сводной таблицы станут все столбцы индекса, кроме даты
- columns = [name for name in cac_history.index.names if name not in ['dt']]
- filtered_data = cac_history.pivot_table(
- index='dt', columns=columns, values='cac', aggfunc='mean'
- )
- filter_data(filtered_data, window).plot(grid=True, ax=ax3)
- plt.xlabel('Дата привлечения')
- plt.title('Динамика стоимости привлечения пользователей')
- # четвёртый график — кривые roi
- ax4 = plt.subplot(2, 3, 4)
- roi.T.plot(grid=True, ax=ax4)
- plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
- plt.legend()
- plt.xlabel('Лайфтайм')
- plt.title('ROI')
- # пятый график — динамика roi
- ax5 = plt.subplot(2, 3, 5, sharey=ax4)
- # столбцами сводной таблицы станут все столбцы индекса, кроме даты
- columns = [name for name in roi_history.index.names if name not in ['dt']]
- filtered_data = roi_history.pivot_table(
- index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
- )
- filter_data(filtered_data, window).plot(grid=True, ax=ax5)
- plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
- plt.xlabel('Дата привлечения')
- plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))
- plt.tight_layout()
- plt.show()
- observation_date = datetime(2019, 11, 1).date() # момент анализа
- horizon_days = 14 # горизонт анализа
- # считаем LTV и ROI
- ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
- profiles, orders, observation_date, horizon_days
- )
- # строим графики
- plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
- ---------------------------------------------------------------------------
- ValueError Traceback (most recent call last)
- /tmp/ipykernel_48/39697251.py in <module>
- 1 # считаем LTV и ROI
- ----> 2 ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
- 3 profiles, orders, observation_date, horizon_days
- 4 )
- 5
- ValueError: not enough values to unpack (expected 5, got 3)