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.
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.
Step 1 Step 2 Step 3
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.
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