Skip to content

Media budgets

Marketing-mix budget scheduling: which channel to spend on, and when, from confounded logs. Media spend is planned against demand — a marketer raises budget in the weeks a product sells anyway — so a model fitted on the log credits the channel with the season. Optimise against that model and the plan under-invests: crediting every channel with the season, it believes it needs less spend to reach its sales target than it does.

The plant is control-affine by construction, so identification and safety read the same object:

d(sales)/dt     = -decay*(sales - base) + sum_c beta_c * hill(adstock_c) + sum_c gamma_c * spend_c
d(adstock_c)/dt = -theta_c * adstock_c + spend_c

Each channel acts twice: gamma_c is the immediate incremental return and is the control channel; beta_c is the carried-over return through a saturating adstock and is part of the drift. Under a policy that chases seasonality it is gamma_c that is confounded, and gamma_c is exactly what cross-fit Robinson partialling-out identifies. The adstock rows are mechanical and go to prescribe as known=, so the fit has only the sales row to learn.

uv run python scripts/mmm_demo.py

What it printed when this site was built:

== weekly media log, planner chased the season; true incremental returns {'search': 0.5, 'social': 0.2, 'video': 0.35} ==

| arm | total spend | cumulative sales | lift | lift / extra spend |
|---|---|---|---|---|
| adjusted | 41.389 | 83.757 | +42.693 | +1.0784 |
| confounded | 35.499 | 78.206 | +37.142 | +1.1022 |
| flat | 41.389 | 82.032 | +40.968 | +1.0348 |
| myopic | 41.389 | 85.033 | +43.969 | +1.1106 |
| none | 1.800 | 41.064 | +0.000 | +nan |

what each arm believes each channel is worth (channel x box width):
channel             adjusted  confounded   inflation
----------------------------------------------------
spend_search           1.002       2.468       2.46x
spend_social           0.122       1.880      15.42x
spend_video            0.703       2.173       3.09x

At the same total budget (41.4) the prescribed schedule buys +4.2% more lift than an equal split (cumulative sales over the do-nothing arm), and the myopic rule on the same fit buys +7.3%. The myopic rule differs from the equal split only in how it splits each week's budget across the channels, so that gap is what identification buys; the gap between the prescribed schedule and the myopic rule is what the horizon buys, and its sign is a property of this plant.
The confounded arm credits every channel with the season, so it believes it needs less: it spends 35.5 and buys 87% of the lift.
Read at return-per-unit-spend it looks BETTER (1.102 against 1.078), because the response saturates and it under-invests. That is why the arms are compared at matched budget.

The arms are the same optimiser reading the same log through different causal claims, every one audited by rolling its schedule out on the true plant:

  • adjusted — the graph, so the season is adjusted for and the incremental returns are identified;
  • confounded — an empty adjustment set asserted, which is what fitting the log directly amounts to;
  • flat — an equal split held constant at the adjusted arm's total spend, so the head-to-head is at matched budget and differs only in allocation;
  • myopic — the identified fit spent on each week's return alone, at the same budget, which isolates what the plan's horizon buys from what identification buys;
  • none — spend held at the floor, the do-nothing counterfactual every lift is measured against.

Scope, and it bounds what the numbers mean:

  • The saturating plant is fitted with a polynomial drift, so the fitted plant is a local surrogate of the truth. That is why every arm is audited on the true plant, and the sales reported are the audited ones rather than the planner's own forecast.
  • Spend is priced through Lever.unit_cost rather than capped. prescribe takes a budget (budgets=), but the arms here are compared at matched spend instead: the flat and myopic arms are held at the adjusted arm's total spend.
  • The carryover rates theta_c are taken as known. In practice they are fitted (Robyn, Meridian); holding them fixed isolates the incremental return, which is what this case study is about.
  • What the whole-horizon plan buys over the myopic one is a property of the plant's parameters, not of the method. The myopic arm is there to measure it rather than assume it.

Code: chc.mmm, through prescribe.