Skip to content

MMM Budget Allocation

In this example, we train a Market Mix Model (MMM) to allocate a marketing budget across two different channels. We use a custom data feed to retrieve data from PyMC Marketing's tutorial, and re-train our model every three months. Lastly, we backtest the model and visualize its predictions and rolling error from 2010 to the present day.


Source

Load PyMC Marketing's tutorial data into an OracleDataFrame to fetch historical data on-the-fly during the backtest.

1
2
3
4
from anterior.source import OracleDataFrame

url = "https://raw.githubusercontent.com/pymc-labs/pymc-marketing/main/datasets/mmm_example.csv"
data = OracleDataFrame.pd_from_csv(url, parse_dates=["date_week"], date_col="date_week")

Warp

First, we declare our MMM and initial variables. Then, we declare a function to optimize it with the latest data and collect logs on the model's expected contribution to sales and that of an equal allocation strategy. Lastly, using anterior's BackTester, we schedule the aforementioned function to run every six months and backtest the whole pipeline.

Declare the MMM model and initial variables.

from datetime import datetime
from pymc_marketing import mmm

from anterior.warp import BackTester

model = mmm.DelayedSaturatedMMM(
    date_column="date_week",
    channel_columns=["x1", "x2"],
    control_columns=["event_1", "event_2", "t"],
    adstock_max_lag=8, yearly_seasonality=2)

budget = 0.5
logs = []


def get_budget_allocation():
    features, targets = data[[c for c in data.columns if c != "y"]], data["y"]

    model.fit(features.reset_index(), targets, progressbar=False)

    sigmoid_params = model.compute_channel_curve_optimization_parameters_original_scale()

    opt = model. \
        optimize_channel_budget_for_maximum_contribution(method="sigmoid",
                                                         total_budget=budget,
                                                         parameters=sigmoid_params)

    x1_contribution = mmm.utils.extense_sigmoid(budget / 2, *sigmoid_params["x1"])
    x2_contribution = mmm.utils.extense_sigmoid(budget / 2, *sigmoid_params["x2"])

    logs.append({"date": datetime.now(),
                 "equal_alloc": x1_contribution + x2_contribution,
                 "mmm_alloc": opt["estimated_contribution"]["total"]})


bt = BackTester()
bt.every(months=6).do(get_budget_allocation)
bt.run(start="2019-01-01", end="2021-08-30")

Define an optimization function with logging.

from datetime import datetime
from pymc_marketing import mmm

from anterior.warp import BackTester

model = mmm.DelayedSaturatedMMM(
    date_column="date_week",
    channel_columns=["x1", "x2"],
    control_columns=["event_1", "event_2", "t"],
    adstock_max_lag=8, yearly_seasonality=2)

budget = 0.5
logs = []


def get_budget_allocation():
    features, targets = data[[c for c in data.columns if c != "y"]], data["y"]

    model.fit(features.reset_index(), targets, progressbar=False)

    sigmoid_params = model.compute_channel_curve_optimization_parameters_original_scale()

    opt = model. \
        optimize_channel_budget_for_maximum_contribution(method="sigmoid",
                                                         total_budget=budget,
                                                         parameters=sigmoid_params)

    x1_contribution = mmm.utils.extense_sigmoid(budget / 2, *sigmoid_params["x1"])
    x2_contribution = mmm.utils.extense_sigmoid(budget / 2, *sigmoid_params["x2"])

    logs.append({"date": datetime.now(),
                 "equal_alloc": x1_contribution + x2_contribution,
                 "mmm_alloc": opt["estimated_contribution"]["total"]})


bt = BackTester()
bt.every(months=6).do(get_budget_allocation)
bt.run(start="2019-01-01", end="2021-08-30")

Schedule the optimization function and backtest the pipeline.

from datetime import datetime
from pymc_marketing import mmm

from anterior.warp import BackTester

model = mmm.DelayedSaturatedMMM(
    date_column="date_week",
    channel_columns=["x1", "x2"],
    control_columns=["event_1", "event_2", "t"],
    adstock_max_lag=8, yearly_seasonality=2)

budget = 0.5
logs = []


def get_budget_allocation():
    features, targets = data[[c for c in data.columns if c != "y"]], data["y"]

    model.fit(features.reset_index(), targets, progressbar=False)

    sigmoid_params = model.compute_channel_curve_optimization_parameters_original_scale()

    opt = model. \
        optimize_channel_budget_for_maximum_contribution(method="sigmoid",
                                                         total_budget=budget,
                                                         parameters=sigmoid_params)

    x1_contribution = mmm.utils.extense_sigmoid(budget / 2, *sigmoid_params["x1"])
    x2_contribution = mmm.utils.extense_sigmoid(budget / 2, *sigmoid_params["x2"])

    logs.append({"date": datetime.now(),
                 "equal_alloc": x1_contribution + x2_contribution,
                 "mmm_alloc": opt["estimated_contribution"]["total"]})


bt = BackTester()
bt.every(months=6).do(get_budget_allocation)
bt.run(start="2019-01-01", end="2021-08-30")

Push

We create a table to compare the MMM's performance with that of an equal allocation strategy on a rolling, six-month basis.

1
2
3
4
5
import pandas as pd

results = pd.DataFrame.from_records(logs, index="date")
results['percentage_gain'] = (results['mmm_alloc'] - results['equal_alloc']) / results['equal_alloc']
print(results)

Output:

date equal_alloc mmm_alloc percentage_gain
2019-01-01 00:00:00 2096.76 2327.42 0.110011
2019-07-01 00:00:00 2124.46 2362.46 0.112029
2020-01-01 00:00:00 2367.61 2500.78 0.0562446
2020-07-01 00:00:00 2289.19 2425.39 0.059496
2021-01-01 00:00:00 2306.23 2414.29 0.0468537
2021-07-01 00:00:00 2279.21 2358.85 0.0349413