Небольшая библиотека на Python для моделирования маркетингового комплекса: MaMiMo

Автор: Дмитрий Иванов [Команда P9X]

~8 минут чтения

Привет! Я заметил, что людям действительно интересны мои статьи о моделировании маркетингового микса, поэтому я создал для вас небольшую библиотеку, которая поможет вам самостоятельно создавать простые модели маркетингового микса! В этой статье я покажу вам, как её использовать.

⚠ Обязательное предупреждение ⚠: я старался сделать эту библиотеку максимально надёжной и без ошибок. Однако ошибки всё равно могут быть, поэтому всегда проводите проверку работоспособности после, прежде чем сообщать о чём-либо заинтересованным сторонам. Если вы обнаружите ошибку или запросите функцию, просто напишите мне сообщение или — ещё лучше — создайте запрос на включение изменений в GitHub! 😉


Если вы не знаете, что такое моделирование маркетингового микса, представьте, что вы работаете в компании, которая что-то продаёт. Чтобы продавать ещё больше, вы делаете рекламу. В какой-то момент вы хотите знать, насколько хорошо ваша реклама работала по каналам, то есть ТВ, радио, веб-баннеры и т. д., и ответить на вопросы вроде: «Эти 1000 €, которые я вложил в рекламу на ТВ на той неделе, на сколько они увеличили мой доход?». Моделирование маркетингового микса — это простой способ сделать это. Вы можете найти больше информации в моих статьях об этом:

Введение в моделирование маркетингового микса на Python

Модернизированное моделирование маркетингового микса на Python

Библиотека, которую я создал, называется MaMiMo, и если вы умеете пользоваться scikit-learn, вы можете использовать и эту. Давайте начнём с простого:

pip install mamimo

Небольшой пример

Если вы прочитали обе статьи сверху — что я предполагаю сейчас — вы, наверное, помните, что мы использовали там искусственный набор данных. Затем мы определили некоторые преобразования насыщения и переноса для проведения моделирования маркетингового микса. Я поместил в MaMiMo аналогичный, но немного более сложный пример набора данных, чтобы вы начали:

from mamimo.datasets import load_fake_mmm

data = load_fake_mmm()

X = data.drop(columns=['Sales'])
y = data['Sales']

Это даёт нам набор данных с индексом даты на еженедельной основе, тремя медиаканалами и столбцом продаж, который мы хотим объяснить.

Построение модели

Мы можем напрямую реализовать этот конвейер, используя подмодули переноса и насыщения из MaMiMo, а также его LinearRegression, который более гибкий, чем версия scikit-learn. Что нам нужно от scikit-learn, так это функциональность конвейера.

from mamimo.carryover import ExponentialCarryover
from mamimo.saturation import ExponentialSaturation
from mamimo.linear_model import LinearRegression
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

adstock = ColumnTransformer(
    [
        ('tv_pipe', Pipeline([
            ('carryover', ExponentialCarryover()),
            ('saturation', ExponentialSaturation())
        ]), ['TV']),
        ('radio_pipe', Pipeline([
            ('carryover', ExponentialCarryover()),
            ('saturation', ExponentialSaturation())
        ]), ['Radio']),
        ('banners_pipe', Pipeline([
            ('carryover', ExponentialCarryover()),
            ('saturation', ExponentialSaturation())
        ]), ['Banners']),
    ]
)

model = Pipeline([
    ('adstock', adstock),
    ('regression', LinearRegression(positive=True))
])

Это даёт модель, которая сама по себе не очень хороша, потому что ей всё ещё нужна настройка гиперпараметров.

print(model.fit(X, y).score(X, y))

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

Настройка гиперпараметров

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

from scipy.stats import uniform, randint
from sklearn.model_selection import RandomizedSearchCV, TimeSeriesSplit

tuned_model = RandomizedSearchCV(
    model,
    param_distributions={
        'adstock__tv_pipe__carryover__window': randint(1, 10),
        'adstock__tv_pipe__carryover__strength': uniform(0, 1),
        'adstock__tv_pipe__saturation__exponent': uniform(0, 1),
        'adstock__radio_pipe__carryover__window': randint(1, 10),
        'adstock__radio_pipe__carryover__strength': uniform(0, 1),
        'adstock__radio_pipe__saturation__exponent': uniform(0, 1),
        'adstock__banners_pipe__carryover__window': randint(1, 10),
        'adstock__banners_pipe__carryover__strength': uniform(0, 1),
        'adstock__banners_pipe__saturation__exponent': uniform(0,1),
    },
    cv=TimeSeriesSplit(),
    random_state=0,
    n_iter=100
)

После обучения мы можем проверить лучшие гиперпараметры:

print(tuned_model.best_params_)

Итак, модель считает, что эффект переноса ТВ длится четыре недели, примерно с 4,68% эффекта, переносимого в следующую неделю, например. Подобные заявления — золото для бизнеса.

Этот вывод позволяет вам объяснить вашу модель заинтересованным сторонам простым и понятным способом.

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

Включение временных характеристик

Я также добавил несколько удобных функций для добавления дополнительных характеристик. Просто посмотрите на это:

