Implementation of the Advertising Adstock theory in R

In this article we show how to implement the Simple Decay Model, Delayed Decay Model and Log Decay Model in R

Kylie Fu
7 min readNov 27, 2021

Advertising Adstock theory is well known in the marketing field. I read this theory and implemented its models on the real-world data, and pleasingly found they were working. It is too good to hide this implementation. This article shows the implementation of three Adstock models - Geometric Simple Decay Model, Geometric Log Decay Model and Delayed Simple Decay Model in R.

1. Advertising and Sales.

Given that we have a time series data in this form:

Image by the author

Transforming the advertising spend into the advertising adstock in some way, it is assumed that there is a linear relationship between the advertising adstock and the sales:

where S_t is the sales at time t and A_t is the advertising adstock at time t.

2. Adstock Models.

This transformation can have various forms. In this article, we discuss three forms.

The most popular Adstock model is the Simple Decay Model. Here I call it the Geometric Simple Decay Model because Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects refers it as Geometric Decay Model to differentiate it from the Delayed Decay Model.

Another popular model is the Geometric Log Decay Model (corresponds to the Log Decay Model in Understanding Advertising Adstock Transformations) which has a more complex form and is harder to interpret compared with the Geometric Simple Decay Model.

Google’s paper Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects proposed a Delayed Decay Model which sounds interesting to me. It uses a Radial Basis Function (RBF) to illustrate the advertising Adstock.

Below compares the Geometric Simple Decay and the Delayed Simple Decay curves:

Chart from Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects

I choose R for implementation because R has the most comprehensive functions for statistical models such as non-linear model which we will use in this article.

(1) Geometric Simple Decay Model

where A_t is the advertising adstock at time t, T_t is the advertising spend at time t, and lambda is the adstock decay rate.

Image by the author

We define a function that takes the advertising spend sequence as the input and returns the advertising adstock sequence.

GeometricSimpleAdstock <- function(advertising, lambda){

adstock <- as.numeric(stats::filter(advertising, lambda, method="recursive"))

return(adstock)
}

The code is so neat! We look into what the filter function in the “stats” package does.

x <- 1:5y <- stats::filter(x, rep(0.9), method="recursive")> x[1] 1 2 3 4 5> y[1]  1.0000  2.9000  5.6100  9.0490 13.1441

We input a time series sequence x [1, 2, 3, 4, 5] and set lambda as 0.9, it will output a corresponding time series sequence y [1.0000, 2.9000, 5.6100, 9.0490, 13.1441] where each value in y is a weighted sum of the previous elements in x. In this example, the fourth element in y 9.0490 equals to 1 * .09³ + 2 * 0.9² + 3 * 0.9¹ +4.

(2) Geometric Log Decay Model

It is similar to the Geometric Simple Decay Model:

GeometricLogAdstock <- function(advertising, lambda){

adstock <- as.numeric(stats::filter(log(advertising), lambda, method="recursive"))

return(adstock)
}

(3) Delayed Simple Decay Model

Image from Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects (notations please refer to the paper)

If you have difficulty reading the mathematical formula, the following chart will help you:

Image by the author

Now we need to define a function to transform the advertising spend into the advertising adstock. Matrix multiplication is an efficient way of doing this.

Image by the author

Implementing this in code:

DelayedSimpleAdstock <- function(advertising, N, lambda, theta, L){  '''
Return the advertising adstock using Delayed Decay Model.
---
Inputs:
advertising: A sequence.
N: Length of the advertising sequence.
lambda: Adstock decay rate.
theta: The delay of the peak effect.
L: The maximum duration of carryover effect.
---
Returns:
adstock: Advertising adstock
'''
weights <- matrix(0, N, N)
for (i in 1:N){
for (j in 1:N){
k = i - j
if (k < L && k >= 0){
weights[i, j] = lambda ** ((k - theta) ** 2)
}
}
}

adstock <- as.numeric(weights %*% matrix(advertising))

return(adstock)
}

In this function, we generate a weight matrix W, and apply matrix multiplication to derive the output sequence. To illustrate, we run a little toy example:

N <- 8
lambda <- 0.9
theta <- 1
L <- 4
weights <- matrix(0, N, N) # backbone of the function
for (i in 1:N){
for (j in 1:N){
k = i - j
if (k < L && k >= 0){
weights[i, j] = lambda ** ((k - theta) ** 2)
}
}
}

