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:

  1. Extracts posterior coefficient draws for the scenario’s channel terms.
  2. Generates feasible candidate allocations within channel bounds that sum to the total budget.
  3. Evaluates each candidate across all posterior draws to obtain a distribution of KPI outcomes.
  4. Ranks candidates by the configured objective and risk scoring function.
  5. Returns the best allocation, channel-level summaries, response curves, and impact breakdowns.

Entry point

result <- optimise_budget(model, scenario, n_candidates = 2000L, seed = 123L)

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. If kpi_baseline is 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:

  1. Respect per-channel lower bounds.
  2. Respect per-channel upper bounds.
  3. 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_updater models, optimise_budget() aborts if scale=TRUE and an offset is present.

Cross-references