Использование ML для прогнозирования CLTV

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

1. Формула CLTV и постановка задачи

Из прошлой статьи мы узнали, что CLTV (customer lifetime value) — метрика, используемая для оценки прибыли, которую компания может получить от своего клиента за время его пользования продуктами и сервисами компании.

Разберем, что означает каждая буква в определении CLTV (customer lifetime value). Кто такой клиент, что мы понимаем под lifetime и ценностью, которую приносит нам клиент. 

CLTV строится для клиента, а не для номера телефона, так как мы не хотим терять историю взаимодействий с ним. Мы учитываем, что абонент может сменить номер телефона и/или может измениться номер договора. Также билайн — это не только мобильная связь, но и домашний интернет, которым наши абоненты могут пользоваться в рамках одного договора. Поэтому мы сразу решили собирать информацию и по этим услугам в рамках одной записи по клиенту. В будущем мы планируем прогнозировать CLTV уже на уровне физического лица и домохозяйств, объединяя историю пользования всех сим-карт клиента.

Под lifetime мы понимаем не полный жизненный цикл клиента от момента заключения договора до момента его закрытия, а пятилетний горизонт, который мы отсчитываем от текущего момента времени. То есть, если мы строим прогноз от января 2023 года, то прогноз будет построен помесячно до декабря 2027 года. Почему 5 лет? Этот срок был определен опытным путем — при нем достигается баланс между качеством предсказаний и потребностью в бизнес-процессах.

В билайне под ценностью клиента принято понимать маржу, которую нам приносит абонент с учетом всех затрат и доходов, которые мы можем аллоцировать на конкретного клиента.

Чтобы посчитать маржу абонента, нам надо максимально аллоцировать выручку и расходы до уровня абонента.

Компоненты выручки
  • абонентская плата

  • плата за трафик сверх пакетов

  • оплата сервисов

  • выручка за мобильную коммерцию

  • оплата за интерконнект*

  • плата за домашний интернет и подключённые услуги (родительский контроль, аренда оборудования и пр.) 

  • прочие статьи выручки

*интерконнект - это система, определяющая расчет между операторами-партнерами за трафик, проходящий по «чужим» каналам связи. То есть если абонент билайна звонит абоненту другого оператора, то билайн платит плату за интерконнект другому оператору, если же абонент другого оператора звонит абоненту билайна, то другой оператор платит плату за интерконнект билайну.

Компоненты затрат
  • плата за интерконнект

  • плата за интернет провайдера

  • плата за аренду линий связи и номерную емкость

  • стоимость трафика

  • стоимость роуминга

  • стоимость контента

  • стоимость SMS/MMS

  • затраты на привлечение новых абонентов: стоимость сим-карт, заработную плату сотрудников офисов, и дилерские комиссии агентам

  • затраты на data и голосовой трафик, которые включают в себя техподдержку и ИТ-сопровождение, транспортную сеть и другие расходы

  • затраты на оплату труда

  • затраты на маркетинг и рекламу 

  • административные и управленческие расходы

  • прочие статьи расходов

Есть расходы, которые можно распределить до уровня конкретного абонента — их можно относительно легко посчитать, и мы прогнозируем их напрямую. С расходами на интернет и голосовой трафик такое не работает: от года к году стоимость гигабайта/минуты может меняться, и при прямом прогнозе качество получается неудовлетворительное. Поэтому они считаются через прогноз объёма потребленного трафика, который затем умножается на его стоимость. 

Также есть расходы, которые мы считаем на уровне клиентских сегментов. Это Advertising & Marketing, Salaries & Benefits и General & Administrative. Для этих расходов мы считаем среднюю ставку коста на одного абонента. А для получения прогноза расходов на конкретного абонента рассчитанную ставку коста умножаем на вероятность выживаемости этого абонента на горизонте прогнозирования CLTV.

Всего у нас получилось девять компонент для прогнозирования, и формула расчета CLTV выглядит следующим образом:

CLTV = SM Mobile + SM FMC fix + SM FTTB - SAC cost - DATA mobile opex cost - VOICE mobile opex cost - SALARIES & BENEFITS cost -  GENERAL & ADMINISTRATIVE cost - ADVERTISING & MARKETING cost