from mamimo.time_utils import add_time_features, add_date_indicators

X = (X
     .pipe(add_time_features, month=True)
     .pipe(add_date_indicators, special_date=["2020-01-05"])
     .assign(trend=range(200))
)

Это добавляет:

  • столбец месяца (целые числа от 1 до 12),
  • двоичный столбец с именем special_date, который равен 1 5 января 2020 года и 0 везде else, и
  • линейный тренд, который только считает от 0 до 199.

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

  • месяц получает однократное кодирование;
  • линейный тренд может быть возведён в некоторую степень, например тренд² для тренда, который растёт квадратично (бизнес был бы счастлив);
  • специальная дата может иметь эффект переноса, то есть мы говорим, что важна не только неделя 5 января 2020 года, но и потенциально недели после неё, по той же логике, что и для переноса в медиаканалах.
from mamimo.time_utils import PowerTrend
from mamimo.carryover import ExponentialCarryover
from mamimo.saturation import ExponentialSaturation
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

cats =  [list(range(1, 13))] 

preprocess = ColumnTransformer(
    [
        ('tv_pipe', Pipeline([
            ('carryover', ExponentialCarryover()),
            ('saturation', ExponentialSaturation())
        ]), ['TV']),
        ('radio_pipe', Pipeline([
            ('carryover', ExponentialCarryover()),
            ('saturation', ExponentialSaturation())
        ]), ['Radio']),
        ('banners_pipe', Pipeline([
            ('carryover', ExponentialCarryover()),
            ('saturation', ExponentialSaturation())
        ]), ['Banners']),
        ('month', OneHotEncoder(sparse=False, categories=cats), ['month']),
        ('trend', PowerTrend(), ['trend']),
        ('special_date', ExponentialCarryover(), ['special_date'])
    ]
)

new_model = Pipeline([
    ('preprocess', preprocess),
    ('regression', LinearRegression(
        positive=True,
        fit_intercept=False) 
    )
])

Фитирование этой всё ещё ненастроенной модели показывает гораздо лучшую производительность:

Последняя настройка

Давайте настроим новые гиперпараметры. Возможно, линейный тренд — это не лучшее, что мы можем сделать. Кроме того, пока что переноса нет, потому что это поведение по умолчанию для ExponentialCarryover, если вы не предоставите гиперпараметры. Мы создадим ещё одну настройку гиперпараметров:

from scipy.stats import randint, uniform
from sklearn.model_selection import RandomizedSearchCV, TimeSeriesSplit

tuned_new_model = RandomizedSearchCV(
    new_model,
    param_distributions={
        'preprocess__tv_pipe__carryover__window': randint(1, 10),
        'preprocess__tv_pipe__carryover__strength': uniform(0, 1),
        'preprocess__tv_pipe__saturation__exponent': uniform(0, 1),
        'preprocess__radio_pipe__carryover__window': randint(1, 10),
        'preprocess__radio_pipe__carryover__strength': uniform(0,1),
        'preprocess__radio_pipe__saturation__exponent': uniform(0, 1),
        'preprocess__banners_pipe__carryover__window': randint(1, 10),
        'preprocess__banners_pipe__carryover__strength': uniform(0, 1),
        'preprocess__banners_pipe__saturation__exponent': uniform(0, 1),
        'preprocess__trend__power': uniform(0, 2),           
        'preprocess__special_date__window': randint(1, 10),  
        'preprocess__special_date__strength': uniform(0, 1), 
    },
    cv=TimeSeriesSplit(),
    random_state=0,
    n_iter=1000, 
)

tuned_model.fit(X, y)

Вычисление вклада каналов

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

from mamimo.analysis import breakdown

contributions = breakdown(tuned_new_model.best_estimator_, X, y)
ax = contributions.plot.area(
    figsize=(16, 10),
    linewidth=1,
    title="Predicted Sales and Breakdown",
    ylabel="Sales",
    xlabel="Date",
)

handles, labels = ax.get_legend_handles_labels()
ax.legend(
    handles[::-1],
    labels[::-1],
    title="Channels",
    loc="center left",
    bbox_to_anchor=(1.01, 0.5),
)

Вычисление возврата инвестиций

Я не создавал для этого удобную функцию (пока?), но вы можете рассчитать ROI для каждого канала следующим образом:

for channel in ['TV', 'Radio', 'Banners']:
    roi = contributions[channel].sum() / X[channel].sum()
    print(f'{channel}: {roi:.2f}')

Заключение

Я представил вам свою новую библиотеку MaMiMo, чтобы облегчить вашу жизнь при моделировании маркетингового микса. Она хорошо интегрируется с scikit-learn и подобными библиотеками и позволяет вам делать следующие вещи (пока!):

  • определять насыщения (экспоненциальные, Хилла, Adbudg, BoxCox);
  • определять переносы (экспоненциальные, гауссовские);
  • добавлять временные характеристики (день, месяц, неделя месяца, …, тренд);
  • изменять тренд;
  • анализировать модель, проверяя вклад каналов.

Установите её через pip install mamimo!

Спасибо за чтение!