Introduction to Marketing Mix Modeling in Python | by Dr. Robert Kübler | Towards Data Science

To keep a business running, spending money on advertising is crucial — this is the case regardless of whether the company is small or already established. And the number of ad spendings in the industry are enormous:
These volumes make it necessary to spend each advertising dollar wisely. However, this is easier said than done, or as US retail magnate John Wanamaker or UK industrialist Lord Leverhulme put it about a hundred years ago:
“Half the money I spend on advertising is wasted; the trouble is I don’t know which half.”
You might think that this is less of a problem nowadays, but strangely enough, it still persists. Luckily, we are in the position of having access to a lot of data and powerful computers to change this state of affairs through advanced analyses, such as Attribution Modeling or Marketing Mix Modeling. In this article, we will focus on the latter.
Imagine now that you are responsible for the marketing budget of some established company. To increase sales, you play advertising in three different advertising channels:
TV,
radio and
web banners.
In each week, you decide to spend some amount of money on each channel, or not. Additionally, you get to observe the number of sales each week. The data collected for 200 weeks might look like this:
All numbers from the table are in a currency of your choice, I’ll use € from now on. You can get the above file here.
From the small sneak peek above, we can see that there are a lot of weeks without TV advertising (71%), and also some without radio ads (54%). Web banners are only disabled in around 24% of the observations, making it the most frequently used channel.
However, when we make TV spendings, they tend to be higher than radio spending, which in turn are higher than web banner advertising spending. Furthermore, there are sales all the time.
Now, before we start modeling, let us clarify the goal first.
In the end, we want to be able to answer questions such as
How much of the 15,904.11 € sales in the week ending on 2021–10–10 (see table above) was generated by TV advertising? And how much by radio and web banners? And what is the baseline, i.e. the number of sales we would have had without any advertising?
If our model can do this, we can also use it to compute ROIs and optimize spending, which is what companies ultimately want. Having this goal in mind, we can restrict ourselves to additive models, i.e. models of the form
Sales = f(TV) + g(Radio) + h(Banners) + Base
because they let us decompose the sales easily. The sales are just the sum of some function that only depends on the TV spendings, another function that only depends on radio spendings, another function that only depends on web banner spendings, and a (constant) baseline.
Models such as random forests, gradient boosting, or (plain and simple feed-forward) neural networks are not suitable here, as we cannot get such a decomposition out of them.
Note: Sure, there are Shapley values that kind of do what we want, but often the contributions according to the Shapley values are negative, which is something unreasonable that marketing people do not want to hear.
A candidate that gives us an additive contribution for each channel is an old friend — linear regression, the simplest representative of additive models!
You can find the follow-up article with a more elaborated model here:
https://towardsdatascience.com/an-upgraded-marketing-mix-modeling-in-python-5ebb3bddc1b6
After storing the data from above in the variable data , we do the following:
from sklearn.linear_model import LinearRegressionfrom sklearn.model_selection import cross_val_score, TimeSeriesSplitimport pandas as pddata = pd.read_csv(    'https://raw.githubusercontent.com/Garve/datasets/4576d323bf2b66c906d5130d686245ad205505cf/mmm.csv',    parse_dates=['Date'],    index_col='Date')X = data.drop(columns=['Sales'])y = data['Sales']lr = LinearRegression()print(cross_val_score(lr, X, y, cv=TimeSeriesSplit()))# Output: [0.69594303 0.69302285 0.66850729 0.78807363 0.73512387]
Note: We do not use the standard k-fold cross-validation here because we are dealing with time series data. TimeSeriesSplit is a more reasonable thing to do, and you can read more about it here.
Well, this looks reasonable already, although it could be better. But this model lets us break up the sales as we wanted to, as the formula is simply
Sales = 0.36 * TV + 0.49 * Radio + 1.23 * Banners + 6678.40
We can get the coefficients and the intercept via a simple
lr.fit(X, y) # refit the model with the complete datasetprint('Coefficients:', lr.coef_)print('Intercept:', lr.intercept_)# Output:# Coefficients: [0.35968382 0.48833246 1.2159193 ]# Intercept: 6678.396933606161
To illustrate the computation of the contributions, let us consider a single week:
Let us plug in the numbers and see what we get:
print(lr.predict(([[11543.58, 4615.35, 2518.88]])))# Output: [16147.01594158]
This is not exactly the true answer of 15904.11 from the table above, but let us stick with it for now. We can now see that the (unadjusted) contribution of TV is
coef_TV * spendings_TV = 0.36 * 11543.58 = 4155.69,
and for the other channels accordingly. Now, the contributions sum up to the model prediction 16147.0159, which is not the true target of 15904.11, so let us just multiply the contributions and baseline by a correction factor of correction_factor = 15904.11 / 16147.0159 ≈ 0.985 and everything is fine. We get
contribution_TV = correction_factor * 4155.69 = 4089.57.
We also get
contribution_radio = 2219.92 and
contribution_banners = 3016.68
base = 6577.93.
Adding everything up yields the observed label, as we want it:
4089.57 + 2219.93 + 3016.68 + 6577.93 = 15904.11.
We can generate a nice contribution plot for all observations like this:
weights = pd.Series(    lr.coef_,    index=X.columns)base = lr.intercept_unadj_contributions = X.mul(weights).assign(Base=base)adj_contributions = (unadj_contributions                     .div(unadj_contributions.sum(axis=1), axis=0)                     .mul(y, axis=0)                    ) # contains all contributions for each dayax = (adj_contributions[['Base', 'Banners', 'Radio', 'TV']]      .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))
The output is:
We can see (or compute) that the baseline is around 6500 sales each day, banners and radio contribute about 2500 on average whenever they are active, and TV around 3500 when it is active. Nice!
We can now determine which channel was the best in terms of ROI, a number that measures the efficiency. The formula is simple:
channel_ROI = Sales from the channel / channel spendings
With the code snippets from above, we already have all the data we need for the computation. You can calculate the TV ROI as follows:
sales_from_tv = adj_contributions['TV'].sum()spendings_on_tv = data['TV'].sum()tv_roi = sales_from_tv / spendings_on_tv# tv_roi is around 0.36
Simple as that. An ROI less than 1 means that the channel performed poorly. For the TV ROI we can say:
For each 1 € we spent on TV, we got 36 cents back.
That is a kind of deal that we do not want to make too often if we want the company to survive. On the other hand, the banners have an ROI of 1.21 which is much better, it seems that this channel worked quite well in the time period that we considered.
While the above approach seems reasonable, it has certain shortcomings that we have to address:
The performance could be better. Sometimes, there is nothing we can do about bad performance because it depends on the data a lot, but we should try our best anyway.Even more severe: The model itself definitely does not reflect reality. According to the linear formula, we can drive sales as high as we want by spending more and more money on advertising. Since the banners have a high coefficient of 1.23, for each 1 € we spend in this channel we generate 1.23 € additional sales. Repeat for infinite money, customers will hate this trick!Optimizing becomes trivial and unrealistic as well. To maximize sales, we would now put all the money into the web banners channel because it has the highest coefficient. We would abandon TV and radio advertising altogether which may be not the right thing to do if the company wants to retain its awareness among people.
The performance could be better. Sometimes, there is nothing we can do about bad performance because it depends on the data a lot, but we should try our best anyway.
Even more severe: The model itself definitely does not reflect reality. According to the linear formula, we can drive sales as high as we want by spending more and more money on advertising. Since the banners have a high coefficient of 1.23, for each 1 € we spend in this channel we generate 1.23 € additional sales. Repeat for infinite money, customers will hate this trick!
Optimizing becomes trivial and unrealistic as well. To maximize sales, we would now put all the money into the web banners channel because it has the highest coefficient. We would abandon TV and radio advertising altogether which may be not the right thing to do if the company wants to retain its awareness among people.
We will solve all of these problems in the follow-up article, stay tuned!
We have seen that companies use a significant proportion of their revenues for advertising to encourage customers to buy their products. However, using the marketing budget efficiently is not easy, both a hundred years ago and today. It is not that easy to figure out how much a certain TV ad spending influenced the sales, and hence if the ad spending was worth it and how to optimize it for next time.
To solve this problem, we created a small marketing mix model that allowed us to break down the observed sales into shares: a TV, radio, banner, and base share. These channel contributions let us compute ROIs that allowed us to see how each channel performed.
Nevertheless, this model is too simple to capture reality, which creates a lot of problems. But we will learn how to circumvent those by making the model slightly more complex, but still interpretable.
You can find the follow-up article here:
towardsdatascience.com
I hope that you learned something new, interesting, and useful today. Thanks for reading!
As the last point, if you
want to support me in writing more about machine learning andplan to get a Medium subscription anyway,
want to support me in writing more about machine learning and
plan to get a Medium subscription anyway,
why not do it via this link? This would help me a lot! 😊
To be transparent, the price for you does not change, but about half of the subscription fees go directly to me.
Thanks a lot, if you consider supporting me!
If you have any questions, write me on LinkedIn!