DSAMbayes Documentation

Documentation for DSAMbayes v1.3.3 — a Bayesian marketing mix modelling toolkit for R, built on Stan.

DSAMbayes provides a unified interface for building, fitting, and interpreting MMM models. It supports single-market regression (BLM), multi-market hierarchical models with partial pooling, and pooled models with structured media coefficients. All model types share the same post-fit interface for posterior extraction, diagnostics, decomposition, and budget optimisation.

The docs are organised around a simple idea: DSAMbayes is not just an API or a runner. It is a way of operating a principled Bayesian MMM workflow with explicit assumptions, diagnostic gates, and decision rules.

If you are coming from OLS or frequentist MMM

Start with the workflow pages, not the YAML reference.

Where to start

You want to… Start here
Install and run your first model Install and SetupQuickstart
Understand the modelling workflow Principled Bayesian WorkflowWhat Principled Means
Translate from classical MMM thinking Frequentist to Bayesian Translation
Decide how to set priors Stage 2: Model and PriorsPriors and Boundaries
Decide which diagnostics matter most Stage 4: Computation and SamplerStage 5: Model Adequacy
Run a reproducible YAML-driven pipeline QuickstartCLI Usage
Interpret run outputs and plots Interpret DiagnosticsPlot Catalogue
Compare models and select a candidate Compare Runs

Documentation sections

  • Getting Started — installation, environment setup, first runs, and first model tutorials
  • Principled Bayesian Workflow — the methodology spine: stages, assumptions, prior-setting discipline, diagnostics, and decision gates
  • Runner — CLI usage, YAML config schema, and output artefacts
  • Modelling Reference — model classes, priors, boundaries, diagnostics, response scale, and optimisation semantics
  • Plots — catalogue of every plot the runner produces, with interpretation guidance
  • How-To Guides — task-oriented recipes for common workflows
  • FAQ — answers to common questions
  • Appendices — glossary, module index, and traceability map

The workflow contract in one view

Stage Main question Typical DSAMbayes evidence
Model and priors Are the assumptions explicit and defensible? formula, priors, boundaries, response-scale choice
Computation Are the posterior draws trustworthy? Rhat, ESS, divergences, treedepth, BFMI
Adequacy Does the fitted model describe the data credibly? fit plots, PPC, residual behavior, LOO/Pareto-k
Interpretation Are decomposition and optimisation outputs fit for use? overall gate status plus uncertainty-aware reporting

Passing one row does not automatically imply the next row passes.

Support boundaries in v1.3.3

  • Supported workflows — BLM, RE, CRE, and pooled modelling; interactive R workflows; YAML runner validate and run; diagnostics; model selection; and budget optimisation.
  • Supported with explicit limits — pooled models require MCMC, target.offset_column is supported only for model.type: blm, outputs.save_deployment_model_rds is supported for model.type: blm, for model.type: pooled with fit.method: mcmc, and for hierarchical model.type: re/cre with fit.method: mcmc, hierarchical deployment scoring is seen-groups-only, time-series CV is not supported for pooled runs, and hierarchical response decomposition may be skipped when model.matrix() cannot evaluate formulas with random-effects syntax.
  • Reserved or limited surfacesforecast currently creates only the 70_forecast/ stage with no forecast files or plots, and post-run decomposition artefacts are written only when they are enabled and can be computed from the fitted model.

What changed in v1.3.3

Key changes in this release (see CHANGELOG.md for full details):

  • Reliable local-run statusrunme.R now preserves completed but non-publishable runner outcomes and exits non-zero when action is required.
  • Fail-closed quality lanes — explicitly requested lint and test checks fail when their required tooling is unavailable.

Earlier v1.3.2 changes included release-metadata alignment, stricter prior guardrails, and the pooled-boundary scaling fix.

Authorship

Subsections of DSAMbayes Documentation

Getting Started

Purpose

Onboard a new user from install to first successful DSAMbayes run, then point them into the workflow guidance needed for serious modelling use.

Audience

  • New DSAMbayes users.
  • Analysts running DSAMbayes through R scripts or CLI.
  1. Install and Setup
  2. Quickstart (YAML Runner)
  3. Principled Bayesian Workflow
  4. Your First BLM Model or Your First Hierarchical Model

If you are coming from classical econometrics, read Frequentist to Bayesian Translation before customising priors or interpreting diagnostics.

Pages

Page Topic
Install and Setup Prerequisites, installation commands, and verification
Concepts What DSAMbayes does and how Bayesian MMM differs from classical regression
Your First BLM Model Build, fit, and interpret a single-market model using the R API
Your First Hierarchical Model Multi-market model with partial pooling and CRE
Quickstart (YAML Runner) Minimal end-to-end CLI run from config to output inspection
FAQ Answers to common questions

Use Getting Started for setup, not for the whole methodology

The Getting Started pages are intentionally practical. Once you can run the package, use the workflow section to answer the two questions that matter most in Bayesian MMM:

  • how should I think about priors?
  • which diagnostics matter before I trust outputs?

Subsections of Getting Started

Install and Setup

Audience

Engineers and analysts setting up DSAMbayes for local development or modelling runs.

Prerequisites

  • R >= 4.1 — check with R --version.
  • A C++ toolchain for Stan compilation. This is the most common source of setup issues:
    • macOS: install Xcode Command Line Tools (xcode-select --install).
    • Windows: install Rtools matching your R version. Ensure make is on your PATH.
    • Linux (Ubuntu/Debian): sudo apt install build-essential.
    • See the RStan Getting Started Guide for detailed platform instructions.
  • A local checkout of this repository.

Open a terminal in the repository root and run:

# 1. Select an ABI-safe host library and create it with the cache
source scripts/r-library-path.sh
dsambayes_set_r_library host
mkdir -p "$R_LIBS_USER" .cache

# 2. Set the cache path (add both settings to .bashrc/.zshrc for persistence)
export XDG_CACHE_HOME="$PWD/.cache"

# 3. Install DSAMbayes from the local checkout
R -q -e 'install.packages(".", repos = NULL, type = "source")'

This keeps all package libraries and Stan compilation caches inside the repo, avoiding permission issues with system library paths.

Do not share a compiled package library between R runtimes or containers. dsambayes_set_r_library host selects .Rlib-host-r<active-R-version>/; use dsambayes_set_r_library container inside a container instead. The path is deliberately derived from the active runtime, not hard-coded in the repository.

Verify the installation

1. Confirm DSAMbayes loads

R -q -e 'library(DSAMbayes); cat("Version:", as.character(utils::packageVersion("DSAMbayes")), "\n")'

Expected: prints Version: 1.3.3 (or current version).

2. Confirm the runner works

Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml

Expected: validation completes without errors.

3. (Optional) Run the test suite

R -q -e 'testthat::test_dir("tests/testthat")'

Expected: all tests pass.

Alternative: install from GitHub

If you do not have a local checkout, install from the GitHub remote:

R -q -e 'remotes::install_github("groupm-global/DSAMbayes")'

This installs the latest version on main. For the development fork with v1.3.3 features, use the local-checkout path above.

Using renv (optional)

The repository includes a renv.lock file for fully reproducible dependency management. To use it:

R -q -e 'install.packages("renv"); renv::restore()'

This installs the exact dependency versions used during development. It is optional but recommended for production runs where reproducibility matters.

Runner setup and first execution

1. Validate the example config

Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml

2. Execute a full run

Rscript scripts/dsambayes.R run --config config/cre_geo_panel.yaml

Expected: a timestamped run directory is created under results/ with model outputs and diagnostics.

Troubleshooting

Stan compilation fails

Symptom: errors during Compiling model... referencing C++ or compiler issues.

Actions:

  1. Confirm your C++ toolchain is working: R -q -e 'pkgbuild::has_build_tools(debug = TRUE)'.
  2. On Windows, ensure Rtools is installed and make is on your PATH.
  3. Clear the Stan cache and retry: rm -rf .cache/dsambayes.
  4. Follow the RStan Getting Started Guide for your platform.

Package installation fails

Symptom: install.packages(".", repos = NULL, type = "source") errors.

Actions:

  1. Confirm you are in the repository root directory.
  2. Confirm the selected R_LIBS_USER directory exists and is writable.
  3. Check for missing system dependencies in the error output.

Stale Stan cache

Symptom: unexpected model behaviour after updating the package.

Actions:

  1. Clear the cache: rm -rf .cache/dsambayes.
  2. Re-run with model.force_recompile: true in your config if you need to invalidate a stale compiled model.

Permission issues

Symptom: write failures for library, cache, or run outputs.

Actions:

  1. Ensure R_LIBS_USER, .cache, and results/ are writable.
  2. Keep R_LIBS_USER and XDG_CACHE_HOME set in your shell session.
  3. Run all commands from the repository root.

Concepts

Purpose

Give new DSAMbayes users a compact conceptual orientation before they move into tutorials, runner usage, or the workflow section.

This page is intentionally introductory. It does not try to be the full methodology guide for Bayesian MMM. For that, use Principled Bayesian Workflow.

What is DSAMbayes?

DSAMbayes is an R package for Bayesian marketing mix modelling built on Stan. It provides:

  • an lm()-style modelling interface for interactive work
  • model classes for single-series, hierarchical, and pooled MMM
  • prior and boundary controls
  • post-fit extraction, diagnostics, decomposition, and optimisation tooling
  • a YAML/CLI runner for reproducible runs

The main practical difference from classical regression is that DSAMbayes works with a posterior distribution, not just a single fitted coefficient vector.

Why Bayesian MMM?

MMM datasets often have the exact features that make naive regression unstable:

  • short time series
  • overlapping media timing
  • strong baseline structure
  • uncertain functional form
  • real business need for uncertainty-aware decisions

Bayesian modelling helps because it makes several things explicit:

  • regularisation through priors
  • structural constraints through boundaries
  • uncertainty propagation into downstream outputs
  • diagnostic gates rather than fit-statistic-only thinking

The DSAMbayes mental model

DSAMbayes should be thought of as a workflow, not just a fitter.

At a high level:

  1. specify the model and priors
  2. fit the model
  3. check whether the posterior computation is trustworthy
  4. check whether the fitted model is adequate for the data
  5. only then interpret decomposition, comparison, or optimisation outputs

That is the main philosophical shift from a simpler OLS-style workflow.

Model classes

DSAMbayes supports three main model classes.

BLM

Single-market Bayesian linear model.

Use when:

  • you have one KPI series
  • one market / brand / region is the modelling unit
  • you want the simplest Bayesian MMM starting point

Hierarchical

Multi-group model with partial pooling.

Use when:

  • you have panel data across markets, regions, or brands
  • you want to borrow strength across groups while preserving group structure

Pooled

Single-market model with structured pooling across labelled media dimensions.

Use when:

  • the outcome is one series
  • the media structure has nested or repeated dimensions that should share information

For the detailed class contract, see Model Classes.

Interactive API vs runner

DSAMbayes has two main ways of working:

Interactive R API

Best when you want to prototype directly in R:

  • blm()
  • set_prior()
  • set_boundary()
  • fit()
  • get_posterior()

YAML / CLI runner

Best when you want reproducibility and staged artefacts:

  • validate
  • run
  • staged outputs under results/

See Quickstart and Runner.

What this page does not try to teach

This page does not try to fully answer:

  • how to choose priors
  • which diagnostics matter most
  • when business interpretation is allowed

Those are workflow questions, and they are handled in the dedicated methodology pages:

Your First BLM Model

Goal

Build, fit, and interpret a single-market Bayesian linear model (BLM) using the DSAMbayes R API.

This page is a hands-on tutorial. For the broader methodological questions behind prior-setting and diagnostics, use the workflow pages:

Prerequisites

  • DSAMbayes installed locally (see Install and Setup).
  • Familiarity with R and lm()-style formulas.

Dataset

This walkthrough uses the synthetic dataset shipped at data/synthetic_dsam_example_wide_data.csv. It contains weekly observations for a single market with columns for:

  • Response: kpi_os_hfb01_value — weekly KPI (e.g. revenue).
  • Media: m_tv, m_search, m_social, m_display, m_ooh, m_email, m_affiliate.
  • Controls: t_scaled (trend), sin52_1/cos52_1/sin52_2/cos52_2 (Fourier seasonality), price_index, distribution, bm_ikea_trust_12r (brand metric).
  • Holidays: h_black_friday, h_christmas, h_new_year, h_easter, h_summer_sale.
library(DSAMbayes)
df <- read.csv("data/synthetic_dsam_example_wide_data.csv")
str(df)

Step 1: Construct the model

blm() creates an unfitted model object. No fitting happens yet.

model <- blm(
  kpi_os_hfb01_value ~
    t_scaled + sin52_1 + cos52_1 + sin52_2 + cos52_2 +
    h_black_friday + h_christmas + h_new_year + h_easter + h_summer_sale +
    bm_ikea_trust_12r + price_index + distribution +
    m_tv + m_search + m_social + m_display + m_ooh + m_email + m_affiliate,
  data = df
)

Inspect defaults:

peek_prior(model)      # normal(0, 5) for all terms
peek_boundary(model)   # (-Inf, Inf) — unconstrained

Step 2: Set boundaries

Media channels should have non-negative effects. Use inequality notation:

model <- model %>%
  set_boundary(
    m_tv > 0, m_search > 0, m_social > 0,
    m_display > 0, m_ooh > 0, m_email > 0, m_affiliate > 0
  )

Step 3: (Optional) Override priors

Default priors are weakly informative. Override only with domain knowledge:

model <- model %>%
  set_prior(price_index ~ normal(-0.2, 0.1))

See Stage 2: Model and Priors and Minimal-Prior Policy for guidance.

Step 4: Fit with MCMC

fitted_model <- model %>%
  fit(cores = 2, iter = 2000, warmup = 1000, seed = 123)

First-time Stan compilation takes 1–3 minutes. Subsequent runs use a cached binary. With 2 chains on synthetic data, sampling typically completes in under 2 minutes.

Step 5: Sampler diagnostics

chain_diagnostics(fitted_model)
Metric Good Concern
Max Rhat <= 1.01 > 1.01 means chains have not converged
Min ESS (bulk) > 400 < 200 means too few effective samples
Divergences 0 Any non-zero count warrants investigation

These are the computational checks. They do not by themselves prove the model is adequate for interpretation.

Step 6: Extract the posterior

post <- get_posterior(fitted_model)

post is a tibble with one row per draw containing coef (named coefficient list), yhat (fitted values), noise_sd, r2, rmse, and smape.

Summarise coefficients:

library(dplyr); library(tidyr)

coef_summary <- post %>%
  select(coef) %>%
  unnest_wider(coef) %>%
  pivot_longer(everything(), names_to = "term") %>%
  group_by(term) %>%
  summarise(
    mean = mean(value), median = median(value), sd = sd(value),
    ci_low = quantile(value, 0.025), ci_high = quantile(value, 0.975),
    .groups = "drop"
  )
print(coef_summary, n = 30)

What to look for:

  • Media coefficients should be positive (boundaries enforce this).
  • Wide credible intervals mean the prior dominates — the data cannot identify the effect precisely.

Step 7: Assess model fit

fit_tbl <- fitted(fitted_model)
cat("Median R²:", median(r2(fitted_model)), "\n")
cat("Median RMSE:", median(rmse(fitted_model)), "\n")

For a well-specified MMM on weekly data, in-sample R² above 0.85 is typical.

Step 8: Response decomposition

decomp_tbl <- decomp(fitted_model)
head(decomp_tbl)

Shows each term’s contribution (coefficient × design-matrix column) to the predicted KPI at each time point — the foundation for media contribution and ROI reporting.

Step 9: MAP for rapid iteration

During development, use MAP for fast point estimates:

map_model <- model %>% fit_map(n_runs = 10)
get_posterior(map_model)

Use MCMC for final reporting; MAP for formula iteration. A MAP result is one point estimate, not posterior draws: it has no valid credible intervals or MCMC diagnostics. Review the restart diagnostics if objectives differ materially; use fit() when uncertainty, convergence, or decision risk matters.

Common pitfalls

Pitfall Symptom Fix
Forgetting to set boundaries Media coefficients go negative Add set_boundary(m_x > 0)
Too few iterations High Rhat, low ESS Increase iter and warmup
Missing controls High residual autocorrelation Add trend, seasonality, or holiday terms
Scaling confusion Coefficients look wrong model.scale: true is default; get_posterior() back-transforms automatically

Next steps

Your First Hierarchical Model

Goal

Build, fit, and interpret a multi-market hierarchical model with partial pooling and optional CRE (Mundlak) correction using the DSAMbayes R API.

This page is a hands-on tutorial. For the broader methodological questions behind prior-setting, diagnostics, and interpretation, use the workflow pages:

Prerequisites

Dataset

This walkthrough uses data/synthetic_dsam_example_hierarchical_data.csv — a panel dataset with weekly observations across multiple markets. Key columns:

  • Response: kpi_value — weekly KPI per market.
  • Group: market — market identifier.
  • Media: m_tv, m_search, m_social — media exposure variables.
  • Controls: trend, seasonality, brand_metric.
  • Date: date — weekly date index.
library(DSAMbayes)
panel_df <- read.csv("data/synthetic_dsam_example_hierarchical_data.csv")
table(panel_df$market)  # Check group counts

Step 1: Construct the hierarchical model

The (term | group) syntax tells DSAMbayes to fit random effects. Terms inside the parentheses get group-specific deviations from the population mean:

model <- blm(
  kpi_value ~
    trend + seasonality + brand_metric +
    m_tv + m_search + m_social +
    (1 + m_tv + m_search + m_social | market),
  data = panel_df
)

This specifies:

  • Population (fixed) effects for all terms — the average effect across markets.
  • Random intercepts and slopes for media terms by market — each market can deviate from the population average.

Step 2: Set boundaries

model <- model %>%
  set_boundary(m_tv > 0, m_search > 0, m_social > 0)

Boundaries apply to the population-level coefficients.

Step 3: (Optional) Add CRE / Mundlak correction

If you suspect that group-level spending patterns are correlated with unobserved market characteristics (e.g. high-spend markets also have higher baseline demand), CRE controls for this:

model <- model %>%
  set_cre(vars = c("m_tv", "m_search", "m_social"))

This adds cre_mean_m_tv, cre_mean_m_search, cre_mean_m_social as fixed effects — the group-level means of each media variable. The within-group coefficients then represent purely temporal variation, controlling for between-group confounding.

See CRE / Mundlak for when and why to use this.

Step 4: Fit with MCMC

fitted_model <- model %>%
  fit(cores = 4, iter = 2000, warmup = 1000, seed = 42)

Hierarchical models are slower than BLM — expect 10–30 minutes depending on group count and data size. First-time Stan compilation of the hierarchical template adds 2–3 minutes.

Step 5: Check diagnostics

chain_diagnostics(fitted_model)

Pay special attention to Rhat and ESS for sd_* parameters (group-level standard deviations), which are often harder to estimate than population coefficients.

Step 6: Extract the posterior

post <- get_posterior(fitted_model)

For hierarchical models, coefficient draws from get_posterior() return vectors (one value per group) rather than scalars. The population-level (fixed-effect) estimates are averaged across groups.

Step 7: Group-level results

Fitted values and decomposition are returned per group:

# Fitted values — one row per observation, grouped by market
fit_tbl <- fitted(fitted_model)
head(fit_tbl)

# Decomposition — per-group predictor contributions
decomp_tbl <- decomp(fitted_model)

Step 8: Budget optimisation (population level)

Budget optimisation uses population-level (fixed-effect) beta draws, not group-specific totals:

# See Budget Optimisation docs for full scenario specification
result <- optimise_budget(fitted_model, scenario = my_scenario)

Key differences from BLM

