Adstock and Saturation
PanelMMM requires one adstock transform and one saturation transform. ammm
applies them inside the model graph rather than as a fixed preprocessing step.
For the econometrics framing of these transforms, see Adstock and Saturation for Econometricians.
How the transform path works
ammm combines the two transforms through forward_pass(...):
- if
adstock_first=True, the order is adstock then saturation - if
adstock_first=False, the order is saturation then adstock
The transformed result becomes channel_contribution on the model scale.
Adstock options
PanelMMM accepts any AdstockTransformation. The built-in options include:
| Class | Key parameter priors by default | Notes |
|---|---|---|
GeometricAdstock | alpha ~ Beta(1, 3) | Standard geometric carryover |
BinomialAdstock | alpha ~ Beta(1, 3) | Alternative finite-lag carryover |
DelayedAdstock | alpha ~ Beta(1, 3), theta ~ HalfNormal(1) | Allows a delayed peak |
WeibullPDFAdstock | lam ~ Gamma(mu=2, sigma=1), k ~ Gamma(mu=3, sigma=1) | Flexible PDF-shaped carryover |
WeibullCDFAdstock | lam ~ Gamma(mu=2, sigma=2.5), k ~ Gamma(mu=2, sigma=2.5) | Flexible CDF-shaped carryover |
All adstock transforms also take:
l_max: maximum lagnormalize: whether the carryover weights are normalisedmode: convolution mode
Half-life parameterisation
GeometricAdstock and DelayedAdstock also accept
parametrization="halflife". In this mode ammm samples a positive
halflife parameter and derives alpha; it does not sample both. An explicit
half-life prior is required. The legacy alpha parameterisation remains the
default when parametrization is omitted.
For geometric adstock, halflife is the number of model periods after which a
weight falls to half its initial value: alpha = exp(-log(2) / halflife).
For delayed adstock, it is the half-width at half maximum around theta:
alpha = exp(-log(2) / halflife**2). It is not the geometric half-life.
l_max remains the finite lag window in either parameterisation.
import math
from pymc_extras.prior import Prior
from ammm.mmm import GeometricAdstock
adstock = GeometricAdstock(
l_max=16,
parametrization="halflife",
priors={"halflife": Prior("LogNormal", mu=math.log(4), sigma=0.35)},
)
Saturation options
PanelMMM accepts any SaturationTransformation. Common built-ins include:
| Class | Key parameter priors by default | Notes |
|---|---|---|
LogisticSaturation | lam ~ Gamma(alpha=3, beta=1), beta ~ HalfNormal(2) | Default retained choice |
MichaelisMentenSaturation | alpha ~ Gamma(mu=2, sigma=1), lam ~ HalfNormal(1) | Common diminishing-returns form |
HillSaturation | slope ~ HalfNormal(1.5), kappa ~ HalfNormal(1.5), beta ~ HalfNormal(1.5) | Flexible Hill curve |
HillSaturationSigmoid | sigma ~ HalfNormal(1.5), beta ~ HalfNormal(1.5), lam ~ HalfNormal(1.5) | Sigmoid Hill variant |
RootSaturation | alpha ~ Beta(alpha=1, beta=2), beta ~ Gamma(mu=1, sigma=1) | Square-root style curvature |
TanhSaturation | b ~ HalfNormal(1), c ~ HalfNormal(1) | Hyperbolic tangent form |
TanhSaturationBaselined | x0, gain, r, beta all HalfNormal(1) | Baselined tanh form |
Default prior dims
When you pass a transform to PanelMMM, ammm assigns default prior dims for
any transform prior that does not already have explicit dims:
- adstock priors default to
(*dims, "channel") - saturation priors default to
(*dims, "channel")
If you want a different structure, set the prior dims explicitly on the transform.
Configure transforms in Python
from ammm.mmm import GeometricAdstock, LogisticSaturation
from ammm.mmm.panel import PanelMMM
mmm = PanelMMM(
date_column="date",
channel_columns=["tv", "search"],
target_column="sales",
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
adstock_first=True,
)
Example with more customised transforms:
from pymc_extras.prior import Prior
from ammm.mmm import MichaelisMentenSaturation, WeibullCDFAdstock
adstock = WeibullCDFAdstock(
l_max=12,
priors={
"lam": Prior("Gamma", mu=2, sigma=1, dims=("geo", "channel")),
"k": Prior("Gamma", mu=3, sigma=1, dims=("geo", "channel")),
},
)
saturation = MichaelisMentenSaturation(
priors={
"alpha": Prior("Gamma", mu=2, sigma=1, dims=("geo", "channel")),
"lam": Prior("HalfNormal", sigma=1, dims="geo"),
}
)
Configure transforms in YAML
data:
date_column: date
target:
column: sales
type: revenue
media:
channels: [tv, search]
adstock:
type: geometric
l_max: 8
saturation:
type: logistic
For a YAML half-life declaration, use the user-facing period median. ammm
converts median: 4 to mu: log(4) for a LogNormal prior.
media:
channels: [tv, search]
adstock:
type: geometric
l_max: 16
parametrization: halflife
priors:
halflife:
distribution: LogNormal
median: 4
sigma: 0.35
dims: [channel]
saturation:
type: logistic
See examples/half_life_yaml_quickstart.py for a
runnable configuration check.
Override transform priors through priors
Transform priors also appear in model_config under prefixed variable names.
For example:
adstock_alphaadstock_halflifewhen half-life mode is activeadstock_lamadstock_ksaturation_lamsaturation_beta
That means you can override transform priors centrally through the top-level priors
if you prefer. See Priors and Configuration.
Choose the composition order
adstock_first is part of the model specification, not a plotting choice.
The current public YAML schema does not expose adstock_first; it uses the
library default. If you need to change the composition order, use the Python
API.
Use adstock_first=True when you want the model to interpret carryover before
diminishing returns. Use False when you want each period’s spend to saturate
before the carryover step.
The code path is explicit:
True->saturation(adstock(x))False->adstock(saturation(x))
Common pitfalls
- Forgetting that
l_maxis required for adstock classes - Assuming
dimsautomatically change transform priors even when you have already set explicit incompatible dims on the transform - Using
adstock_first=Falsewithout a substantive reason - Treating transform priors as if they were on original business units rather than the model scale
Next steps
- Read Priors and Configuration to control the prior surface.
- Read Time-Varying Parameters if you want media contributions to vary over time.