Adstock: how advertising keeps working after you stop paying

Adstock is how a marketing mix model lets this week's ads keep working next week. The idea is simple; the conventions, the shapes and the estimation are where people trip up.

Mike Taylor By Mike Taylor · Updated · 13 min read

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 xtx_t as spend (or impressions, or GRPs) in week tt and θ\theta as the retention rate between 0 and 1:

At=xt+θ At−1A_t = x_t + \theta \, A_{t-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 1/(1−θ)1/(1-\theta) times the spend. The other common convention normalizes the weights so the long-run total equals what you put in:

At=(1−θ) xt+θ At−1A_t = (1-\theta)\, x_t + \theta \, A_{t-1}

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 LL periods with weights θl\theta^l divided by their sum, which matches the normalized form once LL 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.

Interactive
Follow one £100k pulse through carryover

Change how much of last week's effect survives into this week, and how long your report waits before it counts.

Half-life
1.0 wk
Counted inside window
94%
Arrives after report
6%
Geometric adstock on one pulse of 100 in week 1. Normalised exposure multiplies the pulse by (1 − retention) so the infinite-horizon total stays at 100. Invented numbers, for intuition only.

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:

half-life=ln⁡0.5ln⁡θθ=0.51/half-life\text{half-life} = \frac{\ln 0.5}{\ln \theta} \qquad\qquad \theta = 0.5^{1/\text{half-life}}
Weekly retention θHalf-life (weeks)Share of the effect left after 4 weeks
0.30.580.8%
0.51.006.3%
0.61.3613.0%
0.71.9424.0%
0.83.1141.0%
0.96.5865.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 0.71/7≈0.950.7^{1/7} \approx 0.95. 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:

w(l)=α(l−θ)2,l=0,…,L−1w(l) = \alpha^{(l-\theta)^2}, \qquad l = 0, \dots, L-1

Here α\alpha is still a retention rate and θ\theta 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 LL 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 θ\theta weeks after exposure, with α\alpha 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, λ=\lambda = the scale quantile of 1,…,T1, \dots, T, where TT is the number of periods, and rounds it. A scale of 0.05 on two years of weekly data puts λ\lambda at about week six.

  • Weibull CDF: the weight at lag 0 is 1, and each later weight is the previous one multiplied by 1−F(l)1 - F(l), where FF 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 TT, 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.

Interactive
Compare adstock shapes on your own flight

Type a weekly spend pattern (or pick one), then tune each shape. Every kernel is scaled to share out the same total, so you're only comparing when the effect lands.

Presets
Geometric
Delayed
Weibull

What one week of spend turns into: share of the total effect landing at each lag

Your spend pattern after each transformation (same total, different timing)

ShapePeak lagHalf landed by lag90% landed by lagEffect after last spend week

Lag weights rescaled to sum to 1 over the window. Geometric: θ^lag. Delayed: α^((lag − peak)²) with a 13-week maximum lag, as in Jin et al. (2017). Weibull: Robyn's CDF and PDF kernels on a 52-week window, with scale unrounded. Spend is whatever you type; the default is an invented 4-week flight of 100 a week.

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 rangeHalf-life implied
TV0.3 – 0.80.6 – 3.1 weeks
Out-of-home, print, radio0.1 – 0.40.3 – 0.8 weeks
Digital0.0 – 0.30 – 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.

py
Adstock grid search (Python)Runs on the practice CSV. Shows that the raw (noiseless, no-carryover) data always picks zero, then plants a retention of 0.5, adds noise and shows the fit, coefficient and contribution at each theta across ten draws.
Download

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.

xlsx
Adstock calculator (Excel)Geometric (both conventions), delayed and Weibull CDF/PDF adstock with live formulas, half-life helpers and charts. Edit the yellow cells.
Download

Downloads

xlsx
Adstock calculator (Excel)Geometric (both conventions), delayed and Weibull CDF/PDF adstock on your own spend, with live formulas, half-life helpers and charts. No macros.
Download
py
Adstock grid search (Python)Shows the raw practice data picks zero carryover, then plants a known one and tests how well a grid search recovers it.
Download
csv
Practice dataset (CSV)104 weeks of synthetic revenue, three channels and a promotion flag, used by the grid search.
Download

Frequently asked questions

What is adstock in marketing?

Adstock is a transformation used in marketing mix modeling to represent the lingering effect of advertising. Instead of assuming an ad only affects sales in the week it runs, the model carries a fraction of each week's media pressure into the following weeks, so a campaign that stops on Friday can still be credited for sales the week after.

What is the formula for geometric adstock?

The common unnormalized form is A_t = x_t + theta times A_(t-1), where x_t is this week's spend and theta is the retention rate between 0 and 1. A normalized version multiplies this week's spend by (1 - theta) first, so the long-run total equals the spend. Both decay at the same rate, but they produce different numbers, so check which one a tool uses before comparing results.

What is a good adstock rate?

There isn't a universal one. Robyn's documentation offers anecdotal ranges for weekly models of 0.3 to 0.8 for TV, 0.1 to 0.4 for out-of-home, print and radio, and 0 to 0.3 for digital, and it says to use your own judgment. Treat those as a starting range for a search or a prior, then estimate the rate from your data and, ideally, an experiment.

How do you calculate advertising half-life from an adstock rate?

For geometric adstock the half-life in periods is ln(0.5) divided by ln(theta). A weekly retention of 0.5 gives a half-life of one week, 0.7 gives about 1.9 weeks and 0.9 gives about 6.6 weeks. To go the other way, theta equals 0.5 raised to the power of one over the half-life.

What is the difference between Weibull CDF and Weibull PDF adstock?

Both are options in Meta's Robyn. Weibull CDF adstock lets the decay rate change over time but still puts the biggest effect in the week of spend. Weibull PDF adstock can put the peak several periods later, which suits products with a long consideration window, but it is more flexible and takes more iterations to converge.

Should adstock be applied before or after saturation?

Most tools apply adstock first and then the saturation curve, which Jin et al. (2017) recommend when weekly spend is small relative to the spend accumulated across weeks. If spend is concentrated in short on-and-off bursts, saturating first can make more sense. Meridian applies adstock first by default and lets you reverse the order.

References

  1. Jin, Wang, Sun, Chan and Koehler (2017), Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects Normalised adstock over a maximum lag L (13 weeks in their examples), geometric and delayed weight functions, and the discussion of adstock-then-saturation order.
  2. Robyn documentation: Features (adstock and saturation) Geometric, Weibull CDF and Weibull PDF options, recommended hyperparameter bounds, and the anecdotal theta ranges by channel.
  3. Robyn source: transformation.R Exact definitions of adstock_geometric and adstock_weibull, including how scale becomes a position in the data window.
  4. Meridian documentation: model specification Normalised Adstock and Hill definitions and the hill_before_adstock option.
  5. Meridian API reference: ModelSpec max_lag default of 8, geometric and binomial decay, adstock-then-Hill by default.
  6. PyMC-Marketing API: GeometricAdstock l_max required, normalize defaults to True, default alpha prior Beta(1, 3).
  7. Wikipedia: Advertising adstock Secondary source for Simon Broadbent coining the term in 1979; journal volume is cited inconsistently, so it's left out here.
Mike Taylor
Mike Taylor

I've built marketing mix models for Monzo Bank, Booking.com, Time Out, The AA and others, advised Google, Appsflyer and Recast on MMM, and teach marketing mix modeling on LinkedIn Learning. Before that I co-founded a 50-person growth marketing agency. I write the Vexpower MMM course.

Keep reading

Learn MMM by doing itThe Vexpower course walks through thirty lessons on one invented brand, with simulators, Python labs and a decision case you can defend.
See the course