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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/python/benchmark_callaway.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def main():
)
# Post-fit aggregations stay inside the timed region: the R `aggte`
# yardstick is fit + simple/dynamic/group aggregation (the 3.x
# fit(aggregate="all") route, deprecated since 3.9 and removed at the 4.0 cut).
# fit(aggregate="all") route, removed at 4.0).
es = results.aggregate("event_study")
grp = results.aggregate("group")

Expand Down
3 changes: 1 addition & 2 deletions benchmarks/python/benchmark_stacked_did.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ def main():
)

# The event-study surface is always computed at fit (3.9, row M-024);
# the deprecated aggregate= kwarg would emit a FutureWarning inside
# the timed block and contaminate the measurement.
# fit() takes no aggregate= kwarg since 4.0.
with Timer() as estimation_timer:
results = est.fit(
df,
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/speed_review/bench_callaway.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def bench_fit(n_units, n_bootstrap=0, covariates=None, n_cohorts=5, n_runs=3,
covariates=covariates,
)
# Keep "fit + aggregations" as the measured work (the 3.x
# fit(aggregate="all") route, deprecated since 3.9 and removed at the 4.0 cut).
# fit(aggregate="all") route, removed at 4.0).
res.aggregate("event_study")
res.aggregate("group")
elapsed = time.perf_counter() - start
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/speed_review/bench_solve_ols_fastpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def fit(d):
covariates=covs,
)
# fit + aggregations is the measured work (the 3.x
# fit(aggregate="all") route, deprecated since 3.9 and removed at the 4.0 cut).
# fit(aggregate="all") route, removed at 4.0).
res.aggregate("event_study")
res.aggregate("group")
return float(res.overall_att), float(res.overall_se)
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/speed_review/validate_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def run_estimator(df, estimation_method="reg", covariates=None, control_group="n
first_treat="first_treat",
covariates=covariates,
)
# Post-fit containers (the 3.x fit(aggregate="all") route is deprecated and goes at the 4.0 cut);
# Post-fit containers (the 3.x fit(aggregate="all") route, removed at 4.0);
# the fit is bootstrapped, so each call replays the fit-time bootstrap.
es_container = results.aggregate("event_study")
grp_container = results.aggregate("group")
Expand Down
55 changes: 55 additions & 0 deletions changelog.d/20261021-v4-pr3b-aggregate-removals-mechanical.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
### Removed
- **The fit-time `aggregate=` / `balance_e=` keywords are gone on the estimators whose post-fit
route already existed (ledger rows flipped to `removed`).** `fit(aggregate=)` is removed from
`CallawaySantAnna` (`M-020`), `EfficientDiD` (`M-023`), `StackedDiD` (`M-024`),
`ChaisemartinDHaultfoeuille` (`M-026`) and `HeterogeneousAdoptionDiD` (`M-027`),
`fit(balance_e=)` from `CallawaySantAnna` (`M-117`) and `EfficientDiD` (`M-120`), and
`aggregate=` from `did_had_pretest_workflow` (`M-139`; the `HADPretestReport.aggregate`
output field survives). The successor is post-fit `results.aggregate(type, balance_e=)`; HAD
selects its mode from the panel shape. With the keywords go StackedDiD's fit-time
`group`/`all`/unknown-value `ValueError`s (`aggregate('group')` already fails closed in the
mixin) and the private `_VALID_AGGREGATES` sets in `had.py` / `had_pretests.py`
(`_infer_aggregate_mode` is the sole selector). `ImputationDiD`, `TwoStageDiD` and
`ContinuousDiD` keep their deprecated keywords until the next cut PR adds the
bootstrapped-fit relay.
- **CallawaySantAnna and EfficientDiD compute no event-study/group surface at fit.** Their
legacy `event_study_effects` / `group_effects` fields - and CS's `cband_crit_value`,
`event_study_vcov`, `event_study_vcov_index`, `event_study_df` - are `None` after EVERY fit;
the post-fit replay (`aggregate('event_study'|'group', balance_e=)`) is the only bootstrap
route and is unchanged.

### Breaking Changes
- Passing `aggregate=` or `balance_e=` to `CallawaySantAnna.fit`, `EfficientDiD.fit`,
`StackedDiD.fit`, `ChaisemartinDHaultfoeuille.fit` or `HeterogeneousAdoptionDiD.fit` (or
`aggregate=` to `did_had_pretest_workflow`) now raises Python's native
`TypeError: ... unexpected keyword argument`. `TripleDifference`'s staggered-mode
`aggregate=`/`balance_e=` are untouched (`M-140`/`M-141`, canonical until the DDD container
unification).
- **Positional slots after the removed parameters shift left.** `CallawaySantAnna`:
`survey_design` 9 -> 7; `EfficientDiD`: `survey_design` 9 -> 7, `store_eif` 10 -> 8;
`StackedDiD` (`aggregate` sat before them): `population`, `survey_design`, `covariates`
7/8/9 -> 6/7/8; `ChaisemartinDHaultfoeuille`: `L_max`, `covariates`, `trends_linear`,
`trends_nonparam`, `honest_did`, `heterogeneity`, `design2`, `survey_design` each move one
slot left (7..14 -> 6..13). `HeterogeneousAdoptionDiD.fit` shifts nothing (everything after
the removed slot is keyword-only; a seventh positional now raises). Pass these by keyword.
- `CallawaySantAnnaResults.to_dataframe(level="event_study"|"group")` and the
`EfficientDiDResults` twins raise on every fresh fit (the legacy fields are always `None`);
use `results.aggregate(...).to_dataframe()`. The column-schema rewire of these levels onto the
container is `M-093`'s (PR 8).

