Skip to content

Commit 2437e80

Browse files
authored
feat(v4): PR 5b of the 4.0 cut - flip the headline-quintet storage on the nine M-050..M-058 containers (#877)
1 parent eb59806 commit 2437e80

57 files changed

Lines changed: 1848 additions & 1092 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Breaking Changes
2+
3+
- **4.0 cut, storage flip of the headline quintet (`M-050`..`M-058`).** `CallawaySantAnnaResults` (and `DMLDiDResults`), `SunAbrahamResults`, `ImputationDiDResults`, `TwoStageDiDResults`, `StackedDiDResults`, `EfficientDiDResults`, `WooldridgeDiDResults`, `ChaisemartinDHaultfoeuilleResults` and `ContinuousDiDResults` now store the canonical `att` / `se` / `t_stat` / `p_value` / `conf_int` as their native dataclass fields (renamed in slot, so the positional constructor layout is unchanged) and take those names as constructor keywords (`overall_att=` raises `TypeError`). The 3.x `overall_att` / `overall_se` / `overall_t_stat` / `overall_p_value` / `overall_conf_int` names - and on `ContinuousDiDResults` all nine legacy spellings, the `overall_att_*` fields and the former silent `overall_se`-style aliases - are read-only properties that emit a `FutureWarning` and are removed at 5.0; they are absent from `dataclasses.fields()`, `asdict()` and `to_dict()`. 3.x pickles load unchanged (each container's `__setstate__` rewrites the legacy keys on load). The bootstrap draw containers (`bootstrap_results.overall_att_se` and siblings) are unchanged, and `StaggeredTripleDiffResults` keeps its `overall_*` storage until the DDD container unification.
4+
- **Report schemas.** `DiagnosticReport.to_dict()` reports `headline_metric.name` and `target_parameter.headline_attribute` as `"att"` for the nine containers above (`"overall_att"` under 3.x); `DIAGNOSTIC_REPORT_SCHEMA_VERSION` 2.0 -> 3.0. `BusinessReport.to_dict()` reports `target_parameter.headline_attribute` likewise; `BUSINESS_REPORT_SCHEMA_VERSION` 3.0 -> 4.0 (its `headline` block carries no name key). `StaggeredTripleDiffResults` keeps `headline_attribute: "overall_att"` until item 6.
5+
6+
### Internal
7+
8+
- Ledger rows `M-050`..`M-058` flip to `shimmed` with `tests/test_v4_storage_flip.py` as their `test_ref`; `diff_diff._deprecation` gains `LEGACY_OVERALL_QUINTET` and `migrate_renamed_state_keys` (5.0-window infrastructure); `DMLDiDResults` re-declares the five shims so the warning names the subclass (naming-guard allowlist reason reworded); `_extract_scalar_headline`'s legacy alias row is deleted; `tests/test_result_aliases.py` pins the post-flip direction (SDDD kept in the 3.x direction); the API autosummary stubs are regenerated; the bootstrap draw containers' payload names are recorded as a REGISTRY `Note` (user decision 2026-10-26); spec item 5 carries its SHIPPED note.

‎diff_diff/_deprecation.py‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"""
4444

4545
import warnings
46-
from typing import Any
46+
from typing import Any, Dict, Mapping
4747

4848
__all__: "list[str]" = []
4949

@@ -186,3 +186,39 @@ def _get(self: Any) -> Any:
186186
_get.__name__ = old
187187
_get.__doc__ = f"Deprecated alias for :attr:`{new}` (removed in {removed_in})."
188188
return property(_get)
189+
190+
191+
#: The 3.x ``overall_*`` storage names of the nine results containers flipped
192+
#: at the 4.0 cut (ledger rows M-050..M-058), keyed to the canonical quintet
193+
#: field that now stores each value. Shared by the containers' ``__setstate__``
194+
#: pickle migration (spec section 5); ``ContinuousDiDResults`` keeps a
195+
#: module-local map for its deviant ``overall_att_*`` spellings.
196+
LEGACY_OVERALL_QUINTET: Dict[str, str] = {
197+
"overall_att": "att",
198+
"overall_se": "se",
199+
"overall_t_stat": "t_stat",
200+
"overall_p_value": "p_value",
201+
"overall_conf_int": "conf_int",
202+
}
203+
204+
205+
def migrate_renamed_state_keys(
206+
state: Mapping[str, Any], renames: Mapping[str, str]
207+
) -> Dict[str, Any]:
208+
"""Rewrite renamed storage keys of a pickled ``__dict__`` on load.
209+
210+
Returns a NEW dict (the input is never mutated). For each ``old -> new``
211+
pair the legacy key is popped when present and its value seeds the
212+
canonical key only when that key is absent - an existing canonical
213+
value wins, and the legacy key never lingers in ``__dict__`` (a
214+
``deprecated_field_property`` data descriptor would shadow it on read,
215+
but it would survive a re-pickle). Used by the ``__setstate__`` of the
216+
M-050..M-058 containers; the ``SyntheticDiDResults.__setstate__``
217+
precedent (``results.py``) is the hand-written form of the same rule.
218+
"""
219+
out = dict(state)
220+
for old, new in renames.items():
221+
if old in out:
222+
value = out.pop(old)
223+
out.setdefault(new, value)
224+
return out

‎diff_diff/_reporting_helpers.py‎

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
5151
- ``ImputationDiD`` / ``TwoStageDiD``: horizon / group tables come
5252
from post-fit ``results.aggregate('event_study'/'group')`` since
5353
3.9 (the fit-time ``aggregate`` kwarg was removed at 4.0) and never
54-
change ``overall_att``. The headline is always the sample-mean
54+
change ``att``. The headline is always the sample-mean
5555
overall ATT (per BJS 2024 Step 3 with ``w_it = 1/N_1``);
5656
disambiguate via the event-study or group aggregate if you need
5757
the horizon / group target.
58-
- ``CallawaySantAnna``: ``overall_att`` is cohort-size-weighted
58+
- ``CallawaySantAnna``: ``att`` is cohort-size-weighted
5959
across post-treatment ``ATT(g, t)`` cells regardless of any
6060
aggregation. The event-study / group tables are produced post-fit
6161
via ``results.aggregate('event_study'/'group')``; the legacy
@@ -66,13 +66,13 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
6666
both regime readings (``ATT^loc`` under PT,
6767
``ATT^glob`` under SPT) so the user can pick the
6868
interpretation that matches their assumption.
69-
- ``WooldridgeDiD``: ``overall_att`` depends on the fit-time
69+
- ``WooldridgeDiD``: ``att`` depends on the fit-time
7070
``method`` — for OLS ETWFE (``method="ols"``) it is the
7171
observation-count-weighted average of ``ATT(g, t)``
7272
coefficients from the saturated regression; for nonlinear
7373
ETWFE (``method="logit"`` / ``"poisson"``) it is the
7474
average-structural-function (ASF) contrast across cohort x
75-
time cells. Both paths preserve ``overall_att`` across
75+
time cells. Both paths preserve ``att`` across
7676
``.aggregate("event_study")`` calls (which only populate additional
7777
event-study tables).
7878
"""
@@ -123,15 +123,15 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
123123
"definition": (
124124
"A cohort-size-weighted average of group-time ATTs "
125125
"``ATT(g, t)`` across post-treatment cells (``t >= g``). "
126-
"``overall_att`` is the simple-aggregation headline regardless "
126+
"``att`` is the simple-aggregation headline regardless "
127127
"of aggregation choices; event-study and group tables are "
128128
"produced post-fit via "
129129
"``results.aggregate('event_study'/'group')`` (the legacy "
130130
"``event_study_effects`` / ``group_effects`` fields are None "
131131
"on every CallawaySantAnna fit since 4.0)."
132132
),
133133
"aggregation": "simple",
134-
"headline_attribute": "overall_att",
134+
"headline_attribute": "att",
135135
"reference": "Callaway & Sant'Anna (2021); REGISTRY.md Sec. CallawaySantAnna",
136136
}
137137

@@ -203,12 +203,12 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
203203
"anticipation=0) — " + weight_clause + " — "
204204
"where each cell is a conditional-on-covariates ATT "
205205
+ score_clause
206-
+ ". ``overall_att`` is the simple-aggregation "
206+
+ ". ``att`` is the simple-aggregation "
207207
"headline; event-study and group tables are produced "
208208
"post-fit via ``results.aggregate('event_study'/'group')``."
209209
),
210210
"aggregation": "simple",
211-
"headline_attribute": "overall_att",
211+
"headline_attribute": "att",
212212
"reference": reference,
213213
}
214214

@@ -222,7 +222,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
222222
"each event time; the headline is the resulting overall ATT."
223223
),
224224
"aggregation": "iw",
225-
"headline_attribute": "overall_att",
225+
"headline_attribute": "att",
226226
"reference": "Sun & Abraham (2021); REGISTRY.md Sec. SunAbraham",
227227
}
228228

@@ -237,12 +237,12 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
237237
"untreated observations only (BJS 2024 Step 3). Post-fit "
238238
"``results.aggregate('event_study'/'group')`` (3.9; the "
239239
"fit-time ``aggregate`` kwarg was removed at 4.0) yields the "
240-
"horizon / group tables and does NOT change ``overall_att`` "
240+
"horizon / group tables and does NOT change ``att`` "
241241
"— for the horizon or group estimand, consult the "
242242
"event-study or group aggregate directly."
243243
),
244244
"aggregation": "simple",
245-
"headline_attribute": "overall_att",
245+
"headline_attribute": "att",
246246
"reference": "Borusyak-Jaravel-Spiess (2024); REGISTRY.md Sec. ImputationDiD",
247247
}
248248

@@ -258,10 +258,10 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
258258
"equivalent to Borusyak-Jaravel-Spiess imputation. As with "
259259
"ImputationDiD, post-fit ``results.aggregate(...)`` (3.9) "
260260
"yields the additional tables and does NOT change "
261-
"``overall_att``."
261+
"``att``."
262262
),
263263
"aggregation": "simple",
264-
"headline_attribute": "overall_att",
264+
"headline_attribute": "att",
265265
"reference": "Gardner (2022); REGISTRY.md Sec. TwoStageDiD",
266266
}
267267

