Skip to content
Was It Causal?

Adstock, Saturation, and Honest Response Curves

Lagged effects and diminishing returns are where most mix models smuggle in their conclusions. How to tell a fitted curve from an identified one, and what variation would identify it.

Two numbers per channel decide what a mix model believes about the future. How long an impression keeps working (the adstock decay), and how quickly returns flatten (the saturation point).

They arrive in the output looking like findings, with two decimal places. This asks how much the data behind them constrains them.

The method

Take one channel, hold every other channel at its fitted values, and sweep that channel’s decay and saturation across a grid of 180 combinations, solving the linear coefficients exactly at each one. Any combination whose fit sits within a whisker of the best is a combination the data cannot rule out.

Then ask what each surviving curve implies about the marginal return, the quantity a media plan is built from.

For the channel below, the model reported a decay of 0.05 and a saturation point of $4,611. The values that generated the data were 0.45 and $22,000.

How many curves survive

That would be an academic complaint if the survivors agreed about anything useful. The interesting question is where they disagree.

  • The curve that generated the data
  • Best fit (decay 0.05)
  • Also fits within 1% (decay 0.55)
0 100 200 300 400 500 600 700 800 0 6k 13k 19k 25k 32k 38k 44k 51k Weekly conversions The curve that generated the data, 0: 0.0 The curve that generated the data, 3k: 79.9 The curve that generated the data, 6k: 132.4 The curve that generated the data, 10k: 169.4 The curve that generated the data, 13k: 197.0 The curve that generated the data, 16k: 218.3 The curve that generated the data, 19k: 235.3 The curve that generated the data, 22k: 249.1 The curve that generated the data, 25k: 260.6 The curve that generated the data, 29k: 270.3 The curve that generated the data, 32k: 278.6 The curve that generated the data, 35k: 285.8 The curve that generated the data, 38k: 292.1 The curve that generated the data, 41k: 297.6 The curve that generated the data, 44k: 302.5 The curve that generated the data, 48k: 306.9 The curve that generated the data, 51k: 310.8 Best fit (decay 0.05), 0: 0.0 Best fit (decay 0.05), 3k: 366.9 Best fit (decay 0.05), 6k: 492.1 Best fit (decay 0.05), 10k: 555.3 Best fit (decay 0.05), 13k: 593.4 Best fit (decay 0.05), 16k: 618.8 Best fit (decay 0.05), 19k: 637.1 Best fit (decay 0.05), 22k: 650.8 Best fit (decay 0.05), 25k: 661.4 Best fit (decay 0.05), 29k: 670.0 Best fit (decay 0.05), 32k: 677.0 Best fit (decay 0.05), 35k: 682.8 Best fit (decay 0.05), 38k: 687.7 Best fit (decay 0.05), 41k: 691.9 Best fit (decay 0.05), 44k: 695.6 Best fit (decay 0.05), 48k: 698.8 Best fit (decay 0.05), 51k: 701.7 Also fits within 1% (decay 0.55), 0: 0.0 Also fits within 1% (decay 0.55), 3k: 65.2 Also fits within 1% (decay 0.55), 6k: 116.3 Also fits within 1% (decay 0.55), 10k: 157.3 Also fits within 1% (decay 0.55), 13k: 191.0 Also fits within 1% (decay 0.55), 16k: 219.2 Also fits within 1% (decay 0.55), 19k: 243.1 Also fits within 1% (decay 0.55), 22k: 263.6 Also fits within 1% (decay 0.55), 25k: 281.5 Also fits within 1% (decay 0.55), 29k: 297.1 Also fits within 1% (decay 0.55), 32k: 310.9 Also fits within 1% (decay 0.55), 35k: 323.2 Also fits within 1% (decay 0.55), 38k: 334.2 Also fits within 1% (decay 0.55), 41k: 344.2 Also fits within 1% (decay 0.55), 44k: 353.2 Also fits within 1% (decay 0.55), 48k: 361.3 Also fits within 1% (decay 0.55), 51k: 368.8 Weekly spend
Show the data
Weekly spend The curve that generated the data Best fit (decay 0.05) Also fits within 1% (decay 0.55)
0 0.0 0.0 0.0
3k 79.9 366.9 65.2
6k 132.4 492.1 116.3
10k 169.4 555.3 157.3
13k 197.0 593.4 191.0
16k 218.3 618.8 219.2
19k 235.3 637.1 243.1
22k 249.1 650.8 263.6
25k 260.6 661.4 281.5
29k 270.3 670.0 297.1
32k 278.6 677.0 310.9
35k 285.8 682.8 323.2
38k 292.1 687.7 334.2
41k 297.6 691.9 344.2
44k 302.5 695.6 353.2
48k 306.9 698.8 361.3
51k 310.8 701.7 368.8
Two curves that fit the same three years within 1% of each other, against the one that generated the data. The better-fitting of the two has a decay of 0.05 against a true 0.45, and the curve that fits fractionally worse is much closer to the truth on both carryover and level while overstating what extra spend would buy. Fit does not pick the right one. Simulated data · scripts/curve_identification.py

Neither fitted curve is right, and they are wrong in different ways. The best-fitting one puts almost no weight on carryover and sits at roughly two and a half times the true level across the whole range. That is the contribution overstatement showing up as a shape. The curve that fits a fraction of a percent worse gets carryover nearly right and tracks the true level closely, then keeps climbing where the true curve has flattened, so it promises more from extra spend than is there.

