Marketing Mix Modeling Implementation in Python
From concept to a working model: what actually needs to be built
Conceptually, Marketing Mix Modeling regresses an outcome (revenue, conversions) against marketing inputs (spend by channel) plus control variables (seasonality, pricing, macroeconomic factors). Implementing one in practice requires three non-trivial pieces beyond a plain linear regression: adstock transformation (modeling that an ad's effect carries over into future periods, decaying over time, rather than only affecting the week it ran), saturation transformation (modeling diminishing returns, since doubling spend on a channel rarely doubles its effect, especially at already-high spend levels), and regularization (because marketing channels are often correlated with each other in spend timing — multiple channels ramping up together for a product launch — which causes severe multicollinearity in a plain regression).
Adstock: why last week's ad spend still matters this week
Adstock models the idea that advertising effect doesn't vanish the instant the ad stops running — brand awareness and purchase intent built by an ad decay gradually rather than dropping to zero immediately. The standard implementation is a geometric decay transformation: this week's effective "adstocked" spend is this week's actual spend plus a decay-weighted fraction of last week's adstocked value, recursively. A decay rate near 0 means almost no carryover (very short-lived effect, typical of a flash sale); a decay rate near 1 means spend from months ago still meaningfully influences the current period (typical of brand-building channels like TV or high-funnel video).
import numpy as np
import pandas as pd
def adstock_transform(spend: pd.Series, decay_rate: float) -> pd.Series:
"""Geometric adstock: carries over a decaying fraction of prior spend effect."""
adstocked = np.zeros(len(spend))
adstocked[0] = spend.iloc[0]
for t in range(1, len(spend)):
adstocked[t] = spend.iloc[t] + decay_rate * adstocked[t - 1]
return pd.Series(adstocked, index=spend.index)
def saturation_transform(adstocked_spend: pd.Series, half_saturation: float, shape: float = 1.0) -> pd.Series:
"""Hill-function saturation: models diminishing returns at higher spend levels."""
x = adstocked_spend.values
return pd.Series(
(x ** shape) / (x ** shape + half_saturation ** shape),
index=adstocked_spend.index,
)
# Example channel transformation pipeline
df["tv_adstocked"] = adstock_transform(df["tv_spend"], decay_rate=0.6)
df["tv_saturated"] = saturation_transform(df["tv_adstocked"], half_saturation=df["tv_adstocked"].median())Fitting the model with regularization
Once each channel's spend has been adstock- and saturation-transformed into a bounded, carryover-aware feature, the regression step itself uses a regularized linear model — Ridge regression is the common default — rather than ordinary least squares, specifically because correlated channel spend (campaigns frequently launching together) makes OLS coefficients unstable and prone to nonsensical signs (a channel showing a negative contribution purely due to collinearity, not because it actually hurt revenue). Bayesian approaches (implemented via PyMC, and productized in Google's open-source Robyn and LightweightMMM / Meridian libraries) go further, incorporating prior beliefs about plausible channel effectiveness ranges, which helps stabilize estimates further and produces full uncertainty intervals rather than single point estimates.
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
feature_cols = ["tv_saturated", "search_saturated", "social_saturated", "price_index", "seasonality_index"]
X = StandardScaler().fit_transform(df[feature_cols])
y = df["weekly_revenue"].values
model = Ridge(alpha=1.0) # alpha tuned via cross-validation on held-out time periods
model.fit(X, y)
contributions = dict(zip(feature_cols, model.coef_))
print(contributions) # relative channel contribution, in standardized unitsCross-validate on time, not random rows
A fitted MMM's real output is the response curve, not the coefficient
What's Next?
A fitted model's recommendations are only as trustworthy as its validation against ground truth — pairing MMM output with periodic geo-holdout or incrementality experiments remains the strongest practical safeguard against a model confidently recommending a budget shift based on a spurious correlation rather than a real causal effect.
I build these systems professionally.
Whether it's a RAG pipeline, analytics migration, or AI workflow — let's talk.