Словарь
  • Paid база (платящая активная база) — абоненты, имеющие выручку (прямую/косвенную) и трафик в отчётном периоде (голос трафик/смс/дата трафик).

  • Active база (активная абонентская база) — абоненты, имеющие выручку (прямую/косвенную) и/или трафик в отчётном периоде (голос трафик/смс/дата трафик).

  • SM — сервисная маржа.

  • Mobile клиенты — клиенты, которые используют мобильную связь.

  • FMC fix клиенты — клиенты, использующие комбинированный тариф, который включает как мобильную связь, так и домашний интернет.

  • FTTB-клиенты — клиенты, использующие только домашний интернет.

  • SAC — выплаты агентам за привлечение нового абонента.

  • DATA cost — оперативные расходы на мобильный интернет трафик.

  • VOICE cost — оперативные расходы на мобильный голосовой трафик.

  • Salaries & Benefits cost — затраты на оплату труда

  • General & Administrative cost — административные и управленческие расходы

  • Advertising & Marketing cost — затраты на маркетинг и рекламу

Исходя из всего вышеперечисленного, перед командой CLTV стоит следующая задача: прогнозирование ожидаемого CLTV для каждого абонента на пятилетнем горизонте. А так как CLTV абонента определяется как сумма отдельных компонент выручки и затрат, то задачу прогнозирования CLTV можно свести к прогнозированию отдельных компонент.

2. Выбор целевой переменной

Некоторые расходы мы прогнозируем не напрямую, а через умножение ставки коста на вероятность удержания абонента по paid/active базам. Из этого получаются первые две целевые переменные — нахождение абонента в paid, active базах. Тут решается задача бинарной классификации.

Остальные компоненты формулы CLTV прогнозируем напрямую — их у нас 6 (задача регрессии). Здесь требуется преобразование целевых переменных — они собирается помесячно, и их нужно привести к среднедневному значению. Например, сервисная маржа у абонента в июле равна 100 рублям, эти 100 рублей мы делим на количество дней в июле и подаём в модель. Это делается потому, что величина абонентской платы и других денежных показателей зависит от количества дней в месяце (сказывается подневное списание абонентской платы). Без такого преобразования регрессионные модели в феврале и месяцах с 30 днями завышают прогноз, а в месяцах с 31 днём — занижают.

Отдельно стоит рассмотреть вопрос выбора горизонта прогнозирования для ML-моделей. С одной стороны, можно обучить модели прогнозировать целевые переменные на 5 лет вперёд. Но при таком длинном горизонте прогнозирования качество прогноза будет очень низким, к тому же за 5 лет данные для моделей сильно меняются в силу изменения экономической ситуации, роста цен, появления новых тарифов. После нескольких экспериментов было установлено, что наиболее оптимальный горизонт прогнозирования для ML-моделей составляет 24 месяца. А получаемый прогноз на 24 месяца можно экстраполировать на 5 лет (об этом будет отдельная статья).

Итого получается 8 моделей, которые можно разделить на три группы:

  1. Объём абонентской базы = выживаемость: 

  • Прогнозы выживаемости по paid, active базам. Используются для расчёта непрямых расходов, а также как признаки в моделях регрессии.

  1. Выручка: 

  • SM Mobile, SM FMC, SM FTTB — прогнозы сервисной маржи. 

  1. Расходы: 

  • Data — прогноз потреблённого дата трафика (в гб), далее умножается на стоимость гигабайта.

  • Voice — прогноз потреблённого голосового трафика (в минутах), далее умножается на стоимость минуты.

  • SAC — прогноз затрат на привлечение абонентов.

3. Принцип формирования обучающей выборки

У нас есть 8 целевых переменных и 8 моделей, которые нужно обучить прогнозировать эти целевые переменные на горизонт от 1 до 24 месяцев вперёд. Этого можно добиться благодаря добавлению в модели горизонта прогнозирования [h=1...24] в качестве признака.

Таким образом, при сборе выборки для каждого абонента в зависимости от горизонта h необходимо определить правильный лаг для сбора признаков. Для абонента с целевой переменной за месяц T и горизонтом h нужно присоединить признаки за месяц T - h.

inline/7815e021ad18c4c3a17fb159f57c57729540788b.png

Алгоритм сбора выборки:

  1. Выбираем интервал [T1, T2] сбора целевых переменных. Берём последние 13 месяцев, например с 2022-07-01 по 2023-07-01.

  2. В цикле для каждого горизонта прогнозирования собираем свою выборку:

    a. Фиксируем горизонт = h [h=1...24].

    b. Получаем интервал сбора признаков абонентов [T1 - h, T2 - h].

    c. Собираем целевые переменные за интервал [T1, T2] и признаки за [T1 - h, T2 - h].

    d. Cемплируем абонентов помощью вычисления хэша от идентификатора абонента (подробно идея описана тут). Делаем это для того, чтобы получить репрезентативную выборку из генеральной совокупности (абонентская база довольно большая — десятки миллионов в каждый месяц). Хеширование используется, чтобы для каждой даты выбрать уникальных абонентов, так как хотим, чтобы абоненты не попадали в обучающую выборку более одного раза (во избежание эффекта переобучения). 

  1. Объединяем полученные на 2 шаге выборки в одну. 

