Marketing Mix Modeling for Digital vs Offline Media
1 Introduction
FourTex is an apparel brand. Its products are targeted at the mass market with a focus on casual apparel for men and women. The brand’s advertising is prominent and extensive, and is present on multiple channels such as television, radio, paid search, and social media.
Recently, the digital marketing tools have extended FourTex’s reach among the potential customers. The marketing director, Hannah Schmidt, was so proud of her team’s achievements. She was getting ready for a meeting with the management board to explain how successful their previous marketing campaigns were, and hoping to get some additional budget in order to further improve the brand’s online store visits and sales leads performance metrics.
The meeting did not go as planned. In a difficult conversation, the chief executive officer (CEO) of the company said: “Mrs. Schmidt, you always ask for more money, but can rarely explain how much incremental value this money will generate”. Coming out of the meeting, Mrs. Schmidt felt that she was under enormous pressure to demonstrate the value of her marketing decisions.
Next day, she pulled some data from the company’s database, taking her notes. The table below provides a brief description of her data set:
| Marketing_Mix_Variable | Description | Channel |
|---|---|---|
| Google AdWords | Cost of Google AdWords campaigns | Online |
| Facebook ads | Cost of sponsered ads delivered on Facebook | Online |
| TV ads | Cost of TV advertising | Offline |
| Radio ads | Cost of radio advertising | Offline |
| Web traffic | Total number of visits to the website | Online |
Being very keen to demonstrate marketing’s value, Mrs. Schmidt asked her analytics team to assess the impact of their Google AdWords, Facebook, TV, and radio ads on website traffic performance. She did not want to include the sales performance metric in the analysis because her campaigns last year aimed to increase conversion to the website rather than the sales outcomes. Therefore, she thought that the relevant key performance indicator was website traffic.
Finally, Mrs. Schmidt prepared a checklist for the analytics team. She seeks answers to the following questions:
Which marketing mix instrument really drives the performance outcome, i.e. website traffic?
What is the return on marketing investment?
Should I keep pushing on with Google adwords and Facebook ads? Should I stop advertising on TV and radio channels?
How can Mrs. Schmidt demonstrate the impact of her marketing mix decisions to the management board?
To address Mrs. Schmidt’s questions, we will build a marketing mix model that gauges the effectiveness of her marketing mix decisions and estimates the contribution of each advertising vehicle to website traffic performance.
Before we roll out the analyses, we should make sure that all our files are organised and our R environment is set up.
2 Preparation and set-up
3 Exploratory data analysis
3.1 Packages
3.2 Data
spc_tbl_ [57 × 6] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
$ week_beg : chr [1:57] "06/07/2020" "13/07/2020" "20/07/2020" "27/07/2020" ...
$ Google_Adwords: num [1:57] 94017 93635 112239 121250 130471 ...
$ Facebook : num [1:57] 82950 66150 94650 96450 114600 ...
$ TV : num [1:57] 0 0 0 0 0 0 0 0 0 0 ...
$ Radio : num [1:57] 0 0 0 0 0 ...
$ traffic : num [1:57] 6719812 6186229 6790416 7146957 7815741 ...
- attr(*, "spec")=
.. cols(
.. week_beg = col_character(),
.. Google_Adwords = col_double(),
.. Facebook = col_double(),
.. TV = col_double(),
.. Radio = col_double(),
.. traffic = col_double()
.. )
- attr(*, "problems")=<externalptr>
Code
class(data)[1] "spec_tbl_df" "tbl_df" "tbl" "data.frame"
Code
data <- data |> clean_names()The data set has 6 variables and 57 time series observations for each variable.
Next, we would like to extract each of the variables to the environment tab:
Code
# Extract the variables
google_adwords <- data$google_adwords
facebook <- data$facebook
tv <- data$tv
radio <- data$radio
traffic <- data$trafficCode
# Make the data time series data in R
# Frequency=52
google_adwords <- ts(google_adwords, frequency = 52, start = c(2020, 28))
facebook <- ts(facebook, frequency = 52, start = c(2020, 28))
tv <- ts(tv, frequency = 52, start = c(2020, 28))
radio <- ts(radio, frequency = 52, start = c(2020, 28))
traffic <- ts(traffic, frequency = 52, start = c(2020, 28))Setting ts() arguments
Note that the dataset runs on a weekly basis. We set the frequency of the data to weeks. However, not all the years have weeks. Normally, the year has 365.25/7 = 52.18 weeks, on average. This allows for a leap year every fourth year. Therefore, some years may have 53 weeks. This is not an issue for our data, as we have 57 observations spanning over two years. None of them covers 53 weeks.
3.3 Plot the data
To get a feel for the data patterns, we need to perform a visual inspection through time series plots. First, we sum up online spending variables to find the total online spending. We do the same for offline spending variables.
Code
online_total <- google_adwords + facebook
offline_total <- tv + radio3.4 Current Budget allocation
Next, we will have a look at the current budget allocation of online vs. traditional media spending.
Pie chart
Code
### Media spending share
sum_online <- sum(online_total)
sum_offline <- sum(offline_total)
total_spend <- sum_online + sum_offline
online_share <- sum_online / total_spend
offline_share <- sum_offline / total_spend
### Pie-Chart for Media Spending Share
slices <- c(online_share, offline_share)
lbls <- c("Online", "Offline")
pct <- round(slices * 100)
lbls <- paste(lbls, pct) # add percent data to labels
lbls <- paste(lbls, "%", sep = "") # add % sign to labels
par(mfrow = c(1, 1))
pie(
slices,
labels = lbls,
col = rainbow(length(lbls)),
main = "Ad Spending Share"
)
Line chart
Code
library(lubridate)
data <- data |>
mutate(
google_adwords = ts(google_adwords, frequency = 52, start = c(2020, 28)),
facebook = ts(facebook, frequency = 52, start = c(2020, 28)),
tv = ts(tv, frequency = 52, start = c(2020, 28)),
radio = ts(radio, frequency = 52, start = c(2020, 28))
) |>
mutate(sum_online = google_adwords + facebook, sum_offline = tv + radio) |>
mutate(week_beg = dmy(week_beg))
library(scales)
data |>
pivot_longer(
c(sum_online, sum_offline),
names_to = "type",
values_to = "amount"
) |>
ggplot(aes(week_beg, amount, group = type)) +
geom_line(aes(color = type)) +
scale_y_continuous(labels = comma_format()) +
labs(
title = "Advertising Spending by Month",
x = "Week",
y = "Pound",
color = ""
)
Bar chart
Code
data |>
summarize(online = sum(sum_online), offline = sum(sum_offline)) |>
pivot_longer(everything(), names_to = "ad_type", values_to = "amount") |>
mutate(percent = amount / sum(amount)) |>
ggplot(aes(ad_type, amount, fill = ad_type)) +
geom_col(show.legend = FALSE) +
geom_text(
aes(label = paste0(round(percent * 100, digits = 2), "%")),
vjust = -0.4
) +
scale_y_continuous(labels = comma_format()) +
labs(title = "Advertising Spending by Type of Ads")
4 Marketing mix modeling
After exploring the main features of the data, we are ready to investigate the drivers of the web traffic performance. We will develop a multiple regression model that uses the traffic data as dependent variable (i.e. response variable) while Google AdWords, Facebook, TV, and radio variables will be used as independent variables, also known as predictors.
4.1 Modeling Diminishing Returns
At this point, an important decision we need to make is which functional form to use in the model. Shall we assume a linear or a non-linear relationship? Marketing literature suggests that the relationship between advertising and performance variables mostly follows a diminishing return pattern (Hanssens et al., 2001, 2014), as illustrated in the following plot.
This plot tells us that initially spending more and more money on advertising is beneficial but after a certain point the additional value gained from an extra spending will be very small. How can we introduce this type of non-linearity in the model? A typical approach is to use a log-log model specification. The log-log regression model suggests that log transformation is performed for both sides of the equation.
4.2 log-transformation
To estimate the model, we will perform log transformation on our variables.
The reason for adding +1 to the variables is that some variables include zero observations. When we take the logarithm of zero, it is not identified. Therefore, we should add a small number to be able to take the logarithm.
Code
| week_beg | google_adwords | tv | radio | traffic | sum_online | sum_offline | lngoogle_adwords | lnfacebook | lntv | lnradio | lntraffic | L1.lntraffic | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2020-07-06 | 94017.2 | 82950 | 0 | 0 | 6719812 | 176967.2 | 0 | 11.45124 | 11.32601 | 0.00000 | 0.00000 | 15.72057 | NA |
| 2020-07-13 | 93634.5 | 66150 | 0 | 0 | 6186229 | 159784.5 | 0 | 11.44716 | 11.09970 | 0.00000 | 0.00000 | 15.63784 | 15.72057 |
| 2020-07-20 | 112239.4 | 94650 | 0 | 0 | 6790416 | 206889.4 | 0 | 11.62840 | 11.45795 | 0.00000 | 0.00000 | 15.73102 | 15.63784 |
| 2020-07-27 | 121250.3 | 96450 | 0 | 0 | 7146957 | 217700.3 | 0 | 11.70562 | 11.47679 | 0.00000 | 0.00000 | 15.78220 | 15.73102 |
| 2020-08-03 | 130470.9 | 114600 | 0 | 0 | 7815741 | 245070.9 | 0 | 11.77891 | 11.64921 | 0.00000 | 0.00000 | 15.87165 | 15.78220 |
| 2020-08-10 | 131955.3 | 119550 | 0 | 0 | 8444791 | 251505.3 | 0 | 11.79023 | 11.69150 | 0.00000 | 0.00000 | 15.94906 | 15.87165 |
| 2020-08-17 | 129812.0 | 106350 | 0 | 0 | 8267982 | 236162.0 | 0 | 11.77385 | 11.57450 | 0.00000 | 0.00000 | 15.92790 | 15.94906 |
| 2020-08-24 | 110176.6 | 116550 | 0 | 0 | 7059093 | 226726.6 | 0 | 11.60985 | 11.66608 | 0.00000 | 0.00000 | 15.76983 | 15.92790 |
| 2020-08-31 | 97206.0 | 90150 | 0 | 1298507 | 6137780 | 187356.0 | 1298507 | 11.48460 | 11.40924 | 0.00000 | 14.07673 | 15.62997 | 15.76983 |
| 2020-09-07 | 107410.0 | 82650 | 0 | 0 | 6356676 | 190060.0 | 0 | 11.58442 | 11.32238 | 0.00000 | 0.00000 | 15.66502 | 15.62997 |
| 2020-09-14 | 77073.5 | 70650 | 0 | 0 | 5237124 | 147723.5 | 0 | 11.25253 | 11.16551 | 0.00000 | 0.00000 | 15.47128 | 15.66502 |
| 2020-09-21 | 103647.3 | 70950 | 0 | 0 | 4999990 | 174597.3 | 0 | 11.54876 | 11.16974 | 0.00000 | 0.00000 | 15.42495 | 15.47128 |
| 2020-09-28 | 116805.4 | 76650 | 0 | 0 | 5449003 | 193455.4 | 0 | 11.66827 | 11.24702 | 0.00000 | 0.00000 | 15.51094 | 15.42495 |
| 2020-10-05 | 148509.5 | 96750 | 0 | 0 | 6726743 | 245259.5 | 0 | 11.90841 | 11.47990 | 0.00000 | 0.00000 | 15.72160 | 15.51094 |
| 2020-10-12 | 125055.1 | 88500 | 4341824 | 0 | 6226920 | 213555.1 | 4341824 | 11.73652 | 11.39077 | 15.28381 | 0.00000 | 15.64439 | 15.72160 |
| 2020-10-19 | 142059.0 | 85650 | 0 | 0 | 6316911 | 227709.0 | 0 | 11.86400 | 11.35804 | 0.00000 | 0.00000 | 15.65874 | 15.64439 |
| 2020-10-26 | 137317.6 | 80400 | 0 | 0 | 6045622 | 217717.6 | 0 | 11.83006 | 11.29478 | 0.00000 | 0.00000 | 15.61485 | 15.65874 |
| 2020-11-02 | 169948.5 | 92250 | 0 | 0 | 6532776 | 262198.5 | 0 | 12.04326 | 11.43227 | 0.00000 | 0.00000 | 15.69234 | 15.61485 |
| 2020-11-09 | 180195.8 | 100500 | 0 | 0 | 7013419 | 280695.8 | 0 | 12.10180 | 11.51792 | 0.00000 | 0.00000 | 15.76334 | 15.69234 |
| 2020-11-16 | 171117.0 | 80400 | 0 | 0 | 6661626 | 251517.0 | 0 | 12.05011 | 11.29478 | 0.00000 | 0.00000 | 15.71187 | 15.76334 |
| 2020-11-23 | 322622.5 | 338700 | 6693444 | 1514925 | 13174892 | 661322.5 | 8208369 | 12.68424 | 12.73287 | 15.71664 | 14.23088 | 16.39382 | 15.71187 |
| 2020-11-30 | 212631.2 | 98100 | 4341824 | 0 | 7408386 | 310731.2 | 4341824 | 12.26732 | 11.49375 | 15.28381 | 0.00000 | 15.81812 | 16.39382 |
| 2020-12-07 | 234100.7 | 101400 | 4341824 | 1514925 | 8927318 | 335500.7 | 5856749 | 12.36351 | 11.52684 | 15.28381 | 14.23088 | 16.00463 | 15.81812 |
| 2020-12-14 | 260568.5 | 128850 | 4341824 | 0 | 10791803 | 389418.5 | 4341824 | 12.47062 | 11.76641 | 15.28381 | 0.00000 | 16.19430 | 16.00463 |
| 2020-12-21 | 290988.0 | 137250 | 4341824 | 1514925 | 12784412 | 428238.0 | 5856749 | 12.58104 | 11.82957 | 15.28381 | 14.23088 | 16.36374 | 16.19430 |
| 2020-12-28 | 133238.1 | 102600 | 0 | 0 | 8615943 | 235838.1 | 0 | 11.79990 | 11.53860 | 0.00000 | 0.00000 | 15.96913 | 16.36374 |
| 2021-01-04 | 109836.6 | 82350 | 0 | 0 | 4922788 | 192186.6 | 0 | 11.60676 | 11.31875 | 0.00000 | 0.00000 | 15.40939 | 15.96913 |
| 2021-01-11 | 128108.2 | 72900 | 0 | 0 | 4336508 | 201008.2 | 0 | 11.76064 | 11.19686 | 0.00000 | 0.00000 | 15.28258 | 15.40939 |
| 2021-01-18 | 119720.6 | 69750 | 0 | 0 | 4409387 | 189470.6 | 0 | 11.69292 | 11.15269 | 0.00000 | 0.00000 | 15.29925 | 15.28258 |
| 2021-01-25 | 128990.1 | 66000 | 0 | 0 | 4183630 | 194990.1 | 0 | 11.76750 | 11.09743 | 0.00000 | 0.00000 | 15.24669 | 15.29925 |
| 2021-02-01 | 135977.3 | 53700 | 0 | 0 | 3995216 | 189677.3 | 0 | 11.82025 | 10.89119 | 0.00000 | 0.00000 | 15.20061 | 15.24669 |
| 2021-02-08 | 143930.6 | 131400 | 0 | 0 | 4505004 | 275330.6 | 0 | 11.87709 | 11.78601 | 0.00000 | 0.00000 | 15.32070 | 15.20061 |
| 2021-02-15 | 145172.9 | 73800 | 0 | 0 | 5002462 | 218972.9 | 0 | 11.88569 | 11.20913 | 0.00000 | 0.00000 | 15.42544 | 15.32070 |
| 2021-02-22 | 168285.3 | 85050 | 0 | 0 | 5252341 | 253335.3 | 0 | 12.03342 | 11.35101 | 0.00000 | 0.00000 | 15.47418 | 15.42544 |
| 2021-03-01 | 176839.9 | 81450 | 0 | 0 | 5112821 | 258289.9 | 0 | 12.08301 | 11.30776 | 0.00000 | 0.00000 | 15.44726 | 15.47418 |
| 2021-03-08 | 189027.1 | 85050 | 4397123 | 1511474 | 5689169 | 274077.1 | 5908598 | 12.14965 | 11.35101 | 15.29646 | 14.22860 | 15.55407 | 15.44726 |
| 2021-03-15 | 180690.1 | 101250 | 0 | 0 | 6502390 | 281940.1 | 0 | 12.10454 | 11.52536 | 0.00000 | 0.00000 | 15.68768 | 15.55407 |
| 2021-03-22 | 171390.5 | 76200 | 0 | 0 | 5524652 | 247590.5 | 0 | 12.05171 | 11.24113 | 0.00000 | 0.00000 | 15.52473 | 15.68768 |
| 2021-03-29 | 162937.5 | 83550 | 0 | 0 | 5214240 | 246487.5 | 0 | 12.00113 | 11.33321 | 0.00000 | 0.00000 | 15.46690 | 15.52473 |
| 2021-04-05 | 177504.1 | 90450 | 0 | 0 | 5687588 | 267954.1 | 0 | 12.08675 | 11.41256 | 0.00000 | 0.00000 | 15.55380 | 15.46690 |
| 2021-04-12 | 193782.7 | 104100 | 0 | 0 | 5847569 | 297882.7 | 0 | 12.17450 | 11.55312 | 0.00000 | 0.00000 | 15.58154 | 15.55380 |
| 2021-04-19 | 209021.0 | 111000 | 4397123 | 1511474 | 7508050 | 320021.0 | 5908598 | 12.25019 | 11.61729 | 15.29646 | 14.22860 | 15.83149 | 15.58154 |
| 2021-04-26 | 144461.4 | 90000 | 0 | 0 | 5374538 | 234461.4 | 0 | 11.88077 | 11.40758 | 0.00000 | 0.00000 | 15.49718 | 15.83149 |
| 2021-05-03 | 124752.6 | 73800 | 0 | 0 | 4588852 | 198552.6 | 0 | 11.73410 | 11.20913 | 0.00000 | 0.00000 | 15.33914 | 15.49718 |
| 2021-05-10 | 169455.0 | 115050 | 0 | 0 | 5512025 | 284505.0 | 0 | 12.04035 | 11.65313 | 0.00000 | 0.00000 | 15.52244 | 15.33914 |
| 2021-05-17 | 190908.2 | 376650 | 0 | 0 | 6834931 | 567558.2 | 0 | 12.15955 | 12.83907 | 0.00000 | 0.00000 | 15.73756 | 15.52244 |
| 2021-05-24 | 185839.3 | 144900 | 0 | 0 | 6863975 | 330739.3 | 0 | 12.13264 | 11.88381 | 0.00000 | 0.00000 | 15.74180 | 15.73756 |
| 2021-05-31 | 180518.6 | 290850 | 0 | 0 | 7158438 | 471368.6 | 0 | 12.10359 | 12.58057 | 0.00000 | 0.00000 | 15.78380 | 15.74180 |
| 2021-06-07 | 193529.5 | 139350 | 4397123 | 0 | 7074652 | 332879.5 | 4397123 | 12.17319 | 11.84475 | 15.29646 | 0.00000 | 15.77203 | 15.78380 |
| 2021-06-14 | 147352.4 | 116250 | 0 | 0 | 6488603 | 263602.4 | 0 | 11.90059 | 11.66351 | 0.00000 | 0.00000 | 15.68556 | 15.77203 |
| 2021-06-21 | 148203.4 | 131850 | 0 | 0 | 6464867 | 280053.4 | 0 | 11.90635 | 11.78943 | 0.00000 | 0.00000 | 15.68189 | 15.68556 |
| 2021-06-28 | 159418.6 | 126750 | 0 | 0 | 7041649 | 286168.6 | 0 | 11.97929 | 11.74998 | 0.00000 | 0.00000 | 15.76735 | 15.68189 |
| 2021-07-05 | 154105.7 | 147150 | 0 | 1036439 | 6832453 | 301255.7 | 1036439 | 11.94540 | 11.89921 | 0.00000 | 13.85130 | 15.73719 | 15.76735 |
| 2021-07-12 | 132009.8 | 106200 | 0 | 0 | 5883640 | 238209.8 | 0 | 11.79064 | 11.57309 | 0.00000 | 0.00000 | 15.58769 | 15.73719 |
| 2021-07-19 | 183341.3 | 214500 | 0 | 0 | 5900047 | 397841.3 | 0 | 12.11911 | 12.27607 | 0.00000 | 0.00000 | 15.59047 | 15.58769 |
| 2021-07-26 | 189316.0 | 133200 | 0 | 1036439 | 6549324 | 322516.0 | 1036439 | 12.15118 | 11.79961 | 0.00000 | 13.85130 | 15.69487 | 15.59047 |
| 2021-08-02 | 201080.7 | 142950 | 0 | 0 | 7018006 | 344030.7 | 0 | 12.21147 | 11.87026 | 0.00000 | 0.00000 | 15.76399 | 15.69487 |
Code
Call:
lm(formula = lntraffic ~ L1.lntraffic + lngoogle_adwords + lnfacebook +
lntv + lnradio, data = data)
Residuals:
Min 1Q Median 3Q Max
-0.32118 -0.08169 -0.00214 0.08718 0.21416
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 3.150739 1.498074 2.103 0.04050 *
L1.lntraffic 0.535935 0.076474 7.008 0.00000000583 ***
lngoogle_adwords 0.155433 0.089834 1.730 0.08976 .
lnfacebook 0.193668 0.056041 3.456 0.00113 **
lntv 0.004569 0.004282 1.067 0.29107
lnradio 0.006963 0.004046 1.721 0.09147 .
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.1252 on 50 degrees of freedom
(1 observation deleted due to missingness)
Multiple R-squared: 0.7578, Adjusted R-squared: 0.7336
F-statistic: 31.29 on 5 and 50 DF, p-value: 0.0000000000000273
Code
Call:
lm(formula = lntraffic ~ L1.lntraffic + lngoogle_adwords + lnfacebook +
lntv + lnradio, data = .)
Residuals:
Min 1Q Median 3Q Max
-0.32118 -0.08169 -0.00214 0.08718 0.21416
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 3.150739 1.498074 2.103 0.04050 *
L1.lntraffic 0.535935 0.076474 7.008 0.00000000583 ***
lngoogle_adwords 0.155433 0.089834 1.730 0.08976 .
lnfacebook 0.193668 0.056041 3.456 0.00113 **
lntv 0.004569 0.004282 1.067 0.29107
lnradio 0.006963 0.004046 1.721 0.09147 .
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.1252 on 50 degrees of freedom
(1 observation deleted due to missingness)
Multiple R-squared: 0.7578, Adjusted R-squared: 0.7336
F-statistic: 31.29 on 5 and 50 DF, p-value: 0.0000000000000273
4.3 Model Output
Residuals:
Residuals represent the ‘unexplained’ part of the model, i.e. the impact of other factors that are not explicitly included in the model. The descriptive statistics (e.g. min, max) of residuals are reported at the top of the model output.
Coefficient estimates:
We can write down the estimated coefficients of this marketing mix model in an equation format:
The standard error of each coefficient shows at what precision level we estimated that particular coefficient, i.e. represents the uncertainty surrounding that coefficient. The t-value is obtained by dividing the coefficient by its standard error. P-value, Pr(>|t|), is computed based on the t-value, and helps us understand whether the coefficient is statistically significant. For example, lagged traffic is a strong indicator of the next period’s traffic as it is highly significant (p-value is close to zero). The effect of Facebook ads is significant at 1% level while Google AdWords and Radio effects are significant at 10% level. Finally, the effect of TV ads is not statistically significant.
4.4 Model Fit
After we estimate the coefficients, we can obtain the model fit plot to see whether our model captures the patterns in the traffic data.
line plot
Code
# Model fit plot
fitted_traffic <- ts(modfit$fitted.values, frequency = 52, start=c(2020,28))
fitted_traffic <- c(NA, fitted_traffic)
plot(data$lntraffic, type="l", col="blue", main="Web Traffic",lwd=3)# l=line, lwd=line width
lines(fitted_traffic, type="b", col="red") # b=both
legend("topleft", lty=1, col=c("blue", "red"), #lty=line type; 1=solid line, 2=dotted line
c("Logged Traffic Data","Fitted"))
line plot
Code
data <- data.frame(data, fitted_traffic)
data |>
pivot_longer(c(lntraffic,fitted_traffic),
names_to = "type", values_to = "value") |>
ggplot(aes(week_beg, value, color = type)) +
geom_line() +
labs(title = "How does fitted traffic capture actual traffic?",
x = "Begining of Week",
color = "")
4.5 Model Diagnostics
Once we estimate our marketing mix model, it is usually a good practice to perform model diagnostic checks on the estimated residuals. In the marketing mix model above, we assume that residuals are uncorrelated (i.e.independent), have zero mean and constant variance. If the model passes these diagnostics, we conclude that the model is not misspecified and can be used to make statistical inferences and predictions.
Core Assumptions of Linear Regression
| Assumption | Diagnostic Test / Plot | Explanation |
|---|---|---|
| Linearity | Residuals vs Fitted Plot | Residuals should have no pattern (random scatter). |
| Independence of errors | Durbin-Watson Test, ACF Plot | Especially important in time series data. |
| Homoscedasticity (constant variance) | Scale-Location Plot, Residuals vs Fitted Plot | Variance of residuals should be constant across fitted values. |
| Normality of residuals | Q-Q Plot, Histogram of residuals | Needed for valid hypothesis tests and confidence intervals. |
| No multicollinearity | Variance Inflation Factor (VIF) | Predictors should not be highly correlated. |
| No influential outliers | Cook’s Distance, Standardized Residuals | Outliers can disproportionately influence the regression line. |
Next, we turn our attention to the following questions that are central to the case study:
What drives the web traffic performance?
What is the traffic return on marketing investment?
What is the optimal budget allocation?
5 What drives web traffic performance?
What is the marketing’s contribution to web traffic performance? How much traffic was generated thanks to Google AdWords, Facebook, TV, and radio? To see this, first, we need to convert the elasticities to unit effects using the following formula:
5.1 Calculating unit effects
The following code chunk retrieves the coefficients (elasticities) from our log-log model output and then computes the unit effects.
Code
#Retrieve each model coefficient:
beta_adwords <- summary(modfit)$coefficients[3,1]
beta_facebook <- summary(modfit)$coefficients[4,1]
beta_tv <- summary(modfit)$coefficients[5,1]
beta_radio <- summary(modfit)$coefficients[6,1]
#Calculate the baseline (average) traffic:
average_traffic <- mean(traffic)
#Calculate the baseline (average) advertising spending for each media:
average_adwords <- mean(google_adwords)
average_facebook <- mean(facebook)
average_tv <- mean(tv)
average_radio <- mean(radio)
# Finally, calculate the unit effects:
theta_adwords <- beta_adwords*(average_traffic/average_adwords)
theta_facebook <- beta_facebook*(average_traffic/average_facebook)
theta_tv <- beta_tv*(average_traffic/average_tv)
theta_radio <- beta_radio*(average_traffic/average_radio)5.2 Calculating media contribution
Next, we compute the contribution of each advertising media to the overall traffic performance using the following formula:
Code
#How much traffic we got thanks to TV, Adwords etc.?
sum_adwords <- sum(google_adwords)
sum_facebook <- sum(facebook)
sum_tv <- sum(tv)
sum_radio <- sum(radio)
#Each media's contribution to traffic
adwords_contribution <- theta_adwords * sum_adwords
facebook_contribution <- theta_facebook * sum_facebook
tv_contribution <- theta_tv * sum_tv
radio_contribution <- theta_radio*sum_radio
print(adwords_contribution)[1] 57994699
Code
print(facebook_contribution)[1] 72261042
Code
print (tv_contribution)[1] 1704751
Code
print (radio_contribution)[1] 2597922
5.3 Visualizing media contributions
We need to plug in the necessary data for the bar plot. So, we need the following pieces:
Code
# Bar plot information
media_contribution <- c(adwords_contribution,
facebook_contribution,
tv_contribution,
radio_contribution)
media_contribution = round(media_contribution, digits=0)
media_names <- c("AdWords","Facebook", "TV","Radio")
df <- data.frame(media_names, media_contribution)
head(df)| media_names | media_contribution |
|---|---|
| AdWords | 57994699 |
| 72261042 | |
| TV | 1704751 |
| Radio | 2597922 |
Cross-checking data
Bar Plot with total
We will use this information to create a bar plot of the web traffic contribution of each media:
Code
df <- df |> as_tibble()
df |>
mutate(media_names = as_factor(media_names)) |>
mutate(media_names = fct_reorder(media_names, -media_contribution)) |>
ggplot(aes(media_names, media_contribution, fill = media_names)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label=media_contribution), vjust=-0.3, size=3.5) +
scale_y_continuous(labels = comma_format()) +
labs(title="Contribution to Traffic",
x="Media",
y="Contribution") +
theme_minimal() 
Bar Plot with Percentage
Sometimes, it is difficult to communicate large numbers displayed above the bars. To avoid this, we can compute the contribution of each traffic driver in percentage terms.
Code
#install.packages("viridis")
library(viridis)
df |>
mutate(percent_media_cont = media_contribution/sum(media_contribution)) |>
mutate(media_names = fct_reorder(media_names, percent_media_cont, .desc = TRUE)) |>
ggplot(aes(media_names, percent_media_cont)) +
geom_col(aes(fill=media_names), show.legend = FALSE) +
geom_text(aes(label=paste0(round(percent_media_cont*100, digits = 2),"%")),
vjust=-0.3) +
labs(title="Contribution to Traffic",
x="Media",
y="Contribution") +
theme_minimal()+
scale_y_continuous(labels = percent_format()) +
scale_fill_viridis(discrete = TRUE)
The bar plot above suggests that 53.7% of the web traffic is driven by Facebook, 43.1% is driven by Google AdWords, 1.9% by radio, and 1.3% by TV.
6 Return on Marketing Investment (ROMI)
Financially oriented marketing executives are very often concerned about the return on marketing investment (ROMI). That is, they would like to know how much they earn with respect to how much they spend. Usually, the return metric is sales, revenues, or profits. However, it can also be something non-financial, e.g.customer engagement, web traffic and store traffic.
Let’s calculate the traffic return on marketing investment (TROMI) for FourTex.
The first input we need is the cost data:
Code
| sum_adwords |
|---|
| 8999557 |
| total_spend |
|---|
| 67969701 |
Code
# adding media cost vector to data
cost <- c(cost_adwords,cost_facebook,cost_tv,cost_radio)
cost = round(cost, digits=0)
df <- data.frame(df, cost)
df| media_names | media_contribution | cost |
|---|---|---|
| AdWords | 57994699 | 8999557 |
| 72261042 | 6437100 | |
| TV | 1704751 | 41593933 |
| Radio | 2597922 | 10939111 |
6.1 Barplot with cost vs.traffic contribution (Dodged)
Then, we can see the traffic return and cost data together in a bar plot:
Code
df <- df |>
as_tibble() |>
rename(traffic = media_contribution)
df |>
pivot_longer(-media_names, names_to = "traf_cost", values_to = "value") |>
mutate(media_names = fct_reorder(media_names, -value)) |>
ggplot(aes(media_names, value, fill = traf_cost)) +
geom_col(position = position_dodge())+
scale_y_continuous(labels = comma_format()) +
labs(title = "Traffic vs. Media Cost",
x = "Media",
y = "Traffic and Cost")
6.2 Barplot with TROMI
Instead of showing cost and return data together, we can just compute the TROMI, and show the results in percentages. To do so, we need to divide the traffic contribution of each media by the cost of each media:
Code
# data manipulation
df |>
mutate(media_names = fct_reorder(media_names, -roi)) |>
ggplot(aes(media_names, roi, fill = media_names)) +
geom_col(show.legend = FALSE)+
geom_text(aes(label=roi), vjust=-0.3, size=3.5) +
scale_fill_viridis(discrete = TRUE) +
labs(title="Traffic Return on Marketing Investment",
x="Media",
y="TROMI") +
theme_bw()
7 Marketing budget allocation
Marketing analysts follow two main approaches to guide their resource allocation strategies. One of them is normative decision making based on constrained optimization models (e.g.profit maximization subject to budget constraints).
Another method is elasticity-based allocation. In this R application, we will allocate the marketing budget of FourTex by making use of the elasticities obtained from the log-log regression model.
7.1 Actual budget spending
Before diving into the optimal resource allocation, let’s see what the current budget allocation looks like:
Pie Chart
Code
df <- df |>
mutate(costshare = cost/sum(cost))
#library(ggrepel)
df |>
ggplot(aes(x="", y = costshare, fill = media_names))+
geom_bar(stat = "identity", width = 1)+
coord_polar("y", start = 0) +
geom_text(aes(label = paste0(round(costshare*100, digits = 1),"%")),
position = position_stack(vjust = 0.2),
hjust = 0.2)+
geom_label(aes(label = media_names),
position = position_stack(vjust = 0.8),
hjust = 0.7,
show.legend = FALSE) +
labs(title = "Actual Ad Spending",
x = "",
y = "") +
theme_void() +
theme(legend.position = "none")
7.2 Elasticity-Based Allocation
Pie chart with Base R
Code
#The sum of all elasticities
beta_allmedia <- beta_adwords + beta_facebook + beta_tv + beta_radio
#Optimal resource allocation
optim_adwords <- beta_adwords/beta_allmedia
optim_facebook <- beta_facebook/beta_allmedia
optim_tv <- beta_tv/beta_allmedia
optim_radio <- beta_radio/beta_allmediaYou can see the computed allocation at the top right of the screen under the Environment tab. Now, we can get a pie-chart that shows the allocation visually with percentages.
Code
[1] 0.43 0.54 0.01 0.02
Code
slices_optim <- c(optim_adwords,optim_facebook,optim_tv,optim_radio)
lbls_optim <- c("Adwords", "Facebook", "TV","Radio")
pct_optim <- round(slices_optim*100)
lbls_optim <- paste(lbls_optim, pct_optim) # paste variable names to data labels
lbls_optim <- paste(lbls_optim, "%", sep="") # add % sign to labels
# Get the pie-chart
pie(slices_optim, labels=lbls_optim, col=rainbow(length(lbls_optim)), main="Optimal Budget Allocation" )
Pie chart with tidyverse
Code
# Create a vector
beta <- c(beta_adwords, beta_facebook, beta_tv, beta_radio)
# additing it to df
df <- data.frame(df, beta)
# creating percentage
df <- df |>
mutate(opt_alloca = beta/sum(beta))
df| media_names | traffic | cost | roi | costshare | beta | opt_alloca |
|---|---|---|---|---|---|---|
| AdWords | 57994699 | 8999557 | 6.4 | 0.1324054 | 0.1554325 | 0.4310002 |
| 72261042 | 6437100 | 11.2 | 0.0947054 | 0.1936680 | 0.5370236 | |
| TV | 1704751 | 41593933 | 0.0 | 0.6119482 | 0.0045689 | 0.0126692 |
| Radio | 2597922 | 10939111 | 0.2 | 0.1609410 | 0.0069627 | 0.0193070 |
Code
# chart
df |>
ggplot(aes(x="", y = opt_alloca, fill = media_names))+
geom_bar(stat = "identity", width = 1)+
coord_polar("y", start = 0) +
geom_text(aes(label = paste0(round(opt_alloca*100, digits = 1),"%")),
position = position_stack(vjust = 0.7),
hjust = 0.9) +
geom_label(aes(label = media_names),
position = position_stack(vjust = 0.2),
hjust = 0.3,
show.legend = FALSE) +
labs(title = "Optimal Budget Allocation",
x = "",
y = "") +
theme_void() +
theme(legend.position = "right")
8 Results
The FourTex case study shows a clear imbalance between where marketing budget is spent and which channels actually drive website traffic. Based on the model results, digital channels are the strongest contributors to web traffic, while offline media delivers little measurable impact in this traffic-focused framework.
More specifically, Facebook is the most effective channel, with an estimated elasticity of 0.194, meaning that a 10% increase in Facebook spend is associated with a 1.94% increase in web traffic.
Google also performs strongly, with an elasticity of 0.155, or roughly a 1.55% traffic increase for every 10% increase in spend.
In contrast, radio has only a 0.007 elasticity, and TV just 0.005, indicating very limited responsiveness. These weak offline effects are also less reliable statistically: Facebook is highly significant (p = 0.00113), radio is only marginally significant (p = 0.09147), and TV is not statistically significant (p = 0.29107).
The model also confirms that traffic is not driven by advertising alone. The inclusion of the lagged traffic term is important, as its coefficient of 0.536 suggests that around 54% of traffic momentum carries over into the following period. This indicates that website visits are partly persistent over time, and that current performance reflects both present marketing activity and past traffic dynamics.
In terms of model quality, the results are reasonably strong. The regression explains about 75.8% of the variation in website traffic (R² = 0.7578), with an adjusted R² of 0.7336, which suggests the model captures substantial variation without severe overfitting. In addition, the residual standard error of 0.1252 indicates that prediction error remains relatively modest for a log-log specification. Taken together, these figures suggest the model provides a credible basis for managerial interpretation.
From a return-on-investment perspective, the contrast across channels is even more striking. Facebook generates 11.2 visits per £1 spent, making it the most efficient channel in the mix. Google returns 6.4 visits per £1 spent, also making it a strong performer. By comparison, radio returns only 0.2 visits per £1 spent, while TV generates effectively 0 visits per £1 spent in this model. Despite these results, FourTex currently allocates 77% of its budget to offline media and only 23% to online channels, even though digital activity accounts for approximately 97% of attributed website traffic.
These findings suggest that the current allocation is poorly aligned with the objective of maximizing web traffic. If traffic generation is the primary goal, FourTex should substantially reduce investment in TV and radio and reallocate budget toward Facebook and Google. The optimization results support this conclusion, indicating that the budget should be directed almost entirely toward the two digital channels, which together account for the overwhelming majority of measurable traffic response.
At the same time, the weak returns from TV and radio should be interpreted carefully. A traffic model captures direct and relatively immediate website response, but offline media often works through brand awareness, delayed effects, and assisted pathways that are harder to observe in session-level traffic data. This means that TV and radio may still have strategic value, but not necessarily for the specific outcome measured here. If FourTex wants to justify continued offline investment, those channels should be evaluated against broader KPIs such as sales, brand lift, or long-term customer acquisition, rather than web traffic alone.
Code
library(gt)
library(scales)
df %>%
mutate(
media_names = factor(media_names, levels = media_names)
) %>%
gt() %>%
tab_header(
title = md("**Marketing Mix Model Summary**")
) %>%
cols_label(
media_names = "Channel",
traffic = "Attributed Traffic",
cost = "Cost",
roi = "ROI",
costshare = "Cost Share",
beta = "Beta",
opt_alloca = "Optimal Allocation"
) %>%
fmt_number(columns = c(traffic, cost), decimals = 0, sep_mark = ",") %>%
fmt_number(columns = c(roi, beta), decimals = 2) %>%
fmt_percent(columns = c(costshare, opt_alloca), decimals = 1) %>%
cols_align(align = "center", columns = everything()) %>%
tab_options(
table.font.size = px(13),
data_row.padding = px(6)
)| Marketing Mix Model Summary | ||||||
| Channel | Attributed Traffic | Cost | ROI | Cost Share | Beta | Optimal Allocation |
|---|---|---|---|---|---|---|
| AdWords | 57,994,699 | 8,999,557 | 6.40 | 13.2% | 0.16 | 43.1% |
| 72,261,042 | 6,437,100 | 11.20 | 9.5% | 0.19 | 53.7% | |
| TV | 1,704,751 | 41,593,933 | 0.00 | 61.2% | 0.00 | 1.3% |
| Radio | 2,597,922 | 10,939,111 | 0.20 | 16.1% | 0.01 | 1.9% |
Metric meanings
Attributed Traffic: estimated traffic generated by each media channel in the model.
Cost: total spend for that channel.
ROI: return on investment, usually interpreted as traffic or return generated per unit of spend.
Cost Share: proportion of the total budget currently spent on that channel.
Beta: regression coefficient from the MMM; it shows the direction and strength of the channel’s relationship with traffic, holding other variables constant.
Optimal Allocation: recommended share of total budget to assign to that channel based on the optimization output.
9 Conclusion
Overall, the central conclusion is that Facebook and Google are the true performance drivers for website traffic, while TV and radio show minimal direct contribution in this model. For a traffic objective, FourTex should rebalance its spending toward digital media, where both elasticities and returns on marketing investment are substantially higher. However, if the company’s goal shifts from traffic to another metric such as sales or brand growth, the optimal allocation could look very different. In that sense, the most important managerial lesson is that marketing mix decisions should always be aligned with the specific business outcome being optimized.