### Changed
- Runtime messages, the four `llms*.txt` guides, the API docs, the migration guide and the
benchmarks no longer steer users to the fit-time route on these five estimators;
`CallawaySantAnna`'s class docstring, `plot_event_study`, `honest_did` / `pretrends` and the
results-class "not stored" messages point at `results.aggregate('event_study')`.

### Internal
- `tests/test_aggregate_contract.py`: the `Test*Shim` classes, fit-time fixtures and
inertness comparisons for the five estimators are retired in favour of `Test*Removal`
pins (TypeError, signature, positional-slot binding); the committed oracle fixture is the
numerical reference. `tests/_capture_v4_aggregate_oracles.py` is marked historical (it
drives the removed route; runnable only at the recorded `captured_at_head`). Naming-guard
allowlist entries for the `llms.txt` / `llms-full.txt` canonical post-fit `aggregate`
vocabulary. Ledger row `M-093` records that CS/EfficientDiD populate the legacy fields on
no fit. `warn_deprecated_kwarg` loses its last library caller and is retained (spec section
8 rule 11) with a direct unit pin.
11 changes: 8 additions & 3 deletions diff_diff/_deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
default. ``resolve_renamed_kwarg`` maps old to new with a single
``FutureWarning``, rejects both-supplied calls loudly, and returns the
caller-declared default when neither is passed.
- Dropped parameter (no successor name, e.g. the fit-time ``aggregate=``
override): the parameter keeps its position with a sentinel default; the
owner calls ``warn_deprecated_kwarg`` when a value was actually supplied.
- Dropped parameter (no successor name): the parameter keeps its position
with a sentinel default; the owner calls ``warn_deprecated_kwarg`` when a
value was actually supplied. (The 3.9 fit-time ``aggregate=`` shims on
CallawaySantAnna, EfficientDiD, StackedDiD, ChaisemartinDHaultfoeuille and
HeterogeneousAdoptionDiD were this shape; they were deleted at the 4.0 cut
(PR 3b) and ImputationDiD / TwoStageDiD / ContinuousDiD follow in PR 3c.
``warn_deprecated_kwarg`` has no library caller any more (dCDH was the
last) and is retained per section 8 rule 11 for the 5.0 window.)
- Renamed results field: the dataclass field takes the new name and
``deprecated_field_property`` builds the read-only warning alias under the
old name (a plain class attribute, so it is a descriptor but NOT a
Expand Down
16 changes: 8 additions & 8 deletions diff_diff/_reporting_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
disambiguate via the event-study or group aggregate if you need
the horizon / group target.
- ``CallawaySantAnna``: ``overall_att`` is cohort-size-weighted
across post-treatment ``ATT(g, t)`` cells regardless of the
fit-time ``aggregate`` kwarg. The event-study / group tables are
produced post-fit via ``results.aggregate('event_study'/'group')``
(the deprecated fit-time ``aggregate=`` kwarg populates the legacy
``event_study_effects`` / ``group_effects`` fields until 4.0).
across post-treatment ``ATT(g, t)`` cells regardless of any
aggregation. The event-study / group tables are produced post-fit
via ``results.aggregate('event_study'/'group')``; the legacy
``event_study_effects`` / ``group_effects`` fields are None on
every CallawaySantAnna fit since 4.0.
- ``ContinuousDiD``: the regime (PT vs. SPT) is a user-level
assumption, not a library setting. The ``definition`` names
both regime readings (``ATT^loc`` under PT,
Expand Down Expand Up @@ -140,9 +140,9 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
"``overall_att`` is the simple-aggregation headline regardless "
"of aggregation choices; event-study and group tables are "
"produced post-fit via "
"``results.aggregate('event_study'/'group')`` (the deprecated "
"fit-time ``aggregate=`` kwarg populates the legacy "
"``event_study_effects`` / ``group_effects`` fields until 4.0)."
"``results.aggregate('event_study'/'group')`` (the legacy "
"``event_study_effects`` / ``group_effects`` fields are None "
"on every CallawaySantAnna fit since 4.0)."
),
"aggregation": "simple",
"headline_attribute": "overall_att",
Expand Down
45 changes: 6 additions & 39 deletions diff_diff/chaisemartin_dhaultfoeuille.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import pandas as pd

from diff_diff._base import BaseEstimator
from diff_diff._deprecation import NOT_SUPPLIED, warn_deprecated_kwarg
from diff_diff.chaisemartin_dhaultfoeuille_bootstrap import (
ChaisemartinDHaultfoeuilleBootstrapMixin,
)
Expand Down Expand Up @@ -401,8 +400,8 @@ class ChaisemartinDHaultfoeuille(ChaisemartinDHaultfoeuilleBootstrapMixin, BaseE
- TWFE decomposition diagnostic from Theorem 1 of AER 2020

Aggregation is a post-fit step: ``results.aggregate('event_study')`` /
``results.aggregate('simple')`` (the deprecated fit-time ``aggregate=``
never computed anything and now warns; row M-026).
``results.aggregate('simple')``; ``fit()`` takes no aggregation
keyword.

Parameters
----------
Expand Down Expand Up @@ -878,7 +877,6 @@ def fit(
time: str,
treatment: str,
# ---------- forward-compat parameters ----------
aggregate: Any = NOT_SUPPLIED,
L_max: Optional[int] = None,
covariates: Optional[List[str]] = None,
trends_linear: Optional[bool] = None,
Expand Down Expand Up @@ -914,15 +912,6 @@ def fit(
within each ``(unit, time)`` cell. Both binary ``{0, 1}`` and
non-binary (ordinal or continuous) treatment are supported.
Non-binary treatment requires ``L_max >= 1``.
aggregate : str, optional
DEPRECATED since 3.9, removed in 4.0 (ledger row M-026).
Passing it emits a ``FutureWarning``. The parameter never
computed aggregations here (it was reserved and any non-None
value has always raised); aggregation is a POST-FIT step:
``results.aggregate('event_study')`` for the multi-horizon
surface, ``results.aggregate('simple')`` for the overall
estimand. A non-None value now raises ``ValueError`` pointing
at that post-fit route.
L_max : int, optional
Maximum event-study horizon. When set, computes ``DID_l``
for ``l = 1, ..., L_max`` using the per-group building block
Expand Down Expand Up @@ -1063,28 +1052,8 @@ def fit(
# ------------------------------------------------------------------
# Step 2: Forward-compat gates
# ------------------------------------------------------------------
# M-026: fit(aggregate=) is deprecated - the aggregation entry point
# is post-fit (results.aggregate()). The param never computed
# anything here (any non-None value has always raised), so the shim
# warns on supply and rejects non-None values with a pointer at the
# post-fit route.
if aggregate is not NOT_SUPPLIED:
warn_deprecated_kwarg(
"ChaisemartinDHaultfoeuille.fit",
"aggregate",
"aggregation is a post-fit step: "
"results.aggregate('event_study') / results.aggregate('simple')",
)
if aggregate is not None:
raise ValueError(
f"fit(aggregate={aggregate!r}) never computed aggregations "
"on ChaisemartinDHaultfoeuille (the parameter was reserved "
"and always raised). Aggregate post-fit instead: "
"results.aggregate('event_study') for the multi-horizon "
"surface (Phase-1 fits without L_max return the 2-row "
"l=1 view), or results.aggregate('simple') for the "
"overall estimand."
)
# Aggregation is a post-fit step (results.aggregate('event_study') /
# results.aggregate('simple')); fit() takes no aggregation keyword.
_check_forward_compat_gates(
L_max=L_max,
controls=controls,
Expand Down Expand Up @@ -4395,10 +4364,8 @@ def _check_forward_compat_gates(
"""Raise ``NotImplementedError`` for any non-default Phase 3 parameter.

Phase 2 parameters (``L_max``) are validated inline in ``fit()``
after period detection. The deprecated ``aggregate`` parameter is
resolved in ``fit()`` itself (row M-026: FutureWarning on supply,
ValueError on any non-None value, pointing at the post-fit
``results.aggregate()`` route).
after period detection. Aggregation is a post-fit step
(``results.aggregate()``); ``fit()`` takes no aggregation keyword.
"""
# L_max is validated inline in fit() after period detection (needs
# the period count). Not gated here.
Expand Down
8 changes: 4 additions & 4 deletions diff_diff/diagnostic_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ def _resolve_event_study_surface(

The raw ``event_study_effects`` field always takes precedence when it
is not ``None`` — including the requested-but-empty ``{}`` sentinel,
which encodes fit-time configuration (e.g. an EfficientDiD
which encodes fit-time configuration (e.g. an ImputationDiD
``balance_e=`` that emptied the window) that a re-derivation without
those settings would silently overturn. Derivation failures
(bootstrapped fits, missing kits) are cached as skip reasons, never
Expand Down Expand Up @@ -1006,9 +1006,9 @@ def _instance_skip_reason(self, check: str) -> Optional[str]:
"per-event-time direct-effect output."
)
# StackedDiD (3.9, row M-024): the event-study surface
# is ALWAYS computed, so ``aggregate='event_study'`` is
# inert (and deprecated - it now warns). Zero estimated
# pre-periods here means the event window has none:
# is ALWAYS computed at fit (no aggregation keyword
# exists since 4.0). Zero estimated pre-periods here
# means the event window has none:
# the default ``kappa_pre=1`` grid minus the reference
# leaves no pre-period columns. Estimator-accurate
# remediation, per the SpilloverDiD precedent above.
Expand Down
Loading
Loading