Skip to main content
Back to blog
Marketing Mix Modeling
Adstock
Bayesian Statistics
Priors
PyMC-Marketing

Half-Life Is the Parameter: A Two-Line Fix for Geometric Adstock

Advertising keeps working after it runs, and MMMs capture that with adstock. But the standard way of writing it, a retention rate called alpha, means nothing to anyone in a marketing meeting. Here's why that causes real problems, and the two-line change that fixes it.

Niall Oulton & Joe WilkinsonAugust 7, 202610 min read

Run a decent burst of TV this week and the sales don't all arrive this week. Someone sees the ad on Tuesday, thinks about it, mentions it to a friend at the weekend, and buys three weeks later. Advertising has a memory. Any marketing mix model that ignores that will quietly hand your TV budget's credit to whatever channel happened to be running when the sale finally landed.

MMMs deal with this using something called adstock, old ad-industry jargon for the “stock” of advertising effect you've built up over time. Instead of relating this week's sales to this week's spend, the model relates them to this week's spend plus a fading echo of every previous week. The most popular version, geometric adstock, makes one simple assumption: each week, the echo fades by a fixed proportion. That proportion is the retention rate, α.

So α = 0.5 means half the effect survives into next week. α = 0.9 means ninety percent survives. Fit the model, read off α, job done. Except that in practice, α is where MMM conversations go to die. I've watched a room of smart marketers nod politely at “TV's alpha came out at 0.93” with nobody, sometimes including the modellers, having any real feel for whether that's plausible. And the confusion isn't the room's fault. The scale itself is broken. Watch what happens when we take two identical-sized steps on it:

Two identical steps in α…
…and what each one actually does to the memory in your model.
0.20 → 0.25
+0.07 weeks
0.95 → 0.97
+9.24 weeks
The same two-point move on the α scale carries roughly 133× the impact on what the model believes about memory.

The first step is a rounding error. The second quietly adds two months of advertising memory. Same-sized move, 133 times the consequence. The reason: the thing α actually controls, the number of weeks an effect hangs around, is squashed into the very top of the scale. Every long-memory channel you care about (TV, video, anything brand-building) lives crammed up against α = 1, where the third decimal place suddenly matters.

Try it yourself

Honestly, the quickest way to get this is to play with it for ten seconds. Both sliders below control the same decay curve: the top one walks through α at a steady pace, the bottom one walks through the memory itself.

One model, two dials
Both sliders control the same adstock curve.
retention
α = 0.700
effect halves every
1.9 weeks
50%half strength at 1.9 weeks0 wks510152025

Notice the top slider barely changes the curve for most of its travel, then suddenly changes everything. The bottom slider feels even the whole way; that is the coordinate your intuition lives in.

That “flat, flat, flat, cliff” feeling in the top slider is the whole problem in miniature. It's also why a tidy-looking model result such as α = 0.96 ± 0.01 can hide enormous uncertainty about the thing you actually wanted to know. The same ± 0.01 around α = 0.30 means basically nothing.

There's a better word for this, and physics got there first

The friendly way to describe something that fades by a constant proportion is its half-life: how long until it drops to half strength. Same maths as radioactive decay. “TV's effect halves in about ten weeks” is a sentence a brand manager, a finance director and a statistician can all argue about over one coffee. “TV's α is 0.93” is not, even though it's the same claim.

This matters more than it sounds, because a Bayesian MMM runs on prior knowledge, and the knowledge genuinely exists. Your media team roughly knows how their channels decay: search burns out in days, TV lingers for months. But that knowledge lives in weeks. If the model demands it as a distribution over α, you're forcing a translation through the hockey stick, and the hockey stick mangles it. The classic “play it safe” choice (a flat prior over α, every value equally likely, surely harmless) is the best example. Translate it back into memory units and look at what it actually claims:

What a “neutral” flat prior on α secretly believes
Implied half-life at each percentile of α ~ Uniform(0, 1).
median
1 week
90th percentile
6.6 weeks
95th percentile
13.5 weeks
99th percentile
69 weeks
Half of all prior belief says memory lasts under one week, with a thin tail stretching past a year.

