MMM Calibration Export#
Assemble Marketing Mix Model (MMM) calibration inputs from experiment results.
Overview#
MMM practitioners calibrate their models against experimental evidence. The two dominant Python MMM frameworks consume that evidence in different shapes:
PyMC-Marketing (and prophetverse, which mirrors the same schema) ingests a lift-test DataFrame - columns
channel, optional model dims (e.g.geo),x,delta_x,delta_y,sigma- viaMMM.add_lift_test_measurements.Google Meridian calibrates through lognormal priors:
roi_mfor the return on a channel’s full spend (a zero-spend / full-holdout estimand) andmroi_mfor a marginal return. The point estimate maps to the prior mean and its standard error to the prior standard deviation, converted to lognormal(mu, sigma)with Google’s closed form.
Explicit in, validated out. These functions do not rescale a result’s headline
ATT. Reconciling an experiment’s estimate to a calibration input needs context
diff-diff cannot see - the target MMM’s row granularity (per-geo vs national), its
time window, and the outcome’s scale (additive levels vs a log/rate/share) - so the
caller supplies the already-scoped incremental outcome and its standard error (the
numbers read off a fitted result’s summary(), aggregated to the population and
window one MMM row represents). diff-diff does only what it can verify: assemble the
exact schema, enforce each consumer’s guards, convert to the lognormal
parameterization (parity with Google’s closed form), pool, and emit snippets. It is
pure numpy/pandas and imports no MMM package.
to_pymc_marketing_lift_test#
Assemble a lift-test DataFrame for pymc_marketing.mmm.MMM.add_lift_test_measurements.
- diff_diff.to_pymc_marketing_lift_test(*, channel, x, delta_x, delta_y, sigma, dims=None, on_wrong_sign='raise')[source]#
Assemble a PyMC-Marketing lift-test DataFrame from scoped experiment results.
Produces one row per experiment with columns
channel, anydimscolumns,x,delta_x,delta_y,sigma- the exact schema consumed bypymc_marketing.mmm.MMM.add_lift_test_measurements(prophetverse’s lift-test API accepts the same shape). All values stay in original data units (PyMC-Marketing rescales internally).The caller supplies the scoped effect.
delta_yandsigmaare the measured incremental outcome and its standard error, already aggregated to the population and time window ONE target-MMM row represents. This function does not rescale an ATT, because the reconciliation needs the MMM’s row granularity and outcome scale, which it cannot see. Read the effect and SE off your fitted result’ssummary()and scope them yourself: PyMC-Marketing scores one row’sdelta_yagainstsaturation(x + delta_x) - saturation(x), sox,delta_x,delta_y, andsigmamust all describe the SAME observation (same channel, same population, same period span, additive-level outcome).- Parameters:
channel (str or sequence of str) – MMM channel name(s); must match the target model’s
channel_columns.x (float or sequence of float) – Baseline channel spend for the row’s observation, in original spend units.
delta_x (float or sequence of float) – Spend change during the experiment (nonzero; negative for go-dark/holdout tests, with
x + delta_x >= 0), in the same units asx.delta_y (float or sequence of float) – Measured incremental outcome for the SAME observation as
x/delta_x, in original outcome units. Finite; must be nonzero and sharedelta_x’s sign (seeon_wrong_sign).sigma (float or sequence of float) – Standard error of
delta_y(finite, positive).dims (mapping or sequence of mappings, optional) – Extra model-dimension columns, e.g.
{"geo": "US-CA"}for a geo-level MMM built withMMM(dims=("geo",)). Values must match the target model’s coordinate values exactly and identify the populationdelta_ydescribes (do not label a multi-geo average with one specific geo). All rows must share one key set; column order follows the first mapping.on_wrong_sign ({"raise", "drop", "keep"}, default "raise") – Policy for rows PyMC-Marketing cannot use:
sign(delta_y)contradictingsign(delta_x)(rejected upstream withNonMonotonicError), ordelta_y == 0(degenerate for its strictly-positive Gamma lift likelihood, which its own monotonicity check does not catch)."raise"(default) errors with guidance;"drop"warns and removes such rows (raises if that would empty the frame);"keep"warns and emits them anyway (for non-PyMC consumers - the frame is not valid PyMC-Marketing input).
- Returns:
Columns
[channel, *dims, x, delta_x, delta_y, sigma], one row per experiment.- Return type:
pd.DataFrame
- Raises:
ValueError – On invalid inputs (non-positive
sigma, non-finite values,delta_x == 0,x + delta_x < 0, broadcasting length mismatches, empty inputs, heterogeneousdimskey sets) or wrong-signed/zero rows under the default policy.
Example#
from diff_diff import SyntheticDiD, to_pymc_marketing_lift_test
# Single-treated-geo experiment (US-CA): TV spend raised there vs control
# geos. With exactly ONE treated geo, the SDID ATT is that geo's per-week
# lift, so labelling the row with its coordinate is sound. (For MULTIPLE
# treated geos the pooled ATT is an average - do not assign it to one geo;
# export each geo's own effect, or omit the geo dim and match aggregate
# spend to an aggregate lift.)
result = SyntheticDiD().fit(panel, outcome='revenue', treatment='treated',
unit='geo', time='week')
df_lift = to_pymc_marketing_lift_test(
channel='tv',
x=50_000.0, # baseline weekly TV spend in US-CA
delta_x=20_000.0, # spend increase during the test
delta_y=result.att, # US-CA's per-week lift (one treated geo)
sigma=result.se, # its standard error
dims={'geo': 'US-CA'},
)
# -> columns [channel, geo, x, delta_x, delta_y, sigma], ready for
# MMM.add_lift_test_measurements(df_lift)
to_meridian_roi_prior#
Build a Meridian lognormal roi_m/mroi_m prior from scoped experiment result(s).
- diff_diff.to_meridian_roi_prior(*, incremental_outcome, incremental_outcome_se, spend, parameter='roi_m', se_widening=1.0)[source]#
Build a Meridian lognormal ROI/mROI prior from scoped experiment result(s).
Per experiment
roi = incremental_outcome / spendwith standard deviationincremental_outcome_se / spend * se_widening. Multiple experiments for the same channel are pooled with spend weights - the spend-weighted average ROI the Meridian FAQ suggests for multiple experiments (citing section 3.4 of Google’s MMM calibration whitepaper):roi_mean = sum(w_i * roi_i)withw_i = spend_i / sum(spend). The pooledroi_sd = sqrt(sum((w_i * sd_i)^2))is this library’s uncertainty propagation for that average (treats experiments as independent - widen viase_wideningwhen experiments share controls or windows). The pooled mean/sd map to lognormal(mu, sigma)via Google’s closed form.The caller supplies the scoped estimand.
incremental_outcomeis the total incremental outcome the experiment measured, matching the estimand of the target prior: forparameter="roi_m"that is the outcome attributable to the channel’s spend against a zero-spend counterfactual (a full holdout), divided here byspend= the channel’s total spend over the window; forparameter="mroi_m"it is the marginal outcome of the spend change, divided by that spend change. Sign, aggregation, and population are the caller’s responsibility - diff-diff does not rescale a headline ATT.- Parameters:
incremental_outcome (float or sequence of float) – Total incremental outcome per experiment (the estimand matching
parameter), in the same currency/units as the MMM. Must be finite; the pooled ROI must be positive (lognormal support).incremental_outcome_se (float or sequence of float) – Standard error of
incremental_outcome(finite, positive), computed for the caller’s aggregation.spend (float or sequence of float) – Spend the incremental outcome is divided by (finite, positive): the channel’s total spend over the window for
roi_m, or the spend change formroi_m.parameter ({"roi_m", "mroi_m"}, default "roi_m") – Which Meridian prior the estimate informs.
roi_mis the return on the channel’s full spend (a full-holdout / zero-spend estimand);mroi_mis the marginal return of a spend change. Under saturation these differ, so the caller declares which theirincremental_outcomemeasures; the returned prior and.to_code()target that parameter with its own Meridian default for non-experiment channels.se_widening (float, default 1.0) – Multiplier on each experiment’s ROI standard deviation (finite, positive). Values above 1 encode skepticism about experiment-to-MMM transferability.
- Returns:
Pooled
roi_mean/roi_sd, the lognormalmu/sigma, the targetparameter, per-experiment detail, plus.to_dict()and the channel-scoped.to_code()snippet helper.- Return type:
- Raises:
ValueError – On an invalid
parameter, non-positivespend/se/se_widening, non-finite inputs, broadcasting mismatches, empty inputs, a non-positive pooled ROI mean (lognormal support), or non-finite scaled/pooled results.
Example#
from diff_diff import DifferenceInDifferences, to_meridian_roi_prior
result = DifferenceInDifferences().fit(panel, outcome='revenue',
treatment='treated', time='post')
# Caller aggregates the ATT to a total incremental outcome over the treated
# population and window (e.g. att x treated units x post periods) and supplies
# its SE - diff-diff does not rescale the headline effect.
prior = to_meridian_roi_prior(
incremental_outcome=180_000.0, # total incremental revenue
incremental_outcome_se=45_000.0, # its standard error
spend=200_000.0, # total channel spend over the window
parameter='roi_m', # or 'mroi_m' for a marginal experiment
se_widening=1.5, # optional transferability skepticism
)
print(prior.roi_mean, prior.roi_sd)
# Channel- and time-scoped snippet: roi_m is per-channel, and the prior was
# estimated on the experiment window.
print(prior.to_code(channel='tv', media_channels=['search', 'tv'],
roi_calibration_period='experiment_window_mask'))
MeridianROIPrior#
Result container for the Meridian exporter.
- class diff_diff.MeridianROIPrior[source]
Bases:
objectLognormal prior parameters for Meridian’s
roi_m/mroi_mcalibration.mu/sigmareproducemeridian.model.prior_distribution. lognormal_dist_from_mean_std(roi_mean, roi_sd)exactly (closed form; no Meridian dependency).parameterrecords which Meridian prior the caller is informing ("roi_m"or"mroi_m").per_experimentrecords each pooled experiment’s ROI, widened sd, spend, and spend weight.- roi_mean: float
- roi_sd: float
- mu: float
- sigma: float
- parameter: str = 'roi_m'
- per_experiment: Tuple[ExperimentROI, ...] = ()
- to_code(*, channel=None, media_channels=None, single_channel=False, roi_calibration_period=None, full_model_window=False)[source]
Ready-to-paste Meridian snippet (channel- and time-scoped; 1.7.0 pinned).
Meridian’s
roi_m/mroi_mprior has batch shapen_media_channelsand a scalar distribution broadcasts to EVERY media channel - a TV experiment’s prior must not silently calibrate search, social, etc. Channel scope is therefore always explicit:to_code(channel="tv", media_channels=["search", "tv"])emits vectormu/sigmain exactly themedia_channelsorder (which must match the MeridianInputDatachannel order); the experiment channel carries this prior and every other channel keeps Meridian’s default.to_code(single_channel=True)emits the scalar snippet for single-channel models, marked as such in the generated code.Calling with neither raises
ValueError.
The prior’s TIME scope is also required: Meridian’s default
roi_calibration_period=Noneapplies the prior over all model times, but the prior was estimated on the experiment window. Passroi_calibration_period="<expression>"(the boolean(n_media_times, n_media_channels)mask for your window) orfull_model_window=Trueto acknowledge that the two coincide.Snippets use the TensorFlow substrate of TensorFlow Probability; JAX-backed Meridian users should swap the import for
tensorflow_probability.substrates.jax(noted in the generated code).
References#
PyMC-Marketing lift-test calibration: https://www.pymc-marketing.io/en/stable/notebooks/mmm/mmm_lift_test.html
Google Meridian, “Set custom prior distributions using past experiments”: https://developers.google.com/meridian/docs/advanced-modeling/set-custom-priors-past-experiments
Google Meridian, ROI/mROI/contribution parameterizations: https://developers.google.com/meridian/docs/advanced-modeling/roi-mroi-contribution-parameterizations
Zhou, G., Choe, Y., & Hetrakul, C. (2023). Calibrated MMM better predicts true ROAS. Meta Marketing Science.