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 enumerationspriors—PriorConfigtransforms—AdstockConfig,SaturationConfigvariables— per-variable configs (KPI, media, control)mff— MFF column mappings, dimension alignment,MFFConfigmodel— model-level configuration (ModelConfigand 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]¶
-
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]¶
-
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]¶
-
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.
MEDIATORandCOLLIDERare refused at model-construction time. A control left unmarked (None) is treated asPRECISION_CONTROLfor 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]¶
-
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 separatespend_column, or acpm/cpcconstant) 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 likeIMPRESSIONSfor efficiency but labeled per unit.
See
mmm_framework.config.variables.MediaChannelConfigfor the companionspend_column/cpm/cpcfields andmmm_framework.reporting.helpers.measurementfor the resolver that turns this into a divisor + metric labels.- SPEND = 'spend'¶
- IMPRESSIONS = 'impressions'¶
- CLICKS = 'clicks'¶
- OTHER = 'other'¶
- class mmm_framework.config.AdstockType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
-
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]¶
-
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 exactlyHILL, and the exponential-CDF form1 - exp(-lam * x)is exactlyLOGISTIC.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]¶
-
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]¶
-
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]¶
-
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.
MAPis a point estimate;LAPLACEadds a Gaussian curvature approximation around the MAP point (cheap uncertainty, better-behaved than bare MAP on high-dimensional models);ADVI/FULLRANK_ADVIare variational;PATHFINDERis quasi-Newton variational (via the declaredpymc-extrasdependency, like LAPLACE).- NUTS = 'nuts'¶
- SMC = 'smc'¶
- MAP = 'map'¶
- LAPLACE = 'laplace'¶
- ADVI = 'advi'¶
- FULLRANK_ADVI = 'fullrank_advi'¶
- PATHFINDER = 'pathfinder'¶
- class mmm_framework.config.InferenceMethod(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
-
Available inference methods.
The three Bayesian entries differ only in the NUTS backend used to sample the same graph (
ModelConfig.nuts_samplermaps them onto thepm.sample(nuts_sampler=...)value): reference PyMC, JAX/NumPyro, or the Rustnutpiesampler.- 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]¶
-
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]¶
-
Observation (likelihood) family for the KPI.
NORMALis the historical default and the only family the built-in additive model fits directly on standardizedy(all of its component priors are calibrated in KPI standard deviations).STUDENT_Tis a safe, heavier-tailed drop-in on the same standardized, identity-link scale.The remaining families (
LOGNORMALand the count/boundedBINOMIAL/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/ subclassCustomMMM), e.g. a binomial awareness model.is_gaussian/standardizes_yroute this inLikelihoodConfigand the model.- NORMAL = 'normal'¶
- STUDENT_T = 'student_t'¶
- LOGNORMAL = 'lognormal'¶
- BINOMIAL = 'binomial'¶
- BETA_BINOMIAL = 'beta_binomial'¶
- POISSON = 'poisson'¶
- NEGATIVE_BINOMIAL = 'negative_binomial'¶
- BETA = 'beta'¶
- class mmm_framework.config.LinkFunction(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
-
Link mapping the linear predictor
muto the observation’s natural parameter.IDENTITYfor Gaussian families;LOGITfor bounded proportions (binomial/beta);LOGfor 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]¶
-
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:
BaseModelA 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_SCHEMAclass attribute — the runtime then coerces/validates the incoming data against it (mirroringCONFIG_SCHEMA+model_params).- SCHEMA_VERSION: str¶
Class-level version constant (read by the serializer, as
CONFIG_SCHEMAexposesSCHEMA_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¶
- binding(name)[source]¶
The
RoleBindingfor columnname(orNone).- Return type:
- classmethod from_mff(mff)[source]¶
Build a
DatasetSchemafrom anMFFConfig.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:
- to_mff()[source]¶
Best-effort reconstruction of an
MFFConfigfrom this schema.Lossless for the MMM roles; non-MMM roles (indicator/offset/…) are dropped from the MMM view. Used only as the fall-back
configwhen a Dataset is adaptedas_panel()with no originalMFFConfigattached. When a realMFFConfigis present it is preserved verbatim, so this never runs on the MMM path.- Return type:
- class mmm_framework.config.RoleBinding(**data)[source]¶
Bases:
BaseModelOne column → its
DatasetRoleplus per-role metadata.Generalizes
VariableConfig. The openmetadict 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 asLikelihoodConfig.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:
BaseModelConfiguration for a prior distribution.
- distribution: PriorType¶
- params: dict[str, float]¶
- dims: str | list[str] | None¶
- class mmm_framework.config.EventSpec(**data)[source]¶
Bases:
BaseModelOne named event (or recurring holiday).
Exactly one of
dates(explicit) orholiday(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
countrycalendar (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:
BaseModelA set of holiday / event regressors to add to the model.
Off by default (
ModelConfig.events is None). Effects enter as an additiveevent_componentdistinct from the Fourierseasonality_component, so they do not double-count.- country: str | None¶
Country code for named-holiday lookups (e.g. “US”, “GB”). Requires the optional
holidayspackage.None⇒ onlycustom_eventsare 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¶
- class mmm_framework.config.ChannelInteraction(**data)[source]¶
Bases:
BaseModelOne named channel-pair interaction term.
expected_signencodes 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']¶
- class mmm_framework.config.PriceConfig(**data)[source]¶
Bases:
BaseModelA 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).Noneuseslog(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:
BaseModelA 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]¶
-
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:
BaseModelDeclare a channel as reach modulated by a frequency-saturation curve.
The named
channel’s media column is treated as reach. Thefrequency_columnis 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 becomesbeta · 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 (
kfor exponential; the slope / half-saturation priors for Hill). Larger ⇒ faster frequency saturation a priori.
- class mmm_framework.config.LikelihoodConfig(**data)[source]¶
Bases:
BaseModelObservation family + link + family parameters for the KPI likelihood.
Parameters¶
- family:
Observation family. Default
NORMAL(historical behavior).- link:
Link mapping the linear predictor
muto the family’s natural parameter.None(default) resolves to the family’s canonical link.- params:
Family parameters.
student_tacceptsnu(degrees of freedom);negative_binomialacceptsalpha.binomial/beta_binomialmay carryn_trials(a positive int, or a per-observation column name) here, but a custom model is free to source it from its ownCONFIG_SCHEMAinstead (e.g. an awareness model’snumber_of_trials) — the family declares the scale/observation type (which drives whetheryis standardized), while bespoke counts can live inmodel_params. Unknown keys are passed through for a model to interpret.
- family: LikelihoodFamily¶
- link: LinkFunction | None¶
- params: dict[str, Any]¶
- classmethod normal()[source]¶
The default Gaussian likelihood (identity link, standardized
y).- Return type:
- classmethod student_t(nu=4.0)[source]¶
Heavier-tailed Gaussian-scale likelihood (robust to outliers).
- Return type:
- class mmm_framework.config.AdstockConfig(**data)[source]¶
Bases:
BaseModelConfiguration 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:
- 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:
- classmethod weibull(l_max=12, shape_prior=None, scale_prior=None)[source]¶
Weibull (PDF) carryover: a flexible, possibly delayed decay shape.
Shape
k > 1yields a delayed peak;k == 1is exponential;k < 1is front-loaded. Scalelambdaspreads 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 meanm = 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 atl_max=26). Rationale, measured on the synth stress harness: :rtype:AdstockConfigA 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 atl_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 / 3was 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 tighterGamma(3, 1.5)was tested and both failed to suppress the aliased mode and degraded long-window recovery). Explicitly passed priors are used unchanged.
- class mmm_framework.config.SaturationConfig(**data)[source]¶
Bases:
BaseModelConfiguration 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 (seemmm_framework.transforms.adstock.adstock_weights()and critique.md §3.6). To constrain it, anchor the prior to the observed spend: compute bounds withcompute_kappa_bounds_from_data()(usingkappa_bounds_percentiles) and pass them askappa_lower/kappa_uppertommm_framework.mmm_extensions.components.priors.create_saturation_prior(), which then uses a boundedUniforminstead 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=Trueto 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 (seelogistic()).The core
BayesianMMMhonorstypeper channel:logistic(the default, a singlesat_lam_<ch>),hill(sat_half_<ch>andsat_slope_<ch>),michaelis_menten/tanh(sat_half_<ch>),root(sat_exponent_<ch>), ornone(identity). The Hillkappa/slope/betaprior fields are also read bymmm_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.xis an array-like of (adstocked) spend for one channel.percentilesare probabilities in[0, 1]. Anchoringkappato 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 askappa_lower/kappa_uppertommm_framework.mmm_extensions.components.priors.create_saturation_prior().Raises
ValueErroron degenerate input (empty, all-NaN, or a collapsed range) rather than silently returning a meaningless prior.
- classmethod logistic(lam_prior=None)[source]¶
Logistic saturation
1 - exp(-lam * x)(the core model default).With
lam_prior=Nonethe core model uses a prior stated in units of observed spend: media is normalized by the channel maximum before saturation, sosat_lamis the elbow position (half-saturation atln(2)/lam), and the default places its median at half of maximum spend. Seemodel/base.py::DEFAULT_LOGISTIC_ELBOW_FRACTIONfor why.To restore the pre-1.3
Exponential(lam=0.5), passlam_prior=PriorConfig(distribution="Gamma", params={"alpha": 1.0, "beta": 0.5})– Gamma(1, rate) is the Exponential, andPriorTypehas no Exponential member.- Return type:
- classmethod michaelis_menten(kappa_prior=None)[source]¶
Michaelis-Menten saturation
x / (x + k)(half-saturationk).- Return type:
- classmethod root(exponent_prior=None)[source]¶
Root / power saturation
x ** kwith0 < k < 1(concave).The classic econometric power-response curve: constant returns fall off as a power of the (adstocked, normalized) spend. The exponent
kis thesat_exponent_<ch>RV;exponent_prior=Nonedefaults toBeta(2, 2), which keepskin(0, 1)so the curve is strictly concave (diminishing returns).kreuses theslope_priorfield.For an S-shaped curve use
hill(); for a hyperbolic elbow usemichaelis_menten().- Return type:
- class mmm_framework.config.SpecChange(path, kind, old, new)[source]¶
Bases:
objectA single difference between a frozen spec and the current one.
- __init__(path, kind, old, new)¶
- mmm_framework.config.diff_spec(frozen, current)[source]¶
Return the structural differences from
frozentocurrent.Each
SpecChangeis an added / removed / changed leaf, identified by a dotted path. An empty list means the current spec matches what was locked.- Return type:
- mmm_framework.config.summarize_spec_diff(changes)[source]¶
Human-readable summary; empty changes => an explicit ‘no divergence’.
- Return type:
- class mmm_framework.config.VariableConfig(**data)[source]¶
Bases:
BaseModelConfiguration for a single variable in the MFF.
- name: str¶
- role: VariableRole¶
- dimensions: list[DimensionType]¶
- display_name: str | None¶
- unit: str | None¶
- class mmm_framework.config.MediaChannelConfig(**data)[source]¶
Bases:
VariableConfigExtended 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 all_dimensions: list[DimensionType]¶
All dimensions including splits.
- class mmm_framework.config.ControlVariableConfig(**data)[source]¶
Bases:
VariableConfigConfiguration 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:
VariableConfigConfiguration for the target KPI variable.
- role: VariableRole¶
- log_transform: bool¶
- floor_value: float¶
- class mmm_framework.config.DimensionAlignmentConfig(**data)[source]¶
Bases:
BaseModelConfiguration 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:
BaseModelColumn name mappings for MFF format.
- period: str¶
- geography: str¶
- product: str¶
- campaign: str¶
- outlet: str¶
- creative: str¶
- variable_name: str¶
- variable_value: str¶
- class mmm_framework.config.MFFConfig(**data)[source]¶
Bases:
BaseModelComplete 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 target_dimensions: list[DimensionType]¶
Dimensions of the target KPI.
- get_variable_config(name)[source]¶
Get any variable config by name (media, control, or KPI).
- Return type:
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 frommedia_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 makeschannel_nameslonger than the real data — surfacing as duplicate channel rows in the ROI / decomposition tables (and achannel_namesvsn_channelsmismatch). A duplicate media/control name is never meaningful (you cannot model the same column twice), so keep the FIRST occurrence and warn. Runs beforevalidate_dimensionsso downstream validators see the de-duplicated lists.- Return type:
- class mmm_framework.config.HierarchicalConfig(**data)[source]¶
Bases:
BaseModelConfiguration 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:
BaseModelConfiguration for seasonality components.
Amplitude scale note: the Fourier coefficients get
Normal(0, prior_sigma)priors on standardizedy, soprior_sigmabounds 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 overprior_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¶
- class mmm_framework.config.ControlSelectionConfig(**data)[source]¶
Bases:
BaseModelConfiguration for control variable selection.
- method: Literal['none', 'horseshoe', 'spike_slab', 'lasso']¶
- expected_nonzero: int¶
- regularization: float¶
- class mmm_framework.config.ModelConfig(**data)[source]¶
Bases:
BaseModelComplete 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_frequentist: bool¶
True for the ridge / constrained estimation path (epic #180).
The complement of
is_bayesianover the implemented methods, but written as its own membership test rather thannot is_bayesianso a future third paradigm cannot silently inherit the frequentist branch.
- 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 onis_frequentistbefore reading this, so the value is never consumed on a frequentist config.
- mmm_framework.config.create_national_media_config(name, adstock_lmax=8, display_name=None)[source]¶
Create config for national-level media channel.
- Return type:
- mmm_framework.config.create_geo_media_config(name, adstock_lmax=8, display_name=None)[source]¶
Create config for geo-level media channel.
- Return type: