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), updatingbeta, 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:
objectTurn 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_priorapplied.- Return type:
- 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
CalibrationOutcomewith the derivationreport, theconfigcarrying the new priors, and – whenrefit– a freshly fittedmodeland itsresults.- 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:
objectResult of a calibration run, optionally including the refit model.
-
report:
CalibrationReport
-
config:
MFFConfig
-
model:
BayesianMMM|None= None
- __init__(report, config, model=None, results=None)
-
report:
- class mmm_framework.calibration.CalibrationReport(channel_calibrations=<factory>, skipped=<factory>)[source]
Bases:
objectPer-channel derivation of experiment-calibrated priors.
-
channel_calibrations:
list[ChannelCalibration]
- priors()[source]
Map of channel -> derived
roi_prior(only calibrated channels).- Return type:
- __init__(channel_calibrations=<factory>, skipped=<factory>)
-
channel_calibrations:
- class mmm_framework.calibration.ChannelCalibration(channel, roi_prior, beta_target, beta_sigma, beta_fit_mean, observations=<factory>, notes=<factory>, skipped_reason=None)[source]
Bases:
objectDerived experiment-calibrated prior for one channel.
-
channel:
str
-
roi_prior:
PriorConfig|None
-
observations:
list[LiftObservation]
- property calibrated: bool
- __init__(channel, roi_prior, beta_target, beta_sigma, beta_fit_mean, observations=<factory>, notes=<factory>, skipped_reason=None)
-
channel:
- class mmm_framework.calibration.LiftObservation(test_period, measured_lift, lift_se, design_factor, usable, note='')[source]
Bases:
objectA single lift test reduced to the coefficient scale.
measured_liftandlift_seare the experiment’s original-scale values;design_factoris the period-specificK_c. The coefficient-scale target ismeasured_lift / design_factor.-
measured_lift:
float
-
lift_se:
float
-
design_factor:
float
-
usable:
bool
-
note:
str= ''
- __init__(test_period, measured_lift, lift_se, design_factor, usable, note='')
-
measured_lift:
- mmm_framework.calibration.derive_channel_prior(channel, observations)[source]
Derive a channel’s coefficient prior from reduced lift observations.
Pure: takes pre-computed
LiftObservationrecords (each carrying its own period-specificdesign_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_cis the original-scale contribution that one unit ofbetaproduces over the period the samples were drawn for.- Return type:
- mmm_framework.calibration.combine_inverse_variance(targets, ses)[source]
Inverse-variance (fixed-effect meta-analytic) combination of estimates.
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
meanandsdso the derived prior is centered atmeanwith spreadsd.pm.Gamma’sbetais the rate, which is what is returned.Raises¶
- ValueError
If
meanis 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:
objectA 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
valueis a contribution, a ROAS, or a marginal ROAS.- spend_lift_pct:
For
MROASonly: the percentage by which the experiment scaled the channel’s spend over the window (e.g.10.0for a +10% scaling cell). Required forMROAS; ignored otherwise.- spend:
Optional override for the
ROASspend denominator. WhenNonethe channel’s observed spend over the window is used. Not supported forMROAS– its marginal-spend denominator isspend_lift_pctx 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 onvalue;"lognormal"places a Normal onlog(value)aroundlog(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.Nonefor 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”). LeaveNonefor 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 wheneval_spendis set.- eval_units:
Off-panel calibration: the number of treated units (e.g. geos) that ran at
eval_spendper period. Defaults to1(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 unlesseval_spendis 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 overW(right for a burst/pulse launched from dark). Always validated, but only affects the estimand wheneval_spendis set.
-
channel:
str
-
value:
float
-
se:
float
-
estimand:
ExperimentEstimand= 'contribution'
-
distribution:
str= 'normal'
-
eval_units:
int= 1
-
adstock_state:
str= 'steady_state'
- classmethod from_dict(data)[source]
- Return type:
- classmethod from_lift_test(lift_test)[source]
Bridge a
LiftTestResultto a contribution measurement.A full-holdout lift test measures the channel’s total incremental KPI over its window – exactly the
CONTRIBUTIONestimand.- Return type:
- __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]
-
Unit in which an experiment’s result is expressed.
The three estimands are the same counterfactual contrast viewed three ways:
CONTRIBUTIONis the total incremental KPI over the window;ROASdivides it by the window’s spend;MROASis 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_exprto 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.Modelcontext 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, anddistributiondefine 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_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:
- 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 issqrt(exp(sigma**2) - 1). Inverting atCV = se / valuegives 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:
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, sincesat_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
Pbyspend_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/seare on the experiment’s natural scale: KPI units forcontribution; KPI-per-spend-dollar forroas/mroas. The model estimand is converted to the same scale (viay_stdand 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_regionsthe obs mask is restricted to those geos so the estimand is the geo-restricted lift (the coefficient is still the pooled one); specifyingholdout_regionson a model with no geo dimension is a configuration error.Carryover generated during
Pthat lands afterPis not counted (the estimand sums only overP), matchingcompute_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
seis 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 ateval_spendis chosen per experiment viaadstock_state.
- class mmm_framework.calibration.likelihood.ExperimentEstimand(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
-
Unit in which an experiment’s result is expressed.
The three estimands are the same counterfactual contrast viewed three ways:
CONTRIBUTIONis the total incremental KPI over the window;ROASdivides it by the window’s spend;MROASis 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:
objectA 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
valueis a contribution, a ROAS, or a marginal ROAS.- spend_lift_pct:
For
MROASonly: the percentage by which the experiment scaled the channel’s spend over the window (e.g.10.0for a +10% scaling cell). Required forMROAS; ignored otherwise.- spend:
Optional override for the
ROASspend denominator. WhenNonethe channel’s observed spend over the window is used. Not supported forMROAS– its marginal-spend denominator isspend_lift_pctx 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 onvalue;"lognormal"places a Normal onlog(value)aroundlog(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.Nonefor 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”). LeaveNonefor 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 wheneval_spendis set.- eval_units:
Off-panel calibration: the number of treated units (e.g. geos) that ran at
eval_spendper period. Defaults to1(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 unlesseval_spendis 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 overW(right for a burst/pulse launched from dark). Always validated, but only affects the estimand wheneval_spendis set.
-
estimand:
ExperimentEstimand= 'contribution'¶
- classmethod from_lift_test(lift_test)[source]¶
Bridge a
LiftTestResultto a contribution measurement.A full-holdout lift test measures the channel’s total incremental KPI over its window – exactly the
CONTRIBUTIONestimand.- Return type:
- __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')¶
Bases:
objectAn observed within-channel share vector to fold into the likelihood.
Where
ExperimentMeasurementcarries 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’sbreakout_share_<C>Deterministic; the two are compared viaattach_share_likelihood().Two measurement-error families:
"logistic_normal"(default) – an MvNormal on theK-1additive log-ratios (ALR) w.r.t. the last breakout as reference:z_k = log(share_k / share_K). Matches the simplex’sK-1degrees of freedom exactly and preserves the correlation structure of the source draws. Requireslog_ratio_cov."dirichlet"–shares ~ Dirichlet(concentration * model_share). A single-scalar-precision alternative with the Dirichlet’s rigid negative-correlation structure. Requiresconcentration.
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(andlog_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 theMvNormallogp-infdeep inside the fit; add a small diagonal ridge, e.g.1e-9, to an empirically-estimated covariance). Required for"logistic_normal"; must beNonefor"dirichlet". NOTE: the ALR coordinates depend on the breakout ORDER – reorderingbreakoutsrequires re-deriving the covariance, so consumers require an exact order match rather than reindexing.- concentration:
Dirichlet precision (larger = tighter). Required for
"dirichlet"; must beNonefor"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.
- Return type:
- Return type:
- mmm_framework.calibration.likelihood.attach_experiment_likelihood(name, estimand_expr, measurement)[source]¶
Add an observed likelihood comparing
estimand_exprto 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.Modelcontext 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, anddistributiondefine the likelihood.
Returns¶
The created observed random variable.
Add an observed likelihood comparing a model simplex to measured shares.
Mirrors
attach_experiment_likelihood()for compositional evidence:share_expris the model-implied share vector (a length-Ksimplex tensor, e.g. the breakout model’sbreakout_share_<C>Deterministic) andmeasurementcarries the observed shares in the SAME breakout order. Call inside thepm.Modelcontext."logistic_normal"places an MvNormal on theK-1additive log-ratios (last component as reference) – matching the simplex’s degrees of freedom exactly;"dirichlet"places a Dirichlet witha = 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
Kmatchingmeasurement.breakoutsin 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_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:
- 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 issqrt(exp(sigma**2) - 1). Inverting atCV = se / valuegives 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: