Estimands¶
Named, serializable estimand system: declarative counterfactual quantities (contribution ROI, marginal ROAS, contributions) realized as mean + HDI.
First-class, declarative estimands — the counterfactual causal lens.
A model declares named, serializable Estimand
objects; the framework realizes them from the posterior as mean + HDI, either
post-hoc (EstimandEvaluator) or in the
PyMC graph (build_estimand_expr()). This is
the single registry that subsumes the framework’s previously-scattered estimand
logic (decomposition ROI, counterfactual ROI, marginal ROAS, calibration
estimands) while keeping every existing number bit-stable.
spec is dependency-light (pure Pydantic); the realization engines and the
registry import numpy/pytensor lazily, so importing this package is cheap.
- class mmm_framework.estimands.Constant(**data)[source]
Bases:
BaseModelA fixed scalar.
- type: Literal['constant']
- value: float
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.Contrast(**data)[source]
Bases:
BaseModelreduce_over(op(quantity|intervention, quantity|baseline)).With
baseline=Nonethe baseline is the factual world (Observed).paired_seedis a post-hoc realization hint: when set and no explicit seed is supplied, the two worlds share one synthesized seed so observation noise cancels in the per-draw difference (the marginal path); the in-graph engine ignores it.- quantity: Quantity
- intervention: Intervention
- baseline: Intervention | None
- op: Literal['difference', 'ratio', 'identity']
- reduce: Literal['sum', 'mean']
- over: list[str]
- paired_seed: bool
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.Contribution(**data)[source]
Bases:
BaseModelA channel’s contribution.
source="counterfactual"realizes it asoutcome|observed − outcome|zero(target)(aContrast);source="in_graph_deterministic"reads the in-graphchannel_contributionsDeterministic from the posterior (the dashboard decomposition). The two are different numbers — see the registry’scounterfactual_roivscontribution_roi.- type: Literal['contribution']
- target: str
- source: Literal['counterfactual', 'in_graph_deterministic']
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.CustomIntervention(**data)[source]
Bases:
BaseModelAn intervention realized by a registered callable, keyed by
ref.- type: Literal['custom']
- ref: str
- params: dict[str, Any]
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.Estimand(**data)[source]
Bases:
BaseModelA named, serializable counterfactual estimand.
The headline value is
numerator(aContrastor bareQuantity), optionally divided bydenominator.mean/hdi_low/hdi_highin the result are inunits; companion quantities (the raw contribution + its HDI, spend,contribution_pct) land inresult.extra.- name: str
- kind: str
- numerator: Contrast | Quantity
- denominator: Contrast | Quantity | None
- op_ratio_zero_denominator: Literal['zero', 'skip', 'nan']
- window: TimeWindow | None
- hdi_prob: float
- summaries: list[Summary]
- realization: Realization
- required_capabilities: list[str]
- units: str
- causal_assumptions: str
- schema_version: str
- to_dict()[source]
JSON-ready dict (mirrors
ExperimentMeasurement.to_dict).
- classmethod from_dict(data)[source]
Parse a serialized estimand (mirrors
ExperimentMeasurement.from_dict).- Return type:
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.EstimandResult(**data)[source]
Bases:
BaseModelRealized value of an
Estimandfrom a fitted posterior.status="unsupported"(withreason) is returned — never raised — whenrequired_capabilitiesare not met, mirroring the agent ops’_errshape.mean/hdi_*areNonefor an unsupported result.- name: str
- kind: str
- status: Literal['ok', 'unsupported']
- mean: float | None
- hdi_low: float | None
- hdi_high: float | None
- hdi_prob: float
- units: str
- extra: dict[str, Any]
- reason: str
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.LatentVar(**data)[source]
Bases:
BaseModelA named latent posterior variable (e.g. an awareness state).
- type: Literal['latent_var']
- name: str
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.MarginalSpend(**data)[source]
Bases:
BaseModelThe incremental spend implied by a
ScaleInput.intervention_ref="numerator"reads the numerator contrast’s intervention factor (never a second literal percentage — a desync guard): the marginal denominator iscurrent_spend * (factor - 1).- type: Literal['marginal_spend']
- target: str
- intervention_ref: str
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.Observed(**data)[source]
Bases:
BaseModelThe factual world: inputs as observed (no intervention).
- type: Literal['observed']
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.ObservedInput(**data)[source]
Bases:
BaseModelAn observed input level (spend), used as a denominator.
source="raw"sumsX_media_raw[window, target];source="panel"uses the reporting spend extractor (panel.X_media). The dashboard ROI usespanel; the counterfactual / marginal paths useraw.- type: Literal['observed_input']
- target: str
- source: Literal['raw', 'panel']
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.Outcome(**data)[source]
Bases:
BaseModelThe model’s predicted outcome (KPI), in original scale.
- type: Literal['outcome']
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.Realization(**data)[source]
Bases:
BaseModelPer-estimand realization knobs that pin bit-stable arithmetic.
These encode where a naive engine diverges between the four legacy notions (see
technical-docs/estimands.md), kept separate from the causal definition above:point_rule—"diff_of_means"(point = reduced per-call means thenop; the counterfactual / marginal paths) vs"mean_of_samples"(point = mean of the per-draw ratio; the dashboard decomposition ROI).hdi_method—"percentile"(compute_hdi_bounds),"az_hdi"(az.hdiw/ percentile fallback; the dashboard) or"finite_percentile"(_hdi_finite; the marginal path, filters non-finite draws).
- point_rule: Literal['diff_of_means', 'mean_of_samples']
- hdi_method: Literal['percentile', 'az_hdi', 'finite_percentile']
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.ScaleInput(**data)[source]
Bases:
BaseModelMultiply
target’s input byfactor(e.g.1.1for +10%).- type: Literal['scale_input']
- target: str
- factor: float
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.SetInput(**data)[source]
Bases:
BaseModelSet
target’s input to a fixedvalue.sustained/carryover_statecarry the off-panel adstock convention for the in-graph engine (steady-state vs cold-start ramp); the post-hoc engine sets the column tovalueover the active window.- type: Literal['set_input']
- target: str
- value: float
- sustained: bool
- carryover_state: Literal['steady_state', 'cold_start'] | None
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.Summary(**data)[source]
Bases:
BaseModelA posterior tail probability over the estimand’s per-draw samples.
side="gt"/"lt"withthresholdcarriesprob_positive(gt 0) andprob_profitable(gt 1).- kind: Literal['tail_prob']
- name: str
- threshold: float
- side: Literal['gt', 'lt']
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.SupportsEstimands(*args, **kwargs)[source]
Bases:
ProtocolThe surface an
Estimandis realized against.BayesianMMMimplements it today; a future non-MMM family implements the same four members (declaring its own capabilities + quantities) to gain the whole estimand stack.predict_underreturns aPredictionResults-like object exposingy_pred_meanandy_pred_samples.- evaluate_estimands(estimands=Ellipsis)[source]
- Return type:
- __init__(*args, **kwargs)
- class mmm_framework.estimands.TimeWindow(**data)[source]
Bases:
BaseModelAn inclusive
[start, end]window in period-index units.Lives at the
Estimandlevel so the numerator and denominator share one time mask (BayesianMMM._get_time_mask((start, end))).- start: int
- end: int
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.ZeroInput(**data)[source]
Bases:
BaseModelSet
target’s input to zero (the channel-off counterfactual).- type: Literal['zero_input']
- target: str
- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- mmm_framework.estimands.__getattr__(name)[source]
Lazily expose the heavier members (numpy/pytensor) on first access.
Uses
import_module(notfrom . import) so binding the submodule name does not re-enter this hook (_handle_fromlist→__getattr__).
Spec¶
Declarative estimand specification — the counterfactual causal lens.
An Estimand is a named, serializable counterfactual contrast:
reduce_over( op( quantity | intervention, quantity | baseline ) ) / normalizer
It is the single source of truth shared by the two realization engines:
mmm_framework.estimands.evaluate— post-hoc realization (numpy; reads the posterior-predictive viaBayesianMMM.predict_under(), paired/unpaired seeds, boolean time masks).mmm_framework.estimands.graph— in-graph realization (pytensor; builds a likelihood-time scalar; deterministic, integer-indexed windows).
This module imports neither numpy nor pytensor so it can be loaded anywhere
(host or kernel) for serialization / validation without paying the model-stack
import cost — mirroring mmm_framework.garden.contract.
The schema is intentionally model-agnostic: a future non-MMM family (CFA / LCA /
EFA) declares its own estimands over its own quantities (a latent factor, a class
membership) and gates them with Estimand.required_capabilities. The MMM
built-ins live in mmm_framework.estimands.registry.
- mmm_framework.estimands.spec.ESTIMAND_SCHEMA_VERSION = '1.0'¶
Bumped when the serialized estimand surface changes. Stored on every
Estimandso a consumer (a reloaded model, a saved spec) can detect drift.
- mmm_framework.estimands.spec.ALL_CHANNELS = '*'¶
Sentinel
targetmeaning “expand this estimand once per channel”. The evaluator substitutes each channel name into every wildcard target and emits one result per channel (keyed"{name}:{channel}"). A non-MMM model with no channels yields nothing — natural capability filtering.
- class mmm_framework.estimands.spec.Observed(**data)[source]¶
Bases:
BaseModelThe factual world: inputs as observed (no intervention).
- type: Literal['observed']¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.ZeroInput(**data)[source]¶
Bases:
BaseModelSet
target’s input to zero (the channel-off counterfactual).- type: Literal['zero_input']¶
- target: str¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.ScaleInput(**data)[source]¶
Bases:
BaseModelMultiply
target’s input byfactor(e.g.1.1for +10%).- type: Literal['scale_input']¶
- target: str¶
- factor: float¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.SetInput(**data)[source]¶
Bases:
BaseModelSet
target’s input to a fixedvalue.sustained/carryover_statecarry the off-panel adstock convention for the in-graph engine (steady-state vs cold-start ramp); the post-hoc engine sets the column tovalueover the active window.- type: Literal['set_input']¶
- target: str¶
- value: float¶
- sustained: bool¶
- carryover_state: Literal['steady_state', 'cold_start'] | None¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.CustomIntervention(**data)[source]¶
Bases:
BaseModelAn intervention realized by a registered callable, keyed by
ref.- type: Literal['custom']¶
- ref: str¶
- params: dict[str, Any]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.Outcome(**data)[source]¶
Bases:
BaseModelThe model’s predicted outcome (KPI), in original scale.
- type: Literal['outcome']¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.Contribution(**data)[source]¶
Bases:
BaseModelA channel’s contribution.
source="counterfactual"realizes it asoutcome|observed − outcome|zero(target)(aContrast);source="in_graph_deterministic"reads the in-graphchannel_contributionsDeterministic from the posterior (the dashboard decomposition). The two are different numbers — see the registry’scounterfactual_roivscontribution_roi.- type: Literal['contribution']¶
- target: str¶
- source: Literal['counterfactual', 'in_graph_deterministic']¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.ObservedInput(**data)[source]¶
Bases:
BaseModelAn observed input level (spend), used as a denominator.
source="raw"sumsX_media_raw[window, target];source="panel"uses the reporting spend extractor (panel.X_media). The dashboard ROI usespanel; the counterfactual / marginal paths useraw.- type: Literal['observed_input']¶
- target: str¶
- source: Literal['raw', 'panel']¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.MarginalSpend(**data)[source]¶
Bases:
BaseModelThe incremental spend implied by a
ScaleInput.intervention_ref="numerator"reads the numerator contrast’s intervention factor (never a second literal percentage — a desync guard): the marginal denominator iscurrent_spend * (factor - 1).- type: Literal['marginal_spend']¶
- target: str¶
- intervention_ref: str¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.LatentVar(**data)[source]¶
Bases:
BaseModelA named latent posterior variable (e.g. an awareness state).
- type: Literal['latent_var']¶
- name: str¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.Constant(**data)[source]¶
Bases:
BaseModelA fixed scalar.
- type: Literal['constant']¶
- value: float¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.Contrast(**data)[source]¶
Bases:
BaseModelreduce_over(op(quantity|intervention, quantity|baseline)).With
baseline=Nonethe baseline is the factual world (Observed).paired_seedis a post-hoc realization hint: when set and no explicit seed is supplied, the two worlds share one synthesized seed so observation noise cancels in the per-draw difference (the marginal path); the in-graph engine ignores it.- quantity: Quantity¶
- intervention: Intervention¶
- baseline: Intervention | None¶
- op: Literal['difference', 'ratio', 'identity']¶
- reduce: Literal['sum', 'mean']¶
- over: list[str]¶
- paired_seed: bool¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.Summary(**data)[source]¶
Bases:
BaseModelA posterior tail probability over the estimand’s per-draw samples.
side="gt"/"lt"withthresholdcarriesprob_positive(gt 0) andprob_profitable(gt 1).- kind: Literal['tail_prob']¶
- name: str¶
- threshold: float¶
- side: Literal['gt', 'lt']¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.Realization(**data)[source]¶
Bases:
BaseModelPer-estimand realization knobs that pin bit-stable arithmetic.
These encode where a naive engine diverges between the four legacy notions (see
technical-docs/estimands.md), kept separate from the causal definition above:point_rule—"diff_of_means"(point = reduced per-call means thenop; the counterfactual / marginal paths) vs"mean_of_samples"(point = mean of the per-draw ratio; the dashboard decomposition ROI).hdi_method—"percentile"(compute_hdi_bounds),"az_hdi"(az.hdiw/ percentile fallback; the dashboard) or"finite_percentile"(_hdi_finite; the marginal path, filters non-finite draws).
- point_rule: Literal['diff_of_means', 'mean_of_samples']¶
- hdi_method: Literal['percentile', 'az_hdi', 'finite_percentile']¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.TimeWindow(**data)[source]¶
Bases:
BaseModelAn inclusive
[start, end]window in period-index units.Lives at the
Estimandlevel so the numerator and denominator share one time mask (BayesianMMM._get_time_mask((start, end))).- start: int¶
- end: int¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.Estimand(**data)[source]¶
Bases:
BaseModelA named, serializable counterfactual estimand.
The headline value is
numerator(aContrastor bareQuantity), optionally divided bydenominator.mean/hdi_low/hdi_highin the result are inunits; companion quantities (the raw contribution + its HDI, spend,contribution_pct) land inresult.extra.- name: str¶
- kind: str¶
- numerator: Contrast | Quantity¶
- denominator: Contrast | Quantity | None¶
- op_ratio_zero_denominator: Literal['zero', 'skip', 'nan']¶
- window: TimeWindow | None¶
- hdi_prob: float¶
- summaries: list[Summary]¶
- realization: Realization¶
- required_capabilities: list[str]¶
- units: str¶
- causal_assumptions: str¶
- schema_version: str¶
- classmethod from_dict(data)[source]¶
Parse a serialized estimand (mirrors
ExperimentMeasurement.from_dict).- Return type:
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.EstimandResult(**data)[source]¶
Bases:
BaseModelRealized value of an
Estimandfrom a fitted posterior.status="unsupported"(withreason) is returned — never raised — whenrequired_capabilitiesare not met, mirroring the agent ops’_errshape.mean/hdi_*areNonefor an unsupported result.- name: str¶
- kind: str¶
- status: Literal['ok', 'unsupported']¶
- mean: float | None¶
- hdi_low: float | None¶
- hdi_high: float | None¶
- hdi_prob: float¶
- units: str¶
- extra: dict[str, Any]¶
- reason: str¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mmm_framework.estimands.spec.SupportsEstimands(*args, **kwargs)[source]¶
Bases:
ProtocolThe surface an
Estimandis realized against.BayesianMMMimplements it today; a future non-MMM family implements the same four members (declaring its own capabilities + quantities) to gain the whole estimand stack.predict_underreturns aPredictionResults-like object exposingy_pred_meanandy_pred_samples.- __init__(*args, **kwargs)¶
Registry¶
Canonical built-in estimands + capability-keyed defaults.
Each built-in is a faithful, serializable re-expression of one number the
framework already produces (the four legacy notions) plus two demonstrators that
prove the schema generalizes beyond ROI. The realization knobs
(Realization) pin the bit-stable
arithmetic — see technical-docs/estimands.md and the equivalence tests.
The four legacy notions, by built-in:
contribution_roi↔reporting.helpers.roi.compute_roi_with_uncertainty(the dashboard decomposition ROI — reads the in-graph Deterministic; the number the UI shows).counterfactual_roi↔analysis.MMMAnalyzer.compute_channel_roi(zero-out predict; a different number).marginal_roas↔model.compute_marginal_contributions.contribution↔model.compute_counterfactual_contributionstotals.
Two demonstrators: awareness_lift (mean lift, no denominator) and
cost_per_conversion (inverted ratio: spend / incremental conversions).
- mmm_framework.estimands.registry.BUILTINS = {'awareness_lift': <function _awareness_lift>, 'contribution': <function _contribution>, 'contribution_roi': <function _contribution_roi>, 'cost_per_conversion': <function _cost_per_conversion>, 'counterfactual_roi': <function _counterfactual_roi>, 'marginal_roas': <function _marginal_roas>}¶
name -> zero-arg factory returning a fresh
Estimand(avoids shared mutable state across callers).
- mmm_framework.estimands.registry.DEFAULT_NAMES = ['contribution_roi', 'marginal_roas', 'contribution']¶
The MMM default list surfaced when a model declares no estimands of its own.
- mmm_framework.estimands.registry.DEFAULT_MARGINAL_FACTOR = 1.1¶
Default +10% perturbation for the marginal estimand (matches
compute_marginal_contributions’ defaultspend_increase_pct=10).
- mmm_framework.estimands.registry.get(name)[source]¶
A fresh copy of the named built-in estimand. Raises
KeyErrorif unknown.- Return type:
- mmm_framework.estimands.registry.defaults_for(capabilities)[source]¶
Default estimands whose
required_capabilitiesare met bycapabilities.Keyed by capability, not class name, so a non-MMM model that lacks (say)
HAS_CONTRIBUTION_DETERMINISTICauto-filterscontribution_roirather than erroring.
- mmm_framework.estimands.registry.latent_scalar(name, *, var=None, kind='latent', units='', hdi_prob=0.94, causal_assumptions='Posterior summary of a fitted latent quantity.')[source]¶
A declarative estimand that summarizes the scalar posterior variable
var(default:name). Used by non-MMM families to surface fit indices, factor loadings, class sizes, etc. through the estimand engine.- Return type:
Evaluate¶
Post-hoc estimand realization from a fitted posterior.
EstimandEvaluator walks an Estimand
and realizes it as a mean + HDI (+ tail-probability summaries) from the fitted
model, via BayesianMMM.predict_under() (per-draw posterior-predictive) and
the in-graph channel_contributions Deterministic.
Bit-stability is the contract: each built-in reproduces its legacy number to the
bit under a fixed seed. This is achieved by (a) reusing the exact legacy
sample-extraction helpers for the decomposition path and (b) per-estimand
realization knobs (point arithmetic, HDI function, seed pairing, spend source) —
see Realization and
technical-docs/estimands.md.
This engine is numpy-only and post-hoc; the in-graph (pytensor) realization
lives in mmm_framework.estimands.graph and the two never call into each
other (they share only spec.py + the tiny window/reduce helpers here).
- class mmm_framework.estimands.evaluate.EstimandEvaluator(model, *, random_seed=None)[source]¶
Bases:
objectRealize estimands against a fitted model.
Parameters¶
- model:
A fitted
BayesianMMM(or anything implementingSupportsEstimands).- random_seed:
Seed threaded to every
predict_undercall.Nonereproduces the legacy default (unpaired counterfactual draws; a single synthesized seed for paired/marginal contrasts).
Graph¶
In-graph estimand realization (PyTensor) — likelihood-time scalars.
This is the in-graph counterpart to mmm_framework.estimands.evaluate:
it assembles a model-implied estimand as a PyTensor expression to be compared
against an experiment measurement inside the PyMC graph. It is the canonical
home of build_estimand_expr() (subsuming
mmm_framework.calibration.likelihood.build_estimand_expr, which now
delegates here).
The two realizations deliberately share only spec and never call into
each other: this one is deterministic, integer-indexed and scale-folded
(delta = (pert − base) * scale), whereas the post-hoc engine is numpy,
paired/unpaired-seeded and boolean-masked. pytensor is imported lazily so
importing the package stays cheap.
The contribution-window inputs (contrib_window / contrib_window_pert) are
produced by the caller — the in-panel masked sum or the off-panel global-curve
evaluation (BayesianMMM._offpanel_contribution_std) — so the
eval_spend / adstock_state branches live with the model that owns the
channel handle; this module is the estimand algebra over those windows.
- mmm_framework.estimands.graph.build_estimand_expr(estimand, *, contrib_window, spend_window, scale=1.0, contrib_window_pert=None, lift=None)[source]¶
Assemble a model-implied estimand from a window’s contribution.
contrib_windowis the summed per-obs contribution over the experiment window in model units;scaleconverts it to the estimand’s natural scale (y_stdfor a standardized model,1.0for a raw-ymodel).spend_windowis the observed window spend (the ROAS / marginal-spend denominator).For
MROAS,contrib_window_pertis the contribution under spend scaled by(1 + lift)within the window; the result is the incremental KPI per incremental dollar.Returns a PyTensor scalar. Bit-identical to the historical
calibration.likelihood.build_estimand_expr(which now delegates here).- Return type:
Capabilities¶
Model capability detection for estimand gating.
model_capabilities() returns a set[str] of capability flags by cheap
duck-typing — no graph build, no posterior-predictive sampling. An
Estimand declares
required_capabilities; the evaluator returns an unsupported result when
they are not met (so a non-MMM model auto-filters MMM-only estimands rather than
crashing).
Capabilities are plain strings; parameterized ones use "NS:arg" (e.g.
"HAS_LATENT:awareness") so a typed need — a future CFA’s
"HAS_FACTOR_LOADINGS" — is gateable without a schema change.