Half of that “neutral” prior's belief is that advertising is forgotten inside a single week. Nobody at your company believes that. Nobody chose it, either; it just fell out of a coordinate change no one looked at. (And the Beta(2,2) prior some tools default to isn't much better: its median implied half-life is still one week.) Compare that with simply saying what you mean in time units: “probably around a month, give or take a few weeks”, which is a perfectly ordinary Gamma prior on the half-life itself.

The implied half-life priors
What common α priors secretly say about memory, next to a prior stated directly in half-life units.

The fix is two lines of code

Put the prior on the half-life directly, in weeks, and compute α from it. If h is the half-life, then α = 0.51/h, which is just the definition of a half-life turned inside out. In PyMC:

python
half_life = pm.Gamma("half_life", mu=4.0, sigma=3.0)  # in weeks
alpha = pm.Deterministic(
    "alpha",
    pt.exp(pt.log(0.5) / half_life),
)
# Feed alpha into your existing geometric adstock, unchanged.

The adstock calculation itself doesn't change at all; α still gets fed into the same formula it always did. What changes is who can join the conversation. “Ten weeks feels too long for this campaign” is an objection a brand manager can raise in a meeting and a modeller can act on the same afternoon. “Beta(1,3) feels too strong on α” is a conversation that has never happened in the history of marketing.

Kernel families in alpha and half-life space
Equal steps in α bunch together, then fly apart. Equal steps in half-life move smoothly through the family of decay curves.

Numbers your team can argue with

For weekly data, here's the kind of opening bid we'd put on the table: not defaults to obey, just numbers for your team to disagree with. That disagreement is the point: it's how the knowledge in people's heads gets into the model.

Channel classPlausible half-lifeExample prior
Search or performance0.5 to 3 weeksGamma(μ=1.5, σ=1)
Social or display1 to 6 weeksGamma(μ=3, σ=2)
TV, video or brand4 to 26 weeksGamma(μ=10, σ=6)
Trade or promotion0.5 to 2 weeksGamma(μ=1, σ=0.5)

Checking what a prior like this implies takes a few lines:

python
rng = np.random.default_rng(42)

shape = (4.0 / 3.0) ** 2
scale = 3.0**2 / 4.0
half_life_draws = rng.gamma(shape=shape, scale=scale, size=20_000)
alpha_draws = 0.5 ** (1.0 / half_life_draws)

lags = np.arange(52)
kernels = alpha_draws[:, None] ** lags[None, :]

Working in time units keeps paying off as the model grows, too. Want to share information between similar channels, or encode a belief like “brand memory outlasts performance memory”? In half-life space those are ordinary, checkable statements about weeks. In α space their practical meaning warps depending on where you are on the scale.

It makes the sampler's job easier too

There's a computational side to this as well, and it bit us in our own benchmark. The sampler behind PyMC explores the space of plausible parameter values by taking steps, and it works best when a step of a given size means roughly the same thing everywhere. You can guess the problem: with a flat prior on α, every long-memory model is crammed into that razor-thin sliver below α = 1. The sampler needs microscopic steps there and big ones everywhere else, can't do both, and starts throwing warnings (the dreaded divergences) while producing fewer usable samples.

Implied half-life against equal-sized steps in each sampled coordinate
Walking each model's sampled parameter at a steady pace. Sampling α directly squeezes every long memory against the far end of the walk (the true 16-week brand half-life sits at z = 0.96), while sampling half-life directly makes the quantity of interest linear in the thing being explored.

We tested this properly on simulated weekly data: three channels with known true half-lives of 1.5, 4 and 16 weeks, at one, two and three years of history, everything identical between the two versions except the adstock prior. Our first pass, at PyMC's usual settings, made the point loudly: the flat-α model sprayed 107 divergence warnings on the three-year run (and the half-life model wasn't spotless either, with ten). Divergences are curable if you're willing to pay for them, so for the results below we turned the sampler's caution up (target_accept=0.97, slower and more careful stepping) until everything ran clean. At those settings the half-life model recorded zero divergences at every dataset size. But careful stepping costs time, and it can't fix what a prior believes. Here's the three-year run:

