Budget Optimisation
Purpose
DSAMbayes provides a decision-layer budget optimisation engine that operates on fitted model posteriors. Given a channel scenario with spend bounds, response-transform specifications, and an objective function, the engine searches for the allocation that maximises the chosen objective while respecting channel-level constraints. This page defines the inputs, objectives, risk scoring, response-scale handling, and output structure.
Overview
Budget optimisation is separate from parameter estimation. It takes a fitted model and a scenario specification, then:
- Extracts posterior coefficient draws for the scenario’s channel terms.
- Generates feasible candidate allocations within channel bounds that sum to the total budget.
- Evaluates each candidate across all posterior draws to obtain a distribution of KPI outcomes.
- Ranks candidates by the configured objective and risk scoring function.
- Returns the best allocation, channel-level summaries, response curves, and impact breakdowns.
Entry point
The optimize_budget() alias is also available for American English convention.
Scenario specification
The scenario is a structured list with the following top-level keys:
channels
A list of channel definitions, each containing:
| Key | Required | Default | Description |
|---|---|---|---|
term |
Yes | — | Model formula term name for this channel |
name |
No | Same as term |
Human-readable channel label |
spend_col |
No | Same as name |
Data column used for reference spend lookup |
bounds.min |
No | 0 |
Minimum allowed spend for this channel |
bounds.max |
No | Inf |
Maximum allowed spend for this channel |
response |
No | {type: "identity"} |
Response transform specification |
currency_col |
No | null |
Data column for currency-unit conversion |
Channel names and terms must be unique across the scenario.
budget_total
Total budget to allocate across all channels. All feasible allocations sum to this value.
reference_spend
Optional named list of per-channel reference spend values. If not provided, reference spend is estimated from the mean of the spend_col in the model’s original data.
objective
Defines the optimisation target and risk scoring:
| Key | Values | Description |
|---|---|---|
target |
kpi_uplift, profit |
What to maximise |
value_per_kpi |
numeric (required for profit) |
Currency value of one KPI unit |
risk.type |
mean, mean_minus_sd, quantile |
Risk scoring function |
risk.lambda |
numeric ≥ 0 (for mean_minus_sd) |
Penalty weight on posterior standard deviation |
risk.quantile |
(0, 1) (for quantile) |
Quantile level for pessimistic scoring |
Response transforms
Each channel can specify a response transform that maps raw spend to the transformed value used in the linear predictor. Supported types:
| Type | Formula | Parameters |
|---|---|---|
identity |
spend |
None |
atan |
atan(spend / scale) |
scale (positive scalar) |
log1p |
log(1 + spend / scale) |
scale (positive scalar) |
hill |
spend^n / (spend^n + k^n) |
k (half-saturation), n (shape) |
The response transform is applied within response_transform_value() and determines the shape of the channel’s response curve.
Objective functions
kpi_uplift
Maximises the expected change in KPI relative to the reference allocation. The metric for each candidate is:
$$\Delta\text{KPI}_d = f(\text{candidate}) - f(\text{reference})$$evaluated across posterior draws $d$.
profit
Maximises expected profit, defined as:
$$\text{profit}_d = \text{value\_per\_kpi} \times \Delta\text{KPI}_d - \Delta\text{spend}$$where $\Delta\text{spend} = \text{candidate total} - \text{reference total}$.
Risk-aware scoring
The risk scoring function determines how the distribution of objective draws is summarised into a single score for ranking candidates:
| Risk type | Score formula | Use case |
|---|---|---|
mean |
$\bar{m}$ | Risk-neutral; maximises expected value |
mean_minus_sd |
$\bar{m} - \lambda \cdot \sigma$ | Penalises uncertainty; higher $\lambda$ is more conservative |
quantile |
$Q_\alpha(m)$ | Optimises the $\alpha$-quantile; directly targets worst-case outcomes |
Coefficient extraction
BLM and pooled models
Coefficient draws are extracted via get_posterior() and indexed by the scenario’s channel terms.
Hierarchical models
For hierarchical MCMC models, the population-level (fixed-effect) beta draws are extracted directly from the Stan posterior. If the model was fitted with scale=TRUE, draws are back-transformed to the original scale before optimisation. This ensures that optimisation operates on the population effect rather than group-specific random-effect totals.
Draw thinning
If max_draws is specified, a random subsample of posterior draws is used for computational efficiency. The subsampling uses the configured seed for reproducibility.
Response-scale handling
Budget optimisation handles both identity and log response scales:
- Identity response: $\Delta\text{KPI}$ is the difference in linear-predictor draws between candidate and reference allocations.
- Log response: $\Delta\text{KPI}$ is computed via
kpi_delta_from_link_levels(), which correctly accounts for the exponential back-transformation. Ifkpi_baselineis available, the delta is expressed in absolute KPI units; otherwise, it is expressed as a relative change.
The delta_kpi_from_link() and kpi_delta_from_link_levels() functions ensure Jensen-safe conversions by operating draw-wise.
Feasible allocation generation
sample_feasible_allocation() generates random allocations that:
- Respect per-channel lower bounds.
- Respect per-channel upper bounds.
- Sum exactly to
budget_total.
Allocation is performed by distributing remaining budget (after lower bounds) using exponential random weights, iteratively filling channels until the budget is exhausted. project_to_budget() ensures exact budget equality via proportional adjustment.
Output structure
optimise_budget() returns a budget_optimisation object containing:
| Field | Content |
|---|---|
best_spend |
Named numeric vector of optimal per-channel spend |
best_score |
Objective score of the best allocation |
channel_summary |
Tibble with per-channel reference vs optimised spend, response, ROI, CPA, and deltas |
curves |
List of per-channel response curve tibbles (spend grid × mean/lower/p50/upper) |
points |
Tibble of reference and optimised points per channel with confidence intervals |
impact |
Waterfall-style tibble of per-channel KPI contribution and interaction residual |
objective_cfg |
Echo of the objective configuration |
scenario |
Echo of the input scenario |
model_metadata |
Model class, response scale, and scale flag |
Runner integration
When allocation.enabled: true in YAML, the runner calls optimise_budget() after fitting and writes artefacts under 60_optimisation/:
| Artefact | Content |
|---|---|
allocation_summary.csv |
Channel summary table |
response_curves.csv |
Response curve data for all channels |
allocation_impact.csv |
Waterfall impact breakdown |
| Plot PNGs | Response curves, ROI/CPA panel, allocation waterfall, and other visual outputs |
Constraints and guardrails
- Budget feasibility: if channel lower bounds sum to more than
budget_total, the engine aborts. - Upper bound capacity: if channel upper bounds cannot accommodate the full budget, the engine aborts.
- Missing terms: if a scenario term is not found in the posterior coefficients, the engine aborts with a descriptive error.
- Offset + scale combination: for
bayes_lm_updatermodels,optimise_budget()aborts ifscale=TRUEand an offset is present.
Cross-references
- Model Classes — fit support and posterior extraction per class
- Response Scale Semantics — KPI-scale conversion for log models
- Priors and Boundaries — boundary interaction with optimisation constraints
- Config Schema —
allocation.*YAML keys - Optimisation Plots — visual outputs from budget optimisation