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.
Decision-layer budget allocation, objectives, risk scoring, and response transforms
Subsections of Modelling
Model Classes
Purpose
DSAMbayes provides four 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.
The fixed-effects (FE) class is a separate coefficient-only estimator. A
hierarchical model with unit random intercepts remains a random-effects model,
not an FE estimator.
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 | 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
Fixed effects
fixed_effects, requires_prior, blm
fixed_effects(formula, data, unit) then set_date()
Panel (long format)
Exact within-unit contrasts; common slopes
Within-unit coefficient inference when unrestricted unit intercepts are nuisance parameters
FE v1 removes unrestricted unit intercepts with deterministic orthonormal
within-unit contrasts. It estimates common slopes and residual noise through
MCMC. Formula terms must be bare numeric columns, and retained unit-date keys
must be unique. Unequal unit sizes and irregular date spacing are allowed.
Output boundary
Use get_posterior() for reporting-scale slope and residual-noise draws and
within-contrast generated quantities. FE v1 does not recover unit intercepts
or support MAP, level fitted values, prediction, model selection,
decomposition, counterfactual analysis, optimisation, forecasting, or
deployment. It supports YAML validation, dry-run, and bounded MCMC fitting
with dedicated coefficient and contrast-space artefacts.
P4-J’s bounded live runner, explicit-prior prior-only, one-replication
recovery, and sampler gates passed. These checks do not estimate
repeated-sampling coverage or production reliability. The implementation is
not production-qualified, so FE remains implemented_not_qualified. See
Estimator Capabilities for the full
contract.
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.
fitted(), predicted response on the original scale
decomp(), native predictor-level decomposition. It returns a
dsambayes_decomposition S3 result for BLM objects; use $DecompedData for
the long-form contribution table. Hierarchical models return a keyed tibble
with one such result in each row’s decomp list-column.
decomp() no longer returns the external DSAMdecomp S4 calculators. This is
a breaking development-version change. Replace S4 slot access such as
result@DecompedData with result$DecompedData; use
result$DecompGroupProps for grouped totals. The DSAMdecomp-only plotting and
post-processing helpers are not part of the native result. Build any required
downstream summaries from these two tables.
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:
Terms to the left of | become random slopes; the variable to the right defines the grouping factor. Multiple grouping terms are supported.
The ordinary random-effects specification partially pools group effects and
uses both within-group and between-group information for population
coefficients. Its interpretation requires the group effects to be
conditionally mean-independent of the regressors. If this assumption is not
credible, use a justified CRE specification or another design. Neither option
solves omitted time-varying confounding by itself.
CRE / Mundlak extension
For correlated random effects, call set_cre() after construction:
CRE v1 is restricted to a selected random-intercept group with no random slope
in that group. Each CRE variable must be one simple numeric main effect and
must not overlap an internal draw-dependent media transform. Fit preparation
calculates the Mundlak means from the exact retained estimation frame. 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 is native and returns one result per retained group.
Probabilistic media transforms remain unsupported for decomposition and are
rejected explicitly.
Formula offsets enter native decomposition as coefficient-one baseline
components and must be included in the decomposition mapping table.
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():
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 where the selected random intercept may correlate with regressors
Hierarchical + CRE
Mundlak terms model that correlation through included group means when the random-intercept conditional-mean specification is adequate
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:
Is the dataset a single time series or a grouped panel?
Do you need partial pooling across real groups, or pooling across labelled
coefficient dimensions?
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 inferential runs where diagnostics and uncertainty quantification matter,
MCMC is the recommended fit method. MAP is useful for rapid iteration during
model development. Fit method alone does not qualify an estimator for
production use.
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.
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.
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)
Native predictor-level decomposition
Returns a dsambayes_decomposition for BLM or a keyed tibble containing one result per retained hierarchical group. Use $DecompedData inside each result for contributions.
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:
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 behaviour.
Use this page when you need exact DSAMbayes semantics: supported prior families,
override syntax, default generation rules, and scaling behaviour. 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.
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 with log-transformed parameters
Positive noise_sd and hierarchical sd_<idx>[<term>] priors
All coefficient priors use normal(). The lognormal_ms family 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(). On the reporting scale,
the intercept prior is normal(ybar, sy), each slope prior is
normal(0, sy / sx), and the residual-SD prior is normal(0, sy), where
sy and sx are the response and term standard deviations. These defaults
therefore map to unit-scale priors after internal standardisation and respond
coherently when measurement units change.
Hierarchical (group-level standard deviations)
default_prior.hierarchical() additionally generates sd_<idx>[<term>] rows
for each group factor. Each constrained group SD receives a zero-centred normal
prior whose scale matches the corresponding coefficient scale: sy for a
group intercept and sy / sx for a random slope. Because the SD parameter is
constrained positive in Stan, this acts as a half-normal prior. It remains
positive when observed group outcome means happen to be equal, and random-slope
defaults change coherently when predictor measurement units change.
Specify hierarchical standard-deviation priors in reporting-scale units. With
scale = TRUE, DSAMbayes transforms them to the Stan scale: intercept
heterogeneity is divided by the response standard deviation, while slope
heterogeneity is multiplied by the predictor-to-response standard-deviation
ratio. The group intercept is defined at the internally centred predictor
reference point. It is not a group intercept at raw predictor values of zero.
With the defaults above, these transformations produce unit-scale group-SD
priors in Stan space. Explicit user overrides retain their supplied
reporting-scale meaning and are transformed by the same rules.
Default population and group-SD scales use the same population-model
complete-case frame. This keeps the reporting-to-Stan transformation exact
when a population term contains missing values. Random-slope terms must be
numeric. Group-only random-slope terms are evaluated on the retained population
rows. DSAMbayes aborts rather than deriving a group-SD default when a group term
would drop additional rows or when a factor random slope is unsupported.
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.
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:
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.
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
Data-dependent: normal(ybar, sy) intercept, normal(0, sy / sx) slopes, normal(0, sy) noise_sd (see Default prior generation above)
Population: same as BLM; group SD: data-derived
Non-pooled: BLM defaults; pooled dimension coefficients only: normal(0, 5) per dimension
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
Model Classes, constructor and fit support per class
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
Default-first: keep priors.use_defaults: true.
Sparse overrides: only add priors.overrides for high-conviction terms.
Selective bounds: add boundaries.overrides only for structural signs.
No blanket constraints: do not force all controls/media to one sign by default.
Diagnose before tightening: use pre-flight and diagnostics gates first, then
add priors/bounds if uncertainty is still unstable.
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:
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:
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).
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:
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":
The output includes source_response_scale (the model’s response scale), response_scale = "kpi", and conversion_method ("lognormal_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.
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.
Config Schema, target.*, media, controls, and model.scale keys
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.
It addresses correlation between group effects and regressors when the
conditional mean of the selected random intercept is adequately represented by
the included group means. It does not correct correlated random slopes,
time-varying endogeneity, or omitted time-varying confounders.
When to use CRE
Use CRE when:
The model is hierarchical (panel data with (term | group) syntax).
Time-varying regressors (e.g. externally transformed media signals) have
group-level means that may be correlated with the selected group intercept.
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).
The selected group needs random slopes, or a CRE variable uses an internal
draw-dependent adstock or Hill transform.
Construction
CRE is applied after model construction via set_cre():
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.
Generates group-mean column names. For each variable in vars, a mean-term column is named cre_mean_<variable> (configurable via prefix).
Preserves the source data.set_cre() does not add generated columns to
.original_data. It creates a provisional prior view for the generated
terms; fit preparation resolves the final priors from the retained frame.
Updates the formula. The generated mean terms are appended to the
population formula as fixed effects.
Builds authoritative means at fit time. DSAMbayes reconciles all
population, random-effect, group, date, offset, and transform dependencies;
excludes incomplete or non-finite rows; then calculates group means over
exactly the rows and fitted covariate basis passed to Stan.
Extends priors and boundaries. Default prior and boundary entries are
added for each new mean term. Explicit prior overrides remain unchanged.
Within-group effect: $\beta$, the coefficient on $x_{gt}$ after
conditioning on the group mean.
Contextual difference: $\theta$, the coefficient on $\bar{x}_g$. It is
the difference between the between-group and within-group slopes.
Total between-group slope: $\beta + \theta$.
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).
The selected group exists and contains a random intercept but no random
slope.
Each CRE variable is one simple numeric population main effect. Factors,
interactions, inline transforms, polynomials, and splines involving a CRE
variable are rejected.
No CRE variable overlaps an enabled internal draw-dependent media transform.
No generated CRE mean term appears in a random-effects block.
Generated column names do not collide with user-authored source columns.
The same typed structural checks run in direct data preparation, pre-flight,
fit(), fit_map(), authored v2 config validation, compiled v1 validation,
and runner preparation. pre_flight_strict = FALSE cannot downgrade them.
Identification warnings
warn_cre_identification() checks two conditions after CRE setup:
More CRE variables than groups. If length(vars) > n_groups, between-effect estimates may be weakly identified. The function emits a warning.
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 the combined between-group design is rank-deficient, pre-flight reports the
problem and strict runner fitting aborts. Reduce hierarchy.cre_variables, add
credible group variation, or use RE only when its independence assumption is
defensible. Disabling scaling does not repair a rank-deficient CRE design.
Panel assumptions
Balanced panels are not required. Each retained observation contributes
once to its group mean, so unequal group sizes are supported.
Missing and non-finite dependencies are reconciled jointly. A row omitted
from the likelihood cannot influence a fitted CRE mean. Mean calculation does
not use na.rm = TRUE after the retained frame is frozen.
Generated columns are owned by DSAMbayes. Fit preparation rebuilds them
from source variables. A user-authored column with a reserved generated name
aborts instead of being overwritten.
Prediction uses fitted means. Holdout, cross-validation, and deployment
paths use training-only lookups derived from the fitted estimation frame.
Estimator evidence smoke
The default suite runs deterministic checks of the repository-owned RE/CRE DGP
and evidence aggregation contracts. An explicit Stan smoke checks that one
fixed RI-02 replication can be fitted through the public RE and CRE APIs:
This smoke asserts only that usable posterior coefficient draws exist. It does
not measure recovery, calibration, bias, relative estimator performance, or
production readiness. The immutable manifest, terminal-record, and aggregation
workflow under scripts/estimator-evidence/ owns pilot and confirmatory Monte
Carlo evidence. Do not treat a smoke result as estimator qualification or
causal validation.
Retained diagnostics evidence
Runner fits retain two CRE-specific diagnostic artefacts under
40_diagnostics/:
estimator_checks.csv records the typed structural-contract result and the
pre-flight CRE configuration result;
row_reconciliation.csv records input, retained, and excluded counts plus
excluded source-row IDs and reason codes. It does not contain response
values.
These artefacts establish which checks and rows the fit used. They do not
establish causal identification or Monte Carlo calibration.
Fixed-effects boundary
DSAMbayes does not currently implement the planned exact within-unit
fixed-effects (FE) estimator. A hierarchical random-intercept model is not an
FE estimator. Do not label it as FE or use it as a substitute for the pending
implementation and evidence gates.
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 is a contextual difference,
not the total between-group slope. For a model containing $x_{gt}$ and
$\bar{x}_g$, calculate the total between-group slope as the sum of their two
coefficients. Treat all three quantities as associational unless the research
design supports a causal interpretation.
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.
Time-index and panel assumptions
DSAMbayes models time using an ordered modelling-wave index, not elapsed
calendar time. The standard MMM workflow assumes:
observations are conditionally independent given the modelled mean, with a
common residual standard deviation;
panel data are balanced, so every group has the same ordered modelling waves;
trend, seasonality and media carry-over are defined per modelling wave; and
the date column identifies wave order and supports reporting, blocked
cross-validation and holiday alignment.
Under this contract, one step in t_scaled and one adstock recursion step both
mean one modelling wave. A longer calendar gap between two observed rows does
not rescale either component. Elapsed calendar duration is outside the model
contract.
For a panel, construct the wave variables once from the common date index and
map them back to every group. Do not create a separate sequential index within
each stacked group:
The low-level hierarchical and CRE constructors can technically accept unequal
group sizes; they do not enforce the balanced-panel production assumption.
Upstream data preparation must therefore verify that every group contains the
same wave set before a production panel run.
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 Friday → black_friday
New Year's Day → new_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:
Parse and validate the calendar.validate_holiday_calendar() checks column presence, date parsing, and label completeness.
Expand holiday windows.expand_holiday_windows() replicates each event row across the [event_date - window_before, event_date + window_after] range.
Align to weekly index. Each expanded event-day is mapped to its containing week using week_floor_date() with the configured week_start.
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.
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.
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.
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.
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, and Pareto-k outputs as supplementary fit and influence
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.
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
P0 sampler and integrity checks can fail; condition-number, residual, boundary and within-variation P1 checks are warn-only
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 critical MCMC reliability
Pre-fit and post-fit publish gates
P1
Design conditioning, residual behaviour and identifiability
Pre-fit and post-fit model-review checks
P2
Supplementary model-selection evidence
Post-fit predictive scoring
Each check row includes:
Field
Meaning
check_id
Unique identifier
phase
P0, P1 or P2
severity
Check priority recorded by the diagnostics registry
status
pass, warn, fail, or skipped
metric
Metric name
value
Observed value
threshold
Applied threshold description
message
Human-readable explanation
Design checks
Check ID
Metric
Pass
Warn
Fail
pre_response_finite
non_finite_response_count
== 0
n/a
> 0
pre_design_constants_duplicates
constant_plus_duplicate_columns
== 0
n/a
> 0
pre_design_rank_deficit
rank_deficit
== 0
> 0 (publish)
> 0 (strict)
pre_design_condition_number (P1)
kappa_X
≤ warn
> warn
> fail
Condition number thresholds by mode
Mode
Warn
Fail
explore
10,000
∞ (cannot fail)
publish
10,000
1,000,000 (warn-only; fail disabled)
strict
10,000
1,000,000
P0 sampler checks (MCMC only)
Check ID
Metric
Direction
Warn
Fail
post_rhat_max
max_rhat
Lower is better
1.01
1.01
post_ess_bulk_min
min_ess_bulk
Higher is better
400
200
post_ess_tail_min
min_ess_tail
Higher is better
200
100
post_ebfmi
min_ebfmi
Higher is better
0.30
0.20
post_treedepth_saturation
treedepth_hit_fraction
Lower is better
0.00
0.01
post_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
post_residual_ljung_box_p_min
resid_lb_p
Higher is better
0.05
0.01
post_residual_max_abs_acf
resid_acf_max
Lower is better
0.20
0.40
Mode adjustments for residual checks
Mode
resid_lb_p warn
Raw fail boundary / effective state
resid_acf warn
Raw fail boundary / effective state
explore
0.05
0.00 (cannot fail)
0.20
∞ (cannot fail)
publish
0.05
0.01 (warn-only; fail disabled)
0.20
0.40 (warn-only; fail disabled)
strict
0.10
0.05
0.15
0.30
In publish mode, values beyond the raw residual fail boundary remain warn.
The persisted threshold field states that failure is disabled and retains the
raw boundary for triage. Non-residual P0 failures can still make the overall
run fail.
P1 boundary hit check
Check ID
Metric
Direction
Warn
Fail
post_boundary_hit_rate_max
boundary_hit_frac
Lower is better
0.05
0.20
In explore mode, boundary hits cannot fail. Publish mode retains the raw
fail > 0.20 boundary for triage but records values beyond it as warn.
Failure is disabled for this check in publish mode. In strict mode,
thresholds tighten to warn > 0.02, fail > 0.10.
P1 within-group variation check
Check ID
Metric
Direction
Warn
Fail
pre_within_variation_ratio_min
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). Publish mode retains the raw
fail < 0.05 boundary for triage but records values below it as warn;
failure is disabled for this check. Strict mode applies warn < 0.15 and
fail < 0.10.
Identifiability gate
The P1 pre_identifiability_baseline_media_corr check measures the maximum
absolute correlation between baseline terms and media terms in the design
matrix. It is configured via diagnostics.identifiability in YAML:
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():
If any check has status == "fail" → overall status is fail.
If any check has status == "warn" (and none fail) → overall status is warn.
Otherwise → overall status is pass.
Checks with status == "skipped" do not affect the overall status.
P2 rows use the emitted post_model_selection_method, post_psis_loo,
post_psis_loo_options_ignored, post_psis_loo_time_dependence,
post_psis_loo_elpd, post_psis_loo_looic and
post_psis_loo_pareto_k identifiers. They record supplementary predictive
evidence and do not override P0 or P1 failures.
The runner also emits post_sampler_params_available when sampler parameters
can be inspected and post_sampler_unavailable_for_optimise for the explicit
MAP not-applicable path. These identifiers are part of the persisted report;
they are not substitute MCMC diagnostics for a MAP fit.
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 enabled check breached its configured threshold. This does not establish substantive adequacy, causal validity or suitability for the intended decision.
warn: review recommended; the model may have quality concerns but does not block the configured policy.
fail: at least one enabled check breached a fail threshold; resolve it before using the result for decisions.
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
Model Classes, which diagnostics apply to each class
counterfactual_response() evaluates one fitted MCMC model under two aligned
predictor paths. It returns posterior draws of the fitted response location
under the proposed scenario, the reference response, and their difference.
The result is a model-implied scenario contrast. It is not, by itself, a causal
effect or an attribution estimate. Causal interpretation requires a credible
identification argument for the fitted model and the intervention being
represented.
Usage skeleton
The following code assumes that fitted_model is a fitted MCMC object and
future_data contains every required model variable and modelling wave.
Both data frames must resolve to the same retained rows. Dates, hierarchical
group keys and row order must match. Predictor values may differ. The reported
difference is scenario - reference. For balanced panels, supply the common
ordered modelling waves used by the fitted model.
Returned quantities
The object has three components:
Component
Content
draws
One row per selected posterior sample. The scenario, reference, and difference columns contain response vectors.
summary
One row per retained observation with posterior means, medians, the requested central interval and .input_row provenance.
These are posterior draws of the fitted response location. They do not include
new observation noise. On KPI scale, a log-response model can return a
conditional median or a lognormal conditional mean. Use the draws to describe
uncertainty in the fitted response surface, not the full range of future
observations.
Use draw_ids to select existing .sample identifiers when the complete
draw-by-row result would be too large:
draw_ids reduces the returned draw-by-row object. The current implementation
still materialises the fitted posterior before selecting those identifiers, so
it does not remove the peak memory cost of posterior extraction.
Aggregate a decision horizon
Use aggregate_counterfactual_response() to sum responses within each
posterior draw before calculating intervals:
The result includes draw-wise scenario totals, reference totals, differences
and percentage lift. Its summary includes posterior means, medians, central
intervals and probability_difference_positive. That probability is the
proportion of retained posterior draws where the aggregated
scenario - reference difference is strictly above zero. It is not the
probability that an intervention has a causal effect.
Group by a retained panel or date label when separate aggregates are needed:
Aggregation is draw-wise. DSAMbayes first sums each draw over the relevant rows
and then calculates posterior intervals. It does not sum row-level interval
bounds.
Percentage lift is defined draw by draw as:
100 * difference_total / reference_total
It requires totals on the KPI or identity-response scale and a strictly
positive reference total in every retained draw and group. For log-response
totals, first call counterfactual_response(..., scale = "kpi"). Set
include_percent_lift = FALSE when only absolute totals and differences are
required.
YAML runner integration
Set scenario_analysis.enabled: true to evaluate two file-backed paths after a
successful MCMC fit. The runner writes row summaries, aggregate summaries and
metadata under 60_scenario_analysis/. Draw-level aggregate output is written
only when scenario_analysis.save_draws: true.
The scenario and reference files must follow the same alignment and input
contracts described above. A failed requested scenario analysis does not erase
the successful model fit. The returned runner result instead records
outcome = "completed_with_scenario_analysis_fail" and the failure reason.
See Config Schema and Output Artefacts.
Response and KPI scales
scale = "response" returns the model response scale. For log(y) models,
this is the log scale.
scale = "kpi" converts both absolute paths before calculating their
difference. For a log-response model:
The second form estimates a conditional expected KPI level under the fitted Gaussian
log-response model. It does not correct model misspecification or confer a
causal interpretation.
Media transforms and carry-over
For a model fitted with probabilistic adstock and Hill saturation, the engine
uses each posterior draw’s fitted decay, half-saturation and media coefficient.
It uses the fixed Hill shape and scaling metadata stored with the fit. The full
Stan posterior must therefore remain available. A materialised posterior table
or compact deployment artefact does not contain enough information and is
rejected.
Carry-over starts at the first supplied row. DSAMbayes does not invent media
history before the scenario window. Hierarchical media transforms reset at the
fitted panel boundary. The current pooled Stan model uses one media panel, so
its carry-over continues across the complete supplied row sequence. Arrange
pooled inputs to match the fitted row contract and review this limitation before
using pooled transformed-media contrasts.
If the scenario continues from a known observed media history, prepend that
common history to both scenario and reference. Keep the historical values
identical in both paths. This reconstructs carry-over at the decision-horizon
boundary. After evaluation, use .input_row or the date and panel keys to keep
only the decision-horizon rows. Omitting known pre-horizon media resets
carry-over and answers a different scenario question.
Changing spend early in the supplied window can affect later rows through
carry-over. Scenario and reference paths should therefore cover the complete
decision horizon rather than isolated rows.
Hierarchical paths are evaluated in the model’s group-major order. The
summary$.input_row column maps every processed row back to its supplied data
row. Draw-vector positions follow summary$.row; do not bind them back to the
input data by position alone.
Supported and rejected fits
The first implementation supports fitted MCMC BLM, hierarchical and pooled
models. It rejects:
unfitted models;
MAP fits, because one optimum is not a posterior sample;
deployment artefacts, because they contain point estimates;
transformed-media objects that no longer retain Stan alpha and k draws;
unseen hierarchical levels;
duplicate date/panel row keys;
transformed paths that are not strictly ordered by modelling wave;
transformed hierarchical paths whose panels do not contain identical wave
sets;
scenario and reference paths with different retained dates, panels or row
counts.
optimise_budget() remains a separate scenario-authored response-surface
workflow. It does not yet call this engine and does not automatically inherit
fitted adstock or Hill parameters.
Budget Optimisation
Purpose
DSAMbayes provides a decision-layer budget optimisation engine that operates on fitted model posteriors. Given a channel scenario with spend bounds, response-transform specifications, and an objective function, the engine searches for the allocation that maximises the chosen objective while respecting channel-level constraints. This page defines the inputs, objectives, risk scoring, response-scale handling, and output structure.
Overview
Budget optimisation is separate from parameter estimation. It takes a fitted model and a scenario specification, then:
Extracts posterior coefficient draws for the scenario’s channel terms.
Generates feasible candidate allocations within channel bounds that sum to the total budget.
Evaluates each candidate across all posterior draws to obtain a distribution of KPI outcomes.
Ranks candidates by the configured objective and risk scoring function.
Returns the best allocation, channel-level summaries, response curves, and impact breakdowns.
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.
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
n/a
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:
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.
Work-size controls and practical limits
n_candidates must be a finite integer from 10 through
.Machine$integer.max. scenario$posterior$draws must be a finite integer from
1 through .Machine$integer.max; it defaults to 500. Integer-valued doubles
such as 2000 are accepted and normalised. Fractional, missing, non-finite and
overflowing values are rejected before candidate or draw allocation begins.
Runtime grows approximately with candidate allocations multiplied by retained
posterior draws. An efficient frontier repeats that work for every feasible
budget multiplier. The optimiser keeps scalar search results for each candidate
and retains full draw vectors only for the winning candidate, so candidate
count no longer multiplies retained draw storage.
The defaults of 2,000 candidates and 500 posterior draws are the routine
starting point. Benchmark the deployment host before exceeding any of the
following review thresholds:
10,000 candidates;
2,000 retained posterior draws; or
seven efficient-frontier levels.
These are measurement thresholds, not hard caps. Channel count, response
transforms and host hardware also affect runtime. Run the bounded benchmark
from the repository root:
The benchmark runs each case in a fresh process and writes results under the
ignored results/ tree. On Linux it reports whole-process peak resident set
size (RSS) from /usr/bin/time -v. Peak RSS includes package-load overhead, so
compare runs on the same host and software environment; do not interpret it as
an optimiser-only heap estimate.
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:
Respect per-channel lower bounds.
Respect per-channel upper bounds.
Sum exactly to budget_total.
Allocation is performed by distributing remaining budget (after lower bounds) using exponential random weights, iteratively filling channels until the budget is exhausted. project_to_budget() ensures exact budget equality via proportional adjustment.
Output structure
optimise_budget() returns a budget_optimisation object containing:
Field
Content
best_spend
Named numeric vector of optimal per-channel spend
best_score
Objective score of the best allocation
channel_summary
Tibble with per-channel reference vs optimised spend, response, ROI, CPA, and deltas
curves
List of per-channel response curve tibbles (spend grid × mean/lower/p50/upper)
points
Tibble of reference and optimised points per channel with confidence intervals
impact
Waterfall-style tibble of per-channel KPI contribution and interaction residual
objective_cfg
Echo of the objective configuration
scenario
Echo of the input scenario
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 80_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
Model Classes, fit support and posterior extraction per class
This document is the approved Gate A contract for hardening the
random-effects (RE), correlated-random-effects (CRE), and fixed-effects (FE)
estimators. Approval fixes the intended scope. It does not mean that every
target capability is implemented or production qualified.
The current implementation status is authoritative until the relevant
implementation and evidence gates have passed:
Estimator
Current status
Safe interpretation
RE
supported_with_limits
Existing Gaussian hierarchical estimator. Its population slopes require conditional mean independence between group effects and regressors. Retained block-support diagnostics are available; Monte Carlo qualification is pending.
CRE
supported_with_limits
Enforced random-intercept Mundlak augmentation for simple externally transformed or untransformed numeric regressors. Retained between-design and hierarchy block-support diagnostics are available; Monte Carlo qualification is pending.
FE
implemented_not_qualified
The direct R API and bounded YAML runner implement coefficient-only Gaussian FE with exact within-unit contrasts. P4-J technical gates passed, but deterministic one-replication checks do not provide repeated-sampling qualification. FE does not recover unit intercepts or provide level prediction.
No estimator is production qualified by this document alone. Production
status requires the implementation, package validation, Stan review where
applicable, and predeclared Monte Carlo evidence.
Shared interpretation boundary
Estimator choice changes how time-invariant group heterogeneity is handled. It
does not establish causal identification. RE, CRE, and FE can all be biased by
omitted time-varying confounding, reverse causality, measurement error, or a
misspecified response function. Applied work must assess those risks
separately.
The package will fail closed for a structurally unsupported estimator and
design combination. Advisory diagnostics will remain distinct from structural
errors and will report their metric, threshold, and recovery action.
Estimator design-diagnostic policy
diagnostics_policy_thresholds() owns one nested estimator_design policy.
This keeps estimator-specific design checks separate from the existing general
design-matrix and fitted-model diagnostics. In particular, the estimator
condition-number rule does not replace the existing kappa_* policy, and the
within-share rule does not replace the existing within_var_* policy.
The version 1 estimator-design policy fixes these boundaries:
Check
Pass boundary
Advisory boundary
Structural or strict failure
SVD rank tolerance
Full column rank at max(n, p) * max(s) * .Machine$double.eps
Not applicable
Numerical rank below the column count at that tolerance
The group-level boundary is an operational small-support warning, not a claim
that four groups guarantee reliable variance-component estimation. The VIF,
condition-number, within-share, residual-degrees-of-freedom, and group-support
boundaries are conservative screening rules rather than universal statistical
constants.
For YAML runner fits, diagnostics.policy_mode is retained before fitting and
selects this estimator-design policy during CRE pre-flight and preparation.
Consequently, strict mode rejects a full-rank combined between-design with fewer
than two residual degrees of freedom before Stan compilation. Direct API
preparation uses the publish policy.
P2-A defines and tests this policy. P2-B through P2-E implement the
numerical reports, apply the policy to the exact retained frame, and retain the
resulting evidence. The Stan model and estimation algebra are unchanged.
Structurally invalid retained designs now fail before fitting; finite support
limitations remain advisory. These diagnostics do not qualify an estimator for
production use.
Capability matrix
The following matrix records the implemented FE v1 boundary and the approved
RE and CRE scope. Implementation does not imply production qualification.
Capability
RE target
CRE v1 target
FE v1 target
Group structure
One or more existing hierarchy blocks
One selected CRE group within an otherwise valid hierarchy
Exactly one unit identifier
Panel keys
Existing hierarchy/date contract
Existing hierarchy/date contract
One unit key and one date key; retained unit-date pairs must be unique
Group effects
Existing Gaussian random intercepts and slopes
A random intercept is required in the selected group
Unit intercepts are removed by exact within contrasts
Random slopes
Existing support
Rejected in the selected CRE group; other hierarchy blocks retain RE semantics
Rejected
Balanced panels
Supported
Supported
Supported
Unbalanced panels
Supported
Supported
Supported when every retained unit has at least two dates
Internally transformed media
Existing support
Rejected when a CRE variable overlaps an internally transformed channel
Rejected
External numeric transforms
Supported
Supported as simple one-column main effects
Supported as simple one-column main effects
Factors and interactions
Existing formula contract
Allowed only when they do not involve CRE variables
Rejected in v1
Scaling
Existing global contract
Existing contract after retained-sample CRE augmentation
Existing global contract followed by within contrasts
Offsets
Existing hierarchy contract
Same as RE
Rejected in v1
MCMC
Supported
Supported
Supported
MAP
Existing support
Existing support
Rejected in v1
Level fitted values and prediction
Existing support
Existing support with the retained CRE lookup
Rejected in v1
New-group prediction
Existing hierarchy contract
Existing contract plus a fitted CRE lookup
Rejected
Time-series cross-validation
Existing supported cases
Supported only with fold-local CRE means
Rejected in v1
LOO, WAIC, and run comparison
Existing supported cases
Existing supported cases
Rejected because contrast rows are not original-observation likelihood terms
Decomposition and optimisation
Existing supported cases
Existing support; CRE mean terms are not causal media contributions
Rejected in v1
Random effects
Target estimator
RE remains the existing Gaussian hierarchical estimator. Group coefficients
are partially pooled through the declared random-effects blocks. This can be a
useful variance model when its conditional independence assumptions are
credible.
Retained support diagnostics
DSAMbayes retains diagnostics for each random-effects block:
group count and group-size distribution;
random-effects design dimensions and rank by group;
all-zero and non-finite random-effect columns;
within-group variation for random slopes; and
covariance dimension relative to available groups.
All-zero columns, non-finite matrices, fewer than two group levels, and invalid
dimensions are structural errors. The response is also rejected as a
random-effect design term or grouping key. Weak within variation, per-group
rank loss, and small group counts are advisory. Covariance dimension and the
groups-per-covariance-parameter ratio are reported without an identification
threshold. A group-constant random slope is not automatically described as
mathematically unidentified when between-group information may still inform
its covariance.
Assumption boundary
Plain RE does not protect population slopes when the random intercept is
correlated with included regressors. In that case, use CRE only if its narrower
conditional-mean specification is credible, or use another design.
Correlated random effects
Target estimator
CRE v1 is a random-intercept Mundlak specification. For each selected numeric
regressor, DSAMbayes adds its mean over the exact retained estimation rows for
the selected group. The coefficient on the original regressor is the within
slope. The coefficient on its group mean is the contextual difference. Their
sum is the between slope.
Supported scope
CRE v1 requires:
one explicitly selected hierarchy group;
a random intercept and no random slope in that selected group;
a CRE variable that maps to one simple numeric population-design column;
no overlap between that variable and an enabled internal media transform;
group means computed from the same retained rows and fitted covariate basis
used by Stan; and
holdout, cross-validation, and deployment lookups derived from the fitted
training frame only.
Other hierarchy blocks may retain their ordinary RE semantics. Repeated
selected-group/date pairs are allowed when another hierarchy level provides
multiple observations in the same period.
Rejected specifications and recovery
Rejected condition
Reason
Recovery
Random slope in the selected CRE group
Additive means do not correct correlated random slopes
Remove that random slope from the selected group or use RE and state its assumption
CRE variable uses an internal adstock or Hill transform
The fitted regressor changes with posterior parameters, so a raw-column mean is not the fitted-basis mean
Precompute and declare a fixed transformed numeric column, or omit CRE for that channel
CRE variable appears in a factor, interaction, spline, polynomial, or inline transform
The variable does not map to one unambiguous fitted column
Author a simple numeric column explicitly
Selected group has no random intercept
The approved Mundlak contract concerns correlation with a random intercept
Add a random intercept or do not request CRE
Generated mean name collides with a user column
Silent replacement would make the fitted basis ambiguous
Rename the user column or choose a non-conflicting CRE prefix
New group is absent from the fitted lookup
Its training group mean is undefined
Fit with the group represented or use an estimator with an approved new-group contract
Retained-sample rule
set_cre() will become configuration-only. At fit time, DSAMbayes will build
one explicit completeness and finiteness mask across the response, population
terms, random-effects terms, grouping keys, date, offset, and transformation
inputs. CRE means, default priors, design diagnostics, Stan data, and retained
lookups will all derive from that frozen frame. na.rm = TRUE will not define
the fitted group mean.
CRE does not correct omitted time-varying confounding or guarantee that the
conditional mean of the group effect is linear in the included means.
Fixed effects
Implemented estimator
FE v1 is a separate fixed_effects model class constructed with
fixed_effects(formula, data, unit). Before fitting, the caller must supply
exactly one date mapping through the existing set_date() lifecycle.
Surface
Status
Boundary
Direct R API fitting
Implemented
MCMC coefficient and residual-noise inference within the FE v1 contract
Runner configuration
Implemented
Use model.type: fe plus fixed_effects.unit; validation and dry-run do not compile or sample
Runner fitting and artefacts
Implemented, not qualified
Non-dry runs use the existing MCMC fit and dedicated coefficient, sampler, within-design, contrast-residual, and contrast posterior-predictive artefacts
Qualification
Technical gates passed; production qualification not complete
P4-J bounded runner, prior-only, one-replication recovery, and sampler gates passed; P5 repeated Monte Carlo remains separately gated
The non-dry YAML path uses the same estimator, prior, boundary, scaling, and
sampling contracts as the direct R API. It returns before generic level-scale
diagnostics, model selection, scenario analysis, optimisation, forecasting, or
deployment. A completed runner result has no publishability claim, and its
factual FE diagnostics summary records qualification_status: not_assessed.
For each retained unit, FE builds a deterministic orthonormal Helmert contrast
matrix Q_i and fits:
Q_i y_i ~ Normal(Q_i X_i beta, sigma)
This removes unrestricted unit intercepts without estimating shrinkage priors
for them. Unequal unit sizes are allowed. Singleton units, duplicate retained
unit-date keys, non-finite values, time-invariant regressors, and rank-deficient
within designs will abort before sampling.
Version 1 output contract
FE v1 supports slope and noise inference, within-design diagnostics, and
contrast-space posterior predictive checks. Its strongest possible release
label is production for coefficient inference only.
FE v1 rejects:
level-scale fitted() and predict() output;
conditional unit-intercept reconstruction;
unseen-unit prediction;
decomposition and counterfactual response;
budget optimisation;
time-series cross-validation;
pointwise LOO, WAIC, and run comparison;
MAP fitting; and
internal media transformations.
Contrast-space fitted values are diagnostics. They are not level-scale MMM
fitted values or predictions. Adding level outputs later requires a separate
posterior contract, linear oracle, coverage study, and counterfactual
specification.
The P4-G bounded synthetic integration fit proves only that the direct API can
compile, sample, extract its declared output, and survive a same-environment
RDS round-trip. P4-I proves the bounded runner orchestration and artefact
contract with a mocked Stan boundary. P4-J adds bounded live runner and
explicit-prior prior-only checks plus balanced and unbalanced coefficient,
noise, and sampler regression gates. All passed their predeclared technical
checks.
The two P4-J recovery fits are deterministic one-replication regression
evidence. They do not estimate repeated-sampling coverage or production
reliability. They also do not estimate bias, root mean squared error, failure
rates, or robustness. FE therefore remains implemented_not_qualified.
Repeated Monte Carlo evidence and its separate review remain P5 work.
Qualification requirements
Each estimator receives its own release decision. A package-wide pass cannot
hide a failed estimator cell.
Qualification requires:
deterministic design and algebra oracles;
typed structural failures before Stan compilation;
four-chain sampler diagnostics on every declared estimand and likelihood
scale parameter;
repeated paired Monte Carlo evidence under RE-valid, RE-invalid/CRE-valid,
unbalanced, weak-design, and misspecified scenarios;
conditional coverage and unconditional success-and-cover rates, so failed
fits remain visible;
Monte Carlo uncertainty for bias, coverage, and paired loss comparisons;
bounded prior-sensitivity evidence; and
human statistical and Stan review where model code changes.
Possible release labels are production, experimental, and unsupported.
Any failed core gate blocks production. An inconclusive stress cell narrows
the supported capability and remains visible in the evidence report.