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)
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.
| Site | Prior | Why 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. |
| sigma | HalfNormal(5) | Residual scale, on a KPI standardized to roughly unit variance. |
| nu | Gamma(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