Chapter 01 of 10

The generative model

The mean surface, the likelihood, and every prior on it — written out in full, because a model you cannot transcribe is one you cannot disagree with.

The KPI in geo g at period t is modelled as an additive surface on a standardized per-capita scale. Four terms: a geo intercept, a time-varying baseline shared across geos, the media response, and controls.

mu[g,t] = tau[g] + baseline[t] + media[g,t] + controls[g,t]

media[g,t]    = sum_m beta[g,m] * delta[g,t,m]
baseline[t]   = sum_k knots[k] * spline_basis[k,t]
controls[g,t] = sum_c control_coef[g,c] * X[g,t,c]

y[g,t] ~ Normal(mu[g,t], sigma)          # or StudentT(nu, mu, sigma)
mu is in scaled-KPI units; every reported figure is passed back through the KPI scaler first.

deltais transformed media net of its value at zero spend, defined in the next chapter. The subtraction is what makes “a channel that did not run contributed nothing” exactly true rather than true to twelve decimal places, which is what lets the contribution decomposition add up.

Every prior, written out

There are no hidden hyperparameters. The table is the complete set of sampled sites for a national model; the geo hierarchy adds four more, covered in chapter 06.

SitePriorWhy this one
roi[m]LogNormal(0.2, 0.9)The default is scale-free: a median return of 1.22× with 90% of the mass between 0.28× and 5.4×. This is the site the analyst is expected to overrule — see chapter 03.
adstock_alpha[m]Beta(2, 2)Centred at 0.5 with zero density at the boundaries, which keeps NUTS away from alpha = 1 (infinite memory) and alpha = 0 (no carryover at all).
hill_ec[m]TruncatedNormal(0.8, 0.8) on [0.1, 10]Half-saturation in scaled media units. Truncated because ec ≤ 0 is meaningless and ec > 10 makes the channel indistinguishable from linear.
hill_slope[m]pinned at 1, or TruncatedNormal(1, 0.3) on [0.3, 5]Pinned unless explicitly freed. A free slope everywhere is the fastest route to an unidentified model at MMM sample sizes: slope and ec trade off along a ridge the data barely constrains.
baseline_knots[k]Normal(0, 5)Weak, because the identification dial here is the number of knots, not their prior width. Chapter 04.
control_coef[c]Normal(0, 5)Sign-free: a control is not asserted to help or hurt, and pricing usually hurts.
sigmaHalfNormal(5)Residual scale, on a KPI standardized to roughly unit variance.
nuGamma(2, 0.1)Only when the Student-t likelihood is selected. Mean around 20 — heavy enough to absorb a promotional spike without letting the model dismiss real weeks as outliers.

source · engine/mmm/model.py · engine/mmm/priors.py

Holdout by masking

When periods are held out, the likelihood is masked rather than the rows dropped. Held-out cells still flow through adstock, because carryover from the held-out window into the training window is real and deleting the rows would silently change the training data.

likelihood = likelihood.mask(~holdout_mask)

What this functional form assumes

Additivity across channels: no interaction term, so the model cannot express “TV makes search work harder”. Media affects the KPI within the geo and period it is adstocked into, so there is no spillover between geos. And the baseline is shared across geos up to an intercept — a geo whose seasonality genuinely differs in shape, not just level, is misspecified here.