Budget Optimisation
Use PanelBudgetOptimizerWrapper when you want to optimise spend for a fitted
PanelMMM over a future date window.
The wrapper builds a synthetic future dataset for the requested window, swaps
the model’s channel_data for an optimisation variable, and then calls the
generic BudgetOptimizer. If you want to compare several plans in total
horizon spend units, see Scenario Planning.
What the optimiser maximises
For PanelBudgetOptimizerWrapper, optimize_budget() defaults to:
response_variable="total_media_contribution_original_scale"utility_function=average_response- SciPy
SLSQPwithftol=1e-9andmaxiter=1000
The optimiser therefore maximises the average posterior response of the chosen response variable, subject to your budget bounds and constraints.
Budget units
The low-level wrapper uses per-period spend units.
budgetis the total spend across all optimised cells for one model period.- The returned allocation has no
datedimension, so Abacus repeats that allocation across the optimisation window. - If the window has
num_periods=8and you passbudget=100_000, the simulated spend over the full horizon is800_000before any carryover effects are applied.
This is different from Scenario Planning, which
treats total_budget and manual allocations as total horizon spend and
converts them to per-period units internally.
The Stage 70 pipeline optimisation uses the same units as the low-level
wrapper because it passes optimization.total_budget directly to
PanelBudgetOptimizerWrapper.optimize_budget(...).
Required inputs
| Input | What Abacus expects | Notes |
|---|---|---|
model |
A fitted PanelMMM with idata.posterior |
The optimiser needs posterior draws and model graph variables. |
start_date, end_date |
A future window at the model’s observed date frequency | Abacus infers num_periods from the training data frequency. |
budget |
Per-period total spend | See Budget units. |
response_variable |
A variable available from the fitted optimisation graph | The wrapper default is total_media_contribution_original_scale. |
Basic example
This example assumes that mmm is already fitted.
allocation is an xarray.DataArray over the non-date budget dimensions.
For a model with dims=("geo",), the result dims are typically
("geo", "channel").
Bounds and masks
budget_bounds
Use budget_bounds to cap spend for each optimised cell.
- If the budget has only one non-date dimension, you can pass a dict such as
{"tv": (0.0, 50_000.0), "search": (0.0, 30_000.0)}. - For panel budgets, pass an
xarray.DataArraywith dims(*budget_dims, "bound"), where"bound"contains"lower"and"upper". - If you omit
budget_bounds, Abacus warns and uses(0, total_budget)for every optimised cell. - Abacus reindexes
DataArraybounds to the model’s internal coordinate order, so the input coordinate order does not need to match exactly.
budgets_to_optimize
Use budgets_to_optimize to choose which cells can move.
- The mask must be a boolean
xarray.DataArrayover the budget dimensions. - Unoptimised cells are fixed at zero in the returned allocation.
- If you omit the mask, Abacus optimises every cell where the fitted model has
non-zero historical
channel_contributioninformation. - If your mask includes
Truefor a cell where the model has no information, Abacus raisesValueError.
Time distribution across the window
Use budget_distribution_over_period to flight each allocation cell over time
instead of repeating the same spend every period.
The object must be an xarray.DataArray with:
- dims exactly
("date", *budget_dims) - one date weight per optimisation period
- weights that sum to
1across thedatedimension for every budget cell
Example for a two-geo, two-channel weekly window:
Use the same budget_distribution_over_period again when you call
sample_response_distribution(),
otherwise you will optimise one spend path and simulate another.
For response simulation through the wrapper, the date coordinates can be:
- integer positions
0 .. num_periods - 1, or - exact dates that match the optimisation window
Constraints and solver controls
default_constraints=True adds the default equality constraint:
This is enabled by default and emits a warning so you can see that the default constraint set is active.
You can also pass:
- extra SciPy minimise keyword arguments directly to
optimize_budget(...)to tweak the underlying solver call callback=Trueto get a third return value with per-iteration objective, gradient, and constraint diagnostics
YAML note for the pipeline runner
If you run optimisation through the structured pipeline, configure the
optimization block in YAML:
In this pipeline path, optimization.total_budget uses the wrapper contract
described above: it is passed straight to optimize_budget(...) as
per-period spend, not total horizon spend.
Common pitfalls
- Passing a total horizon budget to
optimize_budget(...). Divide bywrapper.num_periodsfirst, or use Scenario Planning. - Passing dict bounds for a panel budget. Dict bounds only work when the budget
dims are just
("channel",). - Omitting a budget dimension from
budget_distribution_over_period. The distribution must include every budget dim, not just the one you want to vary. - Forgetting that
response_variablemust exist in the fitted optimisation graph. - Using one budget distribution for optimisation and a different one for response simulation.