Полученная выборка для обучения случайно разбивается на train/val/test. Также отдельно выделяется выборка out of time, которая включает в себя последний месяц по целевым переменным.

Во избежание переобучения train выборка делится на две части для регрессионных моделей и для моделей выживаемости, так как прогноз выживаемости используется как признак при построении регрессионных моделей.

4. Модели

Лучше всего и в классификации, и в регрессии у нас сработал CatBoost.

Для классификации loss_function — Logloss, eval_metric - AUC.

Для регрессии loss_function — Huber:delta, eval_metric - WAPEs.

Формула Huber:

l(t, a)=  \left\{\begin{matrix}  \frac{1}{2}(t - a)^{2}, & \left | t - a \right | \le \delta\\  \delta \left| t - a \right| - \frac1 2 \delta^{2}, & \left| t - a \right| > \delta  \end{matrix}\right.

Функция потерь Хубера работает как комбинация MSE и MAE одновременно. Параметр дельта позволяет задать границу того, что мы будем считать выбросами. То есть для значений ошибок меньших дельта используется MSE, для больших дельта — MAE.

Формула WAPEs:

WAPE_{s} = \frac{\sum_{}^{}\left|f_{i} - a_{i}  \right|}{\sum_{}^{} max(f_{i}, a_{i})}

Для нашей задачи мы взяли WAPE (weighted average percentage error) и сделали её симметричной (нам важно, чтобы модель одинаково штрафовалась как за перепрогноз, так и за недопрогноз).

4.1 Конфигурирование моделей

У нас 8 в каком-то смысле однотипных моделей (8 моделей CatBoost): у них разные целевые переменные и признаки, но клиентская база одна. Поэтому для удобства и экономии времени на обучении моделей мы собираем один общий датасет для всех моделей, который содержит признаки и целевые переменные сразу всех моделей. Мы поняли, что будет неудобно поддерживать код для 8 однотипных моделей, поэтому написали единую кодовую базу для работы с ними. Для решения этой задачи мы создали конфиги для параметров, признаков и других настроек моделей на основе pydantic. А также написали единый класс Learner для обучения и предсказания.

Далее на упрощённом примере (на две модели) покажем, как мы это сделали.

С помощью pydantic создаём конфиги для параметров моделей:
from typing import List, Optional
 
from pydantic import BaseSettings
 
 
class SubModelConstructor(BaseSettings):
    # параметры CatBoost
    iterations: int
    learning_rate: Optional[float] = None
    loss_function: str
    grow_policy: str
    eval_metric: str
 
 
class SubModelFitParams(BaseSettings):
    # параметры обучения CatBoost
    use_best_model: bool = True
    early_stopping_rounds: int = 100
    verbose: int = 100
    cat_features: List[str] # список всех категориальных признаков в обучающей выборке

Конфигурируем модели:
cat_cols = ["f1"] # список категориальных переменных
 
 
class SubModelProfile(BaseSettings):
    model_id: str # напрмер paid
    feature_cols: List[str] # cписок признаков
    constructor: SubModelConstructor
 
    @property
    def fit_params(self) -> SubModelFitParams:
        return SubModelFitParams(cat_features=sorted(set(cat_cols) & set(self.feature_cols))) # получаем список категориальных признаков для модели
 
    @property
    def target_col(self) -> str:
        return f"{self.model_id}_fact" # название целевой переменной модели
 
    @property
    def score_col(self) -> str:
        return f"{self.model_id}_pred" # название колонки с предсказанием модели
 
    def get_dict(self):
        # получение конфига модели в dict
        result = self.dict()
        result.update(
            {
                "fit_params": self.fit_params.dict(),
                "target_col": self.target_col,
                "score_col": self.score_col
            }
        )
        return result
 
 
class ModelProfile(BaseSettings):
    paid: SubModelProfile = SubModelProfile(
        model_id="paid",
        feature_cols=["f1", "f2"],
        constructor=SubModelConstructor(iterations=1000, grow_policy="Depthwise", loss_function="Logloss",
                                        eval_metric="AUC"),
    )
    aab: SubModelProfile = SubModelProfile(
        model_id="aab",
        feature_cols=["f1", "f3"],
        constructor=SubModelConstructor(iterations=1000, grow_policy="Depthwise", loss_function="Logloss",
                                        eval_metric="AUC"),
    )
    
    def dict(self):
        # нужен для инициализации модели по model_name
        model_names = self.__dict__.keys()
        result = {}
        for model_name in model_names:
            result[model_name] = getattr(self, model_name).get_dict()
        return result

И создаём Learner для обучения и предсказания:
from typing import Dict
 
import numpy as np
import pandas as pd
from catboost import CatBoostClassifier, CatBoostRegressor
 
classification_tasks = ["paid", "aab"] # две модели для примера
regression_tasks = []
 
 
class Learner:
    def __init__(self):
        self.cfg = ModelProfile().dict()
        self.learners = {}
        self._init_learners()
 
    def _init_learners(self):
        # инициализируем модели
        for model_id, params in self.cfg.items():
            if model_id in regression_tasks:
                self.learners[model_id] = CatBoostRegressor(**params["constructor"])
            elif model_id in classification_tasks:
                self.learners[model_id] = CatBoostClassifier(**params["constructor"])
            else:
                raise Exception
 
    def fit(self, model_list: List[str], X_train: pd.DataFrame, y_train: pd.DataFrame, X_val: pd.DataFrame, y_val: pd.DataFrame) -> None:
        for model_id in model_list:
            self.learners[model_id].fit(
                X_train[self.cfg[model_id]["feature_cols"]],
                y_train[self.cfg[model_id]["target_col"]],
                eval_set=(X_val[self.cfg[model_id]["feature_cols"]], 
                          y_val[self.cfg[model_id]["target_col"]]),
                **self.cfg[model_id]["fit_params"]
            )
 
    def predict(self, model_list: List[str], X: pd.DataFrame) -> Dict[str, np.ndarray]:
        scores = {}
        for model_id in model_list:
            df = X[self.cfg[model_id]["feature_cols"]]
            if model_id in classification_tasks:
                pred = self.learners[model_id].predict_proba(df)[:, 1]
            elif model_id in regression_tasks:
                pred = self.learners[model_id].predict(df)
            else:
                raise Exception
            scores[model_id] = pred
        return scores

Пример обучения моделей и скоринга:
model = Learner()
# в X собраны признаки всех моделей, в y - целевые переменные
# обучение моделей
model.fit(["aab", "paid"], X_train, y_train, X_val, y_val)
# прогноз
scores = model.predict(["aab", "paid"], X_oot)

Валидация

При валидации качество моделей смотрим по всем сплитам (выборка разделена на train, val, test, oot), в разрезе по горизонтам.

Для классификации смотрим:

  • Brier score

  • ROC AUC

  • Калибровку моделей 

Для регрессии смотрим:

  • Bias

  • MAE

  • R2

  • RMSE

  • WAPEs

_scroll_external/attachments/aab-roc_auc-4bed26cbd73ed0d7de8a2576077bc41e248e3066b12f63296a48bf491d787c69.png
_scroll_external/attachments/paid-brierloss-1-d0e6d3a8bfda7b0b219cbc72c37e1ab8a62e2f579def33e353ec71162760f75d.png

Помимо этого, у нас реализован мониторинг моделей в проде, где мы уже смотрим на качество прогнозирования в разрезе по сегментам и когортам абонентов.

5. Вывод в прод, шедулинг и переобучение моделей

Так как прогнозы моделей используются другими командами компании для выставления KPI, планирования, оценки и других задач, мы используем релизный подход при выводе в прод. Релиз состоит из 8 обученных моделей. В него могут входить: переобучение на новом периоде данных, добавление новых фичей, добавление новых компонент.

Обученные модели сохраняем в mlflow. Шедулинг реализован на Argo Workflow — по мере поступления данных раз в месяц поднимается docker-контейнер, в котором загружаются модели из mlflow и скорят данные (данные в Hadoop, используем Spark).

6. Заключение

В одной из следующих статей расскажем о том кто и как использует прогнозы наших моделей.

Над моделью работали:

Антон Мельников @a_melnikov

Наталия Култыгина @nataliiiya

Олег Вавулов

Источник: https://habr.com/ru/companies/beeline_tech/articles/771246/


Интересные статьи

Интересные статьи

Недавно я узнал про довольно интересный инструмент, встроенный в РНР. Оказывается, в языке нативно поддерживается универсальный формат шаблонов для сообщений, ICU Message Format. В частности, он испол...
4х повышение разрешения изображения с использованием ESRGANВ данной статье разобрано применение предобученной нейронной сети ESRGAN для увеличения разрешения изображения в четыре раза c использование...
Отладка в контейнерной среде – дело не самое простое, поэтому разработчики зачастую прибегают к неэффективным методам локализации ошибок на этапе развертывания. Быстрее и красивее будет использовать о...
На сайте регулярно появляются статьи про автогенерацию музыки или об авто-аккомпанименте. Некоторые в качестве результата воспроизводят невнятное бибикание, у некоторых п...
Еще один мой проект в Godot 3 с использованием разных шейдеров, все шейдеры довольно простые. Ссылка для запуска на itch.io, требуется WebGL2. Исходный код проекта на github, прое...