Scenario Specifications

This page documents the public spec classes under abacus.scenarios.

abacus.scenario_planner still re-exports these classes for compatibility with existing imports, but new code should use abacus.scenarios.

Most users create one of the three concrete scenario specs:

  • CurrentScenarioSpec
  • ManualAllocationScenarioSpec
  • FixedBudgetOptimizedScenarioSpec

Abacus also exposes shared base models such as HistoricalReferenceScenarioSpec and SimulatedScenarioSpec, but you do not normally instantiate those directly.

The named FE and CRE presets support historical and manual scenarios for the complete set of fitted units; CRE also retains its frozen fitted summaries. They do not support FixedBudgetOptimizedScenarioSpec. The RE preset remains release-gated. Check the estimator support matrix before choosing a scenario type.

Shared fields

All public scenario specs inherit these core fields:

Field Meaning
name Display name for the scenario
start_date Requested scenario start date
end_date Requested scenario end date
scenario_id Stable scenario key used in outputs

If you do not set scenario_id, Abacus derives one by slugifying name.

Scenario IDs must be unique within one ScenarioPlanner.compare(...) call.

CurrentScenarioSpec

Use CurrentScenarioSpec for a historical reference plan.

from abacus.scenarios import CurrentScenarioSpec

spec = CurrentScenarioSpec(
    name="Current baseline",
    start_date="2025-01-06",
    end_date="2025-02-24",
)

Requirements:

  • the requested window must overlap observed data
  • no allocation or budget inputs are needed

Shared simulated-scenario fields

ManualAllocationScenarioSpec and FixedBudgetOptimizedScenarioSpec both inherit these fields:

Field Default Meaning
budget_distribution_over_period None Optional time distribution of the total budget
include_last_observations False Passed through to response sampling for lag context
include_carryover True Extend the evaluated window to capture lagged effects
noise_level 0.001 Response-sampling noise level

Set noise_level=0.0 when you want deterministic realised spend paths.

ManualAllocationScenarioSpec

Use ManualAllocationScenarioSpec when you already know the total allocation you want to simulate.

from abacus.scenarios import ManualAllocationScenarioSpec

spec = ManualAllocationScenarioSpec(
    name="Manual reallocation",
    start_date="2025-03-03",
    end_date="2025-03-24",
    noise_level=0.0,
    include_carryover=False,
    allocation={
        "channel_1": 420_000.0,
        "channel_2": 280_000.0,
        "channel_3": 200_000.0,
    },
)

Supported allocation shapes

allocation can be:

  • a dict of {channel: total_budget} for ("channel",) budgets only
  • an xarray.DataArray
  • a DataArraySpec

For panel budgets such as ("geo", "channel") or ("geo", "brand", "channel"), use xarray.DataArray or DataArraySpec.

Dict allocations must match the model’s channel coordinates exactly. Missing or extra keys raise ValueError.

FixedBudgetOptimizedScenarioSpec

Use FixedBudgetOptimizedScenarioSpec when you want Abacus to optimise the allocation.

from abacus.scenarios import FixedBudgetOptimizedScenarioSpec

spec = FixedBudgetOptimizedScenarioSpec(
    name="Optimised plan",
    start_date="2025-03-03",
    end_date="2025-03-24",
    noise_level=0.0,
    include_carryover=False,
    total_budget=900_000.0,
)

Optimisation fields

Field Meaning
total_budget Total spend over the full scenario horizon
response_variable Variable used by the optimiser
budget_bounds Explicit lower and upper bounds
spend_constraint_lower Relative lower bound when deriving defaults
spend_constraint_upper Relative upper bound when deriving defaults
default_constraints Passed through to the underlying optimiser

The default response_variable is "total_media_contribution_original_scale".

Default bound derivation

If you do not pass budget_bounds, Abacus derives them from historical reference spend.

For each omitted relative constraint side, Abacus uses 0.3. That gives the default Meridian-style bounds:

  • lower bound: scaled reference spend × (1 - 0.3)
  • upper bound: scaled reference spend × (1 + 0.3)

If historical reference spend sums to zero, Abacus cannot derive those default bounds and raises ValueError.

Supported budget_bounds shapes

budget_bounds can be:

  • a dict of {channel: (lower, upper)} for ("channel",) budgets only
  • an xarray.DataArray
  • a DataArraySpec

For xarray or DataArraySpec, the dims must be (*budget_dims, "bound") with "lower" and "upper" values on the bound dimension.

budget_distribution_over_period

Both simulated scenario types support budget_distribution_over_period.

The object must:

  • have dims ("date", *budget_dims)
  • contain one weight per scenario period
  • sum to 1 across date for every budget cell

The date coordinates can be:

  • integer positions 0 .. num_periods - 1, or
  • exact dates that match the requested scenario window

If the dates do not match the scenario window exactly, Abacus raises ValueError.

DataArraySpec

Use DataArraySpec when you want JSON-friendly or YAML-friendly planner inputs.

from abacus.scenarios import DataArraySpec

allocation = DataArraySpec(
    values=[[420_000.0, 280_000.0], [300_000.0, 200_000.0]],
    dims=("geo", "channel"),
    coords={
        "geo": ["UK", "FR"],
        "channel": ["channel_1", "channel_2"],
    },
)

Abacus materialises DataArraySpec as an xarray.DataArray before it validates dims and coordinates.

Versioned scenario recipes

Use ScenarioRecipe to retain several scenario specifications as one versioned request. The recipe contract rejects unknown versions, empty scenario lists, duplicate scenario IDs, and unknown top-level fields.

scenario_contract_version: "1"
scenarios:
  - scenario_type: current
    name: Observed 13-week reference
    start_date: "2024-11-04"
    end_date: "2025-01-27"

  - scenario_type: manual_allocation
    name: Manual 13-week reallocation
    start_date: "2025-02-03"
    end_date: "2025-04-28"
    include_last_observations: true
    include_carryover: false
    noise_level: 0.0
    allocation:
      dims: [geo, channel]
      coords:
        geo: [DE, FR, UK]
        channel: [channel_1, channel_2]
      values:
        - [8000000, 2500000]
        - [7900000, 2450000]
        - [7800000, 2400000]

The complete executable FE and CRE examples are:

  • data/demo/geo_fe/scenario_recipe.yml
  • data/demo/geo_fe/scenario_recipe.py
  • data/demo/geo_cre/scenario_recipe.yml
  • data/demo/geo_cre/scenario_recipe.py

The Python files expose build_recipe() and are regression-tested for semantic equivalence with their YAML counterparts.

Run a YAML recipe against an existing fitted run:

python -m abacus.scenarios \
  --results-dir results/<fitted-run> \
  --recipe data/demo/geo_fe/scenario_recipe.yml

Abacus writes a new immutable evidence directory under <fitted-run>/scenario_planner/recipes/. It does not refit the model or alter the fitted run manifest.

Common pitfalls

  • Reusing the same scenario_id twice in one comparison
  • Using dict allocations or dict bounds for panel budgets
  • Passing an allocation or bounds object with missing coordinates
  • Providing a budget_distribution_over_period that does not sum to 1
  • Reusing an existing recipe output directory; retained evidence is never overwritten