Chapter 03 of 10

ROI is sampled, beta is derived

The structural choice the rest of the engine hangs off: put the prior where a marketer has an opinion, and solve for the coefficient.

Most MMM implementations put a prior on the media coefficient. That prior is unusable: its scale depends on the KPI units, the spend units, the population divisor, the media scaler and the saturation point, so nobody can state one honestly and almost nobody tries. What gets shipped instead is a weakly-informative default that the analyst has no opinion about, and the elicitation step quietly does not happen.

This engine inverts the dependency. Average ROI is the sampled quantity; the coefficient is solved for.

The derivation

Incremental outcome for channel m, in currency, is the transformed media summed over every geo and period and converted back into KPI units:

I[m] = sum_g sum_t beta[g,m] * delta[g,t,m] * rpk[g,t] * kpi_scale * pop[g]

Collect the geo-and-time sum into A[g,m], substitute the non-centered hierarchy beta[g,m] = exp(log_beta[m]) * geo_factor[g,m], and require that incremental outcome equal ROI times spend. That pins the shared log coefficient exactly:

A[g,m]      = sum_t delta[g,t,m] * rpk[g,t] * pop[g] * kpi_scale
denom[m]    = sum_g A[g,m] * geo_factor[g,m]
log_beta[m] = log(roi[m] * total_spend[m]) - log(denom[m])
denom is floored at 1e-9 so a channel that never ran cannot send log(0) to -inf and NaN every gradient in the chain.

source · engine/mmm/model.py · _derive_log_beta

Why there is no Jacobian correction

This is a common place to get a change of variables wrong, so it is worth being explicit about why nothing is needed here. The prior is placed on roi itself, and log_beta is a deterministic function of that draw and of quantities already in the trace. No density is being transported from one parameterization to another; there is exactly one scalar per channel and its prior is stated directly in the space it is sampled in.

The consequence worth internalizing is that contribution[m] equals roi[m] * total_spend[m]identically, by construction, for every draw. That identity is what makes the prior studio’s implied-contribution check closed form and therefore fast enough to run on every keystroke rather than behind a button.

What the parameterization costs

ROI is positive by construction, because a LogNormal has no mass at or below zero. The model therefore cannot express “this channel destroyed value”, and a channel whose true effect is nil comes back as a small positive number with a wide band rather than an interval straddling zero. Read a lower bound near zero as the model declining to rule out no effect — it is not the same statement as a frequentist interval covering zero, and chapter 09 explains how the placebo test was reworded to survive the difference.