Math 00: The MMM as a Generative Model

The framework's BayesianMMM is, at heart, a generative story for the weekly KPI: a recipe that, given parameters \(\theta\), generates a plausible sales series. Fitting the model is running that recipe backwards. This page — the index of the math series — writes the recipe down: the full additive mean function \(\mu_t\), the Normal likelihood, every default prior, and the spend-to-contribution pipeline. Then it runs the recipe forward, live in your browser: one draw from the prior, decomposed into its additive pieces, with a button to draw again. The six pages that follow (0106) each make one term of this equation precise.

7
additive terms in the mean function \(\mu_t\)
11
priors in the default model, all tabulated below
104
weeks of synthetic KPI in the live prior draw
0
model fits on this page — pure forward math

Definition: Forward (generative) model

A complete probabilistic recipe for the data: sample parameters \(\theta\) from the priors, compute the deterministic mean \(\mu_t(\theta)\), then draw the observation from the likelihood \(\tilde y_t \sim \mathrm{Normal}(\mu_t, \sigma)\). An MMM answers a causal-flavored question — what did each channel cause, and what would another dollar buy? — by inverting this recipe: NUTS explores the posterior \(p(\theta \mid y) \propto p(y \mid \theta)\, p(\theta)\) (math 04).

🧭 Before you start

Prerequisites: comfort with calculus & probability; the Workshop series recommended first.  ·  Time: ~30–45 min.

You will learn:

  • How the full additive mean function \(\mu_t\) plus a Normal likelihood forms a complete generative recipe — and why fitting the model is running that recipe backwards.
  • What every default prior in the model is, and the two structural commitments they bake in before any data arrive: monotone increasing, concave response curves.
  • How one draw from the prior generates a plausible year of sales whose decomposition is exactly additive — the picture the fitted model must recover.

Run it: nbs/math/math_00_overview.ipynb on GitHub.

The generative equation

The framework first standardizes the KPI so all priors live on a common, unit-free scale:

\[ \tilde y_t \;=\; \frac{y_t - \bar y}{s_y}, \qquad \bar y = \mathrm{mean}(y), \quad s_y = \mathrm{std}(y), \]

and maps back at the end with \(y_t = s_y\,\tilde y_t + \bar y\). Everything downstream is modeled on \(\tilde y_t\). The heart of the model is a single additive linear predictor \(\mu_t\) — the model's noiseless expectation for the standardized KPI in period \(t\):

\[ \mu_t \;=\; \underbrace{\text{intercept}}_{\text{baseline level}} \;+\; \text{trend}_t \;+\; \text{seasonality}_t \;+\; \text{geo}_{[g]} \;+\; \text{product}_{[p]} \;+\; \underbrace{\text{media}_t}_{\text{the question}} \;+\; \text{controls}_t \]

The media term is where the marketing nonlinearity lives. It sums, over channels \(c\), a coefficient \(\beta_c\) times a saturated, adstocked version of that channel's spend:

\[ \text{media}_t \;=\; \sum_{c} \beta_c \cdot \mathrm{sat}_c\!\big(\, \mathrm{adstock}_c(x_{c,t}) \,\big). \]