The hardest run: three years of data, long-memory brand channel in play
Same data, same model, both sampling cleanly with zero divergences. Only the adstock prior differs.
Flat prior on α
0.1 – 56 wks
94% range for brand memory (truth: 16 weeks)
17.4
min effective samples per second (higher is better)
Half-life prior
3.5 – 23 wks
94% range for brand memory (truth: 16 weeks)
26.2
min effective samples per second (higher is better)
Sampler diagnostics
Sampler efficiency across all three dataset lengths: the half-life model produces more useful samples per second everywhere, with zero divergences in either arm at these settings.

And the flat-α model's actual answers were as shaky as its warnings suggested. For the brand channel, where we know the true half-life is 16 weeks because we simulated the data, it technically “covered the truth,” but only by being uselessly vague:

The flat-α model's answer for the brand channel
Posterior half-life, three years of weekly data. True value: 16 weeks.
median 8.0truth: 16mean 28.40102030405060 wks
The shaded band is the 94% interval: 0.1 to 56 weeks, or “somewhere between no memory and a year.” Its own median (8) and mean (28) disagree by a factor of 3.5, a signature of the extreme skew the α mapping creates.

“Somewhere between no memory and a year” is not an answer you can plan a media budget around. And when a model's median and mean are telling you stories that different, neither one deserves your trust. Meanwhile the half-life version, on exactly the same data, pinned all three channels down with intervals you could actually use:

Posterior recovery
Posterior recovery of the true half-lives under each parameterisation.

Being honest about why it works

We wanted to know whether the magic was in the half-life coordinate itself or just in having a sensible prior at last. So we ran a control: same three years of data, sampling α directly again, but this time carrying the half-life prior across to the α scale exactly (for the technically minded: including the Jacobian). Result: close. The good prior did most of the work regardless of which coordinate carried it, though sampling half-life directly did keep a modest edge, running clean where the control still produced a few divergences, and getting noticeably more effective samples per second.

Posterior geometry and same-prior control
Posterior geometry for the flat-α, direct half-life, and same-prior control models.

So the honest conclusion is: the big win came from replacing an accidental prior with a deliberate one. Half-life isn't computational magic; it's the coordinate that makes deliberate priors easy to state, easy to defend, and easy to reuse. Which, given how the accidental prior got there in the first place, is exactly the fix that was needed.

The fine print

  • Reparameterisation can't create information. If the data can't tell an 8-week memory from a 16-week one, the posterior should stay wide; half-life just makes that honesty visible in useful units.
  • Respect the truncation window. A half-life near or beyond l_max can't be expressed by the implemented kernel. Extend l_max, bound the prior, or both. Don't put prior mass where the kernel can't follow.
  • Check conventions. We treat α as retention (higher = slower decay); some APIs use a decay rate running the other way. And daily vs weekly data changes the numbers, not the argument.

One more practical note: as of August 2026, the big MMM tools all still hand you the raw coordinate. PyMC-Marketing's geometric adstock takes a direct α with a Beta(1,3) default, Robyn optimises a direct θ, and Meridian samples a direct α with a Uniform(0,1) default. None of them is wrong to do so, but it does mean the translation job lands on you. Before trusting any α prior, including a default, check what it secretly says about memory.

Make half-life the parameter

So: talk about adstock in half-lives. Set your priors in half-lives. Report your results in half-lives. Let α become a two-line implementation detail that feeds the same formula it always did. It costs almost nothing, and it turns “α is 0.94” into “the effect halves every eleven weeks”, a sentence everyone in the room can understand, challenge, and improve. That last part is where better models actually come from.

SIMBA builds on PyMC-Marketing for transparent Bayesian MMM. If you want to talk through adstock priors for your own channels, book a call.

Published on August 7, 2026 by Niall Oulton and Joe Wilkinson

All posts