Specify priors and transformations
Start with a model whose assumptions you can explain. In an ordinary MMM, transformed channel terms, controls, an intercept and optional effects form the linear predictor; the link and likelihood determine how it relates to outcomes.
Adstock and saturation
Adstock describes carryover on the observation grid, so l_max is measured in
model periods, not calendar days. GeometricAdstock offers geometric decay;
other exported transformations include delayed and Weibull forms. Normalisation,
lag direction and parameterisation affect the meaning of the amplitude prior.
LogisticSaturation describes a bounded response to transformed exposure.
The default adstock_first=True applies carryover before saturation. Changing
the order changes the model; it is not a cosmetic option. Examine plausible
impulse responses and response curves before interpreting either parameter.
Carryover and shape modelling
provides a methodological reference, not evidence that this dataset identifies
the selected transformations.
Priors in scaled units
This standalone constructor example changes the intercept, channel amplitude and observation-noise priors. It does not build or fit a graph.
from pymc_extras.prior import Prior
from ammm.mmm import MMM, GeometricAdstock, LogisticSaturation
model = MMM(
date_column="date", channel_columns=["tv", "social"],
adstock=GeometricAdstock(l_max=4), saturation=LogisticSaturation(),
model_config={
"intercept": Prior("Normal", mu=0, sigma=1),
"saturation_beta": Prior("HalfNormal", sigma=0.5, dims="channel"),
"likelihood": Prior("Normal", sigma=Prior("HalfNormal", sigma=0.2)),
},
)
The numbers are illustrative. Express external knowledge in the model’s scaled units, inspect implied outcomes and vary consequential assumptions. A positive amplitude prior constrains signs, so a positive interval alone is not independent evidence against negative effects.
Controls, seasonality and time variation
control_columns adds linear control terms. Choose them from the adjustment
argument, not by maximising fit. Standardise them explicitly when appropriate,
using training-only statistics that can also be applied to future inputs.
yearly_seasonality specifies the number of annual Fourier modes. The
time_varying_intercept and time_varying_media options admit time variation
through a Hilbert-space Gaussian-process approximation, either using configured
defaults or a supported HSGP object. Smoothness and amplitude priors determine
which variation the baseline can absorb. Extra flexibility can compete with
media effects, so compare predictions and channel estimates under defensible
alternatives. Respect the finite approximation domain when forecasting.
Holiday effects and custom effects are separate capabilities with their own persistence and response contracts. See holidays, custom effects, pooling and prior sensitivity.
Prepare a labelled panel
This self-contained construction example creates one row per date and geography.
It adds a training-standardised control to an ordinary MMM; shared input dates
and complete labels do not by themselves create partial pooling or causal
identification (src/ammm/mmm/data_conversion.py:189,
src/ammm/mmm/mmm.py:1375). The numbers are synthetic.
import numpy as np
import pandas as pd
from ammm.mmm import MMM, GeometricAdstock, LogisticSaturation
rng = np.random.default_rng(17)
panel = pd.DataFrame({
"date": np.repeat(pd.date_range("2024-01-01", periods=52, freq="W-MON"), 2),
"geo": ["north", "south"] * 52,
"tv": rng.uniform(5, 80, 104),
"price": rng.uniform(9, 12, 104),
})
price_mean, price_sd = panel["price"].mean(), panel["price"].std(ddof=0)
panel["price_z"] = (panel["price"] - price_mean) / price_sd
panel["revenue"] = 100 + 20 * np.tanh(panel["tv"] / 40) + rng.normal(0, 2, 104)
assert not panel.duplicated(["date", "geo"]).any()
panel_model = MMM(
date_column="date", target_column="revenue", channel_columns=["tv"],
control_columns=["price_z"], dims=("geo",),
adstock=GeometricAdstock(l_max=4), saturation=LogisticSaturation(),
)
panel_model.build_model(panel.drop(columns=["revenue", "price"]), panel["revenue"])
For a real holdout, compute price_mean and price_sd on training rows only,
reject zero standard deviation, and apply those same values to future rows.
Retain both statistics with the model inputs because controls are not
standardised by MMM (src/ammm/mmm/_mmm_graph.py:298). Use the
pooling example when a learned hierarchy is intended.
Compare transform and baseline assumptions
For the default geometric component, trailing normalised weights are proportional
to alpha**lag over the retained lag horizon; the default alpha prior is
Beta(1, 3). The logistic component is beta * tanh(lam * x / 2), with default
lam ~ Gamma(3, 1) and beta ~ HalfNormal(2) in model units
(src/ammm/mmm/components/adstock.py:425,
src/ammm/mmm/components/saturation.py:245). An increased lag horizon changes
both the retained history and normalisation, so compare implied curves rather
than interpreting an alpha value without its horizon.
This constructor pair continues the panel data above. Each member is a separate candidate; the second changes both carryover and shape, so report it as specification sensitivity rather than solely prior sensitivity.
from ammm.mmm import NoAdstock, HillSaturation
shape_alternative = MMM(
date_column="date", target_column="revenue", channel_columns=["tv"],
dims=("geo",), adstock=NoAdstock(l_max=1), saturation=HillSaturation(),
)
shape_alternative.build_model(panel.drop(columns="revenue"), panel["revenue"])
Compare prior predictive support, retained fit diagnostics, independent-period predictions and the channel quantities used for decisions. A better in-sample fit cannot select a causal response shape by itself; the exported transform constructors are listed in the API reference.
Build a seasonal and smooth-baseline candidate
This continues panel and builds an ordinary MMM with two annual Fourier modes
and a smooth intercept. The reduced basis size is illustrative, not a universal
approximation recommendation; this is graph construction, not a fitted comparison.
from ammm.hsgp_kwargs import HSGPKwargs
smooth_model = MMM(
date_column="date", target_column="revenue", channel_columns=["tv"],
dims=("geo",), adstock=GeometricAdstock(l_max=4),
saturation=LogisticSaturation(), yearly_seasonality=2,
time_varying_intercept=True,
model_config={"intercept_tvp_config": HSGPKwargs(m=30, ls_mu=5, ls_sigma=10)},
)
smooth_model.build_model(panel.drop(columns="revenue"), panel["revenue"])
Fourier coefficients default to Laplace(0, 1) and ordinary control coefficients
to Normal(0, 2); boolean time-variation defaults use 200 basis functions,
L=None, eta_lam=1, ls_mu=5, ls_sigma=10
(src/ammm/mmm/mmm.py:1375). Hold the likelihood, data split and response
estimand fixed when comparing basis sizes, domain choices and smoothness priors.
Inspect prior trajectories and posterior contribution changes as well as
prediction: a smooth baseline can absorb the same low-frequency variation as
media. Forecasts must respect the fitted HSGP approximation domain
(src/ammm/mmm/tvp.py:183, src/ammm/mmm/hsgp.py:64).
Implementation reference at 7cb7f20: src/ammm/mmm/mmm.py:377, src/ammm/mmm/_mmm_graph.py:182, src/ammm/mmm/components/adstock.py:348.