Aspect BLM Hierarchical
Data structure Single market Panel (multiple groups)
Coefficient draws Scalars Vectors (one per group)
Fit time 2–5 min 10–30 min
Decomposition Direct May fail gracefully for `
Forest/prior-posterior plots Direct Group-averaged population estimates
Stan template bayes_lm_updater_revised.stan general_hierarchical.stan (templated per group count)

Common pitfalls

Pitfall Symptom Fix
Too few groups Weak partial pooling; group SDs poorly estimated Need 4+ groups for meaningful hierarchical structure
Too few obs per group High Rhat on sd_* parameters Increase iterations; simplify random-effect structure
CRE with too many vars More CRE variables than groups Reduce CRE variable set; see identification warnings
CRE mean has zero variance scale=TRUE aborts with constant column error Use model.type: re (without CRE) or model.scale: false

Next steps

Quickstart (YAML Runner)

Goal

Complete one reproducible DSAMbayes runner execution from validation to artefact inspection, then load the fitted model in R to explore the results interactively.

This page is operational by design. It teaches you how to run the package, not the full modelling methodology. After the quickstart succeeds, use Principled Bayesian Workflow before treating outputs as decision-ready.

Before you start

Complete the setup in Install and Setup. If you want to build a model interactively from R code instead of YAML, see Your First BLM Model.

1. Set up the environment

Open a terminal in the repository root:

source scripts/r-library-path.sh
dsambayes_set_r_library host
mkdir -p "$R_LIBS_USER" .cache
export XDG_CACHE_HOME="$PWD/.cache"

2. Validate the configuration (dry run)

Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml

Expected: exits with code 0. No Stan compilation or sampling occurs.

3. Execute the full run

Rscript scripts/dsambayes.R run --config config/blm_timeseries.yaml

Expected: a timestamped run directory under results/ with staged outputs.

4. Locate and inspect the run directory

latest_run="$(ls -td results/* | head -n 1)"
echo "$latest_run"
find "$latest_run" -maxdepth 2 -type d | sort

Expected stage folders:

Folder Content
00_run_metadata/ Original/resolved/compiled configs, session info
10_pre_run/ VIF report, data dictionary, media spend plots
20_model_fit/ Fitted model object, fit plots
30_post_run/ Posterior summary, fitted/observed CSVs
40_diagnostics/ Diagnostics report, residual plots
50_model_selection/ LOO summary, Pareto-k diagnostics
60_optimisation/ Budget allocation, response curves (when enabled)

5. Verify key artefacts

test -f "$latest_run/00_run_metadata/config.compiled.yaml" && echo "ok: config.compiled.yaml"
test -f "$latest_run/00_run_metadata/config.resolved.yaml" && echo "ok: config.resolved.yaml"
test -f "$latest_run/20_model_fit/model.rds" && echo "ok: model.rds"
test -f "$latest_run/30_post_run/posterior_summary.csv" && echo "ok: posterior_summary.csv"
test -f "$latest_run/40_diagnostics/diagnostics_report.csv" && echo "ok: diagnostics_report.csv"

6. Load the model in R

The fitted model is saved as an RDS object. Load it interactively to explore:

library(DSAMbayes)

model <- readRDS("results/<run_dir>/20_model_fit/model.rds")

# Posterior coefficient summary
post <- get_posterior(model)

# Fit quality
cat("Median R²:", median(r2(model)), "\n")

# Sampler diagnostics
chain_diagnostics(model)

# Fitted values
head(fitted(model))

7. Review diagnostics

Open 40_diagnostics/diagnostics_report.csv:

cat "$latest_run/40_diagnostics/diagnostics_summary.txt"

Quick interpretation:

  • pass — no immediate blocker
  • warn — review before sharing or acting
  • fail — do not treat the run as publishable or decision-ready

For the operational triage, see Interpret Diagnostics. For the methodological meaning of these gates, see:

8. Start from a tracked example config

Copy one of the two tracked examples and adapt it to your data:

  • config/blm_timeseries.yaml for single-series work
  • config/cre_geo_panel.yaml for geo-panel CRE work

Edit the copied YAML to point to your data and columns, then validate and run.

What the quickstart does not prove

A successful run means:

  • the package is installed correctly
  • the runner contract works on the example config
  • you have a complete staged result

It does not by itself prove:

  • the priors are appropriate
  • the fit is decision-ready
  • the decomposition is substantively meaningful
  • the optimisation output should be acted on

That is why the next stop should be the workflow pages.

If the quickstart fails

  • re-run validate before run
  • read the full error message
  • inspect 00_run_metadata/config.resolved.yaml
  • inspect 00_run_metadata/config.compiled.yaml
  • use Debug Run Failures

Next steps

FAQ

This page is for short, practical answers.

For the bigger methodological questions, start with:

Installation and setup

How long does the first Stan compilation take?

Usually 1 to 3 minutes. Subsequent runs typically reuse the cached binary. If compilation appears stuck, check the C++ toolchain in Install and Setup.

Do I need to set R_LIBS_USER every time?

Yes, unless you add it to your shell profile. Use dsambayes_set_r_library host to select a repo-local path that is isolated from both your system library and any container library.

Can I use renv instead of .Rlib?

Yes. The repo includes renv.lock. Use renv::restore() if you want exact dependency restoration.

Modelling

How many weeks of data do I need?

There is no hard minimum, but a useful rule of thumb is:

  • BLM: about 100+ weeks for a model with roughly 10 to 15 predictors
  • Hierarchical: about 80+ weeks per group, ideally with at least 4 groups

Shorter series can still be modelled, but the posterior will usually be much more prior-driven and less decision-ready.

Should I use identity or log response?

  • Identity when the KPI is naturally additive and variance is fairly stable
  • Log when the KPI is strictly positive and effect interpretation is more naturally multiplicative

If unsure, fit both and compare the adequacy and diagnostic picture, not just a single fit metric. See Response Scale Semantics.

How should I think about priors?

Start with Stage 2: Model and Priors.

The short answer is:

  • start with defaults
  • add sparse sign constraints only for structural assumptions
  • add sparse prior overrides only when you can defend them in business or modelling terms
  • do not use priors as a substitute for weak model design

How many MCMC iterations do I need?

The defaults are a reasonable starting point. Then inspect the Stage 4 diagnostics:

  • Rhat <= 1.01 and healthy ESS usually mean the draw count is adequate
  • Rhat > 1.01 or weak ESS usually means you need to increase iterations and warmup
  • any divergences should be addressed before treating the fit as decision-ready

See Stage 4: Computation and Sampler.

How strict is the stationarity requirement for MMM?

DSAMbayes does not require the raw KPI to satisfy a textbook stationarity condition before fitting.

The important question is whether the remaining unexplained structure, after adding sensible controls and baseline terms, is weak enough that media effects are not standing in for missing baseline dynamics.

When should I set boundaries on media coefficients?

Use m_channel > 0 when non-negativity is a structural belief you would defend in writing. Do not apply blanket sign constraints just to make the output look tidier. See Stage 2: Model and Priors and Minimal-Prior Policy.

When should I use CRE (Mundlak)?

Use CRE when you want to separate within-group temporal effects from between-group cross-sectional structure in a hierarchical model. See CRE / Mundlak.

How should I handle CRE mean terms in decomposition / attribution?

Treat cre_mean_* terms as baseline or between-group structure, not as media attribution terms. They are there to absorb confounding structure, not to claim channel contribution.

What priors should I use on CRE mean terms?

Usually the defaults. Avoid manually tightening or positivity-constraining them unless you have a very strong reason, because that can undermine the whole point of CRE adjustment.

Can I add random slopes for CRE mean terms?

No. Those terms are constant within group, so random slopes on them are not separately identifiable from the group intercept.

What does scale = TRUE do?

It standardises the response and predictors before Stan fitting to improve sampler efficiency. Post-fit coefficient extraction is back-transformed automatically.

Runner and outputs

How long does a typical run take?

Roughly:

  • BLM MCMC: a few minutes
  • BLM MAP: seconds
  • Hierarchical MCMC: tens of minutes depending on size
  • Pooled MCMC: usually between BLM and hierarchical

First-time Stan compilation adds extra startup time.

What is the difference between validate and run?

  • validate checks config and data contracts without compiling or fitting Stan
  • run validates, fits, writes staged artefacts, and runs diagnostics

Always validate first after config changes.

Where do outputs go?

Under results/<timestamp>_<model_name>/ by default. See Output Artefacts.

How do I compare two model runs?

Use compare_runs() or compare the model-selection artefacts directly. See Compare Runs.

Diagnostics

Which diagnostics matter most?

Read them in order:

  1. Stage 4: Computation and Sampler
  2. Stage 5: Model Adequacy

That is more important than memorising one threshold in isolation.

What does “Pareto-k > 0.7” mean?

It means the LOO approximation is unreliable for that observation and the point is highly influential. Investigate the observation and be cautious about using LOO-based comparisons mechanically.

My diagnostics say warn. Should I worry?

Usually yes, but not always in the same way.

  • in exploratory work, a warning may be acceptable if understood
  • in shareable reporting, warnings should be disclosed and interpreted
  • repeated or severe warnings usually mean the model needs revision before decision use

Use Interpret Diagnostics for triage and the workflow pages for meaning.

Budget optimisation

How does the allocator work?

It searches feasible spend allocations within channel constraints and scores them against the fitted model. It is a decision layer built on the model, not an independent source of truth.

Can I use budget optimisation with MAP-fitted models?

Yes, but then the result is point-estimate-driven rather than uncertainty-rich. That is fine for rough iteration, not ideal for final decision support.

Principled Bayesian Workflow

Purpose

Give DSAMbayes users a workflow-shaped mental model for Bayesian MMM. This section is the methodological spine of the docs: it explains the sequence of decisions, assumptions, and diagnostic gates that should sit behind any DSAMbayes run.

Audience

  • Econometricians moving from OLS or other frequentist MMM workflows into Bayesian modelling.
  • Analysts who know how to run DSAMbayes but want a more defensible modelling process.
  • Reviewers who need to understand what a “good” DSAMbayes run should have passed before interpretation.

Why this section exists

DSAMbayes already documents its runner, model classes, priors, and diagnostics in detail. What most users still need is a clear answer to:

  1. What is the modelling workflow?
  2. Where do priors come from?
  3. Which diagnostics matter most?
  4. When is interpretation allowed, and when should it stop?

This section answers those questions directly. Use it before reading the lower-level reference pages.

Workflow vectors

DSAMbayes workflow vectors DSAMbayes workflow vectors

Different operational entry points in DSAMbayes still converge on the same workflow contract. Whether you work interactively in R, use runme.R, or run the YAML / CLI path, the result should still pass the same prior, computation, and adequacy gates before interpretation.

Page Use it when…
What Principled Means You want the high-level contract for a Bayesian MMM workflow
Frequentist to Bayesian Translation You are used to OLS-style modelling and want a practical translation layer
Stage 2: Model and Priors You need guidance on priors, boundaries, and default-vs-override decisions
Stage 4: Computation and Sampler You need to know which post-fit diagnostics are non-negotiable
Stage 5: Model Adequacy You need to decide whether decomposition, model comparison, or optimisation outputs are trustworthy

Workflow contract

The workflow is easiest to remember as a sequence of questions:

Stage Core question Typical DSAMbayes surface What happens if it fails?
Problem framing What decision is the model supposed to support? Analyst design choice before code Re-specify the business question or estimand
Data and identifiability Is the data rich enough to support the claim? formula design, controls, pre-flight, design checks Rework data or narrow the modelling ambition
Model and priors Are the assumptions and priors defensible? blm(), set_prior(), set_boundary(), YAML priors/boundaries Re-specify assumptions before fitting
Computation and sampler Are the posterior draws numerically trustworthy? fit(), chain_diagnostics(), 40_diagnostics/ Do not interpret downstream outputs
Model adequacy Does the fitted model describe the data credibly? fit plots, PPC, residual diagnostics, LOO/Pareto-k Do not use decomposition or optimisation for decisions
Interpretation and decision Are we reporting uncertainty and caveats honestly? decomposition, response curves, optimisation Restrict or block business use

Two rules to remember

1. Passing sampler diagnostics is necessary, not sufficient

Good Rhat, ESS, and zero divergences mean the posterior draws are numerically credible. They do not prove the model is a good description of the data, and they do not prove causal validity.

2. Good fit is not causal proof

A model can fit well, calibrate well, and still estimate the wrong media effects if the identifying assumptions are weak. DSAMbayes can make the workflow more disciplined; it cannot remove the need for analyst judgment.

How this section relates to the rest of the docs

  • Workflow pages answer “what should I do and why?”
  • How-to pages answer “how do I perform this task right now?”
  • Reference pages answer “what exactly does this field, function, or plot mean?”

Subsections of Principled Bayesian Workflow

What Principled Means

Objective

Define what DSAMbayes means by a principled Bayesian MMM workflow.

The term is intentionally about process, not brand loyalty to a particular model class or sampler. A principled workflow is one where assumptions are explicit, diagnostics are stage-gated, and downstream interpretation is conditioned on those gates.

The short definition

A DSAMbayes workflow is principled when it does all of the following:

  1. states a clear modelling objective and decision context
  2. specifies an explicit model with explicit priors and boundaries
  3. checks whether the posterior computation is trustworthy
  4. checks whether the fitted model is adequate for the data
  5. carries uncertainty and gate status into decomposition, optimisation, and reporting

If any one of those steps is skipped, the workflow becomes less defensible even if the final coefficients look plausible.

Workflow contract at a glance

Principled Bayesian MMM workflow Principled Bayesian MMM workflow

The key design choice in DSAMbayes is that downstream outputs are conditional on upstream gates. A model that fits is not automatically a model that should be decomposed, compared, optimised, or deployed.

Four commitments

1. Generative transparency

The model should tell a clear story about how the outcome is generated from:

  • media terms
  • baseline structure
  • controls
  • observation noise

That is why DSAMbayes exposes priors, boundaries, response scale, CRE terms, time components, and model classes explicitly. The point is not to burden users with knobs; it is to make assumptions inspectable.

2. Stage-gated inference

The workflow should move in order:

  1. question and data design
  2. model and priors
  3. fit
  4. computational diagnostics
  5. model adequacy checks
  6. interpretation and decision support

Downstream outputs should only be trusted if the upstream gates have been checked.

3. Diagnostic sufficiency for computation, not causality

Diagnostics answer questions like:

  • did the sampler converge?
  • are the posterior draws stable?
  • does the fitted model describe the observed data credibly?

Diagnostics do not answer:

  • did the model identify the true causal effect of media?
  • did we control for every relevant confounder?
  • is the chosen baseline structure the only defensible one?

This distinction is essential in MMM.

4. Decision-linked reporting

Decomposition shares, response curves, deployment artifacts, and budget recommendations should be treated as functions of the gated fit, not as standalone truths. If the fit has warnings or failures, those limitations must travel with the result.

The three layers of trust

DSAMbayes users should separate three different questions:

Layer Question Typical evidence
Computational faithfulness Are the draws numerically trustworthy? Rhat, ESS, divergences, treedepth, BFMI
Model adequacy Does the model describe the observed data credibly? fitted-vs-observed, PPC, residual behavior, LOO/Pareto-k
Causal credibility Are the media effect estimates interpretable as causal? analyst judgment, design quality, confounder handling, identification logic

Passing the first layer does not imply the second. Passing the second does not imply the third.

How DSAMbayes supports this workflow

DSAMbayes already provides several pieces of the workflow contract:

  • explicit model classes and response-scale semantics
  • default priors plus selective overrides and hard boundaries
  • pre-flight design checks
  • post-fit diagnostics with pass / warn / fail statuses
  • staged runner artifacts under results/
  • model comparison and optional time-series selection tooling
  • decision-layer optimisation with uncertainty-aware summaries

What the package cannot do automatically is replace analyst judgment about:

  • business estimands
  • causal assumptions
  • whether a structural prior is genuinely defensible
  • whether a warned run is acceptable for the specific business use

Failure policy in plain language

If Stage 4 fails

Do not trust decomposition, response curves, or optimisation outputs. The posterior sample itself is numerically unreliable.

If Stage 5 fails

The sampler may have worked, but the model is not yet adequate for business interpretation. Use the run for diagnosis, not for stakeholder recommendations.

If causal assumptions are weak

Even a clean computational and adequacy profile may still only support associational interpretation. Report it that way.

Frequentist to Bayesian Translation

Objective

Translate familiar classical regression instincts into the DSAMbayes workflow so users coming from OLS, GLM, or general frequentist econometrics can adopt Bayesian MMM without losing methodological discipline.

What does not change

Moving to DSAMbayes does not remove the need for:

  • careful data definition
  • sensible controls
  • thinking about omitted variables
  • residual scrutiny
  • skepticism about causal claims

Bayesian MMM is not a shortcut around model design. It is a different way of expressing assumptions and uncertainty.

What changes

1. From one best coefficient to a posterior distribution

In OLS, the default object of interest is a point estimate plus a standard error. In DSAMbayes, the default object is a posterior distribution. That means:

  • coefficients are uncertain objects, not fixed truths
  • decomposition and optimisation should inherit that uncertainty
  • wide intervals are information, not a nuisance to hide

2. From “no prior” to “explicit prior assumptions”

Frequentist workflows often treat themselves as prior-free. In practice, they still encode structure through model choice, variable transformations, and constraints.

DSAMbayes makes that structure explicit:

  • default priors express mild regularisation
  • boundaries express structural sign assumptions
  • overrides should be sparse and justified

3. From significance-thinking to decision-thinking

The key question becomes less “is beta significantly different from zero?” and more:

  • is the posterior sufficiently stable?
  • is the model adequate?
  • is the interval narrow enough for the business decision?
  • what risks remain if we act on this estimate?

Quick translation table

Frequentist instinct DSAMbayes replacement
“Run the regression and inspect coefficients” Specify the model, priors, and boundaries, then inspect the full posterior
“Use p-values to screen variables” Use posterior intervals, sign stability, and workflow diagnostics
“Choose the model with the best fit statistic” Choose among models that first pass diagnostics and then compare predictive evidence
“If the model converged, the answer is credible” Convergence is only the computation gate; adequacy and interpretation are separate gates
“No prior means unbiased starting point” Defaults are still assumptions; make them explicit and inspect whether they are defensible
“A high R-squared validates the model” Fit can be good while causal interpretation remains weak

The priors question in frequentist language

The question “where do priors come from?” is often really one of these:

  1. What assumptions am I already making implicitly?
  2. Which assumptions deserve to be encoded explicitly?
  3. Where do I have stable directional knowledge versus weak intuition?

For DSAMbayes, the practical default is:

  • start with package defaults
  • add boundaries only for structural signs you would defend in writing
  • add sparse prior overrides only for high-conviction terms
  • do not use priors to force a preferred answer out of weak data

See Stage 2: Model and Priors.

The diagnostics question in frequentist language

The question “which diagnostics matter?” is best answered in order:

  1. Are the posterior draws numerically trustworthy?
  2. Does the model fit the data adequately?
  3. Are the business conclusions robust to what remains uncertain?

This translates into:

  • Stage 4: Rhat, ESS, divergences, treedepth, BFMI
  • Stage 5: fit plots, residual behavior, PPC, LOO/Pareto-k

See Stage 4: Computation and Sampler and Stage 5: Model Adequacy.

Common transition mistakes

Mistake 1: treating defaults as magic

Default priors are a sensible starting point, not proof that prior design is solved forever.

Mistake 2: using priors as a repair tool for poor design

If media terms are badly collinear with baseline structure or controls are missing, stronger priors may stabilise numerics without fixing the underlying modelling problem.

Mistake 3: treating warning-level diagnostics as a cosmetic issue

A warned run may still be usable, but only if the warning is understood and disclosed. The right response is not “the model ran, so ship it.”

Mistake 4: confusing predictive success with causal proof

A model can rank well by ELPD and still be causally fragile.

Practical recommendation

If you are used to classical MMM, use DSAMbayes in this order:

  1. run Quickstart to learn the tool surface
  2. read What Principled Means
  3. use Stage 2: Model and Priors before customising priors
  4. use Stage 4: Computation and Sampler and Stage 5: Model Adequacy before interpreting any decision-layer output

Stage 2: Model and Priors

Objective

Specify a model that is explicit enough to be audited and simple enough to be defended.

For most DSAMbayes users, this stage is where the biggest conceptual shift happens. In classical MMM, the common instinct is to choose variables, run the regression, and worry about coefficient stability afterwards. In DSAMbayes, priors and boundaries are part of the specification from the start.

The operating rule

Use a default-first workflow unless there is a strong reason not to.

That means:

  1. start with the package defaults
  2. add sparse sign constraints only where the business logic is structural
  3. add sparse prior overrides only where the prior story is stable and defensible
  4. fit the model
  5. inspect whether the posterior is still dominated by weak design rather than by a sensible prior choice

This is the same operating stance documented in Minimal-Prior Policy, but framed here as part of the modelling workflow rather than as a standalone policy page.

Prior specification pathways

Prior specification pathways Prior specification pathways

This is the short answer to “where should priors come from?” Defaults are the starting point. Boundaries and overrides are additive, sparse, and justified by business or structural reasoning. The blm(lm_object, data) path is the empirical-Bayes-like option when a credible legacy model already exists.

Where priors should come from

In DSAMbayes, priors should usually come from one of four sources.

1. Structural sign knowledge

Examples:

  • additional media exposure should not reduce KPI
  • competitor discount should not increase our sales

This is usually best expressed as a boundary, not as an aggressive mean-shifting prior.

2. Stable business knowledge about magnitude

Examples:

  • price elasticity is probably negative and modest
  • distribution is probably positive and not enormous

This is where a sparse override like normal(-0.2, 0.1) may be justified.

3. Historical learning from previous analyses

If the same brand, market, or response has been analysed repeatedly under a similar data-generating regime, you may have enough evidence to justify informative priors on a small number of terms.

4. Explicit regularisation when the data are weak

Sometimes priors are primarily there to stabilise a short, collinear MMM. That is acceptable, but it should be acknowledged honestly as regularisation rather than presented as deep subject-matter certainty.

Where priors should not come from

Do not set priors mainly because:

  • one previous run looked better with them
  • they remove a warning without improving model design
  • they force a preferred channel ranking
  • they make weak data look more certain than it really is

That is specification-hunting, not principled prior design.

Practical DSAMbayes policy

Step 1: start with defaults

For most first-pass BLM and hierarchical work, the package defaults are the right starting point.

  • default coefficient priors are weakly informative
  • default boundaries are unconstrained
  • you should not feel obliged to invent bespoke priors on every term

Step 2: add selective boundaries

Use set_boundary() or YAML boundaries.overrides when the sign is structural and defensible.

Good examples:

  • media terms constrained positive
  • competitor discount constrained non-positive

Poor examples:

  • constraining every control just to reduce posterior variance
  • forcing signs on variables whose mechanism is genuinely ambiguous

Step 3: add sparse prior overrides only where conviction is real

Typical candidates:

  • price
  • distribution
  • a small number of strategically important baseline controls

Typical non-candidates:

  • every media term
  • every generated seasonal component
  • every term simply because the data are noisy

Step 4: keep the reasoning on the original outcome scale

DSAMbayes can scale internally when scale = TRUE, but your reasoning about priors should still happen on the original business scale. Ask:

  • what outcome change would this prior imply?
  • would that be plausible for this KPI?
  • would I be comfortable defending it in a model review?

Prior predictive discipline in DSAMbayes

The Bayesian workflow ideal is to inspect prior implications before posterior fitting. In practical DSAMbayes use today, that discipline is still partly analyst-driven.

v1.3.3 does not yet provide a fully productised, first-class prior-predictive runner stage with its own public gate contract. So the current disciplined approach is:

  1. keep the prior design simple
  2. reason on the original outcome scale
  3. avoid over-confident overrides unless they are well supported
  4. fit the model and then inspect whether the posterior behavior is compatible with the prior story and the data

That makes the lack of a first-class prior-predictive stage a reason to be more conservative, not less.

Prior calibration and sensitivity loop

Prior calibration and sensitivity loop Prior calibration and sensitivity loop

For DSAMbayes users, a prior grid is a robustness tool, not a scoring contest. Use it to check whether the substantive conclusion survives plausible prior choices. Do not use it to hunt for the prior that makes one run look best.

How to know whether the priors are doing sensible work

After fitting, ask:

Are the intervals still wide?

If yes, the data may simply be weak. Do not respond automatically by tightening priors.

Are signs unstable without a clear design reason?

Check the baseline structure, controls, collinearity, and response-scale choice before strengthening priors.

Are coefficients pinned to hard bounds?

That can indicate that the boundary is too strong, or that the model is trying to express a structure the current formula does not support well.

A simple prior-setting decision table

Situation Recommended action
First pass on a standard MMM Use defaults, then add only obvious sign boundaries
Strong business reason for one control sign or magnitude Add one sparse override or boundary
Weak identification and high collinearity Diagnose design first; do not immediately tighten priors
Short dataset with many channels Accept that intervals may stay wide; simplify model before forcing strong priors
Reviewer asks “why this prior?” Be able to answer in one sentence on business or structural grounds

Cross-references

Stage 4: Computation and Sampler

Objective

Decide whether the posterior draws are numerically trustworthy.

This stage is about computation quality, not business interpretation and not causal validity. If it fails, every downstream quantity that depends on posterior draws becomes unreliable.

The key question

Before asking whether the model is good, ask whether the sampler actually explored the posterior well enough for the summaries to mean what they appear to mean.

In DSAMbayes, this is the stage where you care most about:

  • divergences
  • Rhat
  • effective sample size
  • treedepth and BFMI when available

1. Divergences

Any non-zero divergences should be treated seriously. They are often the strongest sign that the sampler struggled with posterior geometry.

Typical actions:

  • increase adapt_delta
  • simplify the model
  • revisit boundaries or extreme prior choices
  • inspect whether a hierarchical structure is too ambitious for the data

2. Rhat

Rhat answers: did the chains mix into the same posterior region?

Practical rule:

  • at or below 1.01 is the target
  • above 1.01 fails the publish and strict diagnostics policies

3. Effective sample size

ESS answers: how much independent information do the posterior summaries really contain after accounting for autocorrelation?

Low ESS means:

  • interval estimates may be unstable
  • tail probabilities may be noisy
  • apparent posterior precision may be misleading

4. Treedepth and BFMI

These are geometry warnings. They often indicate a difficult posterior shape even when Rhat looks acceptable.

What DSAMbayes gives you

You can inspect this stage through:

  • chain_diagnostics(model) for interactive fitted models
  • 40_diagnostics/diagnostics_report.csv
  • 40_diagnostics/diagnostics_summary.txt
  • diagnostics plots and residual artifacts produced by the runner

The threshold reference lives in Diagnostics Gates. The task-oriented post-run triage guide lives in Interpret Diagnostics.

Minimal decision rule

Status Interpretation
pass Posterior draws are numerically acceptable for the configured policy mode
warn The run may still be usable, but the warning must be understood and disclosed
fail Do not use decomposition, optimisation, or reporting outputs for business decisions

What Stage 4 does not tell you

Passing Stage 4 does not mean:

  • the model fits the data well
  • the residual structure is acceptable
  • the decomposition is substantively meaningful
  • the media effects are causally identified

It only means the sampler did a credible job approximating the posterior of the model you gave it.

Common mistake

The most common analytical error is to stop at Stage 4 and say “the model converged, so we can trust the answer.”

That is wrong.

Convergence tells you the computation is trustworthy. It does not tell you the model is adequate. That is the next stage.

What to do if Stage 4 fails

Start with the least cosmetic explanation:

  1. Is the model too complicated for the data?
  2. Is the baseline structure poorly separated from media?
  3. Are priors or boundaries too aggressive?
  4. Are hierarchical or pooled structures under-informed?

Only after that should you reach for sampler tuning.

Stage 5: Model Adequacy

Objective

Decide whether the fitted model is a credible description of the observed data.

This is the stage that sits between computational trust and business interpretation. A model can pass sampler diagnostics and still fail here.

The key question

If I simulate from the fitted model, does it reproduce the important structure of the observed data well enough for decomposition, comparison, and optimisation to be taken seriously?

What to inspect first

1. Fitted-versus-observed behavior over time

Ask:

  • does the model track the broad level and movement of the KPI?
  • are there long runs of systematic over- or under-prediction?
  • are key seasonal or event patterns still unexplained?

2. Posterior predictive checks

Posterior predictive plots tell you whether the fitted model can generate data that look like what you observed.

In DSAMbayes, this is the right way to think about the ppc.png artifact: not as decoration, but as an adequacy check.

3. Residual behavior

Residual autocorrelation or visible structure usually means the model has not absorbed an important baseline, timing, or event component.

4. LOO and Pareto-k

Model comparison and calibration-style plots help answer:

  • which candidate model predicts better?
  • are some observations highly influential?
  • is the leave-one-out approximation trustworthy?

These are useful, but they should not override a bad adequacy profile.

What this stage means for decisions

Decision gates for interpretation Decision gates for interpretation

The main practical consequence of Stage 5 is that predictive ranking and downstream business outputs are conditional on adequacy. Passing computation checks is not enough if the fitted model still behaves poorly against the data.

If adequacy is poor

Do not interpret decomposition shares as if they were stable statements about media contribution.

Do not treat optimisation outputs as reliable budget guidance.

Use the run to diagnose misspecification, then revise the model.

If adequacy is acceptable but not clean

A warning-level result may still be useful for exploratory work, but the caveat should travel with the output.

Adequacy is not the same as causality

A model can:

  • fit well
  • calibrate well
  • compare well by predictive metrics

and still produce biased media-effect interpretation if confounding or structural misspecification remains.

So Stage 5 is a gate on model adequacy, not proof of causal validity.

Practical DSAMbayes reading order

  1. Check Stage 4 first: are the draws trustworthy?
  2. Inspect fit plots and PPC
  3. Inspect residual diagnostics
  4. Inspect LOO / Pareto-k and compare candidate runs
  5. Only then interpret decomposition or decision-layer outputs

Common failure patterns

Pattern 1: good convergence, bad residual structure

Interpretation: the sampler worked, but the baseline or control structure is incomplete.

Pattern 2: good fit plot, unstable influential observations

Interpretation: apparent adequacy may depend too heavily on a small number of points. Treat model comparison and downstream interpretation cautiously.

Pattern 3: good predictive fit, weak causal story

Interpretation: the model may be operationally useful for forecasting or scenario analysis, but not for strong causal claims about media.

Relevant DSAMbayes surfaces

  • 40_diagnostics/diagnostics_report.csv
  • 40_diagnostics/ppc.png
  • residual plots in 40_diagnostics/
  • 50_model_selection/loo_pit.png
  • 50_model_selection/pareto_k.png
  • compare_runs()

Runner

Purpose

Document CLI and YAML runner contracts for reproducible DSAMbayes runs.

Audience

  • Users operating DSAMbayes through scripts/dsambayes.R.
  • Engineers maintaining runner config and artefact contracts.

Pages

Page Topic
CLI Usage Commands, flags, exit codes, and error modes
Config Schema YAML keys, defaults, and validation rules
Output Artefacts Staged folder layout, file semantics, and precedence rules

Subsections of Runner

CLI Usage

Purpose

Define the supported command-line interface for scripts/dsambayes.R, including required flags, optional flags, and execution semantics.

Prerequisites

Before using the CLI:

  • Complete Install and Setup.
  • Run commands from repository root.
  • Ensure DSAMbayes is installed in the role-specific R_LIBS_USER selected by dsambayes_set_r_library host.

Entry point

Rscript scripts/dsambayes.R <command> [flags]

The script supports these commands:

  • init
  • validate
  • run
  • help (or -h / --help)

Command summary

Command Required flags Optional flags Behaviour
init --out --template, --overwrite Writes a config template file.
validate --config --run-dir Runs config and data checks only (dry_run = TRUE).
run --config --run-dir Executes the full pipeline (dry_run = FALSE) and writes run artefacts.
help none none Prints usage text and exits.

Flag reference

init

  • --out <path> (required): output path for the generated YAML file.
  • --template <name> (optional): template name. Default is blm.
    • Supported values in script: master, blm, re, cre, pooled, hierarchical.
    • hierarchical maps to the same template file as re.
  • --overwrite (optional flag): allow overwrite of an existing --out file.

validate

  • --config <path> (required): YAML config path.
  • --run-dir <path> (optional): explicit run directory path.

run

  • --config <path> (required): YAML config path.
  • --run-dir <path> (optional): explicit run directory path.

Usage examples

Show help

Rscript scripts/dsambayes.R --help

Expected outcome: usage panel is printed with command syntax and notes.

Create a new config from template

Rscript scripts/dsambayes.R init --template blm --out config/local_quickstart.yaml

Expected outcome: config/local_quickstart.yaml is created.

Validate only (dry-run behaviour)

Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml

Expected outcome: validation completes without fitting Stan models.

Validate with explicit run directory

Rscript scripts/dsambayes.R validate \
    --config config/blm_timeseries.yaml \
    --run-dir results/quickstart_validate

Expected outcome: validation uses the provided run directory path when writing run metadata.

Execute full run

Rscript scripts/dsambayes.R run --config config/cre_geo_panel.yaml

Expected outcome: full modelling pipeline executes and artefacts are written under results/.

Execute full run with explicit run directory

Rscript scripts/dsambayes.R run \
    --config config/cre_geo_panel.yaml \
    --run-dir results/quickstart_run

Expected outcome: artefacts are written to results/quickstart_run (subject to overwrite rules in config).

Exit and error behaviour

  • Exit 0: command completed successfully. For run, this means the pipeline completed and diagnostics did not end in overall_status: fail.
  • Exit 1: run completed far enough to preserve the fitted result, but the outcome is non-publishable. This includes diagnostics overall_status: fail, diagnostics publish-gate failures, and post-fit artifact-write failures.
  • Exit 2: CLI argument, config, or runtime error before a completed run result could be returned.
  • Typical hard failures include:
    • DSAMbayes not installed.
    • Missing required flags (--out or --config).
    • Unknown command.
    • Unknown argument format.

Operational notes

  • validate is the recommended pre-run gate. Use it before run whenever you change config or data.
  • run prints a run summary and suggested next-step artefacts at completion.
  • The CLI itself does not define model semantics. It delegates execution to DSAMbayes::run_from_yaml().

Config Schema

Purpose

This page documents the authored YAML contract used by:

  • scripts/dsambayes.R
  • DSAMbayes::run_from_yaml()
  • runme.R

The authored schema is schema_version: 2 only. Older formula-driven YAML files are intentionally rejected.

Processing order

The runner processes configs in this order:

  1. Parse YAML.
  2. Coerce YAML infinity tokens (.Inf, -.Inf).
  3. Apply v2 defaults.
  4. Resolve relative paths against the config file directory.
  5. Validate the authored v2 contract.
  6. Compile the authored config into the internal runner config.
  7. Apply managed holiday terms, then build the model and run.

Root sections

Key Required Purpose
schema_version yes Must be 2.
data yes Input data path, format, and date handling.
target yes Outcome column, KPI type, and response transform.
media yes Modeled media terms.
controls yes Non-media predictors, including manual trend/seasonality terms.
effects no Managed effects. In M1 this is holidays only.
model yes Model class and scaling options.
hierarchy conditional Required for model.type: re and model.type: cre.
pooling conditional Required for model.type: pooled.
priors no Default priors plus grouped or explicit overrides.
boundaries no Grouped or explicit parameter boundaries.
fit no MCMC or optimise settings.
diagnostics no Diagnostics, model selection, and time-series selection settings.
allocation no Budget optimisation settings.
outputs no Output paths and artifact toggles.
forecast no Reserved forecast placeholder; currently only creates an empty stage directory when enabled.

Unknown keys fail validation.

Minimal valid config

schema_version: 2

data:
  path: ../data/timeseries/demo_data_synthetic.csv
  format: csv
  date_var: date

target:
  column: revenue
  type: revenue
  transform: identity

media:
  - channel0_signal
  - channel1_signal

controls:
  - t_scaled
  - sin52_1
  - cos52_1

model:
  type: blm

Key differences from the retired schema

  • model.formula is no longer authored directly.
  • schema_version: 1 configs are rejected.
  • Trend and seasonality stay user-authored as ordinary columns under controls.
  • Managed time effects are limited to holidays under effects.holidays.
  • re and cre models use hierarchy, not cre.enabled flags.
  • pooled models use pooling, not pooling.enabled.

Section reference

schema_version

Key Type Rules
schema_version integer Must be 2.

data

Key Type Rules
data.path string Required. File must exist. Relative paths resolve from the config directory.
data.format string csv, rds, or long.
data.date_var string Required in M1.
data.date_format string or null Optional parser format for date columns.
data.na_action string omit or error.
data.long_id_col string or null Required when data.format: long.
data.long_variable_col string or null Required when data.format: long.
data.long_value_col string or null Required when data.format: long.
data.dictionary_path string or null Optional metadata CSV.
data.dictionary mapping Optional inline metadata keyed by term name.

target

Key Type Rules
target.column string Required response column.
target.type string revenue or subscriptions.
target.transform string identity or log.
target.offset_column string or null Supported only for model.type: blm in M1.

media and controls

  • media is a required list of modeled media terms.
  • controls is a required list, but it may be empty ([]).
  • A term may not appear in both lists.
  • Manual trend and seasonality terms belong in controls.

Compiled formula order is:

  1. generated holiday terms
  2. controls
  3. media
  4. generated CRE mean terms
  5. optional offset
  6. hierarchical random-effects term

effects.holidays

Managed holidays are optional and are the only managed effect in M1.

effects:
  holidays:
    enabled: true
    path: ../data/holidays.csv
    label_col: holiday
    country: gb
    country_col: country
    week_start: monday
    prefix: holiday_
Key Type Rules
effects.holidays.enabled boolean Enables holiday feature generation.
effects.holidays.path string Required when enabled. CSV or RDS.
effects.holidays.date_col string or null Optional calendar date column override.
effects.holidays.label_col string Holiday label column.
effects.holidays.country string or null Optional single-country filter.
effects.holidays.country_col string Calendar column used with country.
effects.holidays.date_format string or null Optional parser format for non-ISO dates.
effects.holidays.week_start string monday through sunday.
effects.holidays.timezone string Timezone used in parsing/alignment. Must be a valid Olson timezone such as UTC.
effects.holidays.prefix string Prefix for generated holiday columns.
effects.holidays.window_before integer Non-negative.
effects.holidays.window_after integer Non-negative.
effects.holidays.aggregation_rule string count or any.
effects.holidays.overlap_policy string count_all or dedupe_label_date.
effects.holidays.overwrite_existing boolean Replaces existing columns only when true.

Notes:

  • The data date column must be aligned to the configured weekly anchor.
  • Country filtering materializes a filtered calendar artifact before the compiled config is written.

model

Key Type Rules
model.name string Defaults to the config filename stem.
model.type string blm, re, cre, or pooled.
model.scale boolean Controls internal scaling before fit.
model.force_recompile boolean Forces Stan recompilation when true.

hierarchy

Required for model.type: re and model.type: cre.

Key Type Rules
hierarchy.group string Grouping column for panel models.
hierarchy.random_intercept boolean Include `(1
hierarchy.random_slopes list of strings Optional subset of authored media and controls.
hierarchy.cre_variables list of strings Required and non-empty for model.type: cre.
hierarchy.cre_prefix string Prefix for generated CRE mean terms. Default cre_mean_.

pooling

Required for model.type: pooled.

Key Type Rules
pooling.grouping_vars list of strings Required and non-empty.
pooling.map_path string Required. CSV or RDS.
pooling.map_format string csv or rds.
pooling.min_waves integer or null Optional positive integer.

priors

Key Type Rules
priors.use_defaults boolean Must remain true in M1.
priors.likelihood mapping Optional explicit alias for noise_sd.
priors.overrides list Explicit parameter-level overrides.

Grouped families are available when applicable:

  • intercept
  • media_beta
  • control_beta
  • holiday_beta
  • cre_beta
  • pooling_beta
  • random_effect_sd
  • noise_sd

Each grouped family accepts either the legacy DSAMbayes style:

family: normal    # or lognormal_ms where supported
mean: 0
sd: 0.5

or the more explicit alias:

distribution: Normal   # or HalfNormal / LogNormalMS where supported
mu: 0
sigma: 0.5

HalfNormal compiles to a zero-centered Normal prior plus an implied lower bound of 0 for unconstrained targeted parameter(s). Parameters that are already positive by construction, such as noise_sd and hierarchical sd_*[...], do not receive an extra boundary row.

The residual-noise prior also accepts this alias:

priors:
  likelihood:
    sigma:
      distribution: HalfNormal
      sigma: 2

boundaries

Boundary families mirror the grouped prior families and may also use explicit boundaries.overrides.

Each grouped or explicit boundary row uses:

lower: -Inf
upper: Inf

fit

Key Type Rules
fit.method string mcmc or optimise. Pooled runs require mcmc.
fit.seed numeric or null Optional scalar seed.
fit.optimise.* mapping Optimisation controls.
fit.mcmc.* mapping Stan sampling controls.
fit.mcmc.parameterization.positive_priors string centered or noncentered.

diagnostics

Retains the current runner surface for:

  • model_selection
  • time_series_selection
  • identifiability
  • publish-gate controls

Important M1 rule:

  • diagnostics.time_series_selection.enabled: true is not supported for pooled runs.
  • time-series selection is advisory only in the current release contract; it is not part of publish-gate enforcement.
  • lower-level runner paths with adstock/Hill media_transforms are not supported by time-series selection.
  • diagnostics.time_series_selection.gap_weeks is optional, defaults to 0, and inserts an embargo between the training window and the scored holdout window.

allocation

Retains the current runner surface for budget optimisation, with channel targeting based on authored media terms.

outputs

outputs.root_dir and outputs.run_dir behave as before, but the metadata contract now includes:

  • config.original.yaml
  • config.resolved.yaml
  • config.compiled.yaml
  • outputs.save_model_rds controls the full fitted analysis artifact 20_model_fit/model.rds
  • outputs.save_deployment_model_rds controls the compact deployment artifact 20_model_fit/deployment_model.rds

Current first-slice limit:

  • outputs.save_deployment_model_rds: true is supported for model.type: blm, model.type: pooled with fit.method: mcmc, or hierarchical model.type: re / cre with fit.method: mcmc.
  • Pooled deployment artifacts score on authored terms and keep the normalized pooling map, but deployment-time newdata / data = ... does not need the pooling columns unless they are also ordinary formula terms.
  • Hierarchical deployment artifacts are seen-groups-only; explicit scoring/decomposition data must include the raw grouping columns, and decomposition also requires the response source column(s).

forecast

Reserved placeholder only. In v1.3.3, enabling forecast can materialise 70_forecast/, but the runner does not emit forecast files or plots.

Examples in this repository

  • config/blm_timeseries.yaml — weekly time-series BLM example
  • config/cre_geo_panel.yaml — weekly geo-panel CRE example

Output Artefacts

Purpose

This page defines what the YAML runner writes, where files are written, and which config flags control each artefact.

Related pages:

Run directory and layout semantics

Run directory precedence:

  1. CLI --run-dir
  2. outputs.run_dir
  3. Timestamped folder under outputs.root_dir

Layout behaviour:

  • outputs.layout: staged (default) writes files under numbered stage folders.
  • outputs.layout: flat writes all files directly under the run directory.

Stage folders used by the runner:

  • 00_run_metadata
  • 10_pre_run
  • 20_model_fit
  • 30_post_run
  • 40_diagnostics
  • 50_model_selection
  • 60_optimisation
  • 70_forecast (reserved; directory only when forecast.enabled: true)

Command behaviour

validate

  • validate uses dry_run = TRUE.
  • If no run directory is resolved, no artefacts are written.
  • If a run directory is resolved (--run-dir or outputs.run_dir), config.original.yaml is written.
  • If a run directory is resolved (--run-dir or outputs.run_dir), config.resolved.yaml is written.
  • If a run directory is resolved (--run-dir or outputs.run_dir), config.compiled.yaml is written.
  • If a managed holiday country filter is active and a run directory is resolved, holiday_calendar.filtered.csv is materialised under 10_pre_run/.
  • If a run directory is resolved and outputs.save_session_info_txt: true, session_info.txt is written.
  • If forecast is enabled and a run directory is materialised, the 70_forecast/ directory is created.

run

  • run writes the full artefact set subject to config toggles and runtime conditions.

Artefact contract by stage

00_run_metadata

File Controlled by Written when Notes
config.original.yaml always run dir materialised Raw YAML text from the input config.
config.resolved.yaml always run dir materialised Authored config after defaults, path resolution, and v2 schema validation.
config.compiled.yaml always run dir materialised Internal compiled runner config after the friendly YAML is translated into the downstream runtime shape.
artifact_schema.yaml always run dir materialised Machine-readable runner artifact contract marker. Includes artifact_schema_version and the active artifact layout (staged or flat) so downstream tooling can reason about cross-version comparisons.
run_status.yaml best-effort run dir materialised Machine-readable terminal run outcome. The runner attempts to write it for dry runs, fit failures after metadata creation, successful completions, diagnostics publish-gate failures, and post-fit artifact-write failures. Severe file-system failures can still prevent the file from being created.
session_info.txt outputs.save_session_info_txt flag is true Includes DSAMbayes version, artifact schema version, config schema version, model/fit metadata, and sessionInfo().

10_pre_run

File Controlled by Written when Notes
transform_assumptions.txt outputs.save_transform_assumptions_txt flag is true Written even if transform sensitivity scenarios are disabled.
transform_sensitivity_summary.csv outputs.save_transform_sensitivity_summary_csv sensitivity object exists with rows Requires transforms.sensitivity.enabled: true and successful scenario execution.
transform_sensitivity_parameters.csv outputs.save_transform_sensitivity_parameters_csv sensitivity object exists with rows Parameter means/SD by scenario.
dropped_groups.csv none groups dropped by pooling.min_waves filter Written only when sparse groups are excluded.
holiday_calendar.filtered.csv none managed holidays enabled with a country filter Materialised filtered holiday calendar consumed by config.compiled.yaml.
holiday_feature_manifest.csv none managed holidays enabled and features generated Documents generated holiday terms and active-week counts.
design_matrix_manifest.csv outputs.save_design_matrix_manifest_csv flag is true and manifest non-empty Per-term design metadata.
data_dictionary.csv outputs.save_data_dictionary_csv flag is true and dictionary table non-empty Merges inline YAML metadata and optional CSV dictionary metadata.
spec_summary.csv outputs.save_spec_summary_csv flag is true and table available Single-row model/spec summary.
vif_report.csv outputs.save_vif_report_csv flag is true and predictors available VIF diagnostics for non-intercept predictors.

20_model_fit

File Controlled by Written when Notes
model.rds outputs.save_model_rds flag is true Fitted model object.
deployment_model.rds outputs.save_deployment_model_rds flag is true and the fitted model is either model.type: blm, model.type: pooled with fit.method: mcmc, or hierarchical model.type: re/cre with fit.method: mcmc Compact deployment artifact for explicit predict(newdata = ...) and explicit-data decomposition. It is additive to model.rds and does not replace the full analysis object. Pooled deployment artifacts retain authored-term scoring behavior without shipping runtime dimension_map state. Hierarchical deployment artifacts are seen-groups-only; explicit prediction and decomposition data must include raw grouping columns, and decomposition also requires the response source column(s).
posterior.rds outputs.save_posterior_rds flag is true and MCMC fit Raw posterior object for MCMC runs only.
fit_metrics_by_group.csv implicit fitted summary is computed Written when any of save_fitted_csv, save_fit_png, save_residuals_csv, save_diagnostics_png is true.
fit_timeseries.png outputs.save_fit_png flag is true and ggplot2 installed Observed vs fitted over time on the model response scale, with a subtitle that states the model form (levels or semilog), the displayed scale, fit metrics including Classical R^2 (posterior mean), and monthly date labels when date is a true Date.
fit_scatter.png outputs.save_fit_png flag is true and ggplot2 installed Observed vs fitted scatter on the model response scale, with a subtitle that states the model form (levels or semilog) and the displayed scale.
posterior_forest.png none posterior draws available and ggplot2 installed Posterior coefficient forest plot; skipped for optimise/MAP runs.
prior_posterior.png none posterior draws available, model has priors, and ggplot2 installed Prior-versus-posterior comparison plot; skipped for optimise/MAP runs.

30_post_run

File Controlled by Written when Notes
observed.csv outputs.save_observed_csv flag is true Observed response on model response scale.
observed_kpi.csv outputs.save_observed_csv flag is true and response scale is log KPI-scale observed values (exp) with conversion_method = point_exp.
fitted.csv outputs.save_fitted_csv flag is true Fitted summaries on model response scale.
fitted_kpi.csv outputs.save_fitted_csv flag is true and response scale is log KPI-scale fitted summaries (exp).
posterior_summary.csv outputs.save_posterior_summary_csv flag is true and MCMC fit Posterior summaries for coefficients and scalar diagnostics.
decomp_predictor_impact.csv outputs.save_decomp_csv flag is true and response decomposition tables are available Predictor-level contribution table. If decomposition cannot be computed, the runner records a skip in artifact_status.csv.
decomp_timeseries.csv outputs.save_decomp_csv flag is true and response decomposition tables are available Long-format contribution-by-date table. If decomposition cannot be computed, the runner records a skip in artifact_status.csv.
decomp_predictor_impact.png outputs.save_decomp_png flag is true, decomposition tables are available, and ggplot2 installed Predictor-impact decomposition plot.
decomp_timeseries.png outputs.save_decomp_png flag is true, decomposition tables are available, and ggplot2 installed Media-contribution time-series decomposition plot.

Active v1.3 pipeline note:

  • 30_post_run/ emits observed, fitted, posterior summary, and decomposition artifacts when the corresponding output toggles are enabled and decomposition can be computed from the fitted model.
  • When decomposition is unavailable, the runner records deterministic skip rows in 40_diagnostics/artifact_status.csv rather than silently dropping the contract entries.

40_diagnostics

File Controlled by Written when Notes
chain_diagnostics.txt outputs.save_chain_diagnostics_txt flag is true and MCMC fit Chain diagnostics text output.
diagnostics_report.csv outputs.save_diagnostics_report_csv flag is true and diagnostics object exists One row per diagnostic check.
diagnostics_summary.txt outputs.save_diagnostics_summary_txt flag is true and diagnostics object exists Counts by status and overall status.
artifact_status.csv none artifact status rows recorded by the runner Per-artifact status log for skipped/warn/error events.
residuals.csv outputs.save_residuals_csv flag is true and fitted summary is computed Residual table on response scale.
residuals_timeseries.png outputs.save_diagnostics_png flag is true and ggplot2 installed Residuals over time.
residuals_vs_fitted.png outputs.save_diagnostics_png flag is true and ggplot2 installed Residuals vs fitted.
residuals_hist.png outputs.save_diagnostics_png flag is true and ggplot2 installed Residual histogram.
residuals_acf.png outputs.save_diagnostics_png flag is true and ggplot2 installed Residual autocorrelation plot.
residual_diagnostics.csv none diagnostics residual checks available Ljung-Box / ACF check outputs.
residuals_latent.csv none diagnostics latent residuals available Latent residual series from diagnostics object.
residuals_latent_acf.png outputs.save_diagnostics_png latent residuals available and ggplot2 installed Latent residual ACF plot.
ppc.png none posterior predictive plot available and ggplot2 installed Posterior predictive check plot; skipped for optimise/MAP runs.
boundary_hits.csv none boundary-hit table available Boundary-hit rates per parameter.
boundary_hits.png outputs.save_diagnostics_png boundary-hit table available and ggplot2 installed Boundary-hit visualisation.
within_variation.csv none within-variation table available Within-variation diagnostics for hierarchical terms.
within_variation.png outputs.save_diagnostics_png within-variation table available and ggplot2 installed Within-variation visualisation.
predictor_risk_register.csv outputs.save_predictor_risk_register_csv flag is true and table non-empty Ranked risk register combining VIF, within-variation, boundary hits, and slow-moving flags.

50_model_selection

File Controlled by Written when Notes
loo_summary.csv outputs.save_model_selection_csv flag is true, diagnostics.model_selection.enabled: true, and diagnostics report exists May be full PSIS-LOO summary or a stub row with skip reason. A successful summary records the conditional-exchangeability assumption and directs time-ordered selection to blocked or leave-future-out CV.
loo_pointwise.csv outputs.save_model_selection_pointwise_csv flag is true, diagnostics report exists, and pointwise PSIS-LOO is available Optional pointwise LOO diagnostics.
loo_pit.png none posterior predictive draws available and ggplot2 installed LOO-PIT calibration plot.
pareto_k.png outputs.save_diagnostics_png pointwise PSIS-LOO available and ggplot2 installed Pareto-k diagnostic plot.
elpd_influence.png outputs.save_diagnostics_png pointwise PSIS-LOO available and ggplot2 installed Pointwise ELPD influence plot.
tscv_folds.csv diagnostics.time_series_selection.enabled time-series selection enabled and folds produced Fold windows plus the active TSCV policy (method, horizon_weeks, stride_weeks, min_train_weeks, gap_weeks) and fold-level runtime/status metadata.
tscv_summary.csv diagnostics.time_series_selection.enabled time-series selection enabled Written for success, skipped, or error outcomes; each row also carries the active TSCV policy fields.
tscv_pointwise.csv diagnostics.time_series_selection.enabled + diagnostics.time_series_selection.save_pointwise enabled and pointwise rows available Optional pointwise holdout log predictive densities.
tscv_elpd_by_fold.png diagnostics.time_series_selection.save_png + outputs.save_diagnostics_png enabled and ggplot2 installed ELPD-by-fold chart.

60_optimisation

File Controlled by Written when Notes
optimisation_runs.csv none fit.method: optimise All optimisation starts, including objective value and return code when available.
optimisation_best.csv none fit.method: optimise The selected MAP optimum: highest optimiser objective when available, otherwise lowest RMSE.
budget_summary.csv outputs.save_allocator_csv allocation enabled and flag is true Scenario-level optimisation summary.
budget_allocation.csv outputs.save_allocator_csv allocation enabled and flag is true Recommended allocation by channel.
budget_diagnostics.csv outputs.save_allocator_csv allocation enabled and flag is true Candidate and objective diagnostics.
budget_response_curves.csv outputs.save_allocator_csv allocation enabled and flag is true Response-curve payload.
budget_response_points.csv outputs.save_allocator_csv allocation enabled and flag is true Key plotted points for response curves.
budget_roi_cpa.csv outputs.save_allocator_csv allocation enabled and flag is true ROI/CPA panel payload (depends on KPI type).
budget_impact.csv outputs.save_allocator_csv allocation enabled and flag is true Allocation impact payload.
budget_response_curves.png outputs.save_allocator_png allocation enabled, flag is true, and ggplot2 installed Response curves plot.
budget_roi_cpa.png outputs.save_allocator_png allocation enabled, flag is true, and ggplot2 installed ROI/CPA panel plot.
budget_impact.png outputs.save_allocator_png allocation enabled, flag is true, and ggplot2 installed Allocation impact plot.
budget_optimisation.json outputs.save_allocator_json allocation enabled, flag is true, and jsonlite installed Combined JSON payload (summary, allocation, diagnostics, plot_data).

70_forecast

Item Controlled by Written when Notes
70_forecast/ directory forecast.enabled flag is true Directory is created, but no forecast data, tables, or plots are emitted by runner writers in v1.3.3.

Deployment artifact note:

  • deployment_model.rds lives under 20_model_fit/, not 70_forecast/.
  • It is a compact packaging artifact for deployment consumers, not a signal that the runner now generates future-data forecasts or scenarios.

Response scale semantics (*_kpi.csv vs base files)

Base files (observed.csv, fitted.csv) are always on the model response scale:

  • identity response: KPI units
  • log response: log(KPI)

KPI-scale files are written only for log-response models:

  • observed_kpi.csv
  • fitted_kpi.csv

Conversion metadata:

  • observed_kpi.csv uses conversion_method = point_exp.
  • fitted_kpi.csv uses conversion_method = lognormal_mean by default for log-response fitted values.
  • fitted_kpi.csv uses conversion_method = point_exp only when the median back-transform is explicitly requested.

Diagnostics status semantics

diagnostics_report.csv status values:

  • pass: check passed configured thresholds
  • warn: check breached warning threshold
  • fail: check breached fail threshold
  • skipped: check not applicable or intentionally skipped

Overall status logic:

  • fail if any check is fail
  • warn if no fails and at least one warn
  • pass otherwise

diagnostics_summary.txt reports:

  • overall_status
  • counts for pass, warn, fail, skipped

Quick verification commands

List produced files for a run:

latest_run="$(ls -td results/* | head -n 1)"
find "$latest_run" -type f | sort

Inspect key diagnostics files:

latest_run="$(ls -td results/* | head -n 1)"
head -n 20 "$latest_run/40_diagnostics/diagnostics_report.csv"
head -n 20 "$latest_run/40_diagnostics/diagnostics_summary.txt"

Modelling

Purpose

Describe model classes, inference contracts, diagnostics, and decision-layer semantics for DSAMbayes. This section is primarily reference material; use Principled Bayesian Workflow for the methodology spine.

Audience

  • Practitioners building and interpreting DSAMbayes models.
  • Reviewers validating modelling assumptions and outputs.

Pages

Page Topic
Model Classes BLM, hierarchical, and pooled class constructors, fit support, and limitations
Model Object Lifecycle State transitions from construction through fitting to post-fit extraction
Priors and Boundaries Prior schema, defaults, overrides, boundary controls, and scale semantics
Minimal-Prior Policy Governance guidance for prior specification in MMM
Response Scale Semantics Identity vs log response, KPI-scale conversion, Jensen-safe reporting
Diagnostics Gates Policy modes, threshold tables, identifiability gate, and remediation actions
CRE / Mundlak Correlated random effects for hierarchical models
Time Components Managed holiday feature generation and weekly anchoring
Budget Optimisation Decision-layer budget allocation, objectives, risk scoring, and response transforms

Subsections of Modelling

Model Classes

Purpose

DSAMbayes provides three model classes for Bayesian marketing mix modelling. Each class targets a different data structure and pooling strategy. This page describes the constructor pathways, fit support, and practical limitations of each class so that an operator can select the appropriate model for a given dataset.

Use this page to choose the right modelling surface for your data structure and decision problem. Do not use it as a substitute for the broader workflow: class selection does not settle prior design, computational trustworthiness, model adequacy, or causal interpretation. For that framing, start with What Principled Means, Stage 2: Model and Priors, and Stage 5: Model Adequacy.

Selection discipline

  • Choose the simplest class that matches the real data structure.
  • Do not move to hierarchical or pooled models only because they look more advanced.
  • Treat model class as a structural choice, not proof that the resulting model is decision-ready.
  • After choosing a class, return to the workflow pages for prior-setting, diagnostics, and interpretation discipline.

Class summary

Class S3 class chain Constructor Data structure Grouping Typical use case
BLM blm blm(formula, data) Single market/brand None One-market regression with full prior and boundary control
Hierarchical hierarchical, blm blm(formula, data) with (term &#124; group) syntax Panel (long format) Random effects by group Multi-market models sharing strength across groups
Pooled pooled, blm pool(blm_obj, grouping_vars, map) Single market Structured coefficient pooling via dimension map Single-market models with media coefficients pooled across labelled dimensions

BLM (blm)

Construction

model <- blm(kpi ~ m_tv + m_search + trend + seasonality, data = df)

blm() dispatches on the first argument. When passed a formula, it creates a blm object with default priors and boundaries. When passed an lm object, it creates a bayes_lm_updater whose priors are initialised from the OLS coefficient estimates and standard errors.

Fit support

Method Function Backend
MCMC fit(model, ...) rstan::sampling()
MAP fit_map(model, n_runs, ...) rstan::optimizing() (repeated starts)

Post-fit accessors

  • get_posterior() — coefficient draws, fitted values, metrics
  • fitted() — predicted response on the original scale
  • decomp() — predictor-level decomposition via DSAMdecomp
  • optimise_budget() — decision-layer budget allocation

Limitations

  • No group structure. For multi-market data, use the hierarchical class.
  • optimise_budget() aborts if scale=TRUE and an offset is present (unsupported combination for the bayes_lm_updater Stan template).

Hierarchical (hierarchical)

Construction

The hierarchical class is created automatically when blm() detects random-effects syntax (|) in the formula:

model <- blm(
  kpi ~ m_tv + m_search + trend + (1 + m_tv + m_search | market),
  data = panel_df
)

Terms to the left of | become random slopes; the variable to the right defines the grouping factor. Multiple grouping terms are supported.

CRE / Mundlak extension

For correlated random effects, call set_cre() after construction:

model <- set_cre(model, vars = c("m_tv", "m_search"))

This augments the population formula with group-mean terms (cre_mean_*) and updates priors and boundaries accordingly. See CRE / Mundlak for details.

Fit support

Method Function Backend
MCMC fit(model, ...) rstan::sampling()
MAP fit_map(model, n_runs, ...) rstan::optimizing() (repeated starts)

Post-fit accessors

Same as BLM. Coefficient draws from get_posterior() return vectors (one value per group) rather than scalars. Budget optimisation uses the population-level (fixed-effect) coefficient draws from the beta parameter.

Limitations

  • Stan template compilation uses a templated source (general_hierarchical.stan) rendered per number of groups and parameterisation mode. First compilation is slow; subsequent runs use a cached binary.
  • Response decomposition via model.matrix() may fail for formulas containing | syntax. The runner wraps this in tryCatch and skips gracefully.
  • Posterior forest and prior-vs-posterior plots average group-specific draws to produce a single population-level estimate.
  • Offset support in the hierarchical Stan template is handled via stats::model.offset() within build_hierarchical_frame_data().

Pooled (pooled)

Construction

The pooled class is created by converting an existing BLM object with pool():

base <- blm(kpi ~ m_tv + m_search + trend + seasonality, data = df)
model <- pool(base, grouping_vars = c("channel"), map = pooling_map)

The map is a data frame with a variable column mapping formula terms to pooling dimension labels. Exact formula-term labels are preferred; raw variable names are accepted only when they resolve unambiguously to a single non-offset formula term. Priors and boundaries are reset to defaults when pool() is called.

Fit support

Method Function Backend
MCMC fit(model, ...) rstan::sampling()

MAP fitting (fit_map) is not currently implemented for pooled models.

Post-fit accessors

Same as BLM. The design matrix is split into base terms (intercept + non-pooled) and media terms (pooled). The Stan template uses a per-dimension coefficient structure.

Limitations

  • MAP fitting is not available.
  • extract_stan_design_matrix() may return a zero-row matrix, which causes VIF computation to be skipped.
  • The pooled Stan cache key includes sorted grouping variable names to avoid collisions between different pooling configurations.
  • Time-series cross-validation is available for pooled MCMC models, subject to the same media-transform restrictions as other classes.

Class selection guide

Scenario Recommended class Rationale
Single market, sufficient data BLM Simplest pathway; full accessor and optimisation support
Single market, OLS baseline available BLM via blm(lm_obj, data) Priors initialised from OLS; Bayesian updating
Multi-market panel Hierarchical Partial pooling shares strength across markets
Multi-market panel with confounding concerns Hierarchical + CRE Mundlak terms control for between-group confounding
Single market with structured media dimensions Pooled Coefficient pooling across labelled media categories

In practice, the class decision should usually be driven by three questions:

  1. Is the dataset a single time series or a grouped panel?
  2. Do you need partial pooling across real groups, or pooling across labelled coefficient dimensions?
  3. Is the added structure necessary for the business question, or are you adding complexity without a clear identifiability benefit?

Fit method selection

Criterion MCMC (fit) MAP (fit_map)
Full posterior Yes No (point estimate only)
Credible intervals Yes No; restart diagnostics only
Diagnostics (Rhat, ESS, divergences) Yes Not applicable
LOO-CV / model selection Yes Not supported
Speed Minutes to hours Seconds to minutes
Budget optimisation Full posterior-based Point-estimate-based

For production runs where diagnostics and uncertainty quantification matter, MCMC is the recommended fit method. MAP is useful for rapid iteration during model development.

MAP returns one selected optimum, not posterior draws. Do not derive credible intervals or MCMC diagnostics from it; its point estimate can understate uncertainty, especially for hierarchical variance components. fit_map() retains the restart results for inspection (and runner outputs include optimisation_runs.csv), so materially different restart objectives should be treated as optimisation instability or competing local optima, not as a substitute for posterior uncertainty.

Cross-references

Model Object Lifecycle

DSAMbayes model objects (blm, hierarchical, pooled) are mutable S3 lists that progress through a well-defined sequence of states. Understanding these states helps avoid calling post-fit accessors on an unfitted object or forgetting to compile before fitting.

This page is an API/runtime reference. It explains how DSAMbayes model objects move through construction, compilation, fitting, and post-fit access. It is not the main guide for prior-setting, diagnostics meaning, or model adequacy. For that, use the Principled Bayesian Workflow.

State-machine diagram

                        ┌─────────────────────────────────────────┐
                        │           CREATED                       │
                        │  blm(), blm.formula(), blm.lm(),        │
                        │  as_bayes_lm_updater()                  │
                        │  Fields set: .formula, .original_data,  │
                        │    .prior, .boundaries                  │
                        └──────────────┬──────────────────────────┘
                                       │
            ┌──────────────────────────┼──────────────────────────┐
            ▼                          ▼                          ▼
    set_prior(obj, …)         set_boundary(obj, …)        set_date(obj, …)
    Mutates .prior            Mutates .boundaries         Sets .date_var
            │                          │                          │
            └──────────────────────────┼──────────────────────────┘
                                       │
                    (optional: pool() transitions blm → pooled,
                     resets .prior/.boundaries, adds .pooling_vars/.pooling_map)
                                       │
                                       ▼
                        ┌─────────────────────────────────────────┐
                        │           CONFIGURED                    │
                        │  Priors, boundaries, date variable are  │
                        │  set (may still use defaults).          │
                        └──────────────┬──────────────────────────┘
                                       │
                                       ▼
                        ┌─────────────────────────────────────────┐
                        │           COMPILED                      │
                        │  compile_model(obj)                     │
                        │  Sets .stan_model                       │
                        │  (pre_flight_checks auto-compiles if    │
                        │   .stan_model is NULL)                  │
                        └──────────────┬──────────────────────────┘
                                       │
                                       ▼
                        ┌─────────────────────────────────────────┐
                        │           PRE-FLIGHTED                  │
                        │  pre_flight_checks(obj, data)           │
                        │  Validates formula/data compatibility,  │
                        │  auto-compiles and auto-sets date_var   │
                        │  if missing. Sets .response_transform,  │
                        │  .response_scale.                       │
                        └──────────────┬──────────────────────────┘
                                       │
                                       ▼
                        ┌─────────────────────────────────────────┐
                        │           FITTED                        │
                        │  fit(obj) / fit_map(obj)                │
                        │  Calls pre_flight_checks internally,    │
                        │  then prep_data_for_fit → rstan.        │
                        │  Sets .stan_data, .date_val, .posterior │
                        └──────────────┬──────────────────────────┘
                                       │
                   ┌───────────────────┼───────────────────┐
                   ▼                   ▼                   ▼
           get_posterior(obj)    fitted(obj)         decomp(obj)
           Returns tibble of    Predicted values    Decomposition
           posterior draws      (yhat)              via DSAMdecomp
                   │                                       │
                   ▼                                       ▼
           optimise_budget(obj, …)                         Further analysis
           Budget allocation
           (requires fitted model)

States and key fields

State Entry point Fields populated
Created blm(), blm.formula(), blm.lm(), as_bayes_lm_updater() .formula, .original_data, .prior, .boundaries, .response_transform, .response_scale
Configured set_prior(), set_boundary(), set_date() Mutates .prior, .boundaries, .date_var
Pooled pool(obj, grouping_vars, map) Adds .pooling_vars, .pooling_map; resets .prior, .boundaries; class becomes pooled
Compiled compile_model(obj) .stan_model
Pre-flighted pre_flight_checks(obj, data) .response_transform, .response_scale; auto-sets .stan_model, .date_var if missing
Fitted fit(obj) / fit_map(obj) .stan_data, .date_val, .posterior

Post-fit accessors

These functions require a fitted model (.posterior is not NULL):

Accessor Returns Notes
get_posterior(obj) Tibble of posterior draws (coefficients, metrics, yhat) Back-transforms to original scale when scale=TRUE
fitted(obj) Predicted values (yhat) on original scale
get_optimisation(obj) Optimisation results tibble Only for MAP-fitted models (.posterior inherits optimisation)
decomp(obj) Predictor-level decomposition via DSAMdecomp
optimise_budget(obj, …) Budget allocation results Requires fitted model with media terms
chain_diagnostics(obj) MCMC chain diagnostic summary Only for MCMC-fitted models

Guards and auto-transitions

  • pre_flight_checks() auto-compiles via compile_model() if .stan_model is NULL, and auto-sets .date_var to "date" if not already set.
  • fit() and fit_map() call pre_flight_checks() internally, so explicit compilation is optional.
  • get_posterior() aborts with a clear error if .posterior is NULL.
  • optimise_budget() aborts if the model has scale=TRUE and an offset is present (unsupported combination for the bayes_lm_updater class).

Object field reference

All fields are initialised by model_object_schema_defaults() in R/model_schema.R. The canonical field list:

Field Type Set by
.original lm object or NULL Constructor
.formula formula Constructor
.original_data data.frame Constructor
.response_transform character(1) Constructor / pre_flight_checks
.response_scale character(1) Constructor / pre_flight_checks
.prior tibble Constructor / set_prior
.boundaries tibble Constructor / set_boundary
.stan_model stanmodel compile_model
.stan_data list prep_data_for_fit (via fit)
.posterior stanfit or optimisation fit / fit_map
.fitted logical(1) Internal
.offset matrix or NULL prep_offset (via fit)
.date_var character(1) set_date / pre_flight_checks
.date_val vector fit / fit_map
.cre list or NULL apply_cre_data (hierarchical)
.pooling_vars character pool()
.pooling_map data.frame pool()
.positive_prior_parameterization character(1) Runner config

Runner-injected fields

These are set by the YAML/CLI runner (run_from_yaml()) for artifact writing and are not part of the core modelling API:

  • .runner_config, .runner_kpi_type, .runner_identifiability
  • .runner_time_components, .runner_budget_optimisation
  • .runner_model_selection, .runner_model_type

Priors and Boundaries

For the workflow guidance behind these controls, start with Stage 2: Model and Priors. This page is the technical contract for DSAMbayes prior and boundary behavior.

Use this page when you need exact DSAMbayes semantics: supported prior families, override syntax, default generation rules, and scaling behavior. Do not use it as the main argument for why a prior is reasonable. That reasoning belongs in the workflow pages and in your modelling rationale.

Purpose

This page defines how DSAMbayes specifies, defaults, overrides, and scales coefficient priors and parameter boundaries for all model classes. It covers the prior schema, supported families, default-generation logic, YAML override contract, and the interaction between priors, boundaries, and the scale=TRUE pathway.

How to read this page

  • Use Minimal-Prior Policy if you want the short recommended operating rule.
  • Use this page when you need to know exactly how DSAMbayes will interpret a prior or boundary specification.
  • Return to Stage 2: Model and Priors if the question is whether a custom prior should be added at all.

Prior schema

Each model object carries a .prior tibble with one row per parameter. The columns are:

Column Type Meaning
parameter character Parameter name (matches design-matrix column or special name)
description character Human-readable label
distribution call R distribution call, e.g. normal(0, 5)
is_default logical Whether the row was generated by default_prior()

Supported prior families

Family Stan encoding Use case
normal(mean, sd) Default (prior_family_noise_sd = 0) Coefficient priors (location–scale)
lognormal_ms(mean, sd) Encoded as prior_family_noise_sd = 1 with log-transformed parameters noise_sd prior when positive-support is desired

All coefficient priors use normal(). The lognormal_ms family is available only for the noise_sd parameter and is parameterised by the mean and standard deviation on the original (non-log) scale; DSAMbayes converts these internally to log-space parameters.

Default prior generation

BLM and hierarchical (population terms)

default_prior.blm() calls standard_prior_terms(), which produces normal(0, 5) for each population-formula term (intercept and slope terms) plus a noise_sd entry.

Hierarchical (group-level standard deviations)

default_prior.hierarchical() additionally generates sd_<idx>[<term>] rows for each group factor. The prior standard deviation is set to the between-group standard deviation of the response, rounded to two decimal places.

BLM from lm (Bayesian updating)

default_prior.bayes_lm_updater() initialises coefficient priors from the OLS point estimates (mean) and standard errors (sd), enabling informative Bayesian updating.

Pooled

default_prior.pooled() uses the BLM defaults for non-pooled terms (intercept, base regressors, noise_sd) and normal(0, 5) for each dimension-level pooled coefficient. Default pooled boundaries remain unconstrained; add explicit boundaries if a pooled dimension should be sign-restricted.

Boundary schema

Each model object carries a .boundaries tibble with one row per parameter:

Column Type Meaning
parameter character Parameter name
description character Human-readable label
boundary list-column List with $lower and $upper (numeric scalars)
is_default logical Whether the row was generated by default_boundary()

Default boundaries are lower = -Inf, upper = Inf for all terms. No sign constraints are imposed by default.

YAML override contract

Prior overrides

priors:
  use_defaults: true
  overrides:
    - { parameter: m_tv, mean: 0.5, sd: 0.2 }
    - { parameter: price_index, mean: -0.2, sd: 0.1 }

Each override replaces the distribution call for the named parameter with normal(mean, sd). Overrides are sparse: only the listed parameters are changed; all other parameters keep their defaults.

In M1, use_defaults must remain true. The v2 runner is default-first: it always starts from the generated prior table, then applies sparse grouped aliases and explicit overrides.

The friendly YAML surface also accepts an explicit alias style:

priors:
  media_beta:
    distribution: HalfNormal
    sigma: 1

or:

priors:
  intercept:
    distribution: Normal
    mu: 120000
    sigma: 15000

HalfNormal is implemented by compiling to normal(0, sigma) plus an implied lower bound of 0 on targeted parameters that are otherwise unconstrained. The priors.likelihood.sigma alias compiles to the DSAMbayes noise_sd prior family.

Boundary overrides

boundaries:
  overrides:
    - { parameter: m_tv, lower: 0.0, upper: .Inf }
    - { parameter: competitor_discount, lower: -.Inf, upper: 0.0 }

Each override replaces the boundary entry for the named parameter. YAML infinity tokens (.Inf, -.Inf) are coerced during config resolution.

Scale semantics (scale = TRUE)

When model.scale: true (the default), the response and predictors are standardised before Stan fitting. This affects both priors and boundaries.

Coefficient prior scaling

Prior standard deviations are scaled by the ratio sx / sy for slope terms and by 1 / sy for the intercept. The noise_sd prior standard deviation is multiplied by sy (the response standard deviation) to remain interpretable in the scaled space.

Boundary scaling

  • Zero boundaries (0) are invariant under scaling.
  • Infinite boundaries (±Inf) are invariant under scaling.
  • Finite non-zero boundaries for slope terms are scaled using scale_boundary_for_parameter(), which applies the same sx / sy ratio used for slope priors.
  • If a finite non-zero boundary is specified for a parameter without a matching scale factor in the design matrix, DSAMbayes aborts with a validation error.

Practical implication

Users specify priors and boundaries on the original (unscaled) data scale. DSAMbayes converts them internally before passing data to Stan. Post-fit, coefficient draws are back-transformed to the original scale by get_posterior().

Interaction with model classes

Behaviour BLM Hierarchical Pooled
Default priors normal(0, 5) per term Population: same as BLM; group SD: data-derived Non-pooled: BLM defaults; pooled: normal(0, 5) per dimension
Boundary defaults (-Inf, Inf) per term Same as BLM for population terms Per-dimension boundaries for pooled terms
Prior scaling sx / sy ratio Same, computed on pooled model frame Same, computed on full model frame
Boundary scaling Same ratio Same Same

Programmatic API

Inspect priors and boundaries

peek_prior(model)
peek_boundary(model)

Override priors

model <- model %>%
  set_prior(
    m_tv ~ normal(0.5, 0.2),
    price_index ~ normal(-0.2, 0.1)
  )

Override boundaries

model <- model %>%
  set_boundary(
    m_tv > 0,
    competitor_discount < 0
  )

Minimal-prior policy

The recommended operating profile for MMM is documented in Minimal-Prior Policy. The policy keeps priors weak by default and uses hard constraints only when there is structural business knowledge.

Cross-references

Minimal-Prior Policy

This page is the short operating rule for prior-setting in DSAMbayes. Use it when you want a compact default policy. For the full workflow logic, see Stage 2: Model and Priors. For the mechanics of YAML and API prior specification, see Priors and Boundaries.

Purpose

Use a principled but low-friction prior setup that avoids specification-hunting while preserving identifiability in short, collinear MMM datasets.

Policy

  1. Default-first: keep priors.use_defaults: true.
  2. Sparse overrides: only add priors.overrides for high-conviction terms.
  3. Selective bounds: add boundaries.overrides only for structural signs.
  4. No blanket constraints: do not force all controls/media to one sign by default.
  5. Diagnose before tightening: use pre-flight and diagnostics gates first, then add priors/bounds if uncertainty is still unstable.

YAML mapping

priors:
  use_defaults: true
  overrides:
    # Optional high-conviction override examples:
    # - { parameter: price_index, mean: -0.2, sd: 0.1 }
    # - { parameter: distribution, mean: 0.15, sd: 0.1 }

boundaries:
  overrides:
    # Optional structural sign constraints:
    # - { parameter: m_tv, lower: 0.0, upper: .Inf }
    # - { parameter: competitor_discount, lower: -Inf, upper: 0.0 }

When to override defaults

  • Do override when domain mechanism is stable and defensible.
  • Do not override only to improve one run’s fit metrics.
  • Do not add bounds if sign can plausibly flip under promotion, pricing, or substitution effects.

Review checklist

  • Are overrides fewer than the number of major business assumptions?
  • Is each bound tied to a concrete causal rationale?
  • Did diagnostics indicate a real identifiability problem before tightening?

When to use this page

  • Use this page when you need a concise prior-setting policy for routine MMM work.
  • Use Stage 2: Model and Priors when you need the reasoning behind that policy.
  • Use Priors and Boundaries when you need exact DSAMbayes syntax, scaling rules, or boundary mechanics.

Response Scale Semantics

Purpose

DSAMbayes models can operate on an identity (level) or log response scale. This page defines how response scale is detected, stored, and used for post-fit reporting, so that operators understand which scale their outputs are on and how KPI-scale conversions work.

Response scale detection

Response scale is determined at construction time by detect_response_scale(), which inspects the left-hand side of the formula:

Formula LHS Detected transform Response scale label
kpi ~ ... identity response_level
log(kpi) ~ ... log response_log

The detected value is stored in two model-object fields:

  • .response_transform"identity" or "log". Describes the mathematical transform applied to the response before modelling.
  • .response_scale"identity" or "log". Used as a label when reporting whether outputs are on the model scale or the KPI scale.

Both fields are set by the constructor and confirmed by pre_flight_checks().

Model scale vs KPI scale

Concept Identity response Log response
Model scale Raw KPI units Log of KPI units
KPI scale Same as model scale exp() of model scale
Coefficient interpretation Unit change in KPI per unit change in predictor Change in log(KPI) per unit change in predictor; exact KPI-scale percent change is 100 * (exp(beta) - 1)

For identity-response models, model scale and KPI scale are identical. For log-response models, fitted values and residuals on the model scale are in log units and must be exponentiated to obtain KPI-scale values.

This is a semilog model, not a log-log model. In DSAMbayes, a coefficient from log(kpi) ~ x means:

$$\Delta \log(\mathrm{KPI}) = \beta \cdot \Delta x$$

So for a one-unit increase in x, the exact KPI-scale percentage change is:

$$100 \cdot \left(\exp(\beta) - 1\right)$$

The common shortcut 100 * beta is only a small-coefficient approximation.

Interpreting log-response models

This is the section to use when an analyst asks, “what does the coefficient actually mean on the KPI scale?”

1. Coefficients stay on the model scale

For a model written as:

$$\log(\mathrm{KPI}) = \alpha + \beta x + \cdots$$

the coefficient beta returned by get_posterior() and summarised in posterior_summary.csv is a log-KPI coefficient. DSAMbayes does not silently convert coefficient tables into KPI-scale percentage effects.

2. The exact KPI-scale effect depends on the predictor change

For a change of \Delta x in a predictor, the model implies:

$$\%\Delta \mathrm{KPI} = 100 \cdot \left(\exp(\beta \cdot \Delta x) - 1\right)$$

Special cases:

  • If \Delta x = 1, the exact percent change is 100 * (exp(beta) - 1).
  • If x is a binary indicator changing from 0 to 1, use the same exact formula.
  • The shortcut 100 * beta is only acceptable when beta * \Delta x is small enough that the approximation error is negligible for the use case.

3. This is not automatically an elasticity

log(kpi) ~ x is a semilog model. The coefficient is an elasticity only if the predictor is also logged, for example log(kpi) ~ log(x).

So in DSAMbayes:

  • log(kpi) ~ x gives a semilog coefficient.
  • log(kpi) ~ log(x) would be interpreted as an elasticity.

4. Coefficients attach to the modeled column, not necessarily raw spend

DSAMbayes coefficients describe the predictor that actually enters the model matrix.

That matters because in MMM workflows the modeled term is often:

  • an adstocked media signal,
  • a saturated transform,
  • a normalized exposure metric,
  • or another user-authored transformed column.

So if your YAML media block points to transformed signal columns, the coefficient is per unit of that transformed signal, not per unit of raw spend. The same caution applies to interactive formula workflows.

5. Use the right output for the question

Use these surfaces consistently:

  • posterior_summary.csv and get_posterior() for coefficient summaries on the model scale.
  • fitted.csv and observed.csv for fitted and observed values on the model scale.
  • fitted_kpi.csv, observed_kpi.csv, and fitted_kpi() for business-facing values on the KPI scale.

For log-response models, posterior_summary.csv is therefore the wrong place to read off a KPI-scale uplift directly. It is the right place to get beta, which you then interpret with 100 * (exp(beta * \Delta x) - 1).

6. DSAMbayes labels KPI-scale conversions explicitly

When DSAMbayes writes KPI-scale outputs for log-response models, it records:

  • source_response_scale = "log"
  • response_scale = "kpi"
  • conversion_method

This is intended to make it obvious that the values have been back-transformed and to distinguish the default lognormal-mean conversion from the simpler pointwise exp() median-style conversion.

Post-fit accessors and scale behaviour

fitted() — model scale

fitted() returns predicted values on the model scale. For identity-response models this is the KPI scale. For log-response models this is the log scale.

fit_tbl <- fitted(model)
# fit_tbl$fitted is on model scale

fitted_kpi() — KPI scale

fitted_kpi() applies the inverse transform draw-wise before summarising. For log-response models the default conversion (since v1.2.2) uses the conditional-mean estimator:

$$E[Y] = \exp\!\bigl(\mu + \tfrac{\sigma^2}{2}\bigr)$$

This is the bias-corrected back-transform that accounts for the log-normal variance term. The previous behaviour (v1.2.0) used the simpler exp(mu) estimator, which corresponds to the conditional median on the KPI scale. To retain that behaviour, pass log_response = "median":

# Default (v1.2.2): conditional mean — bias-corrected
kpi_tbl <- fitted_kpi(model)

# Explicit median — equivalent to pre-v1.2.2 behaviour
kpi_tbl <- fitted_kpi(model, log_response = "median")

The output includes source_response_scale (the model’s response scale), response_scale = "kpi", and conversion_method ("conditional_mean" or "point_exp") to label the result.

observed() — model scale

observed() returns the observed response on the model scale after unscaling (if scale=TRUE).

observed_kpi() — KPI scale

observed_kpi() returns the observed response on the KPI scale. For log-response models, this applies exp() to the model-scale observed values.

to_kpi_scale() helper

The internal function to_kpi_scale(x, response_scale) implements the conversion:

  • If response_scale == "log": returns exp(x).
  • Otherwise: returns x unchanged.

This function is used consistently by fitted_kpi(), observed_kpi(), and runner artefact writers.

Runner artefact scale conventions

Runner artefact writers use the response scale metadata to determine which scale to report:

Artefact Scale Notes
fitted.csv Model scale Direct output from fitted()
observed.csv Model scale Direct output from observed()
posterior_summary.csv Model scale Coefficient summaries on model scale; for log-response models these are log-KPI coefficients, not KPI-scale effects
Fit time series plot Model scale Diagnostic plot from fitted.csv plus observed.csv; subtitle states whether the model is levels or semilog and what scale is displayed
Fit scatter plot Model scale Same as fit time series
Diagnostics (residuals) Model scale Residuals computed on model scale
Budget optimisation outputs KPI scale Response curves and allocations reported on KPI scale

Interaction with scale = TRUE

The scale flag and response scale are orthogonal:

  • scale = TRUE standardises predictors and response by centring and dividing by standard deviation before Stan fitting. Coefficients and fitted values are back-transformed to the original scale by get_posterior().
  • Response scale determines whether the original scale is levels (identity) or logs (log).

Both transformations compose: a log-response model with scale=TRUE first takes the log of the response (via the formula), then standardises the logged values. Post-fit, draws are first unscaled, then (for KPI-scale outputs) exponentiated.

Jensen’s inequality and draw-wise conversion

When converting log-scale posterior draws to KPI scale, DSAMbayes applies exp() to each draw individually before computing summaries (mean, median, credible intervals). This is the correct Bayesian approach because:

  • E[exp(X)] ≠ exp(E[X]) when X has non-zero variance (Jensen’s inequality).
  • Draw-wise conversion preserves the full posterior distribution on the KPI scale.
  • Summary statistics (mean, quantiles) computed after conversion correctly reflect KPI-scale uncertainty.

Practical guidance

  • Use identity-response models when the KPI is naturally additive and coefficients should represent unit changes.
  • Use log-response models when the KPI is naturally multiplicative, when variance scales with level, or when the response must remain positive.
  • Always check response_scale_label(model) before interpreting coefficient magnitudes.
  • Do not call log-response coefficients elasticities unless the predictor is also logged. In log(kpi) ~ x, they are semilog coefficients.
  • For KPI-scale percentage interpretation, use 100 * (exp(beta) - 1), not 100 * beta, unless the coefficient is small enough that the approximation is acceptable.
  • Use fitted_kpi() for business reporting; use fitted() for diagnostics.
  • Do not manually exponentiate posterior means from log-response models. Use fitted_kpi() or to_kpi_scale() on individual draws.

Cross-references

CRE / Mundlak

Purpose

The correlated random effects (CRE) pathway, implemented as a Mundlak device, augments hierarchical DSAMbayes models with group-mean terms. This separates within-group variation from between-group variation for selected regressors, reducing confounding bias when group-level means are correlated with the random effects.

When to use CRE

Use CRE when:

  • The model is hierarchical (panel data with (term | group) syntax).
  • Time-varying regressors (e.g. media spend) have group-level means that may be correlated with the group intercept or slope.
  • You want to decompose effects into within-group (temporal) and between-group (cross-sectional) components.

Do not use CRE when:

  • The model is BLM or pooled (CRE requires hierarchical class).
  • The panel has only one group (no between-group variation exists).
  • All regressors of interest are time-invariant (CRE mean terms would be constant).

Construction

CRE is applied after model construction via set_cre():

model <- blm(
  kpi ~ m_tv + m_search + trend + (1 + m_tv + m_search | market),
  data = panel_df
)
model <- set_cre(model, vars = c("m_tv", "m_search"))

What set_cre() does

  1. Resolves the grouping variable. If the formula has one group factor, it is used automatically. If multiple group factors exist, the group argument must be specified explicitly.

  2. Generates group-mean column names. For each variable in vars, a mean-term column is named cre_mean_<variable> (configurable via prefix).

  3. Augments the data. apply_cre_data() computes group-level means of each CRE variable and joins them back to the panel data as new columns.

  4. Updates the formula. The generated mean terms are appended to the population formula as fixed effects.

  5. Extends priors and boundaries. Default prior and boundary entries are added for each new mean term, matching the existing prior schema.

YAML runner configuration

When using the runner, CRE is configured via:

model:
  type: cre

hierarchy:
  group: market
  random_intercept: true
  random_slopes: []
  cre_variables: [m_tv, m_search, m_social]
  cre_prefix: cre_mean_

The runner calls set_cre() during model construction for model.type: cre.

Mundlak decomposition

For a regressor $x_{gt}$ (group $g$, time $t$), the Mundlak device decomposes the effect into:

  • Within-group effect: the coefficient on $x_{gt}$ in the population formula captures temporal variation after conditioning on the group mean.
  • Between-group effect: the coefficient on $\bar{x}_g$ (the CRE mean term) captures cross-sectional variation in group-level averages.

The original coefficient on $x_{gt}$ in a standard random-effects model conflates both sources. Adding $\bar{x}_g$ as a fixed effect separates them.

Validation and identification warnings

Input validation

set_cre() validates:

  • The model is hierarchical (aborts for BLM or pooled).
  • All vars are present in the data and are numeric.
  • The group variable exists in the formula’s group factors.
  • No CRE mean terms appear in random-slope blocks (would cause double-counting).

Identification warnings

warn_cre_identification() checks two conditions after CRE setup:

  1. More CRE variables than groups. If length(vars) > n_groups, between-effect estimates may be weakly identified. The function emits a warning.

  2. Near-zero within-group variation. For each CRE variable, the within-group residual ($x_{gt} - \bar{x}_g$) standard deviation is checked. If it is effectively zero, within-effect identification is weak. The function emits a per-variable warning.

Zero-variance CRE mean terms

If a CRE mean term has zero variance across all observations (possible when the underlying variable has identical group means), calculate_scaling_terms() in R/scale.R will abort when scale=TRUE. The error message identifies the constant CRE columns and suggests using model.type: re (without CRE) or model.scale: false as workarounds.

Panel assumptions

  • Balanced panels are not required. apply_cre_data() computes group means using dplyr::group_by() and mean(), which handles unequal group sizes.
  • Missing values in CRE variables are excluded from the group-mean calculation (na.rm = TRUE).
  • Group-mean recomputation. CRE mean columns are recomputed each time apply_cre_data() is called, including during prep_data_for_fit.hierarchical(). Existing CRE mean columns are dropped and regenerated to prevent stale values.

Decomposition and reporting

CRE mean terms appear as ordinary fixed-effect terms in the population formula. This means:

  • Posterior summary includes CRE mean-term coefficients alongside other population coefficients.
  • Response decomposition via decomp() attributes fitted-value contributions to CRE mean terms separately from their within-group counterparts.
  • Plots (posterior forest, prior-vs-posterior) include CRE mean terms.

Interpretation note: the CRE mean-term coefficient represents the between-group effect conditional on the within-group variation. It does not represent the total effect of the underlying variable.

Cross-references

Time Components

Purpose

DSAMbayes provides managed time-component generation through the effects.holidays config section. When enabled, the runner deterministically generates holiday feature columns from a calendar file and appends them to the compiled model formula. This page defines the configuration contract, generation logic, naming conventions, and audit properties.

Overview

Time components in DSAMbayes cover:

  • Holidays — deterministic weekly indicator features derived from an external calendar file.
  • Trend and seasonality — specified directly in the model formula (e.g. t_scaled, sin52_1, cos52_1). These are not generated by the time-components system; they are user-supplied columns in the data.

The managed-effects system is responsible only for holiday feature generation.

YAML configuration

effects:
  holidays:
    enabled: true
    path: ../data/holidays.csv
    date_col: null
    label_col: holiday
    country: gb
    country_col: country
    date_format: null
    week_start: monday
    timezone: UTC
    prefix: holiday_
    window_before: 0
    window_after: 0
    aggregation_rule: count
    overlap_policy: count_all
    overwrite_existing: false

Key definitions

Key Default Description
holidays.enabled false Toggle for holiday feature generation
holidays.path null Path to the holiday calendar CSV/RDS (resolved relative to the config file)
holidays.date_col null Date column in the calendar; auto-detected from date, ds, or event_date
holidays.label_col holiday Column containing holiday event labels
holidays.country null Optional single-country filter
holidays.country_col country Calendar column used for country filtering
holidays.date_format null Date parse format; null assumes ISO 8601
holidays.week_start monday Day-of-week anchor for weekly aggregation
holidays.timezone UTC Timezone used when parsing POSIX date-time inputs
holidays.prefix holiday_ Prefix prepended to generated feature column names
holidays.window_before 0 Days before each event date to include in the holiday window
holidays.window_after 0 Days after each event date to include in the holiday window
holidays.aggregation_rule count Weekly aggregation: count sums event-days per week; any produces a binary indicator
holidays.overlap_policy count_all Overlap handling: count_all counts every event-day; dedupe_label_date deduplicates per label and date
holidays.overwrite_existing false Whether existing columns with matching names are overwritten

Calendar file contract

The holiday calendar is a CSV (or data frame) with at minimum:

Column Required Content
Date column Yes Daily event dates (one row per event occurrence)
Label column Yes Human-readable event name (e.g. Christmas, Black Friday)

Date column detection

If date_col is null, the system tries column names in order: date, ds, event_date. If none is found, validation aborts.

Label normalisation

Holiday labels are normalised to lowercase, alphanumeric-plus-underscore form via normalise_holiday_label(). For example:

  • Black Fridayblack_friday
  • New Year's Daynew_year_s_day
  • Empty labels → unnamed

The generated feature column name is {prefix}{normalised_label}, e.g. holiday_black_friday.

Generation pipeline

The runner calls build_weekly_holiday_features() with the following steps:

  1. Parse and validate the calendar. validate_holiday_calendar() checks column presence, date parsing, and label completeness.

  2. Expand holiday windows. expand_holiday_windows() replicates each event row across the [event_date - window_before, event_date + window_after] range.

  3. Align to weekly index. Each expanded event-day is mapped to its containing week using week_floor_date() with the configured week_start.

  4. Aggregate per week. Events are counted per week per feature. Under aggregation_rule: any, counts are collapsed to binary (0/1). Under overlap_policy: dedupe_label_date, duplicate label-date pairs within a week are removed before counting.

  5. Join to model data. The generated feature matrix is left-joined to the model data by the date column. Weeks with no events receive zero.

  6. Append to formula. Generated feature columns are appended as additive terms to the compiled population formula.

Weekly anchoring

All weekly alignment uses week_floor_date(), which computes the most recent occurrence of week_start on or before each date. The model data’s date column must contain week-start-aligned dates; normalise_weekly_index() validates this and aborts if dates are not aligned.

Supported week-start values

monday, tuesday, wednesday, thursday, friday, saturday, sunday.

Timezone handling

  • Calendar dates are parsed using the configured timezone (default UTC).
  • If the calendar contains POSIXt values, they are coerced to Date in the configured timezone.
  • Character dates are parsed as ISO 8601 by default, or using date_format if specified.

Generated-term audit contract

Generated holiday terms are tracked for downstream diagnostics and reporting:

  • The list of generated term names is stored in model$.runner_time_components$generated_terms.
  • The identifiability gate in R/diagnostics_report.R uses this list to auto-detect baseline terms (via detect_baseline_terms()), so generated holiday terms are included in baseline-media correlation checks without requiring explicit configuration.

Feature naming collision

If two different holiday labels normalise to the same feature name, build_weekly_holiday_features() aborts with a collision error. Ensure calendar labels are distinct after normalisation.

Interaction with existing data columns

  • If overwrite_existing: false (default), the runner aborts if any generated column name already exists in the data.
  • If overwrite_existing: true, existing columns with matching names are replaced by the generated features.

Practical guidance

  • Start with aggregation_rule: count to capture multi-day holiday effects (e.g. a holiday spanning two days in one week produces a count of 2).
  • Use window_before and window_after for events with known anticipation or lingering effects (e.g. window_before: 7 for pre-Christmas shopping).
  • Use aggregation_rule: any when you want binary holiday indicators regardless of how many event-days fall in a week.
  • Check generated terms in the resolved config (config.resolved.yaml) and posterior summary to confirm which holidays entered the model.

Cross-references

Diagnostics Gates

For the workflow interpretation of these checks, start with Stage 4: Computation and Sampler and Stage 5: Model Adequacy. This page is the threshold and policy reference.

Use this page when you need exact gate thresholds, status aggregation, or YAML policy semantics. It does not replace substantive model review: a model can clear threshold tables and still be a poor basis for interpretation.

Model selection for time-ordered data

PSIS-LOO and WAIC treat pointwise observations as conditionally exchangeable. That assumption is not generally appropriate for time-ordered MMM data, where nearby weeks can remain dependent after conditioning on the fitted model.

Use the runner’s expanding-window blocked CV or leave-future-out CV as the primary evidence when selecting among time-series MMM specifications. Treat PSIS-LOO, WAIC, Pareto-k, and LOO-PIT outputs as supplementary fit, influence, and calibration diagnostics. They do not establish future-period predictive performance, causal validity, or a publish-gate pass on their own.

Purpose

DSAMbayes runs a deterministic diagnostics framework after model fitting. Each diagnostic check produces a pass, warn, or fail status. The policy mode controls how lenient or strict the thresholds are. This page defines the check taxonomy, threshold tables, policy modes, identifiability gate, and the overall status aggregation rule.

How to use this page

  • Use Stage 4: Computation and Sampler to understand which checks are non-negotiable before trusting the posterior.
  • Use this page to see the exact DSAMbayes thresholds and artifact semantics.
  • Use Stage 5: Model Adequacy before treating a passing diagnostics table as permission for decomposition, model comparison, or optimisation.

Policy modes

The diagnostics framework supports three policy modes, configured via diagnostics.policy_mode in YAML:

Mode Intent Threshold behaviour
explore Rapid iteration during model development Relaxed fail thresholds; many checks can only warn, not fail
publish Default production mode for shareable outputs Balanced thresholds; condition-number fail is downgraded to warn
strict Audit-grade gating for release candidates Tightest thresholds; rank deficit fails rather than warns

The mode is resolved by diagnostics_policy_thresholds(mode) in R/diagnostics_report.R.

Check taxonomy

Checks are organised into phases:

Phase Scope When evaluated
P0 Data integrity and design matrix validity Pre-fit (design matrix available)
P1 Sampler quality, residual behaviour, identifiability Post-fit (posterior available)

Each check row includes:

Field Meaning
check_id Unique identifier
phase P0 or P1
severity Priority rating (P0 = critical, P1 = important)
status pass, warn, fail, or skipped
metric Metric name
value Observed value
threshold Applied threshold description
message Human-readable explanation

P0 design checks

Check ID Metric Pass Warn Fail
pre_response_finite non_finite_response_count == 0 > 0
pre_design_constants_duplicates constant_plus_duplicate_columns == 0 > 0
pre_design_rank_deficit rank_deficit == 0 > 0 (publish) > 0 (strict)
pre_design_condition_number kappa_X ≤ warn > warn > fail

Condition number thresholds by mode

Mode Warn Fail
explore 10,000 ∞ (cannot fail)
publish 10,000 1,000,000 (downgraded to warn)
strict 10,000 1,000,000

P1 sampler checks (MCMC only)

Check ID Metric Direction Warn Fail
sampler_rhat_max max_rhat Lower is better 1.01 1.01
sampler_ess_bulk_min min_ess_bulk Higher is better 400 200
sampler_ess_tail_min min_ess_tail Higher is better 200 100
sampler_ebfmi_min min_ebfmi Higher is better 0.30 0.20
sampler_treedepth_frac treedepth_hit_fraction Lower is better 0.00 0.01
sampler_divergences divergent_fraction Lower is better 0.00 0.00

Mode adjustments for sampler checks

DSAMbayes treats any Rhat above 1.01 as a failure in publish and strict modes, following the rank-normalised, folded Rhat guidance in Vehtari et al. (2021) and the Stan warnings guide. In explore mode, the fail threshold is deliberately relaxed to 1.10, while the warning threshold remains 1.01.

P1 residual checks

Check ID Metric Direction Warn Fail
resid_ljung_box_p resid_lb_p Higher is better 0.05 0.01
resid_acf_max resid_acf_max Lower is better 0.20 0.40

Mode adjustments for residual checks

Mode resid_lb_p warn resid_lb_p fail resid_acf warn resid_acf fail
explore 0.05 0.00 (cannot fail) 0.20 ∞ (cannot fail)
publish 0.05 0.01 0.20 0.40
strict 0.10 0.05 0.15 0.30

P1 boundary hit check

Check ID Metric Direction Warn Fail
boundary_hit_fraction boundary_hit_frac Lower is better 0.05 0.20

In explore mode, boundary hits cannot fail. In strict mode, thresholds tighten to warn > 0.02, fail > 0.10.

P1 within-group variation check

Check ID Metric Direction Warn Fail
within_var_ratio within_var_min_ratio Higher is better 0.10 0.05

This check applies to hierarchical models and flags groups where within-group variation is extremely low relative to between-group variation. In explore mode, the fail threshold is zero (cannot fail).

Identifiability gate

The identifiability gate measures the maximum absolute correlation between baseline terms and media terms in the design matrix. It is configured via diagnostics.identifiability in YAML:

diagnostics:
  identifiability:
    enabled: true
    media_terms: [m_tv, m_search, m_social]
    baseline_terms: [trend, seasonality]
    baseline_regex: ["^h_", "^sin", "^cos"]
    abs_corr_warn: 0.80
    abs_corr_fail: 0.95

Term detection

  • Media terms: explicitly listed in media_terms.
  • Baseline terms: union of baseline_terms, generated time-component terms, and matches from baseline_regex patterns.
  • Both sets are intersected with actual design-matrix columns and filtered to remove constant columns.

Thresholds by mode

Mode Warn Fail
explore 0.80 ∞ (cannot fail)
publish 0.80 0.95
strict 0.70 0.85

Skip conditions

The identifiability gate reports skipped when:

  • identifiability.enabled: false
  • No configured media terms found in the design matrix
  • No baseline terms detected from configured terms/regex
  • All resolved baseline or media terms are constant

Overall status aggregation

The overall diagnostics status is determined by diagnostics_overall_status():

  1. If any check has status == "fail" → overall status is fail.
  2. If any check has status == "warn" (and none fail) → overall status is warn.
  3. Otherwise → overall status is pass.

Checks with status == "skipped" do not affect the overall status.

Runner artefact output

The diagnostics framework produces:

Artefact Location Content
diagnostics_report.csv 40_diagnostics/ Full check table with all fields
diagnostics_summary.txt 40_diagnostics/ Human-readable summary of overall status and failing checks

Interpretation guidance

  • pass — no remediation needed; model is suitable for the configured policy mode.
  • warn — review recommended; the model may have quality concerns but does not block the configured policy.
  • fail — remediation required before the model can be considered production-ready under the configured policy.

Common remediation actions

Diagnostic area Warning signs Actions
High Rhat > 1.01 Increase MCMC iterations or warmup; simplify model
Low ESS < 400 bulk or < 200 tail Increase iterations; check for multimodality
Divergences Any non-zero fraction Increase adapt_delta; reparameterise model
High condition number kappa > 10,000 Reduce collinearity; remove redundant terms
Residual autocorrelation High ACF or low Ljung-Box p Add time controls (trend, seasonality, holidays)
Boundary hits > 5% of draws Review boundary specification; widen or remove constraints
High baseline-media correlation > 0.80 Add controls to separate baseline from media; consider alternative model specifications

Cross-references

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.

Response-surface contract

The allocator’s response curves are scenario-authored: each channel’s response specification supplies the identity, atan, log1p, or Hill curve used to score allocations. optimise_budget() records this as response_surface = "scenario_authored" in the returned object and its summary artefact.

For models fitted with probabilistic adstock/Hill media transforms, these decision-layer curves are not the fitted Stan response. They do not reuse posterior adstock decay, posterior Hill half-saturation, historical pacing, or carry-over state. Treat them as an explicit scenario model, not as a model-sourced marginal-response estimate.

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
response_surface "scenario_authored"; scenario response functions, not a fitted transformed-media response
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

Plots

Purpose

This section documents every plot the DSAMbayes runner produces. Each page covers one pipeline stage, describes what the plot shows, explains when and why the runner generates it, and gives practical interpretation guidance. The target reader is a modelling operator or analyst who needs to assess run quality without reading source code.

Pipeline stages

The runner writes artefacts into timestamped directories under results/. Plots are organised into six stages, each with its own subdirectory:

Stage Directory Role Page
Pre-run 10_pre_run/ Data quality and input sanity checks before fitting Pre-run plots
Model fit 20_model_fit/ Posterior summaries and fitted-vs-observed visualisations Model fit plots
Post-run 30_post_run/ Observed/fitted exports plus decomposition outputs when enabled and available Post-run plots
Diagnostics 40_diagnostics/ Residual analysis, boundary monitoring, posterior predictive checks Diagnostics plots
Model selection 50_model_selection/ LOO-CV diagnostics for model comparison and calibration Model selection plots
Optimisation 60_optimisation/ Budget allocation, response curves, efficiency comparisons Optimisation plots

Plot catalogue

All plots listed below map to files in docs/images/. Where generation is conditional or may be skipped, that is called out explicitly.

Pre-run (10_pre_run/)

Filename Description Type
media_spend_timeseries.png Stacked area chart of channel spend over time Descriptive
kpi_media_overlay.png KPI and total media spend on dual axes Descriptive
vif_bar.png Variance inflation factor per predictor Diagnostic

Model fit (20_model_fit/)

Filename Description Type
fit_timeseries.png Observed vs fitted response over time with credible band Descriptive
fit_scatter.png Observed vs fitted scatter with 45-degree reference Descriptive
posterior_forest.png Coefficient estimates with 90% credible intervals Descriptive
prior_posterior.png Prior-to-posterior shift for media coefficients Descriptive

Post-run (30_post_run/)

Filename Description Type
decomp_predictor_impact.png Total contribution per model term (bar chart) Conditional on decomposition output availability
decomp_timeseries.png Stacked media contribution over time Conditional on decomposition output availability

Diagnostics (40_diagnostics/)

Filename Description Type
ppc.png Posterior predictive check fan chart Diagnostic
residuals_timeseries.png Residuals over time Diagnostic
residuals_vs_fitted.png Residuals vs fitted values Diagnostic
residuals_hist.png Residual distribution histogram Diagnostic
residuals_acf.png Residual autocorrelation function Diagnostic
residuals_latent_acf.png Latent-scale residual ACF (log-response models) Diagnostic
boundary_hits.png Share of posterior draws near coefficient boundaries Diagnostic/Gating

Model selection (50_model_selection/)

Filename Description Type
pareto_k.png Pareto-k diagnostic scatter from PSIS-LOO Diagnostic/Gating
loo_pit.png LOO probability integral transform histogram Diagnostic
elpd_influence.png Pointwise ELPD contributions over time Diagnostic

Optimisation (60_optimisation/)

Filename Description Type
budget_response_curves.png Channel response curves with current/optimised points Decision
budget_roi_cpa.png ROI or CPA comparison (current vs optimised) Decision
budget_impact.png Spend reallocation and response impact diverging bars Decision
budget_contribution.png Absolute response comparison by channel Decision
budget_confidence_comparison.png Posterior credible intervals for allocations Decision
budget_sensitivity.png Sensitivity of optimised allocation to budget changes Decision
budget_efficient_frontier.png Efficient frontier across budget levels Decision
budget_kpi_waterfall.png KPI waterfall from reference to optimised allocation Decision
budget_marginal_roi.png Marginal ROI curves at the optimised point Decision
budget_spend_share.png Spend share comparison (current vs optimised) Decision

Source code references

Plot generation is implemented across three files:

  • R/runner_fit_plots.R — pre-run, fit, diagnostics, and model selection plots
  • R/optimise_budget_plots.R — budget optimisation plots
  • R/run_artifacts.R — stage map and orchestration
  • R/run_artifacts_enrichment.R — wiring for fit-stage and pre-run plots
  • R/run_artifacts_diagnostics.R — wiring for diagnostics and model selection plots

Subsections of Plots

Pre-run Plots

Purpose

Pre-run plots are generated before the model is fitted. They visualise the input data and flag structural problems — multicollinearity, missing spend periods, implausible KPI–media relationships — that could compromise inference. Treat these as a data quality gate: review them before interpreting any downstream output.

All pre-run plots are written to 10_pre_run/ within the run directory. They require ggplot2 and are generated by write_pre_run_plots() in R/run_artifacts_enrichment.R. The runner produces them whenever an allocation.channels block is present in the configuration and the data contains the referenced spend columns.

Plot catalogue

Filename What it shows Conditions
media_spend_timeseries.png Stacked channel spend over time Allocation channels defined with valid spend_col
kpi_media_overlay.png KPI and total spend on dual axes Allocation channels defined; response variable present
vif_bar.png VIF per predictor with severity thresholds Design matrix extractable with >1 predictor and >1 row

Media spend time series

Filename: media_spend_timeseries.png

Media spend time series Media spend time series

What it shows

A stacked area chart of weekly media spend by channel, drawn from the raw spend_col columns declared in the allocation configuration. The x-axis is the date variable; the y-axis is spend in model units.

When it is generated

The runner generates this plot when:

  • The configuration includes an allocation.channels block.
  • At least one declared spend_col exists in the input data.

If no valid spend columns are found, the plot is silently skipped.

How to interpret it

Look for three things. First, check that each channel has plausible seasonal patterns and no unexpected gaps — zero-spend weeks in the middle of a campaign period suggest data ingestion problems. Second, verify that the relative magnitudes make sense: if TV dominates the stack but the brand has historically been digital-first, the data may be mislabelled or aggregated incorrectly. Third, confirm that the date range matches the modelling window declared in the configuration.

Warning signs

  • Flat channels: A channel with constant spend across all weeks contributes no variation and cannot be identified by the model. The coefficient will be driven entirely by the prior.
  • Sudden jumps or drops: Step changes in spend that do not correspond to known campaign events may indicate data joins across sources with different reporting conventions.
  • Missing periods: Gaps where spend drops to zero mid-series can distort adstock calculations if the model applies geometric decay.

Action

If a channel shows no variation, consider removing it from the formula or fixing the upstream data. If gaps are genuine (e.g. a seasonal channel), confirm the adstock specification handles zero-spend periods correctly.

  • data_dictionary.csv in 10_pre_run/ provides summary statistics for every input column.

KPI–media overlay

Filename: kpi_media_overlay.png

KPI–media overlay KPI–media overlay

What it shows

A dual-axis time series with the KPI response variable on the left axis (blue) and total media spend (sum of all declared spend_col values) on the right axis (red, rescaled to share the vertical space). This is a visual correlation check, not a causal claim.

When it is generated

The runner generates this plot when:

  • The configuration includes an allocation.channels block with at least one valid spend_col.
  • The response variable exists in the data.

If the total spend has zero variance, the plot is skipped.

How to interpret it

The overlay reveals whether KPI and aggregate spend move together over time. A rough co-movement is expected in MMM data — media drives response — but the relationship need not be tight. Seasonal KPI peaks that precede or lag media bursts suggest confounding (e.g. demand-driven spend timing). Divergences where spend rises but KPI falls (or vice versa) are worth investigating: they may reflect diminishing returns, competitor activity, or a structural break in the data.

Warning signs

  • Perfect alignment: If the two series track each other almost exactly, the model may be fitting spend timing rather than incremental media effects.
  • Opposite trends: A persistent negative relationship between total spend and KPI suggests reverse causality or omitted-variable bias.
  • Scale artefacts: The dual-axis rescaling can exaggerate or suppress visual correlation. Do not draw quantitative conclusions from this plot.

Action

Use this plot as a sanity check only. If the relationship looks implausible, investigate the data and consider whether the formula includes adequate controls for seasonality, trend, and external factors.


Variance inflation factor (VIF) bar chart

Filename: vif_bar.png

VIF bar chart VIF bar chart

What it shows

A horizontal bar chart of variance inflation factors for each predictor in the model’s design matrix. Bars are colour-coded by severity: green (VIF < 5), amber (5 ≤ VIF < 10), and red (VIF ≥ 10). Dashed vertical lines mark the 5 and 10 thresholds.

When it is generated

The runner generates this plot when:

  • The design matrix has more than one predictor column and more than one row.
  • The VIF computation does not encounter a singular or degenerate correlation matrix.

For pooled models, the design matrix extraction may return zero rows, in which case the plot is skipped.

How to interpret it

VIF measures how much the variance of a coefficient estimate inflates due to correlation with other predictors. A VIF of 1 means no multicollinearity; a VIF of 10 means the standard error is roughly three times larger than it would be with orthogonal predictors. In Bayesian MMM, high VIF does not break inference the way it does in OLS — priors regularise the estimates — but it does reduce the data’s ability to inform the posterior, making results more prior-dependent.

Warning signs

  • VIF > 10 on media channels: The model cannot reliably separate the effects of those channels. Posterior estimates will lean heavily on the prior. Consider whether the channels can be combined or whether one should be dropped.
  • VIF > 10 on seasonality terms: Common and usually harmless if the terms are included as controls rather than as interpretive outputs.
  • All terms moderate or high: The overall collinearity structure may be too severe for the data length. Consider increasing the sample size or simplifying the formula.

Action

Review the top-VIF terms. If two media channels are highly collinear (e.g. search and affiliate), consider whether they can be meaningfully separated given the available data. If not, combine them or use informative priors to anchor the split.

  • design_matrix_manifest.csv in 10_pre_run/ lists all design matrix columns with variance and uniqueness statistics.
  • spec_summary.csv in 10_pre_run/ summarises the model specification.

Cross-references

Model Fit Plots

Purpose

Model fit plots summarise the posterior and compare fitted values against observed data. They answer two questions: does the model track the response variable adequately, and are the estimated coefficients plausible? These plots are written to 20_model_fit/ within the run directory.

The runner generates them via write_model_fit_plots() in R/run_artifacts_enrichment.R. All four plots require ggplot2 and the fitted model object. Each is wrapped in tryCatch so that a failure in one does not prevent the others from being written.

Plot catalogue

Filename What it shows Conditions
fit_timeseries.png Observed vs fitted over time with 95% credible band Always generated after a successful fit
fit_scatter.png Observed vs fitted scatter Always generated after a successful fit
posterior_forest.png Coefficient point estimates with 90% CIs Posterior draws available via get_posterior()
prior_posterior.png Prior-to-posterior density shift for media terms Model has a .prior table with media (m_*) parameters

Fit time series

Filename: fit_timeseries.png

Fit time series Fit time series

What it shows

The observed response (orange) and posterior mean fitted values (blue) plotted over time on the model response scale, with a shaded 95% credible interval band. The subtitle begins by stating both the model form and the displayed scale:

  • Model form: levels (identity response); plotted scale: KPI
  • Model form: semilog (log response); plotted scale: model response (log KPI)

It then reports in-sample fit metrics: classical R² computed from the posterior mean fit, RMSE, MAE, mean error (bias), sMAPE, 95% prediction interval coverage, lag-1 ACF of residuals, and sample size. The time axis uses month-year labels when the date column is available as a true date. For hierarchical models the plot facets by group.

For log-response models this is a diagnostic plot on the log-KPI model scale, not a business-facing KPI-scale chart. Use observed_kpi.csv and fitted_kpi.csv if you need original-scale KPI values.

When it is generated

Always, provided the model has been fitted successfully and the fit table (observed, mean, percentiles) can be computed.

How to interpret it

The fitted line should track the general level and seasonal pattern of the observed series. The 95% credible band should contain most observed points — the subtitle reports the actual coverage, which should be close to 95%. Systematic departures reveal model misspecification: if the fitted line consistently overshoots during holidays or undershoots during quiet periods, the formula may lack appropriate seasonal or event terms.

Warning signs

  • Coverage well below 95%: The model underestimates uncertainty. Common when the noise prior is too tight or the model is overfit to a subset of the data.
  • Coverage well above 95%: The credible interval is too wide. The model is underfit or the noise prior is too diffuse.
  • Persistent bias (ME far from zero): The model systematically over- or under-predicts. Check for missing structural terms (trend, level shifts, intercept misspecification).
  • High lag-1 ACF (> 0.3): Residuals are autocorrelated. The model is missing temporal structure — consider adding lagged terms or checking adstock specifications.

Action

If coverage or bias is unacceptable, revisit the formula (missing controls, wrong functional form) or the prior specification (overly tight noise SD). Cross-reference with the residuals diagnostics for a more detailed picture.

  • fit_metrics_by_group.csv in 20_model_fit/ provides the same metrics in tabular form, broken down by group for hierarchical models.

Fit scatter

Filename: fit_scatter.png

Fit scatter Fit scatter

What it shows

A scatter plot of observed values (y-axis) against posterior mean fitted values (x-axis), with a 45-degree reference line. Like fit_timeseries.png, the subtitle states whether the model is a levels or semilog specification and whether the points are shown on the KPI scale or the log-KPI model scale. Points on the line indicate perfect fit. For hierarchical models the plot facets by group.

When it is generated

Always, provided the fit table is available.

How to interpret it

Points should cluster tightly around the diagonal. Curvature away from the line suggests a systematic misfit — for instance, if the model underpredicts at high KPI values, the response may need a nonlinear term or a log transformation. Outliers far from the line warrant investigation: they may correspond to anomalous weeks (data errors, one-off events) that the model cannot capture.

Warning signs

  • Fan shape (wider scatter at higher values): Heteroscedasticity. A log-scale model or a variance-stabilising transform may be more appropriate.
  • Systematic curvature: The mean function is misspecified. Consider adding polynomial or interaction terms.
  • Isolated outliers: Check the dates of extreme residuals against the residuals time series and the input data for data quality issues.

Action

If the scatter reveals non-constant variance, consider fitting a log-response model (log(kpi) ~ ... in the formula or target.transform: log in the runner). If curvature is evident, review the functional form of media transforms and control variables.


Posterior forest plot

Filename: posterior_forest.png

Posterior forest Posterior forest

What it shows

A horizontal forest plot of posterior coefficient estimates. Each row is a model term (excluding the intercept). The point marks the posterior median; the horizontal bar spans the 5th to 95th percentile (90% credible interval). Terms whose interval excludes zero are drawn in colour; those consistent with zero are grey.

For hierarchical models, the plot displays population-level (group-averaged) estimates.

When it is generated

The runner generates this plot when posterior draws are available via get_posterior(). It is skipped if the posterior extraction fails.

How to interpret it

Focus on the media coefficients. Positive values indicate that higher media exposure is associated with higher KPI, which is the expected direction for most channels. The width of the interval reflects estimation precision: a narrow interval means the data informed the estimate strongly; a wide interval means the prior dominates.

Terms ordered by absolute magnitude (bottom to top) give a quick ranking of effect sizes, but note that these are on the model’s internal scale. For models fitted on the log scale, coefficients represent approximate percentage effects; for levels models, they represent absolute KPI units per unit of the transformed media input.

Warning signs

  • Media coefficient crosses zero: The model cannot confidently distinguish the channel’s effect from noise. This is not necessarily wrong — some channels may genuinely have weak effects — but it warrants scrutiny, especially if the prior was informative.
  • Implausibly large coefficients: Check for scaling issues. If model.scale: true, coefficients are on the standardised scale and must be interpreted accordingly.
  • All intervals very wide: The data may not have enough variation to identify individual effects. Review the VIF bar chart for multicollinearity.

Action

If a media coefficient is unexpectedly negative, investigate whether the data supports it (e.g. counter-cyclical spend) or whether multicollinearity is pulling the estimate. Cross-reference with the prior vs posterior plot to see how far the data moved the estimate from its prior.


Prior vs posterior

Filename: prior_posterior.png

Prior vs posterior Prior vs posterior

What it shows

Faceted density plots for each media coefficient (m_* parameters). The grey distribution is the prior (Normal, as specified in the model’s .prior table); the blue distribution is the posterior (estimated from MCMC draws). Overlap indicates that the data did not strongly inform the estimate; separation indicates data-driven updating.

For hierarchical models, posterior draws are averaged across groups to show the population-level density.

When it is generated

The runner generates this plot when:

  • The model has a .prior table (i.e. it is a requires_prior model).
  • The prior table contains at least one m_* parameter.
  • Posterior draws are available.

If the model has no prior table (e.g. a pure OLS updater), the plot is skipped.

How to interpret it

A well-identified coefficient shifts noticeably from prior to posterior. If the two densities sit on top of each other, the data provided little information for that channel — the estimate is prior-driven. This is not inherently wrong (the prior may be well-calibrated from previous studies), but it does mean the current dataset alone cannot validate the estimate.

Warning signs

  • No shift at all: The channel has insufficient variation or is too collinear with other terms for the data to update the prior. The resulting coefficient is essentially assumed, not estimated.
  • Posterior much narrower than prior: Expected and healthy. The data concentrated the estimate.
  • Posterior shifted to the boundary: If a boundary constraint is active (e.g. non-negativity), the posterior may pile up at zero. Cross-reference with the boundary hits plot to confirm.

Action

If key media channels show no prior-to-posterior shift, consider whether the prior is appropriate, whether the data period is long enough, or whether multicollinearity prevents identification. For channels where the prior dominates, document this clearly when reporting ROAS or contribution estimates — the output reflects an assumption, not a data-driven finding.


Cross-references

Post-run Plots

Purpose

Post-run plots decompose the fitted response into its constituent parts. They answer the question: how much does each predictor contribute to the modelled KPI, and how do those contributions evolve over time?

In the active v1.3 runner, these files are emitted under 30_post_run/ when decomposition output flags are enabled and the fitted model supports decomposition table generation. If decomposition cannot be computed, the runner records deterministic skip rows in 40_diagnostics/artifact_status.csv. For hierarchical models with random-effects formula syntax (|), decomposition can still fail gracefully because stats::model.matrix() may not evaluate the formula against the original data.

Plot catalogue

Filename What it shows Conditions
decomp_predictor_impact.png Total contribution per model term (bar chart) outputs.save_decomp_png: true and successful decomposition tables
decomp_timeseries.png Stacked media channel contribution over time outputs.save_decomp_png: true, successful decomposition tables, and at least one media term

Predictor impact

Filename: decomp_predictor_impact.png

Predictor impact Predictor impact

What it shows

A horizontal bar chart of the total contribution of each model term to the response, computed as the sum of coefficient × design-matrix column across all observations. Terms are sorted by absolute contribution magnitude. The intercept and total rows are excluded.

When it is generated

Generation requires runner_response_decomposition_tables() to return a valid predictor-level summary table and outputs.save_decomp_png: true. That in turn requires that stats::model.matrix() can parse the model formula against the original input data and that the fitted model retains the data needed for decomposition. This generally holds for BLM and pooled models and may succeed for hierarchical models that can be reduced to fixed-effect decomposition tables, but formulas with random-effects syntax can still fail gracefully.

How to interpret it

The bar lengths represent total modelled impact over the data period. Media channels with large positive bars drove the most KPI in the model’s account of the data. Control variables (trend, seasonality, holidays) often dominate in absolute terms because they capture baseline demand — this is expected and does not diminish the media findings.

Negative contributions can arise for terms with negative coefficients (e.g. price sensitivity) or for seasonality harmonics where the net effect over the year partially cancels.

Warning signs

  • A media channel with negative total contribution: Unless the coefficient is intentionally unconstrained (no lower boundary at zero), a negative contribution suggests the model is absorbing noise or confounding through that channel. Review the posterior forest plot and check whether the coefficient’s credible interval excludes zero.
  • Intercept-dominated decomposition (not shown here, but visible in the CSV): If the intercept accounts for >90% of the total, media effects are negligible relative to baseline demand. This may be correct, but it limits the utility of the model for budget allocation.
  • Missing plot: If the decomposition failed (logged as a warning), the model type likely does not support direct model.matrix() decomposition. The CSV companions will also be absent.

Action

Use this plot to prioritise which channels to scrutinise. Cross-reference large contributors with the prior vs posterior plot to confirm they are data-driven rather than prior-driven.

  • decomp_predictor_impact.csv is the corresponding tabular output when decomposition artifacts are enabled.
  • posterior_summary.csv in 30_post_run/ provides the coefficient summary underlying the decomposition.

Decomposition time series

Filename: decomp_timeseries.png

Decomposition time series Decomposition time series

What it shows

A stacked area chart of media channel contributions over time. Each layer represents one media term’s weekly contribution (coefficient × transformed media input). Non-media terms (intercept, controls, seasonality) are excluded to focus the view on the media mix.

When it is generated

The plot is generated alongside the predictor impact chart when outputs.save_decomp_png: true and the decomposition tables include at least one media term.

How to interpret it

The height of each band at a given week represents how much that channel contributed to the modelled response. Seasonal patterns in the stack reflect campaign timing and adstock carry-over. The total height of the stack is the aggregate media contribution — the gap between this and the observed KPI is accounted for by non-media terms and noise.

Warning signs

  • A channel with near-zero contribution throughout: The model assigns negligible effect to that channel. This could be correct (low spend, weak signal) or a sign that multicollinearity is suppressing the estimate.
  • Implausibly large single-channel dominance: If one channel accounts for the vast majority of the media stack, verify the coefficient is plausible and not inflated by collinearity with a correlated channel.
  • Abrupt jumps unrelated to spend changes: Check whether the design matrix term (adstock/saturation output) is well-behaved. Sudden spikes in contribution without corresponding spend changes suggest a data or transform issue.

Action

Compare the relative channel contributions here with the business’s spend allocation. Channels that receive large spend but show small contributions may have diminishing returns or weak effects. This comparison motivates the budget optimisation stage.

  • decomp_timeseries.csv is the corresponding long-format output when decomposition artifacts are enabled.

Cross-references

Diagnostics Plots

Purpose

Diagnostics plots assess whether the fitted model’s assumptions hold and whether any structural problems warrant remedial action. They cover residual behaviour, posterior predictive adequacy, and boundary constraint monitoring. These plots are written to 40_diagnostics/ within the run directory.

The runner generates residual plots via write_residual_diagnostics() in R/run_artifacts_diagnostics.R, the PPC plot via write_model_fit_plots() in R/run_artifacts_enrichment.R, and the boundary hits plot via write_boundary_diagnostics() in R/run_artifacts_diagnostics.R. Each plot is wrapped in tryCatch so that individual failures do not block the remaining outputs.

Plot catalogue

Filename What it shows Conditions
ppc.png Posterior predictive check fan chart Posterior draws (yhat) extractable from fitted model
residuals_timeseries.png Residuals over time Fit table available
residuals_vs_fitted.png Residuals vs fitted values Fit table available
residuals_hist.png Residual distribution histogram Fit table available
residuals_acf.png Residual autocorrelation function Fit table available
residuals_latent_acf.png Latent-scale residual ACF Model uses log-scale response (response_scale != "identity")
boundary_hits.png Posterior draw proximity to coefficient bounds Boundary hit rates computable from posterior and bound specifications

Posterior predictive check (PPC)

Filename: ppc.png

Posterior predictive check Posterior predictive check

What it shows

A fan chart of posterior predictive draws overlaid with observed data. The blue line is the posterior mean of the predicted response; the dark band spans the 25th–75th percentile (50% CI) and the light band spans the 5th–95th percentile (90% CI). Red dots mark observed values.

When it is generated

The runner generates this plot whenever posterior predictive draws (yhat) can be extracted from the fitted model via runner_yhat_draws(). This works for BLM, hierarchical, and pooled models fitted with MCMC.

How to interpret it

Well-calibrated models produce bands that contain roughly 50% and 90% of observed points in the respective intervals. The key diagnostic is whether observed values fall systematically outside the bands during specific periods — this reveals time-localised misfit that aggregate metrics like RMSE can mask.

Warning signs

  • Observed points consistently outside the 90% band: The model underestimates uncertainty or misses a structural feature (holiday, promotion, regime change).
  • Bands that widen dramatically in specific periods: The model is uncertain about those periods, possibly because the training data lacks similar observations.
  • Bands that are uniformly very wide: The noise prior may be too diffuse, or the model has too many weakly identified parameters.

Action

If the PPC reveals localised misfit, check whether the affected periods correspond to missing control variables (holidays, events). If the bands are too wide overall, consider tightening the noise prior or simplifying the formula. Cross-reference with the LOO-PIT histogram for an aggregate calibration assessment.


Residuals over time

Filename: residuals_timeseries.png

Residuals time series Residuals time series

What it shows

A line chart of residuals (observed minus posterior mean) over time. A horizontal reference line at zero marks perfect fit. For hierarchical models, the plot facets by group.

When it is generated

Always, provided the fit table is available.

How to interpret it

Residuals should scatter randomly around zero with no discernible trend or seasonal pattern. Any structure in the residuals indicates that the model has failed to capture a systematic component of the data.

Warning signs

  • Trend in residuals: The model’s trend specification is inadequate. Consider adding a higher-order polynomial or a structural-break term.
  • Seasonal oscillation: The Fourier harmonics or holiday dummies are insufficient. Add more harmonics or specific event indicators.
  • Clusters of large residuals: Localised misfit — check corresponding dates for data anomalies.

Action

Residual structure that persists across multiple weeks warrants a formula revision. Short isolated spikes are often data outliers and may not require model changes.


Residuals vs fitted

Filename: residuals_vs_fitted.png

Residuals vs fitted Residuals vs fitted

What it shows

A scatter plot of residuals (y-axis) against posterior mean fitted values (x-axis), with a horizontal reference at zero. For hierarchical models, the plot facets by group.

When it is generated

Always, provided the fit table is available.

How to interpret it

The scatter should form a horizontal band centred on zero with roughly constant vertical spread across the fitted-value range. Patterns in this plot diagnose specific model violations.

Warning signs

  • Funnel shape (wider spread at higher fitted values): Heteroscedasticity. A log-scale model would be more appropriate.
  • Curvature: The mean function is misspecified. The model under- or over-predicts at the extremes.
  • Discrete clusters: May indicate grouping structure that the model does not account for.

Action

Heteroscedasticity in a levels model is the most common finding. If the funnel pattern is pronounced, re-fit on the log scale and compare diagnostics. Cross-reference with the fit scatter plot which shows the same information from a different angle.


Residual distribution

Filename: residuals_hist.png

Residual histogram Residual histogram

What it shows

A histogram of residuals across all observations (40 bins). For hierarchical models with six or fewer groups, the histogram facets by group.

When it is generated

Always, provided the fit table is available.

How to interpret it

The distribution should be approximately symmetric and unimodal if the Normal noise assumption holds. Heavy tails or skewness indicate departures from normality.

Warning signs

  • Strong right skew: Common in levels models when the response is strictly positive and has occasional large values. A log transform may help.
  • Bimodality: Suggests a mixture or an omitted grouping variable. Check whether the data contains distinct regimes.
  • Extreme outliers: Individual residuals several standard deviations from the mean warrant data inspection.

Action

Moderate departures from normality in the residuals are tolerable in Bayesian inference — the posterior is still valid if the model is otherwise well-specified. Severe skewness or heavy tails, however, can distort credible intervals and predictive coverage. Consider robust likelihood specifications or transformations.


Residual autocorrelation (ACF)

Filename: residuals_acf.png

Residual ACF Residual ACF

What it shows

A bar chart of the sample autocorrelation function of residuals, computed up to lag 26 (roughly half a year of weekly data). Red dashed lines mark the 95% significance bounds (±1.96/√n). For hierarchical models, the plot facets by group.

When it is generated

Always, provided the fit table is available.

How to interpret it

Bars within the significance bounds indicate no serial correlation at that lag. Significant autocorrelation — especially at low lags (1–4 weeks) — means the model misses short-run temporal dependence. Significant spikes at lag 52 (if the series is long enough) suggest residual annual seasonality.

Warning signs

  • Lag-1 ACF > 0.3: Strong short-run autocorrelation. The model’s uncertainty estimates are anti-conservative (credible intervals too narrow), and coefficient estimates may be biased if lagged effects are present.
  • Decaying positive ACF: Suggests an omitted AR component or insufficient adstock decay modelling.
  • Spike at lag 52: Residual annual seasonality not captured by the Fourier terms.

Action

If lag-1 ACF is material, consider adding lagged response terms or increasing the number of Fourier harmonics. For adstock-driven channels, verify that the decay rate is not too fast (underfitting carry-over) or too slow (overfitting noise).


Latent-scale residual ACF

Filename: residuals_latent_acf.png

Latent ACF Latent ACF

What it shows

The same ACF plot as above, but computed on the latent (log) scale when the model’s response scale is not identity. This is relevant for models fitted with model.scale: true or log-transformed response variables.

When it is generated

The runner generates this plot when response_scale != "identity". It is skipped for levels-scale models.

How to interpret it

Interpretation is identical to the standard ACF plot. The latent-scale version is preferred for log models because autocorrelation in the log residuals is more directly interpretable as a model adequacy check on the scale where inference is performed.

Warning signs

Same as the standard ACF. Compare both plots if both are generated — discrepancies may indicate that the log transformation introduces or removes autocorrelation artefacts.


Boundary hits

Filename: boundary_hits.png

Boundary hits Boundary hits

What it shows

A horizontal chart showing, for each constrained coefficient, the share of posterior draws that fall within a tolerance of the finite lower or upper bound. Bars are colour-coded: green (0% hit rate), amber (1–10%), red (≥10%). When all hit rates are zero, the plot displays green dots with explicit “0.0%” labels.

When it is generated

The runner generates this plot when the model has finite boundary constraints set via set_boundary() and boundary hit rates can be computed from the posterior draws. It is written by write_boundary_diagnostics() in R/run_artifacts_diagnostics.R.

How to interpret it

A zero hit rate for all parameters means no posterior draws approached any boundary — the constraints are not binding and the posterior is effectively unconstrained. This is the ideal outcome.

A non-zero hit rate means the boundary is influencing the posterior shape. Moderate rates (1–10%) suggest the data mildly conflicts with the constraint; high rates (≥10%) mean the data wants the coefficient outside the allowed range and the boundary is actively truncating the posterior.

Warning signs

  • Hit rate ≥10% on a media coefficient: The non-negativity constraint is binding. The true effect may be zero or negative, but the boundary forces a positive estimate. This inflates the channel’s apparent contribution.
  • Hit rate ≥10% on many parameters simultaneously: The overall constraint specification may be too tight for the data. Consider widening bounds or reviewing the formula.
  • Lower-bound hits on a coefficient with strong prior mass at zero: The prior and boundary together may create a “pile-up” at the bound. The posterior is not reflecting the data faithfully.

Action

For channels with high boundary hit rates, critically assess whether the non-negativity constraint is justified by domain knowledge. If the constraint is essential (e.g. media cannot destroy demand), document that the estimate is boundary-driven. If it is not essential, consider relaxing the bound and re-fitting to see whether the unconstrained estimate is materially different.

  • boundary_hits.csv in 40_diagnostics/ provides the per-parameter hit rates in tabular form.
  • diagnostics_report.csv in 40_diagnostics/ includes a summary check for boundary binding.

Hierarchical-specific: within variation

Filename: within_variation.png (generated only for hierarchical models)

This plot shows the within-group variation ratio for each non-CRE (correlated random effects) term: Var(x − mean_g(x)) / Var(x). Low ratios indicate that most variation in a predictor is between groups rather than within groups, making it difficult to identify the coefficient from within-group variation alone. Dashed lines at 5% and 10% mark conventional concern thresholds.

This plot is generated only for hierarchical models and is not included in the standard BLM image set.


Cross-references

Model Selection Plots

Purpose

Model selection plots provide leave-one-out cross-validation (LOO-CV) diagnostics that assess predictive adequacy and calibration. They help answer: does the model generalise to unseen observations, and are any individual data points unduly influencing the fit? These plots are written to 50_model_selection/ within the run directory.

The runner generates them via write_model_selection_artifacts() in R/run_artifacts_diagnostics.R. LOO-CV is computed using Pareto-smoothed importance sampling (PSIS-LOO) from the loo package, which approximates exact leave-one-out predictive densities from a single MCMC fit. All three plots depend on the pointwise LOO table (loo_pointwise.csv), which contains per-observation ELPD contributions, Pareto-k diagnostics, and influence flags.

PSIS-LOO assumes conditionally exchangeable pointwise observations. For time-ordered MMM model selection, use blocked or leave-future-out CV as the primary evidence. Treat these plots as supplementary fit, influence, and calibration diagnostics.

Plot catalogue

Filename What it shows Conditions
pareto_k.png Pareto-k diagnostic scatter over time Pointwise LOO table available with pareto_k column
loo_pit.png LOO-PIT calibration histogram Posterior draws (yhat) extractable from fitted model
elpd_influence.png Pointwise ELPD contributions over time Pointwise LOO table available with elpd_loo and pareto_k columns

Pareto-k diagnostic

Filename: pareto_k.png

Pareto-k diagnostic Pareto-k diagnostic

What it shows

A scatter plot of Pareto-k values over time, one point per observation. Points are colour-coded by severity:

  • Green (k < 0.5): PSIS approximation is reliable.
  • Amber (0.5 ≤ k < 0.7): Approximation is acceptable but warrants monitoring.
  • Red (0.7 ≤ k < 1.0): Approximation is unreliable. The observation is influential.
  • Purple (k > 1.0): PSIS fails entirely. The observation dominates the posterior.

Dashed horizontal lines mark the 0.5, 0.7, and 1.0 thresholds. The legend always displays all four severity levels regardless of whether points exist in each category.

When it is generated

The runner generates this plot whenever the pointwise LOO table contains a pareto_k column. This requires a successful PSIS-LOO computation, which in turn requires the fitted model to produce log-likelihood values.

How to interpret it

Most points should be green. A small number of amber points is typical and does not invalidate the LOO estimate. Red and purple points identify observations where the posterior changes substantially when that observation is excluded — these are influential data points.

Influential observations concentrated in a specific time period (e.g. a cluster of red points around a holiday) suggest that the model struggles with those conditions. Isolated influential points may correspond to data anomalies or outliers.

Warning signs

  • More than 10% of points above 0.7: The overall PSIS-LOO estimate is unreliable. The loo package will issue a warning. Consider moment-matching or exact refitting for affected observations.
  • Purple points (k > 1): These observations are so influential that removing them would substantially change the posterior. Investigate whether they represent data errors, one-off events, or genuine but rare conditions.
  • Influential points at the start or end of the series: Edge effects in adstock transforms can create artificial influence at series boundaries.

Action

For isolated red/purple points, inspect the corresponding dates and data values. If they are data errors, correct the data. If they are genuine but extreme, consider whether the model’s likelihood (Normal) is appropriate — heavy-tailed alternatives (Student-t) are more robust to outliers. If influential points are numerous, the model may be misspecified more broadly: revisit the formula, priors, and functional form.

  • loo_pointwise.csv in 50_model_selection/ contains the per-observation Pareto-k, ELPD, and influence flags.
  • loo_summary.csv in 50_model_selection/ reports the aggregate ELPD with standard error.

LOO-PIT calibration histogram

Filename: loo_pit.png

LOO-PIT histogram LOO-PIT histogram

What it shows

A histogram of leave-one-out probability integral transform (LOO-PIT) values across all observations. The PIT value for observation t is the proportion of posterior predictive draws that fall below the observed value: PIT_t = Pr(ŷ_t ≤ y_t | y_{-t}). The histogram uses 20 equal-width bins from 0 to 1. A dashed red horizontal line marks the expected count under a perfectly calibrated model (n/20).

When it is generated

The runner generates this plot whenever posterior predictive draws can be extracted via runner_yhat_draws(). It does not require the pointwise LOO table — it computes PIT values directly from the posterior predictive distribution. The plot is written by write_model_fit_plots() in R/run_artifacts_enrichment.R and filed under 50_model_selection/.

How to interpret it

A well-calibrated model produces a uniform PIT distribution — all bins should be roughly equal in height, close to the dashed reference line. Departures from uniformity reveal specific calibration failures:

  • U-shape (excess mass at 0 and 1): The model is overdispersed — its predictive intervals are too narrow. Observed values fall in the tails of the predictive distribution more often than expected.
  • Inverse U-shape (excess mass in the centre): The model is underdispersed — its predictive intervals are too wide. The model is more uncertain than it needs to be.
  • Left-skewed (excess mass near 0): The model systematically overpredicts. Observed values tend to fall below the predictive distribution.
  • Right-skewed (excess mass near 1): The model systematically underpredicts.

Warning signs

  • Strong U-shape: The noise variance is underestimated or the model is missing a source of variation. This is the most concerning pattern because it means the credible intervals are anti-conservative — reported uncertainty is too low.
  • One bin dramatically taller than others: A single bin containing many more observations than expected suggests a discrete cluster of misfits. Check the dates of those observations.
  • Monotone slope: A systematic bias that the model has not captured. Check the residuals time series for trend.

Action

U-shaped PIT histograms call for wider predictive intervals: increase the noise prior, add missing covariates, or allow for heavier tails. Inverse-U patterns suggest the noise prior is too diffuse — tighten it. Skewed patterns indicate systematic bias that should be addressed through formula changes (missing controls, trend, level shifts). Cross-reference with the PPC fan chart for a visual complement.


ELPD influence plot

Filename: elpd_influence.png

ELPD influence ELPD influence

What it shows

A lollipop chart of pointwise expected log predictive density (ELPD) contributions over time. Each vertical stem connects the observation’s ELPD value to zero; the dot marks the ELPD value. Blue points and stems indicate non-influential observations (Pareto-k ≤ 0.7); red indicates influential ones (Pareto-k > 0.7). Larger red dots draw attention to the problematic observations.

When it is generated

The runner generates this plot whenever the pointwise LOO table contains both elpd_loo and pareto_k columns. It is written by write_model_selection_artifacts() in R/run_artifacts_diagnostics.R, immediately after the Pareto-k scatter.

How to interpret it

ELPD values quantify each observation’s contribution to the model’s out-of-sample predictive performance. Values near zero indicate observations that the model predicts well. Large negative values indicate observations where the model assigns low predictive probability — these are the worst-predicted points.

The combination of ELPD magnitude and Pareto-k severity is informative:

  • Large negative ELPD + low k: The model predicts this observation poorly, but the PSIS estimate is reliable. The model genuinely struggles with this data point.
  • Large negative ELPD + high k: Both the prediction and the LOO approximation are unreliable. This observation is highly influential and poorly fit — it warrants the closest scrutiny.
  • Near-zero ELPD + high k: The observation is influential but well-predicted. It may be a leverage point (extreme in predictor space) that happens to lie on the fitted surface.

Warning signs

  • Cluster of large negative values in a specific period: The model systematically fails during that period. Check for missing events, structural breaks, or data quality problems.
  • Many red (influential) points with large negative ELPD: The model’s aggregate LOO estimate is unreliable, and the worst-fit observations are also the most influential. This combination makes model comparison results untrustworthy.
  • Monotone trend in ELPD values: Suggests time-varying model adequacy — the model may fit the training period well but degrade towards the edges.

Action

Investigate the dates of the worst ELPD observations. If they correspond to known anomalies (data errors, one-off events), consider excluding or down-weighting them. If they correspond to regular conditions that the model should handle, the model needs revision. Use the Pareto-k plot to confirm which observations are both poorly predicted and influential, and prioritise those for investigation.

  • loo_pointwise.csv in 50_model_selection/ contains the full pointwise table with ELPD, Pareto-k, and influence flags.
  • loo_summary.csv in 50_model_selection/ reports the aggregate ELPD estimate and standard error for model comparison.

Cross-references

Optimisation Plots

Purpose

Optimisation plots visualise the outputs of the budget allocator. They translate model estimates into actionable budget decisions by showing response curves, efficiency comparisons, and the sensitivity of recommendations to budget changes. These are decision-layer artefacts: they sit downstream of all modelling and diagnostics, and their quality depends entirely on the credibility of the upstream fit.

All optimisation plots are written to 60_optimisation/ within the run directory. The runner generates them via write_budget_optimisation_artifacts() in R/run_artifacts_enrichment.R, which calls the public plotting APIs in R/optimise_budget_plots.R. They require a successful call to optimise_budget() that produces a budget_optimisation object with a plot_data payload.

Plot catalogue

Filename What it shows Conditions
budget_response_curves.png Channel response curves with current/optimised points Optimisation completed with response curve data
budget_roi_cpa.png ROI or CPA comparison by channel Optimisation completed with ROI/CPA summary
budget_impact.png Spend reallocation and response impact (diverging bars) Optimisation completed with ROI/CPA summary
budget_contribution.png Absolute response comparison by channel Optimisation completed with ROI/CPA summary
budget_confidence_comparison.png Posterior credible intervals for current vs optimised Optimisation completed with response points
budget_sensitivity.png Total response change when each channel varies ±20% Optimisation completed with response curve data
budget_efficient_frontier.png Optimised response across budget levels Efficient frontier computed via budget_efficient_frontier()
budget_kpi_waterfall.png KPI decomposition waterfall (base + channels + controls) Waterfall data computable from model coefficients and data means
budget_marginal_roi.png Marginal ROI (or marginal response) curves by channel Optimisation completed with response curve data
budget_spend_share.png Current vs optimised spend allocation as percentage Optimisation completed with ROI/CPA summary

Response curves

Filename: budget_response_curves.png

Response curves Response curves

What it shows

Faceted line charts of the estimated response curve for each media channel. The x-axis is raw spend (model units); the y-axis is expected response. A shaded band shows the posterior credible interval around the mean curve. Two marked points per channel indicate the current (reference) and optimised spend allocations.

The subtitle notes which media transforms were applied (e.g. Hill saturation, adstock). A caption reports the marginal response at the optimised point for each channel.

When it is generated

The runner generates this plot whenever optimise_budget() returns response curve data in the plot_data payload. This requires at least one media channel in the allocation configuration with a computable response function.

How to interpret it

The curve shape encodes diminishing returns. Steep initial slopes indicate high marginal response at low spend; flattening curves indicate saturation. The gap between the current and optimised points shows the direction of the recommended reallocation: if the optimised point sits to the right (higher spend) of the current point, the allocator recommends increasing that channel’s budget.

The credible band width reflects posterior uncertainty about the response function. Wide bands mean the shape is poorly identified — the recommendation is sensitive to modelling assumptions. Narrow bands indicate data-informed estimates.

Warning signs

  • Very wide credible bands: The response curve shape is uncertain. Budget recommendations based on it carry substantial risk.
  • Optimised point near the flat part of the curve: The channel is saturated at the recommended spend. Further increases yield negligible marginal returns.
  • Current and optimised points nearly identical: The allocator found little room for improvement on that channel. The current allocation is already near-optimal (or the response function is too uncertain to justify a change).

Action

Compare the marginal response values across channels. The allocator equalises marginal response at the optimum — if marginal values differ substantially, the optimisation may have hit a constraint (spend floor/ceiling). Cross-reference with the budget sensitivity plot to assess how robust the recommendation is.

  • budget_response_curves.csv in 60_optimisation/ contains the curve data.
  • budget_response_points.csv in 60_optimisation/ contains the current and optimised point coordinates.

ROI/CPA comparison

Filename: budget_roi_cpa.png

ROI/CPA comparison ROI/CPA comparison

What it shows

A grouped bar chart comparing ROI (or CPA, for subscription KPIs) by channel under the current and optimised allocations. If currency_col is defined per channel, bars show financial ROI; otherwise they show response-per-unit-spend in model units. A TOTAL bar summarises the portfolio-level metric.

The metric choice is automatic: the allocator uses ROI for revenue-type KPIs and CPA for subscription-type KPIs.

When it is generated

The runner generates this plot whenever the optimisation result includes a roi_cpa summary table.

How to interpret it

Channels where the optimised bar exceeds the current bar gain efficiency from the reallocation. Channels where the optimised bar is lower have had spend reduced — their marginal efficiency was below the portfolio average. The TOTAL bar shows the net portfolio improvement.

Warning signs

  • Optimised ROI lower than current for most channels: The allocator redistributed spend towards higher-response channels, which may have lower per-unit efficiency but larger absolute contribution. This is not necessarily wrong — the allocator maximises total response, not per-channel ROI.
  • TOTAL bar shows negligible improvement: The current allocation is already near-optimal, or the model’s response functions are too flat to support meaningful reallocation.
  • Very large ROI values on low-spend channels: Small denominators inflate ROI. These channels may have high marginal returns at low spend but limited capacity to absorb budget.

Action

Do not interpret this plot in isolation. Cross-reference with the contribution comparison and the response curves to distinguish efficiency improvements from scale effects.

  • budget_roi_cpa.csv in 60_optimisation/ contains the per-channel ROI/CPA values.
  • budget_summary.csv in 60_optimisation/ provides the top-level allocation summary.

Allocation impact

Filename: budget_impact.png

Budget impact Budget impact

What it shows

A horizontal diverging bar chart in two facets. The left facet shows spend reallocation (positive = increase, negative = decrease) per channel. The right facet shows the corresponding response impact. Bars are coloured green for increases and red for decreases. A TOTAL row at the bottom summarises the net change with muted styling.

Channels are sorted by response impact magnitude — the channels most affected by the reallocation appear at the top.

When it is generated

The runner generates this plot whenever the optimisation result includes a roi_cpa summary with delta_spend and delta_response columns.

How to interpret it

The spend facet shows where the allocator moves budget. The response facet shows the expected consequence. A useful pattern is a channel that receives a spend decrease (red bar, left) but shows a small response decrease (small red bar, right) — that channel was inefficient and the freed budget drives larger gains elsewhere.

Warning signs

  • Large spend increase on a channel with modest response gain: Diminishing returns may be steep. Verify against the response curve.
  • Response decreases that exceed response gains: The allocator expects a net negative outcome. This should not happen with a correctly specified max_response objective, and suggests a configuration or constraint issue.

Action

Use this chart to brief stakeholders on the “where and why” of reallocation. Pair it with the confidence comparison to communicate whether the expected gains are statistically distinguishable from zero.


Response contribution

Filename: budget_contribution.png

Contribution comparison Contribution comparison

What it shows

A grouped bar chart comparing absolute expected response (contribution) by channel under the current and optimised allocations. Delta annotations above each pair show the change. A TOTAL bar with muted styling shows the portfolio-level gain. The subtitle reports the percentage total response gain from optimisation.

When it is generated

The runner generates this plot whenever the optimisation result includes mean_reference and mean_optimised columns in the roi_cpa summary.

How to interpret it

This chart answers the question: in absolute terms, how much more (or less) response does each channel deliver under the optimised allocation? Unlike the ROI chart, this view is not distorted by small denominators — it shows the quantity the allocator actually maximises.

Warning signs

  • Negative delta on a channel with high current contribution: The allocator is pulling spend from a channel that currently contributes a great deal. This is rational if the marginal return on that channel is below the portfolio average, but it requires careful communication to stakeholders accustomed to interpreting total contribution as “importance”.
  • TOTAL gain is small: The reallocation may not justify the operational cost of implementing it. Consider whether the confidence intervals overlap (see confidence comparison).

Action

Report the TOTAL percentage gain as the headline number. Caveat it with the credible interval width from the confidence comparison. If the gain is within posterior uncertainty, the recommendation is suggestive rather than conclusive.

  • budget_allocation.csv in 60_optimisation/ contains the per-channel spend and response values.

Confidence comparison

Filename: budget_confidence_comparison.png

Confidence comparison Confidence comparison

What it shows

A horizontal forest plot (dodge-positioned point-and-errorbar) showing the posterior mean response and 90% credible interval for each channel under the current (grey) and optimised (red) allocations. Channels where the intervals overlap suggest that the reallocation gain may not be statistically meaningful.

When it is generated

The runner generates this plot whenever the optimisation result includes response point data with mean, lower, and upper columns for both reference and optimised allocations.

How to interpret it

Focus on channels where the optimised interval (red) does not overlap with the current interval (grey). These are the channels where the reallocation produces a distinguishable change in expected response. Overlapping intervals mean the posterior cannot confidently distinguish the two allocations — the gain exists in expectation but falls within sampling uncertainty.

Warning signs

  • All intervals overlap: The data is too uncertain to support a confident reallocation recommendation. The allocator’s point estimate suggests improvement, but the posterior cannot distinguish it from noise.
  • One channel shows a clear gain while others overlap: The headline portfolio gain may be driven by a single channel. Verify that channel’s response curve and prior-posterior shift.

Action

Use this plot to calibrate the confidence of the recommendation. If intervals overlap for most channels, present the allocation as “directionally suggestive” rather than “statistically supported”. If key channels show clear separation, the recommendation is stronger.


Budget sensitivity

Filename: budget_sensitivity.png

Budget sensitivity Budget sensitivity

What it shows

A spider chart (line plot) showing how total expected response changes when each channel’s spend is varied ±20% from its optimised level, while all other channels are held fixed. Steeper lines indicate channels whose budgets have the most influence on total response. A horizontal dashed line at zero marks the optimised baseline.

When it is generated

The runner generates this plot whenever the optimisation result includes response curve data. The ±20% range and 11 evaluation points per channel are defaults set in plot_budget_sensitivity().

How to interpret it

Channels with steep lines are the most sensitive: small deviations from their optimised spend produce large response changes. Flat lines indicate channels where modest budget deviations have little impact — the response function is either saturated (on the flat part of the curve) or nearly linear (constant marginal return).

Warning signs

  • A channel with an asymmetric slope (steep downward, flat upward): Cutting this channel’s spend is costly, but increasing it yields little. It is at or near its saturation point.
  • All lines nearly flat: The optimisation surface is plateau-like. The allocator’s recommendation is robust to implementation imprecision, but also implies limited upside from optimisation.
  • Lines that cross: Channels swap in relative importance at different budget perturbations. This complicates simple priority rankings.

Action

Use this chart to communicate implementation risk. If the recommended allocation is operationally difficult to achieve exactly, the sensitivity chart shows which channels require precise execution and which have margin for error.


Efficient frontier

Filename: budget_efficient_frontier.png

Efficient frontier Efficient frontier

What it shows

A line-and-point chart of total optimised response as a function of total budget. Each point represents the optimal allocation at that budget level (expressed as a percentage of the current total budget). A red diamond marks the current budget level. The curve shows how much additional response is achievable by increasing the total budget — and the diminishing returns of doing so.

When it is generated

The runner generates this plot when budget_efficient_frontier() produces a budget_frontier object with at least two feasible points. This requires a valid optimisation result and a set of budget multipliers (configured in allocation.efficient_frontier).

How to interpret it

The frontier’s shape reveals the budget’s overall productivity. A concave curve (steepening, then flattening) is the classic diminishing-returns shape: each additional unit of budget buys less incremental response. The gap between the current point and the curve above it shows the unrealised potential at the same budget — the difference between the current allocation and the optimal one.

Warning signs

  • Frontier is nearly linear: Returns are approximately constant across the budget range. The model may not have enough data to identify saturation, or the budget range is too narrow to reveal it.
  • Frontier flattens early: The portfolio saturates at a budget well below the current level. The current spend may be wastefully high.
  • Only 2–3 feasible points: The optimiser could not find feasible allocations at most budget levels. Constraints may be too tight.

Action

Use the frontier to frame budget conversations. The curve shows what is achievable at each budget level. If a stakeholder proposes a budget cut, the frontier quantifies the response cost. If they propose an increase, it quantifies the expected gain. Present the frontier alongside the spend share comparison to show how the allocation shifts at each level.

  • budget_efficient_frontier.csv in 60_optimisation/ contains the frontier data.

KPI waterfall

Filename: budget_kpi_waterfall.png

KPI waterfall KPI waterfall

What it shows

A horizontal waterfall bar chart decomposing the predicted KPI into its constituent components: base (intercept), trend, seasonality, holidays, controls, and individual media channels. Each bar shows the mean posterior coefficient multiplied by the mean predictor value — the average contribution of that component to the predicted KPI. A red TOTAL bar anchors the sum.

When it is generated

The runner generates this plot when build_kpi_waterfall_data() can extract posterior coefficients and match them to predictor means in the original data. This requires that the model’s .formula and .original_data are both accessible. For hierarchical models with random-effects syntax, the waterfall may fail gracefully and be skipped.

How to interpret it

The waterfall answers: “of the total predicted KPI, how much comes from each source?” The base (intercept) typically dominates, representing baseline demand independent of media and controls. Media channels sit at the bottom, showing their individual incremental contributions. The relative sizes of the media bars correspond to the decomposition impact chart (decomp_predictor_impact.png), but computed slightly differently (mean × mean vs sum over time).

Warning signs

  • Negative media contributions: A channel with a negative bar reduces predicted KPI. Unless the coefficient is intentionally unconstrained, this suggests a fitting or identification problem.
  • Intercept dwarfs all other terms: The model attributes nearly all KPI to baseline demand. Media effects are marginal. This may be realistic for low-spend brands but limits the value of budget optimisation.
  • Missing plot (skipped with warning): The model type does not support direct waterfall decomposition.

Action

Use the waterfall to contextualise media contributions within the total predicted KPI. For stakeholder reporting, it provides a clear answer to “what drives our KPI?” — while emphasising that media is one factor among several.

  • budget_kpi_waterfall.csv in 60_optimisation/ contains the waterfall data.

Marginal ROI curves

Filename: budget_marginal_roi.png

Marginal ROI Marginal ROI

What it shows

Faceted line charts of marginal ROI (or marginal response, if no currency conversion is configured) as a function of spend for each channel. The marginal value is computed as the first difference of the response curve: the additional response per additional unit of spend. Current and optimised points are marked.

When it is generated

The runner generates this plot whenever the optimisation result includes response curve data with at least two points per channel.

How to interpret it

The marginal ROI curve is the derivative of the response curve. At the optimised allocation, the allocator equalises marginal ROI across channels (subject to constraints). If one channel’s marginal ROI at the optimised point is substantially higher than another’s, a constraint (spend floor or ceiling) is preventing further reallocation.

Diminishing returns appear as a downward-sloping marginal curve: each additional unit of spend yields less incremental response than the last. Channels with steeper slopes saturate faster.

Warning signs

  • Marginal ROI near zero at the optimised point: The channel is at or near saturation. Additional spend yields negligible incremental response.
  • Marginal ROI that increases with spend: This implies increasing returns, which is unusual for media. It may indicate a response curve misspecification or insufficient data in the high-spend region.
  • Large differences in marginal ROI at the optimised points across channels: Constraints are binding. The allocator cannot equalise marginal returns because spend bounds prevent it.

Action

Use marginal ROI to identify which channels have headroom (high marginal ROI at the optimised point) and which are saturated (marginal ROI near zero). This informs not just the current allocation but also the value of relaxing spend constraints.


Spend share comparison

Filename: budget_spend_share.png

Spend share Spend share

What it shows

Two horizontal stacked bars showing the percentage allocation of total budget across channels: one for the current allocation and one for the optimised allocation. Percentage labels appear within each segment (for segments ≥ 4% of total). The subtitle reports the total budget in currency or model units for both allocations.

When it is generated

The runner generates this plot whenever the optimisation result includes a roi_cpa summary with spend_reference and spend_optimised columns.

How to interpret it

This is the most intuitive optimisation output for non-technical stakeholders. It answers: “how should we split the budget?” Segments that grow from current to optimised represent channels the allocator recommends investing more in; segments that shrink represent channels to reduce.

Warning signs

  • A channel disappears (0% share) in the optimised allocation: The allocator has hit the channel’s spend floor (which may be zero). If this is unintended, raise the minimum spend constraint.
  • Allocations are nearly identical: The current mix is already near-optimal, or the model cannot distinguish channel effects well enough to justify reallocation.
  • Very small segments in both allocations: Channels with negligible spend share contribute little to the optimisation. Consider whether they should be included or grouped.

Action

Present this chart as the primary recommendation visual. Accompany it with the confidence comparison to communicate the certainty of the recommendation and the allocation impact chart to show the expected consequence.


Cross-references

How-To Guides

Purpose

Provide task-oriented recipes for common DSAMbayes operational workflows. Each guide starts from a user objective, gives minimal reproducible steps, and includes expected output artefacts and quick verification checks.

Audience

  • Users who know the concepts but need execution steps.
  • Engineers debugging run and artefact issues.

Pages

Guide Objective
Run from YAML Execute a complete runner workflow and verify staged outputs
Interpret Diagnostics Read and act on diagnostics gate results
Compare Runs Compare multiple runs and select a candidate model
Debug Run Failures Diagnose and resolve common runner failure modes

Subsections of How-To Guides

Run from YAML

Objective

Execute a complete DSAMbayes model run from a YAML configuration file and verify the staged output artefacts.

Prerequisites

  • DSAMbayes installed locally (see Install and Setup).
  • A YAML config file (see Config Schema for structure).
  • Data file(s) referenced by the config are accessible.

Steps

1. Set up the environment

source scripts/r-library-path.sh
dsambayes_set_r_library host
mkdir -p "$R_LIBS_USER" .cache
export XDG_CACHE_HOME="$PWD/.cache"

2. Validate the configuration (dry run)

Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml

Expected outcome:

  • Exit code 0.
  • No Stan compilation or sampling occurs.
  • Metadata artefacts are written only if you supply --run-dir or set outputs.run_dir.

If validation fails:

  • Check the error message for missing data paths, invalid YAML keys, or formula errors.
  • Remember that the authored v2 schema does not expose model.formula; the runner compiles it from target, media, controls, and optional hierarchy / effects.
  • Fix the config and re-run validate before proceeding.

3. Run the model

Rscript scripts/dsambayes.R run --config config/cre_geo_panel.yaml

Expected outcome:

  • Exit code 0.
  • Full staged artefact tree under the run directory.

4. Locate the run directory

The runner prints the run directory path during execution. It follows the pattern:

results/YYYYMMDD_HHMMSS_<run_label>/

5. Verify artefacts

Check that the following stage folders are populated:

Stage Folder Key files
Metadata 00_run_metadata/ config.original.yaml, config.resolved.yaml, config.compiled.yaml, artifact_schema.yaml, session_info.txt
Pre-run 10_pre_run/ Media spend plots, VIF bar chart
Model fit 20_model_fit/ model.rds, optional deployment_model.rds, fit plots
Post-run 30_post_run/ posterior_summary.csv, observed.csv, fitted.csv, plus decomposition tables/plots when enabled and available
Diagnostics 40_diagnostics/ diagnostics_report.csv, diagnostic plots
Model selection 50_model_selection/ LOO summary, Pareto-k plot (if MCMC)
Optimisation 60_optimisation/ Allocation summary, response curves (if enabled)

Optional deployment artifact:

  • Set outputs.save_deployment_model_rds: true to write 20_model_fit/deployment_model.rds.
  • This artifact is a compact deployment package for explicit predict(newdata = ...) and explicit-data decomposition; it does not replace model.rds.
  • Supported for model.type: blm, model.type: pooled with fit.method: mcmc, or hierarchical model.type: re / cre with fit.method: mcmc.
  • Pooled deployment artifacts score on authored terms and do not require pooling columns in deployment-time newdata / data = ... unless those columns are also ordinary formula terms.
  • Hierarchical deployment artifacts are seen-groups-only. Explicit newdata / data = ... must include the raw grouping columns, and decomposition also requires the response source column(s).

6. Quick verification commands

# Check diagnostics overall status
head -1 results/<run_dir>/40_diagnostics/diagnostics_report.csv

# View posterior summary
head results/<run_dir>/30_post_run/posterior_summary.csv

# Count artefact files
find results/<run_dir> -type f | wc -l

Failure handling

Symptom Likely cause Action
Exit code 2 during validate Config, data, or environment error Read error message; fix config or local setup
Exit code 1 during run Diagnostics overall status is fail, diagnostics publish-gate enforcement failed, or a post-fit artifact write failed Review diagnostics and 00_run_metadata/run_status.yaml if present; the fit completed but the outcome is not publishable
Exit code 2 during run Stan compilation, sampling, config, or environment failure before a completed run result was returned Check the CLI error message, Stan cache, and local setup
Missing 20_model_fit/model.rds Fit did not complete Review runner log for Stan errors
Missing 20_model_fit/deployment_model.rds outputs.save_deployment_model_rds is false, model type / fit method is unsupported, or fit did not complete Check resolved config and confirm either model.type: blm, model.type: pooled with fit.method: mcmc, or hierarchical model.type: re/cre with fit.method: mcmc
Missing 40_diagnostics/ Diagnostics writer failed Check for upstream fit failures; review tryCatch messages

Programmatic API note:

  • DSAMbayes::run_from_yaml() can now return a completed runner_result with outcome: completed_with_artifact_write_fail when the fit succeeded but a later artifact-writing step failed, including TSCV artifact writes.
  • For automation, inspect outcome, postfit_issue, and postfit_message instead of assuming every non-error return is fully successful.
  • Diagnostics publish-gate failures still raise dsambayes_runtime_error, with the same runner_result attached as condition$result.

Interpret Diagnostics

Objective

Read and act on the diagnostics report produced by a DSAMbayes runner execution, understanding which checks matter most and what remediation steps to take.

This is the operational triage guide. For the methodological meaning of the gates themselves, see Stage 4: Computation and Sampler and Stage 5: Model Adequacy.

Prerequisites

  • A completed runner run execution with artefacts under 40_diagnostics/.
  • Familiarity with Diagnostics Gates definitions.

Steps

1. Open the diagnostics report

cat results/<run_dir>/40_diagnostics/diagnostics_report.csv

Each row is one diagnostic check. The key columns are:

Column What to look at
check_id Identifies the specific diagnostic
status pass, warn, fail, or skipped
value The observed metric value
threshold The threshold that was applied
message Human-readable explanation

2. Check the overall status

The overall status follows a simple rule:

  • Any fail → overall fail.
  • Any warn (no fails) → overall warn.
  • All pass → overall pass.

If the overall status is pass, no further action is required for the configured policy mode.

3. Triage failing checks

Focus on fail rows first, then warn rows. Use the check phase to prioritise:

Phase Priority Meaning
P0 Highest Data integrity issues — fix before interpreting model results
P1 High Sampler quality or residual issues — may affect inference reliability

4. Common diagnostics and actions

Design matrix issues (P0)

Check Symptom Action
pre_response_finite fails Non-finite values in response Clean data; remove or impute NA/Inf rows
pre_design_constants_duplicates fails Constant or duplicate columns Remove redundant terms from formula
pre_design_condition_number warns/fails High collinearity Reduce correlated predictors; simplify formula

Sampler quality (P1, MCMC only)

Check Symptom Action
sampler_rhat_max warns/fails Poor convergence Increase fit.mcmc.iter and fit.mcmc.warmup; simplify model
sampler_ess_bulk_min or sampler_ess_tail_min warns/fails Insufficient effective samples Increase iterations; check for multimodality
sampler_divergences fails Divergent transitions Increase fit.mcmc.adapt_delta (e.g. 0.95 → 0.99); consider reparameterisation
sampler_treedepth_frac warns/fails Max treedepth saturation Increase fit.mcmc.max_treedepth
sampler_ebfmi_min warns/fails Low energy diagnostic Indicates difficult posterior geometry; simplify model or increase warmup

Residual behaviour (P1)

Check Symptom Action
resid_ljung_box_p warns/fails Significant residual autocorrelation Add time controls (trend, seasonality, holidays)
resid_acf_max warns/fails High residual ACF at early lags Same as above; check for missing structural components

Boundary and variation checks (P1)

Check Symptom Action
boundary_hit_fraction warns/fails Posterior draws hitting parameter bounds Review boundary specification; widen constraints or remove unnecessary bounds
within_var_ratio warns/fails Low within-group variation (hierarchical) Check group structure; some groups may have insufficient temporal variation

Identifiability gate (P1)

Check Symptom Action
pre_identifiability_baseline_media_corr warns/fails High baseline-media correlation Add controls to separate baseline from media effects; review formula specification

5. Review diagnostic plots

Cross-reference the numeric report with visual diagnostics in 40_diagnostics/:

  • Residual diagnostics plot — check for patterns in residuals over time.
  • Boundary hits plot — identify which parameters are constrained.
  • Latent residual ACF plot — confirm autocorrelation structure.

See Diagnostics Plots for interpretation guidance.

6. Decide on next steps

Overall status Action
pass Proceed to post-run analysis and reporting
warn Review warnings; proceed if acceptable for the use case
fail Remediate failing checks before using model results for decisions

7. Change policy mode if appropriate

If you are in early model development, consider switching to explore mode to relax thresholds:

diagnostics:
  policy_mode: explore

For production or audit runs, use publish (default) or strict.

Compare Runs

Objective

Compare multiple DSAMbayes runner executions and select a candidate model for reporting or decision-making, using predictive scoring and diagnostic summaries.

This page is a late-stage selection aid, not a full workflow. Use it only after the candidate runs are computationally trustworthy enough to compare. In the principled workflow, that means Stage 4 and Stage 5 work has already been done: the sampler is behaving acceptably, and the model is at least adequate enough to remain a candidate. For the surrounding methodology, see Stage 4: Computation and Sampler and Stage 5: Model Adequacy.

Prerequisites

  • Two or more completed runner run executions (MCMC fit method).
  • Artefacts under 50_model_selection/ for each run (LOO summary, ELPD outputs).
  • Familiarity with Diagnostics Gates and Model Selection Plots.
  • Candidate runs that are still eligible after basic diagnostic review. Do not compare obviously broken runs just because they produced LOO outputs.

Steps

1. Collect run directories

Identify the run directories to compare:

results/20260228_083808_blm_synth_kpi_os_hfb01/
results/20260228_084410_blm_synth_kpi_os_hfb01/
results/20260228_084602_blm_synth_kpi_os_hfb01/

2. Compare ELPD scores

The compare_runs() helper ranks runs by expected log predictive density (ELPD):

library(DSAMbayes)
comparison <- compare_runs(
  run_dirs = c(
    "results/20260228_083808_blm_synth_kpi_os_hfb01",
    "results/20260228_084410_blm_synth_kpi_os_hfb01"
  )
)
print(comparison)

The output ranks runs by ELPD (higher is better) and reports Pareto-k diagnostics. When TSCV summaries are present, the table also carries tscv_method, tscv_horizon_weeks, tscv_stride_weeks, tscv_min_train_weeks, and tscv_gap_weeks so you can see whether holdout policies actually match. When 00_run_metadata/artifact_schema.yaml is present, the table also carries artifact_schema_version. Treat this as ranking among plausible candidates, not as an automatic winner-selection rule.

3. Check Pareto-k reliability

Examine the loo_summary.csv in each run’s 50_model_selection/ folder:

cat results/<run_dir>/50_model_selection/loo_summary.csv

Key metrics:

Metric Interpretation
elpd_loo Expected log predictive density; higher is better
p_loo Effective number of parameters
looic LOO information criterion; lower is better
Pareto-k counts Observations with k > 0.7 indicate unreliable LOO estimates

If many observations have high Pareto-k values, the LOO approximation is unreliable for that run. Consider time-series cross-validation as an alternative.

4. Review time-series CV (if available)

If diagnostics.time_series_selection.enabled: true was configured, check:

cat results/<run_dir>/50_model_selection/tscv_summary.csv

This provides blocked-CV or leave-future-out scores (holdout ELPD, RMSE, SMAPE), optionally with an embargo gap when gap_weeks is configured, and is usually more appropriate for time-series data than standard LOO.

compare_runs() warns if candidate runs used different TSCV policies. When that happens, rank_tscv and delta_tscv_elpd are left NA. Treat those fields as comparable only when method, horizon_weeks, stride_weeks, min_train_weeks, and gap_weeks match.

compare_runs() also warns when explicit artifact_schema_version values differ across runs. That warning does not block ranking, but it means the helper is reading known artifacts on a best-effort basis across evolving run contracts.

Time-series selection is advisory in the current runner contract. It is useful for model comparison, but it does not change publish-gate status.

5. Cross-reference diagnostics

For each candidate run, check the diagnostics overall status:

head -1 results/<run_dir>/40_diagnostics/diagnostics_report.csv

A model with better ELPD but failing diagnostics should not be preferred over a model with slightly lower ELPD and passing diagnostics.

If a run is computationally untrustworthy, remove it from contention before you start arguing about small predictive-score differences.

6. Compare fit quality visually

Review the fit time series and scatter plots in 20_model_fit/ for each run:

  • Fit time series — does the model track the observed KPI?
  • Fit scatter — is the predicted-vs-observed relationship close to the diagonal?
  • Posterior forest — are coefficient estimates reasonable and well-identified?

7. Selection decision matrix

Criterion Weight Run A Run B
Eligible after diagnostics review? Gate yes/no yes/no
ELPD (higher is better) High value value
Pareto-k reliability (fewer high-k) High value value
Diagnostics overall status High pass/warn/fail pass/warn/fail
TSCV holdout RMSE (if available) Medium value value
Coefficient plausibility Medium judgement judgement
Fit visual quality Low judgement judgement

Use the matrix in order:

  1. Remove runs that are not computationally trustworthy enough to compare.
  2. Rank the remaining candidates by predictive evidence.
  3. Prefer the run whose coefficients, decomposition, and fit behaviour remain most defensible for the business question.

8. Record the selection

Document the selected run directory and rationale. If using the runner for release evidence, the selected run’s artefacts form part of the evidence pack.

Caveats

  • ELPD is not causal validation. Predictive scoring measures in-sample predictive quality, not whether the model identifies causal media effects correctly.
  • ELPD is not a substitute for adequacy. Stronger predictive ranking does not rescue a run that is diagnostically broken or substantively implausible.
  • Pooled models do not support time-series CV (rejected by config validation).
  • Adstock/Hill media transforms are not supported by time-series CV; lower-level scoring aborts if transformed-media paths are used.
  • MAP-fitted models do not produce LOO diagnostics. Use MCMC for model comparison.

Debug Run Failures

Objective

Diagnose and resolve the most common failure modes encountered when running DSAMbayes via the YAML/CLI runner.

Prerequisites

  • A failed runner execution (non-zero exit code or missing artefacts).
  • Access to the terminal output or log from the failed run.
  • Familiarity with CLI Usage and Config Schema.

Triage by failure stage

Stage 0: Config resolution failures

Symptoms: runner exits immediately after validate or at the start of run; no run directory created or only 00_run_metadata/ is present.

Error pattern Cause Fix
data_path not found Data file path is wrong or missing Check data.path in YAML; use absolute path or path relative to config file
Unknown YAML key Typo or unsupported config key Compare against Config Schema; fix spelling
Compiled formula error Invalid generated model formula Check target, media, controls, and hierarchy for unsupported or missing terms
effects.holidays.path not found Holiday calendar file missing Check effects.holidays.path; ensure file exists

Quick check:

Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml

If validate passes, the config is structurally valid.

Stage 1: Stan compilation failures

Symptoms: runner reports compilation errors after “Compiling model”; may reference C++ or Stan syntax errors.

Error pattern Cause Fix
Stan syntax error in generated template Template rendering issue Clear the Stan cache (rm -rf .cache/dsambayes/) and retry
C++ compiler not found Toolchain not installed Install a C++ toolchain (see Install and Setup)
Permission denied on cache directory Cache path not writable Set XDG_CACHE_HOME to a writable directory

Quick check:

mkdir -p .cache
export XDG_CACHE_HOME="$PWD/.cache"

Stage 2: Data preparation failures

Symptoms: runner fails after compilation but before sampling; error messages reference prep_data_for_fit, model.frame, or scaling.

Error pattern Cause Fix
“Cannot scale model data with zero variance” A column in the model frame is constant Remove constant terms from formula, or set model.scale: false
“Constant CRE mean terms” CRE variable has identical group means Use model.type: re (without CRE) or add variation
“non-finite values” in model frame NA or Inf values in data Clean data before running; remove rows with missing values
“Offset vector length does not match” NA handling created length mismatch Ensure offset column has no NA values, or report as a bug

Stage 3: Sampling failures

Symptoms: runner fails during rstan::sampling() or rstan::optimizing(); may report Stan runtime errors.

Error pattern Cause Fix
“Exception: validate transformed params” Parameter hits boundary during sampling Widen boundaries; check for overly tight constraints
“Initialization failed” Poor initial values Increase fit.mcmc.init range or simplify model
Timeout or very slow sampling Model too complex for data size Reduce iterations for initial testing; simplify formula
All chains fail Severe model misspecification Review formula, priors, and data for fundamental issues

Stage 4: Post-fit artefact failures

Symptoms: runner completes sampling but some artefact folders are empty or missing files.

Error pattern Cause Fix
00_run_metadata/run_status.yaml shows completed_with_artifact_write_fail Fit completed but a later artifact write failed Inspect run_status.yaml if present for the message, keep the fitted run directory, then fix the file-system or payload issue and rerun artifact generation
00_run_metadata/run_status.yaml shows completed_with_publish_gate_fail Fit completed but diagnostics publish-gate enforcement rejected the run Review diagnostics outputs before treating the run as publishable
Missing decomposition files under 30_post_run/ Decomposition failed or was skipped Check formula compatibility with model.matrix(), confirm the fitted model retained original data, and inspect 40_diagnostics/artifact_status.csv for response_decomposition skip details
Missing 40_diagnostics/ files Diagnostics writer error Check for upstream issues in model object; review tryCatch messages in log
Missing 50_model_selection/ files LOO computation failed Ensure MCMC fit (not MAP); check for valid posterior
Missing 60_optimisation/ files Allocation not enabled or failed Check allocation.enabled: true in config; review scenario specification

Quick check:

find results/<run_dir> -type f | sort

Compare against the expected artefact list in Output Artefacts.

Stage 5: Plot generation failures

Symptoms: CSV artefacts are present but PNG plot files are missing.

Error pattern Cause Fix
“cannot open connection” for PNG Graphics device issue Check that grDevices is available; ensure sufficient disk space
Plot function error for hierarchical model Group-level coefficient draws are vectors, not scalars This has been fixed in recent releases; ensure you are running the latest version

General debugging steps

  1. Read the full error message. DSAMbayes uses cli::cli_abort() with descriptive messages that identify the failing function and parameter.

  2. Check the terminal run status first. If a run directory was created, inspect 00_run_metadata/run_status.yaml if present to see whether the run ended as fit_failed, completed, completed_with_publish_gate_fail, or completed_with_artifact_write_fail.

  3. Check the resolved and compiled configs. Inspect 00_run_metadata/config.resolved.yaml to see what defaults were applied and 00_run_metadata/config.compiled.yaml to see the internal runner config that was actually passed downstream.

  4. Check session info. Inspect 00_run_metadata/session_info.txt for package version mismatches.

  5. Clear the Stan cache. Stale compiled models can cause unexpected failures:

    rm -rf .cache/dsambayes/
  6. Run validate before run. Always validate first to catch config errors before committing to a full MCMC run.

  7. Reduce iterations for debugging. Use a small fit.mcmc block or switch temporarily to fit.method: optimise to iterate quickly on schema and data issues.

Appendices

Purpose

Provide reference material that supports core user guidance without duplicating operational instructions.

Audience

  • Readers needing precise terminology definitions
  • Engineers orienting themselves in R/ source modules
  • Reviewers validating implementation traceability

Pages

Usage rules

  1. Use appendices as reference pages, not primary process documentation.
  2. Keep operational runbooks in docs/getting-started/, docs/runner/, and docs/internal/.
  3. Prefer links to authoritative sources instead of duplicating constraints or commands.

Subsections of Appendices

Glossary

Purpose

Define canonical terms used across DSAMbayes modelling, runner, diagnostics, and release documentation.

How to use this glossary

  1. Use these definitions when writing or reviewing DSAMbayes documentation.
  2. Keep term usage consistent across docs/modelling/, docs/runner/, and docs/internal/.
  3. If a term changes behaviour in code, update this page in the same change.

Terms

Term Definition Primary location
adstock Media carry-over transform that spreads spend effect over subsequent periods. Stan media-transform templates and modelling docs
allocation Post-fit budget optimisation stage (allocation.* in YAML). docs/runner/config-schema.md
artefact File written by runner validate/run workflows. docs/runner/output-artifacts.md
baseline term Non-media explanatory term (for example trend, seasonality, holiday controls). docs/modelling/diagnostics-gates.md
blm Base DSAMbayes model class for non-pooled regression workflows. R/blm.R
blocked CV Expanding-window time-series cross-validation used for model selection. R/time_series_cv.R
boundary Lower and upper constraints on model parameters. docs/modelling/priors-and-boundaries.md
chain diagnostics MCMC quality diagnostics such as Rhat, ESS, and divergence indicators. R/diagnostics.R
config resolution Process of applying defaults, coercions, path normalisation, and validation to YAML. R/run_config_*.R
CRE Correlated random effects approach using Mundlak-style within and between variation terms. R/cre_mundlak.R
decomp Post-fit decomposition of formula-term contributions. Unavailable for probabilistic adstock/Hill media-transform models until a posterior-aware transformed-response method exists. R/decomp.R
diagnostics gate Thresholded pass/warn/fail policy checks over model diagnostics. R/diagnostics_report.R
divergence Stan sampler warning indicating problematic Hamiltonian trajectories. MCMC diagnostics outputs
dry run Runner mode that validates config and data without Stan fitting (validate). scripts/dsambayes.R
ELPD Expected log predictive density, used for predictive model comparison. R/compare_runs.R
ESS Effective sample size for MCMC draws. Higher is generally better. Chain diagnostics outputs
fit MCMC fitting path (fit.method: mcmc). R/run_from_yaml.R
fit_map / optimisation MAP optimisation path (fit.method: optimise). R/blm.R, R/hierarchy.R
hierarchical Model class with grouped random effects (`(term group)` syntax).
identifiability check Diagnostic check for baseline and media term correlation risk. R/diagnostics_report.R
kpi scale Business-outcome scale used for reporting. For log-response models this is back-transformed from model scale. docs/modelling/response-scale-semantics.md
lognormal_ms Positive-support prior family parameterised by mean and standard deviation on the original scale. R/prior_schema.R
MAP Maximum a posteriori point estimate from optimisation. Not a posterior mean. fit_map paths
MCMC Markov chain Monte Carlo posterior sampling. rstan::sampling paths
Pareto-k PSIS-LOO reliability diagnostic for influence of observations. loo_summary.csv outputs
pooled Model class with structured pooling over configured grouping variables. R/pooled.R
posterior draw One sampled value from the posterior distribution. get_posterior() outputs
pre-flight checks Guardrails and model/data compatibility checks run before fitting. R/pre_flight.R
prior_only Fit mode sampling only from priors, excluding likelihood learning. prep_data_for_fit.* interfaces
PSIS-LOO Pareto-smoothed importance-sampling leave-one-out approximation. R/diagnostics_report.R
QG-1 to QG-7 Canonical release quality gates for lint, style, tests, package check, runner smoke, and docs build. docs/internal/quality-gates.md
response scale Scale used inside the fitted model (identity or log). docs/modelling/response-scale-semantics.md
Rhat Convergence diagnostic comparing within- and between-chain variance. Chain diagnostics outputs
runner YAML/CLI execution layer around core DSAMbayes APIs. scripts/dsambayes.R, R/run_from_yaml.R
run_dir Output directory used by a runner validate/run execution. docs/runner/output-artifacts.md
staged layout Structured artefact layout with numbered folders (00_ to 70_). docs/runner/output-artifacts.md
Stan cache Compiled model cache location, typically under XDG_CACHE_HOME. install/setup docs
SMAPE Symmetric mean absolute percentage error metric used in fit summaries. R/stats.R
time-components Managed time control features, including holiday-derived regressors. R/holiday_calendar.R
tscv Time-series selection artefact prefix for blocked CV outputs. 50_model_selection/tscv_*.csv
warmup Initial MCMC iterations used for adaptation and excluded from posterior draws. fit.mcmc.warmup
Hill transform Saturation function spend^n / (spend^n + k^n) used in budget optimisation response curves. k is the half-saturation point, n is the shape parameter. R/optimise_budget.R
atan transform Saturation function atan(spend / scale) mapping spend to a bounded response. R/optimise_budget.R
log1p transform Saturation function log(1 + spend / scale) providing diminishing-returns concavity. R/optimise_budget.R
adstock Media carry-over transform that spreads a spend effect over subsequent periods via geometric decay. Applied as a pre-transform in the data, not estimated within DSAMbayes. DSAMbayes does not add automatic warm-up or synthetic pre-history, so carry-over starts from the observed window (and resets at hierarchical panel boundaries). Formula transforms
conditional mean Bias-corrected back-transform for log-response models: exp(mu + sigma^2/2). Default in v1.2.2 for fitted_kpi(). R/fitted.R
Jensen's inequality Mathematical property that E[exp(X)] != exp(E[X]) when X has non-zero variance. DSAMbayes avoids this bias by applying exp() draw-wise before summarising. docs/modelling/response-scale-semantics.md

R Module Index

Purpose

Provide a maintainable orientation map for R/ while preserving the flat R package layout.

Layout rule

DSAMbayes keeps a flat R/ directory. Modules are logical groupings, not folder boundaries.

Module map

Module Responsibility Primary files
Model objects and fit engines Class constructors, fit and MAP pathways, class-specific data preparation R/blm.R, R/hierarchy.R, R/pooled.R, R/model_schema.R, R/blm_compiled.R
Stan media transform support Media-transform config parsing and Stan data wiring R/media_transform_config.R
Priors, boundaries, scaling, transforms Prior parsing, boundary handling, scaling contracts, transform helpers, offsets R/prior.R, R/prior_schema.R, R/scale.R, R/scale_prior_sd.R, R/transformations.R, R/transform_sensitivity.R, R/offset.R
Formula and pre-flight validation Formula parsing/safety, data/date checks, pre-fit guardrails R/formula.R, R/formula_safety.R, R/pre_flight.R, R/date.R, R/variable.R
Diagnostics and model selection Fit diagnostics, gate logic, cross-validation, posterior predictive and metrics R/diagnostics.R, R/diagnostics_report.R, R/crossval.R, R/time_series_cv.R, R/post_pred.R, R/stats.R, R/compare_runs.R
Decomposition and extraction Decomposition APIs and extraction helpers R/decomp.R, R/decomp_prep.R, R/extract.R, R/fitted.R
Runner config and orchestration YAML defaults/coercion/validation, model orchestration, run execution R/run_config.R, R/run_config_helpers.R, R/run_config_defaults.R, R/run_config_validation.R, R/run_orchestrator.R, R/run_from_yaml.R
Runner artefacts and reporting Stage mapping and artefact writers for metadata, diagnostics, enrichment R/run_artifacts.R, R/run_artifacts_diagnostics.R, R/run_artifacts_enrichment.R, R/runner_fit_plots.R
Time components and CRE support Holiday feature engineering and CRE/Mundlak model support R/holiday_calendar.R, R/cre_mundlak.R
Budget optimisation and visual outputs Decision-layer optimisation engine and plotting APIs R/optimise_budget.R, R/optimise_budget_plots.R, R/plot_theme_wpp.R
Package infrastructure and utilities Package lifecycle hooks, utility helpers, package metadata helpers, bundled ASCII art R/zzz.R, R/utils.R, R/utils-pipe.R, R/sitrep_.R, R/data.R, R/ascii.txt

High-coupling files to review carefully

File Why it is high-coupling
R/run_from_yaml.R Central runner execution path connecting config, model build, fit, diagnostics, and artefact writing.
R/run_config_validation.R Cross-field validation rules with direct impact on runner safety and allowed contracts.
R/run_artifacts.R Artefact pathing and stage contract used by docs, local runner workflows, and release evidence capture.
R/hierarchy.R Large class-specific fit, posterior, and scaling logic with multiple behavioural branches.
R/blm.R Base class implementation used across many tests and runner pathways.

Placement guide for new code

  1. Put new logic in the closest logical module listed above.
  2. If touching legacy compatibility files (R/run_config.R, R/run_artifacts.R), prefer adding new logic to their split companion files where possible.
  3. Keep exported function names stable unless there is an explicit API migration plan.
  4. Add or update test coverage in tests/testthat/ for any behavioural change.

Traceability Map

Purpose

Provide a traceability reference that maps DSAMbayes issues and recommendations to implementation status and evidence.

Authoritative data source

The single source of truth for all issue and recommendation status is:

  • code_review/audit_report/issue_register.csv

This register contains every ENG, INF, and GOV issue and recommendation with columns for status, severity, owner, linked IDs, notes, and a long-form explanation field.

Two stakeholder-facing summary CSVs are published alongside this page under docs/appendices/traceability-data/:

These files are derived snapshots. Where they conflict with issue_register.csv, the register takes precedence.

Snapshot metrics (as of 2026-02-28)

Non-GOV issues and recommendations

  • All ENG and INF issues: closed (including backlog items accepted and closed 2026-02-28)
  • All ENG and INF recommendations: closed / implemented

GOV issues

  • GOV-ISSUE-001 through GOV-ISSUE-004: open (governance items; resolution deferred to management review cycle)

How to use this map

  1. Start with an issue ID or recommendation ID.
  2. Open code_review/audit_report/issue_register.csv.
  3. Confirm status in the status column and review linked IDs in linked_ids.
  4. Follow evidence paths referenced in the notes column to code files, tests, and review records.
  5. Use internal engineering documentation for release decision criteria.

Representative mappings

ID Type Status Evidence anchors
ENG-ISSUE-014 Issue closed R/scale.R, tests/testthat/test-scale-guardrails.R
INF-ISSUE-006 Issue closed R/diagnostics_report.R, tests/testthat/test-diagnostics-report.R
INF-REC-004 Recommendation closed Holiday/time-components implementation and review references
ENG-REC-003 Recommendation closed Pooled RMSE and scale wiring closure references

Traceability and release approval

Traceability status alone does not authorise a release. Release sign-off still requires:

  1. Mandatory quality gates passed.
  2. Required release evidence bundle complete.
  3. Final decision recorded in sign-off template.

Docs Build and Deploy

Purpose

Describe how the DSAMbayes documentation site is built, previewed, and deployed.

Documentation layers

DSAMbayes currently has two documentation layers with distinct purposes:

1. Package reference inputs

Package reference content is generated from:

  • roxygen comments in R/
  • vignettes in vignettes/
  • generated man/*.Rd files

This material supports package help pages and package-check workflows. It is not the deployed public docs site.

2. Public documentation site

The public docs site is built from hand-authored Markdown under docs/ plus the Hugo/Relearn wrapper under docs-site/.

Build locally:

python3 docs-site/build_content.py
(cd docs-site && hugo --cleanDestinationDir)

Build flow:

  1. docs/ is the source of truth.
  2. docs-site/build_content.py mirrors and normalizes content into docs-site/content/.
  3. Hugo renders the final site into docs-site/public/ and cleans removed pages from prior builds.

There is no canonical automated deployment pipeline for the public docs site. Build and publish manually if you choose to host updated docs.

Preview locally:

  • open Markdown files directly for quick edits, or
  • serve/build the Hugo site for full navigation and theme rendering

If you maintain an external published mirror such as https://dsambayes.docs.wppma.space/, treat it as a manual distribution channel that may lag the repository. Verify freshness before linking to it in release communication.

Configuration

docs/docs-config.json defines:

  • metadata — site name, description, version.
  • branding — logo, favicon, primary colour.
  • navigation — navbar links and sidebar structure.
  • features — math rendering (enabled), search (local).

Adding a new page

  1. Create the Markdown file in the appropriate section directory (e.g. docs/modelling/new-page.md).
  2. Add a sidebar entry in docs/docs-config.json under the appropriate section.
  3. Add a row to the section’s index.md page table.
  4. Update docs/_plan/content-map.md if tracking authoring status.

Internal (Engineering)

Purpose

Define manual quality gates and release-readiness checks for DSAMbayes. The normal cadence is quarterly; these pages deliberately do not prescribe CI/CD.

Audience

  • Maintainers preparing and validating releases.
  • Reviewers checking evidence before sign-off.

Pages

Page Topic
Testing and Validation Quality-gate execution commands, expected outcomes, and evidence capture
Quality Gates Gate definitions and pass/fail criteria
Runner Smoke Tests Minimal runner validation runs
Repository Size Cleanup Local repository-size findings and cleanup plan
Release Readiness Checklist Gate checklist and sign-off fields
Release Evidence Pack Artefact collection for stakeholder review
Release Playbook Manual, evidence-led quarterly release process
Sign-off Template Release sign-off record template

Subsections of Internal (Engineering)

Quality Gates

Purpose

Define the canonical release-quality gates for DSAMbayes v1.3.3, including commands, pass/fail criteria, and evidence requirements.

Audience

  • Maintainers preparing a release candidate
  • Reviewers signing off release readiness
  • Engineers running local pre-merge quality checks

Gate Matrix

Gate ID Gate Command Pass Criteria Evidence
QG-1 Lint Rscript scripts/check.R --lint Exit code 0, no lint failures, no SKIP: output Terminal log and exit code
QG-2 Style Rscript scripts/check.R --style Exit code 0, no style failures, no SKIP: output Terminal log and exit code
QG-3 Unit tests Rscript scripts/check.R --test Exit code 0, no test failures Terminal log and exit code
QG-4 Stan release evidence Rscript scripts/check.R --stan-release-evidence Exit code 0, high-budget pooled and hierarchical checks pass diagnostic thresholds Terminal log and exit code
QG-5 Package check _R_CHECK_FORCE_SUGGESTS_=false R -q -e 'rcmdcheck::rcmdcheck(args = c("--no-manual"), error_on = "warning")' No ERROR. No unresolved WARNING for release sign-off. Any NOTE requires explicit reviewer acceptance rcmdcheck summary and reviewer decision on NOTEs
QG-6 Runner smoke: validate Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml --run-dir results/quality_gate_validate Exit code 0 CLI log and results/quality_gate_validate/00_run_metadata/config.compiled.yaml
QG-7 Runner smoke: run Rscript scripts/dsambayes.R run --config config/blm_timeseries.yaml --run-dir results/quality_gate_run Exit code 0 and core artefacts exist CLI log and selected artefacts under results/quality_gate_run/
QG-8 Docs build check python3 docs-site/build_content.py && (cd docs-site && hugo --cleanDestinationDir) Exit code 0 Build log and generated site output under docs-site/public/

Gate Definitions

QG-1 Lint

Command:

Rscript scripts/check.R --lint

Fail conditions:

  • Non-zero exit code
  • Any lint issue reported
  • Any SKIP: output

QG-2 Style

Command:

Rscript scripts/check.R --style

Fail conditions:

  • Non-zero exit code
  • Any file reported as requiring reformat
  • Any SKIP: output

QG-3 Unit Tests

Command:

Rscript scripts/check.R --test

Fail conditions:

  • Non-zero exit code
  • Any test failure or error

QG-4 Stan Release Evidence

Command:

Rscript scripts/check.R --stan-release-evidence

Fail conditions:

  • Non-zero exit code
  • Any high-budget Stan evidence test failure
  • Any unresolved diagnostic-threshold failure

QG-5 Package Check

Command:

_R_CHECK_FORCE_SUGGESTS_=false \
  R -q -e 'rcmdcheck::rcmdcheck(args = c("--no-manual"), error_on = "warning")'

Fail conditions:

  • Any ERROR
  • Any WARNING for release sign-off

Escalation condition:

  • Any NOTE must be reviewed and explicitly accepted with rationale.

Operational note:

  • Local package checks do not force Suggests because DSAMdecomp is an optional decomposition-only dependency and the upstream available package is telemetry-enabled. Decomposition-specific checks should be run separately against an approved telemetry-free DSAMdecomp install.

QG-6 Runner Smoke: Validate

Command:

Rscript scripts/dsambayes.R validate \
    --config config/blm_timeseries.yaml \
    --run-dir results/quality_gate_validate

Fail conditions:

  • Non-zero exit code
  • Missing results/quality_gate_validate/00_run_metadata/config.resolved.yaml
  • Missing results/quality_gate_validate/00_run_metadata/config.compiled.yaml

QG-7 Runner Smoke: Run

Command:

Rscript scripts/dsambayes.R run \
    --config config/blm_timeseries.yaml \
    --run-dir results/quality_gate_run

Fail conditions:

  • Non-zero exit code
  • Missing results/quality_gate_run/00_run_metadata/config.resolved.yaml
  • Missing results/quality_gate_run/00_run_metadata/config.compiled.yaml
  • Missing results/quality_gate_run/20_model_fit/model.rds
  • Missing results/quality_gate_run/30_post_run/fitted.csv
  • Missing results/quality_gate_run/30_post_run/observed.csv
  • Missing results/quality_gate_run/40_diagnostics/diagnostics_report.csv

QG-8 Docs Build Check

Command:

python3 docs-site/build_content.py
(cd docs-site && hugo --cleanDestinationDir)

Fail conditions:

  • Non-zero exit code
  • docs-site/build_content.py fails before content mirroring completes
  • Hugo build aborts before site generation
  • Missing docs-site/public/index.html

Command Reference

Recommended environment setup before running gates:

# Navigate to your local DSAMbayes checkout and select the host library
cd /path/to/DSAMbayes
source scripts/r-library-path.sh
dsambayes_set_r_library host
mkdir -p "$R_LIBS_USER" .cache
export XDG_CACHE_HOME="$PWD/.cache"
export _R_CHECK_FORCE_SUGGESTS_=false

Optional consolidated local gate (does not replace all release gates):

Rscript scripts/check.R --docs
Rscript scripts/check.R --smoke
Rscript scripts/check.R --stan-recovery
Rscript scripts/check.R --stan-release-evidence
Rscript scripts/check.R --release
Rscript scripts/check.R --all

Profile meanings:

  • --docs runs the local docs/link sanity helper against README and the tracked docs surfaces.
  • --smoke runs the fast unit path plus the minimal Stan-backed smoke subset.
  • --stan-recovery runs the full-gated DGP coefficient-recovery file only, intended for targeted numerical evidence refreshes.
  • --stan-release-evidence runs high-budget pooled and hierarchical Stan checks with diagnostic thresholds for release-candidate evidence.
  • --release runs lint, style, unit tests, minimal Stan smoke, docs sanity, and coverage.
  • --all remains the legacy lint + style + tests + coverage profile.

None of these profiles replaces rcmdcheck, runner smoke checks, or the docs-site build.

Evidence Requirements

Minimum evidence bundle per release candidate:

  1. Full terminal output and exit code for each gate QG-1 to QG-8.
  2. Runner smoke artefacts under results/quality_gate_validate/.
  3. Runner smoke artefacts under results/quality_gate_run/.
  4. rcmdcheck summary with explicit handling of NOTEs.
  5. Confirmation that no gate result is SKIP.

Evidence review should reference:

Failure and Escalation Rules

  1. Any gate failure blocks release tagging.
  2. Do not proceed to sign-off with unresolved ERROR or WARNING.
  3. NOTEs require written rationale and reviewer acceptance.
  4. If a gate fails due to environment setup, fix the environment and re-run the full affected gate. DSAMdecomp absence alone is not a blocker for QG-4 because the canonical package check flow treats it as an optional Suggests dependency.
  5. If a gate fails due to product code, raise a remediation change and re-run from QG-1.

Sign-off Criteria

Release sign-off requires all of the following:

  1. QG-1 to QG-8 passed.
  2. No SKIP outcomes across mandatory gates.
  3. Evidence bundle completed and reviewed.
  4. Final decision recorded in sign-off-template.md.

Testing and Validation

Purpose

Define the canonical testing and validation workflow for DSAMbayes v1.3.3, from local pre-merge checks through release-quality gates.

Audience

  • Engineers running local checks before merge
  • Maintainers preparing release candidates
  • Reviewers validating release evidence

Validation layers

Layer Objective Primary command(s) Output proof
Lint Catch style and static issues early Rscript scripts/check.R --lint Exit code 0, no lint failures
Style Enforce formatting compliance on changed files Rscript scripts/check.R --style Exit code 0, no reformat-required files
Unit tests Catch behavioural regressions in package logic Rscript scripts/check.R --test Exit code 0, no test failures
Minimal smoke Keep a cheap Stan-backed safety check in the routine local loop Rscript scripts/check.R --smoke Exit code 0, fast unit tests plus minimal Stan smoke pass
Stan smoke Exercise the broader compiled Stan suite beyond the minimal smoke tier Rscript scripts/check.R --stan-smoke Exit code 0, Stan smoke tests enabled
Stan recovery evidence Re-run the full coefficient-recovery file under the nightly Stan gate Rscript scripts/check.R --stan-recovery Exit code 0, full-Stan DGP recovery tests pass
Stan release evidence Re-run high-budget pooled and hierarchical evidence checks for release candidates Rscript scripts/check.R --stan-release-evidence Exit code 0, targeted high-budget Stan evidence passes diagnostic thresholds
Docs sanity Catch local docs-link breakage and high-value contract drift Rscript scripts/check.R --docs Exit code 0, docs sanity checks pass
Package check Validate package-level install and check behaviour R -q -e 'rcmdcheck::rcmdcheck(...)' No ERROR; no unresolved WARNING
Runner validate Validate config and data contracts without fitting Rscript scripts/dsambayes.R validate ... Exit code 0, metadata artefacts
Runner run Validate end-to-end runner execution and artefacts Rscript scripts/dsambayes.R run ... Exit code 0, core run artefacts
Docs build Validate docs-site/Hugo buildability python3 docs-site/build_content.py && (cd docs-site && hugo --cleanDestinationDir) Exit code 0, successful site build

Environment setup

Run all commands from repository root:

# Navigate to your local DSAMbayes checkout
cd /path/to/DSAMbayes
source scripts/r-library-path.sh
dsambayes_set_r_library host
mkdir -p "$R_LIBS_USER" .cache
export XDG_CACHE_HOME="$PWD/.cache"

Expected outcome: checks run in a repo-scoped environment with reproducible library and cache paths.

Dependency source portability

Before a release candidate is signed off, verify all non-CRAN dependency sources in renv.lock and DESCRIPTION.

The preferred release path is a pinned private GitHub remote for optional decomposition dependencies. A local fallback may restore those dependencies from sibling checkouts:

/home/user/Documents/GITHUB/tandpds/DSAMdecomp
/home/user/Documents/GITHUB/tandpds/teller

If any entries remain as file:// sources in renv.lock, release evidence must record the required local paths and pinned commit hashes. For an external release or clone-portable handoff, prefer reachable pinned remotes before sign-off.

When validating either dependency source path, use telemetry-disabled restores:

export GITHUB_PAT="<token with access to private tandpds repos>"
export DSAMDECOMP_DISABLE_TELEMETRY=1
export DSAMBAYES_DISABLE_TELEMETRY=1
export USE_BUNDLED_LIBUV=1
XDG_CACHE_HOME="$PWD/.cache" \
  Rscript -e 'renv::restore(packages = c("teller", "DSAMdecomp"), prompt = FALSE)'

Expected outcome: teller and DSAMdecomp restore and load from the recorded sources. If private GitHub authentication is unavailable, or a local fallback path is missing, hold the release until dependency sources are reproducible in the release environment.

Local validation workflows

Developer fast path (pre-merge)

Use the explicit local ladder:

Rscript scripts/check.R --test
Rscript scripts/check.R --smoke
Rscript scripts/check.R --docs
Rscript scripts/check.R --release

Expected outcomes:

  • --test stays fast and runs the unit path only.
  • --smoke runs the unit path plus a minimal Stan-backed subset: one cheap BLM MCMC smoke, one pooled deployment-artifact smoke on a real pooled fit, and tiny run_from_yaml() runner smokes including pooled deployment_model.rds roundtrip coverage.
  • --docs runs the local docs/link sanity helper against README and the tracked docs surfaces.
  • --release runs lint, style, unit tests, minimal Stan smoke, docs sanity, and coverage as a broader local code gate.

Implementation note:

  • scripts/check.R --all remains a legacy convenience gate for lint, style, tests, and coverage only.
  • scripts/check.R --docs is the explicit docs/link drift check.
  • scripts/check.R --release is the clearer code-focused local release profile.
  • Neither profile replaces rcmdcheck, runner smoke checks, or docs build.

Stan-specific note:

  • use Rscript scripts/check.R --stan-smoke for the broader opt-in compiled Stan suite
  • use --stan-smoke-full for the fuller nightly variant
  • use --stan-recovery when you need a targeted rerun of the DGP recovery evidence without invoking the rest of the full Stan suite
  • use --stan-release-evidence for the high-budget release-candidate lane covering the warning-prone pooled and hierarchical paths

Release-candidate full path

Run mandatory gates in this exact order:

Rscript scripts/check.R --lint
Rscript scripts/check.R --style
Rscript scripts/check.R --smoke
Rscript scripts/check.R --stan-release-evidence
_R_CHECK_FORCE_SUGGESTS_=false \
  R -q -e 'rcmdcheck::rcmdcheck(args = c("--no-manual"), error_on = "warning")'
Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml --run-dir results/quality_gate_validate
Rscript scripts/dsambayes.R run --config config/blm_timeseries.yaml --run-dir results/quality_gate_run
python3 docs-site/build_content.py
(cd docs-site && hugo --cleanDestinationDir)

Expected outcome: all gates complete with exit code 0, with no unresolved release blockers.

Local package-check note:

  • DSAMdecomp remains an optional Suggests dependency. Canonical local rcmdcheck runs set _R_CHECK_FORCE_SUGGESTS_=false so release gates do not depend on installing a telemetry-enabled decomposition package.

Runner smoke-test expectations

Minimum release smoke expectations:

  1. scripts/check.R --smoke succeeds, proving at least one cheap Stan MCMC path, pooled deployment-artifact coverage on a real pooled fit, and tiny runner fit paths.
  2. validate command succeeds and writes metadata artefacts.
  3. run command succeeds and writes model, fitted/observed output, and diagnostics artefacts.
  4. Required runner artefact paths exist under results/quality_gate_validate/ and results/quality_gate_run/.

For matrix and exact artefact paths, use Runner Smoke Tests.

Evidence capture requirements

Before sign-off, capture:

  1. Full command logs and exit codes for all mandatory gates.
  2. Runner smoke artefacts from validate and run directories.
  3. Candidate commit hash and top changelog section.

Use Release Evidence Pack as the authoritative bundle contract.

Failure handling

  1. Any gate failure is a release blocker until resolved.
  2. Re-run the full failed gate after remediation.
  3. If rcmdcheck emits NOTE, record reviewer rationale explicitly.
  4. If runner artefacts are missing, inspect resolved config and outputs.* flags.

Runner Smoke Tests

Purpose

Define the minimal reproducible smoke-test matrix for the YAML runner validate and run commands, including exact commands and expected artefacts.

Audience

  • Maintainers preparing release evidence
  • Engineers triaging runner regressions
  • Reviewers confirming gate QG-5 and QG-6

Test scope

This smoke suite is intentionally small. It proves:

  • CLI argument handling for validate and run
  • Config resolution and runner pre-flight path
  • End-to-end artefact writing for one full run

This smoke suite does not replace unit tests or full package checks.

Preconditions

Run from repository root:

# Navigate to your local DSAMbayes checkout
cd /path/to/DSAMbayes
source scripts/r-library-path.sh
dsambayes_set_r_library host
mkdir -p "$R_LIBS_USER" .cache
export XDG_CACHE_HOME="$PWD/.cache"

Expected outcome: commands resolve local package/library paths and use repo-scoped Stan cache.

Install DSAMbayes locally if needed:

R -q -e 'install.packages(".", repos = NULL, type = "source")'

Expected outcome: library(DSAMbayes) succeeds in the same shell session.

Smoke-test matrix

Test ID Command Config Run directory Expected result
SMK-VAL-01 validate config/blm_timeseries.yaml results/smoke_validate_blm Exit code 0; metadata artefacts written.
SMK-VAL-02 validate config/cre_geo_panel.yaml results/smoke_validate_cre Exit code 0; metadata artefacts written.
SMK-RUN-01 run config/blm_timeseries.yaml results/smoke_run_blm Exit code 0; core fit, post-run, and diagnostics artefacts written.

Canonical commands

SMK-VAL-01

Rscript scripts/dsambayes.R validate \
    --config config/blm_timeseries.yaml \
    --run-dir results/smoke_validate_blm

Expected outcome: validation completes without Stan fitting and prints Status: ok.

SMK-VAL-02

Rscript scripts/dsambayes.R validate \
    --config config/cre_geo_panel.yaml \
    --run-dir results/smoke_validate_cre

Expected outcome: validation completes for the geo-panel CRE config and prints Status: ok.

SMK-RUN-01

Rscript scripts/dsambayes.R run \
    --config config/blm_timeseries.yaml \
    --run-dir results/smoke_run_blm

Expected outcome: full pipeline completes and prints Run complete with a resolved run directory.

Expected artefacts

Validate artefacts (SMK-VAL-01 to SMK-VAL-02)

For each validate run directory, these files are required:

  • 00_run_metadata/config.original.yaml
  • 00_run_metadata/config.resolved.yaml
  • 00_run_metadata/config.compiled.yaml
  • 00_run_metadata/session_info.txt

Failure rule: missing any required file is a smoke-test failure.

Run artefacts (SMK-RUN-01)

Required core artefacts:

  • 00_run_metadata/config.resolved.yaml
  • 00_run_metadata/config.compiled.yaml
  • 20_model_fit/model.rds
  • 30_post_run/fitted.csv
  • 30_post_run/observed.csv
  • 40_diagnostics/diagnostics_report.csv

Recommended additional checks for stronger confidence:

  • 40_diagnostics/diagnostics_summary.txt

Failure rule: missing any required core artefact is a smoke-test failure.

Verification helper commands

Check validate artefacts quickly:

for d in results/smoke_validate_blm results/smoke_validate_cre; do
  test -f "$d/00_run_metadata/config.original.yaml" || echo "MISSING: $d config.original.yaml"
  test -f "$d/00_run_metadata/config.resolved.yaml" || echo "MISSING: $d config.resolved.yaml"
  test -f "$d/00_run_metadata/config.compiled.yaml" || echo "MISSING: $d config.compiled.yaml"
  test -f "$d/00_run_metadata/session_info.txt" || echo "MISSING: $d session_info.txt"
done

Expected outcome: no MISSING: lines.

Check core run artefacts quickly:

d="results/smoke_run_blm"
test -f "$d/00_run_metadata/config.resolved.yaml" || echo "MISSING: config.resolved.yaml"
test -f "$d/00_run_metadata/config.compiled.yaml" || echo "MISSING: config.compiled.yaml"
test -f "$d/20_model_fit/model.rds" || echo "MISSING: model.rds"
test -f "$d/30_post_run/fitted.csv" || echo "MISSING: fitted.csv"
test -f "$d/30_post_run/observed.csv" || echo "MISSING: observed.csv"
test -f "$d/40_diagnostics/diagnostics_report.csv" || echo "MISSING: diagnostics_report.csv"

Expected outcome: no MISSING: lines.

Failure triage

  1. If validate fails, run the same command again with a clean run directory path and inspect CLI error output.
  2. If run fails before fitting, inspect both 00_run_metadata/config.resolved.yaml and 00_run_metadata/config.compiled.yaml to confirm the authored and compiled values.
  3. If run fails during fitting, verify local Stan toolchain and cache path from Install and Setup.
  4. If artefacts are missing after success exit code, inspect outputs.* flags in the resolved config and confirm any compile-time artifacts in config.compiled.yaml.

Evidence capture

For release evidence, capture:

  1. Full terminal logs and exit codes for SMK-VAL-01 to SMK-RUN-01.
  2. Directory listings for each smoke run directory.
  3. The required artefacts listed above.

Store and review evidence with:

Repository Size Cleanup

Current state

Local inspection on 2026-04-04 found:

  • .git size: about 1.4G
  • packed objects: about 1.35 GiB
  • loose objects: 0
  • garbage objects: 0

This means the size is in committed history, not in disposable local Git metadata.

Largest historical contributors found so far:

  • results/: about 635 MiB
  • .Rlib/: about 444 MiB
  • .cache/: about 279 MiB
  • assets/: about 183 MiB
  • code_review/: about 125 MiB

Largest file classes found so far:

  • .rds: about 1.0 GiB
  • .pdf: about 184 MiB
  • .a: about 131 MiB
  • .so: about 118 MiB

The main current tracking gap is code_review/benchmarks/**/cache/, which is still versioned in HEAD.

Goal

Reduce future repository growth immediately, then decide separately whether to rewrite history to shrink existing clones.

Phase 1: Stop future growth

This phase is low risk and should be merged first.

Ignore rules

The repository should ignore:

  • /.Rlib/
  • /.cache/
  • /results/
  • /code_review/benchmarks/**/cache/
  • /assets/books/

/.Rlib/, /.cache/, and /results/ were already ignored before this note. The new additions are:

  • /code_review/benchmarks/**/cache/
  • /assets/books/

Remove cached benchmark artifacts from Git index

This removes tracked cache files from HEAD without deleting the local copies:

git rm -r --cached code_review/benchmarks/parity_blm/cache
git rm -r --cached code_review/benchmarks/parity_hierarchical/cache
git commit -m "chore: stop tracking benchmark caches"

Current tracked files that should be removed from the index:

code_review/benchmarks/parity_blm/cache/main/dsambayes/1.2.0/models/bayes_lm_updater_revised.rds
code_review/benchmarks/parity_blm/cache/refactor/dsambayes/1.2.0/models/bayes_lm_updater_revised_0d26146603.rds
code_review/benchmarks/parity_hierarchical/cache/main/dsambayes/1.2.0/models/hierarchical_1.rds
code_review/benchmarks/parity_hierarchical/cache/main/dsambayes/1.2.0/models/hierarchical_1.stan
code_review/benchmarks/parity_hierarchical/cache/refactor/dsambayes/1.2.0/models/hierarchical_1_centered_offset.stan
code_review/benchmarks/parity_hierarchical/cache/refactor/dsambayes/1.2.0/models/hierarchical_1_centered_offset_665e33077c.rds

Validation after Phase 1

Run:

git status --short
git check-ignore -v --no-index code_review/benchmarks/parity_blm/cache/main/dsambayes/1.2.0/models/example.rds
git check-ignore -v --no-index assets/books/example.pdf

Expected result:

  • benchmark cache files are ignored
  • local cache files remain on disk
  • repository history size does not materially shrink yet

Phase 2: Shrink existing history

This phase is disruptive and should only happen after coordination with every collaborator and any CI jobs or deployment hooks that clone this repository.

Safety rules

  • Freeze merges to main during the rewrite window.
  • Ask every collaborator to stop pushing until the rewrite is complete.
  • Create a backup mirror before modifying history.
  • Use a temporary clone or mirror for the rewrite, not an active working copy.

Suggested backup

cd ..
git clone --mirror DSAMbayes-Charles-Dev DSAMbayes-Charles-Dev.git-backup

Suggested rewrite target set

Remove historical content for paths that are local caches or generated outputs:

  • /.Rlib/**
  • /.cache/**
  • /results/**
  • /code_review/benchmarks/**/cache/**
  • /assets/books/**

Suggested workflow with git filter-repo

Use a fresh mirror clone:

cd ..
git clone --mirror <remote-url> DSAMbayes-Charles-Dev.git-rewrite
cd DSAMbayes-Charles-Dev.git-rewrite
git filter-repo \
  --path-glob '.Rlib/**' \
  --path-glob '.cache/**' \
  --path-glob 'results/**' \
  --path-glob 'code_review/benchmarks/**/cache/**' \
  --path-glob 'assets/books/**' \
  --invert-paths
git reflog expire --expire=now --all
git gc --prune=now --aggressive

Then verify size:

git count-objects -vH
du -sh .

Publish rewritten history

Only after verification:

git push --force --mirror

If branch protections block force-pushes, temporarily adjust them before the rewrite window and restore them immediately after.

Collaborator recovery after rewrite

Every collaborator should re-clone. If someone must salvage local work, they should:

git fetch origin
git switch main
git reset --hard origin/main

Re-cloning is still safer than trying to reuse an old clone after a large rewrite.

Phase 3: Optional future hardening

If large binary artifacts must remain versioned in future, move them to Git LFS. Do not use Git LFS for ephemeral caches, run outputs, or package libraries that should stay untracked.

  1. Merge the .gitignore change.
  2. Remove tracked benchmark cache files from the index with git rm --cached.
  3. Confirm CI and docs are unaffected.
  4. Decide whether the current 1.4G .git size justifies a history rewrite.
  5. If yes, schedule a short maintenance window and perform the rewrite from a mirror clone.

Release Evidence Pack

Purpose

Define the exact evidence bundle required before manual DSAMbayes release sign-off. Substitute the actual release version and candidate SHA; this page is not version-specific.

Audience

  • Release owner preparing sign-off materials
  • Reviewers validating release readiness
  • Maintainers reproducing release gate outcomes

Evidence root and naming

Use one evidence root per candidate release.

Recommended path:

release_evidence/vX.Y.Z/<YYYYMMDD>_<short_sha>/

Example:

release_evidence/v1.3.3/20260714_ab12cd3/

Expected outcome: all sign-off evidence is stored in one deterministic location.

Candidate identity rule:

  1. The candidate hash in 00_release_identity/release_identity.txt, 40_signoff/sign_off_record.md, and the evidence-root short SHA must agree.
  2. If administrative docs or sign-off text change after gate execution, either update the existing bundle without changing candidate identity or rerun the gates into a new evidence root for a new candidate.

Mandatory evidence bundle

All items below are mandatory.

ID Evidence item Required content Source Required path in evidence root
EVD-01 Release identity Candidate commit hash, branch, intended tag, package version git, DESCRIPTION 00_release_identity/release_identity.txt
EVD-02 Changelog proof Top changelog section for release candidate CHANGELOG.md 00_release_identity/changelog_top.md
EVD-03 QG-1 log Lint command output and exit code local command 10_quality_gates/qg1_lint.log, 10_quality_gates/qg1_lint.exit
EVD-04 QG-2 log Style command output and exit code local command 10_quality_gates/qg2_style.log, 10_quality_gates/qg2_style.exit
EVD-05 QG-3 log Unit-test output and exit code local command 10_quality_gates/qg3_tests.log, 10_quality_gates/qg3_tests.exit
EVD-06 QG-4 Stan release evidence log High-budget Stan evidence output and exit code local command 10_quality_gates/qg4_stan_release_evidence.log, 10_quality_gates/qg4_stan_release_evidence.exit
EVD-07 QG-5 package-check log rcmdcheck output, status summary, NOTE rationale if present local command 10_quality_gates/qg5_rcmdcheck.log, 10_quality_gates/qg5_rcmdcheck.exit, 10_quality_gates/qg5_notes_rationale.md
EVD-08 QG-6 validate log Runner validate output and exit code local command 10_quality_gates/qg6_validate.log, 10_quality_gates/qg6_validate.exit
EVD-09 QG-7 run log Runner run output and exit code local command 10_quality_gates/qg7_run.log, 10_quality_gates/qg7_run.exit
EVD-10 QG-8 docs log docs-site build output and exit code local command 10_quality_gates/qg8_docs.log, 10_quality_gates/qg8_docs.exit
EVD-11 Dependency source proof Non-CRAN and file:// dependency source paths, commit hashes, and restore portability decision renv.lock, DESCRIPTION, git 00_release_identity/dependency_sources.md
EVD-12 Validate artefacts Required QG-6 artefacts results/quality_gate_validate 20_runner_artifacts/quality_gate_validate/
EVD-13 Run artefacts Required QG-7 artefacts results/quality_gate_run 20_runner_artifacts/quality_gate_run/
EVD-14 Sign-off record Completed final decision record sign-off template 40_signoff/sign_off_record.md

Exact required artefact paths

EVD-12 validate artefacts (QG-6)

Copy these paths from the run directory:

  • results/quality_gate_validate/00_run_metadata/config.original.yaml
  • results/quality_gate_validate/00_run_metadata/config.resolved.yaml
  • results/quality_gate_validate/00_run_metadata/config.compiled.yaml
  • results/quality_gate_validate/00_run_metadata/session_info.txt

EVD-13 run artefacts (QG-7)

Copy these paths from the run directory:

  • results/quality_gate_run/00_run_metadata/config.resolved.yaml
  • results/quality_gate_run/00_run_metadata/config.compiled.yaml
  • results/quality_gate_run/20_model_fit/model.rds
  • results/quality_gate_run/30_post_run/fitted.csv
  • results/quality_gate_run/30_post_run/observed.csv
  • results/quality_gate_run/40_diagnostics/diagnostics_report.csv

Collection commands

Create evidence structure:

source scripts/r-library-path.sh
dsambayes_set_r_library container
mkdir -p "$R_LIBS_USER"
VERSION="$(awk -F': ' '/^Version:/{print $2}' DESCRIPTION)"
EROOT="release_evidence/v${VERSION}/$(date +%Y%m%d)_$(git rev-parse --short HEAD)"
mkdir -p "$EROOT"/{00_release_identity,10_quality_gates,20_runner_artifacts,40_signoff}

Expected outcome: canonical evidence folders exist.

Capture release identity and changelog proof:

VERSION="$(awk -F': ' '/^Version:/{print $2}' DESCRIPTION)"
EROOT="release_evidence/v${VERSION}/$(date +%Y%m%d)_$(git rev-parse --short HEAD)"
{
  echo "candidate_commit=$(git rev-parse HEAD)"
  echo "candidate_branch=$(git rev-parse --abbrev-ref HEAD)"
  echo "target_tag=v${VERSION}"
  echo "package_version=$(awk -F': ' '/^Version:/{print $2}' DESCRIPTION)"
} > "$EROOT/00_release_identity/release_identity.txt"

sed -n '1,120p' CHANGELOG.md > "$EROOT/00_release_identity/changelog_top.md"

Expected outcome: release_identity.txt and changelog_top.md are populated.

Capture dependency source proof:

VERSION="$(awk -F': ' '/^Version:/{print $2}' DESCRIPTION)"
EROOT="release_evidence/v${VERSION}/$(date +%Y%m%d)_$(git rev-parse --short HEAD)"
{
  echo "# Dependency Sources"
  echo
  echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
  echo
  echo "## DESCRIPTION Remotes"
  awk '/^Remotes:/{flag=1} flag{print} /^[A-Za-z]+:/{if (flag && $1 != "Remotes:") flag=0}' DESCRIPTION
  echo
  echo "## renv non-CRAN sources"
  Rscript - <<'RS'
value_or_empty <- function(x) {
  if (is.null(x)) "" else x
}
lock <- renv:::renv_lockfile_read("renv.lock")
for (pkg in names(lock$Packages)) {
  rec <- lock$Packages[[pkg]]
  source <- value_or_empty(rec$Source)
  remote_url <- value_or_empty(rec$RemoteUrl)
  if (!identical(source, "Repository") || grepl("^file://", remote_url)) {
    cat("- ", pkg, "\n", sep = "")
    cat("  source: ", source, "\n", sep = "")
    if (nzchar(remote_url)) cat("  remote_url: ", remote_url, "\n", sep = "")
    if (!is.null(rec$RemoteSha)) cat("  remote_sha: ", rec$RemoteSha, "\n", sep = "")
  }
}
RS
} > "$EROOT/00_release_identity/dependency_sources.md"

Expected outcome: dependency sources are visible to reviewers. Any file:// source must be accepted as a local-release prerequisite or replaced with a reachable pinned remote before external release.

Capture gate logs and exit codes:

Run these commands inside a container whose R version matches renv.lock (currently 4.5.1). Set the container-specific library before collecting logs.

source scripts/r-library-path.sh
dsambayes_set_r_library container
mkdir -p "$R_LIBS_USER"
VERSION="$(awk -F': ' '/^Version:/{print $2}' DESCRIPTION)"
EROOT="release_evidence/v${VERSION}/$(date +%Y%m%d)_$(git rev-parse --short HEAD)"

Rscript scripts/check.R --lint > "$EROOT/10_quality_gates/qg1_lint.log" 2>&1; echo $? > "$EROOT/10_quality_gates/qg1_lint.exit"
Rscript scripts/check.R --style > "$EROOT/10_quality_gates/qg2_style.log" 2>&1; echo $? > "$EROOT/10_quality_gates/qg2_style.exit"
Rscript scripts/check.R --test > "$EROOT/10_quality_gates/qg3_tests.log" 2>&1; echo $? > "$EROOT/10_quality_gates/qg3_tests.exit"
Rscript scripts/check.R --stan-release-evidence > "$EROOT/10_quality_gates/qg4_stan_release_evidence.log" 2>&1; echo $? > "$EROOT/10_quality_gates/qg4_stan_release_evidence.exit"
_R_CHECK_FORCE_SUGGESTS_=false R -q -e 'rcmdcheck::rcmdcheck(args = c("--no-manual"), error_on = "warning")' > "$EROOT/10_quality_gates/qg5_rcmdcheck.log" 2>&1; echo $? > "$EROOT/10_quality_gates/qg5_rcmdcheck.exit"
Rscript scripts/dsambayes.R validate --config config/blm_timeseries.yaml --run-dir results/quality_gate_validate > "$EROOT/10_quality_gates/qg6_validate.log" 2>&1; echo $? > "$EROOT/10_quality_gates/qg6_validate.exit"
Rscript scripts/dsambayes.R run --config config/blm_timeseries.yaml --run-dir results/quality_gate_run > "$EROOT/10_quality_gates/qg7_run.log" 2>&1; echo $? > "$EROOT/10_quality_gates/qg7_run.exit"
( python3 docs-site/build_content.py && cd docs-site && hugo --cleanDestinationDir ) > "$EROOT/10_quality_gates/qg8_docs.log" 2>&1; echo $? > "$EROOT/10_quality_gates/qg8_docs.exit"

Expected outcome: eight gate logs and eight exit-code files are present.

Copy runner artefacts:

VERSION="$(awk -F': ' '/^Version:/{print $2}' DESCRIPTION)"
EROOT="release_evidence/v${VERSION}/$(date +%Y%m%d)_$(git rev-parse --short HEAD)"
mkdir -p "$EROOT/20_runner_artifacts"
cp -R results/quality_gate_validate "$EROOT/20_runner_artifacts/"
cp -R results/quality_gate_run "$EROOT/20_runner_artifacts/"

Expected outcome: runner artefacts are captured under evidence storage.

Evidence review checklist

Before sign-off, reviewers must confirm all items below:

  1. release_identity.txt commit hash matches the commit being tagged.
  2. sign_off_record.md repeats the same candidate hash and evidence-root path.
  3. package_version in release_identity.txt matches the intended release.
  4. changelog_top.md includes the intended DSAMbayes release section aligned with candidate changes.
  5. Every qg*.exit file contains 0.
  6. qg4_stan_release_evidence.log has no unresolved diagnostic-threshold failures.
  7. dependency_sources.md lists every non-CRAN source and any file:// source has an explicit portability decision.
  8. Required QG-6 and QG-7 artefact paths exist.
  9. Completed sign-off record exists at 40_signoff/sign_off_record.md.

Submission and retention

  1. Record the evidence root path in the manual sign-off record and any release review used by the team.
  2. Do not delete evidence for approved releases.
  3. For rejected releases, retain evidence and mark decision as NO-GO in sign-off.

Release Readiness Checklist

Purpose

Provide the mandatory go or no-go checklist before manually creating a DSAMbayes release tag. It supports the normal quarterly release cycle; it does not require CI/CD or a rapid release cadence.

How to use this checklist

  1. Complete this checklist after running all release-quality gates.
  2. Record evidence paths for each item.
  3. If any mandatory item fails, decision is NO-GO.
  4. Copy final decision details into Release Sign-off Template.

Mandatory checklist

ID Check Pass criteria Evidence required
RL-01 Candidate commit fixed Single candidate commit hash selected and recorded, with matching sign-off metadata 00_release_identity/release_identity.txt, 40_signoff/sign_off_record.md
RL-02 Version metadata aligned DESCRIPTION version equals intended release version 00_release_identity/release_identity.txt
RL-03 Changelog aligned Top CHANGELOG.md section matches candidate scope 00_release_identity/changelog_top.md
RL-04 QG-1 lint passed Exit code 0, no lint failures 10_quality_gates/qg1_lint.log, 10_quality_gates/qg1_lint.exit
RL-05 QG-2 style passed Exit code 0, no style failures 10_quality_gates/qg2_style.log, 10_quality_gates/qg2_style.exit
RL-06 QG-3 tests passed Exit code 0, no test failures 10_quality_gates/qg3_tests.log, 10_quality_gates/qg3_tests.exit
RL-07 QG-4 Stan release evidence passed Exit code 0; diagnostic thresholds passed 10_quality_gates/qg4_stan_release_evidence.*
RL-08 QG-5 package check passed No ERROR; no unresolved WARNING 10_quality_gates/qg5_rcmdcheck.log, 10_quality_gates/qg5_rcmdcheck.exit
RL-09 QG-6 validate passed Exit code 0; required validate artefacts present 10_quality_gates/qg6_validate.*, 20_runner_artifacts/quality_gate_validate/
RL-10 QG-7 run passed Exit code 0; required run artefacts present 10_quality_gates/qg7_run.*, 20_runner_artifacts/quality_gate_run/
RL-11 QG-8 docs build passed Exit code 0; docs-site build completed 10_quality_gates/qg8_docs.*
RL-12 Dependency sources recorded Non-CRAN and file:// dependency sources, revisions, and portability decisions are recorded; no credentials are included 00_release_identity/dependency_sources.md
RL-13 Evidence bundle complete All mandatory evidence items present Evidence root directory
RL-14 Exceptions resolved or accepted All exceptions documented with owner and approval 40_signoff/sign_off_record.md
RL-15 Final sign-off recorded Decision, approver, and timestamp completed 40_signoff/sign_off_record.md

Required runner artefact checks

Validate artefacts that must exist:

  1. 20_runner_artifacts/quality_gate_validate/00_run_metadata/config.original.yaml
  2. 20_runner_artifacts/quality_gate_validate/00_run_metadata/config.resolved.yaml
  3. 20_runner_artifacts/quality_gate_validate/00_run_metadata/config.compiled.yaml
  4. 20_runner_artifacts/quality_gate_validate/00_run_metadata/session_info.txt

Run artefacts that must exist:

  1. 20_runner_artifacts/quality_gate_run/00_run_metadata/config.resolved.yaml
  2. 20_runner_artifacts/quality_gate_run/00_run_metadata/config.compiled.yaml
  3. 20_runner_artifacts/quality_gate_run/20_model_fit/model.rds
  4. 20_runner_artifacts/quality_gate_run/30_post_run/fitted.csv
  5. 20_runner_artifacts/quality_gate_run/30_post_run/observed.csv
  6. 20_runner_artifacts/quality_gate_run/40_diagnostics/diagnostics_report.csv

Decision rules

  1. GO only if every checklist item RL-01 to RL-15 passes.
  2. NO-GO if any mandatory item fails or evidence is incomplete.
  3. HOLD if no hard failure exists but final approval is pending.
  4. Tag creation is allowed only after GO decision is recorded.
  5. A NOTE or expected low-budget smoke-test sampler warning is not a blanket exception: its source and reviewer treatment must be recorded. QG-4 high-budget diagnostic-threshold failures always block release pending an explicit human decision.

Completion record template

Use this section when running the checklist.

Field Value
Release version <fill>
Candidate commit hash <fill>
Checklist executor <fill>
Checklist completion date (UTC) <YYYY-MM-DD>
Checklist result (GO/NO-GO/HOLD) <fill>
Evidence root path <fill>
Sign-off record path <fill>

Audit continuity reference

For programme-level historical traceability, also review:

  • refactoring_plan/release_readiness_checklist_v1.2.md

This page is the operational checklist for current release execution.

Release Playbook

Purpose

Define the deliberate, evidence-led release process for DSAMbayes. It supports the normal quarterly release cycle and material ad-hoc releases. It is not a CI/CD process and does not imply continuous deployment or a high release cadence.

The point is to make the infrequent release decision reproducible and reviewable, not to automate it for its own sake.

Roles and release boundary

  • Release owner: fixes the candidate commit, runs or coordinates the gates, and prepares the evidence bundle.
  • Reviewer: independently checks the evidence and records the GO, NO-GO, or HOLD decision. The reviewer should be different from the release owner when practical.
  • Maintainers: resolve failures or explicitly approve a documented exception.

One release has one candidate commit. Do not make product, dependency, or documentation changes after gates begin. If a change is required, create a new candidate and rerun the affected gates.

Preconditions

Before starting:

  1. The intended version is consistent in DESCRIPTION and CHANGELOG.md.
  2. The candidate commit is identified and the working tree is clean apart from deliberately excluded local evidence and run artefacts.
  3. The local R runtime and package library are recorded. The current tested baseline is R 4.5.1; DESCRIPTION declares the supported runtime floor. The floor is a compatibility policy, not a claim that every R version has been exercised for every release.
  4. Required non-CRAN dependencies, including DSAMdecomp, are reachable at the recorded revision. Record the source and commit SHA in the evidence bundle. Do not record credentials.
  5. No Stan template, prior, boundary-default, or fit-semantics change is released without the required human model review.

Manual release flow

1. Freeze and identify the candidate

git status --short
git rev-parse HEAD
git rev-parse --abbrev-ref HEAD

Record the full SHA, branch, intended tag, package version, R version, and platform in 00_release_identity/release_identity.txt of the evidence bundle. Use the current package version rather than editing this playbook for each release.

2. Prepare the local environment

source scripts/r-library-path.sh
dsambayes_set_r_library container
mkdir -p "$R_LIBS_USER" .cache
export XDG_CACHE_HOME="$PWD/.cache"
export _R_CHECK_FORCE_SUGGESTS_=false
export DSAMDECOMP_DISABLE_TELEMETRY=1
export DSAMBAYES_DISABLE_TELEMETRY=1
export USE_BUNDLED_LIBUV=1

R -q -e 'install.packages(".", repos = NULL, type = "source")'
R --version

Use a local container only when it makes the environment easier to reproduce; it is evidence capture, not a CI service. Its R version must match renv.lock (currently 4.5.1); record the image and digest if one is used. The helper selects .Rlib-container-r<active-R-version>/; never mount a host library as the container package library.

3. Create the evidence root

Use the layout defined in Release Evidence Pack. The evidence root is local working material until the release owner decides how it will be retained. It must not contain .env, credentials, private data, or unredacted tokens.

4. Run the mandatory gates once, against the frozen candidate

Run QG-1 to QG-8 from Quality Gates, capturing both complete logs and exit codes. The canonical commands are:

Rscript scripts/check.R --lint
Rscript scripts/check.R --style
Rscript scripts/check.R --test
Rscript scripts/check.R --stan-release-evidence
_R_CHECK_FORCE_SUGGESTS_=false \
  R -q -e 'rcmdcheck::rcmdcheck(args = c("--no-manual"), error_on = "warning")'
Rscript scripts/dsambayes.R validate \
  --config config/blm_timeseries.yaml --run-dir results/quality_gate_validate
Rscript scripts/dsambayes.R run \
  --config config/blm_timeseries.yaml --run-dir results/quality_gate_run
python3 docs-site/build_content.py
(cd docs-site && hugo --cleanDestinationDir)

scripts/check.R --release is a useful local consolidation check, but it does not replace rcmdcheck, the two runner checks, the docs-site build, or the high-budget --stan-release-evidence gate.

5. Interpret warnings correctly

The release decision is based on the mandatory gates, not on a search for a silent terminal.

  • A non-zero exit code, missing required tool, test failure, unresolved package WARNING, failed diagnostic threshold, or missing required runner artefact is a blocker.
  • rcmdcheck NOTEs require written review and acceptance; they are not silently waived.
  • Low-budget Stan smoke tests can emit deliberately exercised sampler warnings. They are not release evidence and must be recorded as expected test-fixture behaviour if observed. The high-budget QG-4 diagnostics are the release evidence and may not be waived without an explicit human decision.
  • Any new, unexplained warning is a HOLD until it is understood or removed.

6. Review and sign off

Complete the Release Readiness Checklist and copy Release Sign-off Template into the evidence bundle. A GO requires all mandatory checks to pass, the candidate SHA to match throughout, and the reviewer to sign the decision.

7. Tag and publish manually

Only after GO:

git tag -a vX.Y.Z -m "DSAMbayes vX.Y.Z"
git push origin vX.Y.Z

Create any GitHub release and publish any docs mirror as separate manual acts. Record what was published, by whom, and when. No automatic deployment follows from tagging.

8. Retain the decision record

Retain the signed evidence bundle for approved and rejected candidates. Record post-release defects as a new hotfix candidate; do not retag or rewrite the published release history.

Go/no-go rules

  1. GO only when QG-1 to QG-8 pass and the sign-off record is complete.
  2. HOLD when evidence is incomplete, a warning is unexplained, or approval is pending.
  3. NO-GO for any release blocker. Fix the issue, select a new candidate SHA, and rerun the affected gates.

Hotfixes

For a post-release defect, branch from the affected tag, make the smallest safe change, update the version and changelog, and run the same manual process. The smaller scope does not remove the evidence or human-review requirement.

Sign-off Template

Purpose

Provide the final approval record for a DSAMbayes release candidate after all mandatory evidence has been reviewed.

Instructions

  1. Copy this template into the candidate evidence bundle as 40_signoff/sign_off_record.md.
  2. Complete every field.
  3. Use GO, NO-GO, or HOLD for the decision.
  4. If any exception is accepted, record explicit rationale and owner.
  5. Copy the candidate commit hash and evidence-root path directly from 00_release_identity/release_identity.txt; do not introduce a second candidate hash during sign-off.

Release identification

Field Value
Release version vX.Y.Z
Package version (DESCRIPTION) <fill>
Candidate commit hash <fill>
Candidate branch <fill>
Intended tag <fill>
R version and platform <fill>
Container image/digest (if used) <fill or n/a>
DSAMdecomp source and revision <fill or n/a>
Changelog section verified <yes/no>
Evidence root path <fill>

Decision summary

Field Value
Decision <GO/NO-GO/HOLD>
Decision date (UTC) <YYYY-MM-DD>
Decision timestamp (UTC) <YYYY-MM-DDTHH:MM:SSZ>
Release owner <name>
Primary approver <name>
Secondary reviewer (if used) <name or n/a>

Decision rationale:

<fill>

Quality gate outcomes

Gate ID Outcome (pass/fail) Evidence file(s) Reviewer notes
QG-1 Lint <fill> 10_quality_gates/qg1_lint.log, 10_quality_gates/qg1_lint.exit <fill>
QG-2 Style <fill> 10_quality_gates/qg2_style.log, 10_quality_gates/qg2_style.exit <fill>
QG-3 Unit tests <fill> 10_quality_gates/qg3_tests.log, 10_quality_gates/qg3_tests.exit <fill>
QG-4 Stan release evidence <fill> 10_quality_gates/qg4_stan_release_evidence.log, 10_quality_gates/qg4_stan_release_evidence.exit <fill>
QG-5 Package check <fill> 10_quality_gates/qg5_rcmdcheck.log, 10_quality_gates/qg5_rcmdcheck.exit <fill>
QG-6 Runner validate <fill> 10_quality_gates/qg6_validate.log, 10_quality_gates/qg6_validate.exit <fill>
QG-7 Runner run <fill> 10_quality_gates/qg7_run.log, 10_quality_gates/qg7_run.exit <fill>
QG-8 Docs build <fill> 10_quality_gates/qg8_docs.log, 10_quality_gates/qg8_docs.exit <fill>

Mandatory artefact checks

Artefact Present (yes/no) Path Notes
Original config (validate) <fill> 20_runner_artifacts/quality_gate_validate/00_run_metadata/config.original.yaml <fill>
Resolved config (validate) <fill> 20_runner_artifacts/quality_gate_validate/00_run_metadata/config.resolved.yaml <fill>
Compiled config (validate) <fill> 20_runner_artifacts/quality_gate_validate/00_run_metadata/config.compiled.yaml <fill>
Session info (validate) <fill> 20_runner_artifacts/quality_gate_validate/00_run_metadata/session_info.txt <fill>
Resolved config (run) <fill> 20_runner_artifacts/quality_gate_run/00_run_metadata/config.resolved.yaml <fill>
Compiled config (run) <fill> 20_runner_artifacts/quality_gate_run/00_run_metadata/config.compiled.yaml <fill>
Model object (run) <fill> 20_runner_artifacts/quality_gate_run/20_model_fit/model.rds <fill>
Fitted values (run) <fill> 20_runner_artifacts/quality_gate_run/30_post_run/fitted.csv <fill>
Observed values (run) <fill> 20_runner_artifacts/quality_gate_run/30_post_run/observed.csv <fill>
Diagnostics report (run) <fill> 20_runner_artifacts/quality_gate_run/40_diagnostics/diagnostics_report.csv <fill>

Exceptions and risk acceptance

Record every exception. If there are none, write none.

ID Exception Reason Risk owner Expiry date Approved (yes/no)
EX-01 <fill or none> <fill> <fill> <YYYY-MM-DD or n/a> <fill>

Required follow-up actions

Record actions that must happen after release decision.

ID Action Owner Due date Tracking link
ACT-01 <fill or none> <fill> <YYYY-MM-DD or n/a> <fill or n/a>

Final approval signatures

Role Name Signature mode Date (UTC)
Release owner <fill> <typed/e-sign> <YYYY-MM-DD>
Approver <fill> <typed/e-sign> <YYYY-MM-DD>
Additional approver (optional) <fill or n/a> <typed/e-sign or n/a> <YYYY-MM-DD or n/a>

Final decision statement

<Release vX.Y.Z is approved for tagging and publication.>

or

<Release vX.Y.Z is not approved. See exceptions and actions.>