Chapter 10 of 10

Budget allocation

Reallocation against the stored posterior, reporting the probability a plan wins rather than a single optimistic number.

Given a budget and per-channel bounds, find the allocation that maximizes expected incremental outcome under the posterior:

maximize   (1/S) * sum_s sum_m f_m(x_m ; theta_s)
subject to sum_m x_m <= B
           lower_m <= x_m <= upper_m

The objective is evaluated per draw and averaged last. This is not a performance detail: the response is nonlinear in the parameters, so optimizing at the posterior mean is a different problem with a different answer. Averaging the curve is not the same as the curve through the average.

Reporting a plan, not a number

A single expected lift is the wrong output. The optimizer also reports the share of posterior draws under which the proposed plan actually beats current spend — because a 12% expected gain that holds in 55% of draws is a completely different recommendation from one that holds in 99%, and reporting only the mean makes them look identical.

Bounds default to 0.7× to 1.3× of observed spend. That is not timidity: outside the historical support the saturation curve is extrapolation, and an optimizer allowed to propose a channel’s spend at 5× its observed maximum is reading a region of the curve the data never visited.

source · engine/mmm/optimize.py

Two-stage solver

A greedy marginal-return hill-climb runs first on a discrete spend grid. For a separable objective with concave per-channel terms, always handing the next budget increment to the channel with the highest marginal return is globally optimal — this is not a heuristic under those conditions.

SLSQP then polishes with analytic gradients from JAX autodiff. The polish matters because the grid is discrete; the greedy stage matters because SLSQP is a local method and needs a starting point in the right basin. When the risk term is active the objective stops being separable, so the greedy pass runs at kappa = 0 and SLSQP carries the risk term forward from there.

greedy   marginal-return allocation over 81 grid points
polish   SLSQP · maxiter 200 · ftol 1e-9 · JAX gradients
guard    revert to greedy if the polish is worse or infeasible

At the optimum, channels not pinned to a bound have equal marginal ROI. That common value is the shadow price of the budget constraint — the return on the next dollar of total budget — and it is the single most useful number the optimizer produces.

The optimizer inherits every assumption above it

It reallocates under the fitted response curves, so it is only as good as the identification in chapter 04 and the flighting assumption in chapter 02. It holds each channel’s temporal pattern fixed and scales it, so it cannot tell you to re-flight rather than re-budget. And it optimizes a single period’s outcome under carryover estimated from history — a plan that changes spend far outside the observed pattern is outside what the model was ever asked to fit.