Model¶
The model module provides the core BayesianMMM class for fitting
Marketing Mix Models using Bayesian inference.
Core Model¶
BayesianMMM - Main model class.
This module contains the BayesianMMM class which orchestrates model building, fitting, and prediction.
Identifiability note (equifinality, critique.md §3.6)¶
The core model uses logistic saturation (a single sat_lam per channel) by
default, honoring each channel’s configured SaturationConfig type
(logistic / hill / michaelis_menten / tanh / none) and,
by default, normalized adstock (AdstockConfig.normalize=True). Normalization
folds the total carryover magnitude into the channel coefficient beta, so a
channel’s adstock decay, saturation strength, and beta trade off against one
another: visibly different parameter combinations can fit the data almost
equally well. This is intrinsic to additive MMM, not a defect, but it means the
per-channel decomposition is only weakly identified from observational spend
alone. The framework’s primary remedy is experiment-calibrated coefficient
priors (mmm_framework.calibration), which anchor beta to randomized
evidence and thereby break the trade-off; weakly-informative priors and (for the
Hill path) data-anchored kappa bounds help at the margin. See
mmm_framework.transforms.adstock.adstock_weights() for the kernel-level
discussion.
- class mmm_framework.model.base.BayesianMMM(panel, model_config, trend_config=None, adstock_alphas=None, experiments=None, model_params=None)[source]¶
Bases:
objectBayesian Marketing Mix Model - Robust Implementation with Prediction Support.
This implementation prioritizes numerical stability: - All data is standardized before modeling - Adstock: by default a continuous in-graph kernel estimated per channel,
honoring each
MediaChannelConfig.adstock(geometric, delayed, or Weibull). SetModelConfig.use_parametric_adstock=Falsefor the legacy fast path (fixed-alpha bank blended via a learned mix) — the previous default, kept for reproducing older fits; deserialized pre-change models retain their original behavior.Saturation: logistic (
1 - exp(-lam * x)) by default – the most stable choice – honoring eachMediaChannelConfig.saturationtype per channel (logistic, hill, michaelis_menten, tanh, or none)Priors are carefully scaled for standardized data
Flexible trend modeling with GP, spline, and piecewise options
Support for prediction and counterfactual analysis
Save/load functionality for model persistence
Parameters¶
- panelPanelDataset
Panel data from MFFLoader.
- model_configModelConfig
Model configuration.
- trend_configTrendConfig, optional
Trend specification.
- adstock_alphaslist[float], optional
Fixed adstock decay values to pre-compute.
- CONFIG_SCHEMA: type[BaseModel] | None = None¶
Per-model config schema (Pydantic) declaring bespoke fields + defaults.
Noneon the base model (it is fully configured byModelConfig). A subclass / gardenCustomMMMsets this to aBaseModelsubclass so it can declare its own settable, defaulted, validated parameters (e.g. a binomial awareness model’snumber_of_trials); the agent/spec layer validatesspec["model_params"]against it and passes the result to themodel_paramsconstructor argument, where the model readsself.model_params.<field>in_build_model. Seetechnical-docs/custom-model-config.md.
- DATASET_SCHEMA: type[DatasetSchema] | None = None¶
Per-model data schema (a
DatasetSchemasubclass) declaring the role mapping a bespoke family expects, mirroringCONFIG_SCHEMAfor params.Noneon the base model (the default MMM roles: kpi→target, media→predictor, control→control). Seetechnical-docs/flexible-dataset.md.
- REQUIRED_ROLES: tuple[DatasetRole, ...] = ()¶
Dataset roles this model requires the data to provide. Enforced at construction (a clear
ValueErrorwhen missing) only when non-empty — the base model leaves it empty so existing flows are never gated. A family that genuinely needs e.g. a target + predictors declaresREQUIRED_ROLES = (DatasetRole.TARGET, DatasetRole.PREDICTOR).
- REQUIRED_DATASET_CAPABILITIES: tuple[str, ...] = ()¶
Optional duck-typed data-capability needs (e.g.
"HAS_INDICATORS","GEO_PANEL"), checked againstdataset_capabilities(). Enforced only when non-empty. Mirrors an estimand’srequired_capabilitiesgating.
- __init__(panel, model_config, trend_config=None, adstock_alphas=None, experiments=None, model_params=None)[source]¶
- static dataset_capabilities(ds)[source]¶
Cheap, duck-typed flags about the data (no graph build).
Mirrors
estimands.capabilities.model_capabilitiesbut for the dataset:GEO_PANEL(geo/product dimensions),HAS_INDICATORS(indicator columns),HAS_TRIALS(a binomial-denominator column).
- add_experiment_calibration(experiments)[source]¶
Register experiment likelihood terms and invalidate the built graph.
The experiments are folded into the model as likelihood terms the next time the graph is built (so call this before
fit()). Returnsselffor chaining.- Return type:
- compute_adstock_curves(l_max_default=8)[source]¶
Posterior-mean adstock kernel (lag weights) per channel.
Returns the actual estimated carryover shape for reporting, so a delayed or Weibull channel is rendered with its true (possibly humped) kernel rather than being forced to a geometric curve. Requires a fitted model; returns None before fitting.
- property model: pymc.Model¶
Get or build the PyMC model.
- get_prior(samples=500, random_seed=None)[source]¶
Sample from the prior distribution of the model.
- Return type:
InferenceData
- fit(draws=None, tune=None, chains=None, target_accept=None, random_seed=None, method=None, nuts_sampler=None, **kwargs)[source]¶
Fit the model.
By default this runs full NUTS MCMC.
method="smc"runs tempered Sequential Monte Carlo instead — also an exact sampler (MMMResults.approximatestaysFalse), slower than NUTS on well-behaved posteriors but robust to multimodality (it populates modes NUTS gets stuck in) and it estimates the log marginal likelihood (diagnostics["log_marginal_likelihood"]) for model comparison. The remaining methods are approximate — they fit in seconds and are intended for quickly checking a model (bad priors, broken geometry, pathological saturation/adstock) before committing to a full sample. Their uncertainty is not calibrated and convergence diagnostics (R-hat / ESS) do not apply, so do not use them for final inference. Seetechnical-docs/sampling-failure-playbook.mdfor which method to reach for when a fit fails.- Parameters:
draws (
int|None) – Posterior draws per chain (NUTS), particles per SMC run (SMC), or number of approximate draws to take from the fitted approximation. Default from config.tune (
int|None) – Number of tuning samples (NUTS only). Default from config.chains (
int|None) – Number of MCMC chains (NUTS) or independent SMC runs (SMC — R-hat is computed across them). Default from config.target_accept (
float|None) – Target acceptance rate for NUTS. Defaults tomodel_config.target_accept(itself 0.9 unless set viaModelConfigBuilder().with_target_accept(...)).method (
FitMethod|str|None) – Fit method —"nuts"(default, full MCMC),"smc"(Sequential Monte Carlo, exact),"map"(maximum a posteriori point),"laplace"(Gaussian approximation around the MAP point; via the declaredpymc-extrasdependency),"advi"/"fullrank_advi"(variational inference), or"pathfinder"(viapymc-extras— works out of the box). Defaults tomodel_config.fit_method.nuts_sampler (
str|None) – NUTS backend —"pymc","numpyro","nutpie"or"blackjax"(NUTS only; ignored by the other methods). Defaults tomodel_config.nuts_sampler, i.e. the sampler selected on the config viaModelConfigBuilder().bayesian_pymc()/.bayesian_numpyro()/.bayesian_nutpie().**kwargs – Additional arguments passed to the underlying sampler (
pm.samplefor NUTS,pm.sample_smcfor SMC).
- Return type:
- Returns:
Fitted model results with diagnostics. For approximate methods
MMMResults.approximateisTrue(NUTS and SMC:False).- Raises:
NotImplementedError – If the config selects an inference method with no estimator behind it. Currently none do — both frequentist methods dispatch (see below) — but the refusal stays as the mechanism for a future declared-before-implemented value.
UnsupportedModelError – For a frequentist config whose model is not linear given fixed transforms.
Note
Paradigm dispatch happens first.
inference_methodselects the estimation paradigm (Bayesian vs frequentist);method/FitMethodselects among the Bayesian estimators and is ignored by the frequentist path, which has no MAP/ADVI/SMC analogue. A frequentist fit returnsMMMResultswithapproximate=Falseandconverged is None, and stampsdiagnostics["inference_family"] = "frequentist"— every rendering surface branches on that, never onapproximate.
- predict(X_media=None, X_controls=None, return_original_scale=True, hdi_prob=0.94, random_seed=None)[source]¶
Generate predictions from the fitted model.
- Return type:
Parameters¶
- X_medianp.ndarray, optional
New media data for counterfactual. If None, uses training data.
- X_controlsnp.ndarray, optional
New control data. If None, uses training data.
- return_original_scalebool
If True, returns predictions in original scale.
- hdi_probfloat
HDI probability for uncertainty intervals.
- random_seedint, optional
Random seed for reproducibility.
Returns¶
- PredictionResults
Prediction results with samples and uncertainty bounds.
- compute_component_decomposition()[source]¶
Decompose predictions into component contributions.
- Return type:
Returns¶
- ComponentDecomposition
Full component breakdown in original scale.
- compute_counterfactual_contributions(time_period=None, channels=None, compute_uncertainty=True, hdi_prob=0.94, random_seed=None)[source]¶
Compute channel contributions using counterfactual analysis.
- Return type:
Parameters¶
- time_periodtuple[int, int], optional
Time period (start_idx, end_idx) for contribution calculation.
- channelslist[str], optional
List of channel names to compute contributions for.
- compute_uncertaintybool
If True, computes HDI for contributions.
- hdi_probfloat
Probability mass for HDI calculation.
- random_seedint, optional
Random seed for reproducibility.
Returns¶
- ContributionResults
Container with per-observation and total contributions.
- compute_marginal_contributions(spend_increase_pct=10.0, time_period=None, channels=None, compute_uncertainty=True, hdi_prob=0.94, random_seed=None)[source]¶
Compute marginal contributions for a given spend increase.
When
compute_uncertaintyis True the marginal contribution and marginal ROAS are propagated through the full posterior (saturation and coefficient uncertainty included) and the returned frame gains HDI columns. This addresses critique.md §3.9: the headline efficiency number should never be reported as a bare point estimate.Uncertainty requires the baseline and increased posterior-predictive draws to be paired so the sampled observation noise cancels in their per-draw difference; a single shared seed is used to guarantee that.
- Return type:
- predict_under(intervention, time_period=None, random_seed=None)[source]¶
Posterior-predictive outcome under a counterfactual
intervention.A thin wrapper over
predict()that transformsX_media_rawper the intervention.time_periodis accepted for interface symmetry (mmm_framework.estimands.spec.SupportsEstimands); windowing is applied by the estimand reducer, so the full series is returned here.- Return type:
- evaluate_estimands(estimands=None, *, random_seed=None)[source]¶
Realize estimands from the fitted posterior as mean + HDI.
estimandsitems may beEstimandinstances, built-in names (resolved via the registry), or serialized dicts. Withestimands=Noneusesdeclared_estimandsif non-empty, else_default_estimands(). Returns a dict keyed by estimand name (wildcard-channel estimands expand to"{name}:{channel}"). Never raises for an unsupported estimand – it is returned withstatus="unsupported".- Return type:
- what_if_scenario(spend_changes, time_period=None, random_seed=None, compute_uncertainty=True, hdi_prob=0.94, max_draws=200)[source]¶
Run a what-if scenario with custom spend changes.
With
compute_uncertainty(default), the outcome change carries DECISION uncertainty: the per-draw change in total media contribution over the window is evaluated from PAIRED posterior draws (same draws for baseline and scenario, no observation noise — the machinery the budget optimizer uses), yieldingoutcome_change_hdiandprob_positive= P(scenario beats baseline). A point estimate alone is not decision-grade in a budget meeting.- Return type:
- sample_channel_contributions(X_media=None, max_draws=None, random_seed=None)[source]¶
Posterior draws of per-channel contributions under a media scenario.
Evaluates the
channel_contributionsdeterministic withX_mediaswapped in (training data whenNone), returning ORIGINAL-scale contributions of shape(n_draws, n_obs, n_channels). Because the model is additive in channels, a scenario that changes every channel at once still yields each channel’s own response — this is what makes budget-response curves cheap (one pass per spend level, not per channel × level).max_drawsthins the trace evenly to at most that many draws per chain-flattened posterior — response-curve grids don’t need the full posterior.Raises
NotImplementedErroron a multiplicative specification: the additivity this method relies on holds on the LOG scale there, so the returned numbers are not original-scale contributions.- Return type:
- sample_latent_under(var_name, intervention=None, random_seed=None)[source]¶
Posterior draws of a named deterministic
var_nameunder a counterfactualinterventionon the media inputs.Generalizes
sample_channel_contributions()to any registered deterministic:set_dataswaps in the intervention-transformed media (training media forObserved/None), then re-evaluatesvar_namevia posterior-predictive. Returned shape is(n_draws, *var_shape)in the deterministic’s native (model) scale — the engine forms latent contrasts (intervention − baseline) from this, so any constant scale cancels. This is what powers latent-variable estimand contrasts (seemmm_framework.estimands.evaluate).- Return type:
- sample_prior_predictive(samples=500, random_seed=None)[source]¶
Sample from prior predictive distribution.
- Return type:
InferenceData
- compute_parameter_learning(var_names=None, *, prior_samples=1000, random_seed=None, **kwargs)[source]¶
Quantify how much the data updated each parameter relative to its prior.
Draws prior samples and compares them to the fitted posterior, returning the prior-to-posterior contraction, overlap, and location shift for every parameter – the honest way to tell whether a posterior reflects the data or merely re-states an informative/constrained prior. See
mmm_framework.diagnostics.parameter_learning().- Return type:
Parameters¶
- var_names:
Parameters to diagnose.
None(default) uses the model’s free random variables (beta_*,sat_lam_*,adstock_*,intercept, …).- prior_samples:
Number of prior draws used to estimate the prior moments/overlap.
- random_seed:
Seed for the prior draw (reproducibility).
- **kwargs:
Forwarded to
parameter_learning()(e.g.bins,c_strong,c_weak,ovl_dominated).
Returns¶
- pandas.DataFrame
One row per parameter, sorted by
contractionascending (most prior-dominated first).
Results¶
Result containers for BayesianMMM.
This module contains the dataclasses used to return results from model fitting, prediction, and contribution analysis.
- class mmm_framework.model.results.MMMResults(trace, model, panel, channel_contributions=None, diagnostics=<factory>, y_mean=0.0, y_std=1.0, estimands=<factory>, approximate=False)[source]¶
Bases:
objectContainer for fitted model results.
-
trace:
InferenceData¶
-
model:
Model¶
-
panel:
PanelDataset¶
-
estimands:
dict[str,EstimandResult]¶
- property converged: bool | None¶
MCMC convergence verdict (R-hat / ESS / divergences).
True/Falsefor NUTS fits;Nonewhen not assessable (an approximate MAP/ADVI fit, or no usable diagnostics).Noneis NOT “converged” – surface it as “N/A”. Do not act on intervals/ROI from a fit where this isFalse.
- property convergence_flags: list[str]¶
subset of
{divergences, rhat, ess}.- Type:
Which convergence checks failed
- __init__(trace, model, panel, channel_contributions=None, diagnostics=<factory>, y_mean=0.0, y_std=1.0, estimands=<factory>, approximate=False)¶
-
trace:
- class mmm_framework.model.results.PredictionResults(posterior_predictive, y_pred_mean, y_pred_std, y_pred_hdi_low, y_pred_hdi_high, y_pred_samples)[source]¶
Bases:
objectContainer for prediction results.
-
posterior_predictive:
InferenceData¶
- __init__(posterior_predictive, y_pred_mean, y_pred_std, y_pred_hdi_low, y_pred_hdi_high, y_pred_samples)¶
-
posterior_predictive:
- class mmm_framework.model.results.ContributionResults(channel_contributions, total_contributions, contribution_pct, baseline_prediction, counterfactual_predictions, time_period=None, contribution_hdi_low=None, contribution_hdi_high=None)[source]¶
Bases:
objectContainer for channel contribution results.
These results are computed via counterfactual analysis: contribution = prediction(all channels) - prediction(channel zeroed out)
Attributes¶
- channel_contributionspd.DataFrame
Per-observation contributions, shape (n_obs, n_channels). Index matches the original panel index.
- total_contributionspd.Series
Total contribution by channel (summed over time).
- contribution_pctpd.Series
Contribution as percentage of total.
- baseline_predictionnp.ndarray
Prediction with all channels present.
- counterfactual_predictionsdict[str, np.ndarray]
Counterfactual predictions with each channel zeroed out.
- time_periodtuple[int, int] | None
Time period used for calculation, if any.
- contribution_hdi_lowpd.Series | None
Lower HDI for total contributions (if uncertainty computed).
- contribution_hdi_highpd.Series | None
Upper HDI for total contributions (if uncertainty computed).
- __init__(channel_contributions, total_contributions, contribution_pct, baseline_prediction, counterfactual_predictions, time_period=None, contribution_hdi_low=None, contribution_hdi_high=None)¶
- class mmm_framework.model.results.ComponentDecomposition(intercept, trend, seasonality, media_total, media_by_channel, controls_total, controls_by_var, geo_effects, product_effects, total_intercept, total_trend, total_seasonality, total_media, total_controls, total_geo, total_product, y_mean, y_std, events=None, total_events=None, interactions=None, total_interactions=None, levers=None, total_levers=None)[source]¶
Bases:
objectContainer for full component decomposition results.
- __init__(intercept, trend, seasonality, media_total, media_by_channel, controls_total, controls_by_var, geo_effects, product_effects, total_intercept, total_trend, total_seasonality, total_media, total_controls, total_geo, total_product, y_mean, y_std, events=None, total_events=None, interactions=None, total_interactions=None, levers=None, total_levers=None)¶
Trend Configuration¶
Trend configuration classes for BayesianMMM.
This module contains the TrendType enum and TrendConfig dataclass used to configure trend components in the model.
- class mmm_framework.model.trend_config.TrendType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
-
Available trend specifications.
- NONE = 'none'¶
- LINEAR = 'linear'¶
- PIECEWISE = 'piecewise'¶
- SPLINE = 'spline'¶
- GP = 'gaussian_process'¶
- class mmm_framework.model.trend_config.TrendConfig(type=TrendType.LINEAR, n_changepoints=10, changepoint_range=0.8, changepoint_prior_scale=0.5, n_knots=10, spline_degree=3, spline_prior_sigma=1.0, gp_lengthscale_prior_mu=0.3, gp_lengthscale_prior_sigma=0.2, gp_amplitude_prior_sigma=0.5, gp_n_basis=20, gp_c=1.5, growth_prior_mu=0.0, growth_prior_sigma=0.5)[source]¶
Bases:
objectConfiguration for trend component.
- Scale notes:
Trends enter the model on standardized
y(z-scored) against time scaled tot in [0, 1], so a slope of 1.0 means the trend movesyby one standard deviation over the whole series. Default prior widths are chosen so that a realistic trend spanning ~1-2 sd ofysits within ~1-2 prior sd:growth_prior_sigma=0.5for the linear slope, andchangepoint_prior_scale=0.5(Laplace scale of each Prophet-style slope change) for piecewise. The old, much tighter defaults (0.1 / 0.05) effectively pinned the trend near zero and pushed real trend/structural breaks into media and intercept.
- type¶
Type of trend to use.
- n_changepoints¶
Number of potential changepoints for piecewise trend (Prophet-style).
- changepoint_range¶
Proportion of time range to place changepoints (0-1).
- changepoint_prior_scale¶
Prior scale for changepoint magnitudes.
- n_knots¶
Number of knots for spline trend.
- spline_degree¶
Degree of B-spline (default 3 = cubic).
- spline_prior_sigma¶
Prior sigma for spline coefficients.
- gp_lengthscale_prior_mu¶
Prior mean for GP lengthscale (in proportion of time range).
- gp_lengthscale_prior_sigma¶
Prior sigma for GP lengthscale.
- gp_amplitude_prior_sigma¶
Prior sigma for GP amplitude (HalfNormal).
- gp_n_basis¶
Number of basis functions for HSGP approximation.
- gp_c¶
Boundary factor for HSGP (typically 1.5-2.0).
- growth_prior_mu¶
Prior mean for linear growth rate.
- growth_prior_sigma¶
Prior sigma for linear growth rate.
- __init__(type=TrendType.LINEAR, n_changepoints=10, changepoint_range=0.8, changepoint_prior_scale=0.5, n_knots=10, spline_degree=3, spline_prior_sigma=1.0, gp_lengthscale_prior_mu=0.3, gp_lengthscale_prior_sigma=0.2, gp_amplitude_prior_sigma=0.5, gp_n_basis=20, gp_c=1.5, growth_prior_mu=0.0, growth_prior_sigma=0.5)¶
Trend Components¶
Trend component strategies for BayesianMMM.
This module implements the Strategy pattern for building different types of trend components in the model.
- class mmm_framework.model.components.trend.TrendStrategy(*args, **kwargs)[source]¶
Bases:
ProtocolProtocol for trend building strategies.
- build(model, data, config)[source]¶
Build the trend component.
- Return type:
TensorVariable
Parameters¶
- modelpm.Model
The PyMC model context.
- dataPreparedData
Prepared data containing trend features.
- configTrendConfig
Trend configuration parameters.
Returns¶
- pt.TensorVariable
Trend component tensor, shape (n_obs,).
- __init__(*args, **kwargs)¶
- class mmm_framework.model.components.trend.BaseTrendStrategy[source]¶
Bases:
ABCBase class for trend strategies with common utilities.
- class mmm_framework.model.components.trend.LinearTrendStrategy[source]¶
Bases:
BaseTrendStrategySimple linear trend: trend = k * t.
- class mmm_framework.model.components.trend.PiecewiseTrendStrategy[source]¶
Bases:
BaseTrendStrategyProphet-style piecewise linear trend with changepoints.
- The trend is modeled as:
trend(t) = (k + A @ delta) * t + (m + A @ (-s * delta))
Where: - k is the base growth rate - delta are the changepoint adjustments - A is the changepoint indicator matrix - s are the changepoint locations - m is the offset
- class mmm_framework.model.components.trend.SplineTrendStrategy[source]¶
Bases:
BaseTrendStrategyB-spline trend with random walk prior on coefficients.
This provides smooth, flexible trends without explicit changepoints.
- class mmm_framework.model.components.trend.GPTrendStrategy[source]¶
Bases:
BaseTrendStrategyGaussian Process trend using HSGP (Hilbert Space GP) approximation.
This provides non-parametric, smooth trends with uncertainty quantification, while remaining computationally efficient.
- class mmm_framework.model.components.trend.TrendBuilder[source]¶
Bases:
objectFactory for building trend components.
Selects the appropriate strategy based on TrendType.
- STRATEGIES = {'gp': <class 'mmm_framework.model.components.trend.GPTrendStrategy'>, 'linear': <class 'mmm_framework.model.components.trend.LinearTrendStrategy'>, 'none': None, 'piecewise': <class 'mmm_framework.model.components.trend.PiecewiseTrendStrategy'>, 'spline': <class 'mmm_framework.model.components.trend.SplineTrendStrategy'>}¶
- classmethod build(model, data, config)[source]¶
Build trend component using appropriate strategy.
- Return type:
pt.TensorVariable | None
Parameters¶
- modelpm.Model
PyMC model context.
- dataPreparedData
Prepared data with trend features.
- configTrendConfig
Trend configuration.
Returns¶
- pt.TensorVariable | None
Trend component, or None if trend_type is NONE.