Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Meridian `roi_calibration_period` mask builder + `to_code()` array support.**
New `meridian_calibration_mask(media_times=, media_channels=, channel=, window=)`
builds the boolean `(n_media_times, n_media_channels)` mask Meridian's
`ModelSpec` expects, from the MMM's own coordinates taken verbatim: the
experiment channel's column(s) are True exactly on the window, and every
other channel's column is all-True - Meridian's documented convention
(channels not named in an experiment use ALL periods for ROI calibration;
an all-False column would zero that channel's aggregated calibration spend
in `input_data._aggregate_spend`).
Window convention: a 2-tuple is `(start, end)` inclusive bounds (value-ordered,
`pd.to_datetime`-coerced against datetime coordinates, timezone mismatches fail
closed both ways); any other sequence is explicit labels with fail-closed
membership. `MeridianROIPrior.to_code()` now also accepts that array for
`roi_calibration_period` - a new capability, serialized into the generated
snippet as an `np.ones` prelude plus per-column-group window assignments -
alongside the pre-existing expression-string and `full_model_window=True`
routes. Array acceptance is fail-closed: bool or 0/1-numeric only (cast to
bool, matching Google's own float `np.zeros` example), masked arrays
rejected, all-False masks rejected, and ANY entirely-False column rejected
(it would zero that channel's calibration spend). Masks apply to `roi_m`
priors only. The mask's row count and column order are
not machine-checkable inside `to_code` (no time coordinate is passed and the
array carries no channel labels) - documented caveats; the builder guarantees
both when given the model's own coordinates.
- **MMM exporters: container mode on the 3.9 aggregation surface.** Both
`to_pymc_marketing_lift_test` and `to_meridian_roi_prior` now accept
`aggregation_result=` - the pinned `AggregationResult` returned by post-fit
Expand Down Expand Up @@ -49,6 +73,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
complier LATE), and CCFT 2019 covariate adjustment (precision, not
identification).

### Fixed
- **`MeridianROIPrior.to_code()` emitted invalid time-scoped `mroi_m` snippets.**
Meridian 1.7.0's `ModelSpec._validate_roi_calibration_period` rejects a
non-None `roi_calibration_period` unless the media prior type is `'roi'`, so
the expression-string route (shipped with the exporters) produced `mroi_m`
snippets that fail at `ModelSpec` construction. `to_code()` now fails closed
for `parameter="mroi_m"` with any non-None `roi_calibration_period` (array or
expression), pointing at `full_model_window=True` as the mroi route.

### Changed
- **Narrative docs migrated off the deprecated fit-time `aggregate=`** (the
3.9 M-020 family; TODO "fit-time aggregate= teachings" sweep, RST half):
Expand Down
1 change: 0 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ generic sparse-FE, QR+SVD rank-detection redundancy, `check_finite` bypass — m
| Type-blind `n_bootstrap` acceptance in already-validated estimators - HAD bool (`isinstance(..., int)` passes `True`, runs as 1 replicate), dCDH bool+float (its bare `< 0` check passes both `True` and `2.5`), TROP float (`2.5` passes the `>= 2` floor), SyntheticDiD float under all three variance methods + bool/negative under jackknife (its floor check is skipped there) - align these local checks with the `utils.validate_n_bootstrap` type guard (M-081 kept them out of the sweep: it scoped to previously-UNvalidated estimators only) | `diff_diff/had.py`, `diff_diff/chaisemartin_dhaultfoeuille.py`, `diff_diff/trop.py`, `diff_diff/synthetic_did.py` | 2(d) PR-B | Quick | Low |
| Evaluate adding the `BaseEstimator` param surface (get_params/set_params) to the exported classes that never had it - `PowerAnalysis`, `LinearRegression`, `BusinessReport`, `DiagnosticReport`, `TWFEWeightsResult` (a NEW public surface, deliberately out of the 2(c)-i pure-refactor scope; `LinearRegression` is the one `fit`-bearing class excluded from the contract suite's roster-completeness test). | `diff_diff/linalg.py`, `diff_diff/power.py` | mixin PR | Mid | 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 |
| MMM interop follow-up: Meridian `roi_calibration_period` mask builder - accept the MMM's time index + channel order and emit the boolean `(n_media_times, n_media_channels)` mask so `.to_code()` scopes the prior to the experiment window automatically (today the caller passes a mask expression / `full_model_window=True`). | `diff_diff/mmm.py` | mmm-interop | Quick | Low |
| MMM interop PR-B: calibration tutorial notebook (fit DiD/CS -> scope -> `to_pymc_marketing_lift_test` / `to_meridian_roi_prior`) + a `llms-practitioner.txt` Step 8 pointer to the exporters as the MMM hand-off. | `docs/tutorials/`, `diff_diff/guides/llms-practitioner.txt` | mmm-interop | Mid | Low |
| Tracking-file contract guard test: reject NEW active deferred-work pointers at `TODO.md` (deferred rows live in `DEFERRED.md`; allowlist for historical/past-tense prose and actionable-row pointers) and assert rows cross-linking a `docs/v4-deprecations.yaml` `M-xxx` id don't restate ledger status. Origin: tracking-split local review R2. | `tests/`, `TODO.md`, `DEFERRED.md` | tracking-split | Quick | Low |
| 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. Follow-on from the loader-fallback repair (#723), which made provenance explicit but deliberately did not add a network-dependent CI lane. | `tests/test_methodology_lwdid.py`, `.github/workflows/` | LWDiD validation suite | Quick | Low |
Expand Down
2 changes: 2 additions & 0 deletions diff_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
from diff_diff.lpdid_results import LPDiDResults
from diff_diff.mmm import (
MeridianROIPrior,
meridian_calibration_mask,
to_meridian_roi_prior,
to_pymc_marketing_lift_test,
)
Expand Down Expand Up @@ -613,6 +614,7 @@ def __getattr__(name: str) -> _Any:
# MMM calibration export (interop)
"to_pymc_marketing_lift_test",
"to_meridian_roi_prior",
"meridian_calibration_mask",
"MeridianROIPrior",
# LLM guide accessor
"get_llm_guide",
Expand Down
21 changes: 18 additions & 3 deletions diff_diff/guides/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2896,6 +2896,7 @@ parameterization, pools, and emits snippets.
from diff_diff import (
ImputationDiD,
SyntheticDiD,
meridian_calibration_mask,
to_pymc_marketing_lift_test,
to_meridian_roi_prior,
)
Expand Down Expand Up @@ -2935,14 +2936,28 @@ prior.roi_mean, prior.roi_sd # pooled ROI moments
prior.mu, prior.sigma # LogNormal params (match Google's
# lognormal_dist_from_mean_std)
prior.to_dict() # JSON-ready

# Build the (n_media_times, n_media_channels) boolean mask from the MMM's own
# coordinates; window = inclusive (start, end) tuple or explicit-labels list.
# Experiment channel True on the window; every OTHER channel ALL-TRUE
# (Meridian's convention - an all-False column zeroes that channel's
# calibration spend). roi_m priors only: Meridian rejects the mask for mroi_m
# (use full_model_window=True there). to_code() serializes the array.
mask = meridian_calibration_mask(
media_times=media_times, # the model's time coordinate labels
media_channels=["search", "tv"], # InputData channel order (same list
channel="tv", # as to_code's media_channels)
window=("2024-01-15", "2024-03-04"),
)
print(prior.to_code( # ready-to-paste PriorDistribution +
channel="tv", # ModelSpec; roi_m/mroi_m is per-channel
media_channels=["search", "tv"], # so channel scope is required (vector
# prior in model channel order; other
# channels keep the Meridian default),
roi_calibration_period="mask", # AND time scope (mask expr) or
# full_model_window=True; sets
)) # media_prior_type accordingly.
roi_calibration_period=mask, # AND time scope: the boolean ndarray
# above, a mask expr string, or
)) # full_model_window=True; sets
# media_prior_type accordingly.

# Container route (ImputationDiD/TwoStageDiD): derive the totals from the
# post-fit aggregation instead of hand-scoping.
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/guides/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The site is organized into 5 sections, each with a landing page:
- [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
- [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 and on admitted CS-/StackedDiD-sourced `aggregate('event_study')` containers (StackedDiD persists its ES VCV in every inference mode)
- [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
- [MMM Calibration Export](https://diff-diff.readthedocs.io/en/stable/api/mmm.html): Assemble Marketing Mix Model calibration inputs from experiment results. Two input routes: explicit already-scoped numbers, or the pinned `AggregationResult` container from post-fit `results.aggregate('simple'|'group')` via `aggregation_result=` + `scale=` (effect = att x scale per row, in `to_dataframe()` order; `scale="auto"` reads the container's treated-obs count and is honored ONLY for ImputationDiD/TwoStageDiD fits - it acknowledges an additive-level outcome, an unweighted fit, and fully identified effects; other estimators and raw results objects fail closed). `to_pymc_marketing_lift_test(channel, x, delta_x, delta_y=, sigma=, aggregation_result=, scale=, dims=, on_wrong_sign=)` builds the PyMC-Marketing/prophetverse lift-test DataFrame with sign/zero/positivity guards. `to_meridian_roi_prior(incremental_outcome=, incremental_outcome_se=, aggregation_result=, scale=, spend, parameter="roi_m"|"mroi_m", se_widening=)` builds Google Meridian lognormal ROI priors (spend-weighted pooling, lognormal parity with `lognormal_dist_from_mean_std`, channel- and time-scoped `.to_code()` snippet setting `media_prior_type`; widen `se_widening` when pooling a group-level container's same-fit cohorts). Pure numpy/pandas; imports no MMM package; never calls `aggregate()` itself.
- [MMM Calibration Export](https://diff-diff.readthedocs.io/en/stable/api/mmm.html): Assemble Marketing Mix Model calibration inputs from experiment results. Two input routes: explicit already-scoped numbers, or the pinned `AggregationResult` container from post-fit `results.aggregate('simple'|'group')` via `aggregation_result=` + `scale=` (effect = att x scale per row, in `to_dataframe()` order; `scale="auto"` reads the container's treated-obs count and is honored ONLY for ImputationDiD/TwoStageDiD fits - it acknowledges an additive-level outcome, an unweighted fit, and fully identified effects; other estimators and raw results objects fail closed). `to_pymc_marketing_lift_test(channel, x, delta_x, delta_y=, sigma=, aggregation_result=, scale=, dims=, on_wrong_sign=)` builds the PyMC-Marketing/prophetverse lift-test DataFrame with sign/zero/positivity guards. `to_meridian_roi_prior(incremental_outcome=, incremental_outcome_se=, aggregation_result=, scale=, spend, parameter="roi_m"|"mroi_m", se_widening=)` builds Google Meridian lognormal ROI priors (spend-weighted pooling, lognormal parity with `lognormal_dist_from_mean_std`, channel- and time-scoped `.to_code()` snippet setting `media_prior_type` - time scope via `meridian_calibration_mask(media_times=, media_channels=, channel=, window=)`, which builds the boolean (n_media_times, n_media_channels) mask (window = inclusive (start, end) tuple or explicit-labels list; experiment channel True on the window, every other channel ALL-TRUE per Meridian's documented convention; roi_m priors only - Meridian rejects the mask for mroi_m) and whose array `.to_code()` serializes into the snippet, or via a mask expression string, or full_model_window=True; widen `se_widening` when pooling a group-level container's same-fit cohorts). Pure numpy/pandas; imports no MMM package; never calls `aggregate()` itself.
- 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

## Tutorials
Expand Down
Loading
Loading