Configuration

The configuration module provides Pydantic-based configuration classes for defining model structure, variables, and transformations.

Core Configuration

Configuration classes for flexible MMM framework.

Handles variable-dimension MFF data with configurable KPI, media, and control specifications. Uses Pydantic for validation and type safety.

This subpackage is organized by concern:

  • enums — dimension/role/transform/prior/inference enumerations

  • priorsPriorConfig

  • transformsAdstockConfig, SaturationConfig

  • variables — per-variable configs (KPI, media, control)

  • mff — MFF column mappings, dimension alignment, MFFConfig

  • model — model-level configuration (ModelConfig and friends)

  • factories — convenience builders for common configurations

All public names remain importable directly from mmm_framework.config for backwards compatibility.

class mmm_framework.config.DimensionType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Standard MFF dimensions.

PERIOD = 'Period'
GEOGRAPHY = 'Geography'
PRODUCT = 'Product'
CAMPAIGN = 'Campaign'
OUTLET = 'Outlet'
CREATIVE = 'Creative'
class mmm_framework.config.VariableRole(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Role of a variable in the model.

KPI = 'kpi'
MEDIA = 'media'
CONTROL = 'control'
AUXILIARY = 'auxiliary'
class mmm_framework.config.CausalControlRole(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Causal role of a control variable, used to prevent “bad control” bias.

The functional VariableRole (CONTROL) says a variable is a regressor; it says nothing about why it is in the model. Conditioning on the wrong kind of variable induces bias rather than removing it (see the confounder-vs-precision-control proof in the project README). This enum lets the framework distinguish the four causal kinds so it can route each one correctly:

  • CONFOUNDER: a common cause of media and the KPI. Must be included and must not be shrunk toward zero (shrinking a confounder biases the media coefficient). Routed to a wide, un-shrunk coefficient prior.

  • PRECISION_CONTROL: a cause of the KPI only (not of media). Safe to include for efficiency and safe to shrink/select. This is the default, backward-compatible behavior for any unmarked control.

  • MEDIATOR: lies on the causal path media -> … -> KPI (post-treatment). Conditioning on it blocks part of the effect being estimated, so it must not be used as a control for a total-effect estimate.

  • COLLIDER: a common effect of two causes on a backdoor path. Conditioning on it opens a spurious path, so it must not be used as a control.

MEDIATOR and COLLIDER are refused at model-construction time. A control left unmarked (None) is treated as PRECISION_CONTROL for backward compatibility.

CONFOUNDER = 'confounder'
PRECISION_CONTROL = 'precision_control'
MEDIATOR = 'mediator'
COLLIDER = 'collider'
class mmm_framework.config.MeasurementUnit(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

How a media channel’s modeled variable is measured.

This drives how the channel’s efficiency is reported, not how its response curve is fit (the curve is always fit on the modeled variable):

  • SPEND (the default, backward-compatible): the modeled variable is dollars spent, so ROI / marginal ROAS divide the channel’s incremental KPI by the summed spend exactly as before.

  • IMPRESSIONS / CLICKS: the modeled variable is a volume, not dollars. ROI cannot be formed by summing the variable. If a cost is known (a separate spend_column, or a cpm / cpc constant) the volume is converted to a spend series and normal ROI / mROAS are reported; otherwise the framework reports efficiency instead — incremental KPI per 1,000 impressions (or per click) and the marginal efficiency of an extra 1,000 impressions (or click), whose break-even reference is 0, not 1.0.

  • OTHER: a volume with no natural per-1,000 unit; treated like IMPRESSIONS for efficiency but labeled per unit.

See mmm_framework.config.variables.MediaChannelConfig for the companion spend_column / cpm / cpc fields and mmm_framework.reporting.helpers.measurement for the resolver that turns this into a divisor + metric labels.

SPEND = 'spend'
IMPRESSIONS = 'impressions'
CLICKS = 'clicks'
OTHER = 'other'
property is_spend: bool
class mmm_framework.config.AdstockType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Supported adstock transformations.

GEOMETRIC = 'geometric'
WEIBULL = 'weibull'
DELAYED = 'delayed'
NONE = 'none'
class mmm_framework.config.SaturationType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Supported saturation transformations.

Note on completeness: two forms practitioners sometimes name separately are already available here under different names — the ADBUDG S-curve x**s / (k**s + x**s) is exactly HILL, and the exponential-CDF form 1 - exp(-lam * x) is exactly LOGISTIC. ROOT (power) is the genuinely distinct concave form the others don’t cover.

HILL = 'hill'
LOGISTIC = 'logistic'
MICHAELIS_MENTEN = 'michaelis_menten'
TANH = 'tanh'
ROOT = 'root'
NONE = 'none'
class mmm_framework.config.PriorType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Supported prior distributions.

HALF_NORMAL = 'HalfNormal'
NORMAL = 'Normal'
LOG_NORMAL = 'LogNormal'
GAMMA = 'Gamma'
BETA = 'Beta'
TRUNCATED_NORMAL = 'TruncatedNormal'
HALF_STUDENT_T = 'HalfStudentT'
class mmm_framework.config.AllocationMethod(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Methods for allocating national data to sub-dimensions.

EQUAL = 'equal'
POPULATION = 'population'
SALES = 'sales'
CUSTOM = 'custom'
class mmm_framework.config.FitMethod(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

How the posterior is obtained at fit time.

Two methods are exact (asymptotically unbiased posterior samplers):

  • NUTS — full gradient MCMC, the default and the workhorse for final inference.

  • SMC — tempered Sequential Monte Carlo (pm.sample_smc). Slower than NUTS on well-behaved posteriors, but it handles multimodal posteriors NUTS gets mode-locked in (reflected factor modes, label switching, adstock↔AR ridges) and yields a log marginal likelihood for model comparison. Use it to confirm suspected multimodality or to compute model evidence — not as a speedup.

The remaining methods are approximate and exist to fit a model in seconds so you can spot problems — bad priors, broken geometry, pathological saturation/adstock — before paying for a full sample. Treat their uncertainty as unreliable. MAP is a point estimate; LAPLACE adds a Gaussian curvature approximation around the MAP point (cheap uncertainty, better-behaved than bare MAP on high-dimensional models); ADVI/FULLRANK_ADVI are variational; PATHFINDER is quasi-Newton variational (via the declared pymc-extras dependency, like LAPLACE).

NUTS = 'nuts'
SMC = 'smc'
MAP = 'map'
LAPLACE = 'laplace'
ADVI = 'advi'
FULLRANK_ADVI = 'fullrank_advi'
PATHFINDER = 'pathfinder'
property is_approximate: bool

True for the fast-check methods whose uncertainty is not calibrated.

NUTS and SMC are exact samplers — SMC must not trip the “approximate fit — uncertainty is not calibrated” banners/report gating even though it is not NUTS.

class mmm_framework.config.InferenceMethod(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Available inference methods.

The three Bayesian entries differ only in the NUTS backend used to sample the same graph (ModelConfig.nuts_sampler maps them onto the pm.sample(nuts_sampler=...) value): reference PyMC, JAX/NumPyro, or the Rust nutpie sampler.

BAYESIAN_PYMC = 'bayesian_pymc'
BAYESIAN_NUMPYRO = 'bayesian_numpyro'
BAYESIAN_NUTPIE = 'bayesian_nutpie'
FREQUENTIST_RIDGE = 'frequentist_ridge'
FREQUENTIST_CVXPY = 'frequentist_cvxpy'
class mmm_framework.config.ModelSpecification(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Model functional form.

ADDITIVE = 'additive'
MULTIPLICATIVE = 'multiplicative'
class mmm_framework.config.LikelihoodFamily(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Observation (likelihood) family for the KPI.

NORMAL is the historical default and the only family the built-in additive model fits directly on standardized y (all of its component priors are calibrated in KPI standard deviations). STUDENT_T is a safe, heavier-tailed drop-in on the same standardized, identity-link scale.

The remaining families (LOGNORMAL and the count/bounded BINOMIAL/BETA_BINOMIAL/POISSON/NEGATIVE_BINOMIAL/BETA) change the observation scale and require a non-identity link, so the additive model does not fit them directly — they are read by models that define their own observation block (override _build_model / subclass CustomMMM), e.g. a binomial awareness model. is_gaussian / standardizes_y route this in LikelihoodConfig and the model.

NORMAL = 'normal'
STUDENT_T = 'student_t'
LOGNORMAL = 'lognormal'
BINOMIAL = 'binomial'
BETA_BINOMIAL = 'beta_binomial'
POISSON = 'poisson'
NEGATIVE_BINOMIAL = 'negative_binomial'
BETA = 'beta'
property is_gaussian: bool

Identity-link, continuous families the additive model fits directly on standardized y (the built-in dispatch supports these).

property standardizes_y: bool

Whether y is z-scored before entering the graph. Count/bounded families work in their natural (link) scale and are not standardized; LOGNORMAL is fit on standardized log(y) upstream, so the in-graph observation is still standardized.

class mmm_framework.config.LinkFunction(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Link mapping the linear predictor mu to the observation’s natural parameter. IDENTITY for Gaussian families; LOGIT for bounded proportions (binomial/beta); LOG for counts (Poisson/neg-binomial).

IDENTITY = 'identity'
LOGIT = 'logit'
LOG = 'log'
class mmm_framework.config.DatasetRole(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Role of a column in a Dataset.

The first four members map 1:1 to VariableRole; the rest generalize the container to non-MMM families. str-valued so a role serializes as its plain string ("indicator") in JSON specs.

TARGET = 'target'
PREDICTOR = 'predictor'
CONTROL = 'control'
INDICATOR = 'indicator'
GROUP = 'group'
TIME = 'time'
OFFSET = 'offset'
WEIGHT = 'weight'
TRIALS = 'trials'
AUXILIARY = 'auxiliary'
class mmm_framework.config.DatasetSchema(**data)[source]

Bases: BaseModel

A declarative role mapping for a Dataset.

This is the base, family-agnostic declaration. A bespoke model family extends its data contract by subclassing this and assigning it to the model’s DATASET_SCHEMA class attribute — the runtime then coerces/validates the incoming data against it (mirroring CONFIG_SCHEMA + model_params).

SCHEMA_VERSION: str

Class-level version constant (read by the serializer, as CONFIG_SCHEMA exposes SCHEMA_VERSION). Subclasses may bump it independently.

bindings: list[RoleBinding]
time_col: str
group_cols: list[str]
date_format: str
frequency: Literal['W', 'D', 'M']
schema_version: str
names_for(role)[source]

Column names bound to role (in declaration order).

Return type:

list[str]

property target_names: list[str]
property predictor_names: list[str]
property control_names: list[str]
property indicator_names: list[str]
binding(name)[source]

The RoleBinding for column name (or None).

Return type:

RoleBinding | None

classmethod from_mff(mff)[source]

Build a DatasetSchema from an MFFConfig.

Maps kpi→target, media→predictor, control→control, auxiliary→auxiliary, preserving each variable’s dimensions. The group columns are inferred from whether the KPI is defined over geography / product.

Return type:

DatasetSchema

to_mff()[source]

Best-effort reconstruction of an MFFConfig from this schema.

Lossless for the MMM roles; non-MMM roles (indicator/offset/…) are dropped from the MMM view. Used only as the fall-back config when a Dataset is adapted as_panel() with no original MFFConfig attached. When a real MFFConfig is present it is preserved verbatim, so this never runs on the MMM path.

Return type:

MFFConfig

class mmm_framework.config.RoleBinding(**data)[source]

Bases: BaseModel

One column → its DatasetRole plus per-role metadata.

Generalizes VariableConfig. The open meta dict carries family-specific knobs (e.g. {"binarize_threshold": 0.5} for an indicator, {"n_trials_col": "..."} for trials) without a schema change — the same spirit as LikelihoodConfig.params.

name: str
role: DatasetRole
dimensions: list[DimensionType]
display_name: str | None
unit: str | None
meta: dict[str, Any]
class mmm_framework.config.PriorConfig(**data)[source]

Bases: BaseModel

Configuration for a prior distribution.

distribution: PriorType
params: dict[str, float]
dims: str | list[str] | None
classmethod half_normal(sigma=1.0, dims=None)[source]
Return type:

PriorConfig

classmethod gamma(alpha=2.0, beta=1.0, dims=None)[source]
Return type:

PriorConfig

classmethod beta(alpha=2.0, beta=2.0, dims=None)[source]
Return type:

PriorConfig

class mmm_framework.config.EventSpec(**data)[source]

Bases: BaseModel

One named event (or recurring holiday).

Exactly one of dates (explicit) or holiday (a named holiday looked up from the country calendar) identifies when the event occurs.

name: str
dates: list[str] | None

Explicit event dates (any pandas-parseable date strings), e.g. a launch.

holiday: str | None

A named holiday from the config’s country calendar (e.g. “Christmas Day”, “Thanksgiving”). Case-insensitive substring match on the calendar’s holiday names. Recurs every year in the data window.

pre_weeks: int

Weeks of “shoulder” before / after the event that also carry an effect (0 = the event period only).

post_weeks: int
decay: float

Geometric decay applied across the shoulder periods (0 = flat 1.0 over the whole window; 0.5 = each step away from the peak is half as strong).

prior_sigma: float | None

Prior sigma for this event’s coefficient (overrides the group default).

class mmm_framework.config.EventsConfig(**data)[source]

Bases: BaseModel

A set of holiday / event regressors to add to the model.

Off by default (ModelConfig.events is None). Effects enter as an additive event_component distinct from the Fourier seasonality_component, so they do not double-count.

country: str | None

Country code for named-holiday lookups (e.g. “US”, “GB”). Requires the optional holidays package. None ⇒ only custom_events are used.

holidays: list[str]

Which of the country calendar’s holidays to include as regressors. Empty ⇒ every holiday in the calendar (one regressor per distinct holiday name).

custom_events: list[EventSpec]

User-defined events (launches, PR moments, or overrides of a named holiday with custom windows/decay).

prior_sigma: float

Default coefficient prior sigma for events without their own.

holiday_pre_weeks: int

Shoulder weeks / decay applied to the country-holiday regressors.

holiday_post_weeks: int
holiday_decay: float
is_empty()[source]
Return type:

bool

class mmm_framework.config.ChannelInteraction(**data)[source]

Bases: BaseModel

One named channel-pair interaction term.

expected_sign encodes the belief: "positive" (synergy) uses a HalfNormal so the effect can only lift; "negative" (cannibalization) its reflection; "any" a Normal centered at 0. Either way the prior shrinks toward zero, since interactions are weakly identified without designed variation.

channel_a: str
channel_b: str
prior_sigma: float
expected_sign: Literal['positive', 'negative', 'any']
property name: str
class mmm_framework.config.PriceConfig(**data)[source]

Bases: BaseModel

A price lever (log-price elasticity with an optional reference / gap).

variable: str

Name of the price column (a control variable in the data).

reference: float | Literal['mean', 'median', 'max'] | None

Reference / regular price the model responds to the gap from. A float is an absolute reference; "mean" / "median" / "max" derive it from the data (median ≈ the usual “regular” price). None uses log(price) with the constant absorbed by the intercept (still a valid elasticity).

elasticity_prior_sigma: float

Prior sigma of the (sign-guarded, ≤ 0) elasticity coefficient.

class mmm_framework.config.PromoConfig(**data)[source]

Bases: BaseModel

A promotion lever (lift with its own carryover).

variable: str

a discount % or an event flag. Normalized by its max before adstock.

Type:

Name of the promo column (a control variable)

adstock_lmax: int

Max carryover lag (weeks) for the promo’s own geometric adstock. <= 1 ⇒ no carryover (an instantaneous lift).

lift_prior_sigma: float

Prior sigma of the (non-negative) promo lift coefficient.

allow_negative: bool

Allow a negative promo effect (e.g. a pull-forward that later depresses sales). Default False ⇒ a HalfNormal lift ≥ 0.

class mmm_framework.config.FrequencyResponse(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Shape of the frequency-saturation curve g(f) (f = avg frequency, normalized by its mean so the shape prior is scale-free).

EXPONENTIAL = 'exponential'

g(f) = 1 - exp(-k f) — diminishing returns from the first exposure, monotone, asymptotes to 1. The safe planner default.

HILL = 'hill'

g(f) = f^s / (f^s + h^s) — S-shaped, models a minimum effective frequency threshold (low frequency is nearly wasted, then it kicks in).

class mmm_framework.config.ReachFrequencyConfig(**data)[source]

Bases: BaseModel

Declare a channel as reach modulated by a frequency-saturation curve.

The named channel’s media column is treated as reach. The frequency_column is a per-period average-frequency series supplied in the control block — it is pulled out (not double-counted as a linear control) and used only to build the frequency gain. The channel’s effect becomes beta · sat(adstock(reach · g(frequency))) — the standard media pipeline with the input re-mixed to effective reach.

Off by default (ModelConfig.reach_frequency = []): a channel is an ordinary volume channel, byte-identical to today (R0.1/R0.2).

channel: str

Media channel whose column is reach (must be a modeled channel).

frequency_column: str

Per-period average-frequency series (must be a control column).

response: FrequencyResponse

Frequency-saturation shape.

frequency_prior_scale: float

Prior scale on the shape parameter (k for exponential; the slope / half-saturation priors for Hill). Larger ⇒ faster frequency saturation a priori.

class mmm_framework.config.LikelihoodConfig(**data)[source]

Bases: BaseModel

Observation family + link + family parameters for the KPI likelihood.

Parameters

family:

Observation family. Default NORMAL (historical behavior).

link:

Link mapping the linear predictor mu to the family’s natural parameter. None (default) resolves to the family’s canonical link.

params:

Family parameters. student_t accepts nu (degrees of freedom); negative_binomial accepts alpha. binomial/beta_binomial may carry n_trials (a positive int, or a per-observation column name) here, but a custom model is free to source it from its own CONFIG_SCHEMA instead (e.g. an awareness model’s number_of_trials) — the family declares the scale/observation type (which drives whether y is standardized), while bespoke counts can live in model_params. Unknown keys are passed through for a model to interpret.

family: LikelihoodFamily
params: dict[str, Any]
classmethod normal()[source]

The default Gaussian likelihood (identity link, standardized y).

Return type:

LikelihoodConfig

classmethod student_t(nu=4.0)[source]

Heavier-tailed Gaussian-scale likelihood (robust to outliers).

Return type:

LikelihoodConfig

classmethod binomial(n_trials)[source]

Binomial likelihood (logit link). n_trials is the per-observation denominator — a positive int or a trials-column name. For models that define their own observation block (e.g. an awareness model).

Return type:

LikelihoodConfig

property is_gaussian: bool
property standardizes_y: bool
class mmm_framework.config.AdstockConfig(**data)[source]

Bases: BaseModel

Configuration for adstock transformation.

type: AdstockType
l_max: int
normalize: bool
alpha_prior: PriorConfig | None
theta_prior: PriorConfig | None
shape_prior: PriorConfig | None
scale_prior: PriorConfig | None
classmethod geometric(l_max=8, alpha_prior=None)[source]

Geometric (monotonic, peak at lag 0) carryover.

Return type:

AdstockConfig

classmethod delayed(l_max=8, alpha_prior=None, theta_prior=None)[source]

Delayed geometric carryover with the peak effect at lag theta.

Uses the Jin et al. (2017) kernel w_k = alpha ** ((k - theta) ** 2), which can represent media whose effect builds before it decays.

Return type:

AdstockConfig

classmethod weibull(l_max=12, shape_prior=None, scale_prior=None)[source]

Weibull (PDF) carryover: a flexible, possibly delayed decay shape.

Shape k > 1 yields a delayed peak; k == 1 is exponential; k < 1 is front-loaded. Scale lambda spreads the mass over lags.

The default scale prior adapts to the lag window: it is Gamma(alpha=2.0, beta=2.0 / m) with prior mean m = max(2.0, (l_max - 9.0) / 2.0) lag units (weeks for weekly data), i.e. the legacy mean-2 prior for windows up to 13 lags, then half the window beyond ~9 lags (mean 8.5 at l_max=26). Rationale, measured on the synth stress harness: :rtype: AdstockConfig

  • A fixed-mean prior (the old Gamma(2, 1), mean 2) puts nearly all its mass in the first few lags, so a long window (l_max=26) forces the sampler to fight the prior whenever the true kernel is slow – measured as a divergence storm (71 divergences, r-hat 1.07) on a scale-6-9 truth. With this rule (mean 8.5 at l_max=26) the same fit sampled cleanly (0-1 divergences, r-hat < 1.03 across seeds) and recovered contributions on par with a hand-informed prior under the same protocol.

  • The growth is offset, not proportional (l_max / 3 was also tested): media flighting is often periodic at 7-13 weeks, and a mid-size window (l_max=12) whose scale prior puts real mass at those delays lets one chain settle into an aliased kernel (delayed by one flighting period; r-hat ~1.8). Keeping the prior mean at the legacy value for short/mid windows avoids that mode while long windows still get the slow-carryover mass they need.

The shape prior stays Gamma(2, 1) – shape is dimensionless and window-independent (a tighter Gamma(3, 1.5) was tested and both failed to suppress the aliased mode and degraded long-window recovery). Explicitly passed priors are used unchanged.

classmethod none()[source]

No carryover (unit impulse).

Return type:

AdstockConfig

class mmm_framework.config.SaturationConfig(**data)[source]

Bases: BaseModel

Configuration for saturation transformation.

Notes

kappa (the Hill half-saturation point) is only weakly identified from observational data because it trades off against adstock decay and the coefficient (see mmm_framework.transforms.adstock.adstock_weights() and critique.md §3.6). To constrain it, anchor the prior to the observed spend: compute bounds with compute_kappa_bounds_from_data() (using kappa_bounds_percentiles) and pass them as kappa_lower / kappa_upper to mmm_framework.mmm_extensions.components.priors.create_saturation_prior(), which then uses a bounded Uniform instead of the default weakly-informative prior. This keeps the curve’s “elbow” inside the region the data covers.

In the core model, set anchor_kappa_to_data=True to do the same thing automatically from the channel’s own normalized spend. It is opt-in because it changes fitted results; the logistic default, by contrast, is anchored out of the box (see logistic()).

The core BayesianMMM honors type per channel: logistic (the default, a single sat_lam_<ch>), hill (sat_half_<ch> and sat_slope_<ch>), michaelis_menten / tanh (sat_half_<ch>), root (sat_exponent_<ch>), or none (identity). The Hill kappa/slope/beta prior fields are also read by mmm_framework.mmm_extensions.

type: SaturationType
kappa_prior: PriorConfig | None
slope_prior: PriorConfig | None
beta_prior: PriorConfig | None
lam_prior: PriorConfig | None
kappa_bounds_percentiles: tuple[float, float]
anchor_kappa_to_data: bool
static compute_kappa_bounds_from_data(x, percentiles=(0.1, 0.9))[source]

Return (lower, upper) kappa bounds from observed spend percentiles.

x is an array-like of (adstocked) spend for one channel. percentiles are probabilities in [0, 1]. Anchoring kappa to this range keeps the Hill curve’s half-saturation point inside the support of the data, which is the most defensible way to tame adstock/saturation equifinality. Pass the result as kappa_lower / kappa_upper to mmm_framework.mmm_extensions.components.priors.create_saturation_prior().

Raises ValueError on degenerate input (empty, all-NaN, or a collapsed range) rather than silently returning a meaningless prior.

Return type:

tuple[float, float]

classmethod hill(kappa_prior=None, slope_prior=None, beta_prior=None)[source]
Return type:

SaturationConfig

classmethod logistic(lam_prior=None)[source]

Logistic saturation 1 - exp(-lam * x) (the core model default).

With lam_prior=None the core model uses a prior stated in units of observed spend: media is normalized by the channel maximum before saturation, so sat_lam is the elbow position (half-saturation at ln(2)/lam), and the default places its median at half of maximum spend. See model/base.py::DEFAULT_LOGISTIC_ELBOW_FRACTION for why.

To restore the pre-1.3 Exponential(lam=0.5), pass lam_prior=PriorConfig(distribution="Gamma", params={"alpha": 1.0, "beta": 0.5}) – Gamma(1, rate) is the Exponential, and PriorType has no Exponential member.

Return type:

SaturationConfig

classmethod michaelis_menten(kappa_prior=None)[source]

Michaelis-Menten saturation x / (x + k) (half-saturation k).

Return type:

SaturationConfig

classmethod tanh(kappa_prior=None)[source]

Tanh saturation tanh(x / k) (scale k).

Return type:

SaturationConfig

classmethod root(exponent_prior=None)[source]

Root / power saturation x ** k with 0 < k < 1 (concave).

The classic econometric power-response curve: constant returns fall off as a power of the (adstocked, normalized) spend. The exponent k is the sat_exponent_<ch> RV; exponent_prior=None defaults to Beta(2, 2), which keeps k in (0, 1) so the curve is strictly concave (diminishing returns). k reuses the slope_prior field.

For an S-shaped curve use hill(); for a hyperbolic elbow use michaelis_menten().

Return type:

SaturationConfig

classmethod none()[source]
Return type:

SaturationConfig

class mmm_framework.config.SpecChange(path, kind, old, new)[source]

Bases: object

A single difference between a frozen spec and the current one.

path: str
kind: str
old: Any
new: Any
describe()[source]
Return type:

str

__init__(path, kind, old, new)
mmm_framework.config.diff_spec(frozen, current)[source]

Return the structural differences from frozen to current.

Each SpecChange is an added / removed / changed leaf, identified by a dotted path. An empty list means the current spec matches what was locked.

Return type:

list[SpecChange]

mmm_framework.config.summarize_spec_diff(changes)[source]

Human-readable summary; empty changes => an explicit ‘no divergence’.

Return type:

str

class mmm_framework.config.VariableConfig(**data)[source]

Bases: BaseModel

Configuration for a single variable in the MFF.

name: str
role: VariableRole
dimensions: list[DimensionType]
display_name: str | None
unit: str | None
property dim_names: list[str]

Get dimension names as strings.

property has_geo: bool
property has_product: bool
class mmm_framework.config.MediaChannelConfig(**data)[source]

Bases: VariableConfig

Extended configuration for media channels.

role: VariableRole
adstock: AdstockConfig
saturation: SaturationConfig
coefficient_prior: PriorConfig
roi_prior: PriorConfig | None
roi_prior_mu: float | None
roi_prior_sigma: float | None
parent_channel: str | None
time_varying: bool
tvp_innovation_sigma: float
split_dimensions: list[DimensionType]
measurement_unit: MeasurementUnit
spend_column: str | None
cpm: float | None
cpc: float | None
property is_child_channel: bool
property all_dimensions: list[DimensionType]

All dimensions including splits.

class mmm_framework.config.ControlVariableConfig(**data)[source]

Bases: VariableConfig

Configuration for control variables.

role: VariableRole
allow_negative: bool
coefficient_prior: PriorConfig
use_shrinkage: bool
causal_role: CausalControlRole | None
causal_role_reason: str | None
class mmm_framework.config.KPIConfig(**data)[source]

Bases: VariableConfig

Configuration for the target KPI variable.

role: VariableRole
log_transform: bool
floor_value: float
class mmm_framework.config.DimensionAlignmentConfig(**data)[source]

Bases: BaseModel

Configuration for aligning variables across different dimensions.

geo_allocation: AllocationMethod
product_allocation: AllocationMethod
geo_weight_variable: str | None
product_weight_variable: str | None
prefer_disaggregation: bool
class mmm_framework.config.MFFColumnConfig(**data)[source]

Bases: BaseModel

Column name mappings for MFF format.

period: str
geography: str
product: str
campaign: str
outlet: str
creative: str
variable_name: str
variable_value: str
property dimension_columns: list[str]

All dimension column names.

property all_columns: list[str]

All expected columns.

class mmm_framework.config.MFFConfig(**data)[source]

Bases: BaseModel

Complete configuration for MFF data and model specification.

columns: MFFColumnConfig
kpi: KPIConfig
media_channels: list[MediaChannelConfig]
controls: list[ControlVariableConfig]
alignment: DimensionAlignmentConfig
date_format: str
frequency: Literal['W', 'D', 'M']
decimal_separator: Literal['.', ',', 'auto']
fill_missing_media: float
fill_missing_controls: float | None
preserve_explicit_nan: bool
duplicate_policy: Literal['error', 'sum', 'mean', 'first']
property all_variables: list[VariableConfig]

All configured variables.

property variable_names: list[str]

All variable names.

property media_names: list[str]

Media channel names.

property control_names: list[str]

Control variable names.

property target_dimensions: list[DimensionType]

Dimensions of the target KPI.

get_media_config(name)[source]

Get media channel config by name.

Return type:

MediaChannelConfig | None

get_control_config(name)[source]

Get control config by name.

Return type:

ControlVariableConfig | None

get_variable_config(name)[source]

Get any variable config by name (media, control, or KPI).

Return type:

VariableConfig | None

Parameters

namestr

Variable name to search for.

Returns

VariableConfig | None

The config with matching name, or None if not found.

dedupe_channel_names()[source]

Drop media/control entries that repeat an already-configured name.

The panel channel axis (coords.channels == media_names) is built straight from media_channels, but the media matrix is assembled as a dict keyed by name (data_loader), so a repeated name silently collapses to ONE data column while the name axis keeps BOTH labels. That desync makes channel_names longer than the real data — surfacing as duplicate channel rows in the ROI / decomposition tables (and a channel_names vs n_channels mismatch). A duplicate media/control name is never meaningful (you cannot model the same column twice), so keep the FIRST occurrence and warn. Runs before validate_dimensions so downstream validators see the de-duplicated lists.

Return type:

MFFConfig

validate_dimensions()[source]

Ensure dimension compatibility across variables.

Return type:

MFFConfig

get_hierarchical_media_groups()[source]

Get parent->children mapping for hierarchical media.

Return type:

dict[str, list[str]]

class mmm_framework.config.HierarchicalConfig(**data)[source]

Bases: BaseModel

Configuration for hierarchical/panel model structure.

enabled: bool
pool_across_geo: bool
pool_across_product: bool
vary_media_by_geo: bool
media_geo_sigma: float
use_non_centered: bool
non_centered_threshold: int
mu_prior: PriorConfig
sigma_prior: PriorConfig
class mmm_framework.config.SeasonalityConfig(**data)[source]

Bases: BaseModel

Configuration for seasonality components.

Amplitude scale note: the Fourier coefficients get Normal(0, prior_sigma) priors on standardized y, so prior_sigma bounds how much of a standard deviation each harmonic may swing the KPI — it is the seasonal amplitude prior. The historic default (0.3) suits mild seasonality; raise it (e.g. 0.5–1.0) for strongly seasonal categories, or the seasonal signal gets squeezed into trend/media. Per-component overrides win over prior_sigma.

yearly: int | None
monthly: int | None
weekly: int | None
prior_sigma: float
yearly_prior_sigma: float | None
monthly_prior_sigma: float | None
weekly_prior_sigma: float | None
prior_sigma_for(component)[source]

Amplitude prior sigma for component (‘yearly’/’monthly’/’weekly’), falling back to the shared prior_sigma. getattr defaults keep configs pickled before these fields existed loadable.

Return type:

float

class mmm_framework.config.ControlSelectionConfig(**data)[source]

Bases: BaseModel

Configuration for control variable selection.

method: Literal['none', 'horseshoe', 'spike_slab', 'lasso']
expected_nonzero: int
regularization: float
class mmm_framework.config.ModelConfig(**data)[source]

Bases: BaseModel

Complete model configuration.

specification: ModelSpecification
likelihood: LikelihoodConfig
intercept_prior_mu: float
intercept_prior_sigma: float
media_prior_mode: Literal['coefficient', 'roi']
media_roi_prior_mu: float
media_roi_prior_sigma: float
use_grouped_media_priors: bool
inference_method: InferenceMethod
n_chains: int
n_draws: int
n_tune: int
target_accept: float
fit_method: FitMethod | None
hierarchical: HierarchicalConfig
seasonality: SeasonalityConfig
events: EventsConfig | None
channel_interactions: list[ChannelInteraction]
price: PriceConfig | None
promotions: list[PromoConfig]
reach_frequency: list[ReachFrequencyConfig]
control_selection: ControlSelectionConfig
use_parametric_adstock: bool
ridge_alpha: float
bootstrap_samples: int
optim_maxiter: int
optim_seed: int | None
property is_bayesian: bool
property is_frequentist: bool

True for the ridge / constrained estimation path (epic #180).

The complement of is_bayesian over the implemented methods, but written as its own membership test rather than not is_bayesian so a future third paradigm cannot silently inherit the frequentist branch.

property frequentist_estimator: str | None

"ridge" / "constrained", or None for a Bayesian config.

property use_numpyro: bool
property nuts_sampler: str

The pm.sample(nuts_sampler=...) backend this config selects.

Frequentist methods have no NUTS backend; they report "pymc" so a caller that reads this on a non-Bayesian config still gets the historical default rather than an error. That fall-through is why selecting one used to silently fit NUTS — fit() now branches on is_frequentist before reading this, so the value is never consumed on a frequentist config.

property is_implemented: bool

False for declared-but-unbacked inference methods (see #180).

mmm_framework.config.create_national_media_config(name, adstock_lmax=8, display_name=None)[source]

Create config for national-level media channel.

Return type:

MediaChannelConfig

mmm_framework.config.create_geo_media_config(name, adstock_lmax=8, display_name=None)[source]

Create config for geo-level media channel.

Return type:

MediaChannelConfig

mmm_framework.config.create_social_platform_configs(platforms, parent_name='social', adstock_lmax=4)[source]

Create configs for social media platforms with hierarchical structure.

Return type:

list[MediaChannelConfig]

mmm_framework.config.create_simple_mff_config(kpi_name, media_names, control_names=None, kpi_dimensions=None, multiplicative=False)[source]

Create a simple MFF config with sensible defaults.

Return type:

MFFConfig