You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: MMM calibration export (diff_diff.mmm) - PyMC-Marketing lift tests + Meridian ROI priors
New interop module converting DiD experiment results into Marketing Mix Model
calibration inputs, with zero MMM-package dependency:
- to_pymc_marketing_lift_test(): emits the lift-test DataFrame consumed by
pymc_marketing MMM.add_lift_test_measurements (channel/dims/x/delta_x/
delta_y/sigma; explicit unit contract with aggregate= mean/sum scaling,
n_units= geo-to-national scaling, on_wrong_sign= policy mirroring the
upstream monotonicity check, x+delta_x>=0 post-test spend guard, reserved-
column dims validation; prophetverse-compatible schema)
- to_meridian_roi_prior(): emits Google Meridian lognormal ROI prior params
(MeridianROIPrior with mu/sigma via Google's closed form - bit-identical
parity vs meridian prior_distribution.lognormal_dist_from_mean_std at
1.7.0; spend-weighted multi-experiment pooling per the Meridian FAQ
recommendation; se_widening transferability knob; channel-scoped
.to_code() snippet - roi_m is per-channel so vector priors require the
model channel order, scalar output needs explicit single_channel=True)
- Duck-typed against the flat att/se surface every headline results class
exposes; treated counts are never inferred from result metadata
(n_treated is a row count on several classes); n_units/n_periods must be
positive integers
- 51 behavioral tests incl. units-not-rows pin, scipy lognormal round-trip,
channel-scope regression, and cross-estimator contract (DiD/SDID/MPD/CS)
- End-to-end smoke verified against real pymc-marketing 0.19.4 (frame
accepted into the model graph; wrong-signed row raises NonMonotonicError)
- Docs: REGISTRY.md interop section, api/mmm.rst, llms.txt + llms-full.txt,
README one-liner, references.rst, doc-deps.yaml, CHANGELOG, TODO follow-ups
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wAAYSVo1Yf3XMcxNMrqsj
Copy file name to clipboardExpand all lines: README.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -129,6 +129,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
129
129
-[Honest DiD](https://diff-diff.readthedocs.io/en/stable/api/honest_did.html) - Rambachan & Roth (2023) sensitivity analysis: robust CI under PT violations, breakdown values
130
130
-[Pre-Trends Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/pretrends.html) - Roth (2022) minimum detectable violation and power curves
131
131
-[Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/power.html) - analytical and simulation-based MDE, sample size, power curves for study design
132
+
-[MMM Calibration Export](https://diff-diff.readthedocs.io/en/stable/api/mmm.html) - convert experiment results into MMM calibration inputs: PyMC-Marketing lift-test frames and Google Meridian lognormal ROI priors
132
133
- Conley spatial HAC SE (`vcov_type="conley"`) on cross-sectional `LinearRegression` / `compute_robust_vcov` plus panel `DifferenceInDifferences` / `MultiPeriodDiD` / `TwoWayFixedEffects` (with `conley_lag_cutoff` for within-unit Bartlett temporal HAC) - Conley (1999) spatial-correlation-aware SEs with parity vs R `conleyreg` on cross-sectional + panel fixtures, optional combined spatial + cluster product kernel via explicit `cluster=`, auto-activating sparse k-d-tree fast path for `n > 5_000`
| ChangesInChanges/QDiD tutorial notebook (2x2 distributional walkthrough: QTE grid, interior range, uniform bands, CiC-vs-QDiD comparison) - deferred from the implementation PR as a documented decision. |`docs/tutorials/`|#682| Mid | Low |
52
+
| MMM interop follow-up: Meridian `roi_calibration_period` boolean-mask builder (align the ROI prior's definition window to the experiment dates; needs the user's Meridian time index + channel ordering as inputs) - deferred from v1 as a documented decision. |`diff_diff/mmm.py`| mmm-interop | Quick | Low |
| Tighten the mypy suppressions that back the enforced-zero posture: burn down `prep_dgp`'s per-module `[index]` override (needs a None-vs-array restructure that preserves the seeded RNG stream), and evaluate re-enabling the globally disabled codes (`arg-type`, `return-value`, `var-annotated`, `assignment`) one at a time — `assignment` alone hid several real annotation drifts found during the 2026-07 triage. |`pyproject.toml``[tool.mypy]`, `diff_diff/prep_dgp.py`| lint-CI | Mid | Low |
53
55
| Align the four legacy dataset loaders (`load_card_krueger`, `load_castle_doctrine`, `load_divorce_laws`, `load_mpdta`) with the loud-fallback pattern of `load_prop99`/`load_walmart`: `UserWarning` + `df.attrs["source"]` marker on synthetic fallback (currently silent), plus optional checksum pinning for the CSV downloads. **Upgraded to a live defect 2026-07-13: the `causaldata/causal_datasets` GitHub repo backing castle/card_krueger/divorce is dead (404), so those loaders silently serve synthetic data everywhere - needs loud fallback + replacement sources.**|`diff_diff/datasets.py`| LWDiD precursor | Quick | Medium |
54
56
| Real-data CI canary for dataset-backed replication tests: `test_methodology_lwdid.py`'s Prop 99 / Walmart goldens skip (visibly) when loaders fall back to synthetic; add a lane or canary asserting `df.attrs["source"] == "lwdid_ssc_ancillary"` in CI so network regressions cannot silently de-gate the replication tests. Pairs with the loader-fallback repair row above. |`tests/test_methodology_lwdid.py`, `.github/workflows/`| LWDiD validation suite | Quick | Low |
@@ -118,6 +120,9 @@ Doable in principle, but no current caller and/or explicitly out of paper scope.
118
120
| ChangesInChanges analytical SEs (Athey-Imbens Theorems 5.1-5.3 influence functions, panel 5.5-5.7, Appendix B covariances; needs the footnote-31 boundary density estimator - note the review's suspected half-range/midpoint typo). Bootstrap is the shipped inference. |`diff_diff/changes_in_changes.py`|#682| Low |
119
121
| Staggered/multi-period distributional DiD (Athey-Imbens Section 6 / Ciaccio arXiv:2408.01208v2; `ecic` is the staggered event-study CiC lineage - a distinct method from Ciaccio's copula approach, do not conflate). Reviewed: `docs/methodology/papers/ciaccio-2024-review.md`; ROADMAP row is reviewed-deferred pending demand. |`diff_diff/changes_in_changes.py`|#682| Low |
120
122
| ChangesInChanges treatment-on-the-controls (Athey-Imbens Theorem 3.2: group-label exchange + negation; no qte equivalent to anchor parity). |`diff_diff/changes_in_changes.py`|#682| Low |
123
+
| MMM interop: event-study temporal lift export (time-resolved effect path -> per-period calibration rows) - no consumer exists yet; PyMC-Marketing's temporal/adstock lift calibration is an open upstream feature request (pymc-marketing #2433) and the current `add_lift_test_measurements` API consumes only scalar lifts. Revisit when upstream ships. |`diff_diff/mmm.py`| mmm-interop | Low |
124
+
| MMM interop: Robyn `calibration_input` emitter - the only functional consumer is R Robyn (the official `robynpy` Python beta ships calibration data classes but its public API hardcodes `calibration_input=None` and feeds a constant-zero calibration objective; verified 2026-07-18). Revisit if Python Robyn revives. |`diff_diff/mmm.py`| mmm-interop | Low |
125
+
| MMM interop: Meridian GeoX emitter - GeoX ("converts experiment results into priors", SDiD on roadmap) is announced but unshipped (waitlist-only, no repo/spec as of 2026-07-18). Revisit at GeoX GA. |`diff_diff/mmm.py`| mmm-interop | Low |
121
126
| Rust-backend CR2 Bell-McCaffrey port (`return_dof` in the Rust vcov dispatch + CR2 algebra) — **premise re-scoped 2026-07-09**: the scores-based DOF + low-rank factored `A_g` changes made the NumPy CR2-BM path BLAS-bound (`O(n_g k²)` per cluster; 4.1s→38ms at n=100k/k=40), so a Rust port buys ~nothing and adds a parity surface. Revisit only if profiling shows CR2-BM hot again. |`rust/src/linalg.rs`| — | Low |
122
127
| Clustered-CR1 inference df **default flip to `"cluster"` (G−1) at v4** — the opt-in `df_convention=` knob landed 2026-07 (DiD/TWFE/MPD + LinearRegression; REGISTRY §TwoWayFixedEffects deviation note); the remaining work is the major-version default change (moves every clustered p-value/CI) + migration note + flipping `TestDfConvention`/`test_moderate_t_pins_residual_df_convention` expectations. Also evaluate extending the knob to standalone estimators with CR1-t inference at that time. |`diff_diff/linalg.py::LinearRegression`, `diff_diff/estimators.py`, `diff_diff/twfe.py`| — | Medium |
123
128
| CallawaySantAnna **unbalanced-panel R parity — LANDED** via `allow_unbalanced_panel=True` (matches R `did::att_gt(allow_unbalanced_panel=TRUE)` / `DRDID::reg_did_rc`: ATT bit-exact on cells AND dynamic aggregation via fixed unit-cohort-mass `pg` + a per-unit WIF; SE up to the documented CR1 `sqrt(G/(G-1))` factor). The earlier "weighting" framing was a mis-diagnosis — on unbalanced panels the dominant divergence from R is the *estimator* (within-cell differencing vs RC-on-pooled-obs), not only the weighting; both are resolved by the flag. The DEFAULT path keeps within-cell differencing as a documented design choice and now emits a `UserWarning` on unbalanced input (no-silent-failures). **Remaining deferred:** `survey_design=` × `allow_unbalanced_panel=` (per-obs vs per-unit weight resolution — currently fail-closed `NotImplementedError`); and covariate / ipw / dr × the flag R-parity verification (the RC path supports them; the committed golden covers `reg` no-cov). | `staggered.py`, `staggered_aggregation.py` | SE-audit D3 | Low |
Copy file name to clipboardExpand all lines: diff_diff/guides/llms.txt
+1Lines changed: 1 addition & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -82,6 +82,7 @@ Full practitioner guide: call `diff_diff.get_llm_guide("practitioner")`
82
82
- [Honest DiD](https://diff-diff.readthedocs.io/en/stable/api/honest_did.html): Rambachan & Roth (2023) sensitivity analysis — robust CI under parallel trends violations, breakdown values
83
83
- [Pre-Trends Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/pretrends.html): Roth (2022) Section II.A-B no-individually-significant (NIS) box-probability pretest power + minimum detectable violation; `pretest_form='nis'` (default) implements the paper's primary form, `pretest_form='wald'` retained as paper-supported alternative (Propositions 1+3+4 all apply); linear-violation MDV in Roth's γ units when relative-time labels are threaded through `fit()`; full Σ_22 routing on non-bootstrap CallawaySantAnna and SunAbraham adapters
84
84
- [Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/power.html): Analytical and simulation-based power analysis — MDE, sample size, power curves for study design
85
+
- [MMM Calibration Export](https://diff-diff.readthedocs.io/en/stable/api/mmm.html): Convert experiment results into Marketing Mix Model calibration inputs — `to_pymc_marketing_lift_test()` emits the PyMC-Marketing/prophetverse lift-test DataFrame (`channel`/dims/`x`/`delta_x`/`delta_y`/`sigma` for `MMM.add_lift_test_measurements`), `to_meridian_roi_prior()` emits Google Meridian lognormal ROI prior parameters (spend-weighted multi-experiment pooling, `se_widening` transferability knob, channel-scoped `.to_code()` snippet - `roi_m` is per-channel, so it requires `channel=`+`media_channels=` or `single_channel=True`). Works with any result exposing scalar `att`/`se`; experiment context (channel, spend, `n_units`, `n_periods`) is always explicit — treated counts are never inferred from result metadata
85
86
- Conley spatial HAC SE (`vcov_type="conley"`) on cross-sectional `LinearRegression` / `compute_robust_vcov` PLUS panel `DifferenceInDifferences` / `MultiPeriodDiD` / `TwoWayFixedEffects` (with `conley_lag_cutoff=<int>` for within-unit Bartlett temporal HAC) — Conley (1999) spatial-correlation-aware SEs with haversine/euclidean/callable distance metric and Bartlett/uniform spatial kernel; panel path uses the R `conleyreg`-form block-decomposed sandwich (within-period spatial + within-unit Bartlett serial, same-time excluded); parity vs R `conleyreg` (Düsterhöft 2021) on cross-sectional AND panel `lag_cutoff > 0` fixtures. Combining with explicit `cluster=<col>` applies the combined spatial + cluster product kernel `K_total[i,j] = K_space · 1{c_i = c_j}` (cluster must be constant within each unit across periods on the panel path; validator-enforced). DiD takes `unit=<col>` as a fit-time kwarg when `vcov_type="conley"` (not on `__init__`). Sparse k-d-tree fast path auto-activates for `n > 5_000` with bartlett kernel + haversine/euclidean metric
0 commit comments