Calibration

Experiment calibration: fold lift-test and holdout readouts into the model as priors or in-graph likelihoods.

Experiment calibration for the MMM framework.

Anchors the observational model to randomized incrementality evidence via two complementary routes:

  • Priors (ExperimentCalibrator) – a two-stage fit -> derive an experiment-anchored coefficient prior -> refit. Simple and robust; encodes a contribution only, holding the saturation/adstock shape fixed.

  • Likelihood (ExperimentMeasurement + BayesianMMM.add_experiment_calibration()) – folds the experiment into the PyMC graph as a likelihood term on the model-implied estimand (contribution, ROAS, or marginal ROAS), updating beta, the s-curve, and the adstock kernel jointly. More general; this is the route to use when the experiment is reported in ROAS / mROAS terms.

Examples

Prior route:

>>> from mmm_framework.calibration import ExperimentCalibrator
>>> calibrator = ExperimentCalibrator(fitted_model)        
>>> outcome = calibrator.calibrate(lift_tests)             

Likelihood route:

>>> from mmm_framework.calibration import (

… ExperimentMeasurement, ExperimentEstimand) >>> exp = ExperimentMeasurement( # doctest: +SKIP … “TV”, (“2023-01-01”, “2023-03-31”), value=2.5, se=0.4, … estimand=ExperimentEstimand.ROAS) >>> model.add_experiment_calibration([exp]).fit() # doctest: +SKIP

class mmm_framework.calibration.ExperimentCalibrator(model, results=None)[source]

Bases: object

Turn lift / incrementality experiments into informative channel priors.

Parameters

modelBayesianMMM

A fitted model (its posterior supplies the per-channel design factor). Call fit() first.

resultsoptional

Fit results container (passed through to the internal validator helper).

__init__(model, results=None)[source]
derive_priors(lift_tests, *, strict_geo=False)[source]

Derive experiment-calibrated priors without refitting.

Return type:

CalibrationReport

calibrated_config(report)[source]

Deep-copy the model’s MFFConfig with derived roi_prior applied.

Return type:

MFFConfig

calibrate(lift_tests, *, refit=True, draws=None, tune=None, chains=None, random_seed=None, strict_geo=False)[source]

Derive experiment-calibrated priors and (optionally) refit.

Returns a CalibrationOutcome with the derivation report, the config carrying the new priors, and – when refit – a freshly fitted model and its results.

Return type:

CalibrationOutcome

mmm_framework.calibration.calibrate_with_experiments(model, lift_tests, *, refit=True, draws=None, tune=None, chains=None, random_seed=None, strict_geo=False)[source]

Convenience wrapper: derive experiment-calibrated priors and refit.

Return type:

CalibrationOutcome

Examples

>>> from mmm_framework.calibration import calibrate_with_experiments
>>> from mmm_framework.validation import LiftTestResult
>>> base = BayesianMMM(panel, model_config); base.fit()  
>>> tests = [LiftTestResult("TV", ("2023-01-01", "2023-03-31"), 1.2e5, 2e4)]
>>> outcome = calibrate_with_experiments(base, tests)     
>>> outcome.model  # the experiment-anchored refit                
class mmm_framework.calibration.CalibrationOutcome(report, config, model=None, results=None)[source]

Bases: object

Result of a calibration run, optionally including the refit model.

report: CalibrationReport
config: MFFConfig
model: BayesianMMM | None = None
results: Any | None = None
__init__(report, config, model=None, results=None)
class mmm_framework.calibration.CalibrationReport(channel_calibrations=<factory>, skipped=<factory>)[source]

Bases: object

Per-channel derivation of experiment-calibrated priors.

channel_calibrations: list[ChannelCalibration]
skipped: list[tuple[str, str]]
priors()[source]

Map of channel -> derived roi_prior (only calibrated channels).

Return type:

dict[str, PriorConfig]

property calibrated_channels: list[str]
summary()[source]
Return type:

DataFrame

to_dict()[source]
Return type:

dict[str, Any]

__init__(channel_calibrations=<factory>, skipped=<factory>)
class mmm_framework.calibration.ChannelCalibration(channel, roi_prior, beta_target, beta_sigma, beta_fit_mean, observations=<factory>, notes=<factory>, skipped_reason=None)[source]

Bases: object

Derived experiment-calibrated prior for one channel.

channel: str
roi_prior: PriorConfig | None
beta_target: float | None
beta_sigma: float | None
beta_fit_mean: float | None
observations: list[LiftObservation]
notes: list[str]
skipped_reason: str | None = None
property calibrated: bool
to_dict()[source]
Return type:

dict[str, Any]

__init__(channel, roi_prior, beta_target, beta_sigma, beta_fit_mean, observations=<factory>, notes=<factory>, skipped_reason=None)
class mmm_framework.calibration.LiftObservation(test_period, measured_lift, lift_se, design_factor, usable, note='')[source]

Bases: object

A single lift test reduced to the coefficient scale.

measured_lift and lift_se are the experiment’s original-scale values; design_factor is the period-specific K_c. The coefficient-scale target is measured_lift / design_factor.

test_period: tuple[str, str]
measured_lift: float
lift_se: float
design_factor: float
usable: bool
note: str = ''
property beta_target: float | None
property beta_se: float | None
__init__(test_period, measured_lift, lift_se, design_factor, usable, note='')
mmm_framework.calibration.derive_channel_prior(channel, observations)[source]

Derive a channel’s coefficient prior from reduced lift observations.

Pure: takes pre-computed LiftObservation records (each carrying its own period-specific design_factor) and returns the combined prior. Lift tests with non-positive measured lift or design factor are excluded with a note (a positive-coefficient model cannot be anchored to them).

Return type:

ChannelCalibration

mmm_framework.calibration.design_factor(contribution_samples, beta_samples, *, min_beta=1e-06)[source]

Posterior-mean design factor K_c = E[contribution / beta].

Computed per draw (then averaged) rather than as a ratio of means, so the beta<->saturation posterior covariance does not bias the factor. K_c is the original-scale contribution that one unit of beta produces over the period the samples were drawn for.

Return type:

float

mmm_framework.calibration.combine_inverse_variance(targets, ses)[source]

Inverse-variance (fixed-effect meta-analytic) combination of estimates.

Return type:

tuple[float, float]

Parameters

targets, ses

Per-observation point estimates and their standard errors.

Returns

tuple[float, float]

Combined (mean, sd).

mmm_framework.calibration.mean_sd_to_gamma(mean, sd)[source]

Convert a target mean/sd into Gamma(alpha, beta=rate) parameters.

Matches the moments of a Gamma distribution to mean and sd so the derived prior is centered at mean with spread sd. pm.Gamma’s beta is the rate, which is what is returned.

Return type:

tuple[float, float]

Raises

ValueError

If mean is not strictly positive (a positive-coefficient model cannot be anchored at a non-positive effect).

class mmm_framework.calibration.ExperimentMeasurement(channel, test_period, value, se, estimand=ExperimentEstimand.CONTRIBUTION, spend_lift_pct=None, spend=None, holdout_regions=None, distribution='normal', name=None, outcome=None, eval_spend=None, eval_periods=None, eval_units=1, adstock_state='steady_state')[source]

Bases: object

A single experimental result to fold into the likelihood.

Parameters

channel:

Channel the experiment measured. Must be one of the model’s channels.

test_period:

(start, end) of the experiment window, as dates (parsed against the panel) or integer period indices (as strings or ints).

value:

Measured point estimate, on the natural scale of estimand.

se:

Standard error of value (the experiment’s uncertainty). Must be > 0.

estimand:

Whether value is a contribution, a ROAS, or a marginal ROAS.

spend_lift_pct:

For MROAS only: the percentage by which the experiment scaled the channel’s spend over the window (e.g. 10.0 for a +10% scaling cell). Required for MROAS; ignored otherwise.

spend:

Optional override for the ROAS spend denominator. When None the channel’s observed spend over the window is used. Not supported for MROAS – its marginal-spend denominator is spend_lift_pct x the observed window spend (the same spend the perturbation scales), so an independent override would desynchronise numerator and denominator.

holdout_regions:

Geos the experiment was restricted to. Restricts the obs mask to those geos so the estimand is the geo-restricted lift. Requires a geo model.

distribution:

Measurement-error family. "normal" (default) places a Normal likelihood on value; "lognormal" places a Normal on log(value) around log(estimand) with a moment-matched log-scale sd (median-matched; appropriate for strictly-positive ROAS).

name:

Optional explicit name for the likelihood node; auto-generated otherwise.

outcome:

For multi-outcome models (e.g. MultivariateMMM, CombinedMMM): which outcome the experiment measured. None for single-outcome models (the core model, NestedMMM).

eval_spend:

Off-panel calibration. The channel’s spend per period, per treated unit, on the raw dollar scale, during the experiment – supply this when the experiment ran in a window the model was not fit on. When set, the estimand is built by evaluating the channel’s global response curve (the same in-graph beta, saturation and adstock parameters) at this spend level instead of summing training-matrix rows, so the experiment window no longer has to overlap the training period. This is valid under structural stationarity – the response-curve parameters are assumed stable between the experiment period and the training period (see “Assumed semantics”). Leave None for the standard in-panel route (sum the contribution over the training rows inside the window).

eval_periods:

Off-panel calibration: the number of periods the experiment ran (the window length W). Required when eval_spend is set.

eval_units:

Off-panel calibration: the number of treated units (e.g. geos) that ran at eval_spend per period. Defaults to 1 (a single national-level stream). For a multi-geo holdout, set this to the number of treated geos – the estimand assumes those units ran at the same per-unit spend (homogeneous treatment). Ignored unless eval_spend is set.

adstock_state:

Off-panel calibration: carryover convention at eval_spend. "steady_state" (default) assumes the channel had been running at ~that spend long enough for adstock to converge (right for always-on / sustained-holdout / scale tests); "cold_start" assumes spend turned on from zero at the window start and carryover builds over W (right for a burst/pulse launched from dark). Always validated, but only affects the estimand when eval_spend is set.

channel: str
test_period: tuple[Any, Any]
value: float
se: float
estimand: ExperimentEstimand = 'contribution'
spend_lift_pct: float | None = None
spend: float | None = None
holdout_regions: list[str] | None = None
distribution: str = 'normal'
name: str | None = None
outcome: str | None = None
eval_spend: float | None = None
eval_periods: int | None = None
eval_units: int = 1
adstock_state: str = 'steady_state'
to_dict()[source]
Return type:

dict[str, Any]

classmethod from_dict(data)[source]
Return type:

ExperimentMeasurement

classmethod from_lift_test(lift_test)[source]

Bridge a LiftTestResult to a contribution measurement.

A full-holdout lift test measures the channel’s total incremental KPI over its window – exactly the CONTRIBUTION estimand.

Return type:

ExperimentMeasurement

default_node_name(index)[source]
Return type:

str

__init__(channel, test_period, value, se, estimand=ExperimentEstimand.CONTRIBUTION, spend_lift_pct=None, spend=None, holdout_regions=None, distribution='normal', name=None, outcome=None, eval_spend=None, eval_periods=None, eval_units=1, adstock_state='steady_state')
class mmm_framework.calibration.ExperimentEstimand(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, Enum

Unit in which an experiment’s result is expressed.

The three estimands are the same counterfactual contrast viewed three ways: CONTRIBUTION is the total incremental KPI over the window; ROAS divides it by the window’s spend; MROAS is the marginal version – incremental KPI from a small spend perturbation, per incremental dollar.

CONTRIBUTION = 'contribution'
ROAS = 'roas'
MROAS = 'mroas'
mmm_framework.calibration.attach_experiment_likelihood(name, estimand_expr, measurement)[source]

Add an observed likelihood comparing estimand_expr to a measurement.

Reusable across model types: build the model-implied estimand tensor however your graph allows (it must be on the estimand’s natural scale – KPI units for a contribution, KPI-per-dollar for ROAS/mROAS), then call this inside the pm.Model context to fold the experiment into the joint posterior.

Parameters

name:

Name of the observed node (must be unique within the model).

estimand_expr:

PyTensor scalar: the model-implied estimand on the measurement’s scale.

measurement:

The experimental result; its value, se, and distribution define the likelihood.

Returns

The created observed random variable.

mmm_framework.calibration.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_window is the summed per-obs contribution over the experiment window in model units; scale converts it to the estimand’s natural scale (y_std for a standardized model, 1.0 for a raw-y model). spend_window is the observed window spend (the ROAS / marginal-spend denominator).

For MROAS, contrib_window_pert is 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:

Any

mmm_framework.calibration.lognormal_sigma_from_moments(value, se)[source]

Log-scale sd of a lognormal whose natural-scale CV is se / value.

For X ~ LogNormal(mu, sigma) the coefficient of variation is sqrt(exp(sigma**2) - 1). Inverting at CV = se / value gives the log-scale spread that reproduces the experiment’s relative uncertainty:

sigma = sqrt( ln( 1 + (se / value)**2 ) )

Used to place a (median-matched) lognormal measurement-error likelihood on a strictly-positive estimand such as ROAS.

Return type:

float

Likelihood

In-graph experiment likelihoods for the Bayesian MMM.

Where mmm_framework.calibration.experiment folds a lift test into an informative prior on a channel’s coefficient (a two-stage fit -> derive -> refit), this module folds an experiment in as a likelihood term inside the PyMC graph. The experiment’s measured value becomes a data point whose model expectation is the channel’s estimand – contribution, ROAS, or marginal ROAS – expressed as a deterministic function of the same beta, saturation, and adstock parameters the time-series likelihood already estimates.

Why a likelihood (vs. a prior)

The prior route (ExperimentCalibrator) holds the first-stage saturation/adstock shape fixed when it inverts a measured lift to a coefficient target beta_target = measured_lift / K_c – so the derived prior is marginally tighter than a fully joint treatment would justify, and it can only encode a contribution (it has no notion of ROAS or mROAS). Adding the experiment as a likelihood instead lets it update beta, the s-curve, and the adstock kernel jointly, and it generalises to any estimand that can be written as a function of the graph:

measured_value  ~  Normal( model_implied_estimand(theta), measured_se )

where theta are the channel’s in-graph parameters and the estimand is one of:

  • contribution – total incremental KPI over the experiment window P: y_std * sum_{t in P} beta_c * sat_c(adstock_c(x_{c,t})) (exactly the full-holdout counterfactual, since sat_c(0) = 0);

  • ROAS – that contribution divided by the channel’s observed spend over P (a known constant);

  • marginal ROAS – the incremental KPI from scaling the channel’s spend over P by spend_lift_pct, divided by the incremental spend; this re-evaluates the s-curve/adstock at the perturbed spend (a finite-difference matching how a geo scaling experiment is actually run).

The numpy-pure pieces (data structures, the lognormal moment conversion) live here so they are unit-testable without PyMC; the model wires the per-channel estimand tensors in mmm_framework.model.base.BayesianMMM._add_experiment_likelihoods() and calls attach_experiment_likelihood() to add the observed node.

Assumed semantics (read before use)

  • value / se are on the experiment’s natural scale: KPI units for contribution; KPI-per-spend-dollar for roas / mroas. The model estimand is converted to the same scale (via y_std and observed spend), so units must match – a revenue KPI gives a true ROAS; a unit-volume KPI gives a cost-per-acquisition-inverse.

  • The experiment window is summed at the model’s aggregation level. With holdout_regions the obs mask is restricted to those geos so the estimand is the geo-restricted lift (the coefficient is still the pooled one); specifying holdout_regions on a model with no geo dimension is a configuration error.

  • Carryover generated during P that lands after P is not counted (the estimand sums only over P), matching compute_marginal_contributions().

  • The time-series likelihood already sees the experiment window; adding the experiment as a second, independently-weighted measurement of the same period is the standard lift-calibration treatment (PyMC-Marketing, Meridian) – it is not double counting in the pathological sense, but the experiment’s se is what governs how hard it pulls the fit.

Off-panel calibration (experiment ran in a different period)

When an experiment was run in a window the model was not fit on, set eval_spend (+ eval_periods / eval_units / adstock_state) on the measurement. The estimand is then built by evaluating the channel’s global response curve beta_c * sat_c(adstock_c(.)) at the experiment’s own spend level – a deterministic function of the same in-graph structural parameters – without indexing any training row. This rests on one assumption made explicit here:

  • Structural stationarity. The channel’s response-curve parameters (beta_c, the saturation shape, the adstock kernel) are assumed stable between the experiment period and the training period. The experiment is evidence about these global, time-invariant parameters, so a measurement from a non-overlapping window still constrains them – provided the curve has not shifted. This is the standard (usually implicit) assumption behind seeding a future-period MMM with a past lift test; off-panel mode only makes it load-bearing and visible. If you have reason to believe the curve moved (a major creative/format change, a structural break), prefer an in-window test. Steady-state vs cold-start carryover at eval_spend is chosen per experiment via adstock_state.

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

Bases: str, Enum

Unit in which an experiment’s result is expressed.

The three estimands are the same counterfactual contrast viewed three ways: CONTRIBUTION is the total incremental KPI over the window; ROAS divides it by the window’s spend; MROAS is the marginal version – incremental KPI from a small spend perturbation, per incremental dollar.

CONTRIBUTION = 'contribution'
ROAS = 'roas'
MROAS = 'mroas'
class mmm_framework.calibration.likelihood.ExperimentMeasurement(channel, test_period, value, se, estimand=ExperimentEstimand.CONTRIBUTION, spend_lift_pct=None, spend=None, holdout_regions=None, distribution='normal', name=None, outcome=None, eval_spend=None, eval_periods=None, eval_units=1, adstock_state='steady_state')[source]

Bases: object

A single experimental result to fold into the likelihood.

Parameters

channel:

Channel the experiment measured. Must be one of the model’s channels.

test_period:

(start, end) of the experiment window, as dates (parsed against the panel) or integer period indices (as strings or ints).

value:

Measured point estimate, on the natural scale of estimand.

se:

Standard error of value (the experiment’s uncertainty). Must be > 0.

estimand:

Whether value is a contribution, a ROAS, or a marginal ROAS.

spend_lift_pct:

For MROAS only: the percentage by which the experiment scaled the channel’s spend over the window (e.g. 10.0 for a +10% scaling cell). Required for MROAS; ignored otherwise.

spend:

Optional override for the ROAS spend denominator. When None the channel’s observed spend over the window is used. Not supported for MROAS – its marginal-spend denominator is spend_lift_pct x the observed window spend (the same spend the perturbation scales), so an independent override would desynchronise numerator and denominator.

holdout_regions:

Geos the experiment was restricted to. Restricts the obs mask to those geos so the estimand is the geo-restricted lift. Requires a geo model.

distribution:

Measurement-error family. "normal" (default) places a Normal likelihood on value; "lognormal" places a Normal on log(value) around log(estimand) with a moment-matched log-scale sd (median-matched; appropriate for strictly-positive ROAS).

name:

Optional explicit name for the likelihood node; auto-generated otherwise.

outcome:

For multi-outcome models (e.g. MultivariateMMM, CombinedMMM): which outcome the experiment measured. None for single-outcome models (the core model, NestedMMM).

eval_spend:

Off-panel calibration. The channel’s spend per period, per treated unit, on the raw dollar scale, during the experiment – supply this when the experiment ran in a window the model was not fit on. When set, the estimand is built by evaluating the channel’s global response curve (the same in-graph beta, saturation and adstock parameters) at this spend level instead of summing training-matrix rows, so the experiment window no longer has to overlap the training period. This is valid under structural stationarity – the response-curve parameters are assumed stable between the experiment period and the training period (see “Assumed semantics”). Leave None for the standard in-panel route (sum the contribution over the training rows inside the window).

eval_periods:

Off-panel calibration: the number of periods the experiment ran (the window length W). Required when eval_spend is set.

eval_units:

Off-panel calibration: the number of treated units (e.g. geos) that ran at eval_spend per period. Defaults to 1 (a single national-level stream). For a multi-geo holdout, set this to the number of treated geos – the estimand assumes those units ran at the same per-unit spend (homogeneous treatment). Ignored unless eval_spend is set.

adstock_state:

Off-panel calibration: carryover convention at eval_spend. "steady_state" (default) assumes the channel had been running at ~that spend long enough for adstock to converge (right for always-on / sustained-holdout / scale tests); "cold_start" assumes spend turned on from zero at the window start and carryover builds over W (right for a burst/pulse launched from dark). Always validated, but only affects the estimand when eval_spend is set.

channel: str
test_period: tuple[Any, Any]
value: float
se: float
estimand: ExperimentEstimand = 'contribution'
spend_lift_pct: float | None = None
spend: float | None = None
holdout_regions: list[str] | None = None
distribution: str = 'normal'
name: str | None = None
outcome: str | None = None
eval_spend: float | None = None
eval_periods: int | None = None
eval_units: int = 1
adstock_state: str = 'steady_state'
to_dict()[source]
Return type:

dict[str, Any]

classmethod from_dict(data)[source]
Return type:

ExperimentMeasurement

classmethod from_lift_test(lift_test)[source]

Bridge a LiftTestResult to a contribution measurement.

A full-holdout lift test measures the channel’s total incremental KPI over its window – exactly the CONTRIBUTION estimand.

Return type:

ExperimentMeasurement

default_node_name(index)[source]
Return type:

str

__init__(channel, test_period, value, se, estimand=ExperimentEstimand.CONTRIBUTION, spend_lift_pct=None, spend=None, holdout_regions=None, distribution='normal', name=None, outcome=None, eval_spend=None, eval_periods=None, eval_units=1, adstock_state='steady_state')
class mmm_framework.calibration.likelihood.ShareMeasurement(channel, breakouts, shares, log_ratio_cov=None, concentration=None, distribution='logistic_normal', name=None, source=None)[source]

Bases: object

An observed within-channel share vector to fold into the likelihood.

Where ExperimentMeasurement carries a scalar estimand (contribution / ROAS / mROAS) for one channel, this carries a composition: the observed mean shares of a virtual parent channel’s breakout sub-streams (e.g. from a continuous-learning program’s per-arm posterior, or a creative-level lift study). The model-implied counterpart is a simplex expression such as the breakout model’s breakout_share_<C> Deterministic; the two are compared via attach_share_likelihood().

Two measurement-error families:

  • "logistic_normal" (default) – an MvNormal on the K-1 additive log-ratios (ALR) w.r.t. the last breakout as reference: z_k = log(share_k / share_K). Matches the simplex’s K-1 degrees of freedom exactly and preserves the correlation structure of the source draws. Requires log_ratio_cov.

  • "dirichlet"shares ~ Dirichlet(concentration * model_share). A single-scalar-precision alternative with the Dirichlet’s rigid negative-correlation structure. Requires concentration.

Parameters

channel:

The VIRTUAL parent channel the shares decompose (must be one of the model’s channels, e.g. a breakout group’s parent name).

breakouts:

The parent’s sub-stream column names (MFF media columns), in the explicit order the shares (and log_ratio_cov) refer to. At least 2, unique. The ALR reference is the last entry.

shares:

Observed mean shares, one per breakout, same order. Must be non-negative and sum to ~1; they are floored at a tiny epsilon and renormalized to an exact simplex on construction.

log_ratio_cov:

(K-1, K-1) covariance of the ALR log-ratios (last breakout as reference). Must be symmetric and strictly positive definite (checked via Cholesky – a singular/PSD matrix would make the MvNormal logp -inf deep inside the fit; add a small diagonal ridge, e.g. 1e-9, to an empirically-estimated covariance). Required for "logistic_normal"; must be None for "dirichlet". NOTE: the ALR coordinates depend on the breakout ORDER – reordering breakouts requires re-deriving the covariance, so consumers require an exact order match rather than reindexing.

concentration:

Dirichlet precision (larger = tighter). Required for "dirichlet"; must be None for "logistic_normal".

distribution:

"logistic_normal" (default) or "dirichlet".

name:

Optional explicit name for the likelihood node; auto-generated otherwise.

source:

Optional provenance (e.g. {"program_id": ..., "wave": ..., "mode": ..., "spend_ref": ...}) – used by consumers to warn about double counting when the same program also produced a scalar parent-level readout.

channel: str
breakouts: tuple[str, ...]
shares: tuple[float, ...]
log_ratio_cov: tuple[tuple[float, ...], ...] | None = None
concentration: float | None = None
distribution: str = 'logistic_normal'
name: str | None = None
source: dict | None = None
to_dict()[source]
Return type:

dict[str, Any]

classmethod from_dict(data)[source]
Return type:

ShareMeasurement

default_node_name(index)[source]
Return type:

str

__init__(channel, breakouts, shares, log_ratio_cov=None, concentration=None, distribution='logistic_normal', name=None, source=None)
mmm_framework.calibration.likelihood.attach_experiment_likelihood(name, estimand_expr, measurement)[source]

Add an observed likelihood comparing estimand_expr to a measurement.

Reusable across model types: build the model-implied estimand tensor however your graph allows (it must be on the estimand’s natural scale – KPI units for a contribution, KPI-per-dollar for ROAS/mROAS), then call this inside the pm.Model context to fold the experiment into the joint posterior.

Parameters

name:

Name of the observed node (must be unique within the model).

estimand_expr:

PyTensor scalar: the model-implied estimand on the measurement’s scale.

measurement:

The experimental result; its value, se, and distribution define the likelihood.

Returns

The created observed random variable.

mmm_framework.calibration.likelihood.attach_share_likelihood(name, share_expr, measurement)[source]

Add an observed likelihood comparing a model simplex to measured shares.

Mirrors attach_experiment_likelihood() for compositional evidence: share_expr is the model-implied share vector (a length-K simplex tensor, e.g. the breakout model’s breakout_share_<C> Deterministic) and measurement carries the observed shares in the SAME breakout order. Call inside the pm.Model context.

"logistic_normal" places an MvNormal on the K-1 additive log-ratios (last component as reference) – matching the simplex’s degrees of freedom exactly; "dirichlet" places a Dirichlet with a = concentration * model_share. Both clip/floor at a tiny epsilon for numerical safety (the model share is strictly interior by construction in the breakout model).

Parameters

name:

Name of the observed node (must be unique within the model).

share_expr:

PyTensor vector: the model-implied shares, length K matching measurement.breakouts in order.

measurement:

The observed share composition and its measurement-error family.

Returns

The created observed random variable.

mmm_framework.calibration.likelihood.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_window is the summed per-obs contribution over the experiment window in model units; scale converts it to the estimand’s natural scale (y_std for a standardized model, 1.0 for a raw-y model). spend_window is the observed window spend (the ROAS / marginal-spend denominator).

For MROAS, contrib_window_pert is 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:

Any

mmm_framework.calibration.likelihood.lognormal_sigma_from_moments(value, se)[source]

Log-scale sd of a lognormal whose natural-scale CV is se / value.

For X ~ LogNormal(mu, sigma) the coefficient of variation is sqrt(exp(sigma**2) - 1). Inverting at CV = se / value gives the log-scale spread that reproduces the experiment’s relative uncertainty:

sigma = sqrt( ln( 1 + (se / value)**2 ) )

Used to place a (median-matched) lognormal measurement-error likelihood on a strictly-positive estimand such as ROAS.

Return type:

float