From 6ca4d290019420bd6ce256401c51b5eb41bdd2e5 Mon Sep 17 00:00:00 2001 From: igerber Date: Sat, 31 Oct 2026 07:28:41 -0400 Subject: [PATCH] feat(v4): item 8b of the 4.0 cut - unified to_dataframe(level='event_study') schema on every results class (M-093 done) Since 4.0 every results class's to_dataframe(level="event_study") emits the EVENT_STUDY_SCHEMA columns (event_time, att, se, t_stat, p_value, conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, is_reference, estimand) as an identical PREFIX - the frame the unified EventStudyResults container returns. The routing rule is the M-153 LWDiD precedent: the AggregationMixin classes (CallawaySantAnna/DMLDiD, EfficientDiD, ImputationDiD, TwoStageDiD, StackedDiD, ContinuousDiD, TripleDifference staggered, ChaisemartinDHaultfoeuille, HAD event-study, DurationDiD) return aggregate("event_study").to_dataframe(); SunAbraham, LPDiD, WooldridgeDiD and SpilloverDiD return build_event_study_surface(self).to_dataframe(). WooldridgeDiD appends cohort_trends / aggregation_weights after the schema (LWDiD's three config columns are the shipped M-153 precedent); spec section 5 states the prefix rule in the same diff. The 3.x bespoke frames are gone: relative_period -> event_time; effect / att_glob / LPDiD's coefficient -> att; conf_int_lo/hi, conf_low/high and HAD's conf_int_low/high -> conf_int_lower/upper; n_obs -> n (NaN on the is_reference row, never a zero count); the dCDH native frame (horizon, a per-horizon estimand label DID^pl_l / "ref" / DID_l, the n_obs 0 reference row) is retired - event_time on l1_first_switch, estimand "att", is_reference at 0. The "not stored on 4.0 fits" raises on CallawaySantAnna / EfficientDiD / TripleDifference and on analytical ImputationDiD / TwoStageDiD / ContinuousDiD are gone: the level recomputes from the retained kit (bootstrapped CS/EDiD/DDD fits replay the fit-time bootstrap on every call); a kit-less or pre-replay legacy pickle that still carries a stored dict now raises where it returned a frame. WooldridgeDiD before aggregate("event_study", weights=) raises instead of returning an empty frame (the weighting scheme is never chosen silently). New levels: SpilloverDiDResults.to_dataframe(level= "simple" | "event_study") (default "simple" = the former one-row frame; aggregate-mode fits raise), HeterogeneousAdoptionDiDEventStudyResults .to_dataframe(level="event_study"), DurationDiDResults "event_study". level="group" on CS/EDiD/TripleDiff still raises with the post-fit pointer (TODO.md row). results_base._ABSENT_SURFACE_HINTS gains a SunAbrahamResults entry. Tests: new tests/test_v4_event_study_schema.py (M-093 test_ref) pins the schema prefix and trailing config columns on 21 producer fits, bit-equality with the aggregate / builder route (bootstrap relay and replay routes included), the default-level classes, reference / estimand / count spot checks per producer, the error contract (Wooldridge pre-aggregate, Spillover aggregate mode, 2x2x2 DDD, LPDiD absent and empty frames, Stacked and kit-less legacy pickles, the new bad-level texts) and the ledger row. tests/test_v4_sentinel_retirement.py keeps the 8a pins (its frame pin and ledger pin updated); the per-class frame pins in the stacked, sun_abraham, continuous, wooldridge, lpdid, dCDH, had, staggered, imputation, efficient, two_stage, staggered_triple_diff, unify_ddd and storage_flip suites move to the unified schema. Ledger: M-093 -> done (test_ref tests/test_v4_event_study_schema.py, tenth amendment note; results.py covers SpilloverDiD only). Spec item 8 complete. REGISTRY (CS, EfficientDiD, Imputation/TwoStage/Stacked relay and reference-row Notes, DDD, HAD; new to_dataframe-schema Notes for dCDH, SunAbraham, WooldridgeDiD, LPDiD, DurationDiD, SpilloverDiD), the migration guide, llms-full.txt, the practitioner guide, docs/api/wooldridge_etwfe.rst and docs/api/spillover.rst describe the frame. Notebook 16 re-executed under the Rust backend (outputs refreshed; no markdown restates a moved number - it leaves the TODO.md tutorial re-execution row), notebook 19 reads event_time / att / is_reference, notebook 14's export prose updated; 13/14/16/19/20 nbmake-validated. --- TODO.md | 3 +- ...0261031-v4-pr8b-m093-event-study-schema.md | 7 + .../chaisemartin_dhaultfoeuille_results.py | 75 +-- diff_diff/continuous_did_results.py | 33 +- diff_diff/duration_did_results.py | 14 +- diff_diff/efficient_did_results.py | 33 +- diff_diff/guides/llms-full.txt | 12 +- diff_diff/guides/llms-practitioner.txt | 2 +- diff_diff/had.py | 42 +- diff_diff/imputation_results.py | 35 +- diff_diff/lpdid_results.py | 21 +- diff_diff/practitioner.py | 2 +- diff_diff/results.py | 25 + diff_diff/results_base.py | 8 +- diff_diff/stacked_did_results.py | 33 +- diff_diff/staggered_results.py | 34 +- diff_diff/sun_abraham.py | 31 +- diff_diff/triple_diff_results.py | 22 +- diff_diff/two_stage_results.py | 34 +- diff_diff/wooldridge_results.py | 33 +- docs/api/spillover.rst | 6 +- docs/api/wooldridge_etwfe.rst | 10 +- docs/methodology/REGISTRY.md | 30 +- docs/migration-4.0.md | 42 +- docs/tutorials/14_continuous_did.ipynb | 2 +- docs/tutorials/16_wooldridge_etwfe.ipynb | 340 +++++----- docs/tutorials/19_dcdh_marketing_pulse.ipynb | 22 +- docs/v4-deprecations.yaml | 8 +- docs/v4-design.md | 15 +- tests/test_chaisemartin_dhaultfoeuille.py | 14 +- tests/test_continuous_did.py | 17 +- tests/test_efficient_did.py | 11 +- tests/test_had.py | 18 +- tests/test_imputation.py | 8 +- tests/test_lpdid.py | 38 +- tests/test_stacked_did.py | 10 +- tests/test_staggered.py | 11 +- tests/test_staggered_triple_diff.py | 6 +- tests/test_sun_abraham.py | 8 +- tests/test_two_stage.py | 20 +- tests/test_v4_event_study_schema.py | 611 ++++++++++++++++++ tests/test_v4_sentinel_retirement.py | 28 +- tests/test_v4_storage_flip.py | 5 +- tests/test_v4_unify_ddd.py | 13 +- tests/test_wooldridge.py | 16 +- 45 files changed, 1275 insertions(+), 533 deletions(-) create mode 100644 changelog.d/20261031-v4-pr8b-m093-event-study-schema.md create mode 100644 tests/test_v4_event_study_schema.py diff --git a/TODO.md b/TODO.md index e0a95e517..bf2fe6ae3 100644 --- a/TODO.md +++ b/TODO.md @@ -86,7 +86,8 @@ generic sparse-FE, QR+SVD rank-detection redundancy, `check_finite` bypass — m | Issue | Location | Origin | Effort | Priority | |-------|----------|--------|--------|----------| -| Re-execute the nine tutorials whose committed outputs still show the 3.x residual-df p-values/CIs after the `df_convention` default moved to `"cluster"` (t(G-1)): `01_basic_did`, `02_staggered_did`, `04_parallel_trends`, `05_honest_did`, `07_pretrends_power`, `09_real_world_examples`, `11_imputation_did`, `13_stacked_did`, `16_wooldridge_etwfe` (not `16_survey_did`: the survey df wins). Run under `DIFF_DIFF_BACKEND=rust` with the per-worktree kernel, reconcile any prose that restates a moved number, and `--nbmake` each; scheduled for the 4.0 release PR (`docs/v4-design.md` section 9 item 10) together with any item-8/9 movers (24 was refreshed in #879; 23 in 7b, re-validated in 7c - it prints no p-values or CIs). 7b's label changes also drift any committed analytical DiD/TWFE summary (", G=" on the variance line) and every SunAbraham summary (the new "Std. errors:" line) - covered by the same re-execution. | `docs/tutorials/*.ipynb` | #879 | Mid | Medium | +| Relay `to_dataframe(level='group')` to `aggregate('group').to_dataframe()` on CallawaySantAnna/DMLDiD, EfficientDiD and TripleDifference (staggered) - the level still raises with the post-fit pointer while `level='event_study'` was rewired to the aggregate route at the 4.0 cut (item 8b, [M-093]); ImputationDiD/TwoStageDiD keep their native `group` frames and need the same decision | `diff_diff/staggered_results.py`, `diff_diff/efficient_did_results.py`, `diff_diff/triple_diff_results.py` | #883 | Quick | Low | +| Re-execute the eight tutorials whose committed outputs still show the 3.x residual-df p-values/CIs after the `df_convention` default moved to `"cluster"` (t(G-1)): `01_basic_did`, `02_staggered_did`, `04_parallel_trends`, `05_honest_did`, `07_pretrends_power`, `09_real_world_examples`, `11_imputation_did`, `13_stacked_did` (not `16_survey_did`: the survey df wins; `16_wooldridge_etwfe` was re-executed and reconciled in the 8b PR). Run under `DIFF_DIFF_BACKEND=rust` with the per-worktree kernel, reconcile any prose that restates a moved number, and `--nbmake` each; scheduled for the 4.0 release PR (`docs/v4-design.md` section 9 item 10) together with any item-8/9 movers (24 was refreshed in #879; 23 in 7b, re-validated in 7c - it prints no p-values or CIs). 7b's label changes also drift any committed analytical DiD/TWFE summary (", G=" on the variance line) and every SunAbraham summary (the new "Std. errors:" line) - covered by the same re-execution. | `docs/tutorials/*.ipynb` | #879 | Mid | Medium | | TWFE event-study one-way HC2-BM absorbed-FE R parity against the `mpd_absorbed_fe_did` golden (`clubSandwich::vcovCR(cluster=1:n, type="CR2")`): since 4.0 (item 7b) `cluster=False` + hc2_bm reaches the one-way lane directly (no singleton-cluster column needed; `tests/test_v4_cluster_policy.py` pins finiteness and the static-surface LinearRegression parity only), so the open item is the R pin itself; the hc2 lane is pinned by `TestTWFEEventStudyAbsorbedFERParity` and the clustered hc2_bm per-period lane by `test_twfe_event_study_cluster_hc2_bm_per_period_df_matches_clubsandwich`; the MultiPeriodDiD lanes died at the 4.0 cut (PR 4c-ii) | `tests/test_estimators_vcov_type.py` | #v4-4c-i | Mid | Medium | | TWFE event-study power profile: `simulate_power`/`simulate_mde`/`simulate_sample_size` have no `_EstimatorProfile` for `TwoWayFixedEffects.fit(event_study=True)` since the MultiPeriodDiD registry entry went with the class (PR 4c-ii); `EventStudyResults` carries no scalar headline, so the profile needs a design (post-period average via the `tests/helpers/event_study_average.py` arithmetic, or a named period) plus its `_SURVEY_FIT_BUILDERS` twin | `diff_diff/power.py` | #v4-4c-ii | Mid | Low | | Clustered hc2_bm Bell-McCaffrey DOF guard is batch-relative (`linalg._cr2_bm_dof_inner`: a contrast is NaN'd when its scaled max\|B\| falls below 1e-10 of the batch maximum), so the event-study core keeps the otherwise-unused post-period-average contrast column as the anchor to hold the 3.x per-period NaN masks bit-identical (PR 4c-ii, user decision 2026-10-25). Reproduction: `x_nc = treated * 1[period >= 4] + 3e-6 * default_rng(seed).normal(size=n)` as a covariate on the oracle panel flips the mask between `[eye(k), avg]` and `eye(k)` on 20/20 seeds (cond(X) ~ 9e5; 1e-7 never flips, <= 3e-8 rank-drops) - too BLAS-sensitive for a committed oracle, hence the structural pin `test_hc2_bm_contrast_batch_keeps_post_average_anchor`. Decide whether the guard should use a batch-independent anchor (the treatment contrast, or the absolute floor only) and then drop the column | `diff_diff/estimators.py`, `diff_diff/linalg.py` | #v4-4c-ii | Mid | Medium | diff --git a/changelog.d/20261031-v4-pr8b-m093-event-study-schema.md b/changelog.d/20261031-v4-pr8b-m093-event-study-schema.md new file mode 100644 index 000000000..4cd641868 --- /dev/null +++ b/changelog.d/20261031-v4-pr8b-m093-event-study-schema.md @@ -0,0 +1,7 @@ +### Breaking Changes + +- Every results class's `to_dataframe(level="event_study")` now emits the unified `EVENT_STUDY_SCHEMA` ([M-093], item 8b of the 4.0 cut): `event_time, att, se, t_stat, p_value, conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, is_reference, estimand` - the frame `results.aggregate("event_study").to_dataframe()` returns (`CallawaySantAnna`/`DMLDiD`, `EfficientDiD`, `ImputationDiD`, `TwoStageDiD`, `StackedDiD`, `ContinuousDiD`, `TripleDifference` staggered, `ChaisemartinDHaultfoeuille`, HAD event-study results, `DurationDiD`, `LWDiD`) or the builder frame (`SunAbraham`, `LPDiD`, `WooldridgeDiD`, `SpilloverDiD`). The 3.x bespoke frames are gone: `relative_period` -> `event_time`; `effect` / `att_glob` / LPDiD's `coefficient` -> `att`; `conf_int_lo`/`hi`, `conf_low`/`high`, HAD's `conf_int_low`/`high` -> `conf_int_lower`/`conf_int_upper`; `n_obs` -> `n` (NaN on the `is_reference` row); dCDH's `horizon` -> `event_time` on the paper's `l1_first_switch` convention, its per-horizon `estimand` labels (`DID^pl_l` / `ref` / `DID_l`) are not carried (`estimand` is `"att"`) and its reference row is marked `is_reference` instead of `n_obs 0`. `WooldridgeDiD` and `LWDiD` append their fit-configuration columns after the schema (`cohort_trends`, `aggregation_weights`; `rolling`, `estimation_method`, `vcov_type`), and `WooldridgeDiD.to_dataframe(level="event_study")` (its default level) now RAISES before `aggregate("event_study", weights=)` instead of returning an empty frame. `CallawaySantAnna`, `EfficientDiD`, `TripleDifference` and analytical `ImputationDiD` / `TwoStageDiD` / `ContinuousDiD` no longer raise "not stored on 4.0 fits" - the level recomputes the surface from the retained kit (bootstrapped `CallawaySantAnna` / `EfficientDiD` / `TripleDifference` fits replay the fit-time bootstrap on every call; an `ImputationDiD` replicate-weight survey fit with `pretrends=True` raises `NotImplementedError` as `aggregate()` does); a kit-less pre-3.9 pickle, or a bootstrapped pickle predating the replay state, that still carries a stored dict now raises where it returned a frame. `level="group"` on those three classes still raises with the post-fit pointer. New levels: `SpilloverDiDResults.to_dataframe(level="simple" | "event_study")` (default `"simple"`, the former one-row frame; aggregate-mode fits raise on `"event_study"`), `HeterogeneousAdoptionDiDEventStudyResults.to_dataframe(level="event_study")`, `DurationDiDResults.to_dataframe(level="event_study")`. An `LPDiD` fit whose stored event-study frame is empty raises instead of returning the empty frame. + +### Internal + +- The routing rule is the M-153 LWDiD precedent (`aggregate(level).to_dataframe()` on the `AggregationMixin` classes, `build_event_study_surface(self).to_dataframe()` elsewhere); `results_base._ABSENT_SURFACE_HINTS` gains a `SunAbrahamResults` entry. Ledger: M-093 flips to `done` with `test_ref` `tests/test_v4_event_study_schema.py` (the cross-producer schema-prefix / route-equality / error-contract pins; `tests/test_v4_sentinel_retirement.py` keeps the 8a pins) and a tenth amendment note; `docs/v4-design.md` section 5 now states the PREFIX rule and item 8 is complete. REGISTRY (CS, EfficientDiD, Imputation/TwoStage/Stacked relay and reference-row Notes, DDD, HAD, plus new to_dataframe-schema Notes for dCDH, SunAbraham, WooldridgeDiD, LPDiD, DurationDiD and SpilloverDiD), the migration guide, `llms-full.txt`, the practitioner guide, `docs/api/wooldridge_etwfe.rst` and `docs/api/spillover.rst` describe the frame; notebooks 16 (re-executed, outputs refreshed and prose reconciled - it leaves the TODO.md tutorial re-execution row), 19 (reads `event_time`/`att`/`is_reference`) and 14 (export prose) updated. TODO.md gains the `level='group'` relay row. diff --git a/diff_diff/chaisemartin_dhaultfoeuille_results.py b/diff_diff/chaisemartin_dhaultfoeuille_results.py index 855673f5d..37ea8a885 100644 --- a/diff_diff/chaisemartin_dhaultfoeuille_results.py +++ b/diff_diff/chaisemartin_dhaultfoeuille_results.py @@ -1693,9 +1693,19 @@ def to_dataframe(self, level: str = "overall") -> pd.DataFrame: - ``"per_period"``: one row per time period with ``did_plus_t``, ``did_minus_t``, switching cell counts, and the A11-zeroed flags. - - ``"event_study"``: one row per horizon (positive and - negative/placebo), including a reference period at - horizon 0. Available when ``L_max >= 1``. + - ``"event_study"``: the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the + pinned ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, + t_stat, p_value, conf_int_lower, conf_int_upper, cband_lower, + cband_upper, n, df, is_reference, estimand``), identical across + estimators (4.0, ledger row M-093). One row per horizon on the + paper's ``l1_first_switch`` convention: placebos at negative + ``event_time``, the reference marked ``is_reference`` at 0, + ``DID_l`` at ``l >= 1``. The per-horizon estimand labels + (``DID^pl_l`` / ``ref`` / ``DID_l``) are not carried - the + ``estimand`` column is the ATT contract's ``"att"``; + ``summary()`` keeps the paper's labels. Available when + ``L_max >= 1``. - ``"normalized"``: one row per horizon for the normalized effects ``DID^n_l``. Available when ``L_max >= 1``. - ``"twfe_weights"``: per-(group, time) TWFE decomposition @@ -1847,64 +1857,7 @@ def to_dataframe(self, level: str = "overall") -> pd.DataFrame: return pd.DataFrame(rows) elif level == "event_study": - rows = [] - # Placebo horizons (negative keys) - if self.placebo_event_study: - for h in sorted(self.placebo_event_study.keys()): - entry = self.placebo_event_study[h] - cband = entry.get("cband_conf_int", (np.nan, np.nan)) - rows.append( - { - "horizon": h, - "estimand": f"DID^pl_{abs(h)}", - "effect": entry["effect"], - "se": entry["se"], - "t_stat": entry["t_stat"], - "p_value": entry["p_value"], - "conf_int_lower": entry["conf_int"][0], - "conf_int_upper": entry["conf_int"][1], - "n_obs": entry["n_obs"], - "cband_lower": cband[0] if cband else np.nan, - "cband_upper": cband[1] if cband else np.nan, - } - ) - # Reference period (horizon 0) - rows.append( - { - "horizon": 0, - "estimand": "ref", - "effect": 0.0, - "se": np.nan, - "t_stat": np.nan, - "p_value": np.nan, - "conf_int_lower": np.nan, - "conf_int_upper": np.nan, - "n_obs": 0, - "cband_lower": np.nan, - "cband_upper": np.nan, - } - ) - # Positive horizons - if self.event_study_effects: - for h in sorted(self.event_study_effects.keys()): - entry = self.event_study_effects[h] - cband = entry.get("cband_conf_int", (np.nan, np.nan)) - rows.append( - { - "horizon": h, - "estimand": self._horizon_label(h), - "effect": entry["effect"], - "se": entry["se"], - "t_stat": entry["t_stat"], - "p_value": entry["p_value"], - "conf_int_lower": entry["conf_int"][0], - "conf_int_upper": entry["conf_int"][1], - "n_obs": entry["n_obs"], - "cband_lower": cband[0] if cband else np.nan, - "cband_upper": cband[1] if cband else np.nan, - } - ) - return pd.DataFrame(rows) + return self.aggregate("event_study").to_dataframe() elif level == "normalized": if not self.normalized_effects: diff --git a/diff_diff/continuous_did_results.py b/diff_diff/continuous_did_results.py index b015abe5a..9b0aea47a 100644 --- a/diff_diff/continuous_did_results.py +++ b/diff_diff/continuous_did_results.py @@ -494,6 +494,15 @@ def to_dataframe(self, level: str = "dose_response") -> pd.DataFrame: ---------- level : str, default="dose_response" ``"dose_response"``, ``"group_time"``, or ``"event_study"``. + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + Analytical fits recompute the binarized event study from the + retained kit; bootstrapped fits relay the fit-time percentile + surface. """ if level == "dose_response": att_df = self.dose_response_att.to_dataframe() @@ -526,29 +535,7 @@ def to_dataframe(self, level: str = "dose_response") -> pd.DataFrame: ) return pd.DataFrame(rows) elif level == "event_study": - if self.event_study_effects is None: - raise ValueError( - "Event study effects not stored (only bootstrapped fits " - "materialize the surface at fit). Call " - "results.aggregate('event_study') for the unified " - "post-fit container - analytic fits recompute it from " - "the retained kit; a result unpickled from an older " - "release must be re-fit with diff-diff >= 3.9." - ) - rows = [] - for rel_t, data in sorted(self.event_study_effects.items()): - rows.append( - { - "relative_period": rel_t, - "att_glob": data["effect"], - "se": data["se"], - "t_stat": data["t_stat"], - "p_value": data["p_value"], - "conf_int_lower": data["conf_int"][0], - "conf_int_upper": data["conf_int"][1], - } - ) - return pd.DataFrame(rows) + return self.aggregate("event_study").to_dataframe() else: raise ValueError( f"Unknown level: {level}. Use 'dose_response', 'group_time', or 'event_study'." diff --git a/diff_diff/duration_did_results.py b/diff_diff/duration_did_results.py index 50b900034..db3871fa5 100644 --- a/diff_diff/duration_did_results.py +++ b/diff_diff/duration_did_results.py @@ -445,11 +445,21 @@ def to_dataframe(self, level: str = "periods") -> pd.DataFrame: Parameters ---------- - level : {"periods", "att"} + level : {"periods", "att", "event_study"} ``"periods"``: one row per post-treatment date with the effect, pointwise inference, simultaneous band and validity status. ``"att"``: the single headline row. + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + Relative event time (``-1`` is the reference row, ``0..`` the + post-treatment dates in order). """ + if level == "event_study": + return self.aggregate("event_study").to_dataframe() if level == "att": return pd.DataFrame( [ @@ -482,7 +492,7 @@ def to_dataframe(self, level: str = "periods") -> pd.DataFrame: "status": list(self.period_status), } ) - raise ValueError(f"level must be 'periods' or 'att', got {level!r}") + raise ValueError(f"level must be 'periods', 'att' or 'event_study', got {level!r}") # ------------------------------------------------------------------ # # Summary # diff --git a/diff_diff/efficient_did_results.py b/diff_diff/efficient_did_results.py index 3c298fef7..df1ae14fa 100644 --- a/diff_diff/efficient_did_results.py +++ b/diff_diff/efficient_did_results.py @@ -810,6 +810,15 @@ def to_dataframe(self, level: str = "group_time") -> pd.DataFrame: ---------- level : str ``"group_time"``, ``"event_study"``, or ``"group"``. + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + Bootstrapped fits replay the fit-time multiplier bootstrap on + every call; ``"group"`` raises on 4.0 fits with a pointer to + ``aggregate('group')``. """ if level == "group_time": rows = [] @@ -829,29 +838,7 @@ def to_dataframe(self, level: str = "group_time") -> pd.DataFrame: return pd.DataFrame(rows) elif level == "event_study": - if self.event_study_effects is None: - raise ValueError( - "Event study effects are not stored on 4.0 fits; use " - "results.aggregate('event_study').to_dataframe() " - "(bootstrapped fits replay the fit-time multiplier " - "bootstrap - no refit needed); a result unpickled from " - "a pre-3.9 release carries no aggregation kit and must " - "be refit." - ) - rows = [] - for rel_t, data in sorted(self.event_study_effects.items()): - rows.append( - { - "relative_period": rel_t, - "effect": data["effect"], - "se": data["se"], - "t_stat": data["t_stat"], - "p_value": data["p_value"], - "conf_int_lower": data["conf_int"][0], - "conf_int_upper": data["conf_int"][1], - } - ) - return pd.DataFrame(rows) + return self.aggregate("event_study").to_dataframe() elif level == "group": if self.group_effects is None: diff --git a/diff_diff/guides/llms-full.txt b/diff_diff/guides/llms-full.txt index f05198626..d178369f5 100644 --- a/diff_diff/guides/llms-full.txt +++ b/diff_diff/guides/llms-full.txt @@ -335,7 +335,7 @@ for h in sorted(results.event_study_effects): e = results.event_study_effects[h] print(f" DID_{h} = {e['effect']:.3f} (SE={e['se']:.3f})") print(f"Cost-benefit delta: {results.cost_benefit_delta['delta']:.3f}") -df = results.to_dataframe("event_study") # includes placebos as negative horizons +df = results.to_dataframe("event_study") # unified EVENT_STUDY_SCHEMA: placebos at negative event_time, reference marked is_reference at 0 ``` **Standalone TWFE diagnostic** (without fitting the full estimator): @@ -1794,7 +1794,7 @@ Returned by `CallawaySantAnna.fit()`. | `event_study_effects` | `dict[int, dict] | None` | Legacy field - `None` on EVERY fit since 4.0 (fit() computes no event-study surface); use `aggregate("event_study")` | | `group_effects` | `dict | None` | Legacy field - `None` on EVERY fit since 4.0; use `aggregate("group")` | -**Methods:** `aggregate(type, weights=None, *, balance_e=None)`, `summary()`, `print_summary()`, `to_dataframe(level="group_time")` (the event-study / group tables come from `aggregate(...).to_dataframe()`; `level="event_study"|"group"` raises on 4.0 fits) +**Methods:** `aggregate(type, weights=None, *, balance_e=None)`, `summary()`, `print_summary()`, `to_dataframe(level="group_time")` (`level="event_study"` returns `aggregate('event_study').to_dataframe()` - the unified `EVENT_STUDY_SCHEMA` frame, 4.0 M-093; `level="group"` raises on 4.0 fits) `aggregate("simple"|"event_study"|"group"|"total")` re-aggregates POST-FIT with no @@ -1839,7 +1839,7 @@ Returned by `SunAbraham.fit()`. | `n_clusters` | `int | None` | Effective count of those clusters (4.0, appended field); None when `cluster_name` is None | | `event_study_df` | `dict[int, float] | None` | Per-event-time df provenance (G-1 on plain clustered analytic fits under the 4.0 default; the finite residual df under `df_convention="residual"`; None under bootstrap) | -**Methods:** `summary()`, `print_summary()`, `to_dataframe(level="event_study"|"cohort")` +**Methods:** `summary()`, `print_summary()`, `to_dataframe(level="event_study"|"cohort")` (the event-study level is the unified `EVENT_STUDY_SCHEMA` frame with the omitted reference period as a synthesized `is_reference` row - 4.0 M-093) ### SyntheticDiDResults @@ -1938,7 +1938,7 @@ staggered design's event-study / cohort surfaces are post-fit (`aggregate()`). | `bootstrap_results` | `CSBootstrapResults \| None` | Staggered multiplier-bootstrap payload | | `epv_diagnostics` | `dict \| None` | Keyed by subgroup (2x2x2) or `(g, t)` cell (staggered) | -**Methods:** `summary()`, `print_summary()`, `to_dict()`, `to_dataframe(level=None)` (`'overall'` one-row scalars, both designs; `'group_time'` cells, staggered), `epv_summary()`, `aggregate(type, weights=None, *, balance_e=None)` - `'simple'` relays the stored headline in both designs (one-row `AggregationResult`; `weights='event_time'` gives the paper's Eq. 4.14 overall, the unweighted mean of post-treatment ES(e), accepting `balance_e=`); `'event_study'` (`EventStudyResults`, `balance_e=`) and `'group'` (`AggregationResult`) are recomputed from the retained kit in the staggered design - bootstrapped fits REPLAY the fit-time multiplier bootstrap - and raise in the 2x2x2 design. No `'total'`. The 3.x `StaggeredTripleDiffResults`, its `overall_*` fields and the fit-time `aggregate=` / `balance_e=` keywords were removed at 4.0 (rows M-014 / M-140 / M-141). +**Methods:** `summary()`, `print_summary()`, `to_dict()`, `to_dataframe(level=None)` (`'overall'` one-row scalars, both designs; `'group_time'` cells, staggered; `'event_study'` the unified `EVENT_STUDY_SCHEMA` frame via `aggregate('event_study')`, staggered only - 4.0 M-093), `epv_summary()`, `aggregate(type, weights=None, *, balance_e=None)` - `'simple'` relays the stored headline in both designs (one-row `AggregationResult`; `weights='event_time'` gives the paper's Eq. 4.14 overall, the unweighted mean of post-treatment ES(e), accepting `balance_e=`); `'event_study'` (`EventStudyResults`, `balance_e=`) and `'group'` (`AggregationResult`) are recomputed from the retained kit in the staggered design - bootstrapped fits REPLAY the fit-time multiplier bootstrap - and raise in the 2x2x2 design. No `'total'`. The 3.x `StaggeredTripleDiffResults`, its `overall_*` fields and the fit-time `aggregate=` / `balance_e=` keywords were removed at 4.0 (rows M-014 / M-140 / M-141). ### BaconDecompositionResults @@ -2010,6 +2010,8 @@ Individual 2x2 DiD comparison (used in BaconDecompositionResults). ### Common Results Pattern for Staggered Estimators +**`to_dataframe(level="event_study")` (4.0, ledger row M-093):** every results class that carries an event-study surface returns the unified `EVENT_STUDY_SCHEMA` frame (`event_time, att, se, t_stat, p_value, conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, is_reference, estimand`) - identical to `results.aggregate('event_study').to_dataframe()` (CallawaySantAnna/DMLDiD, EfficientDiD, ImputationDiD, TwoStageDiD, StackedDiD, ContinuousDiD, TripleDifference staggered, ChaisemartinDHaultfoeuille, HAD event-study, DurationDiD, LWDiD) or to `build_event_study_surface(results).to_dataframe()` (SunAbraham, LPDiD, WooldridgeDiD, SpilloverDiD). Analytical fits recompute from the retained kit (bootstrapped CallawaySantAnna/EfficientDiD/TripleDifference fits REPLAY the fit-time bootstrap on every call); the 3.x bespoke frames (`relative_period`, `effect`/`att_glob`, `conf_int_lo/hi`, `n_obs`, dCDH's `horizon`/`estimand 'ref'`) and the "not stored on 4.0 fits" raises are gone. Two producers append fit-configuration columns AFTER the schema: WooldridgeDiD (`cohort_trends`, `aggregation_weights`; its frame requires a prior `aggregate('event_study', weights=)` and raises otherwise) and LWDiD (`rolling`, `estimation_method`, `vcov_type`). `SpilloverDiDResults.to_dataframe(level='simple'|'event_study')` and `DurationDiDResults.to_dataframe(level='event_study')` are new levels; `level='group'` on CallawaySantAnna/EfficientDiD/TripleDifference still raises with the post-fit pointer. + ImputationDiDResults, TwoStageDiDResults, StackedDiDResults, and EfficientDiDResults share a similar structure: | Attribute | Type | Description | @@ -2149,7 +2151,7 @@ Per-horizon event-study results container for `HeterogeneousAdoptionDiD`'s event | `cband_n_bootstrap` | `int | None` | Bootstrap iterations used for the band | | `event_study_df` | `float | None` | Scalar survey df governing every horizon's t-inference (the unit-level design's `df_survey`); `None` on non-survey fits. Threaded into the unified container's per-row `df` column via `_from_had` | -**Methods:** `summary()`, `print_summary()`, `to_dict()`, `to_dataframe()`, `aggregate(type, ...)` (M-027 pure view: `'event_study'` only - the unified `EventStudyResults` container via the `_from_had` passthrough, cband fields included; no kit; `'simple'` needs a two-period fit - no overall WAS is stored here) +**Methods:** `summary()`, `print_summary()`, `to_dict()`, `to_dataframe(level="event_study")` (the unified `EVENT_STUDY_SCHEMA` frame via `aggregate('event_study')`; `estimand` carries the WAS label - 4.0 M-093), `aggregate(type, ...)` (M-027 pure view: `'event_study'` only - the unified `EventStudyResults` container via the `_from_had` passthrough, cband fields included; no kit; `'simple'` needs a two-period fit - no overall WAS is stored here) ### ChangesInChangesResults diff --git a/diff_diff/guides/llms-practitioner.txt b/diff_diff/guides/llms-practitioner.txt index 3bd476506..e60783004 100644 --- a/diff_diff/guides/llms-practitioner.txt +++ b/diff_diff/guides/llms-practitioner.txt @@ -520,7 +520,7 @@ SA computes event-study and cohort effects automatically during `fit()`: ```python sa = SunAbraham() results = sa.fit(data, ...) -# Event-study effects (dynamic): +# Event-study effects (dynamic; the unified EVENT_STUDY_SCHEMA frame, 4.0): es_df = results.to_dataframe(level='event_study') # Cohort-specific effects: cohort_df = results.to_dataframe(level='cohort') diff --git a/diff_diff/had.py b/diff_diff/had.py index d82a71b47..c52ef20e1 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -1009,30 +1009,28 @@ def to_dict(self) -> Dict[str, Any]: "event_study_df": self.event_study_df, } - def to_dataframe(self) -> pd.DataFrame: - """Return a tidy per-horizon DataFrame. + def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: + """Return the unified per-horizon event-study table. - Columns: ``event_time, att, se, t_stat, p_value, conf_int_low, - conf_int_high, n_obs``. One row per event-time horizon. When a - simultaneous band was computed (``cband=True`` with a - weighted/survey OR clustered fit), also includes ``cband_low`` and - ``cband_high`` columns. + Parameters + ---------- + level : {"event_study"}, default "event_study" + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, + p_value, conf_int_lower, conf_int_upper, cband_lower, cband_upper, + n, df, is_reference, estimand``), identical across estimators + (4.0, ledger row M-093). One row per estimated horizon (no + reference row - the ``F-1`` anchor is identically zero and the + WAS is not identified there); ``n`` counts units + (``n_kind="units"``); ``estimand`` carries ``target_parameter`` + (``"WAS"`` / ``"WAS_d_lower"``) because the ``att`` column is NOT + an ATT; the ``cband_*`` columns are NaN when no simultaneous band + was computed. """ - data: Dict[str, Any] = { - "event_time": self.event_times, - "att": self.att, - "se": self.se, - "t_stat": self.t_stat, - "p_value": self.p_value, - "conf_int_low": self.conf_int_low, - "conf_int_high": self.conf_int_high, - "n_obs": self.n_obs_per_horizon, - } - if self.cband_low is not None: - data["cband_low"] = self.cband_low - if self.cband_high is not None: - data["cband_high"] = self.cband_high - return pd.DataFrame(data) + if level != "event_study": + raise ValueError(f"Unknown level: {level!r}. Use 'event_study'.") + return self.aggregate("event_study").to_dataframe() # ------------------------------------------------------------------ # Post-fit aggregation (rows M-027/M-122): PURE VIEW, no kit. diff --git a/diff_diff/imputation_results.py b/diff_diff/imputation_results.py index 3c3cc0228..b8886bc0e 100644 --- a/diff_diff/imputation_results.py +++ b/diff_diff/imputation_results.py @@ -828,6 +828,17 @@ def to_dataframe(self, level: str = "observation") -> pd.DataFrame: - "event_study": Event study effects by relative time - "group": Group (cohort) effects + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + Analytical fits recompute the surface from the retained kit + (a replicate-weight survey fit with ``pretrends=True`` raises + ``NotImplementedError``, as ``aggregate('event_study')`` does); + bootstrapped fits relay the fit-time percentile surface. + Returns ------- pd.DataFrame @@ -837,29 +848,7 @@ def to_dataframe(self, level: str = "observation") -> pd.DataFrame: return self.treatment_effects.copy() elif level == "event_study": - if self.event_study_effects is None: - raise ValueError( - "Event study effects not stored on this fit. Use " - "results.aggregate('event_study') - analytical fits " - "recompute from the retained kit; bootstrapped fits relay " - "the surface computed at fit. A result unpickled from a " - "pre-3.9 release carries no kit and must be re-fit." - ) - rows = [] - for h, data in sorted(self.event_study_effects.items()): - rows.append( - { - "relative_period": h, - "effect": data["effect"], - "se": data["se"], - "t_stat": data["t_stat"], - "p_value": data["p_value"], - "conf_int_lower": data["conf_int"][0], - "conf_int_upper": data["conf_int"][1], - "n_obs": data.get("n_obs", np.nan), - } - ) - return pd.DataFrame(rows) + return self.aggregate("event_study").to_dataframe() elif level == "group": if self.group_effects is None: diff --git a/diff_diff/lpdid_results.py b/diff_diff/lpdid_results.py index 36c5c0a88..e1afdfa08 100644 --- a/diff_diff/lpdid_results.py +++ b/diff_diff/lpdid_results.py @@ -4,7 +4,7 @@ import numpy as np import pandas as pd -from diff_diff.results_base import BaseResults, _coverage_pct +from diff_diff.results_base import BaseResults, _coverage_pct, build_event_study_surface @dataclass @@ -140,12 +140,29 @@ def conf_int(self) -> Tuple[float, float]: # serialization # ------------------------------------------------------------------ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: + """Tabular view. + + Parameters + ---------- + level : {"event_study", "pooled"}, default "event_study" + ``level="event_study"`` returns the unified event-study table - + ``build_event_study_surface(self).to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + The native per-horizon frame (``horizon, coefficient, ..., n_obs, + n_clusters``) stays available as the ``event_study`` attribute; a + fit that computed no event study (``only_pooled=True``) raises, and + so does a zero-row stored frame. ``"pooled"`` returns a copy of the + native pooled frame. + """ # "event_study" is the canonical level spelling library-wide (the # 3.x "event" spelling was retired at 4.0 and now raises below). if level == "event_study": if self.event_study is None: raise ValueError("event_study dataframe was not computed") - return self.event_study.copy() + return build_event_study_surface(self).to_dataframe() if level == "pooled": if self.pooled is None: raise ValueError("pooled dataframe was not computed") diff --git a/diff_diff/practitioner.py b/diff_diff/practitioner.py index 1dd29bc57..ae43246c2 100644 --- a/diff_diff/practitioner.py +++ b/diff_diff/practitioner.py @@ -613,7 +613,7 @@ def _handle_sa(results: Any): "these are computed automatically during fit()." ), code=( - "# SA event-study effects:\n" + "# SA event-study effects (the unified EVENT_STUDY_SCHEMA frame):\n" "sa_es_df = results.to_dataframe(level='event_study')\n" "# SA cohort effects:\n" "sa_cohort_df = results.to_dataframe(level='cohort')" diff --git a/diff_diff/results.py b/diff_diff/results.py index cbc64c553..5f1143e88 100644 --- a/diff_diff/results.py +++ b/diff_diff/results.py @@ -16,6 +16,7 @@ BaseResults, _coverage_pct, _require_fit_alpha, + build_event_study_surface, ) @@ -517,6 +518,30 @@ def __setstate__(self, state: Dict[str, Any]) -> None: state[name] = frame self.__dict__.update(state) + def to_dataframe(self, level: str = "simple") -> pd.DataFrame: + """Tabular view. + + Parameters + ---------- + level : {"simple", "event_study"}, default "simple" + ``"simple"``: the one-row headline frame (the inherited + :meth:`DiDResults.to_dataframe`). ``"event_study"`` returns the + unified event-study table - ``build_event_study_surface(self) + .to_dataframe()`` over the per-event-time DIRECT effects + (``att_dynamic``) - with the pinned ``EVENT_STUDY_SCHEMA`` columns + (``event_time, att, se, t_stat, p_value, conf_int_lower, + conf_int_upper, cband_lower, cband_upper, n, df, is_reference, + estimand``), identical across estimators (4.0, ledger row M-093). + An aggregate-mode fit (``event_study=False``) carries no + event-study surface and raises ``ValueError``; the per-ring + spillover dynamics stay on ``spillover_effects``. + """ + if level == "simple": + return super().to_dataframe() + if level == "event_study": + return build_event_study_surface(self).to_dataframe() + raise ValueError(f"Unknown level: {level!r}. Use 'simple' or 'event_study'.") + def summary(self, alpha: Optional[float] = None) -> str: """Extended summary with ATT row, per-event-time direct block, and per-(ring, event-time) spillover block. diff --git a/diff_diff/results_base.py b/diff_diff/results_base.py index 74b13f398..d2bb92d8a 100644 --- a/diff_diff/results_base.py +++ b/diff_diff/results_base.py @@ -901,6 +901,9 @@ def summary(self, alpha: Optional[float] = None) -> str: "4.0 stores no surface - re-fit with diff-diff >= 4.0, which computes " "it at fit)" ), + # SunAbraham always computes its event-study surface at fit (no + # aggregate() step exists); absence means a hand-built or corrupted object. + "SunAbrahamResults": "re-fit; SunAbraham always computes the event-study surface", "WooldridgeDiDResults": "call results.aggregate('event_study') first", "SpilloverDiDResults": "refit with event_study=True", "ChaisemartinDHaultfoeuilleResults": "refit with L_max >= 1", @@ -1623,7 +1626,10 @@ def build_event_study_surface(results: Any) -> EventStudyResults: """Build the unified event-study surface from a fitted results object. Package-internal (spec section 5 / row M-092): public exposure rides - ``results.aggregate(type="event_study")`` (Phase 2 PR (b)); the Phase 3 + ``results.aggregate(type="event_study")`` (Phase 2 PR (b)) and, since the + 4.0 cut (row M-093, item 8b), every results class's + ``to_dataframe(level="event_study")``, which returns this container's + ``to_dataframe()`` (the ``EVENT_STUDY_SCHEMA`` columns); the Phase 3 merged TwoWayFixedEffects event-study mode returns the container natively. Values are copied bit-exactly from the native surface; an absent surface raises ``ValueError`` naming the call that produces diff --git a/diff_diff/stacked_did_results.py b/diff_diff/stacked_did_results.py index 890521be8..6c073e2ea 100644 --- a/diff_diff/stacked_did_results.py +++ b/diff_diff/stacked_did_results.py @@ -554,35 +554,22 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: - "event_study": Event study effects by relative time - "group": Group (cohort) effects + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + Returns ------- pd.DataFrame Results as DataFrame. """ if level == "event_study": - if self.event_study_effects is None: - # Only reachable on pre-3.9 pickles: 3.9+ fits always - # materialize the surface (row M-024). - raise ValueError( - "Event study effects not present on this results object. " - "Re-fit with diff-diff >= 3.9, which always computes the " - "event-study surface." - ) - rows = [] - for h, data in sorted(self.event_study_effects.items()): - rows.append( - { - "relative_period": h, - "effect": data["effect"], - "se": data["se"], - "t_stat": data["t_stat"], - "p_value": data["p_value"], - "conf_int_lower": data["conf_int"][0], - "conf_int_upper": data["conf_int"][1], - "n_obs": data.get("n_obs", np.nan), - } - ) - return pd.DataFrame(rows) + # A pre-3.9 pickle (no stored surface) raises the builder's + # "re-fit with diff-diff >= 3.9" hint; 3.9+ fits always carry it. + return self.aggregate("event_study").to_dataframe() elif level == "group": raise ValueError( diff --git a/diff_diff/staggered_results.py b/diff_diff/staggered_results.py index 6873163b0..ef165ec62 100644 --- a/diff_diff/staggered_results.py +++ b/diff_diff/staggered_results.py @@ -965,6 +965,16 @@ def to_dataframe(self, level: str = "group_time") -> pd.DataFrame: ---------- level : str, default="group_time" Level of aggregation: "group_time", "event_study", or "group". + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + Analytical fits recompute the surface from the retained + aggregation kit; bootstrapped fits REPLAY the fit-time multiplier + bootstrap on every call (no memoization). ``"group"`` raises on + 4.0 fits with a pointer to ``aggregate('group')``. Returns ------- @@ -993,29 +1003,7 @@ def to_dataframe(self, level: str = "group_time") -> pd.DataFrame: return pd.DataFrame(rows) elif level == "event_study": - if self.event_study_effects is None: - raise ValueError( - "Event study effects are not stored on 4.0 fits; use " - "results.aggregate('event_study').to_dataframe() " - "(no refit required)." - ) - rows = [] - for rel_t, data in sorted(self.event_study_effects.items()): - cband_ci = data.get("cband_conf_int", (np.nan, np.nan)) - rows.append( - { - "relative_period": rel_t, - "effect": data["effect"], - "se": data["se"], - "t_stat": data["t_stat"], - "p_value": data["p_value"], - "conf_int_lower": data["conf_int"][0], - "conf_int_upper": data["conf_int"][1], - "cband_lower": cband_ci[0], - "cband_upper": cband_ci[1], - } - ) - return pd.DataFrame(rows) + return self.aggregate("event_study").to_dataframe() elif level == "group": if self.group_effects is None: diff --git a/diff_diff/sun_abraham.py b/diff_diff/sun_abraham.py index a25d5d9cc..ba19c6245 100644 --- a/diff_diff/sun_abraham.py +++ b/diff_diff/sun_abraham.py @@ -39,7 +39,12 @@ from diff_diff.survey import ResolvedSurveyDesign, SurveyDesign from diff_diff.linalg import LinearRegression from diff_diff.results import _format_survey_block, _get_significance_stars -from diff_diff.results_base import BaseResults, _coverage_pct, _require_fit_alpha +from diff_diff.results_base import ( + BaseResults, + _coverage_pct, + _require_fit_alpha, + build_event_study_surface, +) from diff_diff.utils import ( ClusterArg, absorbed_fe_cr1_k_increment, @@ -450,6 +455,15 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: ---------- level : str, default="event_study" Level of aggregation: "event_study" or "cohort". + ``level="event_study"`` returns the unified event-study table - + ``build_event_study_surface(self).to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + The omitted reference period (``reference_period``) appears as a + synthesized ``is_reference`` row; ``n`` is the number of + contributing cohorts per event time. Returns ------- @@ -457,20 +471,7 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: Results as DataFrame. """ if level == "event_study": - rows = [] - for rel_t, data in sorted(self.event_study_effects.items()): - rows.append( - { - "relative_period": rel_t, - "effect": data["effect"], - "se": data["se"], - "t_stat": data["t_stat"], - "p_value": data["p_value"], - "conf_int_lower": data["conf_int"][0], - "conf_int_upper": data["conf_int"][1], - } - ) - return pd.DataFrame(rows) + return build_event_study_surface(self).to_dataframe() elif level == "cohort": if self.cohort_effects is None: diff --git a/diff_diff/triple_diff_results.py b/diff_diff/triple_diff_results.py index 457aadb35..06ae00e7b 100644 --- a/diff_diff/triple_diff_results.py +++ b/diff_diff/triple_diff_results.py @@ -682,10 +682,16 @@ def to_dataframe(self, level: Optional[str] = None) -> pd.DataFrame: ``"overall"`` - one row of the scalar ``to_dict()`` entries (both designs); ``"group_time"`` - the ``ATT(g, t)`` cells (staggered design only). ``None`` selects ``"overall"`` in the 2x2x2 design - and ``"group_time"`` in the staggered design. ``"event_study"`` and - ``"group"`` are not stored on 4.0 fits - use - ``results.aggregate('event_study').to_dataframe()`` / - ``results.aggregate('group').to_dataframe()``. + and ``"group_time"`` in the staggered design. + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + Staggered design only - the 2x2x2 design raises the single-ATT + error from ``aggregate``. ``"group"`` is not stored on 4.0 fits - + use ``results.aggregate('group').to_dataframe()``. Returns ------- @@ -719,10 +725,12 @@ def to_dataframe(self, level: Optional[str] = None) -> pd.DataFrame: row["epv"] = self.epv_diagnostics[(g, t)].get("epv") rows.append(row) return pd.DataFrame(rows) - if level in ("event_study", "group"): + if level == "event_study": + return self.aggregate("event_study").to_dataframe() + if level == "group": raise ValueError( - f"{'Event study' if level == 'event_study' else 'Group'} effects are not " - f"stored on 4.0 fits; use results.aggregate({level!r}).to_dataframe() " + "Group effects are not stored on 4.0 fits; use " + "results.aggregate('group').to_dataframe() " "(no refit required; staggered design only)." ) raise ValueError( diff --git a/diff_diff/two_stage_results.py b/diff_diff/two_stage_results.py index febbe2aae..b6969a1db 100644 --- a/diff_diff/two_stage_results.py +++ b/diff_diff/two_stage_results.py @@ -794,6 +794,16 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: - "group": Group (cohort) effects - "observation": Per-observation treatment effects + ``level="event_study"`` returns the unified event-study table - + ``self.aggregate("event_study").to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + The default level therefore RECOMPUTES the Stage-2 event study + from the retained kit on analytical fits (bootstrapped fits relay + the fit-time percentile surface) instead of raising. + Returns ------- pd.DataFrame @@ -803,29 +813,7 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: return self.treatment_effects.copy() elif level == "event_study": - if self.event_study_effects is None: - raise ValueError( - "Event study effects not stored on this fit. Use " - "results.aggregate('event_study') - analytical fits " - "recompute from the retained kit; bootstrapped fits relay " - "the surface computed at fit. A result unpickled from a " - "pre-3.9 release carries no kit and must be re-fit." - ) - rows = [] - for h, data in sorted(self.event_study_effects.items()): - rows.append( - { - "relative_period": h, - "effect": data["effect"], - "se": data["se"], - "t_stat": data["t_stat"], - "p_value": data["p_value"], - "conf_int_lower": data["conf_int"][0], - "conf_int_upper": data["conf_int"][1], - "n_obs": data.get("n_obs", np.nan), - } - ) - return pd.DataFrame(rows) + return self.aggregate("event_study").to_dataframe() elif level == "group": if self.group_effects is None: diff --git a/diff_diff/wooldridge_results.py b/diff_diff/wooldridge_results.py index 9886b4fc3..28653f5cc 100644 --- a/diff_diff/wooldridge_results.py +++ b/diff_diff/wooldridge_results.py @@ -19,6 +19,7 @@ BaseResults, _coverage_pct, _require_fit_alpha, + build_event_study_surface, ) from diff_diff.utils import safe_inference @@ -824,6 +825,19 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: level : "simple" | "group" | "calendar" | "event_study" | "gt" Use "gt" to export raw group-time effects. Any other value raises ``ValueError``. + ``level="event_study"`` returns the unified event-study table - + ``build_event_study_surface(self).to_dataframe()`` - with the pinned + ``EVENT_STUDY_SCHEMA`` columns (``event_time, att, se, t_stat, p_value, + conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, + is_reference, estimand``), identical across estimators (4.0, ledger + row M-093). + Two fit-configuration columns follow the schema: ``cohort_trends`` + and ``aggregation_weights`` (the weighting scheme of the stored + event-study aggregation). The event-study aggregation is computed + post-fit, so ``to_dataframe(level="event_study")`` before + ``aggregate("event_study", weights=...)`` raises ``ValueError`` + (through 3.x it returned an empty frame) - the weighting scheme + changes the numbers and is never chosen silently. """ valid_levels = ("simple", "group", "calendar", "event_study", "gt") if level not in valid_levels: @@ -837,6 +851,14 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: row.update(eff) rows.append(row) return pd.DataFrame(rows) + if aggregation == "event_study": + # Unified EVENT_STUDY_SCHEMA prefix (4.0, M-093 item 8b); a fit + # whose event-study aggregation was never requested raises the + # builder's "call results.aggregate('event_study') first" hint. + frame = build_event_study_surface(self).to_dataframe() + frame["cohort_trends"] = self.cohort_trends + frame["aggregation_weights"] = self.aggregation_weights.get("event_study", "cell") + return frame # Active weighting scheme for the requested aggregation surface # (default "cell"; flips to "cohort_share" after an opt-in @@ -881,17 +903,6 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: } for t, eff in sorted((self.calendar_effects or {}).items()) ], - "event_study": [ - { - "relative_period": k, - **{kk: vv for kk, vv in eff.items() if kk != "conf_int"}, - "conf_int_lo": eff["conf_int"][0], - "conf_int_hi": eff["conf_int"][1], - "cohort_trends": self.cohort_trends, - "aggregation_weights": active_weights, - } - for k, eff in sorted((self.event_study_effects or {}).items()) - ], } rows = mapping.get(aggregation, []) return pd.DataFrame(rows) diff --git a/docs/api/spillover.rst b/docs/api/spillover.rst index 7f8a926a7..d0f62af76 100644 --- a/docs/api/spillover.rst +++ b/docs/api/spillover.rst @@ -203,7 +203,11 @@ and planned follow-up enhancements: plotter reads the ``reference_period`` attribute, else the row's ``is_reference`` key; since 4.0 the reference row carries ``is_reference: True`` and no ``n_obs``, and the event-study frames a - boolean ``is_reference`` column with NaN ``n_obs`` there - [M-093]). ``DiagnosticReport`` routing for + boolean ``is_reference`` column with NaN ``n_obs`` there - [M-093]). + ``to_dataframe(level='event_study')`` returns the unified + ``EVENT_STUDY_SCHEMA`` table of the direct effects (``level='simple'``, + the default, is the one-row headline frame; an aggregate-mode fit + raises). ``DiagnosticReport`` routing for ``SpilloverDiDResults`` is wired: parallel-trends (event-study on the direct-effect dynamics), design-effect, and heterogeneity apply; Goodman-Bacon is excluded (spillover identifies off far-away diff --git a/docs/api/wooldridge_etwfe.rst b/docs/api/wooldridge_etwfe.rst index af02f65df..7180cbc7e 100644 --- a/docs/api/wooldridge_etwfe.rst +++ b/docs/api/wooldridge_etwfe.rst @@ -142,6 +142,12 @@ Basic OLS (follows Stata ``jwdid y, ivar(unit) tvar(time) gvar(cohort)``):: print(r.to_dataframe(level='group')) print(r.summary()) +Since 4.0 ``to_dataframe(level='event_study')`` is the library-wide unified +event-study table (``event_time, att, se, t_stat, p_value, conf_int_lower, +conf_int_upper, cband_lower, cband_upper, n, df, is_reference, estimand``) +followed by the ``cohort_trends`` and ``aggregation_weights`` configuration +columns; it requires a prior ``aggregate('event_study')`` and raises otherwise. + .. note:: When ``method="ols"`` is applied to a binary (``{0, 1}``) or non-negative @@ -190,7 +196,9 @@ Aggregation Methods Call ``.aggregate(type, weights=...)``, then export with ``.to_dataframe(level=...)`` (``summary()`` prints the headline simple -aggregation): +aggregation; the ``event_study`` level is the library-wide +``EVENT_STUDY_SCHEMA`` plus ``cohort_trends`` / ``aggregation_weights``, and +raises if ``aggregate('event_study')`` has not been called): .. list-table:: :header-rows: 1 diff --git a/docs/methodology/REGISTRY.md b/docs/methodology/REGISTRY.md index 04990fac6..7e0883853 100644 --- a/docs/methodology/REGISTRY.md +++ b/docs/methodology/REGISTRY.md @@ -1364,7 +1364,7 @@ The multiplier bootstrap uses random weights w_i with E[w]=0 and Var(w)=1: - **Note:** Repeated cross-sections (`panel=False`, Phase 7b): supports surveys like BRFSS, ACS annual, and CPS monthly where units are not followed over time. Uses cross-sectional DRDID (Sant'Anna & Zhao 2020, Section 4): `reg` matches `DRDID::reg_did_rc` (Eq 2.2), `dr` matches `DRDID::drdid_rc` (locally efficient, Eq 3.3+3.4 with 4 OLS fits), `ipw` matches `DRDID::std_ipw_did_rc`. Per-observation influence functions instead of per-unit. All three estimation methods support covariates and survey weights. - **Note:** Panel and RCS influence functions use the library-wide `phi_i = psi_i / n` convention (SE = `sqrt(sum(phi^2))`, algebraically equivalent to R's `sd(psi)*sqrt(n-1)/n`). Leading IF terms are computed on psi scale and divided by n; PS nuisance corrections are computed on psi scale (`score @ solve(Hessian)`) with a single `/n` conversion to phi. - **Note:** Non-survey DR path also includes nuisance IF corrections (PS + OR), matching the survey path structure (Phase 7a). Previously used plug-in IF only. As of v3.7 the non-survey reg and ipw paths carry their corrections too (OR estimation-effect / PS score), so the nuisance-IF treatment is method-uniform. -- **Note (post-fit aggregate() - rows M-020/M-117):** `fit(aggregate=, balance_e=)` was deprecated in 3.9 and REMOVED at 4.0 (PR 3b of the cut: `fit()` computes no event-study/group surface, so the legacy `event_study_effects`/`group_effects` fields - and `cband_crit_value`/`event_study_vcov`/`event_study_vcov_index`/`event_study_df` - are None after EVERY fit, `to_dataframe(level='event_study'|'group')` raises on fresh fits until the M-093 schema rewire, and `survey_design` is positional slot 7) in favor of post-fit `results.aggregate(type, balance_e=)` on the fit-retained aggregation kit ('simple' relays the stored overall inference; 'event_study'/'group' recompute from the kit — on BOOTSTRAPPED fits they REPLAY the fit-time multiplier bootstrap from the kit's `BootstrapReplaySpec` RNG-state snapshot: percentile se/CI/cband matching a fit-time aggregation to BLAS reassociation (~1 ULP, `assert_allclose`, never bit-identity; the discrete percentile p-value is a count statistic outside that claim), df cleared (no analytical provenance beside percentile inference), vcov/vcov_index from the same bootstrap draws (see the bootstrap event-study covariance Note above). **Note (df_survey two-channel contract):** the container-level `df_survey` SCALAR - the fit's resolved scalar inference df (`EventStudyResults.df_survey`) - DELIBERATELY persists on bootstrapped replayed (CS, DMLDiD, EfficientDiD publish it identically) and relayed (ImputationDiD/TwoStageDiD/ContinuousDiD, 4.0 PR 3c) surfaces: it is a live consumer channel (HonestDiD's container branches consume it as the `df=` for their critical values on the containers they admit), and only the PER-ROW `df` column is the per-row inference-provenance channel that percentile bootstrap clears. A bootstrapped ImputationDiD survey fit with `pretrends=True` clears the per-row channel even though the lead rows retained analytical survey-df inference (the bootstrap loop skips pre-period horizons) - the shipped producer convention, backend-stamped and failing closed on a Rust-vs-NumPy weight-backend mismatch or a pre-replay legacy pickle — the M-027 per-level policy, with the former blanket fail-closed retired). `DiagnosticReport` now derives the event-study container internally on plain fits (raw `event_study_effects` absent), so its `parallel_trends`, `pretrends_power`, and `sensitivity` checks run on every CS fit — CS containers are M-093-admitted into `compute_pretrends_power` / `HonestDiD.sensitivity_analysis` with pinned raw-route parity, and the raw field, when present (the requested-but-empty `{}` included), always takes precedence. `heterogeneity` is unaffected (it reads `group_time_effects` on plain fits). Bootstrapped fits now DERIVE successfully via the percentile-bootstrap replay: the derived container carries the covariance of the bootstrap draws as `vcov`, so `parallel_trends` runs the joint Wald test and `pretrends_power`/`sensitivity` use the full covariance (the Bonferroni and diagonal-covariance fallbacks remain where no bootstrap covariance is stored); the replay's re-emitted fit-time warnings are recorded and republished per section. The fail-closed skip remains for kit-less/legacy pickles, backend-mismatched replays, and bootstrapped ImputationDiD/TwoStageDiD/ContinuousDiD fits whose fit-materialized surface was fail-soft cleared (since 4.0 PR 3c those three RELAY the surface computed at fit - see their bootstrapped-fit relay Notes). The recompute levels' replay RE-EMITS the fit-time bootstrap warnings by design (the relay levels' no-re-warn convention is scoped to 'simple'/'total'). (8a, [M-093]: the carrier's reference-only horizons carry `is_reference: True` and no `n_groups`.) +- **Note (post-fit aggregate() - rows M-020/M-117):** `fit(aggregate=, balance_e=)` was deprecated in 3.9 and REMOVED at 4.0 (PR 3b of the cut: `fit()` computes no event-study/group surface, so the legacy `event_study_effects`/`group_effects` fields - and `cband_crit_value`/`event_study_vcov`/`event_study_vcov_index`/`event_study_df` - are None after EVERY fit, `to_dataframe(level='event_study')` returns `aggregate('event_study').to_dataframe()` - the unified `EVENT_STUDY_SCHEMA` frame - since the 4.0 cut ([M-093] item 8b; bootstrapped fits replay the fit-time bootstrap on every call), while `to_dataframe(level='group')` still raises on fresh fits with the post-fit pointer, and `survey_design` is positional slot 7) in favor of post-fit `results.aggregate(type, balance_e=)` on the fit-retained aggregation kit ('simple' relays the stored overall inference; 'event_study'/'group' recompute from the kit — on BOOTSTRAPPED fits they REPLAY the fit-time multiplier bootstrap from the kit's `BootstrapReplaySpec` RNG-state snapshot: percentile se/CI/cband matching a fit-time aggregation to BLAS reassociation (~1 ULP, `assert_allclose`, never bit-identity; the discrete percentile p-value is a count statistic outside that claim), df cleared (no analytical provenance beside percentile inference), vcov/vcov_index from the same bootstrap draws (see the bootstrap event-study covariance Note above). **Note (df_survey two-channel contract):** the container-level `df_survey` SCALAR - the fit's resolved scalar inference df (`EventStudyResults.df_survey`) - DELIBERATELY persists on bootstrapped replayed (CS, DMLDiD, EfficientDiD publish it identically) and relayed (ImputationDiD/TwoStageDiD/ContinuousDiD, 4.0 PR 3c) surfaces: it is a live consumer channel (HonestDiD's container branches consume it as the `df=` for their critical values on the containers they admit), and only the PER-ROW `df` column is the per-row inference-provenance channel that percentile bootstrap clears. A bootstrapped ImputationDiD survey fit with `pretrends=True` clears the per-row channel even though the lead rows retained analytical survey-df inference (the bootstrap loop skips pre-period horizons) - the shipped producer convention, backend-stamped and failing closed on a Rust-vs-NumPy weight-backend mismatch or a pre-replay legacy pickle — the M-027 per-level policy, with the former blanket fail-closed retired). `DiagnosticReport` now derives the event-study container internally on plain fits (raw `event_study_effects` absent), so its `parallel_trends`, `pretrends_power`, and `sensitivity` checks run on every CS fit — CS containers are M-093-admitted into `compute_pretrends_power` / `HonestDiD.sensitivity_analysis` with pinned raw-route parity, and the raw field, when present (the requested-but-empty `{}` included), always takes precedence. `heterogeneity` is unaffected (it reads `group_time_effects` on plain fits). Bootstrapped fits now DERIVE successfully via the percentile-bootstrap replay: the derived container carries the covariance of the bootstrap draws as `vcov`, so `parallel_trends` runs the joint Wald test and `pretrends_power`/`sensitivity` use the full covariance (the Bonferroni and diagonal-covariance fallbacks remain where no bootstrap covariance is stored); the replay's re-emitted fit-time warnings are recorded and republished per section. The fail-closed skip remains for kit-less/legacy pickles, backend-mismatched replays, and bootstrapped ImputationDiD/TwoStageDiD/ContinuousDiD fits whose fit-materialized surface was fail-soft cleared (since 4.0 PR 3c those three RELAY the surface computed at fit - see their bootstrapped-fit relay Notes). The recompute levels' replay RE-EMITS the fit-time bootstrap warnings by design (the relay levels' no-re-warn convention is scoped to 'simple'/'total'). (8a, [M-093]: the carrier's reference-only horizons carry `is_reference: True` and no `n_groups`.) - **Note (post-fit `aggregate('total')` - the estimator-owned total incremental outcome, 3.10):** (a) **Estimand:** `total = C x overall_att` with `se = C x overall_se`, CI scaled by `C`, `t`/`p`/`df` inherited unchanged - an exact relay CONDITIONAL ON THE REALIZED AGGREGATION MASS (never call it unqualified "exact"): `C` is the design-fixed complete-case treated-observation count on every admitted routing, the inherited SE prices the overall's full inference (WIF/estimated-share terms included) but treats `C` as fixed at its realized value, and the random-mass `att*dC` variance term is deliberately out of scope (the DEFERRED survey/RC remainder). Every SE branch is homogeneous of degree 1 in the scaling, so the relay is exact under analytical AND percentile-bootstrap inference. (b) **Fail-closed routing gates**, in order: (1) repeated-cross-section-routed fits (`panel=False`, or genuinely-unbalanced `allow_unbalanced_panel=True` fits - a balanced panel with the flag stays panel-routed and admitted) raise `NotImplementedError` - the RC cohort mass counts UNITS and weights every kept post period by it, a ~T-fold overcount of treated observations on a T-wave RCS (execution-verified); (2) fits declaring a `survey_design=` raise - the realized-mass relay omits the survey mass-uncertainty term and design-aware population-scale totals are not implemented (retained weight scale differs by design family: analytic pweight resolves normalized to sum = n and CS retains no PER-OBSERVATION raw-weight record - the `survey_metadata` scalars report raw-scale `sum_weights`/`weight_range` provenance, but not the per-obs raw vector a population-scale total would need; CS accepts pweight only - while replicate designs retain raw scale); the gate reads the fit-time `is_survey_fit` KIT snapshot, so bare-`cluster=` fits are ADMITTED (their synthesized all-ones internal design is not a survey) and a post-fit edit of the public `survey_metadata` cannot bypass it; the DECLARATION BOUNDARY is deliberate - an explicit unweighted `SurveyDesign(psu=...)` fit is numerically identical to bare `cluster=` yet fails closed by declaration (declaring survey_design opts into the survey contract; conservative, never wrong numbers; the message points at the `cluster=` route); a combined survey+RC fit deterministically gets the RC message; (3) the coincidence check: on bare-`cluster=` fits `_aggregate_simple` weights kept cells by the synthesized cohort masses (full cohort size per kept cell), and when kept cells have INCOMPLETE treated support that realized mass diverges from the complete-case count - the same overcount class as (1) - so the fit raises rather than publishing an ambiguous mass; clean bare-`cluster=` fits (the normal case) are admitted with `C` equal to both counts. Panel-routed pre-upgrade (legacy) kits missing the fit-time snapshots raise the refit message - never a mutable-field or public-replay fallback. (c) **Filter-replay contract:** `C` replays `_aggregate_simple`'s cell selection verbatim over the immutable `agg_gt_cells` fit-time kit snapshot (EDiD snapshot discipline; the kit builder stashes `(g, t, effect, n_treated)` 4-tuples in dict order) - anticipation filter FIRST (cells with `g - anticipation <= t < g` COUNT as post, the business-facing subtlety; universal-base reference cells are excluded by this filter alone, their base period being `< g - anticipation` by construction), finite-effect mask SECOND, weight = cohort mass where `fixed_cohort_agg_weights` provides one else per-cell complete-case `n_treated` (the source's `agg_weight` fallback is deliberately absent - RC-only, gated out), pairwise `np.sum`; the overall scalars come from the same stored fields the `'simple'` relay reads, so a post-fit mutation of `group_time_effects` moves NEITHER factor and `total == C x aggregate('simple').att` cannot desynchronize (pinned by a mutation test). NEVER use `agg_total_weight` (the RC all-units WIF mass). (d) **NaN rules:** empty post set / all-NaN effects give `C = NaN` and an all-NaN row WITHOUT re-emitting the fit-time UserWarnings (the RELAY-level no-re-warn convention — the NaN row is the signal; the recompute levels' bootstrap replay re-emits by design); with finite `C` the relay is VERBATIM - inherited NaNs (a degenerate fit's `(nan, nan)` CI beside finite att/se = 0) pass through, mirroring `aggregate('simple')` per the repo's non-estimable-row convention (att/n are never blanked by NaN inference fields); the ONLY additional blanking is true float overflow (a finite overall value made non-finite BY the `x C` scaling - unreachable in practice, guarded per this code family's finiteness convention). (e) **Bootstrap:** 'total' is a RELAY level - available on bootstrapped fits with the df column NaN (the M-027 per-level policy). (f) **Container conventions:** single row, `level="total"`, `label=["total"]`, `target=["total"]` (a total incremental outcome, NOT an ATT - the single-non-att neutral "estimate" rendering applies), `n = [C]` with `n_kind="obs"`, `weight=[1.0]`, per-class df carrier (CS: `resolve_inference_df` - finite on admitted bare-`cluster=` fits, NaN on plain analytical panel fits). Per-level `n` semantics are deliberately DIFFERENT: 'total''s `n` is the treated observations entering the support, while 'simple''s `n` stays treated+control units by contract - `n_kind` is per-container and each is documented, satisfying the never-conflate rule. (g) **MMM admission:** both `diff_diff.mmm` exporters accept the total container ALONE - `scale` (numeric or `"auto"`) is rejected as double-counting; for overall-total exports this route supersedes `scale="auto"` (see the MMM section). The total remains meaningful only for outcomes additive in levels - the same unverifiable caveat every scale route carries. @@ -1578,6 +1578,7 @@ The guard is fired by `_survey_se_from_group_if` (analytical and replicate) and **Per-path user-specified path selection (`paths_of_interest`):** Python-only API extension — R's `did_multiplegt_dyn(..., by_path=k)` only accepts a positive int (top-k automatic ranking) or `-1` (all observed paths) and provides no list-based selection. Activated via `ChaisemartinDHaultfoeuille(paths_of_interest=[(0, 1, 1, 1), (0, 1, 0, 0)], drop_larger_lower=False)` as an alternative to `by_path=k`; the two are **mutually exclusive** (setting both raises `ValueError` at `__init__` and `set_params` time). Each path tuple must have length `L_max + 1`; the type / element / non-empty / length-uniformity checks fire at `__init__`, the length-vs-L_max check fires at fit-time. `bool` and `np.bool_` are explicitly rejected; `np.integer` is accepted and canonicalized to Python `int` for tuple-key consistency. Duplicates emit a `UserWarning` and are deduplicated; paths not observed in the panel emit a `UserWarning` and are omitted from `path_effects`. Paths appear in `results.path_effects` in the user-specified order, modulo deduplication and unobserved-path filtering. Composes with non-binary D and all downstream `by_path` surfaces (bootstrap, per-path placebos, per-path joint sup-t bands, `covariates`, `trends_linear`, `trends_nonparam`) — mechanical filter on observed paths, no methodology change. Behavior + cross-feature regressions live at `tests/test_chaisemartin_dhaultfoeuille.py::TestPathsOfInterest`. - **Note (Phase 3 `by_path` per-path joint sup-t bands):** When `n_bootstrap > 0` is set with `by_path=k`, per-path joint sup-t simultaneous confidence bands are computed across horizons `1..L_max` within each path. **Methodology:** a single `(n_bootstrap, n_eligible)` multiplier weight matrix (using the estimator's configured `bootstrap_weights` — Rademacher / Mammen / Webb) is drawn per path and broadcast across all horizons of that path, producing correlated bootstrap distributions across horizons within the path. The path-specific critical value `c_p = quantile(max_l |t_l|, 1 - α)` is then used to construct symmetric joint bands `effect_l ± c_p · se_l` per horizon, surfaced in `path_effects[path]["horizons"][l]["cband_conf_int"]` and at top-level `results.path_sup_t_bands[path] = {"crit_value", "alpha", "n_bootstrap", "method", "n_valid_horizons"}`. **Gates:** a path must have `>= 2` valid horizons (finite bootstrap SE > 0) AND a strict majority (more than 50%) of finite sup-t draws to receive a band; otherwise the path is absent from `path_sup_t_bands`. Both gates mirror the OVERALL `event_study_sup_t_bands` semantics at `chaisemartin_dhaultfoeuille_bootstrap.py:605,612`: `len(valid_horizons) >= 2` AND `finite_mask.sum() > 0.5 * n_bootstrap`. Exactly half-finite draws are NOT enough — the gate is strictly greater than half. **Empty-state contract:** `path_sup_t_bands is None` when not requested (no bootstrap, or both `by_path` and `paths_of_interest` are `None`); `{}` when requested but no path passes both gates. **`to_dataframe(level="by_path")` integration:** the table now includes `cband_lower` / `cband_upper` columns for parity with OVERALL `level="event_study"`; populated for positive-horizon rows of paths with a finite sup-t crit, NaN for placebo rows / unbanded paths / the requested-but-empty fallback DataFrame. **Methodology asymmetry vs OVERALL:** OVERALL sup-t reuses the same multi-horizon shared-draw distribution for both the SE in the t-stat denominator and the bootstrap distribution in the numerator. The per-path sup-t draws a fresh shared weight matrix per path AFTER the per-path SE bootstrap block has already populated `results.path_ses` via independent per-(path, horizon) draws — numerator: fresh shared draws, denominator: bootstrap SEs from the earlier independent draws. Asymptotically equivalent to OVERALL's self-consistent reuse, but NOT bit-identical. The fresh draw is intentional: it preserves RNG-state isolation and keeps every existing per-path SE seed-reproducibility test bit-stable post-implementation. **Inherited deviation from R:** the bootstrap SE used as the t-stat denominator carries the cross-path cohort-sharing SE deviation from R documented for `path_effects` above; the per-path sup-t crit therefore inherits the same deviation. **Interpretation:** the band covers joint inference *within a single path across horizons*; it does NOT provide simultaneous coverage *across paths* (a different inference target requiring a `path × horizon` re-derivation, deferred to a future wave). **Deviation from R:** `did_multiplegt_dyn` provides no joint / sup-t / simultaneous bands at any surface — this is a Python-only methodology extension, consistent with the existing OVERALL `event_study_sup_t_bands` (also Python-only). Regression test anchor: `tests/test_chaisemartin_dhaultfoeuille.py::TestByPathSupTBands`. +- **Note (to_dataframe schema, 4.0, [M-093] item 8b):** `to_dataframe(level='event_study')` returns `aggregate('event_study').to_dataframe()` - the unified `EVENT_STUDY_SCHEMA` frame on the paper's `l1_first_switch` convention (placebos at negative `event_time`, the reference marked `is_reference` at `event_time 0`, `DID_l` at `l >= 1`; `n` = switcher groups or cells per `n_kind`). The 3.x native frame (`horizon`, a per-horizon `estimand` label `DID^pl_l` / `ref` / `DID_l`, `effect`, `n_obs 0` on the reference row) is retired: the `estimand` column is the ATT contract's `"att"` (the paper's labels stay the `summary()` vocabulary), and the reference row is marked, never a zero count. The `by_path` `cband_*` columns still mirror the event-study table's `cband_lower`/`cband_upper`. **Reference implementation(s):** - R: [`DIDmultiplegtDYN`](https://cran.r-project.org/package=DIDmultiplegtDYN) (CRAN, maintained by the paper authors). The Python implementation matches `did_multiplegt_dyn(..., effects=1)` at horizon `l = 1`. Parity tests live in `tests/test_chaisemartin_dhaultfoeuille_parity.py`. @@ -1749,7 +1750,7 @@ labels.* 7. **Note (lowest-dose-as-control, Remark 3.1 — library extension beyond `contdid` v0.1.0):** `control_group="lowest_dose"` implements CGBS 2024 Remark 3.1 for settings with no untreated group (`P(D=0) = 0`): the lowest-dose group `d_L` becomes the comparison and the estimand is `ATT(d) − ATT(d_L)` (SPT), with `ATT(d_L) = 0` the omitted reference. Mechanically it is a **control-group swap** — the D=0 control pool is replaced by the `d_L` group; the entire linear influence-function / bootstrap / event-study / survey machinery is control-group-generic and reused unchanged (`ee_control` already carries the reference-group variance, so **no new SE plumbing**). On the discrete saturated basis the backward-difference operator's reference shifts from `0` to `d_L` (`ACRT(d_1) = ATT(d_1)/(d_1 − d_L)`); on the continuous B-spline path the reference shifts only `μ_0` (the level), leaving `ACRT = spline'` unchanged. `contdid` v0.1.0 does **not** implement Remark 3.1, so there is **no external R anchor**; validation (R-free, in CI): an **exact `d_L → 0` equivalence** anchor (relabelling a `never_treated` panel's D=0 group as a tiny common dose `d_L = ε` reproduces the `never_treated` ATT and SE exactly, for any ε), a discrete hand-calc of `ATT(d)−ATT(d_L)`/`ACRT`/`att`/`overall_acrt` and the per-level 2×2 SE (`~1e-10`), continuous mass-point DGP recovery, analytical-vs-bootstrap SE agreement, a pre-period placebo, and MC coverage (`tests/test_methodology_continuous_did.py::TestLowestDose`, `tests/test_continuous_did.py::TestLowestDoseAPI`). The continuous path requires a genuine **mass point** at the minimum dose (`>= 2` units at `d_L`, i.e. `P(D=d_L) > 0`) — the Remark 3.1 identification condition; a singleton minimum fails closed. **Fail-closed policies (no-silent-failures):** (i) never-treated units present with `lowest_dose` → `ValueError` (they would be silently dropped); (ii) singleton `d_L` (no mass point) → `ValueError`; (iii) no treated dose above `d_L` → `ValueError`; (iv) user `dvals ≤ d_L` → `ValueError` (`d_L` is the omitted reference); (v) survey/subpopulation weighting that leaves the `d_L` group with `< 2` positive-weight units → `ValueError` (a single positive-weight reference unit gives `ee_control = 0`, i.e. zero control-side variance — the effective-`>= 2` analogue of the raw mass-point guard, applied after weighting); (vi) a boundary gap `d_1 − d_L` that is a tiny fraction of the dose range warns (huge boundary ACRT/SE). **Deferred (fail-closed `NotImplementedError` + TODO):** multi-cohort `lowest_dose` (needs a within-cohort reference + support-aware cross-cohort aggregation) and `covariates=` × `lowest_dose` (conditional-PT-relative-to-`d_L` estimand). Cross-references `docs/methodology/continuous-did.md` § 5.6. 8. **Note (post-fit aggregate() - rows M-025/M-122):** `fit(aggregate=)` was deprecated in 3.9 and REMOVED at 4.0 (PR 3c of the cut; the no-underscore `"eventstudy"` spelling and the fit-time value validation died with it; `survey_design`/`covariates` are positional slots 7/8) in favor of post-fit `results.aggregate(type)` on the unified vocabulary + `'dose'` as this estimator's documented extra level. (a) **MIXED view/recompute architecture** (unique among the aggregate-postfit adopters): the dose-response curves and the overall binarized ATT (ATT^{loc} under PT; equals ATT^{glob} under SPT) plus ACRT^{glob} are ALWAYS computed by `fit()` (`aggregate="dose"` was a fit-time no-op), so `aggregate('simple')` (2 rows, targets att/acrt - the dual-estimand case the `AggregationResult.target` column exists for; `n` = the DISJOINT treated+control unit total, `n_kind='units'`) and `aggregate('dose')` (2N target-discriminated rows; labels = the dose grid twice; no count/mass per row) are pure VIEWS over stored public fields, PERMITTED on bootstrap fits (the library-wide per-level relay rule, since M-027 converged CS/EDiD/Imputation/TwoStage onto it) - they relay stored inference verbatim, including the FINITE `safe_inference` t-stat fit stores beside the percentile p/CI on bootstrapped overall rows and the `DoseResponseCurve.to_dataframe`-exact NaN-t derivation on dose rows; only the df column is uniformly NaN under bootstrap. (b) **`aggregate('event_study')` recomputes** the binarized event study from a pruned per-cell IF payload retained on the fit-built kit: per-(g,t) treated/control positional indices, `delta_y_treated`, `ee_control`, masses and the covariate-path `if_att_glob` (O(n_treated+n_control) per cell), unit-level arrays, and - on survey fits - the PANEL-LEVEL `ResolvedSurveyDesign` ref (the recompute performs the unit collapse itself, keeping the moved body verbatim; on replicate designs the (n_obs x R) replicate matrix rides along - the documented memory cost). The K-dimensional spline machinery (bread, `ee_treated`, `Psi_eval`, `dPsi_*`) is NOT retained; no panel data columns and no raw unit identifiers are retained. Replicate-weight designs ARE supported post-fit (IF-based `compute_replicate_if_variance` - no refit replay). Bootstrap fits carry a SCALARS-ONLY kit; since 4.0 (PR 3c) they ALWAYS materialize the binarized event study at fit and `aggregate('event_study')` RELAYS it (see the bootstrapped-fit relay Note below) - a genuine seeded post-fit bootstrap replay is the TODO.md row. (c) **Fit-faithful quirk:** when no post-treatment (g,t) cells exist, event-study rows keep NaN inference on BOTH routes (the fit-time surface never fills them). (d) **df provenance:** the stored `dose_response_att.df_survey` channel (the value every fit-time `safe_inference` received) drives the views' df column (finite-and-positive else NaN - the replicate-undefined 0 sentinel reports NaN in the column but feeds the t/p derivation raw); the event-study container's PER-ROW df column now threads the same survey df via the results-level `event_study_df` channel (M-092 completion; finite on analytical survey fits, None on non-survey/bootstrapped/no-surface fits and for the 0 sentinel), beside the scalar `df_survey` channel (the two-channel contract - see the CallawaySantAnna post-fit Note). (e) **Rendering:** this is the FIRST heterogeneous-`target` `AggregationResult`; `summary()`/`to_dataframe()` gained the target column / first-appearance target-block ordering amendment (normative rule in `docs/v4-design.md` section 6; uniform-target producers byte-stable). (f) **Consumer admission:** `compute_honest_did`/`compute_pretrends_power` reject ContinuousDiD containers BY DESIGN - no joint event-study covariance exists (per-bin IF SEs only) and the binarized bins carry no reference-period normalization at all (see M-093). Warning stacklevels in the moved bodies remain tuned for the fit-time frame depth, so post-fit-route warnings attribute to a library frame (the shipped EfficientDiD convention). (g) **Report-layer consumption:** `DiagnosticReport` now derives this container internally on plain fits (raw `event_study_effects` absent) for its `heterogeneity` check — the only ES-gated check in ContinuousDiD's applicability; captured recompute warnings are re-published on the section; bootstrapped fits are read from the fit-materialized raw field (raw-field precedence), and derivation failures (kit-less pickles, a fail-soft-cleared bootstrapped surface) surface as explicit skip context. -- **Note (bootstrapped-fit relay, 4.0):** (i) RELAY: a fit with `n_bootstrap > 0` ALWAYS computes the binarized event study at fit (the only route - the kit stays SCALARS-ONLY there, so no recompute is ever possible on a bootstrapped fit and no new kit key is added: a 3.9-3.13 pickle must not `KeyError`), stores it in `event_study_effects` (percentile se/p/CI, `event_study_df=None`), and `aggregate('event_study')` RELAYS it (alpha/anticipation/`base_period`/survey metadata from the kit snapshot; per-row `df` column NaN; `df_survey` scalar from the carrier's survey metadata; `vcov`/`vcov_index` None; no reference row). The gate stays the kit scalar `n_bootstrap > 0` (not the mutable public field); `balance_e` applies to no level here (the mixin rejects it). Analytic fits are unchanged (kit recompute; legacy field None) - the dead analytic fit-time inference arm was deleted. (ii) FAIL-SOFT materialization + storage rule: the surface is built inside one guard - any exception leaves it None and `_run_bootstrap` runs for the overall/dose targets only (the pre-4.0 plain-fit path); a `_run_bootstrap` attempt that raises (anything but the replicate-design `NotImplementedError`/`ValueError`, which are re-raised - the check fires inside `_run_bootstrap` after the ES is computed) WITH the surface present clears it and RETRIES ONCE overall-only (`np.random.default_rng(self.seed)` is rebuilt on entry, so a seeded retry reproduces attempt 1's weight draws and the overall/dose quintets equal a plain fit's); a retry failure propagates out of `fit()` exactly as before 4.0, so a RETURNED bootstrapped fit always carries bootstrap inference. STORAGE RULE: the surface is stored IFF `_run_bootstrap` ran (post-treatment cells exist); a bootstrapped fit with no post cells clears it and `aggregate('event_study')` raises the shared absent-surface `ValueError` (before 4.0 that fit raised `NotImplementedError` from the bootstrap gate; its overall ATT is already NaN with a `UserWarning`). The fail-soft `UserWarning` fires only when the fit carries bootstrap inference and the surface was dropped: 'ContinuousDiD: the event-study/group surfaces could not be materialized on this bootstrapped fit ({e}); the bootstrap covered the overall ATT only, so aggregate('event_study') / aggregate('group') are unavailable here - re-fit with n_bootstrap=0 for the analytical post-fit recompute.'; the relay then raises 'This bootstrapped ContinuousDiDResults carries no stored event_study surface: either it was fitted before 4.0 (a bootstrapped plain fit built none) or the surface could not be materialized at fit (see the fit-time warning). Re-fit with diff-diff >= 4.0 for the relayed surface, or with n_bootstrap=0 for the analytical post-fit recompute.' Net invariant: a bootstrapped plain fit's overall/dose quintets and success/abort outcome are unchanged by the 4.0 removal. (iii) The ES aggregator's `UserWarning`s and the per-target bootstrap-stats `RuntimeWarning`s ('Dropping n/N non-finite bootstrap samples in ... event study (h=...)', 'Too few valid bootstrap samples', 'Bootstrap SE is non-finite or zero') now fire on bootstrapped plain fits that never emitted them. (iv) Pre-4.0 pickles: a 3.9-3.13 bootstrapped pickle fitted with `fit(aggregate='eventstudy')` relays its stored surface; one fitted plain stores none and the relay raises the refit `ValueError`. (v) The relay reads the MUTABLE public `event_study_effects` field (a view by definition - the StackedDiD precedent); only alpha/anticipation/base_period/survey metadata come from kit snapshots, so the recompute route's 'kit snapshots only' invariant does not hold for the relayed dict. (vi) Downstream: `summary()` prints the event-study table and `to_dataframe(level='event_study')` returns a frame on bootstrapped plain fits; `DiagnosticReport`/`BusinessReport` run `heterogeneity` (the only ES-gated check here; no sensitivity) on the relayed surface instead of skipping with `NotImplementedError`; `plot_event_study(results)` plots it natively. +- **Note (bootstrapped-fit relay, 4.0):** (i) RELAY: a fit with `n_bootstrap > 0` ALWAYS computes the binarized event study at fit (the only route - the kit stays SCALARS-ONLY there, so no recompute is ever possible on a bootstrapped fit and no new kit key is added: a 3.9-3.13 pickle must not `KeyError`), stores it in `event_study_effects` (percentile se/p/CI, `event_study_df=None`), and `aggregate('event_study')` RELAYS it (alpha/anticipation/`base_period`/survey metadata from the kit snapshot; per-row `df` column NaN; `df_survey` scalar from the carrier's survey metadata; `vcov`/`vcov_index` None; no reference row). The gate stays the kit scalar `n_bootstrap > 0` (not the mutable public field); `balance_e` applies to no level here (the mixin rejects it). Analytic fits are unchanged (kit recompute; legacy field None) - the dead analytic fit-time inference arm was deleted. (ii) FAIL-SOFT materialization + storage rule: the surface is built inside one guard - any exception leaves it None and `_run_bootstrap` runs for the overall/dose targets only (the pre-4.0 plain-fit path); a `_run_bootstrap` attempt that raises (anything but the replicate-design `NotImplementedError`/`ValueError`, which are re-raised - the check fires inside `_run_bootstrap` after the ES is computed) WITH the surface present clears it and RETRIES ONCE overall-only (`np.random.default_rng(self.seed)` is rebuilt on entry, so a seeded retry reproduces attempt 1's weight draws and the overall/dose quintets equal a plain fit's); a retry failure propagates out of `fit()` exactly as before 4.0, so a RETURNED bootstrapped fit always carries bootstrap inference. STORAGE RULE: the surface is stored IFF `_run_bootstrap` ran (post-treatment cells exist); a bootstrapped fit with no post cells clears it and `aggregate('event_study')` raises the shared absent-surface `ValueError` (before 4.0 that fit raised `NotImplementedError` from the bootstrap gate; its overall ATT is already NaN with a `UserWarning`). The fail-soft `UserWarning` fires only when the fit carries bootstrap inference and the surface was dropped: 'ContinuousDiD: the event-study/group surfaces could not be materialized on this bootstrapped fit ({e}); the bootstrap covered the overall ATT only, so aggregate('event_study') / aggregate('group') are unavailable here - re-fit with n_bootstrap=0 for the analytical post-fit recompute.'; the relay then raises 'This bootstrapped ContinuousDiDResults carries no stored event_study surface: either it was fitted before 4.0 (a bootstrapped plain fit built none) or the surface could not be materialized at fit (see the fit-time warning). Re-fit with diff-diff >= 4.0 for the relayed surface, or with n_bootstrap=0 for the analytical post-fit recompute.' Net invariant: a bootstrapped plain fit's overall/dose quintets and success/abort outcome are unchanged by the 4.0 removal. (iii) The ES aggregator's `UserWarning`s and the per-target bootstrap-stats `RuntimeWarning`s ('Dropping n/N non-finite bootstrap samples in ... event study (h=...)', 'Too few valid bootstrap samples', 'Bootstrap SE is non-finite or zero') now fire on bootstrapped plain fits that never emitted them. (iv) Pre-4.0 pickles: a 3.9-3.13 bootstrapped pickle fitted with `fit(aggregate='eventstudy')` relays its stored surface; one fitted plain stores none and the relay raises the refit `ValueError`. (v) The relay reads the MUTABLE public `event_study_effects` field (a view by definition - the StackedDiD precedent); only alpha/anticipation/base_period/survey metadata come from kit snapshots, so the recompute route's 'kit snapshots only' invariant does not hold for the relayed dict. (vi) Downstream: `summary()` prints the event-study table and `to_dataframe(level='event_study')` returns the unified `EVENT_STUDY_SCHEMA` frame on EVERY fit ([M-093] item 8b: bootstrapped fits relay the stored percentile surface, analytical fits recompute from the kit); `DiagnosticReport`/`BusinessReport` run `heterogeneity` (the only ES-gated check here; no sensitivity) on the relayed surface instead of skipping with `NotImplementedError`; `plot_event_study(results)` plots it natively. 9. **Note:** `survey_metadata` is recomputed from the UNIT-level resolved design on EVERY inference arm - analytic, bootstrap, and the degenerate no-post-cells arm - matching the CallawaySantAnna/EfficientDiD convention (3.12; previously the bootstrap arm published the obs-level metadata from `_resolve_survey_for_fit`, so `sum_weights`/`effective_n`/`n_psu` - and, on implicit-PSU designs, `df_survey` - diverged from the analytic arm by panel length). Provenance-only: estimates and inference are unaffected (the bootstrap arm's inference remains percentile with no df). ### Implementation Checklist @@ -1960,7 +1961,7 @@ where `q_{g,e} = pi_g / sum_{g' in G_{trt,e}} pi_{g'}`. - **Note:** `vcov_type` is permanently narrow to `{"hc1"}` per the Chen-Sant'Anna-Xie (2025) EIF-based variance achieving the semiparametric efficiency bound. Analytical-sandwich families `{classical, hc2, hc2_bm}` are rejected at `__init__` — the per-unit EIF aggregation has no equivalent single design matrix on which hat-matrix leverage or Bell-McCaffrey Satterthwaite DOF can be defined. `cluster=` invokes Liang-Zeger CR1 on cluster-aggregated EIF (`_compute_se_from_eif` with `cluster_indices`); `survey_design=` invokes TSL on the combined IF (`_compute_survey_eif_se`); both live in `diff_diff/efficient_did_aggregation.py` since the M-023 post-fit aggregate() extraction. `vcov_type='conley'` deferred to the EfficientDiD Conley follow-up row in DEFERRED.md. - **Note:** Default `cluster=None` (no survey design) renders summary label "HC1 heteroskedasticity-robust" because the per-unit EIF SE `sqrt(mean(EIF²)/n)` is methodologically HC1-style (no Liang-Zeger G/(G-1) finite-sample correction). M-080 position (category b; shipped item 7b, 2026-10-29): `cluster=False` is an accepted explicit no-op (bit-identical to the default) on `fit()` and on the `hausman_pretest` classmethod's own `cluster` argument; the shared `str | None | False` contract ([M-155]) applies. `EfficientDiDResults.cluster_name` and `n_clusters` stay None under unclustered fits. This diverges from `ImputationDiD` which auto-clusters at unit per Borusyak-Jaravel-Spiess (2024) Theorem 3 — there the default summary renders the CR1 unit-clustered label. - **Note:** `set_params(vcov_type=bad)` raises immediately on EVERY estimator: since the shared `BaseEstimator` mixin (`diff_diff/_base.py`, v4 2(c)-i), `set_params` validates transactionally by constructor probe re-init, so it enforces exactly `__init__`'s validation, eagerly, library-wide. The former split — EfficientDiD eager vs `ImputationDiD`/`TripleDifference`/`CallawaySantAnna` (and six more: SunAbraham, StackedDiD, the since-removed StaggeredTripleDifference, SpilloverDiD, TROP, PreTrendsPower) accepting constructor-rejected values until `fit()` — is retired; the fit-time re-validation layers remain as a second check against DIRECT attribute mutation (`est.vcov_type = ...`), which no setter can see. -- **Note (post-fit aggregate() - rows M-023/M-120):** `fit(aggregate=, balance_e=)` was deprecated in 3.9 and REMOVED at 4.0 (PR 3b of the cut: `fit()` computes no event-study/group surface, the legacy `event_study_effects`/`group_effects`/`event_study_df` fields are None after EVERY fit, `to_dataframe(level='event_study'|'group')` raises on fresh fits until the M-093 schema rewire, and `survey_design`/`store_eif` are positional slots 7/8) in favor of post-fit `EfficientDiDResults.aggregate(type, balance_e=)` - a LAZY RECOMPUTING KIT (the CallawaySantAnna class, not a StackedDiD/dCDH view relay): `fit()` computes nothing extra, the results object retains an `AggregationKit`, and `aggregate('event_study'/'group', balance_e=)` re-runs the extracted `_EfficientAggregationMixin` aggregators on a throwaway host while `aggregate('simple')` relays the stored overall row bit-exact. (a) RETAINED BUFFERS (memory contract; phrased as maxima - optional design fields stay None when unsupplied): the per-(g,t) EIF dict, O(n_units x n_gt), the dominant payload - retained on EVERY fit regardless of `store_eif`, which since 3.9 governs only the public `influence_functions` field; `unit_cohorts` (cohort labels), `unit_level_weights`, factorized cluster codes - O(n_units) each; on ordinary (TSL) survey fits the unit-level `ResolvedSurveyDesign` adds `weights` plus, where supplied, `strata`/`psu`/`fpc` (factorized int codes / float values, never raw labels) - up to four O(n_units) arrays; on replicate designs it adds the O(n_units x n_replicates) replicate matrix plus, where supplied, `replicate_strata`/`replicate_rscales` (O(n_replicates)); per-row dict SNAPSHOTS of `group_time_effects` plus copies of the `groups`/`time_periods` lists and the scalar `pt_assumption`/`n_treated+n_control` provenance (aggregate() recomputes exclusively from these private snapshots, never from the mutable public result fields - a user edit of the public rows cannot mix altered point estimates with the retained EIF variance); scalars `n_units`, `cohort_fractions`, and the POST-OVERALL `df_survey` snapshot (captured after the overall inference and before the ES/group gates: the group pass can degenerate the working df to None on replicate designs with `n_valid <= 1`, and every fit-time aggregation seeds from the post-overall value, so recompute replays the exact seed). The data-minimization guarantee is scoped to unit identifiers - no unit-label container is retained. (b) `balance_e` uses the ANCHOR-HORIZON rule (keep cohorts with a finite effect at the anchor horizon, then retain all their horizons) - the same rule SHAPE CallawaySantAnna uses, with one keying-granularity difference (EDiD anchors on the `int(t - g)` bucket, CS on raw `t - g` - identical on integer-period panels; see the truncation Note below), divergent from ImputationDiD/TwoStageDiD's balanced-window rule; an anchor no cohort reaches warns and yields a legal zero-row container. (c) BOOTSTRAP fits: 'simple' (and 'total') RELAY the stored overall row verbatim (percentile se/p/CI beside the finite safe_inference t) with a NaN df column and never re-warn, while the RECOMPUTE levels (ES/group, balance_e included) REPLAY the fit-time multiplier bootstrap from the kit's `BootstrapReplaySpec` (the fit-captured RNG state + run params BY VALUE - seed=None fits replay; post-fit `set_params`/attribute mutation cannot alter it; pickles carry it): se/CI/t match a fit-time aggregation to BLAS reassociation (~1 ULP, `assert_allclose` - never bit-identity), the percentile p-value is a count statistic (compared at 2/n_bootstrap), and the replay publishes no analytical provenance (percentile overrides via the shared `bootstrap_utils` appliers; group rows' `df_used` cleared exactly as at fit time; EDiD has no sup-t cband). The spec stamps the weight backend at capture ('rust'/'numpy', or 'portable' for backend-independent branches - stratified survey generation, census-FPC zero weights, the single-PSU degenerate early return); a replay under a different backend fails closed naming both, and legacy pickles without the spec fail closed with a refit message. Replays RE-EMIT the fit-time bootstrap warnings for the replayed configuration (low n_bootstrap, single-PSU, empty balance_e anchor, non-finite-draw RuntimeWarnings); the relay levels stay silent. Per-call cost O(n_bootstrap x n_units x n_gt), no memoization. The M-027 rationale - no level publishes analytical-provenance fields beside percentile inference - is honored by the percentile overrides and NaN df channels. (d) CONTAINER ADMISSION into `compute_honest_did`/`compute_pretrends_power` is REJECTED BY DESIGN (both terminal TypeErrors state it): the primary ground is the absent joint event-study covariance (container `vcov=None`; the PER-ROW df column now threads the post-overall `df_survey` snapshot via the results-level `event_study_df` channel on analytical survey fits - M-092 completion; None on non-survey/bootstrapped/no-surface fits and for the replicate-undefined 0 sentinel - beside the scalar `df_survey` channel; see the CS post-fit Note's two-channel contract). Reference semantics are regime-dependent: under `pt_assumption="all"` there is NO reference row (universal first-period baseline; e=-1 is a genuine estimate); under `"post"` the per-cohort baseline cell is materialized as a mechanical zero anchor at `e = -1 - anticipation` whenever it is not the panel's first period, and the MEMBERSHIP-GATED `reference_period` property (the SunAbraham rule - never synthesized when the anchor cell was not estimated) marks it `is_reference` in the container and corrects `plot_event_study`'s inferred reference (previously the `-1` fallback) on PT-Post `anticipation>0` fits. (e) 'simple' relay conventions: `target="att"`, `n = n_treated_units + n_control_units` with `n_kind="units"` (DISJOINT by construction - `last_cohort` trimming reassigns before the counts, so a true total exists, unlike StackedDiD's overlapping sets), `df` = the post-overall snapshot (provenance-exact where `survey_metadata.df_survey` can diverge in the degenerate replicate state); 'group' relay: `n_kind="cells"`, `weight=None` (equal within-cohort weights, no cross-cohort mass), per-row `df_used` array captured at each row's `safe_inference` call (exact by construction; a stated divergence from CS's conservative-min scalar broadcast); 'event_study' rides the shared `_from_relative_dict` builder via a carrier whose `survey_metadata` copy carries the snapshot `df_survey` and whose `event_study_df` threads the same snapshot into the per-row df column (cleared on bootstrap replays and for the 0 sentinel). (f) `aggregate('total')` (3.10): the estimator-owned total incremental outcome - an exact relay `C x overall` CONDITIONAL on the realized aggregation mass, with `C = sum(n_treated)` over the kept post-anticipation FINITE-effect cells of the kit's deep-copied `group_time_effects` snapshot (an exact integer sum; NEVER the routinely-non-integral `n_units x sum(cohort_fractions)` float product). Unweighted fits only: ANY declared `survey_design=` fails closed via the kit's `unit_level_weights`/`resolved_survey_unit` markers (weight-type-agnostic - unweighted psu-only and analytic fweight designs, whose resolved weights stay RAW, gate identically; EDiD never synthesizes an internal design, so the markers are exact declaration provenance and post-fit mutation of the public `survey_metadata` cannot bypass them). No keepers -> NaN mass -> all-NaN row (no re-warn); with finite `C` the relay is verbatim (inherited NaNs pass through; att/n never blanked). Bootstrap fits relay with a NaN df column (M-027); df carrier = the post-overall `df_survey` snapshot; container: single `target='total'` row, `n_kind='obs'`, the per-level n-semantics note in the CS total Note applies. MMM: the container is admitted with NO scale (see the MMM section); the survey/RC totals remainder and its att*dC variance term are the DEFERRED row. +- **Note (post-fit aggregate() - rows M-023/M-120):** `fit(aggregate=, balance_e=)` was deprecated in 3.9 and REMOVED at 4.0 (PR 3b of the cut: `fit()` computes no event-study/group surface, the legacy `event_study_effects`/`group_effects`/`event_study_df` fields are None after EVERY fit, `to_dataframe(level='event_study')` returns `aggregate('event_study').to_dataframe()` - the unified `EVENT_STUDY_SCHEMA` frame - since the 4.0 cut ([M-093] item 8b; bootstrapped fits replay the fit-time bootstrap on every call), while `to_dataframe(level='group')` still raises on fresh fits with the post-fit pointer, and `survey_design`/`store_eif` are positional slots 7/8) in favor of post-fit `EfficientDiDResults.aggregate(type, balance_e=)` - a LAZY RECOMPUTING KIT (the CallawaySantAnna class, not a StackedDiD/dCDH view relay): `fit()` computes nothing extra, the results object retains an `AggregationKit`, and `aggregate('event_study'/'group', balance_e=)` re-runs the extracted `_EfficientAggregationMixin` aggregators on a throwaway host while `aggregate('simple')` relays the stored overall row bit-exact. (a) RETAINED BUFFERS (memory contract; phrased as maxima - optional design fields stay None when unsupplied): the per-(g,t) EIF dict, O(n_units x n_gt), the dominant payload - retained on EVERY fit regardless of `store_eif`, which since 3.9 governs only the public `influence_functions` field; `unit_cohorts` (cohort labels), `unit_level_weights`, factorized cluster codes - O(n_units) each; on ordinary (TSL) survey fits the unit-level `ResolvedSurveyDesign` adds `weights` plus, where supplied, `strata`/`psu`/`fpc` (factorized int codes / float values, never raw labels) - up to four O(n_units) arrays; on replicate designs it adds the O(n_units x n_replicates) replicate matrix plus, where supplied, `replicate_strata`/`replicate_rscales` (O(n_replicates)); per-row dict SNAPSHOTS of `group_time_effects` plus copies of the `groups`/`time_periods` lists and the scalar `pt_assumption`/`n_treated+n_control` provenance (aggregate() recomputes exclusively from these private snapshots, never from the mutable public result fields - a user edit of the public rows cannot mix altered point estimates with the retained EIF variance); scalars `n_units`, `cohort_fractions`, and the POST-OVERALL `df_survey` snapshot (captured after the overall inference and before the ES/group gates: the group pass can degenerate the working df to None on replicate designs with `n_valid <= 1`, and every fit-time aggregation seeds from the post-overall value, so recompute replays the exact seed). The data-minimization guarantee is scoped to unit identifiers - no unit-label container is retained. (b) `balance_e` uses the ANCHOR-HORIZON rule (keep cohorts with a finite effect at the anchor horizon, then retain all their horizons) - the same rule SHAPE CallawaySantAnna uses, with one keying-granularity difference (EDiD anchors on the `int(t - g)` bucket, CS on raw `t - g` - identical on integer-period panels; see the truncation Note below), divergent from ImputationDiD/TwoStageDiD's balanced-window rule; an anchor no cohort reaches warns and yields a legal zero-row container. (c) BOOTSTRAP fits: 'simple' (and 'total') RELAY the stored overall row verbatim (percentile se/p/CI beside the finite safe_inference t) with a NaN df column and never re-warn, while the RECOMPUTE levels (ES/group, balance_e included) REPLAY the fit-time multiplier bootstrap from the kit's `BootstrapReplaySpec` (the fit-captured RNG state + run params BY VALUE - seed=None fits replay; post-fit `set_params`/attribute mutation cannot alter it; pickles carry it): se/CI/t match a fit-time aggregation to BLAS reassociation (~1 ULP, `assert_allclose` - never bit-identity), the percentile p-value is a count statistic (compared at 2/n_bootstrap), and the replay publishes no analytical provenance (percentile overrides via the shared `bootstrap_utils` appliers; group rows' `df_used` cleared exactly as at fit time; EDiD has no sup-t cband). The spec stamps the weight backend at capture ('rust'/'numpy', or 'portable' for backend-independent branches - stratified survey generation, census-FPC zero weights, the single-PSU degenerate early return); a replay under a different backend fails closed naming both, and legacy pickles without the spec fail closed with a refit message. Replays RE-EMIT the fit-time bootstrap warnings for the replayed configuration (low n_bootstrap, single-PSU, empty balance_e anchor, non-finite-draw RuntimeWarnings); the relay levels stay silent. Per-call cost O(n_bootstrap x n_units x n_gt), no memoization. The M-027 rationale - no level publishes analytical-provenance fields beside percentile inference - is honored by the percentile overrides and NaN df channels. (d) CONTAINER ADMISSION into `compute_honest_did`/`compute_pretrends_power` is REJECTED BY DESIGN (both terminal TypeErrors state it): the primary ground is the absent joint event-study covariance (container `vcov=None`; the PER-ROW df column now threads the post-overall `df_survey` snapshot via the results-level `event_study_df` channel on analytical survey fits - M-092 completion; None on non-survey/bootstrapped/no-surface fits and for the replicate-undefined 0 sentinel - beside the scalar `df_survey` channel; see the CS post-fit Note's two-channel contract). Reference semantics are regime-dependent: under `pt_assumption="all"` there is NO reference row (universal first-period baseline; e=-1 is a genuine estimate); under `"post"` the per-cohort baseline cell is materialized as a mechanical zero anchor at `e = -1 - anticipation` whenever it is not the panel's first period, and the MEMBERSHIP-GATED `reference_period` property (the SunAbraham rule - never synthesized when the anchor cell was not estimated) marks it `is_reference` in the container and corrects `plot_event_study`'s inferred reference (previously the `-1` fallback) on PT-Post `anticipation>0` fits. (e) 'simple' relay conventions: `target="att"`, `n = n_treated_units + n_control_units` with `n_kind="units"` (DISJOINT by construction - `last_cohort` trimming reassigns before the counts, so a true total exists, unlike StackedDiD's overlapping sets), `df` = the post-overall snapshot (provenance-exact where `survey_metadata.df_survey` can diverge in the degenerate replicate state); 'group' relay: `n_kind="cells"`, `weight=None` (equal within-cohort weights, no cross-cohort mass), per-row `df_used` array captured at each row's `safe_inference` call (exact by construction; a stated divergence from CS's conservative-min scalar broadcast); 'event_study' rides the shared `_from_relative_dict` builder via a carrier whose `survey_metadata` copy carries the snapshot `df_survey` and whose `event_study_df` threads the same snapshot into the per-row df column (cleared on bootstrap replays and for the 0 sentinel). (f) `aggregate('total')` (3.10): the estimator-owned total incremental outcome - an exact relay `C x overall` CONDITIONAL on the realized aggregation mass, with `C = sum(n_treated)` over the kept post-anticipation FINITE-effect cells of the kit's deep-copied `group_time_effects` snapshot (an exact integer sum; NEVER the routinely-non-integral `n_units x sum(cohort_fractions)` float product). Unweighted fits only: ANY declared `survey_design=` fails closed via the kit's `unit_level_weights`/`resolved_survey_unit` markers (weight-type-agnostic - unweighted psu-only and analytic fweight designs, whose resolved weights stay RAW, gate identically; EDiD never synthesizes an internal design, so the markers are exact declaration provenance and post-fit mutation of the public `survey_metadata` cannot bypass them). No keepers -> NaN mass -> all-NaN row (no re-warn); with finite `C` the relay is verbatim (inherited NaNs pass through; att/n never blanked). Bootstrap fits relay with a NaN df column (M-027); df carrier = the post-overall `df_survey` snapshot; container: single `target='total'` row, `n_kind='obs'`, the per-level n-semantics note in the CS total Note applies. MMM: the container is admitted with NO scale (see the MMM section); the survey/RC totals remainder and its att*dC variance term are the DEFERRED row. - **Note (fractional-period truncation bucketing):** the event-study equation defines `ES(e)` at an exact relative time, but the implementation buckets horizons by `int(t - g)` - truncation TOWARD ZERO - on the analytical aggregator, the bootstrap ES prep (all three keying sites, the `balance_e` anchor filter included; aligned 2026-08), and `hausman_pretest`'s internal aggregation. On integer-period panels `int()` is the identity and nothing below applies. On fractional-period panels (accepted by `fit()` without an integrality check): (i) bucket 0 is DOUBLE-WIDTH, spanning `(-1, 1)`, so the pre-treatment horizon e=-0.5 pools into the post-treatment e=0 bucket; e=1.0/1.5 pool into bucket 1 and e=-1.5/-1.0 into bucket -1; (ii) under `pt_assumption="post"` the bucket at `-1 - anticipation` pools genuine estimated fractional pre-treatment horizons (e.g. raw e=-1.5) with the mechanical zero anchor, and the container's reference normalization then publishes the WHOLE pooled row as `att=0.0` with NaN inference (`is_reference=True`) - genuine fractional-horizon estimates are subsumed into the reference row (the fit-time `event_study_effects` dict keeps the pooled effect + percentile inference unrewritten; the rewrite is container-only); (iii) WEIGHTING is CELL-MASS within the bucket - the aggregator appends one `(effect, pi_g)` term per CELL and normalizes, so a cohort contributing k cells to a bucket carries k*pi_g mass, whereas the exact-relative-time equation has one term per cohort - while `n_groups` counts DISTINCT cohorts in the bucket (identity with the cell count on integer panels). Every aggregation that truncation-buckets a fractional horizon emits a UserWarning naming this Note. - **Note:** `anticipation` is validated at construction (non-negative integer; `bool` rejected) via the shared `utils.validate_anticipation`, and re-checked on the fit path (direct-mutation defense) — see the family-wide adoption note (ledger row [M-144]) in the TripleDifference staggered-mode section. @@ -2083,6 +2084,7 @@ where weights ŵ_{g,e} = n_{g,e} / Σ_g n_{g,e} (sample share of cohort g at eve deliberately narrower than the `event_study_vcov` clear, which also fires under replicate refits whose rows DID use a genuine df. The scalar `inference_df` (added 3.9) mirrors the same rule for the overall ATT. +- **Note (to_dataframe schema, 4.0, [M-093] item 8b):** `to_dataframe(level='event_study')` (the default level) returns `build_event_study_surface(self).to_dataframe()` - the unified `EVENT_STUDY_SCHEMA` frame: the omitted reference period (`reference_period`, `e = -1 - anticipation`) appears as a synthesized `is_reference` row with `att = 0` and NaN inference/`n`; estimated rows carry `n` = the contributing-cohort count (`n_kind='groups'`). The 3.x bespoke `relative_period`/`effect` frame is gone. `'cohort'` is unchanged. - **HC1 finite-sample correction (K_reference convergence, 3.9):** SA's clustered CR1 factor counts the absorbed unit + time FE via the `cluster_k_adjustment` increment (`K_reference = k_dm + 1 + rank(non-nested @@ -2335,8 +2337,8 @@ where `W_it(h) = 1[K_it = h]` are lead indicators, estimated on `Omega_0` only. - [x] Event study and group aggregation - **Note (post-fit aggregate() - rows M-021/M-118):** `fit(aggregate=, balance_e=)` was deprecated in 3.9 and REMOVED at 4.0 (PR 3c of the cut; `survey_design` is positional slot 7; the `imputation_did()` wrapper that used to forward the shared sentinel was removed per M-070 - PR 2) in favor of post-fit `ImputationDiDResults.aggregate(type, balance_e=)` - a PANEL-BACKED lazy recompute kit (not an EIF-payload kit: ES/group aggregation is a target-specific Theorem-3 recompute - each `balance_e` re-masks which treated observations enter every horizon and re-solves the untreated projection - so no compact influence payload can replace the frame). (a) RETAINED BUFFERS (memory contract): the kit's bookkeeping holds REFERENCES to the SAME per-fit objects `_fit_data` already retains for `pretrend_test()` - the working panel copy (all user columns plus `_tau_hat`/`_rel_time`/`_never_treated`), the Omega masks, `unit_fe`/`time_fe`/`grand_mean`/`delta_hat`/`kept_cov_mask`, the resolved survey design, and `survey_weights` - ZERO marginal memory, and pickles are unchanged via memoization (`_estimator_ref` already ships these objects); plus value SNAPSHOTS for isolation (a `treatment_groups` copy, config scalars, a `dataclasses.replace` copy of `survey_metadata`, `att`, `n_treated_obs`) and TWO df-provenance scalars (`survey_df_seed`, what the analytical aggregators received; `survey_df_final`, what the stored overall inference received). Each `aggregate()` call runs on a fresh throwaway host with a call-local projection cache (the fit-local factorizations are unpicklable and never retained). (b) `balance_e` uses the BALANCED-WINDOW rule: a cohort is retained iff its observed relative-time set - checked against the FULL panel via `_build_cohort_rel_times()` - covers the contiguous window `[-balance_e, max_h]`; the SAME rule TwoStageDiD uses, divergent from CS/EfficientDiD's anchor-horizon rule. A window no cohort satisfies warns and yields the reference-marker-only dict (a legal near-empty container). (c) BOOTSTRAP fits: 'simple' RELAYS the stored overall quintet verbatim (finite safe_inference t included) with a NaN df column; since 4.0 (PR 3c) the ES/group levels RELAY the percentile surface materialized at fit (see the bootstrapped-fit relay Note below; `balance_e` fails closed there - the balanced-window replay via the retained psi machinery is the TODO row); the prior uniform fail-closed rule was superseded 2026-08-05 with the M-027 per-level convergence. (d) CONTAINER ADMISSION into `compute_honest_did`/`compute_pretrends_power` is REJECTED BY DESIGN (both terminal TypeErrors state it): the surface carries no joint event-study covariance - per-horizon conservative SEs only (container `vcov=None`; the PER-ROW df column now threads the FINAL survey df via the results-level `event_study_df` channel - M-092 completion; `survey_df_final` at fit, the level-matched replay value on replicate replays; leads included on survey fits; None on non-survey/bootstrapped/no-surface fits and for the 0 sentinel - beside the scalar `df_survey` channel; see the CS post-fit Note's two-channel contract). (e) RELAY CONVENTIONS: 'simple' relays the stored overall quintet bit-exact with `n = n_treated_obs`, `n_kind="obs"` (the treated/control UNIT sets overlap - a treated unit with pre-periods counts in both - so the CS/EDiD disjoint-units convention cannot apply; |Omega_1| is the population the ATT averages over, of which only finite-tau-hat observations enter the average - `n` reports the raw count, so on partially unidentified fits `n` exceeds the averaged support) and `df = survey_df_final`; 'group' rows carry per-row `df_used` captured at each row's `safe_inference` (the replicate override rewrites it, the bootstrap override clears it, the all-NaN cohort branch writes no key - consumers read via `.get`); 'event_study' rides the shared `_from_relative_dict` builder via a carrier whose metadata is a copy-on-use of the KIT's fit-final metadata copy. REPLICATE-WEIGHT fits replay the extracted `_replicate_override_aggregates` with a LEVEL-MATCHED stack: `compute_replicate_refit_variance` validates replicates JOINTLY (all-finite rows), so `aggregate(L)` reproduces `fit(aggregate=L)` exactly, a `fit(aggregate='all')` surface is NOT the equivalence target when a replicate NaNs on exactly one family's targets, and - the documented migration delta - moving a replicate fit from `fit(aggregate=)` to plain fit + post-fit `aggregate()` can change the public OVERALL row's se/CI/df on such degenerate designs (each surface self-consistent; pinned in the contract tests). `pretrends=True` + replicate: post-fit `aggregate('event_study')` raises NotImplementedError (per-replicate lead refits unimplemented; the unreachable fit-time twin of this gate was deleted at 4.0); 'group'/'simple' still work. Recompute re-emits the fit-time warnings (LSMR, Prop-5, empty-window) with fit-tuned stacklevels - post-fit attribution lands on a library frame, an accepted verbatim-move trade-off. `DiagnosticReport` now derives this container internally on plain fits (raw `event_study_effects` absent), so its `parallel_trends` (`pretrends=True` fits) and `heterogeneity` checks run on analytic plain fits (bootstrapped fits are read from the fit-materialized raw fields - raw-field precedence); the re-emitted recompute warnings are captured and re-published on the consuming report section (record-and-republish), and derivation failures (missing kit, the replicate gate above, a fail-soft-cleared bootstrapped surface) surface as explicit per-check skip reasons. (f) `aggregate('total')` (3.10): the estimator-owned total incremental outcome - an exact relay `C x overall` CONDITIONAL on the realized aggregation mass, with `C` the FINITE-tau complete-case support (`total_support`, a float SNAPSHOT stashed at kit build precisely because the kit's `df` is a live `_fit_data` reference - a post-fit frame edit must not move the total; pre-upgrade kits missing the stash raise the refit message). Since the overall IS the finite-support mean, `C x overall = sum(tau)` exactly - this total uses the FINITE support and thereby fixes the documented raw-|Omega_1| overcount of the MMM `scale=\"auto\"` route for total exports ('simple''s `n` stays the raw count by contract; different levels, different documented n semantics). Zero support (the all-unidentified fit, overall already NaN) -> NaN mass -> all-NaN row (no re-warn); finite `C` relays verbatim - the constant-outcome degenerate fit keeps finite att/se = 0 beside its inherited `(nan, nan)` CI, mirroring 'simple'. Unweighted fits only: declared `survey_design=` fails closed via the kit's `survey_metadata` snapshot (mutation-proof). Bootstrap fits relay with a NaN df column (M-027); df carrier = `survey_df_final`; container: single `target='total'` row, `n_kind='obs'`. MMM: admitted with NO scale (see the MMM section). -- **Note (bootstrapped-fit relay, 4.0):** (i) RELAY: a fit with `n_bootstrap > 0` ALWAYS computes the event-study AND group percentile surfaces at fit (`balance_e=None` - exactly what the 3.x `fit(aggregate='all')` computed; percentile se/p/CI, recomputed t, `df_used`/`event_study_df` cleared), stores them in `event_study_effects`/`group_effects`, and `aggregate('event_study')`/`aggregate('group')` RELAY them (alpha/anticipation/survey metadata from the kit snapshot; per-row `df` column NaN - the `pretrends=True` lead rows show NaN df too, identical to the 3.x bootstrapped surface, by design; `df_survey` scalar from the carrier's survey metadata; `vcov`/`vcov_index` None; reference rows via the `is_reference` key). `aggregate('event_study', balance_e=k)` on a bootstrapped fit FAILS CLOSED (`NotImplementedError`: the stored surface is unrestricted and the per-target draws are not retained; re-fit with `n_bootstrap=0`). Analytic fits are unchanged (kit recompute; legacy fields None). The gate is `bootstrap_results is not None`: `None` (n_bootstrap == 0, OR a bootstrapped fit that ended analytical) means the analytical kit RECOMPUTE, the pre-4.0 behaviour. (ii) FAIL-SOFT materialization + storage rule: the surfaces are built inside one guard - any exception (e.g. the `pretrends=True` + `rank_deficient_action='error'` lead regression raising `ValueError` on a lead-collinear covariate) leaves them None and the bootstrap runs overall-only (the pre-4.0 plain-fit path, which SUCCEEDS where the 3.x `fit(aggregate='event_study')` raised); a bootstrap attempt that raises (anything but `NotImplementedError`, re-raised) WITH surfaces present clears them and RETRIES ONCE overall-only - `_precompute_bootstrap_psi` + `_run_bootstrap` re-run with `event_study_effects=None, group_effects=None`; `np.random.default_rng(self.seed)` is rebuilt on entry, so a seeded retry reproduces attempt 1's weight draws and the overall quintet equals a plain fit's (an unseeded retry draws a fresh, equally valid realization). The retry keeps the pre-4.0 failure semantics: a psi-precompute failure -> the 'Bootstrap pre-computation failed ... Skipping bootstrap inference.' warning + analytical overall; a `_run_bootstrap` failure -> RE-RAISED (the fit aborts exactly as before). STORAGE RULE: the legacy fields hold a surface IFF `bootstrap_results is not None`; when the fit ends analytical (`n_valid == 0` skipped the psi precompute, total failure) they are cleared and `aggregate()` RECOMPUTES analytically - an analytical surface must never sit on a bootstrapped fit where `summary()`/`to_dataframe()`/the DiagnosticReport raw-field precedence would read it as the fit's inference. The fail-soft `UserWarning` fires only when the fit DOES carry bootstrap inference and a surface was dropped: 'ImputationDiD: the event-study/group surfaces could not be materialized on this bootstrapped fit ({e}); the bootstrap covered the overall ATT only, so aggregate('event_study') / aggregate('group') are unavailable here - re-fit with n_bootstrap=0 for the analytical post-fit recompute.'; the relay then raises the shared absent-surface `ValueError` ('This bootstrapped ImputationDiDResults carries no stored event_study surface: either it was fitted before 4.0 (a bootstrapped plain fit built none) or the surface could not be materialized at fit (see the fit-time warning). Re-fit with diff-diff >= 4.0 for the relayed surface, or with n_bootstrap=0 for the analytical post-fit recompute.'). Net invariant: a bootstrapped plain fit's overall quintet and success/abort outcome are unchanged by the 4.0 removal. (iii) Newly reachable warnings on bootstrapped plain fits: the aggregators' own `UserWarning`s (Prop-5 unidentified horizons, empty horizon after filters, LOO single-unit auxiliary groups) and the per-target bootstrap-stats `RuntimeWarning`s ('Dropping n/N non-finite bootstrap samples in ... event study (h=...)', 'Too few valid bootstrap samples', 'Bootstrap SE is non-finite or zero'). (iv) Pre-4.0 pickle limitation: a 3.9-3.13 bootstrapped pickle fitted with `fit(aggregate=..., balance_e=k)` stores a balance_e-RESTRICTED surface and records no fit-time `balance_e`, so the relay returns that surface as stored - re-fit under 4.0 for the unrestricted one; one fitted plain stores none and the relay raises the refit `ValueError`. (v) The relay reads the MUTABLE public `event_study_effects`/`group_effects` fields (a view by definition - the StackedDiD precedent); only alpha/anticipation/survey metadata come from kit snapshots, so the recompute route's 'kit snapshots only' invariant does not hold for the relayed dict. (vi) Downstream: `summary()` prints the ES/group tables and `to_dataframe(level='event_study'|'group')` returns frames on bootstrapped plain fits; `DiagnosticReport`/`BusinessReport` run `heterogeneity` (on the relayed GROUP percentile dict - `_collect_effect_scalars` reads `group_effects` first) and `parallel_trends` (`pretrends=True`) on the relayed surface instead of skipping with `NotImplementedError` (no sensitivity check applies); `plot_event_study(results)` plots them natively. Replicate-weight designs never meet the always-on surfaces (replicate + `n_bootstrap > 0` is rejected before them), so the fit-time replicate stack is always `[overall]`-only - the degenerate-design overall-row migration delta in (e) stands. -- **Note (reference-row marking, 4.0, [M-093] item 8a):** the `event_study_effects` reference row (`e = -1 - anticipation`) carries `"is_reference": True` and no `n_obs` key (3.x: the `n_obs == 0` sentinel, retired); every remaining `n_obs` is a genuine count. `summary()` keys its `[ref: h]` line on the marker; the bootstrap loop skips the marked row; 3.x pickles are migrated on load (`_deprecation.migrate_event_study_reference_sentinel` in `__setstate__`). The native `to_dataframe(level='event_study')` frame renders the reference row's `n_obs` as NaN (float column) until the 8b schema rewire. +- **Note (bootstrapped-fit relay, 4.0):** (i) RELAY: a fit with `n_bootstrap > 0` ALWAYS computes the event-study AND group percentile surfaces at fit (`balance_e=None` - exactly what the 3.x `fit(aggregate='all')` computed; percentile se/p/CI, recomputed t, `df_used`/`event_study_df` cleared), stores them in `event_study_effects`/`group_effects`, and `aggregate('event_study')`/`aggregate('group')` RELAY them (alpha/anticipation/survey metadata from the kit snapshot; per-row `df` column NaN - the `pretrends=True` lead rows show NaN df too, identical to the 3.x bootstrapped surface, by design; `df_survey` scalar from the carrier's survey metadata; `vcov`/`vcov_index` None; reference rows via the `is_reference` key). `aggregate('event_study', balance_e=k)` on a bootstrapped fit FAILS CLOSED (`NotImplementedError`: the stored surface is unrestricted and the per-target draws are not retained; re-fit with `n_bootstrap=0`). Analytic fits are unchanged (kit recompute; legacy fields None). The gate is `bootstrap_results is not None`: `None` (n_bootstrap == 0, OR a bootstrapped fit that ended analytical) means the analytical kit RECOMPUTE, the pre-4.0 behaviour. (ii) FAIL-SOFT materialization + storage rule: the surfaces are built inside one guard - any exception (e.g. the `pretrends=True` + `rank_deficient_action='error'` lead regression raising `ValueError` on a lead-collinear covariate) leaves them None and the bootstrap runs overall-only (the pre-4.0 plain-fit path, which SUCCEEDS where the 3.x `fit(aggregate='event_study')` raised); a bootstrap attempt that raises (anything but `NotImplementedError`, re-raised) WITH surfaces present clears them and RETRIES ONCE overall-only - `_precompute_bootstrap_psi` + `_run_bootstrap` re-run with `event_study_effects=None, group_effects=None`; `np.random.default_rng(self.seed)` is rebuilt on entry, so a seeded retry reproduces attempt 1's weight draws and the overall quintet equals a plain fit's (an unseeded retry draws a fresh, equally valid realization). The retry keeps the pre-4.0 failure semantics: a psi-precompute failure -> the 'Bootstrap pre-computation failed ... Skipping bootstrap inference.' warning + analytical overall; a `_run_bootstrap` failure -> RE-RAISED (the fit aborts exactly as before). STORAGE RULE: the legacy fields hold a surface IFF `bootstrap_results is not None`; when the fit ends analytical (`n_valid == 0` skipped the psi precompute, total failure) they are cleared and `aggregate()` RECOMPUTES analytically - an analytical surface must never sit on a bootstrapped fit where `summary()`/`to_dataframe()`/the DiagnosticReport raw-field precedence would read it as the fit's inference. The fail-soft `UserWarning` fires only when the fit DOES carry bootstrap inference and a surface was dropped: 'ImputationDiD: the event-study/group surfaces could not be materialized on this bootstrapped fit ({e}); the bootstrap covered the overall ATT only, so aggregate('event_study') / aggregate('group') are unavailable here - re-fit with n_bootstrap=0 for the analytical post-fit recompute.'; the relay then raises the shared absent-surface `ValueError` ('This bootstrapped ImputationDiDResults carries no stored event_study surface: either it was fitted before 4.0 (a bootstrapped plain fit built none) or the surface could not be materialized at fit (see the fit-time warning). Re-fit with diff-diff >= 4.0 for the relayed surface, or with n_bootstrap=0 for the analytical post-fit recompute.'). Net invariant: a bootstrapped plain fit's overall quintet and success/abort outcome are unchanged by the 4.0 removal. (iii) Newly reachable warnings on bootstrapped plain fits: the aggregators' own `UserWarning`s (Prop-5 unidentified horizons, empty horizon after filters, LOO single-unit auxiliary groups) and the per-target bootstrap-stats `RuntimeWarning`s ('Dropping n/N non-finite bootstrap samples in ... event study (h=...)', 'Too few valid bootstrap samples', 'Bootstrap SE is non-finite or zero'). (iv) Pre-4.0 pickle limitation: a 3.9-3.13 bootstrapped pickle fitted with `fit(aggregate=..., balance_e=k)` stores a balance_e-RESTRICTED surface and records no fit-time `balance_e`, so the relay returns that surface as stored - re-fit under 4.0 for the unrestricted one; one fitted plain stores none and the relay raises the refit `ValueError`. (v) The relay reads the MUTABLE public `event_study_effects`/`group_effects` fields (a view by definition - the StackedDiD precedent); only alpha/anticipation/survey metadata come from kit snapshots, so the recompute route's 'kit snapshots only' invariant does not hold for the relayed dict. (vi) Downstream: `summary()` prints the ES/group tables; `to_dataframe(level='event_study')` returns the unified `EVENT_STUDY_SCHEMA` frame on EVERY fit ([M-093] item 8b: bootstrapped fits relay the stored percentile surface, analytical fits recompute from the kit) and `to_dataframe(level='group')` keeps the native frame on bootstrapped fits; `DiagnosticReport`/`BusinessReport` run `heterogeneity` (on the relayed GROUP percentile dict - `_collect_effect_scalars` reads `group_effects` first) and `parallel_trends` (`pretrends=True`) on the relayed surface instead of skipping with `NotImplementedError` (no sensitivity check applies); `plot_event_study(results)` plots them natively. Replicate-weight designs never meet the always-on surfaces (replicate + `n_bootstrap > 0` is rejected before them), so the fit-time replicate stack is always `[overall]`-only - the degenerate-design overall-row migration delta in (e) stands. +- **Note (reference-row marking, 4.0, [M-093] item 8a):** the `event_study_effects` reference row (`e = -1 - anticipation`) carries `"is_reference": True` and no `n_obs` key (3.x: the `n_obs == 0` sentinel, retired); every remaining `n_obs` is a genuine count. `summary()` keys its `[ref: h]` line on the marker; the bootstrap loop skips the marked row; 3.x pickles are migrated on load (`_deprecation.migrate_event_study_reference_sentinel` in `__setstate__`). Since 8b `to_dataframe(level='event_study')` is the unified `EVENT_STUDY_SCHEMA` frame (`n` NaN on the `is_reference` row). - **Note:** `anticipation` is validated at construction (non-negative integer; `bool` rejected) via the shared `utils.validate_anticipation`, and re-checked on the fit path (direct-mutation defense) — see the family-wide adoption note (ledger row [M-144]) in the TripleDifference staggered-mode section. --- @@ -2432,8 +2434,8 @@ Our implementation uses multiplier bootstrap on the GMM influence function: clus - [x] Event study and overall ATT aggregation - **Note (post-fit aggregate() - rows M-022/M-119):** `fit(aggregate=, balance_e=)` was deprecated in 3.9 and REMOVED at 4.0 (PR 3c of the cut; `survey_design` is positional slot 7; the `two_stage_did()` wrapper that used to forward the shared sentinel was removed per M-071 - PR 2) in favor of post-fit `TwoStageDiDResults.aggregate(type, balance_e=)` - a PANEL-BACKED lazy recompute kit: each level is a fresh Stage-2 OLS + joint Gardner-GMM sandwich on a level-specific design, so no compact influence payload exists. (a) RETAINED BUFFERS (memory contract - the FIRST panel retention on TwoStageDiD results, a deliberate break from the CS/EDiD identifier-minimization guarantee, with a `store_kit` opt-out tracked in DEFERRED.md): a COLUMN-SUBSET COPY of the working frame - `unit`/`time`/`outcome`/`first_treat` + covariates + the cluster column (deduplicated: `cluster=` may legally name a core column) + `_never_treated`/`_rel_time`/`_y_tilde` - O(n_obs) on every results object and pickle; the Stage-1 FE model (`unit_fe`/`time_fe`/`grand_mean`/`delta_hat`/`kept_cov_mask`), the Omega masks, the full-domain `keep_mask`, the Wave-E.3-GATED `score_pad_mask`/`cluster_ids_full` values fit actually passed (None unless the always-treated pad was active), `survey_weights`, and the resolved survey design - on replicate designs that adds the O(n_obs x R) replicate matrix; plus value snapshots (`treatment_groups` copy, `ref_period`, `overall_att`, `n_treated_obs`, a `dataclasses.replace` copy of `survey_metadata`) and TWO df scalars (`survey_df_stage2`, the recompute seed; `survey_df_final`, what the stored overall inference received). (b) `balance_e` uses the BALANCED-WINDOW rule (`[-balance_e, max_h]` coverage against the full panel - the ImputationDiD rule, divergent from CS/EfficientDiD's anchor-horizon rule); zero qualifying cohorts warns and yields the reference-row-only dict with `vcov=None`. (c) BOOTSTRAP fits: 'simple' RELAYS the stored overall quintet verbatim (finite safe_inference t included) with a NaN df column, while since 4.0 (PR 3c) the ES/group levels RELAY the percentile surface materialized at fit (see the bootstrapped-fit relay Note below; `balance_e` fails closed there - a balanced-window replay needs retained per-level GMM scores, which are function-locals: the TODO row) - the prior uniform rule superseded 2026-08-05 with the M-027 per-level convergence; a fit whose bootstrap FAILED (`bootstrap_results=None`, analytical inference retained) aggregates normally (analytical recompute). (d) CONTAINER ADMISSION into `compute_honest_did`/`compute_pretrends_power` is DEFERRED, not by-design (both terminal TypeErrors state it): analytical surfaces DO carry the real joint Gardner-GMM covariance (M-092), but the pre-period coefficients are stage-1 residual MEANS - the reference horizon is dropped from the no-intercept Stage-2 design and the zero anchor row is appended mechanically - not contrasts against the advertised reference, while HonestDiD's Delta^RM/Delta^SD arithmetic hard-codes the `delta_0 = 0` normalization into its boundary/bridge constraints; admission awaits a normalization derivation (either re-estimating Stage 2 with the reference horizon in the design or deriving the residual-to-reference mapping) - the DEFERRED.md paper-gated row. (e) RELAY CONVENTIONS: 'simple' relays the stored overall quintet bit-exact with `n = n_treated_obs`, `n_kind="obs"` (overlapping unit sets - the StackedDiD carve-out class; the pre-filter |Omega_1| count, while the ATT's Stage-2 support excludes rows whose `y_tilde` is non-finite - on such degenerate fits `n` exceeds the averaged support) and `df = survey_df_final` (on replicate fits that value came from the `[overall]`-only joint stack - snapshotted, never re-derived); 'group' relays a SCALAR df broadcast (deliberate divergence from ImputationDiD's per-row `df_used`: `_stage2_group` passes one immutable `survey_df` to every row's `safe_inference`, so the scalar is provenance-exact by construction and the moved method stays verbatim); 'event_study' reproduces the M-092 container contract exactly - analytical fits thread the recomputed joint vcov + `vcov_index` + the finite-and->0 df scalar through the carrier, replicate fits thread `vcov=None`/`index=None` with the REPLAYED level-matched df, and the carrier's metadata is a copy-on-use of the KIT's fit-final metadata copy. REPLICATE-WEIGHT fits replay the extracted `_replay_replicate_inference` with a LEVEL-MATCHED stack (the ImputationDiD semantics: `aggregate(L)` reproduces `fit(aggregate=L)`; `fit(aggregate='all')` is not the equivalence target on degenerate designs; the OVERALL-row migration delta on such designs is documented and pinned). Recompute re-emits fit-time warnings with fit-tuned stacklevels - an accepted verbatim-move trade-off. `DiagnosticReport` now derives this container internally on plain fits (raw `event_study_effects` absent), so its `parallel_trends` (`pretrends=True` fits; the recomputed joint vcov drives the joint-Wald path) and `heterogeneity` checks run on analytic plain fits (bootstrapped fits are read from the fit-materialized raw fields - raw-field precedence, no joint vcov there); re-emitted recompute warnings are captured and re-published on the consuming report section, and derivation failures (missing kit, a fail-soft-cleared bootstrapped surface) surface as explicit per-check skip reasons. (f) `aggregate('total')` (3.10): the estimator-owned total incremental outcome - an exact relay `C x overall` CONDITIONAL on the realized aggregation mass, with `C` the POST-FILTER treatment-indicator support the second stage actually regressed on (`omega_1_mask` minus non-finite `_y_tilde` rows, computed inline from the kit's private column-subset frame COPY - snapshot-grade, no fit-time stash needed, and the fit-time masker is never re-called so its warnings are not re-emitted; `score_pad_mask` is irrelevant here - it pads scores/variance only). This is the post-filter count 'simple''s raw pre-filter `n` documents as exceeding on degenerate fits. `D.sum() == 0` -> NaN mass -> all-NaN row (mirrors the fit's `(nan, nan)`); finite `C` relays verbatim. Unweighted fits only: declared `survey_design=` fails closed via the kit's `survey_metadata` snapshot (mutation-proof). Bootstrap fits relay with a NaN df column (M-027); df carrier = `survey_df_final`; container: single `target='total'` row, `n_kind='obs'`. MMM: admitted with NO scale (see the MMM section). -- **Note (bootstrapped-fit relay, 4.0):** (i) RELAY: a fit with `n_bootstrap > 0` ALWAYS computes the event-study (`_stage2_event_study`) AND group (`_stage2_group`) percentile surfaces at fit (`balance_e=None` - exactly what the 3.x `fit(aggregate='all')` computed; percentile se/p/CI; `event_study_vcov`/`event_study_vcov_index`/`event_study_df` None), stores them in `event_study_effects`/`group_effects`, and `aggregate('event_study')`/`aggregate('group')` RELAY them (alpha/anticipation/survey metadata from the kit snapshot; per-row `df` column NaN on both levels - the group relay passes `group_df=None` so no analytical df leaks beside percentile inference; `df_survey` scalar from the carrier's survey metadata; `vcov`/`vcov_index` None; reference rows via the `is_reference` key). `aggregate('event_study', balance_e=k)` on a bootstrapped fit FAILS CLOSED (`NotImplementedError`: the stored surface is unrestricted and the per-level GMM scores are not retained; re-fit with `n_bootstrap=0`). Analytic fits are unchanged (kit recompute; legacy fields None). The gate is `bootstrap_results is not None`: `None` (n_bootstrap == 0, OR a bootstrapped fit that ended analytical) means the analytical kit RECOMPUTE, the pre-4.0 behaviour. (ii) FAIL-SOFT materialization + storage rule: the surfaces are built inside one guard - any exception leaves them None (the `_es_vcov_ts`/`_es_vcov_index_ts` locals reset too) and the bootstrap runs overall-only (the pre-4.0 plain-fit path); a `_run_bootstrap` attempt that raises (anything but `NotImplementedError`, re-raised) OR RETURNS `None` WITH surfaces present - `_run_bootstrap` returns `None` without an exception when `_compute_cluster_S_scores` hits `_TS_LSMRUnconverged` in the ES or group stage, since the LSMR fallback certifies each RHS column separately and the overall column can succeed while an ES/group column does not - clears them and RETRIES ONCE overall-only (`original_event_study=None, original_group=None, aggregate=None`; `np.random.default_rng(self.seed)` is rebuilt on entry, so a seeded retry reproduces attempt 1's weight draws and the overall quintet equals a plain fit's). A retry failure (a `None` return included) keeps the pre-4.0 catch-all 'Bootstrap failed' warning + analytical overall. STORAGE RULE: the legacy fields hold a surface IFF `bootstrap_results is not None`; when the fit ends analytical they are cleared and `aggregate()` RECOMPUTES analytically. The fail-soft `UserWarning` fires only when the fit DOES carry bootstrap inference and a surface was dropped: 'TwoStageDiD: the event-study/group surfaces could not be materialized on this bootstrapped fit ({e}); the bootstrap covered the overall ATT only, so aggregate('event_study') / aggregate('group') are unavailable here - re-fit with n_bootstrap=0 for the analytical post-fit recompute.'; the relay then raises the shared absent-surface `ValueError` ('This bootstrapped TwoStageDiDResults carries no stored event_study surface: either it was fitted before 4.0 (a bootstrapped plain fit built none) or the surface could not be materialized at fit (see the fit-time warning). Re-fit with diff-diff >= 4.0 for the relayed surface, or with n_bootstrap=0 for the analytical post-fit recompute.'). Net invariant: a bootstrapped plain fit's overall quintet and success/abort outcome are unchanged by the 4.0 removal. (iii) Newly reachable warnings on bootstrapped plain fits: the aggregators' own `UserWarning`s (Stage-1/Stage-2 singular or rank-deficient fallbacks) and the per-target bootstrap-stats `RuntimeWarning`s ('Dropping n/N non-finite bootstrap samples in ... event study (h=...)', 'Too few valid bootstrap samples', 'Bootstrap SE is non-finite or zero'). (iv) Pre-4.0 pickle limitation: a 3.9-3.13 bootstrapped pickle fitted with `fit(aggregate=..., balance_e=k)` stores a balance_e-RESTRICTED surface and records no fit-time `balance_e`, so the relay returns that surface as stored - re-fit under 4.0 for the unrestricted one; one fitted plain stores none and the relay raises the refit `ValueError`. (v) The relay reads the MUTABLE public `event_study_effects`/`group_effects` fields (a view by definition - the StackedDiD precedent); only alpha/anticipation/survey metadata come from kit snapshots, so the recompute route's 'kit snapshots only' invariant does not hold for the relayed dict. (vi) Downstream: `summary()` prints the ES/group tables and `to_dataframe(level='event_study'|'group')` returns frames on bootstrapped plain fits; `DiagnosticReport`/`BusinessReport` run `heterogeneity` (on the relayed GROUP percentile dict - `_collect_effect_scalars` reads `group_effects` first) and `parallel_trends` (`pretrends=True`) on the relayed surface instead of skipping with `NotImplementedError` (no sensitivity check applies); `plot_event_study(results)` plots them natively. Replicate-weight designs never meet the always-on surfaces (replicate + `n_bootstrap > 0` is rejected before them), so the fit-time replicate stack is always `[overall]`-only - the degenerate-design overall-row migration delta in (e) stands. -- **Note (reference-row marking, 4.0, [M-093] item 8a):** the `event_study_effects` reference row (`e = -1 - anticipation`) carries `"is_reference": True` and no `n_obs` key (3.x: the `n_obs == 0` sentinel, retired); the all-filtered horizons keep their GENUINE `effect = NaN, n_obs = 0` rows, which `summary()` now prints as NaN rows (previously mislabeled `[ref: h]`); a REFERENCE-ONLY surface (`balance_e` leaving no cohort, or the `ref_only` return with no Prop-5 rows - Prop-5 rows carry a positive `n_obs`, so an all-Prop-5 surface keeps `n_kind="obs"`) carries `n_kind=None` in the container (no row records a count). The bootstrap loop skips the marked row; 3.x pickles are migrated on load (`__setstate__`). The native `to_dataframe(level='event_study')` frame renders the reference row's `n_obs` as NaN (float column) until the 8b schema rewire. +- **Note (bootstrapped-fit relay, 4.0):** (i) RELAY: a fit with `n_bootstrap > 0` ALWAYS computes the event-study (`_stage2_event_study`) AND group (`_stage2_group`) percentile surfaces at fit (`balance_e=None` - exactly what the 3.x `fit(aggregate='all')` computed; percentile se/p/CI; `event_study_vcov`/`event_study_vcov_index`/`event_study_df` None), stores them in `event_study_effects`/`group_effects`, and `aggregate('event_study')`/`aggregate('group')` RELAY them (alpha/anticipation/survey metadata from the kit snapshot; per-row `df` column NaN on both levels - the group relay passes `group_df=None` so no analytical df leaks beside percentile inference; `df_survey` scalar from the carrier's survey metadata; `vcov`/`vcov_index` None; reference rows via the `is_reference` key). `aggregate('event_study', balance_e=k)` on a bootstrapped fit FAILS CLOSED (`NotImplementedError`: the stored surface is unrestricted and the per-level GMM scores are not retained; re-fit with `n_bootstrap=0`). Analytic fits are unchanged (kit recompute; legacy fields None). The gate is `bootstrap_results is not None`: `None` (n_bootstrap == 0, OR a bootstrapped fit that ended analytical) means the analytical kit RECOMPUTE, the pre-4.0 behaviour. (ii) FAIL-SOFT materialization + storage rule: the surfaces are built inside one guard - any exception leaves them None (the `_es_vcov_ts`/`_es_vcov_index_ts` locals reset too) and the bootstrap runs overall-only (the pre-4.0 plain-fit path); a `_run_bootstrap` attempt that raises (anything but `NotImplementedError`, re-raised) OR RETURNS `None` WITH surfaces present - `_run_bootstrap` returns `None` without an exception when `_compute_cluster_S_scores` hits `_TS_LSMRUnconverged` in the ES or group stage, since the LSMR fallback certifies each RHS column separately and the overall column can succeed while an ES/group column does not - clears them and RETRIES ONCE overall-only (`original_event_study=None, original_group=None, aggregate=None`; `np.random.default_rng(self.seed)` is rebuilt on entry, so a seeded retry reproduces attempt 1's weight draws and the overall quintet equals a plain fit's). A retry failure (a `None` return included) keeps the pre-4.0 catch-all 'Bootstrap failed' warning + analytical overall. STORAGE RULE: the legacy fields hold a surface IFF `bootstrap_results is not None`; when the fit ends analytical they are cleared and `aggregate()` RECOMPUTES analytically. The fail-soft `UserWarning` fires only when the fit DOES carry bootstrap inference and a surface was dropped: 'TwoStageDiD: the event-study/group surfaces could not be materialized on this bootstrapped fit ({e}); the bootstrap covered the overall ATT only, so aggregate('event_study') / aggregate('group') are unavailable here - re-fit with n_bootstrap=0 for the analytical post-fit recompute.'; the relay then raises the shared absent-surface `ValueError` ('This bootstrapped TwoStageDiDResults carries no stored event_study surface: either it was fitted before 4.0 (a bootstrapped plain fit built none) or the surface could not be materialized at fit (see the fit-time warning). Re-fit with diff-diff >= 4.0 for the relayed surface, or with n_bootstrap=0 for the analytical post-fit recompute.'). Net invariant: a bootstrapped plain fit's overall quintet and success/abort outcome are unchanged by the 4.0 removal. (iii) Newly reachable warnings on bootstrapped plain fits: the aggregators' own `UserWarning`s (Stage-1/Stage-2 singular or rank-deficient fallbacks) and the per-target bootstrap-stats `RuntimeWarning`s ('Dropping n/N non-finite bootstrap samples in ... event study (h=...)', 'Too few valid bootstrap samples', 'Bootstrap SE is non-finite or zero'). (iv) Pre-4.0 pickle limitation: a 3.9-3.13 bootstrapped pickle fitted with `fit(aggregate=..., balance_e=k)` stores a balance_e-RESTRICTED surface and records no fit-time `balance_e`, so the relay returns that surface as stored - re-fit under 4.0 for the unrestricted one; one fitted plain stores none and the relay raises the refit `ValueError`. (v) The relay reads the MUTABLE public `event_study_effects`/`group_effects` fields (a view by definition - the StackedDiD precedent); only alpha/anticipation/survey metadata come from kit snapshots, so the recompute route's 'kit snapshots only' invariant does not hold for the relayed dict. (vi) Downstream: `summary()` prints the ES/group tables; `to_dataframe(level='event_study')` returns the unified `EVENT_STUDY_SCHEMA` frame on EVERY fit ([M-093] item 8b: bootstrapped fits relay the stored percentile surface, analytical fits recompute from the kit) and `to_dataframe(level='group')` keeps the native frame on bootstrapped fits; `DiagnosticReport`/`BusinessReport` run `heterogeneity` (on the relayed GROUP percentile dict - `_collect_effect_scalars` reads `group_effects` first) and `parallel_trends` (`pretrends=True`) on the relayed surface instead of skipping with `NotImplementedError` (no sensitivity check applies); `plot_event_study(results)` plots them natively. Replicate-weight designs never meet the always-on surfaces (replicate + `n_bootstrap > 0` is rejected before them), so the fit-time replicate stack is always `[overall]`-only - the degenerate-design overall-row migration delta in (e) stands. +- **Note (reference-row marking, 4.0, [M-093] item 8a):** the `event_study_effects` reference row (`e = -1 - anticipation`) carries `"is_reference": True` and no `n_obs` key (3.x: the `n_obs == 0` sentinel, retired); the all-filtered horizons keep their GENUINE `effect = NaN, n_obs = 0` rows, which `summary()` now prints as NaN rows (previously mislabeled `[ref: h]`); a REFERENCE-ONLY surface (`balance_e` leaving no cohort, or the `ref_only` return with no Prop-5 rows - Prop-5 rows carry a positive `n_obs`, so an all-Prop-5 surface keeps `n_kind="obs"`) carries `n_kind=None` in the container (no row records a count). The bootstrap loop skips the marked row; 3.x pickles are migrated on load (`__setstate__`). Since 8b `to_dataframe(level='event_study')` is the unified `EVENT_STUDY_SCHEMA` frame (`n` NaN on the `is_reference` row). - **Note:** `anticipation` is validated at construction (non-negative integer; `bool` rejected) via the shared `utils.validate_anticipation`, and re-checked on the fit path (direct-mutation defense) — see the family-wide adoption note (ledger row [M-144]) in the TripleDifference staggered-mode section. --- @@ -2586,7 +2588,7 @@ The pooled estimator is `DID^{CBWSDID}_e = Σ_a (N^D_a/N^D_Ω)(Δ̄^D_{a,e} − - **Note:** v1 scope — only `balance="entropy"` with `weighting="aggregate"`. `balance` + `population`/`sample_share` and `balance` + `survey_design=` raise `NotImplementedError`; matching-based balancing and the repeated `0→1/1→0` episode extension are out of scope. - **Note (post-fit aggregate() - row M-024):** `fit(aggregate=)` was deprecated in 3.9 and REMOVED at 4.0 (PR 3b of the cut; its `group`/`all`/unknown ValueErrors died with it - `aggregate('group')` already fails closed in the mixin - and `population`/`survey_design`/`covariates` are positional slots 6/7/8) and the event-study surface is ALWAYS materialized at fit — the pooled regression always included the event-time interactions, so only the extraction and, under `hc2_bm`, the per-event Bell-McCaffrey contrast DOFs were fit-time-gated; both gates are gone (the param had been behaviorally inert since 3.9). `aggregate('event_study')` returns the unified `EventStudyResults` container (a pure view; no recomputation, so every stored inference mode — survey TSL, replicate refit, hc2_bm fail-close — relays faithfully); `aggregate('simple')` relays `overall_att/se/t/p/CI` bit-exactly with `target="att"` (the scalar is the equally-weighted post-period average, NOT the per-event-time trimmed aggregate ATT `θ^e_κ` — `describe_target_parameter` remains the estimand's prose source of truth), `n = n_treated_units` with `n_kind="units"` (TREATED-only scope: under dynamic clean-control definitions a later-treated unit is treated in its own sub-experiment and a clean control in earlier ones, so the treated and control unit sets OVERLAP and no disjoint total exists — cross-container `n` comparisons vs CallawaySantAnna's treated+control convention are out of contract), and `df = inference_df` (the stored overall inference's exact provenance). Consequences of the always-computed surface: plain-fit `summary()` renders the event-study table, `to_dataframe('event_study')` is total on 3.9+ fits, `plot_event_study` renders plain fits, DiagnosticReport's `heterogeneity` (always) and `parallel_trends` (`kappa_pre >= 2`) checks run on plain fits, and the `hc2_bm` overall-ATT contrast now shares the m>1 batch of `_compute_cr2_bm_contrast_dof`, whose degeneracy guard is batch-relative — the noise-floor carve-out on the `Wald_test(test="HTZ")$df_denom` parity claim in the variance-families bullets above widens quantitatively (unchanged behavior when tripped: fail-closed NaN inference plus linalg's loud noise-floor UserWarning; the absolute floor already made this reachable at m=1). - **Note (container admission - rows M-024/M-093):** `compute_honest_did` and `compute_pretrends_power` admit Stacked-sourced `aggregate('event_study')` containers. Provenance: `base_period` is the class-level constant `"universal"` (every sub-experiment normalizes against the single omitted reference `e = -1 - anticipation`) and `reference_event_times` is the singleton `(-1 - anticipation,)`. Requirements: `kappa_pre >= 2` (the default `kappa_pre=1` grid minus the reference has ZERO estimated pre-periods — both consumers reject), and for HonestDiD a non-singular FULL retained event-study covariance — the validated sub-block spans the retained pre AND post rows (`allow_singular=False` per Rambachan-Roth's eigenvalues-bounded-away-from-zero assumption on the full Σ — keep `kappa_pre` small relative to the cluster count; PreTrendsPower keeps its documented singular support). The container's scalar `df_survey` is None on analytical fits — the generic resolver's fallback reads `df_inference`, a name StackedDiD does not carry (`inference_df` is the overall POST-AVERAGE contrast df, not a design-level cluster df, and misusing it for FLCI critical values would be worse than the asymptotic default) — so honest FLCI critical values are NORMAL-THEORY on analytical Stacked fits, diverging from bare-`cluster=` CS fits (which thread `G-1`); survey fits thread the survey df and replicate-undefined designs the fail-closed `0.0` sentinel (NaN FLCI CIs; the identified-set `lb`/`ub` bounds are df-independent and stay finite). Withheld-inference rows (finite `se`, non-finite `p_value` — the hc2_bm BM-DOF fail-close and replicate-undefined designs both emit this shape) are ADMITTED with a source-scoped warning in BOTH consumers: bounds/power consume only the point estimates and covariance, which remain valid; the warning is shape-descriptive because containers are publicly constructible. A rank-dropped interior event-time column (NaN-se row) fails closed via honest's consecutive-grid ValueError with a Stacked-specific remedy (no `balance_e` — Stacked's aggregate() has no such level). REFERENCE-SUPPORT GUARD: `fit()` raises when either omitted-reference cell (treated / control) carries no POSITIVE COMPOSED WLS WEIGHT — Equation 3 is Q-weighted, so raw row presence is not support: a gapped panel can empty the cell outright (IC1 checks only min/max periods), aggregate Q-weights zero a sub-experiment's controls at any event time where that sub-experiment has no treated rows, and survey pweights can zero a cell — without the guard the design rank-drops and QR pivoting silently re-normalizes against an arbitrary horizon while the surface certifies `delta_0 = 0` at the reference. Partial support (some cohorts' own ref period absent while the POOLED omitted category is populated) still fits: the coefficient-vector normalization is real; the per-cohort baseline raggedness is estimand composition, the documented ragged-window behavior. -- **Note (reference-row marking, 4.0, [M-093] item 8a):** the always-materialized `event_study_effects` reference row (`e = -1 - anticipation`) carries `"is_reference": True` and no `n_obs` key (3.x: the `n_obs == 0` sentinel, retired); `summary()` keys its `[ref: h]` line on the marker; `plot_event_study(results)` and `DiagnosticReport` locate the reference by the key; 3.x pickles are migrated on load (`__setstate__`). The native `to_dataframe(level='event_study')` frame renders the reference row's `n_obs` as NaN (float column) until the 8b schema rewire. +- **Note (reference-row marking, 4.0, [M-093] item 8a):** the always-materialized `event_study_effects` reference row (`e = -1 - anticipation`) carries `"is_reference": True` and no `n_obs` key (3.x: the `n_obs == 0` sentinel, retired); `summary()` keys its `[ref: h]` line on the marker; `plot_event_study(results)` and `DiagnosticReport` locate the reference by the key; 3.x pickles are migrated on load (`__setstate__`). Since 8b `to_dataframe(level='event_study')` is the unified `EVENT_STUDY_SCHEMA` frame (`n` NaN on the `is_reference` row). *Covariate-balancing edge cases:* - Infeasible cohort (treated covariate mean outside the clean-control hull → entropy balancing cannot match the moments): **fail-closed** `ValueError` naming the cohort and worst covariate — NOT silently dropped (dropping a cohort would shift the estimand to an overlap-trimmed ATT, Ustyuzhanin 2026 §3.1). @@ -2785,6 +2787,7 @@ where `g(·)` is the link inverse (logistic or exp), `η_i` is the individual li - **Note:** `cohort_trends=True` + `survey_design` is **NOT yet supported** (raises `NotImplementedError` at `fit()`). The full-dummy auto-route composed with the survey TSL variance has not been validated against R-parity goldens. Tracked in DEFERRED.md follow-up. - **Note:** `cohort_trends=True` + `control_group="never_treated"` is **NOT yet supported** (raises `NotImplementedError` at `fit()`). The OLS + never_treated branch emits the `(g, t)` placebo cells (paper W2025 Section 4.4 placebo coverage) MINUS each cohort's `ref(g)` reference cell (W2025 Eq. 6.1/6.4; see the reference-period Note below). The appended `dg_i · t` trend columns are still spanned — jointly by the emitted cells and the unit fixed effects, which absorb `1{cohort=g}` and thereby recover the omitted reference — so the Section 8 trend specification remains unidentified on this branch. Use `control_group="not_yet_treated"` (the default) for `cohort_trends=True`. Tracked in DEFERRED.md follow-up. - **Note:** Identification + baseline normalization for `cohort_trend_coefs` on all-eventually-treated panels: when a never-treated cohort (`g = 0`) is present, **all** `G` treated cohorts get a `dg_i · t` interaction column and `cohort_trend_coefs[g]` reports each cohort's linear slope relative to the never-treated baseline (absorbed by time FE). When **no** never-treated cohort exists, the **last cohort's** trend column is dropped deterministically per paper W2025 Section 5.4 ("all variables in regression (5.3) involving `dT_i` get dropped"); that cohort serves as the trend baseline, and `cohort_trend_coefs` surfaces `G - 1` entries (the last cohort is absent — its slope is the baseline in deviation form). **Note (deviation from W2025 Section 5.4 — narrowed to the covariate block):** Section 5.4's "all variables in regression (5.3) involving `dT_i` get dropped" governs four regressor families: the cells `dT·f_t`, the cell x covariate block `dT·f_t·ẋ`, the cohort trend `dT·t`, and the cohort x covariate block `D_T × X`. The library implements the first three — comparison-support filtering (below) removes the periods at which no unit is untreated, so cohort `G_max` receives no cells and no trend column, and the trend-drop path described here is reachable end-to-end. **The `D_{G_max} × X` normalization is NOT applied:** when covariates are supplied, `D_g × X` is still built for every cohort including `G_max`, so `Σ_g (D_g · x) = x` and the design is rank-deficient. Coefficients are unaffected (QR's arbitrary pick spans the same column space, `max |Δ| = 1.35e-14`), but `rank_deficient_action="error"` raises and `"warn"` drops an arbitrarily-chosen cohort's column rather than deterministically the last. Tracked in `TODO.md`; note for whoever takes it that `xtvar` under the default `demean_covariates=True` is FULL RANK (the raw block carries demeaned values while `D_g × X` carries raw ones, so the dependency does not arise) and forcing the drop there moves `att` 1.11903 → 1.46269. +- **Note (to_dataframe schema, 4.0, [M-093] item 8b):** `to_dataframe(level='event_study')` returns `build_event_study_surface(self).to_dataframe()` - the unified `EVENT_STUDY_SCHEMA` prefix (no reference row: the ETWFE relative-period aggregation carries none; `n` and `df` NaN - no count or df channel) - followed by two fit-configuration columns, `cohort_trends` and `aggregation_weights` (the stored event-study aggregation's weighting scheme). The event-study aggregation is computed post-fit, so calling `to_dataframe(level='event_study')` (the DEFAULT level) before `aggregate('event_study', weights=)` raises `ValueError` - through 3.x it returned an EMPTY frame (a silent failure; the weighting scheme changes the numbers and is never chosen silently). The other levels are unchanged. ### Deviations from the paper / from R / library extensions @@ -2877,6 +2880,7 @@ The paper specifies no standard-error formula (Section 1 defers to "standard, we 7. **Deviation from R:** `no_composition` is intentionally more faithful to the paper's fixed-composition intent (Section 3.6) than the R packages: it fixes the realized sample across *all* post horizons (every post coefficient shares one sample, even on unbalanced panels) and excludes cohorts with `p_g > T-H`, whereas `alexCardazzi/lpdid` uses a looser per-horizon sample and a stricter `treat_date < T-H` cutoff. It therefore has **no exact R-package anchor** and is validated by the pure-Python tests in `tests/test_lpdid.py` (the R-parity golden omits it; `alexCardazzi`'s looser-semantics value is recorded in the golden `meta`). 8. **Note (survey design):** Complex-survey support (`survey_design=SurveyDesign(...)`, PR-D1) covers the **variance-weighted default path** (`reweight=False`, with or without direct-inclusion covariates): each horizon's long-difference regression is fit by WLS on the survey probability weights, and the SE is the stratified-PSU **Taylor-linearization (Binder 1983 TSL)** sandwich `meat = sum_h (1-f_h)*(n_h/(n_h-1))*sum_j (S_hj - S_h_bar)(S_hj - S_h_bar)'` with `df = n_PSU - n_strata`, reusing the shared `diff_diff/survey.py` helpers (`compute_survey_vcov` / `_compute_stratified_psu_meat`). The design is re-resolved on each realized (post-clean-control) sample so weights/strata/PSU align with the regression rows; with no explicit PSU the unit (LP-DiD's default cluster) is injected as the PSU. Supports pweight + strata + PSU + FPC + lonely-PSU handling. It **rejects** `survey_design` combined with `reweight=True` (the equally-weighted / regression-adjustment IF path has no validated survey reference - the same gap as the RA SE in Deviation #2), replicate-weight designs, and non-pweight (fweight/aweight) types, each a deferred follow-up. The non-survey path is byte-for-byte unchanged (gated on `survey_design is None`). **PR-D2 validated all three survey paths end-to-end against `survey::svyglm`** - per-horizon point/SE/df + pooled for the variance-weighted full design (strata+PSU+FPC), the weights-only unit-injected-PSU design, and the direct-covariate variant (`tests/test_methodology_lpdid.py::TestLPDiDSurveyParityR`; point ~1e-6, SE ~1e-5, df exact via the per-design `n_PSU - n_strata` / `n_PSU - 1` formula). `svyglm` is itself the reference implementation of the Binder TSL sandwich, so it anchors the variance directly (no third-party survey-package gate is needed); the clean-sample construction is independently cross-checked in the generator (the unweighted variance-weighted event study matches `alexCardazzi/lpdid` to <1e-8, and selection is weight-independent). A dedicated survey panel (`benchmarks/data/lpdid_survey_panel.csv`, own seed) keeps the absorbing / non-absorbing goldens byte-identical. 9. **Note (clustered CR1 k, nested dummy blocks — 3.9 K_reference):** LPDiD's per-horizon design carries two built-in dummy blocks — the `_event_time` (time-FE) dummies added under `include_time_fe=True` and the inline `drop_first` `absorb=` dummies. The authors' reference recipe ABSORBS the time FE (`feols(... | time, vcov = ~unit)`, `benchmarks/R/generate_lpdid_golden.R`), so both blocks follow the nested convention: when a block is nested in the raw `cluster` (e.g. a time-like cluster for `_event_time`, or an absorb dim coarser than the cluster), its rank is SUBTRACTED from the CR1 factor's `k` via a negative `cluster_k_adjustment`, converging on the same `K_reference` accounting as `feols`/`reghdfe`. Non-nested configs are no-ops (unit clustering leaves `_event_time` non-nested → adjustment 0, preserving bit-identical Rust-lane output). The solve's broad `except (ValueError, ZeroDivisionError)` re-solve re-raises `InvalidClusterKAdjustment` so a contract violation can never degrade to a silent `se=NaN` (`tests/test_variance_conventions.py::TestKReferenceConvergence::test_lpdid_never_swallows_the_contract_raise`). The `t(G-1)` tail reference (Note 1) is unchanged and is, since 3.9, the `df_convention="cluster"` DEFAULT (see Note 1's knob addendum). +10. **Note (to_dataframe schema, 4.0, [M-093] item 8b):** `to_dataframe(level='event_study')` returns `build_event_study_surface(self).to_dataframe()` - the unified `EVENT_STUDY_SCHEMA` frame (`horizon -1` the `is_reference` row; `n` = observations) - instead of a copy of the native per-horizon frame, which stays available as the `event_study` attribute (it keeps `n_clusters`); a fit with `only_pooled=True` raises `event_study dataframe was not computed`, and a zero-row stored frame raises the builder's absent-surface error (through 3.x it passed through as an empty frame). `'pooled'` is unchanged. ### Implementation Checklist @@ -4395,8 +4399,10 @@ shared verbatim. silently ignored (the mixin's shared `balance_e` rejection text lists `'simple'` because of this one selector). `summary()` no longer prints the event-study / cohort / Eq. 4.14 blocks (it points at the post-fit calls); `to_dataframe()` keeps - `'group_time'` (default in the staggered design) and `'overall'` (default in 2x2x2), - while `'event_study'` / `'group'` raise with the post-fit pointer. + `'group_time'` (default in the staggered design) and `'overall'` (default in 2x2x2); + since the 4.0 cut ([M-093] item 8b) `'event_study'` returns the unified + `aggregate('event_study').to_dataframe()` frame (staggered design; the 2x2x2 design + raises the single-ATT error), while `'group'` raises with the post-fit pointer. ## StaggeredTripleDifference @@ -5253,7 +5259,7 @@ Shipped in `diff_diff/had_pretests.py` as `stute_joint_pretest()` (residuals-in - **Note (Phase 2b constant-dose requirement):** The event-study aggregation uses `D_{g, F}` (first-treatment-period dose) as the single regressor for every event-time horizon, per paper Appendix B.2's "once treated, stay treated with the same dose" convention. The validator REJECTS panels where a unit has time-varying dose across post-treatment periods (`D_{g, t} != D_{g, F}` for any `t >= F` within-unit, beyond float tolerance) with a front-door `ValueError`, directing users with genuinely time-varying post-treatment doses to `ChaisemartinDHaultfoeuille` (`did_multiplegt_dyn`). Silent acceptance would misattribute later-horizon treatment-effect heterogeneity to the period-F dose. A follow-up PR could implement a time-varying-dose estimator; tracked in `DEFERRED.md`. - **Note (Phase 2b per-horizon SE):** Each event-time horizon uses an INDEPENDENT sandwich computed on that horizon's first differences: continuous paths use the CCT-2014 robust SE from Phase 1c divided by `|den|`; mass-point path uses the structural-residual 2SLS sandwich from Phase 2a. This produces pointwise CIs per horizon, matching the paper's Pierce-Schott application (Section 5.2, Figure 2: "nonparametric pointwise CIs"). Joint cross-horizon covariance (IF-based stacking or block bootstrap) is NOT computed — the paper does not derive it and all reported CIs are pointwise. Follow-up PRs may add joint covariance for cross-horizon hypothesis tests; current tracking in `DEFERRED.md`. - **Note (Phase 2b baseline convention):** All event-time horizons use a uniform `F-1` anchor: `ΔY_{g,t} = Y_{g,t} - Y_{g,F-1}` for every `t`. This is consistent with the paper's Garrett-et-al. application (Section 5.1: "outcome `Y_{g,t} - Y_{g,2001}`" where `F = 2002`), simplifies event-time indexing (`e = t - F` so `e = -1` is the anchor, skipped), and keeps the implementation symmetric for pre- and post-period horizons. The paper review text's asymmetric "`Y_{g,t} - Y_{g,1}` for pre" / "`Y_{g,t} - Y_{g,F-1}` for post" phrasing is covered by the uniform convention since both give the same placebo interpretation under parallel trends (the paper's own applications use the uniform anchor). - - **Note (Phase 2b result class):** a multi-period panel returns a new `HeterogeneousAdoptionDiDEventStudyResults` dataclass (distinct from the single-period `HeterogeneousAdoptionDiDResults`) with per-horizon arrays (`event_times`, `att`, `se`, `t_stat`, `p_value`, `conf_int_low`, `conf_int_high`, `n_obs_per_horizon`) and shared metadata. `to_dataframe()` returns a tidy per-horizon DataFrame; `to_dict()` returns a dict with list-of-per-horizon fields. The static return-type annotation on `fit()` is `Union[HeterogeneousAdoptionDiDResults, HeterogeneousAdoptionDiDEventStudyResults]`, matching the runtime polymorphism on the panel shape; callers should narrow via `isinstance` (or via the panel shape they supplied) when reading aggregate-specific fields. + - **Note (Phase 2b result class):** a multi-period panel returns a new `HeterogeneousAdoptionDiDEventStudyResults` dataclass (distinct from the single-period `HeterogeneousAdoptionDiDResults`) with per-horizon arrays (`event_times`, `att`, `se`, `t_stat`, `p_value`, `conf_int_low`, `conf_int_high`, `n_obs_per_horizon`) and shared metadata. `to_dataframe(level='event_study')` returns the unified `EVENT_STUDY_SCHEMA` frame via `aggregate('event_study')` (4.0, [M-093] item 8b: `estimand` carries the WAS label, `n` counts units, no reference row - the 3.x bespoke `conf_int_low/high`, `n_obs`, `cband_low/high` columns are gone); `to_dict()` returns a dict with list-of-per-horizon fields. The static return-type annotation on `fit()` is `Union[HeterogeneousAdoptionDiDResults, HeterogeneousAdoptionDiDEventStudyResults]`, matching the runtime polymorphism on the panel shape; callers should narrow via `isinstance` (or via the panel shape they supplied) when reading aggregate-specific fields. - **Note (Phase 3 pretest workflow):** Phase 3 ships the Section 4 pre-test diagnostics in a new module `diff_diff/had_pretests.py` (separate from the 2,800-line `had.py` estimator module). The three tests — `qug_test`, `stute_test`, `yatchew_hr_test` — accept raw `(d, dy)` arrays and return their own result dataclasses (`QUGTestResults`, `StuteTestResults`, `YatchewTestResults`); the composite `did_had_pretest_workflow` dispatches on the panel shape (a balanced two-period panel runs the overall battery; a multi-period panel with >= 3 periods runs the event-study battery) and returns `HADPretestReport` with a priority-ordered verdict string. The `data`-only workflow signature (no `result=`-consuming variant) avoids coupling pretests to the `BiasCorrectedFit` internal state, which does not expose residuals. Below-minimum sample sizes emit `UserWarning` and return all-NaN inference (no `ValueError` raise) for library consistency with the `safe_inference` convention; input-shape violations (non-1D, NaN-containing, mismatched length) still raise. No multiple-testing adjustment (Bonferroni/Holm) is applied — the paper does not prescribe one. **Partial-workflow semantic (path-dependent):** a two-period panel (the overall battery) runs paper steps 1 (QUG) + 3 (linearity via Stute + Yatchew-HR) ONLY; step 2 (Assumption 7 pre-trends test via Equation 18) is NOT covered on this path and a fail-to-reject verdict appends an explicit Assumption 7 gap flag (`"QUG and linearity diagnostics fail-to-reject; Assumption 7 pre-trends test NOT run (paper step 2 deferred)"`). a multi-period panel (the event-study battery; requires >=3 periods + earlier pre-period for joint pretrends) closes step 2 via joint Stute pre-trends (Equation 18) over pre-period horizons AND joint Stute homogeneity over post-period horizons; the verdict on a fail-to-reject across QUG + joint pretrends + joint homogeneity reads "TWFE admissible under Section 4 assumptions" without the Assumption 7 caveat. The `HADPretestReport` docstring and structural fields (`pretrends_joint`, `homogeneity_joint`) reflect the aggregate-dependent coverage. - **Note (Phase 3 Stute bootstrap):** The Stute CvM statistic is implemented via the simplified-form `S = (1/G²) · Σ cumsum²`, which is algebraically equivalent to the paper's stated `Σ(g/G)² · ((1/g) Σ eps_{(h)})²` form. Bootstrap follows the paper's Appendix D Algorithm literally: each iteration refits OLS on `dy_b = a_hat + b_hat · d + eps · eta` (null DGP multiplied by Mammen weights). The ~2× performance follow-up landed 2026-07-07 as **loop-invariant hoisting**, not the once-sketched `M = I - X(X'X)⁻¹X'` materialization (O(G²) memory — would not scale): Mammen draws are batched through memory-bounded `rng.choice` calls whose variate stream is identical to per-iteration draws, and the d-moments + CvM tie-block indices are precomputed once per test — each replicate still runs the paper-literal O(G) refit on the same values, so `bootstrap_S`/`cvm_stat`/`p_value` are bit-identical to the per-iteration form (frozen byte-copy parity test at `tests/test_had_pretests.py::TestStuteBootstrapHoistedInvariants`). Bootstrap reproducibility uses `np.random.default_rng(seed)` (library convention, matching `wild_bootstrap_se`); `seed=42` in tests for bitwise stability. - **Note (Phase 3 Yatchew normalizer):** `σ̂²_diff` in `yatchew_hr_test` divides by `2G` (paper-literal from Theorem 7), NOT by `2(G-1)`. A unit test hand-computes `σ̂²_diff` at `G=4` on deterministic inputs and asserts the `2G`-normalizer form at `atol=1e-12` so any later regression to the (asymptotically equivalent but finite-sample different) `2(G-1)` form would fail the test. `σ̂⁴_W` uses `np.mean(eps_{(g)}² · eps_{(g-1)}²)` which divides by `G-1` via `np.mean` length, matching the paper's Theorem 7 / Equation 29 normalization. @@ -6244,6 +6250,7 @@ equivalently `p <= alpha`. with `n = n_units` and a NaN `df` column: inference is bootstrap-only (centred-bootstrap SD and symmetric `point +/- crit*se` intervals, no percentile intervals; `t_stat` is the analytical z), so no finite df governs it. Nothing is recomputed. +- **Note (to_dataframe schema, 4.0, [M-093] item 8b):** `to_dataframe(level='event_study')` returns `aggregate('event_study').to_dataframe()` - the unified `EVENT_STUDY_SCHEMA` frame on relative event time (`-1` the `is_reference` row, `0..` the post-treatment dates in order; `n` = units); `'periods'` (default) and `'att'` are unchanged. - **Note:** PH coefficient is the mean of usable ratios `sum alpha[t] D1[t]/D2[t]` (Theorem 1 direction, matching the authors' MATLAB/Stata code), not the paper's printed Equation 3.5 (which returns the reciprocal under exact PH) nor the @@ -7137,6 +7144,7 @@ where `D_it` is the treatment indicator (1 if unit `i` is treated by time `t`) a Replaces the aggregate spec with the per-event-time × ring decomposition from Butts Section 5 / Table 2. Direct effects `tau_k` and per-(ring, event-time) spillover effects `delta_jk` are emitted in `att_dynamic` and a MultiIndex `spillover_effects`. A TwoStageDiD-compatible `event_study_effects: Dict[int, Dict]` alias (mirroring `two_stage_aggregation.py::_stage2_event_study` row schema with `conf_int = (low, high)` tuple) is also emitted for `plot_event_study` consumption — `_extract_plot_data` reads the `reference_period` attribute, else the row's `is_reference` key (4.0, [M-093] item 8a; no count heuristic). `DiagnosticReport` routing is now wired (see the DiagnosticReport applicability Note below). - **Note (DiagnosticReport applicability):** `SpilloverDiDResults` is registered in `DiagnosticReport`'s `_APPLICABILITY` / `_PT_METHOD` tables with applicable checks {`parallel_trends` (method `event_study`, on the per-event-time DIRECT-effect dynamics — populated when `event_study=True`; Bonferroni fallback across pre-period coefs since there is no `event_study_vcov`), `design_effect` (instance-gated on `survey_metadata`), `heterogeneity` (reads `event_study_effects`)}. This mirrors the `TwoStageDiD` set **minus `bacon`**. `bacon` is **deliberately excluded**: SpilloverDiD identifies the direct effect off FAR-AWAY control observations (`d_it > d_bar`, Assumption 5), not off the TWFE 2×2 comparisons a Goodman-Bacon decomposition enumerates; running the Bacon decomposition on the raw binary treatment would ignore the ring/distance structure and pool spillover-contaminated in-ring units into the control group — the exact SUTVA violation this estimator handles (same rationale that excludes `bacon` for SyntheticControl / TROP / ContinuousDiD). `pretrends_power` / `sensitivity` / `epv` / `estimator_native` are not applicable (no `compute_pretrends_power` adapter, no HonestDiD adapter, no `epv_diagnostics`, not an SDiD/TROP/SCM native path). +- **Note (to_dataframe schema, 4.0, [M-093] item 8b):** `SpilloverDiDResults.to_dataframe(level=)` gained the uniform selector: `level='simple'` (default) is the inherited one-row headline frame; `level='event_study'` returns the unified `EVENT_STUDY_SCHEMA` frame over the per-event-time DIRECT effects (`build_event_study_surface(self).to_dataframe()`; the per-ring spillover dynamics stay on `spillover_effects`); an aggregate-mode fit (`event_study=False`) carries no event-study surface and raises `ValueError`. **Note (two-clock K_it):** Butts Section 5 uses one symbol `K_it`, but operationally there are TWO event-time clocks. The **direct-effect clock** is `K_direct_{it} = t - effective_first_treat(i)` for ever-treated unit rows (NaN for never-treated). The **spillover-exposure clock** is `K_spill_{it} = t - min{ effective_first_treat(j) : d(i, j) ≤ d_bar AND effective_first_treat(j) ≤ t }` — time since `i` first became exposed to ANY treated neighbor (running min across activated cohorts). `K_spill` is structurally non-negative (pre-trigger rows are NaN and contribute to stage 1 only); spillover placebos at `k < 0` are therefore NOT meaningful and rectangular emission of those cells uses NaN coef + `n_obs = 0`. The trigger cohort for unit `i` is the earliest activated cohort whose treated members fall within `d_bar` of `i` — NOT necessarily the geographically nearest cohort. diff --git a/docs/migration-4.0.md b/docs/migration-4.0.md index 003a24986..2012db323 100644 --- a/docs/migration-4.0.md +++ b/docs/migration-4.0.md @@ -38,7 +38,7 @@ updated, CI fails. | renames-level | Aggregation-level params and their accepted values unify on `level=`/`"event_study"` | 5 | §3 | | renames-post | The post-dummy `time=` becomes `post=` | 3 | §3 | | renames-robust-drop | `robust=` is dropped in favour of `vcov_type=` - read §3, the translation is per-estimator | 4 | §3 | -| results-contract | A legacy sentinel field is retired in favour of the unified event-study surface | 1 | §5 | +| results-contract | The zero-count reference sentinel is retired and every results class's `to_dataframe(level="event_study")` emits the unified `EVENT_STUDY_SCHEMA` ([M-093]) | 1 | §5 | ## The three merges @@ -225,15 +225,45 @@ SpilloverDiD, the CallawaySantAnna/DMLDiD universal-base carrier) marks it with `"is_reference": True` and carries no observation/group count there (3.x: `n_obs == 0` / `n_groups == 0`); SpilloverDiD's event-study `att_dynamic`/`spillover_effects` frames gain a boolean `is_reference` column -(`n_obs` NaN on those rows); the native `to_dataframe(level="event_study")` -frames of StackedDiD / bootstrapped ImputationDiD / TwoStageDiD show -`n_obs = NaN` on the reference row (the column is float) until the 4.0 schema -rewire replaces them. SunAbraham omits its reference row (`reference_period`) +(`n_obs` NaN on those rows); every `to_dataframe(level="event_study")` frame +is the unified schema (next paragraph), where the reference row is the +`is_reference` row. SunAbraham omits its reference row (`reference_period`) and EfficientDiD's PT-Post anchor is an estimated cell named by `reference_period` - both unchanged. Code that located the reference by a zero count should read the key, or the container's `is_reference` column. 3.x pickles are migrated on load. +**Event-study frames ([M-093], item 8b).** Since 4.0 every results class's +`to_dataframe(level="event_study")` emits the unified `EVENT_STUDY_SCHEMA` +(`event_time, att, se, t_stat, p_value, conf_int_lower, conf_int_upper, +cband_lower, cband_upper, n, df, is_reference, estimand`) - the same frame +`results.aggregate("event_study").to_dataframe()` returns (CallawaySantAnna/ +DMLDiD, EfficientDiD, ImputationDiD, TwoStageDiD, StackedDiD, ContinuousDiD, +TripleDifference staggered, ChaisemartinDHaultfoeuille, HAD event-study, +DurationDiD, LWDiD) or the builder frame (SunAbraham, LPDiD, WooldridgeDiD, +SpilloverDiD). What left the 3.x frames: `relative_period` is `event_time`; +`effect` / `att_glob` / LPDiD's `coefficient` are `att`; `conf_int_lo`/`hi`, +`conf_low`/`high` and HAD's `conf_int_low`/`high` are +`conf_int_lower`/`conf_int_upper`; `n_obs` is `n` (NaN on the reference row, +never a zero count); dCDH's `horizon` is `event_time` on the paper's +`l1_first_switch` convention, its per-horizon `estimand` labels +(`DID^pl_l` / `ref` / `DID_l`) are gone (`estimand` is `"att"`) and its +reference row is marked `is_reference`, not `n_obs 0`. WooldridgeDiD appends +`cohort_trends` and `aggregation_weights` after the schema and RAISES before +`aggregate("event_study", weights=)` (3.x returned an empty frame); LWDiD +appends its three configuration columns ([M-153]). CallawaySantAnna, +EfficientDiD, TripleDifference and analytical ImputationDiD / TwoStageDiD / +ContinuousDiD no longer raise "not stored on 4.0 fits" - the level recomputes +from the retained kit (bootstrapped CallawaySantAnna / EfficientDiD / +TripleDifference fits replay the fit-time bootstrap on every call, so cache +the frame); a kit-less pre-3.9 pickle, or a bootstrapped pickle predating the +replay state, that still carries a stored dict now raises where it returned a +frame (read `event_study_effects` directly). `level="group"` on those three +classes still raises with the post-fit pointer. `SpilloverDiDResults` gains +`to_dataframe(level="simple" | "event_study")` (aggregate-mode fits raise), +HAD event-study results gain `level="event_study"` and `DurationDiDResults` +gains `"event_study"`. + ```{warning} **Bootstrapped fits: every adopter has a post-fit route - two mechanisms.** On `CallawaySantAnna`, `DMLDiD` (which inherits the CS replay channel), and `EfficientDiD`, the post-fit recompute levels REPLAY the @@ -671,4 +701,4 @@ does not mean no action is required, so read the `Fix` cell. | M-046 | renames-robust-drop | `diff_diff:TripleDifference[robust]` | — | Drop it. `TripleDifference` accepts only `vcov_type="hc1"`, so `robust=` never changed its inference - do NOT write `vcov_type="classical"`, which raises. | | M-047 | renames-robust-drop | `diff_diff:HeterogeneousAdoptionDiD[robust]` | — | HAD's legacy default is non-robust: `robust=True` becomes `vcov_type="hc1"`; drop `robust=False`. | | M-115 | renames-robust-drop | `diff_diff:LinearRegression[robust]` | — | Drop `robust=True` (already the `hc1` default); `robust=False` becomes `vcov_type="classical"`. | -| M-093 | results-contract | `diff_diff:CallawaySantAnnaResults.event_study_effects` | `diff_diff:EventStudyResults` | The zero-count sentinel is retired (8a): read `is_reference` on the row, or the unified `EventStudyResults` surface; the per-class `to_dataframe(level='event_study')` schema lands in 8b. | +| M-093 | results-contract | `diff_diff:CallawaySantAnnaResults.event_study_effects` | `diff_diff:EventStudyResults` | Read `event_time`/`att`/`conf_int_lower`/`conf_int_upper`/`n`/`is_reference` from `to_dataframe(level='event_study')` - every class emits `EVENT_STUDY_SCHEMA` (WooldridgeDiD/LWDiD append their config columns after it); locate the reference row via `is_reference`, never a zero count; call `aggregate('event_study')` before WooldridgeDiD's frame. | diff --git a/docs/tutorials/14_continuous_did.ipynb b/docs/tutorials/14_continuous_did.ipynb index b63c85c1f..2ae8b78fa 100644 --- a/docs/tutorials/14_continuous_did.ipynb +++ b/docs/tutorials/14_continuous_did.ipynb @@ -646,7 +646,7 @@ "\n", "- **`level=\"dose_response\"`** (default): The dose-response curve with ATT(d) and ACRT(d)\n", "- **`level=\"group_time\"`**: Underlying group-time cell estimates\n", - "- **`level=\"event_study\"`**: Event study effects (legacy surface - populated only on bootstrapped fits since 4.0; prefer the post-fit `results.aggregate(\"event_study\")` container and its `to_dataframe()`)" + "- **`level=\"event_study\"`**: the unified event-study table (`EVENT_STUDY_SCHEMA`, identical to `results.aggregate(\"event_study\").to_dataframe()`; analytical fits recompute it from the retained kit, bootstrapped fits relay the fit-time percentile surface)" ] }, { diff --git a/docs/tutorials/16_wooldridge_etwfe.ipynb b/docs/tutorials/16_wooldridge_etwfe.ipynb index 857d904e0..fb54a1ced 100644 --- a/docs/tutorials/16_wooldridge_etwfe.ipynb +++ b/docs/tutorials/16_wooldridge_etwfe.ipynb @@ -39,10 +39,10 @@ "id": "b2c3d4e5", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:40.936018Z", - "iopub.status.busy": "2026-08-10T22:40:40.935762Z", - "iopub.status.idle": "2026-08-10T22:40:41.663217Z", - "shell.execute_reply": "2026-08-10T22:40:41.662670Z" + "iopub.execute_input": "2026-10-31T11:26:21.485342Z", + "iopub.status.busy": "2026-10-31T11:26:21.485042Z", + "iopub.status.idle": "2026-10-31T11:26:22.166096Z", + "shell.execute_reply": "2026-10-31T11:26:22.165648Z" } }, "outputs": [], @@ -79,10 +79,10 @@ "id": "d4e5f6a7", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.664382Z", - "iopub.status.busy": "2026-08-10T22:40:41.664290Z", - "iopub.status.idle": "2026-08-10T22:40:41.673784Z", - "shell.execute_reply": "2026-08-10T22:40:41.673478Z" + "iopub.execute_input": "2026-10-31T11:26:22.167500Z", + "iopub.status.busy": "2026-10-31T11:26:22.167414Z", + "iopub.status.idle": "2026-10-31T11:26:22.176504Z", + "shell.execute_reply": "2026-10-31T11:26:22.176163Z" } }, "outputs": [ @@ -234,10 +234,10 @@ "id": "f6a7b8c9", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.674678Z", - "iopub.status.busy": "2026-08-10T22:40:41.674622Z", - "iopub.status.idle": "2026-08-10T22:40:41.685853Z", - "shell.execute_reply": "2026-08-10T22:40:41.685484Z" + "iopub.execute_input": "2026-10-31T11:26:22.177632Z", + "iopub.status.busy": "2026-10-31T11:26:22.177569Z", + "iopub.status.idle": "2026-10-31T11:26:22.188567Z", + "shell.execute_reply": "2026-10-31T11:26:22.188100Z" } }, "outputs": [ @@ -259,7 +259,7 @@ "----------------------------------------------------------------------\n", "Parameter Estimate Std. Err. t-stat P>|t| [95% CI]\n", "----------------------------------------------------------------------\n", - "ATT (simple) 1.9817 0.0371 53.422 0.0000*** [1.9090, 2.0544]\n", + "ATT (simple) 1.9817 0.0371 53.422 0.0000*** [1.9087, 2.0547]\n", "======================================================================\n" ] } @@ -292,10 +292,10 @@ "id": "b8c9d0e1", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.686662Z", - "iopub.status.busy": "2026-08-10T22:40:41.686604Z", - "iopub.status.idle": "2026-08-10T22:40:41.688771Z", - "shell.execute_reply": "2026-08-10T22:40:41.688448Z" + "iopub.execute_input": "2026-10-31T11:26:22.189569Z", + "iopub.status.busy": "2026-10-31T11:26:22.189518Z", + "iopub.status.idle": "2026-10-31T11:26:22.191791Z", + "shell.execute_reply": "2026-10-31T11:26:22.191383Z" } }, "outputs": [ @@ -345,10 +345,10 @@ "id": "c9d0e1f2", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.689561Z", - "iopub.status.busy": "2026-08-10T22:40:41.689515Z", - "iopub.status.idle": "2026-08-10T22:40:41.703078Z", - "shell.execute_reply": "2026-08-10T22:40:41.702717Z" + "iopub.execute_input": "2026-10-31T11:26:22.192718Z", + "iopub.status.busy": "2026-10-31T11:26:22.192659Z", + "iopub.status.idle": "2026-10-31T11:26:22.205769Z", + "shell.execute_reply": "2026-10-31T11:26:22.205343Z" } }, "outputs": [ @@ -361,18 +361,18 @@ "Pre-treatment placebo ATT(g,t) cells (should be ~0 under parallel trends)\n", " cohort year | Coef. Std.Err. t P>|t|\n", "------------------------------------------------------------\n", - " 3 0 | 0.0568 0.1181 0.48 0.630\n", - " 3 1 | 0.1425 0.0996 1.43 0.152\n", + " 3 0 | 0.0568 0.1181 0.48 0.631\n", + " 3 1 | 0.1425 0.0996 1.43 0.153\n", " 5 0 | -0.0206 0.1175 -0.18 0.861\n", " 5 1 | 0.0423 0.1086 0.39 0.697\n", " 5 2 | -0.0191 0.1044 -0.18 0.855\n", - " 5 3 | -0.1359 0.1089 -1.25 0.212\n", - " 7 0 | -0.1344 0.1180 -1.14 0.255\n", + " 5 3 | -0.1359 0.1089 -1.25 0.213\n", + " 7 0 | -0.1344 0.1180 -1.14 0.256\n", " 7 1 | -0.0188 0.1135 -0.17 0.868\n", - " 7 2 | -0.2664 0.1118 -2.38 0.017\n", + " 7 2 | -0.2664 0.1118 -2.38 0.018\n", " 7 3 | -0.0605 0.1092 -0.55 0.580\n", - " 7 4 | -0.2214 0.1177 -1.88 0.060\n", - " 7 5 | -0.1768 0.1122 -1.57 0.115\n", + " 7 4 | -0.2214 0.1177 -1.88 0.061\n", + " 7 5 | -0.1768 0.1122 -1.57 0.116\n", "cohort 3: omitted reference cell -> [(np.int64(3), np.int64(2))]\n", "cohort 5: omitted reference cell -> [(np.int64(5), np.int64(4))]\n", "cohort 7: omitted reference cell -> [(np.int64(7), np.int64(6))]\n" @@ -432,10 +432,10 @@ "id": "e1f2a3b4", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.703912Z", - "iopub.status.busy": "2026-08-10T22:40:41.703849Z", - "iopub.status.idle": "2026-08-10T22:40:41.706510Z", - "shell.execute_reply": "2026-08-10T22:40:41.706235Z" + "iopub.execute_input": "2026-10-31T11:26:22.206765Z", + "iopub.status.busy": "2026-10-31T11:26:22.206706Z", + "iopub.status.idle": "2026-10-31T11:26:22.209756Z", + "shell.execute_reply": "2026-10-31T11:26:22.209348Z" } }, "outputs": [ @@ -443,23 +443,32 @@ "name": "stdout", "output_type": "stream", "text": [ - " relative_period att se t_stat p_value conf_int_lo \\\n", - "0 0 2.017643 0.048198 41.861506 4.889180e-295 1.923134 \n", - "1 1 1.961511 0.048986 40.041886 3.922971e-275 1.865456 \n", - "2 2 1.937067 0.050790 38.138931 1.473713e-254 1.837476 \n", - "3 3 2.033384 0.059205 34.344547 1.730538e-214 1.917291 \n", - "4 4 1.974940 0.064935 30.414086 9.171112e-175 1.847612 \n", - "5 5 1.961401 0.099388 19.734829 4.782277e-81 1.766516 \n", - "6 6 2.001811 0.083933 23.850075 3.696498e-114 1.837230 \n", + " event_time att se t_stat p_value conf_int_lower \\\n", + "0 0 2.017643 0.048198 41.861506 4.565337e-127 1.922793 \n", + "1 1 1.961511 0.048986 40.041886 3.626098e-122 1.865109 \n", + "2 2 1.937067 0.050790 38.138931 7.080743e-117 1.837116 \n", + "3 3 2.033384 0.059205 34.344547 8.607685e-106 1.916872 \n", + "4 4 1.974940 0.064935 30.414086 1.632053e-93 1.847152 \n", + "5 5 1.961401 0.099388 19.734829 4.325089e-56 1.765812 \n", + "6 6 2.001811 0.083933 23.850075 3.731356e-71 1.836636 \n", "\n", - " conf_int_hi cohort_trends aggregation_weights \n", - "0 2.112152 False cell \n", - "1 2.057567 False cell \n", - "2 2.036658 False cell \n", - "3 2.149477 False cell \n", - "4 2.102268 False cell \n", - "5 2.156285 False cell \n", - "6 2.166391 False cell \n" + " conf_int_upper cband_lower cband_upper n df is_reference estimand \\\n", + "0 2.112493 NaN NaN NaN NaN False att \n", + "1 2.057913 NaN NaN NaN NaN False att \n", + "2 2.037017 NaN NaN NaN NaN False att \n", + "3 2.149896 NaN NaN NaN NaN False att \n", + "4 2.102728 NaN NaN NaN NaN False att \n", + "5 2.156989 NaN NaN NaN NaN False att \n", + "6 2.166985 NaN NaN NaN NaN False att \n", + "\n", + " cohort_trends aggregation_weights \n", + "0 False cell \n", + "1 False cell \n", + "2 False cell \n", + "3 False cell \n", + "4 False cell \n", + "5 False cell \n", + "6 False cell \n" ] } ], @@ -474,10 +483,10 @@ "id": "f2a3b4c5", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.707347Z", - "iopub.status.busy": "2026-08-10T22:40:41.707293Z", - "iopub.status.idle": "2026-08-10T22:40:41.709711Z", - "shell.execute_reply": "2026-08-10T22:40:41.709337Z" + "iopub.execute_input": "2026-10-31T11:26:22.210645Z", + "iopub.status.busy": "2026-10-31T11:26:22.210588Z", + "iopub.status.idle": "2026-10-31T11:26:22.212953Z", + "shell.execute_reply": "2026-10-31T11:26:22.212568Z" } }, "outputs": [ @@ -486,14 +495,14 @@ "output_type": "stream", "text": [ " cohort att se t_stat p_value conf_int_lo \\\n", - "0 3 2.005216 0.050244 39.909748 1.073586e-273 1.906696 \n", - "1 5 1.999333 0.049288 40.564523 7.899715e-281 1.902687 \n", - "2 7 1.908365 0.055531 34.365894 1.039854e-214 1.799477 \n", + "0 3 2.005216 0.050244 39.909748 8.341377e-122 1.906340 \n", + "1 5 1.999333 0.049288 40.564523 1.369114e-123 1.902338 \n", + "2 7 1.908365 0.055531 34.365894 7.420858e-106 1.799084 \n", "\n", " conf_int_hi cohort_trends aggregation_weights \n", - "0 2.103737 False cell \n", - "1 2.095979 False cell \n", - "2 2.017253 False cell \n" + "0 2.104092 False cell \n", + "1 2.096328 False cell \n", + "2 2.017646 False cell \n" ] } ], @@ -508,10 +517,10 @@ "id": "a3b4c5d6", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.710497Z", - "iopub.status.busy": "2026-08-10T22:40:41.710446Z", - "iopub.status.idle": "2026-08-10T22:40:41.712041Z", - "shell.execute_reply": "2026-08-10T22:40:41.711741Z" + "iopub.execute_input": "2026-10-31T11:26:22.213771Z", + "iopub.status.busy": "2026-10-31T11:26:22.213722Z", + "iopub.status.idle": "2026-10-31T11:26:22.215145Z", + "shell.execute_reply": "2026-10-31T11:26:22.214809Z" } }, "outputs": [ @@ -533,7 +542,7 @@ "----------------------------------------------------------------------\n", "Parameter Estimate Std. Err. t-stat P>|t| [95% CI]\n", "----------------------------------------------------------------------\n", - "ATT (simple) 1.9817 0.0371 53.422 0.0000*** [1.9090, 2.0544]\n", + "ATT (simple) 1.9817 0.0371 53.422 0.0000*** [1.9087, 2.0547]\n", "======================================================================\n" ] } @@ -549,16 +558,16 @@ "id": "b4c5d6e7", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.712850Z", - "iopub.status.busy": "2026-08-10T22:40:41.712806Z", - "iopub.status.idle": "2026-08-10T22:40:41.777050Z", - "shell.execute_reply": "2026-08-10T22:40:41.776659Z" + "iopub.execute_input": "2026-10-31T11:26:22.215939Z", + "iopub.status.busy": "2026-10-31T11:26:22.215888Z", + "iopub.status.idle": "2026-10-31T11:26:22.275272Z", + "shell.execute_reply": "2026-10-31T11:26:22.274936Z" } }, "outputs": [ { "data": { - "image/png": "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INDz4QEDYt/RRx9tmrHokkT6u6nrXWrp34UXXhjtXcNW0L+bWhavHar17+hTTz0lS5YsicvfTdquYZtphuDhhx826+dpO9quXbua9UY0g4D4ph889IT49Morr5jlFZ599llzCvbJJ5+QCYozOpfrjjvuMJnZ3377TXr27Gnee/fZZ59o7xq2kWYKeK+NXzvuuKMJDP7+97/LI488YsrktSz3+OOPj/auYSscfvjhUlpaaj7Lrly5Unr16mWOa48ePSTeJNRRDA4AAAAAnkLpJgAAAAB4DIEeAAAAAHgMgR4AAAAAeAyBHgAAAAB4DIEeAAAAAHgMgR4AAAAAeAyBHgAAAAB4DIEeACBqFi1aZBamRXRxHADAewj0AACycOFC6du3b8hpwIABcvDBB8sdd9whPp8volepsLBQjjzyyIiuu2rVKhk9erSUlZWZy2+99ZYcdthhcXE0tnVfzz77bJk8eXKT13n33XdlwoQJrfJ4LTkObluyZImceeaZUltba+XxAKCtSo72DgAAYsd7770nO+20kzlfWVkpn376qVx++eXStWtXOeecc1r1saqrq0MCyKOPPtqc4oHb+7px40a5++675cUXXxS3hR8Ht/Xu3Vt22WUXefbZZ03ABwBwBxk9AECD0tLS5NBDD5VRo0bJ+++/H9g+d+5c+cMf/iC77767+dmrr77a4O01Y/Pggw/KscceK3vssYcMGTJExo0bJ+Xl5SaIHD58uLne0KFDTfbKn7WqqamR/fffX15//fWQ+9Pra8awJfug9H5OPfVUuf3222WvvfYyWcqnnnoq5DpN3d8rr7wip5xyipx33nkyePBgee2117bIsM2ePVsKCgrM8xwxYkRgP/3efvttOeKII8zPr732WvP8m6L7t99++0mnTp0afF01+D7qqKNk3bp1si0aOg6tYerUqTJy5EjZe++95bLLLpOrrrpKbrvttsDPx4wZIw899JBUVVW1yuMBALZEoAcAaFJdXZ0kJSUFyvzOP/98EzhpcKOlhXfddZdMmzZti9u98cYb8sILL8i9994rc+bMMUHfjBkzTOCkQeT06dPN9T7++OOQcs/k5GQTFGl20e+7774zj63bW7IPft9884389ttv5jE14Js0aZK88847ET+nr7/+WoYNG2Zur8FvsO+//97c/sQTT5RPPvnEBHITJ06UDz74ILDvGuj85S9/kVmzZsl2220n8+bNa/L1fu6550wg1xDdP30+jz32mHTs2HGLn2tJaHgZrv+0cuXKkOs2dRy21oIFC+SKK66QP//5z+Z49+nTx/xfCKbbsrOzQ44xAKB1EegBABpUUVFhgh3NRvkDgGeeeUb23Xdfk73Kysoy5zU7E54hUyeccIJ8+OGH0qtXL0lNTZV99tlHBg0aJGvWrGn2FT/mmGNM4OGfN6aZJs045ebmtmgf/PTxb775ZhMYHXjggSZDp0FopM9JA93TTjtNOnToYPYhmJYg5ufny+mnn26CF8306XWnTJlifv7SSy+ZLKKWeubk5JgM14477tjkfEnN1GkGNNzf//53E5hpkLf99ts3ePtLL71UfvjhhwZPjd2mNWmQqq+BZnL1+WrAp+Wa4fR11sAXAOAO5ugBAAI0YxasR48e8te//tUEMerHH380wZtmh4LtsMMODb6Kv/zyiynj+9///mcCDQ1itHyyORoUakA1c+ZME2TqfWiAtDX7oHbeeWdp37594HK/fv3kzTffjPj+unTpYjKNDfn5559NoBds4MCB8vLLL5vzP/30kylh9EtMTDSP3xjdH836paenh2zXjKQGjxqs6f7YohnC+++/v8GfffTRR1sEj3qcDznkkJBtu+222xa31bmg/qwqAKD1EegBALZoxrJixQq5+uqrTUChDTMSEhLMz7Vph16+/vrrm33VNFtz8cUXm+yOzm076aST5L777ovo1dZgSEsXdX/y8vJk9erVgblkLdmHwB+7sCBN57n5t0Vyf5oRbExKSkqD5ZdNdZVsLGhUmzZtkoyMjAZv88gjj8h1111nAq+//e1vrRKYNUczhHqKlGY/I+momZmZaZ4rAMAdlG4CALbQrVs3efjhh012SYM1f9OMnj17mvl2kdCSSG1sosGdLiegmbyioqLAz/3BY2O01FGDRc3m6fw4Lats6T4EZxa1FDV4Hpk/q7Y19xds1113NfP0gull7SyptGxRHy98iYHGaHloSUnJFts16NbM4f/93//Jk08+2eh9tLR0s7nj0FL6fBcvXhyyTR87XHFxsXmuAAB3EOgBABrNuPzjH/+QL7/8Uu655x6z7YwzzpClS5fKP//5T/NBXYMNbWKi2b9wOj9LA0XNxumcM20iooGedt1U/qyVNghpaA037YCp5ZZPP/10yFIGLdkHP80c6XqAumyBZrW0rNLf2n9r7i/YWWedZZq46Fw9DdC0Ccvzzz8fuH9/kxdtQqP7oVk5LfdsjAaIa9eubTDY85fXamnrTTfdJK2huePQUlrmq6+BdibV5/Cf//zHLMgeHlAuW7bMlNQCANxBoAcAaJRmu8aPHy9PPPGEmcemZZSa6dNg6YADDjCZOp3b5l/YO5guAaDB3uGHH27KNnXOma7FN3/+fPNzbYyiDTu0ccl///vfBh9fM4IaIATP+WrJPvjpOoBaUqhB0i233GKuq81dtvb+wl8jLZXUJiS6LIR27NQy0OOOO878XO/rgQceMPPrtBHMV199tUXnzmD9+/c32bumOnNq+abejy71sK0iOQ4toZlS7TqqXw5oJlaDfc1Ehpe46pIW4XP5AACtJ6FOJxIAAOBRuo6eZuv8yx3EA51nt3z5chM0esHJJ59sgnYNov2lrRdddJHp6hredAYA0DrI6AEAEGPGjh0rn3/+eURLUcQazY5qqa0Gqrogu2YddY6ev5mOf/6mLkBPkAcA7iHQAwAgxmjJq3bVjLRLaSzRNQj32GMPk8XTuYTaOObBBx+U7t27m59roxZdbkOvBwBwD6WbAAAAAOAxZPQAAAAAwGMI9AAAAADAYwj0AAAAAMBjCPQAAAAAwGMI9AAAAADAYwj0AAAAAMBjCPQAAAAAwGMI9AAAAADAYwj0AAAAAEC85f8B7FGT7jTSZS0AAAAASUVORK5CYII=", + "image/png": "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Hv/333+XN954wySipk+fHva7esyLFi1q9r7ff/99ueSSS8z96BJzn376qRxxxBHy8MMPm5+npqbKjz/+KE888YQkfEZPI2WXRrBvv/22OXEffvih9OrVS0444QQ59thjTdSMJugahLvtJvLxxyK5ubxEAAAAndD7i1bIv2csCN6e+Pgc6dklU84bva3sNXRTzx53zz33bLaxoma1NDhSzz//vGmuqEmeUJqxe+SRR0xSR7Nx6r333pPjjjvOZPUqKiokOzvbbNegSLNfrt13391k99p7fE2prKyUW265RZ566qmwdb818NIYZdiwYcHtGkSefvrpJiMZGtO05Oabb5YxY8bItddeKynr18HWwPbiiy+Wgw8+WHr37i3nnXeeCf4OP/xwyY3Dz/dRZ/Tc9Oydd95pUrb6JPUk5+TkmJOrt/v37+/dkSY6/YOYMsUZAQAA0CmDvOuemiur14U3Zlm1rsps15/Hq1133dWMX375pRlramrkk08+MaWLPXv2lI8++ii4XffRwM1LTz/9tGy99day6aZOcFxbW2vKMc8///ywIE/ttNNOJtDTOKa+vl6ioWWfmpEMpZlBDe7cCkZ93vpY8ZrVizqjp2WZL774omy//fYmIteoX2tVtWQzXutS40pqqsjo0bE+CgAAAHQgLResrg2Y64FAQGrq6qWqpk5SUhrC9gvUN4Rl8ppy14yFstOWPSUlueXP1hlpKW3+/K1z1jTrFikyc9ecrKws2XHHHc1cPQ36tByzR48essUWW5igTrN7mgz64osvTAZMgyuXBoH62oTSakCtCmzv8b388sty6KGHBm9rGabOqdtjjz2a3P+AAw6Qf//73/Ldd9/JwIEDW32+p5xyikydOlWKi4vNXD59zttss438+c9/bhT8aVaxLdnIuAv09ARpkKflmfvvv78J8tAGOsFUJ3tqLXATcx0BAACQeEHehAdmy8LiteE/eP3Ndt2fZvbG3vJ6q/sN699NbhtfYD3ZouWbn332mbmugZ1bnqmB3m233WauawC42267hTVH6Wg6jWz+/PlmDl1oBk5169atyd/ptn776tWrowr0zj33XFOKqoGrzlm88cYbTWCqgZ4Gqa5tt91WFi5cGFbSmnCBnqZHX3jhBVP7evXVV5sTqvWokdF5W+nETp3UeP3117e434oVK8wkSu0KNGjQIDnnnHMS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cvTRRze7/2WXXWYCw/z8/OC2iy66SPbYYw9JCFVVIldcITJ5skhmZqyPBgAAAICPxSTQmzdvntx5551SVFQkgwcPlg8++EDOO+882XXXXaVv376N9g8EArJ48WJ5/PHHZdttt43FIQMAAABAwojJHL0XXnhB9t13XxPkqT333FOGDRsmr776apP7f/fdd9LQ0CADBgyQJUuWSGlpqSQczeLpOnpk8wAAAAD4MdD75ptvZNCgQWHbttpqKxPENWXBggWSkpJiSjsvueQSU655ww03mOAvYVRWipx5pjMCAAAAgN9KN9etW9eo+UpOTo4Ua1fKJpSXl5us3+TJkyUvL88EhCeccILJCBYWFjb5O1ruqZe40dAgSX37OsFpPB1XB3Nf87h67dEunEt/4Xz6C+fTPziX/sL59I9AnH6mbcvxJDXEIC02duxYGTNmjJypGa71rr/+elm1apX885//jOo+tHnLypUr5d577w3bXlFRIYsWLerwYwYAAACAeDB06FDJzs6Ov4yedtpcvXp12DYN8nr27Nnk/prB026bvXv3Dm7r0qWLfP/9980+hmb7WnvyVlVUSPLpp0v9/feLxNNxefAtw/z582X48OGm3BaJi3PpL5xPf+F8+gfn0l84n/4RiNPPtJrUam66W1wEeiNGjJDXXnsteLuurk7mzp0rf/vb35rc/5577pGqqiqZNm1acNvHH38su+22W7OPoScknk6KpKeL7LGHpOgYT8flkbh7/dFunEt/4Xz6C+fTPziX/sL59I+UOPtM25ZjiUkzlmOPPdYslD5x4kSZNWuWXHzxxSZjd/DBB5uf19TUyOeffy5lZWXm9hlnnCHvvvuuTJ061SzFoGvqadnmH//4R0kYGRkiEyY4IwAAAAB4KCaBXvfu3eWxxx6T2tpas3C6lmQ+8MADkprqJBjXrFljGq/osgpKl1544oknTLOWu+++W7KysuTJJ58095MwystFRo92RgAAAADwUExKN9WWW24pN998c5M/69OnjwnkQmmw19z+CSEtTUQ7hOoIAAAAAH4M9DodnZsX0mUUAAAAAHxVutkpaclmQQGlmwAAAAA8R6BnM6OnzVh0BAAAAAAPUbppe44eAAAAAHiMjJ4tulTEsGHOCAAAAAAeItCzJTNTZMoUZwQAAAAAD1G6aYuuEajr6AEAAACAx8jo2bJunUi/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li5dmpB/m7Rdw0bTDME999xj1s/TdrS9e/c2641oBgGJTT946AWJ6ZlnnjHLKzz22GPmEuqDDz4gE5RgdC7XTTfdZDKzK1askK222sq89+6yyy6xPjRsJM0U8F6buDbbbDMTGNx6661y7733mjJ5Lcs96qijYn1oaIeDDjpIysvLzWfZlStXysCBA815HTBggCSapAaKwQEAAADAVyjdBAAAAACfIdADAAAAAJ8h0AMAAAAAnyHQAwAAAACfIdADAAAAAJ8h0AMAAAAAnyHQAwAAAACfIdADAMTM119/bRamRWxxHgDAfwj0AACyaNEiGTJkSNhl2LBhss8++8hNN90kgUAgqlepqKhIDjnkkKj2/eWXX2Ts2LFSUVFhbr/88stywAEHJMTZ2NhjPe2002Tq1Kkt7vPaa6/JpEmTOuTx2nIevLZ06VI55ZRTpL6+3srjAUBnlRrrAwAAxI/XX39dNt98c3O9urpaPvzwQ7n44ould+/ecvrpp3foY9XW1oYFkIcddpi5JAKvj7WkpERuueUWefLJJ8VrkefBa4MGDZItt9xSHnvsMRPwAQC8QUYPANCkjIwM2X///WXMmDHyxhtvBLd/+umn8oc//EG2335787Nnn322yd/XjM1dd90lRxxxhOywww4ycuRImTBhglRWVpog8sADDzT77bXXXiZ75Wat6urqZPfdd5fnn38+7P50f80YtuUYlN7PCSecIDfeeKPstNNOJkv58MMPh+3T0v0988wzcvzxx8uZZ54pI0aMkOeee65Rhm327NlSWFhonueoUaOCx+l65ZVX5OCDDzY/v+KKK8zzb4ke32677SY9evRo8nXV4PvQQw+VNWvWyMZo6jx0hBkzZsjo0aNl5513lgsvvFAuvfRSueGGG4I/HzdunNx9991SU1PTIY8HAGiMQA8A0KKGhgZJSUkJlvmdddZZJnDS4EZLC//xj3/Im2++2ej3XnjhBXniiSfk9ttvlzlz5pig7+233zaBkwaRM2fONPu9//77YeWeqampJijS7KJr/vz55rF1e1uOwfXFF1/IihUrzGNqwDdlyhR59dVXo35On3/+uey7777m9zX4DfXVV1+Z3z/mmGPkgw8+MIHc5MmT5a233goeuwY6//d//yfvvfeebLLJJjJ37twWX+/HH3/cBHJN0ePT53P//fdL9+7dG/1cS0Ijy3Ddy8qVK8P2bek8tNfChQvlkksukT//+c/mfA8ePNj8vxBKt+Xm5oadYwBAxyLQAwA0qaqqygQ7mo1yA4BHH31Udt11V5O9ysnJMdc1OxOZIVNHH320vPPOOzJw4EBJT0+XXXbZRYYPHy6rVq1q9RU//PDDTeDhzhvTTJNmnPLz89t0DC59/GuvvdYERnvuuafJ0GkQGu1z0kD3xBNPlG7dupljCKUliAUFBXLSSSeZ4EUzfbrv9OnTzc+feuopk0XUUs+8vDyT4dpss81anC+pmTrNgEa69dZbTWCmQV6fPn2a/P0LLrhAFi9e3OSlud/pSBqk6mugmVx9vhrwablmJH2dNfAFAHiDOXoAgCDNmIUaMGCAXHTRRSaIUd98840J3jQ7FGrTTTdt8lX84YcfTBnf999/bwINDWK0fLI1GhRqQDVr1iwTZOp9aIDUnmNQW2yxhXTt2jV4e5tttpGXXnop6vvr1auXyTQ25bvvvjOBXqjttttOnn76aXP922+/NSWMruTkZPP4zdHj0axfZmZm2HbNSGrwqMGaHo8tmiG88847m/zZu+++2yh41PO83377hW3bdtttG/2uzgV1s6oAgI5HoAcAaNSM5eeff5a//vWvJqDQhhlJSUnm59q0Q29fddVVrb5qmq0577zzTHZH57Yde+yx8q9//SuqV1uDIS1d1OPp16+f/Pbbb8G5ZG05huA/dhFBms5zc7dFc3+aEWxOWlpak+WXLXWVbC5oVOvWrZOsrKwmf+fee++VK6+80gRel112WYcEZq3RDKFeoqXZz2g6amZnZ5vnCgDwBqWbAIBG+vbtK/fcc4/JLmmw5jbN2Gqrrcx8u2hoSaQ2NtHgTpcT0ExecXFx8Odu8NgcLXXUYFGzeTo/Tssq23oMoZlFLUUNnUfmZtXac3+htt56azNPL5Te1s6SSssW9fEilxhojpaHlpWVNdquQbdmDv/2t7/JQw891Ox9tLV0s7Xz0Fb6fJcsWRK2TR87UmlpqXmuAABvEOgBAJrNuPzzn/+Uzz77TG677Taz7eSTT5Zly5bJHXfcYT6oa7ChTUw0+xdJ52dpoKjZOJ1zpk1ENNDTrpvKzVppg5Cm1nDTDphabvnII4+ELWXQlmNwaeZI1wPUZQs0q6VllW5r//bcX6hTTz3VNHHRuXoaoGkTlv/973/B+3ebvGgTGj0OzcppuWdzNEBcvXp1k8GeW16rpa3XXHONdITWzkNbaZmvvgbamVSfw3/+8x+zIHtkQPnjjz+akloAgDcI9AAAzdJs18SJE+XBBx8089i0jFIzfRos7bHHHiZTp3Pb3IW9Q+kSABrsHXTQQaZsU+ec6Vp8CxYsMD/XxijasEMbl7z44otNPr5mBDVACJ3z1ZZjcOk6gFpSqEHSddddZ/bV5i7tvb/I10hLJbUJiS4LoR07tQz0yCOPND/X+5o2bZqZX6eNYObNm9eoc2eooUOHmuxdS505tXxT70eXethY0ZyHttBMqXYd1S8HNBOrwb5mIiNLXHVJi8i5fACAjpPUoBMJAADwKV1HT7N17nIHiUDn2S1fvtwEjX5w3HHHmaBdg2i3tPXcc881XV0jm84AADoGGT0AAOLM+PHj5eOPP45qKYp4o9lRLbXVQFUXZNeso87Rc5vpuPM3dQF6gjwA8A6BHgAAcUZLXrWrZrRdSuOJrkG4ww47mCyeziXUxjF33XWX9O/f3/xcG7Xochu6HwDAO5RuAgAAAIDPkNEDAAAAAJ8h0AMAAAAAnyHQAwAAAACfIdADAAAAAJ8h0AMAAAAAnyHQAwAAAACfIdADAAAAAJ8h0AMAAAAAnyHQAwAAAADxl/8Hu6GjrLv4RMcAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -615,10 +624,10 @@ "id": "d6e7f8a9", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.777983Z", - "iopub.status.busy": "2026-08-10T22:40:41.777921Z", - "iopub.status.idle": "2026-08-10T22:40:41.787366Z", - "shell.execute_reply": "2026-08-10T22:40:41.786896Z" + "iopub.execute_input": "2026-10-31T11:26:22.276413Z", + "iopub.status.busy": "2026-10-31T11:26:22.276357Z", + "iopub.status.idle": "2026-10-31T11:26:22.285386Z", + "shell.execute_reply": "2026-10-31T11:26:22.285007Z" } }, "outputs": [ @@ -663,10 +672,10 @@ "id": "e7f8a9b0", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.788217Z", - "iopub.status.busy": "2026-08-10T22:40:41.788162Z", - "iopub.status.idle": "2026-08-10T22:40:41.790317Z", - "shell.execute_reply": "2026-08-10T22:40:41.790001Z" + "iopub.execute_input": "2026-10-31T11:26:22.286350Z", + "iopub.status.busy": "2026-10-31T11:26:22.286286Z", + "iopub.status.idle": "2026-10-31T11:26:22.288559Z", + "shell.execute_reply": "2026-10-31T11:26:22.288131Z" } }, "outputs": [ @@ -716,10 +725,10 @@ "id": "f8a9b0c1", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.791073Z", - "iopub.status.busy": "2026-08-10T22:40:41.791011Z", - "iopub.status.idle": "2026-08-10T22:40:41.794505Z", - "shell.execute_reply": "2026-08-10T22:40:41.794131Z" + "iopub.execute_input": "2026-10-31T11:26:22.289426Z", + "iopub.status.busy": "2026-10-31T11:26:22.289368Z", + "iopub.status.idle": "2026-10-31T11:26:22.292909Z", + "shell.execute_reply": "2026-10-31T11:26:22.292595Z" } }, "outputs": [ @@ -727,23 +736,32 @@ "name": "stdout", "output_type": "stream", "text": [ - " relative_period att se t_stat p_value \\\n", - "0 0 209.853525 9.262249 22.656865 1.193811e-113 \n", - "1 1 217.094215 11.301395 19.209506 3.081876e-82 \n", - "2 2 211.236155 10.015702 21.090498 9.723169e-99 \n", - "3 3 234.127753 12.505668 18.721732 3.292711e-78 \n", - "4 4 231.207818 12.990070 17.798813 7.218311e-71 \n", - "5 5 238.130506 19.910574 11.960002 5.756036e-33 \n", - "6 6 265.622807 26.491724 10.026634 1.164183e-23 \n", + " event_time att se t_stat p_value \\\n", + "0 0 209.853525 9.262249 22.656865 1.193811e-113 \n", + "1 1 217.094215 11.301395 19.209506 3.081876e-82 \n", + "2 2 211.236155 10.015702 21.090498 9.723169e-99 \n", + "3 3 234.127753 12.505668 18.721732 3.292711e-78 \n", + "4 4 231.207818 12.990070 17.798813 7.218311e-71 \n", + "5 5 238.130506 19.910574 11.960002 5.756036e-33 \n", + "6 6 265.622807 26.491724 10.026634 1.164183e-23 \n", + "\n", + " conf_int_lower conf_int_upper cband_lower cband_upper n df \\\n", + "0 191.699851 228.007200 NaN NaN NaN NaN \n", + "1 194.943888 239.244543 NaN NaN NaN NaN \n", + "2 191.605739 230.866571 NaN NaN NaN NaN \n", + "3 209.617095 258.638412 NaN NaN NaN NaN \n", + "4 205.747749 256.667887 NaN NaN NaN NaN \n", + "5 199.106499 277.154513 NaN NaN NaN NaN \n", + "6 213.699982 317.545631 NaN NaN NaN NaN \n", "\n", - " conf_int_lo conf_int_hi cohort_trends aggregation_weights \n", - "0 191.699851 228.007200 False cell \n", - "1 194.943888 239.244543 False cell \n", - "2 191.605739 230.866571 False cell \n", - "3 209.617095 258.638412 False cell \n", - "4 205.747749 256.667887 False cell \n", - "5 199.106499 277.154513 False cell \n", - "6 213.699982 317.545631 False cell \n", + " is_reference estimand cohort_trends aggregation_weights \n", + "0 False att False cell \n", + "1 False att False cell \n", + "2 False att False cell \n", + "3 False att False cell \n", + "4 False att False cell \n", + "5 False att False cell \n", + "6 False att False cell \n", " cohort att se t_stat p_value conf_int_lo \\\n", "0 3 238.051549 17.463438 13.631425 2.604447e-42 203.823839 \n", "1 5 220.316861 13.840221 15.918594 4.708403e-57 193.190525 \n", @@ -783,10 +801,10 @@ "id": "b0c1d2e3", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.795335Z", - "iopub.status.busy": "2026-08-10T22:40:41.795268Z", - "iopub.status.idle": "2026-08-10T22:40:41.804905Z", - "shell.execute_reply": "2026-08-10T22:40:41.804631Z" + "iopub.execute_input": "2026-10-31T11:26:22.293948Z", + "iopub.status.busy": "2026-10-31T11:26:22.293877Z", + "iopub.status.idle": "2026-10-31T11:26:22.303549Z", + "shell.execute_reply": "2026-10-31T11:26:22.303164Z" } }, "outputs": [ @@ -835,10 +853,10 @@ "id": "c1d2e3f4", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.805864Z", - "iopub.status.busy": "2026-08-10T22:40:41.805807Z", - "iopub.status.idle": "2026-08-10T22:40:41.808291Z", - "shell.execute_reply": "2026-08-10T22:40:41.807940Z" + "iopub.execute_input": "2026-10-31T11:26:22.304575Z", + "iopub.status.busy": "2026-10-31T11:26:22.304518Z", + "iopub.status.idle": "2026-10-31T11:26:22.306888Z", + "shell.execute_reply": "2026-10-31T11:26:22.306519Z" } }, "outputs": [ @@ -880,10 +898,10 @@ "id": "e3f4a5b6", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.809138Z", - "iopub.status.busy": "2026-08-10T22:40:41.809081Z", - "iopub.status.idle": "2026-08-10T22:40:41.813270Z", - "shell.execute_reply": "2026-08-10T22:40:41.812913Z" + "iopub.execute_input": "2026-10-31T11:26:22.307865Z", + "iopub.status.busy": "2026-10-31T11:26:22.307802Z", + "iopub.status.idle": "2026-10-31T11:26:22.312062Z", + "shell.execute_reply": "2026-10-31T11:26:22.311683Z" } }, "outputs": [ @@ -910,10 +928,10 @@ "id": "f4a5b6c7", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.814045Z", - "iopub.status.busy": "2026-08-10T22:40:41.813994Z", - "iopub.status.idle": "2026-08-10T22:40:41.822663Z", - "shell.execute_reply": "2026-08-10T22:40:41.822356Z" + "iopub.execute_input": "2026-10-31T11:26:22.312890Z", + "iopub.status.busy": "2026-10-31T11:26:22.312835Z", + "iopub.status.idle": "2026-10-31T11:26:22.321716Z", + "shell.execute_reply": "2026-10-31T11:26:22.321291Z" } }, "outputs": [ @@ -921,17 +939,23 @@ "name": "stdout", "output_type": "stream", "text": [ - " relative_period att se t_stat p_value conf_int_lo \\\n", - "0 0 -0.031067 0.013621 -2.280835 0.022664 -0.057780 \n", - "1 1 -0.052235 0.018873 -2.767725 0.005697 -0.089247 \n", - "2 2 -0.136078 0.035455 -3.837999 0.000128 -0.205612 \n", - "3 3 -0.104707 0.033874 -3.091059 0.002022 -0.171140 \n", + " event_time att se t_stat p_value conf_int_lower \\\n", + "0 0 -0.031067 0.013621 -2.280835 0.022979 -0.057828 \n", + "1 1 -0.052235 0.018873 -2.767725 0.005855 -0.089315 \n", + "2 2 -0.136078 0.035455 -3.837999 0.000140 -0.205739 \n", + "3 3 -0.104707 0.033874 -3.091059 0.002106 -0.171261 \n", "\n", - " conf_int_hi cohort_trends aggregation_weights \n", - "0 -0.004354 False cell \n", - "1 -0.015222 False cell \n", - "2 -0.066544 False cell \n", - "3 -0.038275 False cell \n" + " conf_int_upper cband_lower cband_upper n df is_reference estimand \\\n", + "0 -0.004306 NaN NaN NaN NaN False att \n", + "1 -0.015155 NaN NaN NaN NaN False att \n", + "2 -0.066418 NaN NaN NaN NaN False att \n", + "3 -0.038154 NaN NaN NaN NaN False att \n", + "\n", + " cohort_trends aggregation_weights \n", + "0 False cell \n", + "1 False cell \n", + "2 False cell \n", + "3 False cell \n" ] } ], @@ -949,10 +973,10 @@ "id": "a5b6c7d8", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.823490Z", - "iopub.status.busy": "2026-08-10T22:40:41.823428Z", - "iopub.status.idle": "2026-08-10T22:40:41.825214Z", - "shell.execute_reply": "2026-08-10T22:40:41.824900Z" + "iopub.execute_input": "2026-10-31T11:26:22.322707Z", + "iopub.status.busy": "2026-10-31T11:26:22.322655Z", + "iopub.status.idle": "2026-10-31T11:26:22.324630Z", + "shell.execute_reply": "2026-10-31T11:26:22.324223Z" } }, "outputs": [ @@ -969,7 +993,7 @@ " 2004 2007 | -0.1047 0.0339 -3.09 0.002\n", " 2006 2006 | 0.0025 0.0199 0.13 0.900\n", " 2006 2007 | -0.0392 0.0240 -1.63 0.103\n", - " 2007 2007 | -0.0431 0.0184 -2.34 0.019\n" + " 2007 2007 | -0.0431 0.0184 -2.34 0.020\n" ] } ], @@ -993,10 +1017,10 @@ "id": "b6c7d8e9", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.826018Z", - "iopub.status.busy": "2026-08-10T22:40:41.825971Z", - "iopub.status.idle": "2026-08-10T22:40:41.834096Z", - "shell.execute_reply": "2026-08-10T22:40:41.833755Z" + "iopub.execute_input": "2026-10-31T11:26:22.325457Z", + "iopub.status.busy": "2026-10-31T11:26:22.325401Z", + "iopub.status.idle": "2026-10-31T11:26:22.333588Z", + "shell.execute_reply": "2026-10-31T11:26:22.333209Z" } }, "outputs": [ @@ -1004,17 +1028,23 @@ "name": "stdout", "output_type": "stream", "text": [ - " relative_period att se t_stat p_value conf_int_lo \\\n", - "0 0 -25.572049 21.809918 -1.172496 0.240998 -68.318703 \n", - "1 1 0.873630 53.217084 0.016416 0.986902 -103.429938 \n", - "2 2 -76.746573 28.036277 -2.737402 0.006193 -131.696667 \n", - "3 3 -102.357517 41.590684 -2.461068 0.013852 -183.873759 \n", + " event_time att se t_stat p_value conf_int_lower \\\n", + "0 0 -25.572049 21.809918 -1.172496 0.240998 -68.318703 \n", + "1 1 0.873630 53.217084 0.016416 0.986902 -103.429938 \n", + "2 2 -76.746573 28.036277 -2.737402 0.006193 -131.696667 \n", + "3 3 -102.357517 41.590684 -2.461068 0.013852 -183.873759 \n", "\n", - " conf_int_hi cohort_trends aggregation_weights \n", - "0 17.174605 False cell \n", - "1 105.177198 False cell \n", - "2 -21.796478 False cell \n", - "3 -20.841275 False cell \n", + " conf_int_upper cband_lower cband_upper n df is_reference estimand \\\n", + "0 17.174605 NaN NaN NaN NaN False att \n", + "1 105.177198 NaN NaN NaN NaN False att \n", + "2 -21.796478 NaN NaN NaN NaN False att \n", + "3 -20.841275 NaN NaN NaN NaN False att \n", + "\n", + " cohort_trends aggregation_weights \n", + "0 False cell \n", + "1 False cell \n", + "2 False cell \n", + "3 False cell \n", " cohort att se t_stat p_value conf_int_lo conf_int_hi \\\n", "0 2004 -56.868740 24.371745 -2.333388 0.019628 -104.636481 -9.100998 \n", "1 2006 58.439881 72.503696 0.806026 0.420228 -83.664751 200.544514 \n", @@ -1080,10 +1110,10 @@ "id": "d8e9f0a1", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.834916Z", - "iopub.status.busy": "2026-08-10T22:40:41.834858Z", - "iopub.status.idle": "2026-08-10T22:40:41.848112Z", - "shell.execute_reply": "2026-08-10T22:40:41.847749Z" + "iopub.execute_input": "2026-10-31T11:26:22.334526Z", + "iopub.status.busy": "2026-10-31T11:26:22.334471Z", + "iopub.status.idle": "2026-10-31T11:26:22.347223Z", + "shell.execute_reply": "2026-10-31T11:26:22.346859Z" } }, "outputs": [ @@ -1095,7 +1125,7 @@ "============================================================\n", "Estimator ATT SE 95% CI\n", "------------------------------------------------------------\n", - "WooldridgeDiD (ETWFE) 1.9817 0.0371 [1.909, 2.054]\n", + "WooldridgeDiD (ETWFE) 1.9817 0.0371 [1.909, 2.055]\n", "CallawaySantAnna 1.9754 0.0528 [1.872, 2.079]\n" ] } @@ -1127,16 +1157,16 @@ "id": "e9f0a1b2", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.849018Z", - "iopub.status.busy": "2026-08-10T22:40:41.848960Z", - "iopub.status.idle": "2026-08-10T22:40:41.916201Z", - "shell.execute_reply": "2026-08-10T22:40:41.915933Z" + "iopub.execute_input": "2026-10-31T11:26:22.348239Z", + "iopub.status.busy": "2026-10-31T11:26:22.348178Z", + "iopub.status.idle": "2026-10-31T11:26:22.400865Z", + "shell.execute_reply": "2026-10-31T11:26:22.400563Z" } }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA94AAAHqCAYAAADyGZa5AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAAkJFJREFUeJzt3QeYVPXVx/Hf9r5LXdoCUgURBFSKvWDDxqsSe0lsUaNGTDS2BEs0iQkmlliCBbti7yj2gogigkgRlCq9be/zPufend3ZPgvTduf7eZ7hzs7cnblz5zI7557zP/8Yj8fjEQAAAAAACIrY4DwsAAAAAAAg8AYAAAAAIMjIeAMAAAAAEEQE3gAAAAAABBGBNwAAAAAAQUTgDQAAAABAEBF4AwAAAAAQRATeAAAAAAAEEYE3AAAAAABBFB/MBwcANO///u//lJ+f3+j93bt317Rp00K+Kz/99FP9+OOP+s1vftPsuosXL9bTTz+thQsXqri4WB06dNC+++6r008/XZ07d96px2zOQw89pLffflsvv/yywvWenH322Vq/fn2TjzVkyBD9/PPPGjlypP7yl7/Uuu+LL75wbuvataueeOKJWvetXr3a2U8XXXSRJk6cqDPOOEObNm1q9HluuukmHXTQQRF7PPmy4+TFF1/UokWLnG3t1q2bDjnkEJ100klKTk72+3GWL1+u3/72t7r11ls1ZsyYej+3Zv7+nwqUuv83H374YT377LP63//+p912263Wur/73e+0ZMkS/fWvf9WoUaMCvi0A0BYReANAmFmANXz4cCdwakhCQoLC4YYbbnCCoebMmDFDkyZN0jnnnOP8Tnp6utasWaOpU6fqmWee0VNPPaW+ffu26DH9sW3bNmffhfM9+dvf/qby8vLq2y0Qt8D273//e/VtFkjeeOONmjlzZr3A+7333tOWLVu0atUqrVy5Ur17966+78svv3Ru32OPPZyfbZ/27NlTt99+e4Pb5A3GIvV48vrXv/6lxx9/3DmRcNVVVykzM9MJLu+55x7neHn00UfVqVMnvx6rtLTU2UdFRUUN/txateT/VKDU/b9p/79sX9o+9WUnN+xY3nPPPfXkk08SeAOAnwi8ASACpKam1gq6WhMLMo855hhde+211bcNHDhQ++23nw4++GDdd999TrDVFt+THj161Po5Pj7eCbTr/t7++++vTz75xAmKLXj2stvOPfdcJ9j86KOPnOtec+bMcbKc3sDbNPTYO7vt4WCBmmVQH3zwQefY8Bo0aJATwJ1wwgnOyQk7ZqJZJPyf6tixo7OsexLkhRde0IABA3TmmWc6J5Q2bNigLl26BHVbAKAtYIw3ALQCHo9Hp512mm677bZ691lZ7XnnnVdr3enTpztB3LHHHqsLL7xQ77//fvX9FvwdccQR+vrrr51S0RNPPNEp8f3Pf/7jlLSao48+Wps3b9abb77prNtYiXNFRYV++eUXJ+Csy4LE1157TX/84x8bfUzLntl1y+7WLSO3IMzLtssyolZybaXU9957r8rKyqrvP//8853saV22nu032yfm6quvrvW4oWIBk/nmm2+qb7PXblnMww8/3Ak6LQj3ZYG3BewxMTERd4zNnTtXV155pY4//njn2LHsur2eptj7Ze/HoYceWivo9rITEpbltdJ8O65MSUmJU/JslQTjx4/Xqaeeqrvvvrv6OPVHU49hx+ORRx6pV155pdb6FvT+4Q9/qPU4119/va677jq/tsvf43FX/0/5sy07+//dAn0bAmEnf3zfQ9tXBx54oLNeXFycnn/++Vrb6M/z+bOOP68NAFoTAm8AaAUs+Bo2bJgzLta3jLawsFAvvfSSRowYUX2bBUEWPFlAZ1mxAw44wAmSvON6veW4dptltaxc+te//rUee+wx3XnnndWBr33httJTK2/1/fLty75427jTV1991QnO5s+fr8rKyur7LRNmX94be8zGSoN9y8gtSLn44oudTJst//znPzulyVZu62Xjp9955x2tW7eu+jbbjueee05Dhw6tDl6vueaasGRTLYixUnDfwPvjjz929oEFmvYeffXVV877aey1W/BlAU6kHWM//PCDc1LHSp3teLEMtQVqVjre1NhyC7LsfT3ssMMaXccCrEsvvdQ5rsxll13mPLeNc//3v//tPIe973bM+qupx7BsbmJioj744IPq9e09+umnn5yTVd6TO3acWj+B3Xff3a/t8vd43NX/U/5sy87+f7dtqBtU2z7ZunWrc/LKyt/HjRvnrOM73MKf5/NnnUC9/wAQKQi8ASACWGMjywA1dHnjjTecdSzTa0HQhx9+WP17NtbSbpswYUJ1cGPZbsu22fhQK+G1IMkycBaE25dmL8v0WSOqwYMHO5lLu7z++uvOfb16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MuMNAECbVVwsTZrkLgEAQFQg8AYAAAAAIBpLzQEAaJOSk6UpU8K9FQAAIISiLvD2zu1tU5SFm0175u3SznRi4FgBny1RwkrM//EP6Zpr3CA8jPg7BI4V8LkC/gbtPG9M6Y0xmxLj8Xg8iiI2BdmKFSvCvRkAAAAAgDZgt912U8eOHZtcJ+oCb5vfe8eOHUpKSlJsLEPcAQAAAAAtZ5nukpISZWVlKT6+6WLyqAu8AQAAAAAIJVK+AAAAAAAEEYE30Ip5GyMBAABEEr6jALUReEeIadOm6eCDD9aee+6p448/Xp988km4NwkRbvHixdprr720dOnScG8KItT06dN11FFHOZ8rRx99tF577bVwbxIi6PPjV7/6lXNsjBs3TjNmzAj3JiGCj5Wzzz5bI0aM0H777adbbrnFmY0FaMr999/v/N0BGvPss8/qsMMO05AhQzR+/HjNnDmzze8sAu8I8MILL+iuu+7SHXfcoblz5+qMM87QlVdeqdzc3HBvGiK4SeD111+vsrKycG8KItTHH3/sfKbcfPPNzufKpEmTnGPmyy+/DPemIQKmPrngggt00EEHafbs2frDH/6ga6+9VkuWLAn3piHCFBQU6MILL9Tee++tzz//XE899ZTmzJmjv/3tb+HeNESw5cuX67///W+4NwMR7K233tJf//pX3XTTTfr222910UUX6eqrr9amTZvUlhF4R4CpU6fq/PPPd84kJyYm6vTTT3cOwszMzHBvGiLUww8/3GznRES3d955RyeccILGjBnjfK4ceeSR2n///fXhhx+Ge9MQZu+9954zq8dll12mtLQ0JytlWQerkAB8ff311yotLXWSAampqerTp49z0uaDDz5gR6HRDs833HCDBg8ezB5Ck7HPWWedpUMPPdT5jjJhwgR999136ty5c5veawTeYbZ161b9/PPPTqkf4O+Z5EceecQp9wMaY9nuyZMn17pt48aNat++PTstyi1cuFB77LGHYmJiqm+zn7///vuwbhcijw2Bs6oI32Nlw4YNfI6gUY8//riztOpNoLGqq0WLFunwww+Puh1E4B2i5hJWGlz3YmcF7YuwWbt2rY499lgNHTpUp556qhYsWBCKTUMrOVa8DUq8Z5IvueQS9ezZM9ybizCyY6GpY6WuN9980znJZ58ziG7bt2+vFzjZ/KPbtm0L2zahddi8ebMee+wxTZw4Mdybggi0evVqZ2y3lRD7nqwBfG3atMn5DmNLq8yzXiMnnXSSMyyuraNWNQT22WcfZ2L1uqyJmpVZeJur2XgYK7GwpY11ePvtt9WuXbtQbCIihA0zaCjrNGjQIL300kvVZ5LPOecc54whoteNN96oV155pd7tcXFx9U7czZo1yxlH9c9//pMTNpDH43EuvuxnviijuRM2VmZ+wAEHOM3WgLqfIZYYsKGT/fr1o4IGjSqr6k9ksY/1uOrevbtTyXnxxRc7SYLs7Ow2u/cIvEPAxms3dXbQXHrpperdu7dz3ZoLPPfcc87YKkrQo8vzzz/f5LFiJ2WefPJJ50yhXYz3uo3ZRPS4/fbbnYs/43ktSLc/blY2Cli2e9WqVbV2xI4dOzjRi0atX7/eCahGjx7tfJ5wkgZ12fdWawp87rnnOtVX3pN7dp2eNPDVriqpaCfy7CSNsZ4jdgxZA1jLgrdVBN5hlpOT43wJ8p2aw4IoKxflDxt8ffHFF8rPz3caUPiy8hzrOnvVVVexw1CLNcv6z3/+o//9738aNmwYewcOK+uz5nu+J+ys0sZuB+qyISoWdJ922mlONR7QEGu4Z9Ob2rRzxgJv+4yxvz3PPPOMM/0pYDp27OhkuX0rN+14sZM0bT32ifHUrTdDyFkW06YUu/fee9WtWzfdd999TpbKvhilpKTwjqDRaV5Gjhyp119/XQMHDmQvoRYr17LMlH3h6d+/f/XtFmhRHRHd7LPD5ne34U520u6zzz7Tn//8Z+dEzYABA8K9eYgg1ofm5JNP1plnnulkp7zsy7ENawEa8+qrrzrfa+37LFDXY4895szQY8fIbrvtpoceesgZUvnuu+8qIyOjze4wMt4RwBpl2R8xKze3cj87W2hjHQi60RTvF5+2fnYQO8dKy623hFVE+LJg6+9//zu7NYrZFGL2JcdmRrBpxKxR47///W+CbtRjQ5u2bNmiu+++27k01UsC8GUneDk5g8acd955TkWEVWtaY0+ruHr00UfbdNBtyHgDAAAAABBEdGMCAAAAACCICLwBAAAAAAgiAm8AAAAAAIKIwBsAAAAAgCAi8AYAAAAAIIgIvAEAAAAACCICbwAAAAAAgojAGwCAAFu8eLEKCgrYr2HG+wAAiBQE3gCANmvRokXafffda12GDBmigw46SH/7299UUVHh1+NMnz5dRx99tF/rbtiwQSeddJIKCwudn998800ddthhag12dVvPO+883XPPPU2u884772jy5MkBeb6WvA/B9uOPP+qss85SZWVlSJ4PANC6xId7AwAACLZ3331XvXv3dq6XlJToiy++0FVXXaUuXbro17/+dUCfq6ysrFZAf+yxxzqX1iDY27pjxw7deeedev755xVsdd+HYBswYID69Omjp59+2gnAAQDwRcYbABBVkpKSdOihh2r8+PF67733qm//+uuvdfLJJ2vYsGHOfS+//HKDv28Zzfvvv1/HH3+89tprL+27776aNGmSioqKnKD+8MMPd9Y74IADnOyuN6tbXl6uMWPG6NVXX631eLa+ZdRbsg3GHue0007THXfcoREjRjhZ/CeeeKLWOk093ksvvaRTTz1VF1xwgUaOHKlXXnmlXgZ61qxZmjhxovM6x40bV72dXm+99ZaOPPJI5/7rr7/eef1Nse0bPXq0Onbs2OB+tZMhxxxzjLZu3apd0dD7EAgzZszQUUcdpb333ltXXHGF/vjHP+qvf/1r9f1nn322HnjgAZWWlgbk+QAAbQeBNwAgKnk8HsXFxVWXJV944YVOIGvBppVC/+Mf/9DMmTPr/d5rr72m5557TnfddZfmzJnjBOEffvihE8haUP/+++8763322We1ytPj4+OdINWy714LFixwnttub8k2eH333Xdat26d85wWgE+ZMkVvv/22369p3rx5Ovjgg53ft5MRvr7//nvn9//v//5Pn3/+uRNY33777frggw+qt90CzyuvvFKffvqpsrOzNXfu3Cb39zPPPOME1g2x7bPX8+ijj6pDhw717rcS9rrDBryX9evX11q3qfdhZ/3www+6+uqr9bvf/c55vwcOHOgcC77stvT09FrvMQAAhsAbABBViouLneDTsrXegOypp57SqFGjnOxuWlqac92yl3UzyGbChAn66KOP1L9/fyUmJmqfffbR0KFDtXnz5maf+7jjjnMCQe+4Y8vEWkY2KyurRdvgZc9/yy23OIHq/vvv72Sw7aSAv6/JTjycfvrpat++vbMNvqxkeuzYsTrjjDOcYNIy4bbu1KlTnftfeOEFJ8tupemZmZlOBrhHjx5Njre3TLZVCNT1z3/+0wmULeju2rVrg79/+eWXa8mSJQ1eGvudQLKTBrYPrNLBXq8F4FZeXpftZzsRAQCAL8Z4AwDaPMso++rVq5d+//vfO0GlWbZsmRNMW/bUV7du3Rp8vJUrVzplxz///LMT+FlQaeXezbEg3QLcTz75xAn67TEsYN2ZbTC77bab2rVrV/3zoEGD9MYbb/j9eJ07d3Yy8Q356aefnMDb15577qkXX3zRub58+XKn5NorNjbWef7G2PZYVjw5ObnW7Zaxt2DegmfbnlCxDPq9997b4H0ff/xxvWDe3udDDjmk1m177LFHvd+1XgLeqgMAALwIvAEAUdNc7ZdfftG1117rBHjWACsmJsa535pw2c833XRTs49l2cxLLrnEyX7a2OhTTjlFd999t1/bYcGplVrb9uTk5GjTpk3VY5Fbsg1edYNmGyftvc2fx7OMeWMSEhIaLBdvqmt3Y0G8ycvLU0pKSoO/89BDD+mGG25wAuFrrrkmIIFycyyDbhd/WXWAPx3LU1NTndcKAIAvSs0BAFGje/fuevDBB53sqwXP3iZYffv2dcZr+8NKuK1RmQXbNn2WZbrXrFlTfb83mG+MlWZb8G7ZbhtfbWXgLd0G38y7lc77jkP2Zp135vF89evXzxnn7ct+ts7dxsqs7fnqTqnVGCtnz8/Pr3e7nQSxzPqf/vQnPf74440+RktLzZt7H1rKXu/SpUtr3WbPXVdubq7zWgEA8EXgDQCIKpaR/Pe//61vvvlG//rXv5zbzjzzTK1YsUL/+c9/nMDJgj9rSmbZ8bpsfK8F7pattjHL1hTMAm/ram68WV1r+NXQHNLWYdzKw5988slaU3e1ZBu8LLNq85HbNF2W9bUycO9UVjvzeL7OOeccpymbjfW2gNmaqj377LPVj+9t2mZN5Ww7LGtt5emNsYB9y5YtDQbf3uEAVop/8803KxCaex9ayoYl2D6wzu/2Gh5++GEtXry4XoC/atUqZwgAAAC+CLwBAFHHssE33nijpk2b5oyDtrJvy4Rb8Lrffvs5mWwbG21BdV025ZUF30cccYRTZm5jlm0u8IULFzr3W6Mza8Bljchef/31Bp/fMuYWsPmOGW7JNnjZPORWAm1B66233uqsa83advbx6u4jK+22pmI2DZp1RLey9RNOOMG53x7rvvvuc8ZnW2O3b7/9tl5ndF+DBw92sttNdT63cnN7HJvabFf58z60hFUSWFd3O1ljlQp28sUy9XVL8m0Kt7pjwQEAiPHYgC0AANCq2Dzels32Tu/VGtg47dWrVztBfFvwq1/9yjmJYic1vKX4v/3tb52u+XWbyAEAohsZbwAAEBLnnnuuZs+e7dfUa5HGqgdsaICdOCgpKXGy8jbG29sczzv+/4ILLiDoBgDUQ+ANAABCwkr0rWu5v13gI4nNgb7XXns5WW4bi26N4O6//3717NnTud8ar9n0crYeAAB1UWoOAAAAAEAQkfEGAAAAACCICLwBAAAAAAgiAm8AAAAAAIKIwBsAAAAAgCAi8AYAAAAAIIgIvAEAAAAACCICbwAAAAAAgojAGwAAAACAICLwBgAAAABAwfP/SivYyG+JOlYAAAAASUVORK5CYII=", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA94AAAHqCAYAAADyGZa5AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjIsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvgI3uAAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAkJFJREFUeJzt3QeYVPXVx/Hf9r5LXdoCUgURBFSKvWDDxqsSe0lsUaNGTDS2BEs0iQkmlliCBbti7yj2gogigkgRlCq9be/zPufend3ZPgvTduf7eZ7hzs7cnblz5zI7557zP/8Yj8fjEQAAAAAACIrY4DwsAAAAAAAg8AYAAAAAIMjIeAMAAAAAEEQE3gAAAAAABBGBNwAAAAAAQUTgDQAAAABAEBF4AwAAAAAQRATeAAAAAAAEEYE3AAAAAABBFB/MBwcANO///u//lJ+f3+j93bt317Rp00K+Kz/99FP9+OOP+s1vftPsuosXL9bTTz+thQsXqri4WB06dNC+++6r008/XZ07d96px2zOQw89pLffflsvv/yywvWenH322Vq/fn2TjzVkyBD9/PPPGjlypP7yl7/Uuu+LL75wbuvataueeOKJWvetXr3a2U8XXXSRJk6cqDPOOEObNm1q9HluuukmHXTQQRF7PPmy4+TFF1/UokWLnG3t1q2bDjnkEJ100klKTk72+3GWL1+u3/72t7r11ls1ZsyYej+3Zv7+nwqUuv83H374YT377LP63//+p912263Wur/73e+0ZMkS/fWvf9WoUaMCvi0A0BYReANAmFmANXz4cCdwakhCQoLC4YYbbnCCoebMmDFDkyZN0jnnnOP8Tnp6utasWaOpU6fqmWee0VNPPaW+ffu26DH9sW3bNmffhfM9+dvf/qby8vLq2y0Qt8D273//e/VtFkjeeOONmjlzZr3A+7333tOWLVu0atUqrVy5Ur17966+78svv3Ru32OPPZyfbZ/27NlTt99+e4Pb5A3GIvV48vrXv/6lxx9/3DmRcNVVVykzM9MJLu+55x7neHn00UfVqVMnvx6rtLTU2UdFRUUN/txateT/VKDU/b9p/79sX9o+9WUnN+xY3nPPPfXkk08SeAOAnwi8ASACpKam1gq6WhMLMo855hhde+211bcNHDhQ++23nw4++GDdd999TrDVFt+THj161Po5Pj7eCbTr/t7++++vTz75xAmKLXj2stvOPfdcJ9j86KOPnOtec+bMcbKc3sDbNPTYO7vt4WCBmmVQH3zwQefY8Bo0aJATwJ1wwgnOyQk7ZqJZJPyf6tixo7OsexLkhRde0IABA3TmmWc6J5Q2bNigLl26BHVbAKAtYIw3ALQCHo9Hp512mm677bZ691lZ7XnnnVdr3enTpztB3LHHHqsLL7xQ77//fvX9FvwdccQR+vrrr51S0RNPPNEp8f3Pf/7jlLSao48+Wps3b9abb77prNtYiXNFRYV++eUXJ+Csy4LE1157TX/84x8bfUzLntl1y+7WLSO3IMzLtssyolZybaXU9957r8rKyqrvP//8853saV22nu032yfm6quvrvW4oWIBk/nmm2+qb7PXblnMww8/3Ak6LQj3ZYG3BewxMTERd4zNnTtXV155pY4//njn2LHsur2eptj7Ze/HoYceWivo9rITEpbltdJ8O65MSUmJU/JslQTjx4/Xqaeeqrvvvrv6OPVHU49hx+ORRx6pV155pdb6FvT+4Q9/qPU4119/va677jq/tsvf43FX/0/5sy07+//dAn0bAmEnf3zfQ9tXBx54oLNeXFycnn/++Vrb6M/z+bOOP68NAFoTAm8AaAUs+Bo2bJgzLta3jLawsFAvvfSSRowYUX2bBUEWPFlAZ1mxAw44wAmSvON6veW4dptltaxc+te//rUee+wx3XnnndWBr33httJTK2/1/fLty75427jTV1991QnO5s+fr8rKyur7LRNmX94be8zGSoN9y8gtSLn44oudTJst//znPzulyVZu62Xjp9955x2tW7eu+jbbjueee05Dhw6tDl6vueaasGRTLYixUnDfwPvjjz929oEFmvYeffXVV877aey1W/BlAU6kHWM//PCDc1LHSp3teLEMtQVqVjre1NhyC7LsfT3ssMMaXccCrEsvvdQ5rsxll13mPLeNc//3v//tPIe973bM+qupx7BsbmJioj744IPq9e09+umnn5yTVd6TO3acWj+B3Xff3a/t8vd43NX/U/5sy87+f7dtqBtU2z7ZunWrc/LKyt/HjRvnrOM73MKf5/NnnUC9/wAQKQi8ASACWGMjywA1dHnjjTecdSzTa0HQhx9+WP17NtbSbpswYUJ1cGPZbsu22fhQK+G1IMkycBaE25dmL8v0WSOqwYMHO5lLu7z++uvOfb16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MuMNAECbVVwsTZrkLgEAQFQg8AYAAAAAIBpLzQEAaJOSk6UpU8K9FQAAIISiLvD2zu1tU5SFm0175u3SznRi4FgBny1RwkrM//EP6Zpr3CA8jPg7BI4V8LkC/gbtPG9M6Y0xmxLj8Xg8iiI2BdmKFSvCvRkAAAAAgDZgt912U8eOHZtcJ+oCb5vfe8eOHUpKSlJsLEPcAQAAAAAtZ5nukpISZWVlKT6+6WLyqAu8AQAAAAAIJVK+AAAAAAAEEYE30Ip5GyMBAABEEr6jALUReEeIadOm6eCDD9aee+6p448/Xp988km4NwkRbvHixdprr720dOnScG8KItT06dN11FFHOZ8rRx99tF577bVwbxIi6PPjV7/6lXNsjBs3TjNmzAj3JiGCj5Wzzz5bI0aM0H777adbbrnFmY0FaMr999/v/N0BGvPss8/qsMMO05AhQzR+/HjNnDmzze8sAu8I8MILL+iuu+7SHXfcoblz5+qMM87QlVdeqdzc3HBvGiK4SeD111+vsrKycG8KItTHH3/sfKbcfPPNzufKpEmTnGPmyy+/DPemIQKmPrngggt00EEHafbs2frDH/6ga6+9VkuWLAn3piHCFBQU6MILL9Tee++tzz//XE899ZTmzJmjv/3tb+HeNESw5cuX67///W+4NwMR7K233tJf//pX3XTTTfr222910UUX6eqrr9amTZvUlhF4R4CpU6fq/PPPd84kJyYm6vTTT3cOwszMzHBvGiLUww8/3GznRES3d955RyeccILGjBnjfK4ceeSR2n///fXhhx+Ge9MQZu+9954zq8dll12mtLQ0JytlWQerkAB8ff311yotLXWSAampqerTp49z0uaDDz5gR6HRDs833HCDBg8ezB5Ck7HPWWedpUMPPdT5jjJhwgR999136ty5c5veawTeYbZ161b9/PPPTqkf4O+Z5EceecQp9wMaY9nuyZMn17pt48aNat++PTstyi1cuFB77LGHYmJiqm+zn7///vuwbhcijw2Bs6oI32Nlw4YNfI6gUY8//riztOpNoLGqq0WLFunwww+Puh1E4B2i5hJWGlz3YmcF7YuwWbt2rY499lgNHTpUp556qhYsWBCKTUMrOVa8DUq8Z5IvueQS9ezZM9ybizCyY6GpY6WuN9980znJZ58ziG7bt2+vFzjZ/KPbtm0L2zahddi8ebMee+wxTZw4Mdybggi0evVqZ2y3lRD7nqwBfG3atMn5DmNLq8yzXiMnnXSSMyyuraNWNQT22WcfZ2L1uqyJmpVZeJur2XgYK7GwpY11ePvtt9WuXbtQbCIihA0zaCjrNGjQIL300kvVZ5LPOecc54whoteNN96oV155pd7tcXFx9U7czZo1yxlH9c9//pMTNpDH43EuvuxnviijuRM2VmZ+wAEHOM3WgLqfIZYYsKGT/fr1o4IGjSqr6k9ksY/1uOrevbtTyXnxxRc7SYLs7Ow2u/cIvEPAxms3dXbQXHrpperdu7dz3ZoLPPfcc87YKkrQo8vzzz/f5LFiJ2WefPJJ50yhXYz3uo3ZRPS4/fbbnYs/43ktSLc/blY2Cli2e9WqVbV2xI4dOzjRi0atX7/eCahGjx7tfJ5wkgZ12fdWawp87rnnOtVX3pN7dp2eNPDVriqpaCfy7CSNsZ4jdgxZA1jLgrdVBN5hlpOT43wJ8p2aw4IoKxflDxt8ffHFF8rPz3caUPiy8hzrOnvVVVexw1CLNcv6z3/+o//9738aNmwYewcOK+uz5nu+J+ys0sZuB+qyISoWdJ922mlONR7QEGu4Z9Ob2rRzxgJv+4yxvz3PPPOMM/0pYDp27OhkuX0rN+14sZM0bT32ifHUrTdDyFkW06YUu/fee9WtWzfdd999TpbKvhilpKTwjqDRaV5Gjhyp119/XQMHDmQvoRYr17LMlH3h6d+/f/XtFmhRHRHd7LPD5ne34U520u6zzz7Tn//8Z+dEzYABA8K9eYgg1ofm5JNP1plnnulkp7zsy7ENawEa8+qrrzrfa+37LFDXY4895szQY8fIbrvtpoceesgZUvnuu+8qIyOjze4wMt4RwBpl2R8xKze3cj87W2hjHQi60RTvF5+2fnYQO8dKy623hFVE+LJg6+9//zu7NYrZFGL2JcdmRrBpxKxR47///W+CbtRjQ5u2bNmiu+++27k01UsC8GUneDk5g8acd955TkWEVWtaY0+ruHr00UfbdNBtyHgDAAAAABBEdGMCAAAAACCICLwBAAAAAAgiAm8AAAAAAIKIwBsAAAAAgCAi8AYAAAAAIIgIvAEAAAAACCICbwAAAAAAgojAGwCAAFu8eLEKCgrYr2HG+wAAiBQE3gCANmvRokXafffda12GDBmigw46SH/7299UUVHh1+NMnz5dRx99tF/rbtiwQSeddJIKCwudn998800ddthhag12dVvPO+883XPPPU2u884772jy5MkBeb6WvA/B9uOPP+qss85SZWVlSJ4PANC6xId7AwAACLZ3331XvXv3dq6XlJToiy++0FVXXaUuXbro17/+dUCfq6ysrFZAf+yxxzqX1iDY27pjxw7deeedev755xVsdd+HYBswYID69Omjp59+2gnAAQDwRcYbABBVkpKSdOihh2r8+PF67733qm//+uuvdfLJJ2vYsGHOfS+//HKDv28Zzfvvv1/HH3+89tprL+27776aNGmSioqKnKD+8MMPd9Y74IADnOyuN6tbXl6uMWPG6NVXX631eLa+ZdRbsg3GHue0007THXfcoREjRjhZ/CeeeKLWOk093ksvvaRTTz1VF1xwgUaOHKlXXnmlXgZ61qxZmjhxovM6x40bV72dXm+99ZaOPPJI5/7rr7/eef1Nse0bPXq0Onbs2OB+tZMhxxxzjLZu3apd0dD7EAgzZszQUUcdpb333ltXXHGF/vjHP+qvf/1r9f1nn322HnjgAZWWlgbk+QAAbQeBNwAgKnk8HsXFxVWXJV944YVOIGvBppVC/+Mf/9DMmTPr/d5rr72m5557TnfddZfmzJnjBOEffvihE8haUP/+++8763322We1ytPj4+OdINWy714LFixwnttub8k2eH333Xdat26d85wWgE+ZMkVvv/22369p3rx5Ovjgg53ft5MRvr7//nvn9//v//5Pn3/+uRNY33777frggw+qt90CzyuvvFKffvqpsrOzNXfu3Cb39zPPPOME1g2x7bPX8+ijj6pDhw717rcS9rrDBryX9evX11q3qfdhZ/3www+6+uqr9bvf/c55vwcOHOgcC77stvT09FrvMQAAhsAbABBViouLneDTsrXegOypp57SqFGjnOxuWlqac92yl3UzyGbChAn66KOP1L9/fyUmJmqfffbR0KFDtXnz5maf+7jjjnMCQe+4Y8vEWkY2KyurRdvgZc9/yy23OIHq/vvv72Sw7aSAv6/JTjycfvrpat++vbMNvqxkeuzYsTrjjDOcYNIy4bbu1KlTnftfeOEFJ8tupemZmZlOBrhHjx5Njre3TLZVCNT1z3/+0wmULeju2rVrg79/+eWXa8mSJQ1eGvudQLKTBrYPrNLBXq8F4FZeXpftZzsRAQCAL8Z4AwDaPMso++rVq5d+//vfO0GlWbZsmRNMW/bUV7du3Rp8vJUrVzplxz///LMT+FlQaeXezbEg3QLcTz75xAn67TEsYN2ZbTC77bab2rVrV/3zoEGD9MYbb/j9eJ07d3Yy8Q356aefnMDb15577qkXX3zRub58+XKn5NorNjbWef7G2PZYVjw5ObnW7Zaxt2DegmfbnlCxDPq9997b4H0ff/xxvWDe3udDDjmk1m177LFHvd+1XgLeqgMAALwIvAEAUdNc7ZdfftG1117rBHjWACsmJsa535pw2c833XRTs49l2cxLLrnEyX7a2OhTTjlFd999t1/bYcGplVrb9uTk5GjTpk3VY5Fbsg1edYNmGyftvc2fx7OMeWMSEhIaLBdvqmt3Y0G8ycvLU0pKSoO/89BDD+mGG25wAuFrrrkmIIFycyyDbhd/WXWAPx3LU1NTndcKAIAvSs0BAFGje/fuevDBB53sqwXP3iZYffv2dcZr+8NKuK1RmQXbNn2WZbrXrFlTfb83mG+MlWZb8G7ZbhtfbWXgLd0G38y7lc77jkP2Zp135vF89evXzxnn7ct+ts7dxsqs7fnqTqnVGCtnz8/Pr3e7nQSxzPqf/vQnPf74440+RktLzZt7H1rKXu/SpUtr3WbPXVdubq7zWgEA8EXgDQCIKpaR/Pe//61vvvlG//rXv5zbzjzzTK1YsUL/+c9/nMDJgj9rSmbZ8bpsfK8F7pattjHL1hTMAm/ram68WV1r+NXQHNLWYdzKw5988slaU3e1ZBu8LLNq85HbNF2W9bUycO9UVjvzeL7OOeccpymbjfW2gNmaqj377LPVj+9t2mZN5Ww7LGtt5emNsYB9y5YtDQbf3uEAVop/8803KxCaex9ayoYl2D6wzu/2Gh5++GEtXry4XoC/atUqZwgAAAC+CLwBAFHHssE33nijpk2b5oyDtrJvy4Rb8Lrffvs5mWwbG21BdV025ZUF30cccYRTZm5jlm0u8IULFzr3W6Mza8Bljchef/31Bp/fMuYWsPmOGW7JNnjZPORWAm1B66233uqsa83advbx6u4jK+22pmI2DZp1RLey9RNOOMG53x7rvvvuc8ZnW2O3b7/9tl5ndF+DBw92sttNdT63cnN7HJvabFf58z60hFUSWFd3O1ljlQp28sUy9XVL8m0Kt7pjwQEAiPHYgC0AANCq2Dzels32Tu/VGtg47dWrVztBfFvwq1/9yjmJYic1vKX4v/3tb52u+XWbyAEAohsZbwAAEBLnnnuuZs+e7dfUa5HGqgdsaICdOCgpKXGy8jbG29sczzv+/4ILLiDoBgDUQ+ANAABCwkr0rWu5v13gI4nNgb7XXns5WW4bi26N4O6//3717NnTud8ar9n0crYeAAB1UWoOAAAAAEAQkfEGAAAAACCICLwBAAAAAAgiAm8AAAAAAIKIwBsAAAAAgCAi8AYAAAAAIIgIvAEAAAAACCICbwAAAAAAgojAGwAAAACAICLwBgAAAABAwfP/SivYyG+JOlYAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -1231,10 +1261,10 @@ "id": "94d596a0", "metadata": { "execution": { - "iopub.execute_input": "2026-08-10T22:40:41.917425Z", - "iopub.status.busy": "2026-08-10T22:40:41.917352Z", - "iopub.status.idle": "2026-08-10T22:40:41.927664Z", - "shell.execute_reply": "2026-08-10T22:40:41.927331Z" + "iopub.execute_input": "2026-10-31T11:26:22.402034Z", + "iopub.status.busy": "2026-10-31T11:26:22.401978Z", + "iopub.status.idle": "2026-10-31T11:26:22.412396Z", + "shell.execute_reply": "2026-10-31T11:26:22.412002Z" } }, "outputs": [ diff --git a/docs/tutorials/19_dcdh_marketing_pulse.ipynb b/docs/tutorials/19_dcdh_marketing_pulse.ipynb index 2b2c40f3e..107b7bc23 100644 --- a/docs/tutorials/19_dcdh_marketing_pulse.ipynb +++ b/docs/tutorials/19_dcdh_marketing_pulse.ipynb @@ -250,25 +250,25 @@ "outputs": [], "source": [ "# Event-study errorbar plot with bootstrap CIs.\n", - "es_plot = es_df[es_df[\"horizon\"] != 0] # drop reference row\n", - "is_pre = es_plot[\"horizon\"] < 0\n", + "es_plot = es_df[~es_df[\"is_reference\"]] # drop the reference row\n", + "is_pre = es_plot[\"event_time\"] < 0\n", "\n", "fig, ax = plt.subplots(figsize=(9, 5))\n", "ax.errorbar(\n", - " es_plot.loc[is_pre, \"horizon\"],\n", - " es_plot.loc[is_pre, \"effect\"],\n", + " es_plot.loc[is_pre, \"event_time\"],\n", + " es_plot.loc[is_pre, \"att\"],\n", " yerr=[\n", - " es_plot.loc[is_pre, \"effect\"] - es_plot.loc[is_pre, \"conf_int_lower\"],\n", - " es_plot.loc[is_pre, \"conf_int_upper\"] - es_plot.loc[is_pre, \"effect\"],\n", + " es_plot.loc[is_pre, \"att\"] - es_plot.loc[is_pre, \"conf_int_lower\"],\n", + " es_plot.loc[is_pre, \"conf_int_upper\"] - es_plot.loc[is_pre, \"att\"],\n", " ],\n", " fmt=\"o\", color=\"#888888\", capsize=4, label=\"Pre-treatment placebos\",\n", ")\n", "ax.errorbar(\n", - " es_plot.loc[~is_pre, \"horizon\"],\n", - " es_plot.loc[~is_pre, \"effect\"],\n", + " es_plot.loc[~is_pre, \"event_time\"],\n", + " es_plot.loc[~is_pre, \"att\"],\n", " yerr=[\n", - " es_plot.loc[~is_pre, \"effect\"] - es_plot.loc[~is_pre, \"conf_int_lower\"],\n", - " es_plot.loc[~is_pre, \"conf_int_upper\"] - es_plot.loc[~is_pre, \"effect\"],\n", + " es_plot.loc[~is_pre, \"att\"] - es_plot.loc[~is_pre, \"conf_int_lower\"],\n", + " es_plot.loc[~is_pre, \"conf_int_upper\"] - es_plot.loc[~is_pre, \"att\"],\n", " ],\n", " fmt=\"o\", color=\"#1f77b4\", capsize=4, label=\"Post-treatment effects\",\n", ")\n", @@ -360,4 +360,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/docs/v4-deprecations.yaml b/docs/v4-deprecations.yaml index 27f2b3555..5ae98c0af 100644 --- a/docs/v4-deprecations.yaml +++ b/docs/v4-deprecations.yaml @@ -1041,11 +1041,11 @@ rows: new: "diff_diff:EventStudyResults" deprecated_in: "4.0" removed_in: null - status: planned + status: done phase: 5 - test_ref: tests/test_v4_sentinel_retirement.py - code_refs: [diff_diff/staggered_results.py, diff_diff/sun_abraham.py, diff_diff/imputation_results.py, diff_diff/two_stage_results.py, diff_diff/stacked_did_results.py, diff_diff/efficient_did_results.py, diff_diff/continuous_did_results.py, diff_diff/wooldridge_results.py, diff_diff/chaisemartin_dhaultfoeuille_results.py, diff_diff/lpdid_results.py, diff_diff/triple_diff_results.py, diff_diff/results.py, diff_diff/had.py, diff_diff/visualization/_event_study.py, diff_diff/honest_did.py, diff_diff/pretrends.py] - notes: "4.0 sentinel retirement + schema enforcement (spec section 5): the n_groups==0 / n_obs==0 reference-row sentinels retire; every estimator's to_dataframe(level='event_study') emits the [M-092] column schema; the plotter / HonestDiD / PreTrendsPower consume the unified surface. Thirteen ES-carrying source modules enumerated file-by-file (results.py covers MultiPeriodDiD + SpilloverDiD) plus the plotter. behavior-at-done requires test_ref. PARTIAL PRE-CUT DELIVERY (2(b) PR-1, with M-026): the consumer half shipped for CS-SOURCED containers - compute_honest_did, compute_pretrends_power (both with SOURCE-SCOPED admission: source == CallawaySantAnnaResults only, rejecting dCDH l1 containers BY DESIGN since their placebo semantics need honest_did's native branch, and every other producer pending its own aggregate() migration) and plot_event_study / plot_honest_event_study (no source scoping - plotting is label-faithful). Admission widening is each later shim PR's methodology decision, not automatic. SECOND PRE-CUT AMENDMENT (2(b) PR-2, with M-024): admission widened to STACKED-SOURCED containers in compute_honest_did + compute_pretrends_power (source in {CallawaySantAnnaResults, StackedDiDResults}; kappa_pre >= 2 required for estimated pre-periods; honest additionally needs a non-singular full retained event-study covariance (pre+post sub-block); withheld-inference rows admitted with a source-scoped warning; the seven producer-derived guard messages now derive the producer from surface.source). Source scoping continues for all other producers. THIRD PRE-CUT AMENDMENT (2(b) PR-3a, with M-023): admission NOT widened to EfficientDiD-sourced containers BY DESIGN (the dCDH precedent) - the PRIMARY ground is the absent joint event-study covariance (container vcov=None; per-row df was all-NaN at that amendment and is finite on analytical survey fits since the 3.12 M-092 completion - the rejection ground is the missing joint covariance, unchanged); reference semantics are regime-dependent (PT-All: no reference row, universal first-period baseline; PT-Post: a materialized mechanical zero anchor at e=-1-anticipation when estimated, marked via the membership-gated reference_period property - never fabricated when absent); both terminal TypeErrors now state the EfficientDiD rejection explicitly; source scoping continues to reject. FOURTH PRE-CUT AMENDMENT (2(b) PR-3b, with M-021/M-022): admission NOT widened by the Imputation/TwoStage migrations - ImputationDiD rejected BY DESIGN (no joint ES covariance; per-horizon Theorem-3 conservative SEs only); TwoStageDiD DEFERRED despite carrying the joint Gardner-GMM covariance on analytical fits (M-092), because its pre-period coefficients are stage-1 residual means (the reference horizon is dropped from the no-intercept Stage-2 design; the zero anchor row is appended mechanically), not contrasts against the advertised reference, while HonestDiD's Delta^RM/Delta^SD arithmetic hard-codes the delta_0=0 normalization into its boundary/bridge constraints - admission awaits a normalization derivation (DEFERRED.md paper-gated row); both terminal TypeErrors state the per-producer grounds. FIFTH PRE-CUT AMENDMENT (2(b) PR-3c, with M-025): admission NOT widened by the ContinuousDiD migration - rejected BY DESIGN on two independent grounds: no joint event-study covariance exists (per-bin IF SEs only), and the binarized bins carry NO reference-period normalization at all (no reference row exists in the surface; each bin is a raw reweighted binarized ATT level), so HonestDiD's delta_0=0 arithmetic has no anchor even in principle without new methodology; both terminal TypeErrors name the ContinuousDiD rejection alongside EfficientDiD/Imputation. Amended pre-cut (2(b) PR-4, with M-027): HeterogeneousAdoptionDiD admission NOT widened by the HAD migration - DEFERRED, not by-design (the TwoStage class): its event-study coefficients ARE reference-normalized (each horizon differences against the F-1 anchor) but the anchor row is omitted from the container (identically zero AND the WAS is not identified there - no reference row exists for the consumer grid) and no joint cross-horizon covariance exists (per-horizon independent sandwiches; the DEFERRED.md row); both terminal TypeErrors name the HAD deferral in that corrected wording. Source scoping continues to reject all other producers. SIXTH PRE-CUT AMENDMENT (Phase 3(a), with M-010): admission widened to TWFE-SOURCED CALENDAR containers in compute_honest_did + compute_pretrends_power via a dedicated calendar route that reconstructs the native MultiPeriodDiDResults-branch inputs from the container's post_periods partition provenance (single-reference gate; HonestDiD additionally GEOMETRY-SCOPED to chronological partitions - suffix post_periods + last-pre reference - with the pre-existing native non-suffix geometry limitation documented in the REGISTRY HonestDiD Note and the DEFERRED.md transform-or-reject row; PreTrendsPower plumbs reference_period through the gamma-unit helper, string-label degradation included); relative-scale admission unchanged. SEVENTH PRE-CUT AMENDMENT (DML PR-B1): admission widened to DMLDiD-SOURCED containers in compute_honest_did + compute_pretrends_power (source in {CallawaySantAnnaResults, StackedDiDResults, DMLDiDResults}): DMLDiDResults subclasses CallawaySantAnnaResults and its aggregate('event_study') container is built by the INHERITED CS kit channel, so it carries the same staggered ATT(g,t) payload, reference semantics, base_period provenance, and (bootstrapped) replay behavior as a CS container - the same-semantics ground the original CS admission rests on; the producer-derived guard messages treat DMLDiDResults like CS (both expose base_period). DMLDiD adds NO builder (M-092 untouched) and never populates fit-time event_study_effects (its sentinel-retirement obligation is discharged by the shared CS code). Source scoping continues to reject all other producers. EIGHTH PRE-CUT AMENDMENT (4.0 cut PRs 3b/3c, with M-020..M-027/M-117..M-120/M-139): the fit-time aggregate=/balance_e= keywords are gone, which fixes WHICH producers populate the legacy event_study_effects/group_effects fields - CallawaySantAnna and EfficientDiD: None on EVERY fit (analytic and bootstrapped; the post-fit replay is the only bootstrap route); ImputationDiD and TwoStageDiD: None on analytic fits, ES + group dicts (percentile se/p/CI, df cleared) on bootstrapped fits IFF bootstrap_results is not None, relayed by aggregate(); ContinuousDiD: None on analytic fits, the ES dict on bootstrapped fits IFF _run_bootstrap ran, relayed by aggregate(); StackedDiD and SunAbraham: always populated (unchanged). CS/EDiD to_dataframe(level='event_study'|'group') therefore raises on every fresh fit until this row's schema rewire lands; admission is unchanged by the removals. This row stays planned: its transition remains the 4.0 sentinel retirement + full-producer schema enforcement. NINTH AMENDMENT (4.0 cut item 8a, 2026-10-30): the SENTINEL half shipped - reference rows in every native event-study dict that materializes one carry is_reference: True and no count key (StackedDiD, ImputationDiD/TwoStageDiD, SpilloverDiD - whose event-study att_dynamic/spillover_effects frames gain a boolean is_reference column with NaN n_obs on the reference rows - and the CallawaySantAnna/DMLDiD aggregator carrier); SunAbraham still omits its row (reference_period) and EfficientDiD's PT-Post anchor stays an estimated cell named by reference_period; results_base._from_relative_dict reads the key (the zero-count heuristic is gone, a zero count is a genuine count, a keyed row with a finite nonzero effect raises); 3.x pickles are migrated by _deprecation.migrate_event_study_reference_sentinel in the carriers' __setstate__ (SpilloverDiDResults gained one; aggregate-mode frames untouched); plot_event_study / plot_honest_event_study / HonestDiD / PreTrendsPower / DiagnosticReport read the key on their raw-dict routes (dispatch, admission, rendering and exception types unchanged); the native Stacked/Imputation/TwoStage to_dataframe(level='event_study') reference row renders n_obs=NaN (float column) until 8b; TwoStageDiD summary() prints its genuine all-filtered horizons as NaN rows (no longer mislabeled [ref: h]); a reference-only TwoStageDiD surface carries n_kind=None. test_ref pins it. REMAINING for done (8b): the to_dataframe(level='event_study') schema rewire on every results class (incl. the dCDH native frame's estimand 'ref', n_obs 0 row)." + test_ref: tests/test_v4_event_study_schema.py + code_refs: [diff_diff/staggered_results.py, diff_diff/sun_abraham.py, diff_diff/imputation_results.py, diff_diff/two_stage_results.py, diff_diff/stacked_did_results.py, diff_diff/efficient_did_results.py, diff_diff/continuous_did_results.py, diff_diff/wooldridge_results.py, diff_diff/chaisemartin_dhaultfoeuille_results.py, diff_diff/lpdid_results.py, diff_diff/triple_diff_results.py, diff_diff/results.py, diff_diff/had.py, diff_diff/visualization/_event_study.py, diff_diff/honest_did.py, diff_diff/pretrends.py, diff_diff/duration_did_results.py] + notes: "4.0 sentinel retirement + schema enforcement (spec section 5): the n_groups==0 / n_obs==0 reference-row sentinels retire; every estimator's to_dataframe(level='event_study') emits the [M-092] column schema; the plotter / HonestDiD / PreTrendsPower consume the unified surface. Thirteen ES-carrying source modules enumerated file-by-file (results.py covers MultiPeriodDiD + SpilloverDiD) plus the plotter. behavior-at-done requires test_ref. PARTIAL PRE-CUT DELIVERY (2(b) PR-1, with M-026): the consumer half shipped for CS-SOURCED containers - compute_honest_did, compute_pretrends_power (both with SOURCE-SCOPED admission: source == CallawaySantAnnaResults only, rejecting dCDH l1 containers BY DESIGN since their placebo semantics need honest_did's native branch, and every other producer pending its own aggregate() migration) and plot_event_study / plot_honest_event_study (no source scoping - plotting is label-faithful). Admission widening is each later shim PR's methodology decision, not automatic. SECOND PRE-CUT AMENDMENT (2(b) PR-2, with M-024): admission widened to STACKED-SOURCED containers in compute_honest_did + compute_pretrends_power (source in {CallawaySantAnnaResults, StackedDiDResults}; kappa_pre >= 2 required for estimated pre-periods; honest additionally needs a non-singular full retained event-study covariance (pre+post sub-block); withheld-inference rows admitted with a source-scoped warning; the seven producer-derived guard messages now derive the producer from surface.source). Source scoping continues for all other producers. THIRD PRE-CUT AMENDMENT (2(b) PR-3a, with M-023): admission NOT widened to EfficientDiD-sourced containers BY DESIGN (the dCDH precedent) - the PRIMARY ground is the absent joint event-study covariance (container vcov=None; per-row df was all-NaN at that amendment and is finite on analytical survey fits since the 3.12 M-092 completion - the rejection ground is the missing joint covariance, unchanged); reference semantics are regime-dependent (PT-All: no reference row, universal first-period baseline; PT-Post: a materialized mechanical zero anchor at e=-1-anticipation when estimated, marked via the membership-gated reference_period property - never fabricated when absent); both terminal TypeErrors now state the EfficientDiD rejection explicitly; source scoping continues to reject. FOURTH PRE-CUT AMENDMENT (2(b) PR-3b, with M-021/M-022): admission NOT widened by the Imputation/TwoStage migrations - ImputationDiD rejected BY DESIGN (no joint ES covariance; per-horizon Theorem-3 conservative SEs only); TwoStageDiD DEFERRED despite carrying the joint Gardner-GMM covariance on analytical fits (M-092), because its pre-period coefficients are stage-1 residual means (the reference horizon is dropped from the no-intercept Stage-2 design; the zero anchor row is appended mechanically), not contrasts against the advertised reference, while HonestDiD's Delta^RM/Delta^SD arithmetic hard-codes the delta_0=0 normalization into its boundary/bridge constraints - admission awaits a normalization derivation (DEFERRED.md paper-gated row); both terminal TypeErrors state the per-producer grounds. FIFTH PRE-CUT AMENDMENT (2(b) PR-3c, with M-025): admission NOT widened by the ContinuousDiD migration - rejected BY DESIGN on two independent grounds: no joint event-study covariance exists (per-bin IF SEs only), and the binarized bins carry NO reference-period normalization at all (no reference row exists in the surface; each bin is a raw reweighted binarized ATT level), so HonestDiD's delta_0=0 arithmetic has no anchor even in principle without new methodology; both terminal TypeErrors name the ContinuousDiD rejection alongside EfficientDiD/Imputation. Amended pre-cut (2(b) PR-4, with M-027): HeterogeneousAdoptionDiD admission NOT widened by the HAD migration - DEFERRED, not by-design (the TwoStage class): its event-study coefficients ARE reference-normalized (each horizon differences against the F-1 anchor) but the anchor row is omitted from the container (identically zero AND the WAS is not identified there - no reference row exists for the consumer grid) and no joint cross-horizon covariance exists (per-horizon independent sandwiches; the DEFERRED.md row); both terminal TypeErrors name the HAD deferral in that corrected wording. Source scoping continues to reject all other producers. SIXTH PRE-CUT AMENDMENT (Phase 3(a), with M-010): admission widened to TWFE-SOURCED CALENDAR containers in compute_honest_did + compute_pretrends_power via a dedicated calendar route that reconstructs the native MultiPeriodDiDResults-branch inputs from the container's post_periods partition provenance (single-reference gate; HonestDiD additionally GEOMETRY-SCOPED to chronological partitions - suffix post_periods + last-pre reference - with the pre-existing native non-suffix geometry limitation documented in the REGISTRY HonestDiD Note and the DEFERRED.md transform-or-reject row; PreTrendsPower plumbs reference_period through the gamma-unit helper, string-label degradation included); relative-scale admission unchanged. SEVENTH PRE-CUT AMENDMENT (DML PR-B1): admission widened to DMLDiD-SOURCED containers in compute_honest_did + compute_pretrends_power (source in {CallawaySantAnnaResults, StackedDiDResults, DMLDiDResults}): DMLDiDResults subclasses CallawaySantAnnaResults and its aggregate('event_study') container is built by the INHERITED CS kit channel, so it carries the same staggered ATT(g,t) payload, reference semantics, base_period provenance, and (bootstrapped) replay behavior as a CS container - the same-semantics ground the original CS admission rests on; the producer-derived guard messages treat DMLDiDResults like CS (both expose base_period). DMLDiD adds NO builder (M-092 untouched) and never populates fit-time event_study_effects (its sentinel-retirement obligation is discharged by the shared CS code). Source scoping continues to reject all other producers. EIGHTH PRE-CUT AMENDMENT (4.0 cut PRs 3b/3c, with M-020..M-027/M-117..M-120/M-139): the fit-time aggregate=/balance_e= keywords are gone, which fixes WHICH producers populate the legacy event_study_effects/group_effects fields - CallawaySantAnna and EfficientDiD: None on EVERY fit (analytic and bootstrapped; the post-fit replay is the only bootstrap route); ImputationDiD and TwoStageDiD: None on analytic fits, ES + group dicts (percentile se/p/CI, df cleared) on bootstrapped fits IFF bootstrap_results is not None, relayed by aggregate(); ContinuousDiD: None on analytic fits, the ES dict on bootstrapped fits IFF _run_bootstrap ran, relayed by aggregate(); StackedDiD and SunAbraham: always populated (unchanged). CS/EDiD to_dataframe(level='event_study'|'group') therefore raises on every fresh fit until this row's schema rewire lands; admission is unchanged by the removals. This row stays planned: its transition remains the 4.0 sentinel retirement + full-producer schema enforcement. NINTH AMENDMENT (4.0 cut item 8a, 2026-10-30): the SENTINEL half shipped - reference rows in every native event-study dict that materializes one carry is_reference: True and no count key (StackedDiD, ImputationDiD/TwoStageDiD, SpilloverDiD - whose event-study att_dynamic/spillover_effects frames gain a boolean is_reference column with NaN n_obs on the reference rows - and the CallawaySantAnna/DMLDiD aggregator carrier); SunAbraham still omits its row (reference_period) and EfficientDiD's PT-Post anchor stays an estimated cell named by reference_period; results_base._from_relative_dict reads the key (the zero-count heuristic is gone, a zero count is a genuine count, a keyed row with a finite nonzero effect raises); 3.x pickles are migrated by _deprecation.migrate_event_study_reference_sentinel in the carriers' __setstate__ (SpilloverDiDResults gained one; aggregate-mode frames untouched); plot_event_study / plot_honest_event_study / HonestDiD / PreTrendsPower / DiagnosticReport read the key on their raw-dict routes (dispatch, admission, rendering and exception types unchanged); the native Stacked/Imputation/TwoStage to_dataframe(level='event_study') reference row renders n_obs=NaN (float column) until 8b; TwoStageDiD summary() prints its genuine all-filtered horizons as NaN rows (no longer mislabeled [ref: h]); a reference-only TwoStageDiD surface carries n_kind=None. test_ref pins it. REMAINING for done (8b): the to_dataframe(level='event_study') schema rewire on every results class (incl. the dCDH native frame's estimand 'ref', n_obs 0 row). TENTH AMENDMENT (4.0 cut item 8b, 2026-10-31): DONE - every results class's to_dataframe(level='event_study') emits EVENT_STUDY_SCHEMA as a PREFIX: the AggregationMixin classes (CallawaySantAnna/DMLDiD, EfficientDiD, ImputationDiD, TwoStageDiD, StackedDiD, ContinuousDiD, TripleDifference staggered, dCDH, HAD event-study, DurationDiD, LWDiD) return aggregate('event_study').to_dataframe(); SunAbraham, LPDiD, WooldridgeDiD and SpilloverDiD return build_event_study_surface(self).to_dataframe(); WooldridgeDiD and LWDiD append their fit-configuration columns (cohort_trends/aggregation_weights; rolling/estimation_method/vcov_type) after the schema - the spec section 5 sentence was amended to the PREFIX rule in the same diff; the 'not stored' raises on CS/EDiD/TripleDiff and analytical Imputation/TwoStage/Continuous are gone (the level recomputes from the kit or replays the fit-time bootstrap; level='group' on CS/EDiD/TripleDiff still raises - TODO.md row); a kit-less or pre-replay legacy pickle that still carries a stored dict now raises where it returned a frame; WooldridgeDiD before aggregate('event_study') raises (was an empty frame); SpilloverDiD gains to_dataframe(level='simple'|'event_study') (aggregate-mode fits raise), HAD event-study results gain level='event_study', DurationDiD gains 'event_study'; the dCDH native frame (horizon / estimand 'ref' / n_obs 0 row) and its per-horizon DID_l labels are retired. Correction to the original note: results.py covers SpilloverDiD only (MultiPeriodDiD was removed at the TWFE merge). test_ref moved to tests/test_v4_event_study_schema.py; tests/test_v4_sentinel_retirement.py keeps the 8a pins." # ---- Behavior policies (schema-tracked, spec-governed; no reality probe) - - id: M-080 diff --git a/docs/v4-design.md b/docs/v4-design.md index b7ddc508d..239fed8f8 100644 --- a/docs/v4-design.md +++ b/docs/v4-design.md @@ -533,7 +533,12 @@ distinct per-cohort positional-base event times whose multi-entry case fails the HonestDiD/PreTrendsPower common-reference guard on gapped universal grids; amended into [M-092] pre-cut with the 2(b) consumer delivery), and `to_dataframe(level="event_study")` -emitting identical column schemas from every estimator. The container +emitting the `EVENT_STUDY_SCHEMA` columns as an identical PREFIX from every +estimator (amended at the 4.0 cut, item 8b [M-093]: a producer may append +documented fit-configuration columns AFTER the schema - LWDiD's +`rolling`/`estimation_method`/`vcov_type` [M-153] and WooldridgeDiD's +`cohort_trends`/`aggregation_weights`; no producer renames, reorders or omits +a schema column). The container OWNS its arrays: `__post_init__` copies every array field including the `vcov`/`vcov_index` pair (amended with [M-024] - `np.asarray` aliased the producer's stored matrix on the post-fit view route, so mutating a @@ -1243,8 +1248,12 @@ tests-first PR that flips no rows). 2026-10-30: the sentinel retirement - every materialized reference row carries `is_reference: True` and no count key, the builder and the consumers' raw-dict routes read the key, 3.x pickles migrate on load; pinned by - `tests/test_v4_sentinel_retirement.py`. 8b = the - `to_dataframe(level="event_study")` schema enforcement + the row flip.) + `tests/test_v4_sentinel_retirement.py`. 8b SHIPPED 2026-10-31: the + `to_dataframe(level="event_study")` schema rewire on every results class - + the `EVENT_STUDY_SCHEMA` prefix rule above, routed through + `aggregate('event_study')` / `build_event_study_surface` - pinned by + `tests/test_v4_event_study_schema.py`; row flipped to `done`; item 8 + complete.) 9. [M-090] docs reorganization: Bacon to the diagnostics family, catalogue counts, JOSS prose, REGISTRY archive framing for the removed classes. 10. Release: bump to 4.0.0 (closes the cut window), changelog compile, diff --git a/tests/test_chaisemartin_dhaultfoeuille.py b/tests/test_chaisemartin_dhaultfoeuille.py index c24b6e70a..31d8c4c22 100644 --- a/tests/test_chaisemartin_dhaultfoeuille.py +++ b/tests/test_chaisemartin_dhaultfoeuille.py @@ -2248,11 +2248,13 @@ def test_event_study_level(self, data): L_max=3, ) df = r.to_dataframe("event_study") - assert "horizon" in df.columns - assert "effect" in df.columns - # Should have: placebos + ref + positive horizons - assert (df["horizon"] == 0).any() # reference period - assert (df["horizon"] > 0).any() # positive horizons + # 4.0 (M-093 8b): the unified EVENT_STUDY_SCHEMA frame on the paper's + # l1_first_switch convention, bit-equal to the aggregate() container's. + assert "event_time" in df.columns + assert "att" in df.columns + assert df.loc[df["is_reference"], "event_time"].tolist() == [0] # reference + assert (df["event_time"] > 0).any() # positive horizons + pd.testing.assert_frame_equal(df, r.aggregate("event_study").to_dataframe()) def test_normalized_level(self, data): est = ChaisemartinDHaultfoeuille(placebo=False, twfe_diagnostic=False) @@ -5121,7 +5123,7 @@ def test_nan_contract_extends_to_placebo_event_study_horizons(self): # Negative-horizon rows must also show NaN in the inference # columns. df_es = res.to_dataframe(level="event_study") - negative_rows = df_es[df_es["horizon"] < 0] + negative_rows = df_es[df_es["event_time"] < 0] if len(negative_rows) > 0: for col in ("se", "t_stat", "p_value", "conf_int_lower", "conf_int_upper"): assert negative_rows[col].isna().all(), ( diff --git a/tests/test_continuous_did.py b/tests/test_continuous_did.py index 24c27e6de..adf6fbbb2 100644 --- a/tests/test_continuous_did.py +++ b/tests/test_continuous_did.py @@ -500,17 +500,12 @@ def test_to_dataframe_group_time(self, results): def test_to_dataframe_event_study_bootstrapped_populated(self, results): """A bootstrapped fit materializes the event-study surface at fit - time (4.0: always, no fit-time kwarg), so the legacy frame is - available and agrees row-for-row with the relayed container; the - analytic-fit error is pinned in test_event_study_to_dataframe.""" + time (4.0: always, no fit-time kwarg); since 8b (M-093) the level + frame IS the relayed container's unified frame, bit-equal.""" assert results.event_study_effects is not None df = results.to_dataframe(level="event_study") - assert "relative_period" in df.columns - assert "att_glob" in df.columns es = results.aggregate("event_study") - assert len(df) == len(es.event_time) - np.testing.assert_array_equal(df["relative_period"].to_numpy(), es.event_time) - np.testing.assert_allclose(df["att_glob"].to_numpy(), es.att, rtol=1e-14, atol=0) + pd.testing.assert_frame_equal(df, es.to_dataframe()) def test_to_dataframe_invalid_level(self, results): with pytest.raises(ValueError, match="Unknown level"): @@ -606,13 +601,13 @@ def test_event_study_to_dataframe(self): est = ContinuousDiD() results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") # analytic fits leave the legacy fit-time field None; the surface is - # the post-fit container + # the post-fit container, which to_dataframe(level="event_study") + # now relays (M-093 8b) instead of raising. assert results.event_study_effects is None - with pytest.raises(ValueError, match=r"aggregate\('event_study'\)"): - results.to_dataframe(level="event_study") df = results.aggregate("event_study").to_dataframe() assert "event_time" in df.columns assert "att" in df.columns + pd.testing.assert_frame_equal(results.to_dataframe(level="event_study"), df) def test_event_study_not_yet_treated(self): """Event study with control_group='not_yet_treated' and analytic SE.""" diff --git a/tests/test_efficient_did.py b/tests/test_efficient_did.py index 6a96dae08..5fdb7bbbc 100644 --- a/tests/test_efficient_did.py +++ b/tests/test_efficient_did.py @@ -346,10 +346,13 @@ def test_summary_and_dataframe(self): def test_to_dataframe_raises_without_aggregation(self): df = _make_simple_panel() result = EfficientDiD().fit(df, "y", "unit", "time", "first_treat") - # 4.0: the legacy fit-time field is never populated; the error - # steers to the post-fit container route. - with pytest.raises(ValueError, match=r"not stored on 4.0 fits.*aggregate\('event_study'\)"): - result.to_dataframe("event_study") + # 4.0: the legacy fit-time field is never populated; since M-093 8b + # level="event_study" relays the post-fit container, level="group" + # still steers there with an error. + pd.testing.assert_frame_equal( + result.to_dataframe("event_study"), + result.aggregate("event_study").to_dataframe(), + ) with pytest.raises(ValueError, match=r"not stored on 4.0 fits.*aggregate\('group'\)"): result.to_dataframe("group") diff --git a/tests/test_had.py b/tests/test_had.py index d941233cf..6a08cbbb9 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -50,6 +50,7 @@ _validate_had_panel_event_study, ) from diff_diff.local_linear import bias_corrected_local_linear +from diff_diff.results_base import EVENT_STUDY_SCHEMA from tests.conftest import assert_nan_inference # ============================================================================= @@ -2589,16 +2590,13 @@ def test_to_dataframe_shape(self): result = self._fit() df = result.to_dataframe() assert len(df) == len(result.event_times) - assert set(df.columns) == { - "event_time", - "att", - "se", - "t_stat", - "p_value", - "conf_int_low", - "conf_int_high", - "n_obs", - } + # 4.0 (M-093 8b): the unified EVENT_STUDY_SCHEMA frame; the estimand + # column carries the WAS label, and no reference row is synthesized. + assert list(df.columns) == list(EVENT_STUDY_SCHEMA) + assert set(df["estimand"]) == {result.target_parameter} + assert not df["is_reference"].any() + with pytest.raises(ValueError, match="Use 'event_study'"): + result.to_dataframe(level="periods") def test_to_dict_shape(self): result = self._fit() diff --git a/tests/test_imputation.py b/tests/test_imputation.py index 699e7b68c..925e017c7 100644 --- a/tests/test_imputation.py +++ b/tests/test_imputation.py @@ -397,8 +397,12 @@ def test_to_dataframe_errors(self): with pytest.raises(ValueError, match="Unknown level"): results.to_dataframe("invalid") - with pytest.raises(ValueError, match="Event study effects not stored on this fit"): - results.to_dataframe("event_study") + # 4.0 (M-093 8b): the analytical fit recomputes the event study from + # the retained kit instead of raising. + pd.testing.assert_frame_equal( + results.to_dataframe("event_study"), + results.aggregate("event_study").to_dataframe(), + ) def test_get_params(self): """Test get_params returns all constructor parameters.""" diff --git a/tests/test_lpdid.py b/tests/test_lpdid.py index 460d434fa..18bb65439 100644 --- a/tests/test_lpdid.py +++ b/tests/test_lpdid.py @@ -23,6 +23,8 @@ import pandas as pd import pytest +from diff_diff.results_base import EVENT_STUDY_SCHEMA + sys.path.insert(0, str(Path(__file__).resolve().parent)) from _dgp_utils import make_lpdid_panel # noqa: E402 from legacy_surface import es_dict # noqa: E402 @@ -278,7 +280,20 @@ def test_rejects_pooled_horizon_outside_supported_window(self): ) def test_results_to_dataframe_and_repr(self): - df = pd.DataFrame({"horizon": [0], "coefficient": [1.0], "se": [0.1]}) + # 4.0 (M-093 8b): level="event_study" is the unified EVENT_STUDY_SCHEMA + # frame built from the native per-horizon frame (horizon -1 = reference). + df = pd.DataFrame( + { + "horizon": [-1, 0], + "coefficient": [0.0, 1.0], + "se": [np.nan, 0.1], + "t_stat": [np.nan, 10.0], + "p_value": [np.nan, 0.0], + "conf_low": [np.nan, 0.8], + "conf_high": [np.nan, 1.2], + "n_obs": [np.nan, 10], + } + ) results = LPDiDResults( event_study=df, pooled=None, @@ -292,12 +307,31 @@ def test_results_to_dataframe_and_repr(self): no_composition=False, pmd=None, ) - assert results.to_dataframe(level="event_study").equals(df) + out = results.to_dataframe(level="event_study") + assert list(out.columns) == list(EVENT_STUDY_SCHEMA) + assert out["event_time"].tolist() == [-1, 0] + assert out["is_reference"].tolist() == [True, False] + assert out["att"].tolist()[1] == 1.0 with pytest.raises(ValueError, match="not computed"): results.to_dataframe(level="pooled") with pytest.raises(ValueError, match="level must be"): results.to_dataframe(level="bad") assert "LPDiDResults" in repr(results) + absent = LPDiDResults( + event_study=None, + pooled=None, + n_obs=10, + n_treated_units=4, + n_control_units=6, + pre_window=2, + post_window=0, + control_group="clean", + reweight=False, + no_composition=False, + pmd=None, + ) + with pytest.raises(ValueError, match="event_study dataframe was not computed"): + absent.to_dataframe(level="event_study") def test_results_summary_and_to_dict_run(self): df = make_lpdid_panel(cohorts=(4,), n_per_cohort=15, n_never=15, n_periods=8, seed=11) diff --git a/tests/test_stacked_did.py b/tests/test_stacked_did.py index 646994b1d..96b65658f 100644 --- a/tests/test_stacked_did.py +++ b/tests/test_stacked_did.py @@ -10,6 +10,7 @@ from diff_diff import StackedDiD, StackedDiDResults from diff_diff.prep_dgp import generate_staggered_data +from diff_diff.results_base import EVENT_STUDY_SCHEMA # ============================================================================= # Test Data Fixtures @@ -764,8 +765,10 @@ def test_to_dataframe_event_study(self, staggered_data): ) df = results.to_dataframe(level="event_study") assert isinstance(df, pd.DataFrame) - assert "relative_period" in df.columns - assert "effect" in df.columns + # 4.0 (M-093 8b): the unified EVENT_STUDY_SCHEMA frame, bit-equal to + # the aggregate() container's. + assert list(df.columns) == list(EVENT_STUDY_SCHEMA) + pd.testing.assert_frame_equal(df, results.aggregate("event_study").to_dataframe()) def test_to_dataframe_group_raises(self, staggered_data): """to_dataframe(level='group') raises ValueError.""" @@ -791,7 +794,8 @@ def test_to_dataframe_event_study_total_on_plain_fit(self, staggered_data): first_treat="first_treat", ) df = results.to_dataframe(level="event_study") - assert sorted(df["relative_period"].tolist()) == [-2, -1, 0, 1, 2] + assert sorted(df["event_time"].tolist()) == [-2, -1, 0, 1, 2] + assert df.loc[df["is_reference"], "event_time"].tolist() == [-1] def test_to_dataframe_legacy_pickle_none_branch(self, staggered_data): """The effects-None branch survives for pre-3.9 pickles only.""" diff --git a/tests/test_staggered.py b/tests/test_staggered.py index 42d284e53..97187c98d 100644 --- a/tests/test_staggered.py +++ b/tests/test_staggered.py @@ -329,16 +329,21 @@ def test_invalid_level_error(self): with pytest.raises(ValueError, match="Unknown level"): results.to_dataframe(level="invalid") - def test_event_study_not_computed_error(self): - """Test error when event study not computed.""" + def test_event_study_level_routes_to_aggregate(self): + """4.0 (M-093 8b): level="event_study" relays the aggregate() container + (kit recompute) instead of raising; level="group" still raises.""" data = generate_staggered_data() cs = CallawaySantAnna() results = cs.fit( data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) + pd.testing.assert_frame_equal( + results.to_dataframe(level="event_study"), + results.aggregate("event_study").to_dataframe(), + ) with pytest.raises(ValueError, match="not stored on 4.0 fits"): - results.to_dataframe(level="event_study") + results.to_dataframe(level="group") def generate_staggered_data_with_covariates( diff --git a/tests/test_staggered_triple_diff.py b/tests/test_staggered_triple_diff.py index 442930a4e..a49d85dd6 100644 --- a/tests/test_staggered_triple_diff.py +++ b/tests/test_staggered_triple_diff.py @@ -8,6 +8,7 @@ """ import numpy as np +import pandas as pd import pytest from diff_diff import ( @@ -452,9 +453,8 @@ def test_event_study_dataframe(self, simple_data): assert col in df.columns assert len(df) == len(es.event_time) np.testing.assert_allclose(df["att"].to_numpy(), es.att, rtol=0, atol=0) - # The fit-time event-study table is gone from the results container. - with pytest.raises(ValueError, match="not stored on 4.0 fits"): - res.to_dataframe("event_study") + # 4.0 (M-093 8b): the results-level frame relays the container. + pd.testing.assert_frame_equal(res.to_dataframe("event_study"), df) def test_event_study_and_group_both_available(self, simple_data): # The former aggregate='all' fit: both surfaces come from the same result. diff --git a/tests/test_sun_abraham.py b/tests/test_sun_abraham.py index 2e0539c7a..8eb60bdc4 100644 --- a/tests/test_sun_abraham.py +++ b/tests/test_sun_abraham.py @@ -10,6 +10,7 @@ from legacy_surface import es_dict from scipy import stats +from diff_diff.results_base import EVENT_STUDY_SCHEMA from diff_diff.sun_abraham import SunAbraham, SunAbrahamResults @@ -244,9 +245,10 @@ def test_to_dataframe(self): # Event study DataFrame df_es = results.to_dataframe(level="event_study") - assert "relative_period" in df_es.columns - assert "effect" in df_es.columns - assert "se" in df_es.columns + # 4.0 (M-093 8b): the unified EVENT_STUDY_SCHEMA frame; the omitted + # reference period appears as the single is_reference row. + assert list(df_es.columns) == list(EVENT_STUDY_SCHEMA) + assert int(df_es["is_reference"].sum()) == 1 def test_get_set_params(self): """Test sklearn-compatible parameter access.""" diff --git a/tests/test_two_stage.py b/tests/test_two_stage.py index 13e33466c..7027b900f 100644 --- a/tests/test_two_stage.py +++ b/tests/test_two_stage.py @@ -241,9 +241,8 @@ def test_to_dataframe_event_study(self): first_treat="first_treat", ) - with pytest.raises(ValueError, match=r"aggregate\('event_study'\)"): - results.to_dataframe("event_study") - df = results.aggregate("event_study").to_dataframe() + df = results.to_dataframe("event_study") + pd.testing.assert_frame_equal(df, results.aggregate("event_study").to_dataframe()) assert isinstance(df, pd.DataFrame) assert "event_time" in df.columns assert "att" in df.columns @@ -295,15 +294,22 @@ def test_to_dataframe_invalid_level(self): results.to_dataframe("invalid") def test_to_dataframe_no_event_study(self): - """to_dataframe raises on an analytic fit (no fit-time event-study - surface) and steers to the post-fit aggregate() route.""" + """The default level is event_study: an analytic fit recomputes it + from the retained kit (M-093 8b); a kit-less legacy pickle raises.""" + import copy + data = generate_test_data() results = TwoStageDiD().fit( data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - with pytest.raises(ValueError, match="not stored on this fit"): - results.to_dataframe("event_study") + pd.testing.assert_frame_equal( + results.to_dataframe(), results.aggregate("event_study").to_dataframe() + ) + legacy = copy.copy(results) + object.__setattr__(legacy, "_aggregation_kit", None) + with pytest.raises(ValueError, match="carries no aggregation kit"): + legacy.to_dataframe("event_study") def test_repr(self): """Test __repr__ contains expected elements.""" diff --git a/tests/test_v4_event_study_schema.py b/tests/test_v4_event_study_schema.py new file mode 100644 index 000000000..9523bf7d7 --- /dev/null +++ b/tests/test_v4_event_study_schema.py @@ -0,0 +1,611 @@ +"""Item 8b of the 4.0 cut - the unified ``to_dataframe(level="event_study")`` schema (ledger row M-093, second half). + +Since 4.0 every results class's ``to_dataframe(level="event_study")`` emits the +``EVENT_STUDY_SCHEMA`` columns (``results_base.EventStudyResults.to_dataframe``) +as an identical PREFIX: the ``AggregationMixin`` classes return +``aggregate("event_study").to_dataframe()``; SunAbraham, LPDiD, WooldridgeDiD +and SpilloverDiD return ``build_event_study_surface(self).to_dataframe()``. +Only two producers append documented fit-configuration columns after the +schema - LWDiD (``rolling``, ``estimation_method``, ``vcov_type``; row M-153) +and WooldridgeDiD (``cohort_trends``, ``aggregation_weights``). The 3.x +bespoke frames (``relative_period`` / ``effect`` / ``att_glob`` / ``horizon`` / +``conf_int_lo`` / ``n_obs`` ...) and the "not stored on 4.0 fits" raises on +CallawaySantAnna / EfficientDiD / TripleDifference and on analytical +ImputationDiD / TwoStageDiD / ContinuousDiD are gone. + +The first half of M-093 (the reference-row sentinel retirement) is pinned by +``tests/test_v4_sentinel_retirement.py``. +""" + +import copy +import dataclasses +import warnings +from typing import Any, Dict + +import numpy as np +import pandas as pd +import pytest +from pandas.testing import assert_frame_equal + +from diff_diff import ( + CallawaySantAnna, + ChaisemartinDHaultfoeuille, + ContinuousDiD, + DMLDiD, + DurationDiD, + EfficientDiD, + ImputationDiD, + LPDiD, + LPDiDResults, + LWDiD, + StackedDiD, + SunAbraham, + TripleDifference, + TwoStageDiD, + TwoWayFixedEffects, + WooldridgeDiD, + generate_continuous_did_data, + generate_ddd_data, + generate_reversible_did_data, + generate_staggered_data, + generate_staggered_ddd_data, + load_mpdta, +) +from diff_diff.results_base import EVENT_STUDY_SCHEMA, build_event_study_surface +from diff_diff.spillover import SpilloverDiD +from tests._dgp_utils import generate_butts_staggered_dgp +from tests.test_duration_did import simulate_panel +from tests.test_event_study_surface import ( + _bjs_panel, + _efficient_panel, + _had_event_study, + _lpdid_panel, + _sa_panel, + _two_group_calendar_panel, +) + +THIS_TEST_REF = "tests/test_v4_event_study_schema.py" +SCHEMA = list(EVENT_STUDY_SCHEMA) +N_SCHEMA = len(SCHEMA) +_PANEL_KW = dict(outcome="outcome", unit="unit", time="period", first_treat="first_treat") +_BJS_KW = dict(outcome="outcome", unit="unit", time="time", first_treat="first_treat") + +# Producers that append documented fit-configuration columns after the schema. +TRAILING = { + "wool": ["cohort_trends", "aggregation_weights"], + "lwdid": ["rolling", "estimation_method", "vcov_type"], +} +# Producers WITHOUT the AggregationMixin: compared against the builder frame. +NON_MIXIN = {"sa", "lpdid", "spill_es", "wool"} + + +def _quiet(fn, *args, **kwargs): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return fn(*args, **kwargs) + + +# --------------------------------------------------------------------------- # +# One fresh fit per producer (module-scoped) +# --------------------------------------------------------------------------- # + + +@pytest.fixture(scope="module") +def cs_varying(): + d = generate_staggered_data(n_units=120, n_periods=8, cohort_periods=[4, 6], seed=7) + return _quiet(CallawaySantAnna(n_bootstrap=0).fit, d, **_PANEL_KW) + + +@pytest.fixture(scope="module") +def cs_universal(): + d = generate_staggered_data(n_units=120, n_periods=8, cohort_periods=[4, 6], seed=7) + return _quiet(CallawaySantAnna(n_bootstrap=0, base_period="universal").fit, d, **_PANEL_KW) + + +@pytest.fixture(scope="module") +def cs_boot(): + d = generate_staggered_data(n_units=120, n_periods=8, cohort_periods=[4, 6], seed=7) + return _quiet(CallawaySantAnna(n_bootstrap=49, seed=11).fit, d, **_PANEL_KW) + + +def _dml_panel(n_units=120, n_periods=5, seed=0): + rng = np.random.default_rng(seed) + cohort = rng.choice([0, 3, 4], size=n_units, p=[0.4, 0.3, 0.3]) + rows = [] + for i in range(n_units): + x1, x2 = rng.normal(), rng.normal() + u = rng.normal() + for t in range(1, n_periods + 1): + y = u + 0.5 * t + 0.3 * x1 - 0.2 * x2 + rng.normal(scale=0.5) + if cohort[i] > 0 and t >= cohort[i]: + y += 1.5 + rows.append((i, t, cohort[i], y, x1, x2)) + return pd.DataFrame(rows, columns=["unit", "time", "first_treat", "y", "x1", "x2"]) + + +@pytest.fixture(scope="module") +def dml(): + return _quiet( + DMLDiD(seed=0).fit, + _dml_panel(), + outcome="y", + unit="unit", + time="time", + first_treat="first_treat", + covariates=["x1", "x2"], + ) + + +@pytest.fixture(scope="module") +def sa(): + return _quiet(SunAbraham().fit, _sa_panel(), **_BJS_KW) + + +@pytest.fixture(scope="module") +def imp(): + return _quiet(ImputationDiD().fit, _bjs_panel(), **_BJS_KW) + + +@pytest.fixture(scope="module") +def imp_boot(): + return _quiet(ImputationDiD(n_bootstrap=49, seed=42).fit, _bjs_panel(), **_BJS_KW) + + +@pytest.fixture(scope="module") +def ts(): + return _quiet(TwoStageDiD().fit, _bjs_panel(), **_BJS_KW) + + +@pytest.fixture(scope="module") +def ts_boot(): + return _quiet(TwoStageDiD(n_bootstrap=49, seed=42).fit, _bjs_panel(), **_BJS_KW) + + +@pytest.fixture(scope="module") +def stacked(): + d = generate_staggered_data(n_units=120, n_periods=12, cohort_periods=[4, 6, 8], seed=42) + return _quiet(StackedDiD(kappa_pre=2, kappa_post=2).fit, d, **_PANEL_KW) + + +def _spillover(event_study): + d = generate_butts_staggered_dgp(seed=42) + kw: Dict[str, Any] = dict( + rings=[0.0, 50.0, 200.0], + d_bar=200.0, + conley_coords=("lat", "lon"), + conley_metric="haversine", + conley_cutoff_km=200.0, + conley_lag_cutoff=0, + ) + if event_study: + kw.update(event_study=True, horizon_max=2) + return _quiet( + SpilloverDiD(**kw).fit, d, outcome="y", unit="unit", time="time", first_treat="first_treat" + ) + + +@pytest.fixture(scope="module") +def spill_es(): + return _spillover(True) + + +@pytest.fixture(scope="module") +def spill_agg(): + return _spillover(False) + + +def _cont_panel(): + return generate_continuous_did_data( + n_units=200, n_periods=5, cohort_periods=[2, 4], seed=42, noise_sd=0.5 + ) + + +@pytest.fixture(scope="module") +def cont(): + return _quiet( + ContinuousDiD(control_group="not_yet_treated", n_bootstrap=0).fit, + _cont_panel(), + "outcome", + "unit", + "period", + "first_treat", + "dose", + ) + + +@pytest.fixture(scope="module") +def cont_boot(): + return _quiet( + ContinuousDiD(control_group="not_yet_treated", n_bootstrap=49, seed=42).fit, + _cont_panel(), + "outcome", + "unit", + "period", + "first_treat", + "dose", + ) + + +@pytest.fixture(scope="module") +def edid(): + return _quiet( + EfficientDiD().fit, + _efficient_panel(), + outcome="y", + unit="unit", + time="time", + first_treat="first_treat", + ) + + +def _wooldridge(): + return _quiet( + WooldridgeDiD(control_group="never_treated").fit, + load_mpdta(), + outcome="lemp", + unit="countyreal", + time="year", + first_treat="first_treat", + ) + + +@pytest.fixture(scope="module") +def wool(): + res = _wooldridge() + res.aggregate("event_study") + return res + + +@pytest.fixture(scope="module") +def wool_fresh(): + return _wooldridge() + + +@pytest.fixture(scope="module") +def ddd_stag(): + d = generate_staggered_ddd_data(n_units=300, treatment_effect=3.0, seed=42) + return _quiet(TripleDifference().fit, d, partition="eligibility", **_PANEL_KW) + + +@pytest.fixture(scope="module") +def ddd_2x2x2(): + d = generate_ddd_data( + n_per_cell=200, + treatment_effect=2.0, + group_effect=1.0, + partition_effect=0.5, + time_effect=0.7, + noise_sd=1.0, + seed=42, + ) + return _quiet( + TripleDifference().fit, + d, + outcome="outcome", + group="group", + partition="partition", + post="time", + ) + + +@pytest.fixture(scope="module") +def dcdh(): + d = generate_reversible_did_data(n_groups=50, n_periods=10, pattern="joiners_only", seed=42) + return _quiet( + ChaisemartinDHaultfoeuille(twfe_diagnostic=False, placebo=True).fit, + d, + outcome="outcome", + unit="group", + time="period", + treatment="treatment", + L_max=3, + ) + + +@pytest.fixture(scope="module") +def lpdid(): + return _quiet( + LPDiD(pre_window=2, post_window=2).fit, + _lpdid_panel(), + outcome="y", + unit="unit", + time="time", + treatment="treat", + ) + + +@pytest.fixture(scope="module") +def had_es(): + return _had_event_study()[1] + + +@pytest.fixture(scope="module") +def duration(): + return _quiet( + DurationDiD(n_bootstrap=60, seed=3).fit, + simulate_panel(), + outcome="exited", + unit="unit", + time="time", + treatment="treated", + last_pre_period=4, + ) + + +@pytest.fixture(scope="module") +def lwdid(): + rng = np.random.default_rng(29) + rows = [] + for u in range(30): + g = 3 if u < 10 else (4 if u < 20 else 0) + alpha = rng.normal() + for t in range(1, 7): + d = int(g > 0 and t >= g) + rows.append(dict(unit=u, time=t, first=g, treat=d, y=alpha + rng.normal() + d)) + return _quiet( + LWDiD().fit, + pd.DataFrame(rows), + outcome="y", + unit="unit", + time="time", + treatment="treat", + first_treat="first", + ) + + +@pytest.fixture(scope="module") +def twfe_es(): + return _quiet( + TwoWayFixedEffects().fit, + _two_group_calendar_panel(), + outcome="outcome", + unit="unit", + time="period", + treatment="treated", + event_study=True, + spec="within", + post_periods=[3, 4, 5], + reference_period=2, + ) + + +PRODUCERS = [ + "cs_varying", + "cs_universal", + "cs_boot", + "dml", + "sa", + "imp", + "imp_boot", + "ts", + "ts_boot", + "stacked", + "spill_es", + "cont", + "cont_boot", + "edid", + "wool", + "ddd_stag", + "dcdh", + "lpdid", + "had_es", + "duration", + "lwdid", +] + + +def _frame(res): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return res.to_dataframe(level="event_study") + + +# --------------------------------------------------------------------------- # +# (1) The schema is an identical prefix on every producer +# --------------------------------------------------------------------------- # + + +@pytest.mark.parametrize("producer", PRODUCERS) +def test_prefix_schema_and_trailing_config_columns(request, producer): + res = request.getfixturevalue(producer) + cols = list(_frame(res).columns) + assert cols[:N_SCHEMA] == SCHEMA + assert cols[N_SCHEMA:] == TRAILING.get(producer, []) + + +def test_twfe_event_study_mode_is_the_container_itself(twfe_es): + # The merged TWFE event-study mode RETURNS EventStudyResults: its level-less + # to_dataframe() is the schema frame every other class now relays. + assert list(twfe_es.to_dataframe().columns) == SCHEMA + + +# --------------------------------------------------------------------------- # +# (2) The frame IS the aggregate / builder frame (bit-equal) +# --------------------------------------------------------------------------- # + + +@pytest.mark.parametrize("producer", PRODUCERS) +def test_equals_aggregate_or_builder_route(request, producer): + res = request.getfixturevalue(producer) + df = _frame(res) + if producer in NON_MIXIN: + expected = build_event_study_surface(res).to_dataframe() + else: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + expected = res.aggregate("event_study").to_dataframe() + assert_frame_equal(df.iloc[:, :N_SCHEMA], expected) + if producer == "wool": + # Config columns are constant per fit; the weighting scheme is the + # stored event-study aggregation's ("cell" by default). + assert df["cohort_trends"].eq(res.cohort_trends).all() + assert df["aggregation_weights"].eq("cell").all() + if producer == "lwdid": + assert df["rolling"].nunique() == 1 + assert df["vcov_type"].eq(res.vcov_type).all() + + +@pytest.mark.parametrize("producer", ["ts", "stacked", "sa", "lpdid", "wool"]) +def test_default_level_is_event_study_where_it_was(request, producer): + # These classes default to level="event_study": a plain to_dataframe() is + # the unified frame (on analytical TwoStageDiD it now RECOMPUTES from the + # kit instead of raising). + res = request.getfixturevalue(producer) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + assert_frame_equal(res.to_dataframe(), res.to_dataframe(level="event_study")) + + +# --------------------------------------------------------------------------- # +# (3) Reference / estimand / count spot checks on the unified frame +# --------------------------------------------------------------------------- # + + +@pytest.mark.parametrize("producer", ["stacked", "imp_boot", "ts_boot", "spill_es", "duration"]) +def test_reference_row_at_minus_one_with_nan_count(request, producer): + df = _frame(request.getfixturevalue(producer)) + ref = df["is_reference"].to_numpy(dtype=bool) + assert df.loc[ref, "event_time"].tolist() == [-1] + assert df.loc[ref, "att"].tolist() == [0.0] + assert np.isnan(df.loc[ref, "n"]).all() + if producer != "duration": + assert np.isfinite(df.loc[~ref, "n"].to_numpy(dtype=float)).all() + + +def test_sun_abraham_synthesizes_the_omitted_reference(sa): + df = _frame(sa) + ref = df["is_reference"].to_numpy(dtype=bool) + assert df.loc[ref, "event_time"].tolist() == [-1] + assert np.isnan(df.loc[ref, "n"]).all() + # SunAbraham records the contributing-cohort count on every estimated row. + assert np.isfinite(df.loc[~ref, "n"].to_numpy(dtype=float)).all() + + +def test_dcdh_frame_is_the_paper_convention_without_native_labels(dcdh): + df = _frame(dcdh) + ref = df["is_reference"].to_numpy(dtype=bool) + assert df.loc[ref, "event_time"].tolist() == [0] + assert (df["event_time"] < 0).any() # placebo rows + assert (df["event_time"] > 0).any() + assert set(df["estimand"]) == {"att"} # the DID^pl_l / "ref" / DID_l labels left + # The retired native row shape is gone. + assert "horizon" not in df.columns and "effect" not in df.columns + assert np.isnan(df.loc[df["event_time"] < 0, "n"]).sum() == 0 + + +def test_had_frame_carries_the_was_estimand_and_no_reference(had_es): + df = _frame(had_es) + assert set(df["estimand"]) == {had_es.target_parameter} + assert not df["is_reference"].any() + assert len(df) == len(had_es.event_times) + + +def test_wooldridge_frame_has_no_reference_row_and_nan_counts(wool): + df = _frame(wool) + assert not df["is_reference"].any() + assert np.isnan(df["n"]).all() + assert np.isnan(df["df"]).all() + + +def test_spillover_default_level_is_the_inherited_headline_frame(spill_es): + assert_frame_equal(spill_es.to_dataframe(), pd.DataFrame([spill_es.to_dict()])) + + +# --------------------------------------------------------------------------- # +# (4) Error contract +# --------------------------------------------------------------------------- # + + +def test_wooldridge_before_aggregate_raises_instead_of_an_empty_frame(wool_fresh): + pat = r"carries no event-study surface - call results\.aggregate\('event_study'\) first" + with pytest.raises(ValueError, match=pat): + wool_fresh.to_dataframe() + with pytest.raises(ValueError, match=pat): + wool_fresh.to_dataframe(level="event_study") + + +def test_spillover_aggregate_mode_raises(spill_agg): + assert spill_agg.event_study_effects is None + with pytest.raises(ValueError, match="refit with event_study=True"): + spill_agg.to_dataframe(level="event_study") + + +def test_triple_difference_2x2x2_raises_the_single_att_error(ddd_2x2x2): + with pytest.raises( + ValueError, match=r"aggregate\('event_study'\): the 2x2x2 TripleDifference design" + ): + ddd_2x2x2.to_dataframe(level="event_study") + + +def _lpdid_results(event_study): + return LPDiDResults( + event_study=event_study, + pooled=None, + n_obs=10, + n_treated_units=4, + n_control_units=6, + pre_window=2, + post_window=0, + control_group="clean", + reweight=False, + no_composition=False, + pmd=None, + ) + + +_LPDID_COLUMNS = [ + "horizon", + "coefficient", + "se", + "t_stat", + "p_value", + "conf_low", + "conf_high", + "n_obs", +] + + +def test_lpdid_absent_and_empty_native_frames_raise(): + with pytest.raises(ValueError, match="event_study dataframe was not computed"): + _lpdid_results(None).to_dataframe(level="event_study") + # A zero-row stored frame used to pass through as an empty frame; it now + # raises the builder's absent-surface hint. + with pytest.raises(ValueError, match="refit with only_pooled=False"): + _lpdid_results(pd.DataFrame(columns=_LPDID_COLUMNS)).to_dataframe(level="event_study") + + +def test_stacked_legacy_pickle_without_surface_raises_the_refit_hint(stacked): + legacy = dataclasses.replace(stacked, event_study_effects=None) + with pytest.raises(ValueError, match=r"re-fit with diff-diff >= 3\.9"): + legacy.to_dataframe(level="event_study") + + +def test_kit_less_legacy_pickle_raises_through_to_dataframe(ts): + legacy = copy.copy(ts) + object.__setattr__(legacy, "_aggregation_kit", None) + with pytest.raises(ValueError, match="carries no aggregation kit"): + legacy.to_dataframe() # the default level is event_study + + +def test_bad_levels_on_the_newly_leveled_classes(had_es, spill_es, duration): + with pytest.raises(ValueError, match="Use 'event_study'"): + had_es.to_dataframe(level="periods") + with pytest.raises(ValueError, match="Use 'simple' or 'event_study'"): + spill_es.to_dataframe(level="group") + with pytest.raises(ValueError, match="'periods', 'att' or 'event_study'"): + duration.to_dataframe(level="bogus") + + +# --------------------------------------------------------------------------- # +# (5) Ledger +# --------------------------------------------------------------------------- # + + +def test_ledger_row_is_done_and_points_here(): + from tests.test_v4_matrix import ROWS as LEDGER_ROWS + + row = {r["id"]: r for r in LEDGER_ROWS}["M-093"] + assert row["kind"] == "behavior" + assert row["status"] == "done" + assert row["test_ref"] == THIS_TEST_REF + assert "TENTH AMENDMENT" in row["notes"] diff --git a/tests/test_v4_sentinel_retirement.py b/tests/test_v4_sentinel_retirement.py index 192f6b18c..937634990 100644 --- a/tests/test_v4_sentinel_retirement.py +++ b/tests/test_v4_sentinel_retirement.py @@ -11,8 +11,9 @@ ``_deprecation.migrate_event_study_reference_sentinel``. The second half of M-093 (every results class's ``to_dataframe(level= -"event_study")`` emitting the unified schema) is item 8b; the row flips to -``done`` there. +"event_study")`` emitting the unified schema) shipped as item 8b and is pinned +by ``tests/test_v4_event_study_schema.py``, the row's ``test_ref``; the row is +``done``. ``THIS_TEST_REF`` below names this file for the 8a pins only. """ import copy @@ -34,7 +35,7 @@ ) from diff_diff._deprecation import migrate_event_study_reference_sentinel from diff_diff.prep_dgp import generate_staggered_data -from diff_diff.results_base import build_event_study_surface +from diff_diff.results_base import EVENT_STUDY_SCHEMA, build_event_study_surface from diff_diff.spillover import SpilloverDiD from tests._dgp_utils import generate_butts_staggered_dgp @@ -200,15 +201,16 @@ def test_spillover_frames_carry_is_reference_with_nan_n_obs(spillover_fit): @pytest.mark.parametrize( "fixture_name", ["stacked_fit", "imputation_boot_fit", "twostage_boot_fit"] ) -def test_native_to_dataframe_event_study_reference_n_obs_is_nan(request, fixture_name): - # The 8a visible change on the native level frames (8b replaces them with - # the unified schema): the reference row's n_obs renders NaN, not 0. +def test_native_to_dataframe_event_study_reference_n_is_nan(request, fixture_name): + # Since 8b the level frame IS the unified schema: the reference row is the + # is_reference row (n NaN by contract), never a zero count. res = request.getfixturevalue(fixture_name) df = res.to_dataframe(level="event_study") - ref_mask = df["relative_period"] == -1 - assert ref_mask.sum() == 1 - assert np.isnan(float(df.loc[ref_mask, "n_obs"].iloc[0])) - assert np.all(np.isfinite(df.loc[~ref_mask, "n_obs"].to_numpy(dtype=float))) + assert list(df.columns) == list(EVENT_STUDY_SCHEMA) + ref_mask = df["is_reference"].to_numpy(dtype=bool) + assert df.loc[ref_mask, "event_time"].tolist() == [-1] + assert np.isnan(float(df.loc[ref_mask, "n"].iloc[0])) + assert np.all(np.isfinite(df.loc[~ref_mask, "n"].to_numpy(dtype=float))) # --------------------------------------------------------------------------- # @@ -570,11 +572,11 @@ def test_bootstrap_override_skips_reference_row(request, fixture_name): # --------------------------------------------------------------------------- # -def test_ledger_row_points_here_and_stays_planned_until_8b(): +def test_ledger_row_done_and_points_at_the_8b_schema_file(): from tests.test_v4_matrix import ROWS as LEDGER_ROWS row = {r["id"]: r for r in LEDGER_ROWS}["M-093"] assert row["kind"] == "behavior" - assert row["status"] == "planned" # flips to done in 8b (the schema rewire) - assert row["test_ref"] == THIS_TEST_REF + assert row["status"] == "done" + assert row["test_ref"] == "tests/test_v4_event_study_schema.py" assert row["old"] == "diff_diff:CallawaySantAnnaResults.event_study_effects" diff --git a/tests/test_v4_storage_flip.py b/tests/test_v4_storage_flip.py index f3a876379..fbd94d6d9 100644 --- a/tests/test_v4_storage_flip.py +++ b/tests/test_v4_storage_flip.py @@ -113,11 +113,12 @@ class (the subclass for ``DMLDiDResults``) and relays the canonical value; StackedDiDResults: ("event_study", "group"), EfficientDiDResults: ("group_time", "event_study", "group"), WooldridgeDiDResults: ("simple", "group", "calendar", "event_study", "gt"), - ChaisemartinDHaultfoeuilleResults: ("overall",), + ChaisemartinDHaultfoeuilleResults: ("overall", "event_study"), ContinuousDiDResults: ("dose_response", "group_time", "event_study"), } # Classes whose fits may legitimately refuse a level because the surface -# was not stored on a 4.0 fit (the documented ValueError). +# was not stored on a 4.0 fit (the documented ValueError) - since M-093 8b +# only the "group" level does so (event_study relays aggregate()). NOT_STORED_RE = re.compile( r"not stored|aggregate\(|not available|not supported|no .* surface|None", re.I ) diff --git a/tests/test_v4_unify_ddd.py b/tests/test_v4_unify_ddd.py index 755a270f6..7cfa2ccab 100644 --- a/tests/test_v4_unify_ddd.py +++ b/tests/test_v4_unify_ddd.py @@ -542,9 +542,16 @@ def test_to_dataframe_levels(self, stag_fit, cross_fit): assert cols_expected in cross_fit.to_dataframe().columns with pytest.raises(ValueError, match="fit the staggered design"): cross_fit.to_dataframe("group_time") - for level in ("event_study", "group"): - with pytest.raises(ValueError, match="not stored on 4.0 fits"): - stag_fit.to_dataframe(level) + # 4.0 (M-093 8b): level="event_study" relays the aggregate() container; + # level="group" still raises; the 2x2x2 design has no event study. + pd.testing.assert_frame_equal( + stag_fit.to_dataframe("event_study"), + stag_fit.aggregate("event_study").to_dataframe(), + ) + with pytest.raises(ValueError, match="not stored on 4.0 fits"): + stag_fit.to_dataframe("group") + with pytest.raises(ValueError, match="2x2x2 TripleDifference design"): + cross_fit.to_dataframe("event_study") with pytest.raises(ValueError, match="Unknown level"): stag_fit.to_dataframe("bogus") diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index 045611bec..3934bab4e 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -7,6 +7,7 @@ import pandas as pd import pytest +from diff_diff.results_base import EVENT_STUDY_SCHEMA from diff_diff.wooldridge import ( WooldridgeDiD, _build_interaction_matrix, @@ -84,6 +85,19 @@ def test_to_dataframe_event(self): df = r.to_dataframe("event_study") assert isinstance(df, pd.DataFrame) assert "att" in df.columns + # 4.0 (M-093 8b): the unified EVENT_STUDY_SCHEMA prefix plus the two + # Wooldridge fit-configuration columns. + assert list(df.columns) == list(EVENT_STUDY_SCHEMA) + [ + "cohort_trends", + "aggregation_weights", + ] + + def test_to_dataframe_event_before_aggregate_raises(self): + # Through 3.x this returned an EMPTY frame (a silent failure); the + # weighting scheme changes the numbers, so 4.0 raises instead. + r = _make_minimal_results() + with pytest.raises(ValueError, match=r"aggregate\('event_study'\) first"): + r.to_dataframe("event_study") def test_aggregate_simple_returns_self(self): r = _make_minimal_results() @@ -389,7 +403,7 @@ def test_summary_runs(self, fitted): def test_to_dataframe_event(self, fitted): fitted.aggregate("event_study") df = fitted.to_dataframe("event_study") - assert "relative_period" in df.columns + assert "event_time" in df.columns assert "att" in df.columns def test_to_dataframe_gt(self, fitted):