If I show you a TV commercial today, you don't instantly buy the product and then forget about it tomorrow. The ad lingers in your memory. You might buy the product three days later, or next week. This is called the Carry-Over Effect.
Raw media spend data does not account for this. If you spend $100 on Monday and $0 on Tuesday, your raw data says Tuesday has 0 marketing pressure. But reality says Tuesday still has "leftover" pressure from Monday.
In MMM, we model this using a transformation called Adstock.
The simplest way to model memory is to assume it fades at a constant rate every day (or week). This is the "Geometric" decay.
It uses a single parameter: Alpha (α), also known as the decay rate.
- If α = 0.9, you retain 90% of the ad's effect next week. (Long memory, like TV).
- If α = 0.1, you retain only 10% of the effect. (Short memory, like Direct Response FB ads).
def geometric_adstock(x, alpha): """ x: Array of media spend alpha: Decay rate (0 to 1) """ x_decayed = [x[0]] for i in range(1, len(x)): x_decayed.append(x[i] + alpha * x_decayed[i-1]) return x_decayed # Example Usage # TV usually has high retention (0.8), Social has low (0.3) df['tv_adstock'] = geometric_adstock(df['tv_spend'], 0.8)
Geometric decay is limited because it assumes the peak impact always happens on Day 1. But what if you see an ad today, but it takes 3 days for it to "sink in"?
Imagine a complex B2B product or a movie trailer. The buzz might build up before it decays. Geometric adstock cannot model this "delayed peak." Weibull Adstock can.
Starts high, drops immediately. Good for CPG/Retail sales.
Can start low, peak later (Day 3), then drop. Good for complex purchases.
Weibull uses two parameters: Shape (k) and Scale (lambda). It essentially creates a distribution of weights over time.
import numpy as np from scipy import stats def weibull_adstock(x, shape, scale): """ Weibull PDF transformation for flexible decay """ # Create a window of lag weights (e.g., 52 weeks) lag_weights = stats.weibull_min.pdf(np.arange(52), shape, scale=scale) # Normalize weights so they sum to 1 lag_weights = lag_weights / np.sum(lag_weights) # Convolve raw spend with weights return np.convolve(x, lag_weights)[:len(x)]
In a modern MMM pipeline, we don't guess. We treat Alpha (for Geometric) or Shape/Scale (for Weibull) as hyperparameters.
When we get to Module 9 (Optimization), we will let the model test thousands of different Alpha values (0.1, 0.2, ... 0.9) to see which one creates the best fit with Sales. We let the data tell us how long the memory of a TV ad lasts.