Adstock is the part of a marketing mix model that lets advertising keep working after the week you paid for it: each week’s media pressure is that week’s spend plus a fraction of the pressure left over from the week before. It’s how the model represents the carryover effect of marketing, so a TV flight that ends on Friday can still be credited with some of next week’s sales instead of the model pretending the effect stopped dead.
I’ve used adstock in the marketing mix models I’ve built for companies like Monzo Bank and Booking.com, and it’s the transformation people find easiest to understand and hardest to pin down. The formula is one line long. However, two tools can use the same retention rate and give you different numbers, the shape you choose decides when the effect lands, and the data is often surprisingly bad at telling you what the decay rate should be. I’ll walk through the formula and its variants, then how to estimate the rate and how it interacts with the way you report results.
Where the idea of adstock came from
The term is usually credited to Simon Broadbent, a British advertising researcher at Leo Burnett in London, who introduced it in a 1979 paper in the Journal of the Market Research Society called “One Way TV Advertisements Work”. (Secondary sources disagree about the volume number, so I’d check a library catalog before citing it formally.) The idea was that advertising builds up a stock of awareness or goodwill, a bit like a reservoir, and that stock leaks away at a steady rate when you stop topping it up. Broadbent also popularised talking about that leak in terms of a half-life, the number of weeks it takes for the effect to fall by half, which is still the most intuitive way to explain the number to a stakeholder.
What’s striking is how little the core idea has changed. The open-source tools people use today, Meta’s Robyn, Google’s Meridian and PyMC-Marketing, all ship a geometric adstock that is essentially Broadbent’s reservoir, plus one or two more flexible shapes for when the peak doesn’t arrive straight away. If you want the longer story of how adstock fits into the development of the field, the history of marketing mix modeling covers it.
The geometric adstock formula, and why two people get different numbers
The simplest and most common version is geometric adstock, where a fixed share of last period’s adstock survives into this period. Written out, with as spend (or impressions, or GRPs) in week and as the retention rate between 0 and 1:
That’s the unnormalized form, and it’s what Robyn’s adstock_geometric does. The catch is that it inflates the total: a single pulse of 100 becomes 100, 50, 25, 12.5 and so on at 50 percent retention, which sums to 200, or more generally times the spend. The other common convention normalizes the weights so the long-run total equals what you put in:
Here the same pulse becomes 50, 25, 12.5, 6.25, which sums to 100. Jin et al. (2017), Meridian and PyMC-Marketing (whose GeometricAdstock has normalize=True by default) take a third, closely related route: they compute a weighted average of the last periods with weights divided by their sum, which matches the normalized form once is long enough for the weights to die out. Meridian’s default maximum lag is 8 periods and PyMC-Marketing makes you choose one, while Jin et al. used 13 weeks.
Both conventions decay at exactly the same rate and have the same half-life. What changes is the scale of the transformed variable, and therefore the size of the coefficient the regression has to put on it. The course lesson on adstock is blunt about this: these are modeling conventions, not properties of the channel, so inspect the definition before you compare coefficients or totals across two examples. In practice the mistake I see most is someone taking a coefficient from one tool’s output and multiplying it by spend transformed with another tool’s convention, which can be off by a factor of two or more.
The chart makes the other point the convention hides. A higher retention rate starts lower (in the normalized version) and spreads the same total over more weeks, so more of it arrives after a short report has already been written. I’ll come back to that in the section on reporting windows.
Advertising half-life, the number people remember
Retention rates are hard to reason about; nobody has a feel for the difference between 0.7 and 0.8. Half-life is easier, and for geometric adstock the conversion is exact:
| Weekly retention θ | Half-life (weeks) | Share of the effect left after 4 weeks |
|---|---|---|
| 0.3 | 0.58 | 0.8% |
| 0.5 | 1.00 | 6.3% |
| 0.6 | 1.36 | 13.0% |
| 0.7 | 1.94 | 24.0% |
| 0.8 | 3.11 | 41.0% |
| 0.9 | 6.58 | 65.6% |
Two things are worth keeping in mind. First, the relationship is very non-linear at the top end: going from 0.8 to 0.9 doubles the half-life, so a search space that runs up to 0.95 is allowing for some very long tails. Second, retention depends on the time unit. A weekly retention of 0.7 is not a daily retention of 0.7; the daily equivalent is . If you move a model from weekly to daily data, or copy a prior from someone who used a different granularity, convert it first.
When the peak arrives later: delayed adstock
Geometric adstock assumes the effect peaks in the week the money is spent and then only falls. That’s reasonable for a lot of direct response media, but less so for something like a car or a mortgage, where an ad might start a consideration process that only turns into a sale weeks later. Jin et al. (2017), in the Google paper that a lot of today’s Bayesian MMM is built on, proposed a “delayed adstock” weight function to handle this:
Here is still a retention rate and is the lag at which the effect peaks. The weights are then normalized to sum to one and applied as a weighted average of the last weeks of spend. The paper points out that this is proportional to a normal density centered on the peak lag, so you can think of it as a bell curve of response spread around weeks after exposure, with controlling how wide it is. I’ve written up the paper itself on the Jin et al. (2017) page, including the identifiability problems it found with these parameters.
Weibull adstock in Robyn: CDF and PDF
Robyn offers two adstocks built from the Weibull distribution, which has a shape and a scale parameter and can bend into a lot of different decay patterns. The definitions here come from Robyn’s transformation.R, because the documentation describes them in words rather than equations. In both, Robyn first turns the scale hyperparameter (between 0 and 1) into a position inside your data window, the scale quantile of , where is the number of periods, and rounds it. A scale of 0.05 on two years of weekly data puts at about week six.
- Weibull CDF: the weight at lag 0 is 1, and each later weight is the previous one multiplied by , where is the Weibull cumulative distribution function. The decay rate changes over time (it can be slow at first and then fast, or the reverse), but the peak always stays in the spend week. Robyn recommends shape between 0.0001 and 2 and a conservative scale of 0 to 0.1.
- Weibull PDF: the weights are the Weibull density evaluated at lags 1 to , rescaled so the largest is 1. With a shape above 1 the peak moves later, which is why Robyn’s documentation strongly recommends it for products with a long conversion window. Its suggested shape range is 0.0001 to 10, or 2.0001 to 10 if you only care about strong lagged effects, and the docs warn it needs more iterations to converge.
Because the weights aren’t rescaled to sum to one, Weibull adstock in Robyn changes the scale of the variable as well as its timing, just like the unnormalized geometric version. The widget below rescales every kernel to sum to one so you’re only comparing timing. Type in your own flight, or use the presets, and watch where each shape puts the effect.
A few patterns are worth trying. With the default 4-week flight, the geometric shape front-loads the effect while delayed and Weibull PDF push the hump past the end of the flight, which is exactly the part of the response a short campaign report will miss. Switch to the “Always on” preset and the three shapes become almost indistinguishable after the first few weeks, because steady spend smooths over the differences in timing. That’s also why the data struggles to tell them apart for always-on channels: if spend barely varies, every reasonable shape fits about equally well.
What adstock rates look like by channel
I’d love to give you a table of reliable decay rates by channel, but I don’t know of a public source that has measured them in a way I’d stand behind. The most cited numbers come from Robyn’s documentation, which says that in weekly models TV has tended to land between 0.3 and 0.8, out-of-home, print and radio between 0.1 and 0.4, and digital between 0 and 0.3, and it explicitly labels this anecdotal advice. PyMC-Marketing’s default prior for the retention rate is a Beta(1, 3) distribution, which has a mean of 0.25 and leans towards short carryover unless the data argues otherwise.
| Channel group (weekly data) | Robyn’s anecdotal theta range | Half-life implied |
|---|---|---|
| TV | 0.3 – 0.8 | 0.6 – 3.1 weeks |
| Out-of-home, print, radio | 0.1 – 0.4 | 0.3 – 0.8 weeks |
| Digital | 0.0 – 0.3 | 0 – 0.6 weeks |
Treat that table as a sensible place to start a search, or the rough location of a prior, rather than a benchmark. Retention depends on the product’s purchase cycle, the creative, what the variable measures (spend, impressions and GRPs can decay differently once a flight is bought in advance) and how much of the long-term effect the model is even trying to capture. A brand campaign whose real effect is a slow lift in baseline demand may show a short adstock in a weekly model simply because the model has nowhere to put an effect that unfolds over years; that effect tends to end up in the baseline instead.
How to estimate adstock decay from your data
There are three ways to get a decay rate, and in my experience you want at least two of them to agree before you trust it.
Grid search and hyperparameter optimization
The oldest method is to transform the channel at a range of retention rates, refit the model each time and keep the one that fits best. Robyn does a much more sophisticated version of this, using the Nevergrad optimizer to search adstock and saturation hyperparameters jointly and keeping a set of models that trade off fit against how plausible the decomposition looks. The basic version is worth doing by hand once, because it shows you how flat the fit can be. One warning before you run it on the Vexpower practice dataset: that file is synthetic and exactly linear, with no noise and no carryover built in, so a grid search on the raw data will always pick a retention of zero with a near-perfect fit, and that says nothing about real video. The script shows that first, then plants a known carryover (retention 0.5 on video), adds noise, and runs the grid again:
for theta in np.round(np.arange(0.0, 0.91, 0.1), 1):
X = np.column_stack([
np.ones(len(df)),
np.sin(2 * np.pi * t / 52), np.cos(2 * np.pi * t / 52), # yearly season
df["promotion"], df["search_spend"], df["social_spend"],
geometric_adstock(video, theta),
])
coef, *_ = np.linalg.lstsq(X, y, rcond=None)
sse = np.sum((y - X @ coef) ** 2)On the planted data with the script’s default noise draw, the best fit came out at 0.3 rather than the true 0.5, and the sum of squared errors moved by only about 1 percent anywhere between 0 and 0.5. Meanwhile the video coefficient fell from 1.38 to 0.70 across that range, so the model was happy to trade a bigger immediate effect for a shorter tail. The total video contribution barely moved across those rates (between about 174,500 and 177,000), but all of them overstated the planted 151,000 by roughly 15 percent, so a stable answer isn’t necessarily a right one. Rerun it with ten different noise draws, as the last part of the script does, and the winning rate lands anywhere from 0.2 to 0.7. None of those numbers is a finding about video advertising; they’re what a flat likelihood looks like when you know the true answer, and it’s the reason I’d rather report a range of plausible decay rates than a single winner.
Priors
In a Bayesian MMM you don’t pick a single rate; you give the model a prior distribution for it and let the data update that belief. That’s the honest way to handle a flat likelihood like the one above: if the data can’t tell 0.2 from 0.5, the posterior will stay wide and you’ll see that, rather than a grid search confidently handing you 0.3. The risk is the reverse, where a strong prior quietly decides the answer and everyone forgets it was an assumption. Check how much the posterior moved away from the prior; if it barely moved, the decay rate in your results came from you, not the data.
Experiments
The most direct evidence comes from switching something off or on and watching what happens after it stops. A geo-lift test that keeps measuring the test and control regions for several weeks after the campaign ends gives you the tail directly, and a clean go-dark test does the same in reverse. Those results can then be used to calibrate the model, either as a prior or as a constraint. The practical point is to plan the post-period into the test from the start, because a test that stops measuring the day spend stops can’t tell you anything about carryover.
Adstock before saturation, or saturation before adstock?
Most MMMs put spend through two transformations: adstock for timing and a saturation curve for diminishing returns. The order matters more than people expect. Jin et al. discuss both options and prefer applying adstock first when spend in each week is small relative to the spend accumulated over several weeks, because then it’s the accumulated pressure that saturates. If spend is concentrated in single weeks with an on-and-off pattern, they note that saturating first may be the better choice, since a huge one-week burst hits diminishing returns within that week regardless of what came before.
Meridian applies adstock and then the Hill curve by default, with a hill_before_adstock option to reverse it (that option doesn’t apply to its reach and frequency channels, which have their own structure, covered on the reach and frequency page). Robyn’s documented formula feeds the adstocked variable into its Hill function, so it also goes adstock first. The order changes the shape of the response curve you’ll read off afterwards, so it’s worth asking which order a model used before you compare its curves with another model’s.
How adstock interacts with reporting windows
This is where adstock stops being a modeling detail and starts changing business decisions. If a campaign’s effect has a half-life of three weeks and the post-campaign report is written four weeks after launch, a good share of the response hasn’t arrived yet when someone decides whether the campaign worked. Set the reporting window to four weeks in the first widget and the retention to 0.8, and the report counts roughly 59 percent of the pulse; the other 41 percent arrives after it’s been judged. The longer the carryover, the more a short window penalizes the channel, and the more it favors channels whose response is nearly immediate.
The same mismatch shows up when you put MMM results next to platform attribution, which typically counts conversions inside a fixed click or view window, and it’s one of the reasons the two disagree (the comparison is covered in MMM vs multi-touch attribution). Neither number is wrong by itself; they’re counting different time spans. In my experience the useful move is to name the mismatch explicitly before anyone starts debating which return figure is right.
Before you accept the adstock assumptions in a model, whether you built it or someone handed it to you, these are the questions I’d want answered:
If you’d rather work through this with your own numbers, the Excel calculator below has all four shapes with live formulas, a half-life converter and a reporting-window readout, and it slots into the kind of spreadsheet MMM the earlier Vexpower course started with. The Vexpower MMM course has a lesson on adstock in its invented Northstar case, where a video campaign stops on a Friday and the question is how much of the next month’s sales it should get.