@@ -295,19 +295,19 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
295295
"set over the event window ``[-kappa_pre, +kappa_post]``; "
296296
"each per-horizon ``delta_h`` is the paper's "
297297
"``theta_kappa^e`` treated-share-weighted cross-event "
298-
"aggregate. The ``overall_att`` headline is the equally-"
298+
"aggregate. The ``att`` headline is the equally-"
299299
"weighted average of these per-horizon coefficients, not a "
300300
"separate cross-event weighted aggregate at the ATT level. " + control_clause
301301
),
302302
"aggregation": "stacked",
303-
"headline_attribute": "overall_att",
303+
"headline_attribute": "att",
304304
"reference": "Wing-Freedman-Hollingsworth (2024); REGISTRY.md Sec. StackedDiD",
305305
}
306306

307307
if name == "WooldridgeDiDResults":
308308
# PR #347 R4 P2: Wooldridge ETWFE has two identification paths
309309
# (REGISTRY.md splits them at Sec. WooldridgeDiD): the OLS
310-
# path computes ``overall_att`` as an observation-count-
310+
# path computes ``att`` as an observation-count-
311311
# weighted aggregation of ``ATT(g, t)`` coefficients from the
312312
# saturated regression, while the nonlinear (logit / Poisson)
313313
# paths produce an ASF-based ATT from the average-structural-
@@ -324,14 +324,14 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
324324
"definition": (
325325
"The overall ATT under OLS ETWFE (Wooldridge 2025): the "
326326
"saturated regression fits cohort x time ATT(g, t) "
327-
"coefficients, and ``overall_att`` is their "
327+
"coefficients, and ``att`` is their "
328328
"observation-count-weighted average across post-"
329329
'treatment cells. Calling ``.aggregate("event_study")`` '
330330
"populates additional event-study tables but does NOT "
331-
"change the ``overall_att`` scalar."
331+
"change the ``att`` scalar."
332332
),
333333
"aggregation": "simple",
334-
"headline_attribute": "overall_att",
334+
"headline_attribute": "att",
335335
"reference": ("Wooldridge (2025); REGISTRY.md Sec. WooldridgeDiD (OLS path)"),
336336
}
337337
return {
@@ -348,17 +348,17 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
348348
f"nonlinearity; OLS ETWFE uses the saturated-regression "
349349
f'coefficient path instead. Calling ``.aggregate("event_study")`` '
350350
f"populates additional event-study tables but does NOT "
351-
f"change the ``overall_att`` scalar."
351+
f"change the ``att`` scalar."
352352
),
353353
"aggregation": "simple",
354-
"headline_attribute": "overall_att",
354+
"headline_attribute": "att",
355355
"reference": (
356356
"Wooldridge (2023, 2025); REGISTRY.md Sec. WooldridgeDiD " "(nonlinear / ASF path)"
357357
),
358358
}
359359

360360
if name == "EfficientDiDResults":
361-
# PR #347 R7 P1: the BR/DR headline ``overall_att`` is the
361+
# PR #347 R7 P1: the BR/DR headline ``att`` is the
362362
# library's cohort-size-weighted average over post-treatment
363363
# ``(g, t)`` cells (see ``efficient_did.py`` around line 1274
364364
# and REGISTRY.md Sec. EfficientDiD). This is distinct from
@@ -369,7 +369,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
369369
# the aggregation choice for the headline scalar.
370370
pt_assumption = getattr(results, "pt_assumption", "all")
371371
library_aggregation_note = (
372-
" The BR/DR headline ``overall_att`` is the library's "
372+
" The BR/DR headline ``att`` is the library's "
373373
"cohort-size-weighted average of ATT(g, t) over post-"
374374
"treatment cells, NOT the paper's ``ES_avg`` uniform event-"
375375
"time average (see REGISTRY.md Sec. EfficientDiD for the "
@@ -386,7 +386,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
386386
"single-baseline DiD (Corollary 3.2)." + library_aggregation_note
387387
),
388388
"aggregation": "pt_post_single_baseline",
389-
"headline_attribute": "overall_att",
389+
"headline_attribute": "att",
390390
"reference": "Chen-Sant'Anna-Xie (2025) Cor. 3.2; REGISTRY.md Sec. EfficientDiD",
391391
}
392392
return {
@@ -399,7 +399,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
399399
"efficiency bound on the no-covariate path." + library_aggregation_note
400400
),
401401
"aggregation": "pt_all_combined",
402-
"headline_attribute": "overall_att",
402+
"headline_attribute": "att",
403403
"reference": "Chen-Sant'Anna-Xie (2025) Lemma 2.1; REGISTRY.md Sec. EfficientDiD",
404404
}
405405

