Workshop 01 — Priors: Saying What You Know

Before you've seen a single week of data, you already know things. TV's effect on sales isn't negative. Carryover from a radio ad doesn't last two years. The return on a search campaign isn't 500-to-1. A prior — your model's starting beliefs about a parameter, written as a probability distribution (the idea from Workshop 00) — is how you write that knowledge down so the model can use it. And here is the part that surprises people: refusing to choose a prior is itself a choice — usually the choice that ROAS of −80 and ROAS of +500 are exactly as believable as ROAS of 2. That's not neutrality. That's a strong, silly opinion wearing a "no opinion" costume.

50%
of the "no opinion" flat prior's belief says TV hurts sales
3.5 wk
median carryover half-life implied by a Beta(8, 2) retention prior
30%
of the default saturation prior's belief: "we never got halfway to saturating this channel"
0.000
gap between flat and weakly-informative answers after 3 years of data

Measured — printed outputs of the seeded nbs/workshop/workshop_01_priors.ipynb run; each is re-verified by an assert when the notebook bakes.

🧭 Before you start

Prerequisites: Workshop 00.  ·  Time: ~60–75 min

You will learn:

  • Why refusing to choose a prior is itself a choice — the "no opinion" flat prior spends half its belief on TV destroying sales.
  • How to match distribution shapes to MMM jobs (Beta for retention, HalfNormal for effect sizes) and translate hyperparameters into marketing units like half-life and half-saturation spend.
  • Why a prior on a parameter is a prior on behavior, and how the prior predictive check catches nonsense before you ever fit.

Run it: nbs/workshop/workshop_01_priors.ipynb on GitHub.

1 — The prior vocabulary: three ways to "know" something

Take one concrete parameter: the effect of TV — incremental sales dollars generated per $1 of TV spend (you may know this as ROAS). Three analysts write down three priors:

All three on one axis. The slider controls the strength of the weakly informative prior — its scale \( \sigma \) — so you can feel what tightening or loosening a prior does to its tail.

2.00
0.27%
3.29

Computed live in your browser — the same math as the notebook. Flat = Uniform(−10, 10); weakly informative = HalfNormal(σ); informative = TruncatedNormal(0.8, 0.3) cut at zero. At the notebook's settings (σ = 2): P(TV effect < 0) = 50% under the flat prior, P(effect > 6) = 0.27% under the weak prior, and the informative prior puts 96% of its belief in (0.2, 1.4). Drag σ and watch the readouts: σ is a pure stretch — doubling it doubles every quantile.

Read the three curves as three sentences:

priorwhat it literally sayswhen to use it
flat"−8 is as plausible as +2; harm is a coin flip"almost never — it isn't neutral, it's reckless
weakly informative"not negative, probably small, extremes need extraordinary evidence"your default for MMM effect parameters
informative"around 0.8 ± 0.3 — I have benchmarks/lift tests and I'm using them"when you genuinely have the evidence (and can defend it in a meeting)

One more vocabulary item before the zoo: the numbers that define a prior — the σ = 2 in HalfNormal(σ = 2), the (0.8, 0.3) in the informative one — are called hyperparameters (the knobs you set on a prior distribution, as opposed to the parameters the model learns from data). Choosing a prior = choosing a shape from the zoo + setting its hyperparameters.

2 — The distribution zoo for MMM

An MMM has four recurring kinds of parameter, and each has a distribution family whose shape matches the job. This section is a field guide; the full prior table for every parameter the model owns lives in Math 00 — The Generative Model.

2.1 HalfNormal → channel effect sizes