Adstock spreads each week's spend forward in time — carryover (math 01). Saturation then imposes diminishing returns. The framework's core saturation is the exponential curve (this is model/base.py's _build_modelnot a Hill curve; the Hill mention in the docstrings is a reference alternative only, a distinction math 02 dissects and stress 01 shows can cost +38% on total media):

\[ \mathrm{sat}_c(u) \;=\; 1 - \exp(-\lambda_c\, u), \qquad \lambda_c > 0, \]

clipped via \(\exp(\mathrm{clip}(-\lambda_c u, -20, 0))\) so it stays numerically in \([0, 1)\). The controls term is plain linear: \(\text{controls}_t = \sum_j \beta^{\text{ctrl}}_j z_{j,t}\). Finally the likelihood — the generative step that adds irreducible noise:

\[ \tilde y_t \;\sim\; \mathrm{Normal}\big(\mu_t,\ \sigma\big). \]

That is the entire forward model. To generate data: sample \(\theta\) from the priors, compute \(\mu_t\), draw \(\tilde y_t\). We do exactly that below.

Every symbol in \(\mu_t\)

SymbolNameRoleDeep dive
\(\tilde y_t\)standardized KPIthe modeled response, \((y_t - \bar y)/s_y\)00 / 04
\(\mu_t\)linear predictornoiseless expectation of \(\tilde y_t\)00
\(\text{intercept}\)baseline levelthe KPI with zero media (standardized)04
\(\text{trend}_t\)long-run driftslow non-media movement (linear / piecewise / spline)03
\(\text{seasonality}_t\)periodic structureFourier features \(\times\) coefficients03
\(\text{geo}_{[g]}\)geo random effectpartial-pooled offset for region \(g\)04
\(\text{product}_{[p]}\)product random effectpartial-pooled offset for product \(p\)04
\(x_{c,t}\)raw spenddollars on channel \(c\) in week \(t\)01
\(\mathrm{adstock}_c(\cdot)\)carryoverspreads spend forward in time01
\(\lambda_c\)saturation ratecurvature of diminishing returns02
\(\mathrm{sat}_c(\cdot)\)saturation\(1 - e^{-\lambda_c u}\), bounded in \([0,1)\)02
\(\beta_c\)channel coefficienteffect size / contribution magnitude04 / 05
\(z_{j,t}\)control variableprice, distribution, weather, …04
\(\beta^{\text{ctrl}}_j\)control coefficientlinear effect of control \(j\)04
\(\sigma\)noise scaleirreducible week-to-week variation04

The priors — the model's beliefs before seeing data

A Bayesian model is its priors plus its likelihood. These are the exact priors the framework places (verbatim from _build_model in src/mmm_framework/model/base.py), each with a one-line intuition. They are deliberately weakly informative: regularizing enough to keep the sampler well-behaved, loose enough to let the data speak. (If priors are new to you, workshop 01 is the gentler on-ramp; this table is the reference card.)

ParameterPriorIntuition
\(\text{intercept}\)\(\mathrm{Normal}(0,\,0.5)\)baseline near the KPI mean (data are standardized)
\(\beta_c\) (channel)\(\mathrm{Gamma}(\mu{=}1.5,\,\sigma{=}1.0)\)positive effect with mass above 1 on the standardized max-contribution scale — \(\beta_c\) is the channel's standardized contribution ceiling, not its ROI (dollar ROI also depends on spend, saturation, and KPI scale); or an experiment-calibrated prior (math 05)
\(\lambda_c\) (saturation rate)\(\mathrm{Exponential}(0.5)\)mostly gentle saturation; mean \(1/0.5 = 2\)
\(\alpha_c\) (adstock decay — the default)\(\mathrm{Beta}(1,\,3)\) (geometric)continuous in-graph decay (use_parametric_adstock=True, the default since June 2026) — prior mean 0.25; delayed adds \(\theta_c \sim \mathrm{HalfNormal}(2)\), Weibull uses Gamma shape/scale. Made the default after the stress series measured the legacy blend at ~28% error vs ~7% parametric on carryover-sensitive worlds
\(m_c\) (legacy adstock blend, opt-out)\(\mathrm{Beta}(2,\,2)\)mix weight between two fixed-\(\alpha\) geometric adstocks, centered at 0.5 — not a decay rate (math 01); the pre-change default, via use_parametric_adstock=False
seasonality coefficients\(\mathrm{Normal}(0,\,0.3)\)small, symmetric Fourier amplitudes
\(\beta^{\text{ctrl}}_j\)\(\mathrm{Normal}(0,\,\sigma_{\text{ctrl}})\)linear control; wider prior for a flagged confounder
geo scale\(\mathrm{HalfNormal}(0.3)\)spread of geo offsets (hierarchical scale)
geo offset\(\mathrm{Normal}(0,\,1)\)non-centered: effect = scale \(\times\) offset
product scale / offset\(\mathrm{HalfNormal}(0.3)\), \(\mathrm{Normal}(0,1)\)same non-centered pattern as geo
trend slope (linear)\(\mathrm{Normal}(0,\,0.5)\)modest long-run drift in either direction
\(\sigma\)\(\mathrm{HalfNormal}(0.5)\)residual noise scale (standardized units)

💡 Two subtleties to leave with

\(\beta_c\) is Gamma, hence strictly positive — the framework assumes media cannot hurt the KPI. This is a strong, deliberate modeling choice (and the lever experiment calibration tightens; math 05, stress 05). The default adstock now estimates a real decay rate (\(\alpha_c\) in-graph, since June 2026). On the legacy path, \(m_c\) is a Beta blend weight, not a decay rate — the model precomputes geometric adstock at two fixed decay rates and learns how much to lean on each. Math 01 dissects both in full.

See the priors rather than just tabulating them. Each density below is built from 30,000 seeded draws with the analytic density overlaid; the dashed line marks the sample mean.

Computed live in your browser — the same math as the notebook (30,000 seeded draws per prior; analytic density overlaid in dark green). Note how much mass the \(\beta\) prior puts above 1, how \(\mathrm{Exp}(0.5)\) favors gentle saturation rates, and how \(\mathrm{Beta}(2,2)\) is a symmetric hill centered at \(0.5\).

Deriving the \(\mathrm{Gamma}(\mu, \sigma) \to\) shape/scale conversion

The framework parameterizes the channel prior by its mean \(\mu\) and standard deviation \(\sigma\), but samplers want the canonical shape/scale pair \((k, \theta)\). For a Gamma,

\[ \mathbb{E}[\beta] = k\theta = \mu, \qquad \mathrm{Var}[\beta] = k\theta^2 = \sigma^2 \;\;\Longrightarrow\;\; k = \frac{\mu^2}{\sigma^2}, \quad \theta = \frac{\sigma^2}{\mu}. \]

For the framework's \(\mu = 1.5\), \(\sigma = 1.0\) this gives \(k = 2.25\), \(\theta = 0.667\). The notebook asserts this conversion and its round-trip: the 60,000 sampled draws recover mean 1.502 (target 1.5) and standard deviation 1.001 (target 1.0); the \(\mathrm{Exponential}(0.5)\) draws recover mean 1.982 (target 2.0); the \(\mathrm{Beta}(2,2)\) draws recover mean 0.501 (target 0.5). That assertion is what ties the plotted histogram to the stated prior.

The pipeline, drawn

Before any numbers, the shape of the computation. Raw spend enters on the left; each channel is pushed through carryover (adstock) and then diminishing returns (saturation), scaled by its coefficient \(\beta_c\), summed across channels, added to the baseline, and a Normal draw turns the deterministic \(\mu_t\) into the observed KPI.

MEDIA — per channel c

raw spend \(x_{c,t}\) — dollars per week
adstock carryover spreads spend forward (01)
saturation \(1 - e^{-\lambda_c u}\) — diminishing returns (02)
\(\times\, \beta_c\) channel contribution

BASELINE — non-media structure

intercept + trend\(_t\) + seasonality\(_t\) + controls\(_t\) (+ geo / product offsets in panel models) — 03, 04
↓  sum across channels, add baseline  ↓
\(\mu_t = \text{baseline}_t + \sum_c \beta_c\, \mathrm{sat}_c(\mathrm{adstock}_c(x_{c,t}))\) the noiseless mean
↓  add irreducible noise  ↓
\(\tilde y_t \sim \mathrm{Normal}(\mu_t, \sigma)\) the observed (standardized) KPI

The generative pipeline: spend → carryover → diminishing returns → \(\times\beta\) → sum + baseline → KPI. Fitting runs this diagram backwards.

Simulate one draw from the prior — live

The payoff of an overview: run the generative model once. Below, your browser samples one value of every parameter from its exact framework prior — \(\beta_c \sim \mathrm{Gamma}(\mu{=}1.5, \sigma{=}1.0)\), \(\lambda_c \sim \mathrm{Exponential}(0.5)\), intercept, trend slope, two Fourier coefficients, \(\sigma\) — plus a per-channel geometric decay \(\alpha_c \sim \mathrm{Beta}(2,2)\) (illustratively wider than the shipped default prior \(\mathrm{Beta}(1,3)\); the default model estimates exactly such an \(\alpha_c\) in-graph since June 2026 — math 01). It then pushes a fixed two-year, four-channel spend plan through adstock → saturation → \(\beta\), assembles \(\mu_t\), adds \(\mathrm{Normal}(0, \sigma)\) noise, and decomposes the result. The spend plan never changes between draws — only the parameters do, so what you are watching is pure prior-predictive diversity.

Channelβ (Gamma)λ (Exp)α (Beta, illustrative)max saturation reached

Computed live in your browser — the same forward math as the notebook. Dots are the observed draw \(\tilde y_t\); the line is the noiseless mean \(\mu_t\); the band is \(\mu_t \pm \sigma\). The gap between dots and line is the likelihood. The first draw uses a hand-picked seed so every channel's saturation lands mid-curve (the notebook does the same with its seed 83) — it is one honest draw of the true priors, not a re-shaped distribution. Re-draws are unfiltered: expect occasional wild worlds. That diversity is the prior.

Computed live in your browser. The additive identity drawn as a stacked area: baseline plus each channel's contribution stacks up exactly to \(\mu_t\); the white dots add the noise back. This is the picture an MMM tries to recover from data — except in real life only the dots are observed, and the colors must be inferred. (A small constant display offset keeps the stack positive; it cancels from every identity.)

Identities the notebook asserts (machine-checked facts)

Every claim below is encoded as an assert in math_00_overview.ipynb — "executes clean" means "the math is right":

  • Saturation is bounded: \(0 \le \mathrm{sat}_c(u) < 1\) for every channel and week (the clip at \(-20\) keeps it strictly below its asymptote).
  • \(\mathrm{adstock}(x, \alpha{=}0) = x\) — zero carryover is the identity — and carryover never loses mass: \(\sum_t \mathrm{adstock}(x)_t \ge \sum_t x_t\).
  • The decomposition is exact: \(\text{intercept} + \text{trend} + \text{seasonality} + \sum_c \text{contrib}_c + \text{noise} = \tilde y_t\) to atol=1e-12.
  • \(\tilde y_t - \mu_t = \text{noise}_t\) exactly — \(\mu_t\) is the noiseless expectation.

💡 The notebook's reference draw (measured)

For the record, the baked notebook's single seeded draw (NumPy seed 83) sampled: TV \(\beta{=}0.35, \lambda{=}1.77, \alpha{=}0.52\) (max sat 0.71); Search \(\beta{=}1.24, \lambda{=}1.60, \alpha{=}0.86\) (0.63); Social \(\beta{=}0.54, \lambda{=}2.70, \alpha{=}0.45\) (0.69); Display \(\beta{=}1.49, \lambda{=}3.22, \alpha{=}0.36\) (0.53); intercept 0.36, \(\sigma{=}0.17\). Your browser uses a different random stream, so the draws here are siblings of that one, not replicas — same priors, same pipeline.

Many prior draws of a single channel's response curve

The same forward model, viewed as a response curve: for one channel, how does contribution \(\beta \cdot \mathrm{sat}(x) = \beta\,(1 - e^{-\lambda x})\) rise with adstocked spend \(x\)? Each light line below is a fresh draw of \((\beta, \lambda)\) from the priors; together they are a prior-predictive envelope for the shape of diminishing returns. Every curve is monotone increasing (because \(\beta > 0\)) and concave / saturating (because of the \(1 - e^{-\lambda x}\) form) — the two structural commitments the priors bake in before any data arrive. Both are notebook-asserted facts.

Computed live in your browser — the same math as the notebook (60 seeded prior draws). The dark line is the prior-mean curve (\(\beta{=}1.5\), \(\lambda{=}2\)). Whether this concave-only family is the right commitment is exactly what math 02 and stress 01 interrogate.

Computed live in your browser. The marginal return at the prior-mean parameters: \(\frac{d}{dx}\,\beta(1 - e^{-\lambda x}) = \beta \lambda e^{-\lambda x}\) — another dollar buys less and less. At \(x = 0\) the marginal return is \(\beta\lambda = 3.00\) (prior means), decaying toward zero as spend grows. Average vs marginal return is the decision lever workshop 05 builds on.

Roadmap — the rest of the series

Every term in \(\mu_t\) that we just sampled gets a dedicated page. Math 01 derives carryover: the geometric IIR recurrence vs normalized FIR kernels, the \(1/(1-\alpha)\) multiplier, and the parametric default vs the legacy Beta(2,2) blend weight. Math 02 owns diminishing returns: \(1 - e^{-\lambda u}\) vs Hill, the half-saturation point \(\ln 2 / \lambda\), and marginal ROAS. Math 03 builds the baseline: Fourier seasonality and three trend families (linear, piecewise/Prophet, B-spline). Math 04 assembles the full Bayesian machine: priors → prior predictive → NUTS → convergence diagnostics → posterior intervals → decomposition. Math 05 confronts identifiability — the decay/saturation/\(\beta\) equifinality trap — and shows how a randomized experiment enters as a prior or directly in the likelihood (see also stress 03 and the gauntlet). Math 06 extends the equation itself: NestedMMM mediation, MultivariateMMM cross-effects and correlated residuals, and CombinedMMM as their synthesis. The full system-level reference lives in the technical guide.

The one idea to carry forward: an MMM is a forward recipe for the KPI; fitting is running that recipe backwards. Everything else — carryover, saturation, hierarchy, calibration — is detail on one term of the additive \(\mu_t\), and on the priors that regularize it:

\[ \tilde y_t \sim \mathrm{Normal}\!\Big( \underbrace{\text{intercept} + \text{trend}_t + \text{seasonality}_t + \text{geo} + \text{product} + \textstyle\sum_c \beta_c\, \mathrm{sat}_c(\mathrm{adstock}_c(x_{c,t})) + \text{controls}_t}_{\mu_t},\ \sigma\Big). \]

What to take away

  • The MMM is one additive mean function \(\mu_t\) plus a Normal likelihood — a complete generative recipe. Fitting inverts it.
  • The media term is \(\sum_c \beta_c\, \mathrm{sat}_c(\mathrm{adstock}_c(x_{c,t}))\): carryover, then diminishing returns (\(1 - e^{-\lambda u}\), not Hill), then a strictly positive coefficient.
  • The default priors are weakly informative and exactly tabulated: \(\beta \sim \mathrm{Gamma}(\mu{=}1.5, \sigma{=}1)\), \(\lambda \sim \mathrm{Exp}(0.5)\), \(\sigma \sim \mathrm{HalfNormal}(0.5)\), an in-graph adstock decay \(\alpha_c \sim \mathrm{Beta}(1,3)\) (the default since June 2026; the legacy path's \(\mathrm{Beta}(2,2)\) adstock parameter is a blend weight, not a decay rate).
  • One prior draw generates a plausible year of sales whose decomposition is exactly additive — the picture the fitted model must recover when only the dots are observed.
  • The priors bake in two structural commitments before any data arrive: response curves are monotone increasing and concave. Math 01–06 make every term precise.

Check your understanding

Commit to an answer before opening each one.

Why is the MMM described as a generative model, and what does fitting it correspond to?

The model is a complete forward recipe for the KPI: sample parameters from the priors, compute the additive mean μt, then draw the observation from the Normal likelihood. Fitting runs that recipe backwards — NUTS explores the posterior, proportional to likelihood times prior, to infer which parameter settings could have generated the observed series. The definition box at the top of the page and "The generative equation" section develop this.

The default priors commit the model to two structural properties of every channel's response curve before any data arrive. What are they, and which choices enforce them?

Every prior response curve is monotone increasing and concave (saturating). Monotonicity comes from the Gamma prior on βc, which is strictly positive; concavity comes from the 1 − e−λu saturation form, which bends downward at every spend level. The "Many prior draws of a single channel's response curve" section shows both properties as notebook-asserted facts.

Why does the framework standardize the KPI before modeling, and why does that mean βc is not a channel's ROI?

Standardizing puts the KPI on a common, unit-free scale, so one set of weakly informative priors makes sense for any dataset. On that scale βc is the channel's standardized maximum-contribution ceiling, not its ROI — a dollar ROI also depends on spend, saturation, and the KPI's original scale. The priors table in "The priors" section makes this distinction explicit.

💡 Run it yourself

This page mirrors nbs/math/math_00_overview.ipynb, authored by nbs/builders/build_math_00_overview.py — pure NumPy + matplotlib, bakes in seconds, and every identity above is an assert in the notebook. The series README is nbs/README_math.md. Source: github.com/redam94/mmm-framework.