@@ -418,7 +418,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
418418
"dose-specific inference."
419419
),
420420
"aggregation": "dose_overall",
421-
"headline_attribute": "overall_att",
421+
"headline_attribute": "att",
422422
"reference": "Callaway-Goodman-Bacon-Sant'Anna (2024); REGISTRY.md Sec. ContinuousDiD",
423423
}
424424

@@ -460,7 +460,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
460460
# ``linear_trends_effects is not None``. The estimator can set
461461
# ``linear_trends_effects = None`` when the cumulated-horizon
462462
# dict is empty (no estimable horizons) while still
463-
# unconditionally NaN-ing ``overall_att`` under
463+
# unconditionally NaN-ing ``att`` under
464464
# ``trends_linear=True`` + ``L_max >= 2``
465465
# (``chaisemartin_dhaultfoeuille.py:2828-2834``). The previous
466466
# inference missed that edge case; the new explicit flag
@@ -476,7 +476,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
476476
"de Chaisemartin & D'Haultfoeuille (2020, 2024); "
477477
"REGISTRY.md Sec. ChaisemartinDHaultfoeuille"
478478
)
479-
# PR #347 R2 P1 review: mirror the exact ``overall_att``
479+
# PR #347 R2 P1 review: mirror the exact ``att``
480480
# contract from ``chaisemartin_dhaultfoeuille.py`` lines
481481
# 2602-2634 (L_max>=2 overrides with cost-benefit ``delta``,
482482
# NaN if non-estimable) and lines 2828-2834 (``trends_linear``
@@ -485,7 +485,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
485485
# ``chaisemartin_dhaultfoeuille_results.py:454-490`` is the
486486
# source-of-truth; this branch tracks that logic.
487487

488-
# Trends + L_max>=2: overall_att is NaN. No scalar aggregate;
488+
# Trends + L_max>=2: ``att`` is NaN. No scalar aggregate;
489489
# per-horizon effects are on ``linear_trends_effects``.
490490
if has_trends and l_max is not None and l_max >= 2:
491491
# PR #347 R12 P1: distinguish the populated-surface case
@@ -509,7 +509,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
509509
definition_text = (
510510
"Under ``trends_linear=True`` with ``L_max >= 2``, the "
511511
"estimator intentionally does NOT produce a scalar "
512-
"aggregate in ``overall_att`` (it is NaN by design, "
512+
"aggregate in ``att`` (it is NaN by design, "
513513
"matching R's ``did_multiplegt_dyn`` with "
514514
"``trends_lin=TRUE``). On this fit, no cumulated level "
515515
f"effects ``{estimand_label}`` survived estimation "
@@ -525,7 +525,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
525525
definition_text = (
526526
"Under ``trends_linear=True`` with ``L_max >= 2``, the "
527527
"estimator intentionally does NOT produce a scalar "
528-
"aggregate in ``overall_att`` (it is NaN by design, "
528+
"aggregate in ``att`` (it is NaN by design, "
529529
"matching R's ``did_multiplegt_dyn`` with "
530530
"``trends_lin=TRUE``). Per-horizon cumulated level "
531531
"effects ``DID^{fd}_l`` (or ``DID^{X,fd}_l`` when "
@@ -586,7 +586,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
586586
"``delta = sum_l w_l * DID_l`` (Lemma 4 of the dCDH "
587587
"dynamic companion paper), a weighted cross-horizon "
588588
"combination where ``w_l`` reflects the cumulative dose at "
589-
"each horizon. ``overall_att`` holds this delta when "
589+
"each horizon. ``att`` holds this delta when "
590590
"``L_max >= 2``; if delta is non-estimable (no eligible "
591591
"switchers at any horizon) it is NaN with the same "
592592
"``overall_se`` / CI surface."
@@ -635,7 +635,7 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
635635
"name": f"{name_label} ({base_label})",
636636
"definition": definition_core + suffix_clause,
637637
"aggregation": agg_base + agg_suffix,
638-
"headline_attribute": "overall_att",
638+
"headline_attribute": "att",
639639
"reference": reference,
640640
}
641641

0 commit comments

Comments
 (0)