Optimise a budget
Use the Python optimiser for an explicit objective, horizon and set of feasible
allocations. The runner currently has no validated optimisation YAML block:
its 70_optimisation directory is reserved and that stage is skipped. Use
manual scenario recipes when you need a retained
configuration-driven comparison without a solver.
Monetary-channel example
This continues the fitted ordinary MMM from the Python quickstart and assumes
its tv and social inputs are monetary spend. The two-period window starts
immediately after training. total_budget and the returned allocation are
per-period amounts; the window total is twice that amount in this example.
import pandas as pd
start = X["date"].max() + pd.Timedelta(weeks=1)
end = start + pd.Timedelta(weeks=1)
budget_per_period = 80.0
optimizer = model.budget_optimizer(start_date=start, end_date=end)
result = optimizer.allocate_budget(
total_budget=budget_per_period,
budget_bounds={"tv": (10.0, 70.0), "social": (10.0, 70.0)},
callback=True,
)
print(result.budgets)
print(result.scipy_result.success, result.scipy_result.message)
samples = model.sample_response_distribution(
allocation_strategy=result.budgets, start_date=start, end_date=end,
noise_level=0.0, include_last_observations=True, include_carryover=True,
include_observation=False,
)
allocation_table = optimizer.summary.allocation_roas(samples=samples)
BudgetOptimizationResult exposes budgets, scipy_result, optimized_vars,
spend_var_allocations and optional callback diagnostics. Two-value unpacking
also yields budgets and the SciPy result. noise_level=0.0 prevents perturbing
the evaluation allocation. Keep factual carry-in and trailing carryover consistent
between optimisation and evaluation, and score the same response variable.
For non-monetary channels, supply the optimiser’s cost-per-unit conversion and verify the evaluation inputs on the same units and dates. Do not assume this monetary-channel snippet handles impressions merely by renaming the columns.
Constraints and additional variables
Default bounds are zero to total budget for each optimised cell. A
budgets_to_optimize mask excludes cells by fixing their budgets to zero; it
does not preserve historical spend automatically. Labelled bounds require
(*budget_dims, "bound") coordinates. Dict bounds are for the one-dimensional
channel case.
The low-level optimiser can include monetary spend_vars and non-monetary
optimizable_vars levers. Both can appear in optimized_vars, but only monetary
variables belong in a spend total. Include spend_var_allocations when checking
budget conservation. Custom Constraint objects can replace or extend the
budget rule; verify which constraints are active. Declare meaningful bounds for
levers in their own units.
Objective and decision evidence
The default utility averages the configured response over posterior draws.
Under a log link, the default media response is a conditional-median contrast,
not expected revenue. Media-dependent custom effects can carry response outside
that default variable; an explicit objective such as
total_response_original_scale may be required. Inspect its graph definition
and units before using it.
The default SLSQP solver is local. Compare multiple feasible starts, solver
status, objective values and independently calculated constraint residuals.
Returning a failed result with return_if_fail=True does not make it feasible.
Agreement across starts is useful numerical evidence, not proof of global
optimality.
Before acting, review sampling precision for the decision contrast, held-out performance, causal assumptions, spend support and operational limits. Evaluate posterior differences against the current feasible plan, including downside risk, rather than optimising an attractive point estimate alone. Retain the model identity, objective, inputs, constraints and decision rationale. FE and CRE reject fixed-budget optimisation under their current contracts.
Independently check allocations and compare starts
This continues the monetary-channel example above, using only channel budgets,
the default total-budget constraint and its stated bounds. It checks feasibility
without relying on the solver’s success flag, then compares three feasible
starts using the same optimiser objective. Additional spend variables or custom
constraints require their own checks (src/ammm/mmm/budget_optimizer.py:704).
import numpy as np
import pandas as pd
import xarray as xr
bounds = {"tv": (10.0, 70.0), "social": (10.0, 70.0)}
channels = list(model.channel_columns)
records = []
for tv_budget in (20.0, 40.0, 60.0):
initial = xr.DataArray(
[tv_budget if c == "tv" else budget_per_period - tv_budget for c in channels],
dims="channel", coords={"channel": channels},
)
candidate = optimizer.allocate_budget(
total_budget=budget_per_period, budget_bounds=bounds, x0=initial,
)
amounts = candidate.budgets.sel(channel=channels)
assert bool(np.isfinite(amounts).all())
assert np.isclose(float(amounts.sum()), budget_per_period, rtol=0, atol=1e-6)
for channel, (lower, upper) in bounds.items():
amount = float(amounts.sel(channel=channel))
assert lower - 1e-6 <= amount <= upper + 1e-6
records.append({
"initial_tv": tv_budget,
"solver_success": bool(candidate.scipy_result.success),
"minimised_objective": float(candidate.scipy_result.fun),
"tv": float(amounts.sel(channel="tv")),
"social": float(amounts.sel(channel="social")),
})
comparison = pd.DataFrame(records)
print(comparison)
The numerical objective is the minimised solver objective, so preserve its sign
and utility definition rather than relabelling it as revenue. Similar feasible
solutions provide evidence about local numerical stability only; use paired
posterior scenario contrasts against the current plan for decision uncertainty.
Retain the complete constraint set, tolerances and any failed starts, and inspect
extrapolation before using an apparently better allocation
(src/ammm/mmm/budget_optimizer.py:431).
Implementation reference at 7cb7f20: src/ammm/mmm/budget_optimizer.py:704, src/ammm/mmm/mmm.py:2168, src/ammm/mmm/_budget_optimizer_results.py:35.