Choosing between them on fit picks the first, and picking on fit is what a mix model does.

The spread is tightest where you have already spent

Across the surviving set, the implied marginal return varies least at the spend the channel ran, by a factor of 1.67, and more on either side of it: 2.01 at half that spend and 2.57 at double.

A mix model is asked what happens if this channel doubles. That is the question its own fit constrains least, and the reason is not subtle: the curve was fitted on spend that mostly sat near one level, so its shape away from that level is the functional form talking rather than the data.

Adstock and saturation are not separately identified

The two parameters trade off against each other. A long carryover with an early saturation point produces almost the same fitted contribution as a short carryover with a late one, because both describe the same total effect arriving over the same weeks.

That is why the surviving set is a ridge rather than a blob, and it is why reporting the pair as two independent findings is misleading even when the contribution total is roughly right. The tightest set here, within a quarter of a percent of the best fit, does not even contain the true decay. It lands on 0.05 to 0.10 against a true 0.45, while implying a marginal return close to the right one. The best-fitting curve is wrong about the mechanism and roughly right about the slope where you already are.

What would identify it

Carryover is only visible when spending stops. A channel running at a steady level every week produces a series where a long decay and a short one imply nearly the same weekly contribution, so nothing in the data prefers either.

So the same channel was re-simulated with its spend flighted, 4 weeks on and 2 off, at the same annual total. Nothing about the budget changed. Only whether it ever stopped.

The surviving set falls from 53 curves to 8. The decay range narrows from 0.05 to 0.55, down to 0.40 to 0.45, against a true 0.45. The spread in marginal return at double current spend falls from 2.57 to 1.66.

That is the practical finding in this lesson. The identification problem in a mix model is usually described as something to model your way around, with a better prior or a more flexible curve. It is mostly a media planning problem. The variation that identifies these parameters is variation somebody has to decide to create, and flighting is cheap because it costs nothing in budget.

Saturation needs the other kind of variation: spend at different levels rather than the same level interrupted. Regional differences, a staggered launch, or a deliberate test market at double weight all produce it. Both kinds have to be planned, and neither shows up by waiting.

Context

Best suited for

Programs whose mix model is used to set budgets rather than to report contribution, and where somebody is about to act on a recommendation to move a channel well outside its historical range.

Data requirements

  • Spend history with genuine variation in level, not only in season, or a plan to create some
  • At least one period where a channel stopped, since carryover is invisible while spending is continuous
  • The ability to refit the model across a grid rather than accept one reported optimum
  • Experimental readings at two or more spend levels, if the curve shape is going to carry weight

What changes by context

B2C ecommerce
Promotional calendars create spend variation that is confounded with demand, so the variation that exists is the least useful kind. Flighting outside promotional windows is what separates them.
B2C subscription
Long consideration windows make carryover long, so the decay parameter matters more here and is harder to pin down. A longer off period is needed before the tail becomes visible at all.
Marketing-led
Media budgets are large enough that a flighting test is politically expensive and financially cheap, the wrong way round. The argument to make is that the same money is being spent either way.

When it breaks down

  • Channels that cannot be paused, such as always-on branded search where stopping has competitive consequences beyond the test.
  • Histories shorter than a couple of years, where there is not enough data to distinguish anything regardless of its shape.
  • Cases where the true response is not in the fitted family at all, so the identified set is a set of approximations rather than a credible range.
  • Programs where the model only reports contribution and never sets budgets, in which case the curve shape matters much less than the level.

What this does not establish

  • That fifty-three curves survive in your model. That count is a property of this grid, this tolerance, and three years of this particular spend history. The method transfers; the number does not.
  • That a 1% fit tolerance is the right threshold. It is a judgment, which is why three thresholds are reported. At a quarter of a percent the set is small and excludes the truth, and at five percent it is nearly the whole grid.
  • That flighting always identifies adstock this cleanly. It works here because the off periods are long enough for the tail to show and the channel is large enough to see. A short pause on a small channel identifies nothing.
  • That the true curve is in the surviving set. At the tightest tolerance here it is not, which is a warning about selecting curves on fit rather than a reason to trust a wider set.

What to test next

Before arguing about priors, run the grid. Refit your own model across a range of decay and saturation values for the largest channel, keep everything within a small fit tolerance of the best, and look at what those survivors disagree about at the spend level the plan is proposing. If they agree, the recommendation is robust and you can stop worrying. If they span a factor of two, you have found the real uncertainty in the plan, and it was never in the confidence interval the model reported.

Then plan the variation. Flighting a large channel for a few cycles a year costs nothing in budget and is the single cheapest thing that makes next year’s model better than this year’s. Spend-level variation across regions is the companion for saturation, and it is also what a two-level calibration experiment needs, so the two projects are the same project.

How the sweep was run Code

The same simulated advertiser and fitting code as the experiment-calibrated mix model, so this describes the same company as the rest of the track. One channel’s decay and saturation are swept across 180 combinations while the other channels stay at their fitted values, and the linear coefficients are re-solved by least squares at every point, so each combination gets the best fit available to it.

Marginal return is computed at steady-state adstock, the level a sustained weekly spend settles to, and reported per $1,000 a week. Curves are compared at half, one, one and a half, and double the channel’s historical weekly spend of $23,055.

The flighted variant replaces that channel’s weekly spend with a 4 on, 2 off pattern scaled to the identical annual total, regenerates outcomes from the true parameters, and repeats the whole procedure. These are not measurements from a real company.