The generated “weights” matrix looks like:

> weights[,1]   [,2]   [,3]   [,4]   [,5] [,6] [,7] [,8]
[1,] 0.9000 0.0000 0.0000 0.0000 0.0000 0.0 0.0 0.0
[2,] 1.0000 0.9000 0.0000 0.0000 0.0000 0.0 0.0 0.0
[3,] 0.9000 1.0000 0.9000 0.0000 0.0000 0.0 0.0 0.0
[4,] 0.6561 0.9000 1.0000 0.9000 0.0000 0.0 0.0 0.0
[5,] 0.0000 0.6561 0.9000 1.0000 0.9000 0.0 0.0 0.0
[6,] 0.0000 0.0000 0.6561 0.9000 1.0000 0.9 0.0 0.0
[7,] 0.0000 0.0000 0.0000 0.6561 0.9000 1.0 0.9 0.0
[8,] 0.0000 0.0000 0.0000 0.0000 0.6561 0.9 1.0 0.9

Next, we return the advertising spend multiplied by the “weights” matrix, and the output sequence has the same shape as the input.

Choosing any one of these three methods, we convert the advertising spend to the advertising adstock.

3. Find the Optimal Adstock Rate.

We simultaneously fit Eq.(1) and one of the three models to find the optimal Adstock rate. The nls function in R finds the optimal Adstock rate lambda by minimizing the model’s sum of squared error. We want the lambda between 0 and 1, hence this is a constrained optimization problem. Sometimes the nls function returns a lambda value that is out of this range, we need the nlsLM function (a modified version of nls) to apply the constraint.

adstock_model <- function(df, t, L = 13, theta = 1){  '''
Find the optimal Adstock model.

---
Inputs:
df: Data Frame.
t: Model type. Options: "GeoSimple", "Delayed", "GeoLog".
L: The maximum duration of carryover effect.
theta: The delay of the peak effect.

---
Returns:
A model.
'''

if (t == 'GeoSimple'){
model <- nls(data = df, sales ~ alpha + beta * GeometricSimpleAdstock(adstock, lambda), start = c(alpha = 1, beta = 1, lambda = 0))

if (summary(model)$coef[3, 1] < 0 || summary(model)$coef[3, 1] >= 1){

model <- nlsLM(data = df, sales ~ alpha + beta * GeometricSimpleAdstock(adstock, lambda), start = list(alpha = 1, beta = 1, lambda = 0), lower = c(alpha = -Inf, beta = -Inf, lambda = 0), upper = c(alpha = Inf, beta = Inf, lambda = 1))
}

} else if (t == 'Delayed'){

N <- dim(df)[1]
model <- nlsLM(data = df, sales ~ alpha + beta * DelayedSimpleAdstock(adstock, N, lambda, theta, L), start = list(alpha = 1, beta = 1, lambda = 0), lower = c(alpha = -Inf, beta = -Inf, lambda = 0), upper = c(alpha = Inf, beta = Inf, lambda = 1))

} else if (t == 'GeoLog'){

model <- nls(data = spots, sales ~ alpha + beta * GeometricLogAdstock(adstock, lambda), start = c(alpha = 1, beta = 1, lambda = 0))

if (summary(model)$coef[3, 1] < 0 || summary(model)$coef[3, 1] >= 1){

model <- nlsLM(data = df, sales ~ alpha + beta * GeometricLogAdstock(adstock, lambda), start = list(alpha = 1, beta = 1, lambda = 0), lower = c(alpha = -Inf, beta = -Inf, lambda = 0), upper = c(alpha = Inf, beta = Inf, lambda = 1))
}
}

return(model)
}
mod = adstock_model(df, 'GeoSimple')
summary(mod)

4. Further Tuning.

For the Delayed Simple Decay Model, we may further tune the following parameters to get a better fit:

L (The maximum duration of carryover effect).

Theta (The delay of the peak effect).

The power of the radial basis function. Just note that when the power is not 2, don’t forget to take the absolute value of the difference between l and theta.

Enjoy your data science journey!

Reference:

  1. Understanding Advertising Adstock Transformations
  2. Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects

--

--