Two-Way MMM Integration (PyMC-Marketing, Meridian & Robyn)
High-frequency spot attribution and aggregate Media Mix Modeling (MMM) solve opposite halves of the television measurement problem:
- High-frequency response (0–8 minutes): Causally sharp at the spot, station, daypart, and creative level because minute-level airings act as localized natural experiments. However, it systematically under-reports total TV impact (missing delayed brand search, word-of-mouth, retail halo, and multi-week carryover).
- Weekly MMM: Estimates macro saturation curves, multi-week adstock, and total campaign contribution, but cannot identify 60+ collinear TV stations
creatives inside weekly aggregate regressions.
SpotLift resolves this asymmetry with a bidirectional integration — never pretending that an immediate 8-minute spike equals total TV contribution, and never silently inflating immediate spikes by
Direction 1: SpotLift ──► MMM (Causal Lower Bounds & Latent Share)
┌─────────────────────────────────────────────────────────────────────┐
│ Minute-level spot lifts (0–8 min) │
│ └─► Signed OLS power-law fit + cluster-block bootstrap + EB SEs │
│ └─► export_causal_lower_bound() / attach_pymc_lower_bound() │
│ • Mode A: Asymmetric floor (C_tv >= direct_lift_floor) │
│ • Mode B: Latent immediate share (direct ~ rho * C_tv) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
Weekly Macro MMM
(PyMC / Meridian / Robyn)
│
▼
Direction 2: MMM ──► SpotLift (Anchored Optimization & Disaggregation)
┌─────────────────────────────────────────────────────────────────────┐
│ Macro MMM posterior contributions & draws │
│ └─► load_mmm_results() │
│ ├─► optimize_tv_with_mmm(): macro scale + elasticity + p10/p90│
│ └─► disaggregate_mmm_contributions(): EB weights + │
│ joint posterior-draw conservation to station x creative │
└─────────────────────────────────────────────────────────────────────┘Direction 1: SpotLift → MMM (Causal Lower-Bound & Latent Share)
1. Exporting Period-Aligned Causal Lower Bounds
spotlift.export_causal_lower_bound() aggregates spot-level direct response to your MMM's time grain (freq="W-MON" or "D") and pools broadcaster stations (mmm_channel_name="tv_broadcast") to prevent collinearity.
Unlike naive i.i.d. aggregation (
- It fits a signed OLS power-law response curve (
fit_spot_power_law_ols) so negative/zero-response spots cancel noise symmetrically. - It estimates period standard errors via cluster-block bootstrap (grouping overlapping
collision_cluster_idor 2-hour broadcast blocks) combined with Empirical-Bayes variance pooling () and an explicit model-discrepancy term ( discrepancy_rel, default0.10). - It sets
carryover_mode="none"by default (estimand="causal_lower_bound"), keeping immediate direct lift uninflated unless you explicitly opt into finite- or infinite-horizon carryover scaling.
import spotlift as sl
evidence = sl.export_causal_lower_bound(
attributed_spots,
freq="W-MON", # Match weekly MMM rows
lift_col="key_event_response", # Match MMM outcome metric
mmm_channel_name="tv_broadcast",
overlap_with_mmm_outcome="shared_same_series",
)
print(evidence.to_metadata_dict())
print(evidence.data[["period_start", "spend", "direct_lift_floor", "sigma", "lower_95_floor"]])2. Attaching to PyMC / PyMC-Marketing
Use attach_pymc_lower_bound() (or generate a standalone Python script with generate_pymc_lower_bound_snippet()):
mode="lower_bound"(Default): Adds apm.Potentialimposing a one-sided soft HalfNormal penalty only when the MMM's modeled TV contribution falls belowdirect_lift_floor(scaled by cluster-block bootstrapsigma). Model contributions above the direct floor are left unconstrained so the MMM can freely infer long-term brand/carryover halo.mode="latent_share": Introduces a learned immediate-share parameterand models .
# Inside your PyMC / PyMC-Marketing model context:
sl.attach_pymc_lower_bound(
mmm.model,
evidence,
tv_contribution_tensor=tv_contrib_weekly,
mode="lower_bound", # Or "latent_share" with rho_prior=(2.0, 3.0)
)3. Support-Matched Priors & Exploratory Exporters (PyMC, Meridian & Robyn)
export_pymc_lift_test(): Exports[channel, x, delta_x, delta_y, sigma]records (with cluster-block bootstrap SEs anddiscrepancy_rel) as an exploratory calibration table (carryover_modeopt-in).export_mmm_priors(): Exportscausal_lower_bound, exploratorypymc_marketing_lift_test, support-matched Meta Robyn hyperparameter bounds (alphas,gammas), support-matched Google Meridian Hill priors (half_saturation_prior,slope_prior), and explicitprovenance.- Pro Interchange Exports (SpotLift Pro):
export_meridian_package()writescalibration_data.csv+meridian_model_spec.yaml(interchange_specYAML for translation into your pinned MeridianModelSpec);export_robyn_package()writescalibration_input.csv+run_robyn_calibration.R(leavingconfidenceasNAunless an independently measuredexperimental_confidenceis supplied).
Direction 2: MMM → SpotLift (Anchored Optimization & Disaggregation)
Once your macro MMM has been fitted, feed its TV contributions back into SpotLift so granular planning inherits macro scale, saturation curvature, and posterior uncertainty.
1. MMM-Anchored Portfolio Optimization (optimize_tv_with_mmm)
load_mmm_results() ingests scalar totals, DataFrames (date, contribution, lower, upper, or draw_* columns), or CSV/Parquet/JSON files. Passing those results into optimize_tv_with_mmm() (or optimize_budget(..., mmm_results=...)):
- Calibrates total portfolio lift and elasticity (
mmm_calibrated_beta) to the macro MMM while ranking stations using Empirical-Bayes shrunk SpotLift efficiencies. - Evaluates posterior uncertainty across
n_posterior_draws, optionally penalizing variance viarisk_aversion >= 0. - Flags low-sample channels (
low_information_flag) and out-of-sample spend shifts (extrapolation_warning), while reportingprojected_lift_p10,projected_lift_p90,direct_lift_floor, andimplied_mmm_multiplier.
opt = sl.optimize_tv_with_mmm(
spots_df=attributed_spots,
mmm_results=mmm_posterior_df, # Or path to CSV/Parquet/JSON or scalar float
total_budget=500000.0,
max_shift=0.35,
risk_aversion=0.25,
)
opt.print_summary()
print(opt.recommendations[["channel", "optimized_spend", "direct_lift_floor", "projected_lift_p10", "projected_lift", "projected_lift_p90"]])2. Joint Draw-Conserving Disaggregation (disaggregate_mmm_contributions)
Decomposes weekly MMM TV posterior contributions down to stations ("channel"), creatives ("creative_id"), or ["channel", "creative_id"] using Empirical-Bayes shrunk efficiency weights (avoiding max(0, D) winner's-curse clipping) and joint posterior-draw conservation (
creative_mmm = sl.disaggregate_mmm_contributions(
spots_df=attributed_spots,
mmm_contributions=mmm_posterior_df,
by=["channel", "creative_id"],
freq="W-MON",
)
print(
creative_mmm[
[
"channel",
"creative_id",
"spend",
"direct_lift_floor",
"allocated_mmm_contribution",
"allocated_lower",
"allocated_upper",
"reconciliation_residual",
"implied_multiplier",
"mmm_cpiv",
"low_information_flag",
]
]
)CLI Workflow
# Direction 1: Export causal lower-bound CSV, PyMC potential script, provenance JSON, & priors
spotlift mmm \
--spots attributed_spots.csv \
--target all \
--freq W-MON \
--pymc-mode lower_bound \
--mmm-channel-name tv_broadcast \
--out-dir ./mmm_export
# Direction 2: Run station budget optimization anchored to MMM posterior CSV/Parquet/JSON
spotlift optimize \
--spots attributed_spots.csv \
--mmm ./mmm_posterior.csv \
--mmm-adstock 0.35 \
--risk-aversion 0.25 \
--max-shift 0.35 \
--scenarios \
--out ./mmm_optimized_budget.csvSee the CLI Reference, Budget & MMM Optimizer, and Python API Reference for full parameter details.