A media channel's effect on sales can't be negative (advertising may be useless, but it isn't anti-persuasive — and if you truly believe a channel hurts sales, you want a different model, not a negative coefficient) and is probably small. So we want a shape whose support — the set of values a distribution allows at all — starts at zero, puts plenty of belief near zero, and fades smoothly into a tail. That's the HalfNormal(σ): a bell curve folded at zero, with one knob — "how big could this effect plausibly be?" You already drove it with the slider above. The measured quantile check from the notebook: the 90th percentile of HalfNormal(σ) is 0.82 / 1.64 / 3.29 for σ = 0.5 / 1 / 2 — exact linear scaling, asserted in the bake. When you have a location in mind, not just a scale, use its cousin TruncatedNormal(μ, σ, lower=0): a bell centered at your benchmark, chopped at zero.

2.2 Beta → adstock retention (a fraction in [0, 1])

Adstock is MMM's word for carryover — an ad you run this week still nudges sales next week, a bit less the week after, and so on. The simplest ("geometric") version has one parameter: the retention rate \( \alpha \) — the fraction of this week's advertising pressure that survives into next week. After \( k \) weeks an impulse is worth \( \alpha^k \). A retention rate is a fraction — it lives strictly between 0 and 1 — so its natural prior family is the Beta distribution (the flexible distribution on [0, 1] you met with conversion rates in Workshop 00).

Here is the move that makes priors talkable in a marketing meeting: convert retention into half-life — the number of weeks until the carryover effect has decayed to half its original strength. Setting \( \alpha^h = \tfrac12 \) and solving:

\[ h(\alpha) = \frac{\ln(0.5)}{\ln(\alpha)} \]

A retention of 0.5 means a half-life of exactly 1 week; higher retention stretches it fast. So a Beta prior on retention is a prior on half-lives — and you can ask the brand team "does a 3-week TV half-life sound right?" instead of "is Beta(8, 2) okay?". Drag the two hyperparameters and watch both panels move together.

2.0
2.0
1.0 weeks

Computed live in your browser — the same math as the notebook (the half-life panel uses the exact change-of-variables density where the notebook histograms 20,000 draws; same identity, no sampling noise). Notebook anchors, asserted in the bake: median implied half-life is 0.44 weeks for fast-decay Beta(1, 3), 1.0 week for open-minded Beta(2, 2), and 3.5 weeks for long-memory Beta(8, 2). Try those three settings on the sliders.

2.3 Gamma & Exponential → the saturation rate λ

Saturation is the other workhorse transform: doubling spend doesn't double sales — audiences run out, auctions get pricier. The framework's core model uses the logistic curve \( f(x) = 1 - e^{-\lambda x} \), where \( x \) is media pressure scaled so the biggest observed week is 1, and the saturation rate \( \lambda \) controls how fast returns diminish. \( \lambda \) must be positive and could be anywhere from "barely saturating" to "saturates immediately" — a job for a positive, right-skewed family like Gamma or the one-knob Exponential (the framework's built-in default for this parameter, with mean 2).

Like retention → half-life, \( \lambda \) has a marketing translation: the half-saturation point — the spend level at which you've already banked half of the channel's maximum possible effect. From \( f(x_{1/2}) = \tfrac12 \):

\[ x_{1/2} = \frac{\ln 2}{\lambda} \]

Multiply by the channel's max weekly spend (here $50k, as in the notebook) and your prior on \( \lambda \) becomes a statement like "we hit half of TV's ceiling somewhere around $X thousand a week" — something a media planner can actually argue with. Watch the right-hand readout: a chunk of the default prior's belief puts the half-saturation point beyond the biggest week you've ever bought — i.e. "we never even got halfway to saturating this channel." Decide whether you believe that.

1.0
2.0
30%

Computed live in your browser — the same math as the notebook (exact change-of-variables density for the half-saturation panel). Shape a = 1 is the Exponential — the framework default. Notebook anchors, asserted in the bake: P(half-saturation beyond the max observed week) = 30% under the default Exponential(mean 2), 15% under Gamma(2, 1), and 0% under the confident Gamma(6, 2) (set a = 6, mean = 3). Saturation-adjacent defaults are not cosmetic: in Stress 01 the Weibull adstock scale prior's default materially shaped the recovered carryover kernel.

2.4 Normal → control & seasonal coefficients — and the zoo, summarized

Controls are different: a price increase, a competitor launch, a heat wave can push sales either way. For sign-free coefficients the plain Normal(μ, σ) is right: centered at zero ("no effect until the data says otherwise"), symmetric, with σ setting how big an effect you'd entertain. The framework's default for controls is Normal(0, 0.5) on standardized data — widened to Normal(0, 2.0) for confounder-role controls (an explicitly-set coefficient prior overrides these role-based widths). The same idea powers seasonal (Fourier) coefficients.

parameterlives infamilymarketing translation of the prior
channel effect β[0, ∞)HalfNormal / TruncatedNormal / Gamma"not harmful, probably modest"
adstock retention α(0, 1)Betaa half-life in weeks
saturation rate λ(0, ∞)Gamma / Exponentiala half-saturation spend in $
control / seasonal coef(−∞, ∞)Normal"either direction, probably small"

3 — Priors on parameters are priors on BEHAVIOR

Here is the single most useful mental shift on this page — its core doctrine. You never actually care about \( \alpha \) or \( \lambda \). You care about curves: how carryover decays week by week, how sales respond as spend ramps. Every draw from the prior on \( \alpha \) is a whole decay curve; every draw from the prior on \( \lambda \) is a whole response curve. So a prior on a parameter is really a prior over families of behavior — and the honest way to inspect a prior is to draw a hundred parameter values and plot the hundred curves they imply (a "spaghetti plot").

Doctrine

If the spaghetti contains curves your gut rejects — carryover lasting half a year for a flash-sale channel, response curves still climbing steeply at twice your maximum budget — your prior said that, whether you meant to or not.

The payoff exhibit. Each slider reshapes a prior; each render draws 70 parameter values from it and plots the 70 behaviors they imply. Left: carryover curves \( \alpha^k \) from the Beta retention prior. Right: response curves \( 1 - e^{-\lambda x} \) from the \( \lambda \) prior. The dark line is the median curve; the dotted markers are the half-strength line (left) and the biggest week ever bought (right).

2.0
2.0
2.0

Computed live in your browser, seeded — the same math as the notebook. Every curve starts at full strength and only decays (left) or starts at zero, rises, and never exceeds the ceiling (right) — the bake asserts both, for every slider setting it tests. The notebook's measured gut-check at the defaults: under the framework's default Exponential(mean 2) λ prior, 28% of prior response curves are still below half their ceiling at 1.2× the maximum observed week — the prior leaves "this channel barely saturates" firmly on the table. Try Beta(8, 2): week-4 carryover under the long-memory prior is more than 5× the fast-decay Beta(1, 3) setting (asserted in the bake).

4 — The prior predictive check: simulate sales before you fit

Curves are behavior one transform at a time. The full-strength version of the same idea runs the whole model forward from its priors: draw one value of every parameter from its prior, push spend through the model, add noise — out comes one completely fake year of weekly sales. Repeat a few hundred times and you have the prior predictive distribution — the spread of datasets your model considers plausible before seeing any real data. Comparing that spread against common sense is the prior predictive check.

Doctrine, worth memorizing

This is the cheapest, highest-value check in the entire workflow. It needs no fitting, no MCMC, no waiting — and it catches the embarrassing failures (negative sales! 10× revenue weeks!) at the moment they're easiest to fix: before the data ever gets a vote.

The toy world: two channels (TV-ish and Search-ish), 104 weeks, sales in $thousands — the exact MMM skeleton you'll meet for real in Workshop 03: intercept + β·saturate(adstock(spend)) per channel + noise. History says weekly sales live roughly between $800k and $1.3M (the green band). First attempt: an analyst who "doesn't want to assume anything" sets huge, vague priors. Then the fix: say what you actually know — sales hover around $1M/week, no channel plausibly adds more than a few hundred $k at full saturation, week-to-week noise is tens of $k.

Computed live in your browser (seeded recreation of the notebook's simulation; the quoted shares are measured from the baked notebook run). 200 fake years from the priors: shaded band = 90% of prior draws, thin lines = 30 individual fake years, green band = where real weekly sales actually live, dashed red = zero sales. Measured under the vague priors: 43% of prior draws contain at least one negative sales week and only 27% of simulated weekly values land within shouting distance ($400k–$2.2M) of reality. After the check: 0% negative weeks, 100% in the sane range — and the 90% band is still comfortably wider than the historical range, so the data keeps room to speak.

43%
vague-prior draws with a negative sales week
27%
vague-prior weekly values anywhere near reality
0%
negative weeks after the check
100%
weekly values in the sane range after the check

Nothing about the second set of priors is "biased" — it still entertains flat TV, powerful TV, fast and slow carryover, every saturation speed. It has simply stopped insisting that negative million-dollar sales weeks deserve belief. That's the whole craft: rule out the absurd, stay loose about the plausible.

💡 Two habits

Run the prior predictive every time you change a prior or add a channel — it costs seconds. And judge it against outside knowledge (what sales can be), never against the dataset you're about to fit — tuning priors to the data you'll fit is using the data twice. In Workshop 04 you'll meet the posterior predictive check — same trick, run after fitting.

5 — How mmm_framework says it

Everything above was raw math so you could see the gears. In the framework, a prior is a small config object — PriorConfig — holding a distribution name and its hyperparameters, and you attach priors to the things they govern: the adstock config carries the retention prior, the saturation config carries the λ prior, the channel config carries the effect-size prior. Classmethod shortcuts and fluent builders produce identical configs (the notebook asserts their equality):

from mmm_framework.config import PriorConfig
from mmm_framework.builders import PriorConfigBuilder

effect_prior    = PriorConfig.half_normal(sigma=2.0)
retention_prior = PriorConfig.beta(alpha=8, beta=2)   # long-memory carryover
rate_prior      = PriorConfig.gamma(alpha=6, beta=2)  # confident saturation

# ...or the fluent builder — same object either way.
effect_prior_b = PriorConfigBuilder().half_normal(sigma=2.0).build()
assert effect_prior == effect_prior_b

A channel's priors live on its transform configs plus the channel itself:

from mmm_framework.config import AdstockConfig, SaturationConfig, MediaChannelConfig, DimensionType
from mmm_framework.builders import MediaChannelConfigBuilder

tv_channel = MediaChannelConfig(
    name="TV",
    dimensions=[DimensionType.PERIOD],
    adstock=AdstockConfig.geometric(            # carryover: retention prior
        l_max=8, alpha_prior=PriorConfig.beta(alpha=3, beta=3)),
    saturation=SaturationConfig.logistic(),     # λ keeps the built-in Exponential default
    roi_prior=PriorConfig.half_normal(sigma=1.0),
)

search_channel = (MediaChannelConfigBuilder("Search")
                  .with_geometric_adstock(l_max=4)
                  .with_logistic_saturation()
                  .with_positive_prior(sigma=2.0)   # sets coefficient_prior (HalfNormal)
                  .build())

⚠️ A naming gotcha

Despite its name, roi_prior is a prior on the (saturation-scaled) coefficient \( \beta_{TV} \), not on ROI directly. When it's None, the core model's default is Gamma(mu=1.5, sigma=1.0), which gently favors paying channels. And remember the rest of the defaults: λ gets Exponential(mean 2) — the prior whose belief you inspected in §2.3 — and controls get Normal(0, 0.5) on standardized data (Normal(0, 2.0) for confounder-role controls).

The real prior predictive: hand those channel configs to BayesianMMM (properly introduced in Workshop 03) and ask it to simulate sales from its priors — no fitting involved:

cfg = ModelConfig(inference_method=InferenceMethod.BAYESIAN_NUMPYRO,
                  use_parametric_adstock=True)  # honor the configured adstock priors
mmm = BayesianMMM(panel, cfg, TrendConfig(type=TrendType.LINEAR))
idata = mmm.sample_prior_predictive(samples=300)

pp = idata.prior["y_obs_scaled"].values[0]      # (draws, weeks), original $k scale

Two details worth knowing, both verified by the notebook's asserts: the framework standardizes the KPI internally (a quiet prior-hygiene trick — priors live on a unitless scale, so defaults transfer across brands), and the simulated KPI back in original units is the derived y_obs_scaled variable, which lands in the prior group of the returned object (the prior_predictive group holds the standardized-scale y_obs). The baked run confirms the configured priors were honored — TV effect draws never negative, retention draws inside (0, 1) with a Beta(3, 3)-ish mean — and the prior fan brackets the observed sales level. That's the full configuration loop you'll reuse for real: zoo shape → PriorConfig → attach to configs → simulate sales → eyeball → adjust.

6 — Sensitivity in one picture: when priors matter

The last worry every newcomer has: "doesn't the prior just decide the answer?" The honest reply is it depends on how much the data knows. Tiny model: weekly incremental sales \( y_t = \beta x_t + \varepsilon_t \) with known noise. For this special case the posterior has an exact pencil-and-paper formula (the Normal prior is conjugate here — maths jargon for "prior and posterior come out in the same family, so no sampler is needed"). Three analysts: near-flat Normal(0, 50); weakly informative Normal(1, 1); and a strongly informative, miscalibrated Normal(0.5, 0.15) — confident, and (unknown to its author) centered well below the truth. The world's true \( \beta \) is 1.2. Six weeks of data, then three years:

Computed live in your browser — the same conjugate math as the notebook (browser-seeded data, so curve positions are an illustrative recreation; the quoted numbers are measured from the notebook run). Measured: the spread of the three posterior means is 0.57 after 6 weeks → 0.20 after 156 weeks, and the gap between the near-flat and weakly-informative answers at 156 weeks is 0.000 — reasonable priors wash out. The confident-and-wrong prior is still dragging its posterior below the truth after three years.

Now be all three analysts at once — pick a prior, pick a data size:

0.50
0.50
12

Computed live in your browser — the same math as the notebook. The conjugate identity behind the update (asserted in the bake): posterior precision is exactly prior precision + data precision, \( 1/\sigma_{\text{post}}^2 = 1/\sigma_0^2 + \sum_t x_t^2 / \sigma^2 \) — so data can only sharpen the belief. Set the prior sd tiny and watch even 156 weeks fail to drag a confident wrong prior to the truth.

Three lessons, straight off the picture:

  1. Small data ⇒ the prior is load-bearing. With six weeks, the three analysts walk out of the meeting with three different βs. Choose priors like they matter — because here they do.
  2. Reasonable priors wash out. With three years, near-flat and weakly informative land on the same answer. The weakly informative one got there with less drama (narrower at every stage, no absurd detours).
  3. Overconfident wrong priors don't wash out politely. The Normal(0.5, 0.15) author is still biased after three years. Informative priors are powerful exactly because they're strong — earn them with evidence (lift tests, last year's model), and prior-predictive-check them like everything else.

⚠️ The MMM-specific kicker

A real MMM is permanently in the small-data panel. Channels move together, carryover and saturation trade off against each other, and three years of weekly data is only 156 rows against dozens of parameters — the data is never as loud as you'd hope (weak identification: the situation where the data alone can't pin parameters down). In MMM, priors aren't training wheels you outgrow; they're part of the vehicle. The stress series shows what that costs when assumptions break — start with the pressure-testing scorecard.

7 — Glossary & what's next

termplain English
prioryour starting beliefs about a parameter, written as a distribution
flat / uninformative priorspreads belief evenly over a huge range; sounds neutral, usually isn't
weakly informative priorrules out the absurd, loose about the plausible — your default
informative priorcommits to real external knowledge (benchmarks, lift tests)
hyperparametera knob that defines a prior (the σ in HalfNormal(σ))
supportthe set of values a distribution allows at all
adstock / retention ratecarryover; the fraction of ad pressure surviving each week
half-lifeweeks until carryover halves: ln(0.5)/ln(α)
saturation rate λhow fast diminishing returns bite in 1 − e−λx
half-saturation pointspend where you've banked half the ceiling: ln 2/λ
prior predictive distributionfake datasets simulated from priors alone
prior predictive checkcomparing those fakes against common sense, before fitting
conjugateprior/posterior in the same family — exact pencil-and-paper update
weak identificationwhen the data alone can't pin parameters down — MMM's natural habitat

What to take away

  • Refusing to choose a prior is itself a choice — the flat prior spends half its belief on TV destroying sales. That's an opinion, not neutrality.
  • Match the shape to the job: Beta for retention fractions, Gamma / Exponential for positive rates, HalfNormal for effect sizes, Normal for sign-free controls — and translate every hyperparameter into marketing units (half-life, half-saturation spend) before defending it in a meeting.
  • A prior on a parameter is a prior on behavior. Plot the implied decay and response curves; if the spaghetti contains nonsense, your prior said it.
  • The prior predictive check is the cheapest, highest-value check in the workflow — judge it against outside knowledge, never against the data you're about to fit.
  • In MMM, priors never fully wash out (weak identification), so the loop is: zoo shape → PriorConfig → simulate → eyeball → adjust.

Check your understanding

Commit to an answer before opening each one.

A colleague sets a flat prior on TV's effect "to stay neutral". What opinion has that prior actually expressed?

A flat prior over a huge range makes harm a coin flip: half of its belief says TV hurts sales, and TV destroying $8 of sales per dollar is treated as exactly as plausible as TV earning $2. Refusing to choose a prior is itself a choice — a strong opinion wearing a "no opinion" costume. Section 1 puts all three prior styles on one axis so you can read what each one literally says.

The brand team has never heard of a Beta distribution. How do you discuss the adstock retention prior with them anyway?

Translate retention into a half-life — the number of weeks until the carryover effect decays to half strength. A Beta prior on retention is a prior on half-lives: Beta(8, 2) implies a median half-life of 3.5 weeks, so you can ask "does a roughly 3-week TV echo sound right?" instead of "is Beta(8, 2) okay?". Section 2.2 makes the conversion, and Section 2.3 does the same for saturation via the half-saturation spend.

What is the prior predictive check, and why does this page call it the cheapest, highest-value check in the workflow?

Draw one value of every parameter from its priors, run the model forward, and repeat a few hundred times — the result is the spread of fake sales histories your model considers plausible before seeing any data. Comparing that spread with common sense catches the embarrassing failures (negative sales weeks, ten-times-revenue weeks) with no fitting and no waiting, at the moment they are cheapest to fix. One caution from Section 4: judge it against outside knowledge of what sales can be, never against the dataset you are about to fit.

💡 Run it yourself

This guide mirrors nbs/workshop/workshop_01_priors.ipynb, authored by nbs/builders/build_workshop_01_priors.py — see nbs/README_workshop.md for the full six-notebook series. Every computational cell ends with seeded asserts encoding the claim it plots, so a clean bake re-verifies every number quoted here. The notebook adds three ipywidgets live-exploration cells matching the sliders on this page. Next: Workshop 02 — Sampling: How the Machine Learns — why we draw samples instead of solving equations, and the diagnostics that tell you whether to trust the draws. Source: github.com/redam94/mmm-framework.