Skip to content

Commit 5d110f3

Browse files
authored
feat(v4): PR 1 of the 4.0 cut - new-estimator results-contract conformance (#857)
1 parent 615b35b commit 5d110f3

17 files changed

Lines changed: 604 additions & 124 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
### Added
2+
- **`LWDiDResults.summary(alpha=None)` / `print_summary(alpha=None)`** join the family-wide
3+
summary-alpha guard (ledger row `M-146`): a value other than the fit-time `alpha` raises
4+
`ValueError` instead of being ignored; the methods previously took no `alpha` at all.
5+
- **`LWDiDResults.to_dataframe(level=...)` / `to_csv(path, level=...)`** gain the uniform
6+
`level` selector (`"simple"` default, `"group"`, `"event_study"`) - see Breaking Changes.
7+
- **`DurationDiDResults.aggregate("simple")`**: a verbatim relay of the stored headline
8+
quintet (`n = n_units`, `df` NaN - inference is bootstrap-only), joining the spec section 6
9+
`simple` contract every aggregation adopter honours.
10+
- **`RDDensityTestResult.to_dict()`**: JSON-serializable scalars with native casts (NaN
11+
inference passes through), the `*_conventional` block `None` unless `report_all=True`, and
12+
the bandwidth-selection table nested under `bandwidths` only when it was estimated.
13+
- `diff_diff._deprecation.deprecated_field_property` takes `removed_in=` so the warning and the
14+
generated docstring name the row's actual removal release.
15+
16+
### Deprecated
17+
- **`LWDiDResults.pvalue` and `LWDiDResults.ci`** (ledger rows `M-151`, `M-152`): the
18+
3.10-born alias properties for `p_value` / `conf_int` now emit a `FutureWarning` and are
19+
removed in 5.0. Read `.p_value` / `.conf_int`.
20+
21+
### Breaking Changes
22+
- **`LWDiDResults.to_dataframe()` is reshaped onto the shared results schema** (ledger row
23+
`M-153`): each `level` returns `aggregate(level).to_dataframe()` - columns `level, label,
24+
target, att, se, t_stat, p_value, conf_int_lower, conf_int_upper, n, weight, df` (the
25+
event-study schema for `level="event_study"`) - plus LWDiD's `rolling`, `estimation_method`
26+
and `vcov_type` columns. The default `"simple"` is ONE headline row; the former per-cohort
27+
rows are `level="group"` (without the old trailing `"Overall"` row). The `term` / `cohort`
28+
column is now `label`, `n_treated` is the schema's `n`, `n_obs` and `n_control` are no longer
29+
in the frame (both stay in `to_dict()`), and `ci_lower` / `ci_upper` are
30+
`conf_int_lower` / `conf_int_upper`. `to_csv()` follows and takes the same `level`.

‎diff_diff/_deprecation.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,24 +151,31 @@ def require_arg(qualname: str, name: str, value: Any) -> None:
151151
raise TypeError(f"{qualname}() missing required argument: '{name}'")
152152

153153

154-
def deprecated_field_property(cls_name: str, old: str, new: str) -> property:
154+
def deprecated_field_property(
155+
cls_name: str, old: str, new: str, *, removed_in: str = "4.0"
156+
) -> property:
155157
"""Build the read-only warning alias for a renamed results field.
156158
157159
Assigned WITHOUT an annotation in the dataclass body, so it stays a
158160
plain descriptor (never a ``__dataclass_fields__`` entry). Wording
159161
follows the ``SyntheticDiDResults`` renamed-field alias precedent
160162
(M-003, ``diff_diff/results.py``), with the FutureWarning category the
161-
ledger declares for the M-094/M-095/M-114 rows.
163+
ledger declares for the M-094/M-095/M-114 rows. ``removed_in`` names the
164+
release the ledger row schedules the removal for (``"4.0"`` for the 3.9
165+
rename sweep; ``"5.0"`` for aliases deprecated AT 4.0, e.g. the LWDiD
166+
``pvalue``/``ci`` rows M-151/M-152) so the warning and the generated
167+
docstring never contradict the row.
162168
"""
163169

164170
def _get(self: Any) -> Any:
165171
warnings.warn(
166-
f"{cls_name}.{old} is deprecated; use {new} instead. " "Will be removed in 4.0.",
172+
f"{cls_name}.{old} is deprecated; use {new} instead. "
173+
f"Will be removed in {removed_in}.",
167174
FutureWarning,
168175
stacklevel=2,
169176
)
170177
return getattr(self, new)
171178

172179
_get.__name__ = old
173-
_get.__doc__ = f"Deprecated alias for :attr:`{new}` (removed in 4.0)."
180+
_get.__doc__ = f"Deprecated alias for :attr:`{new}` (removed in {removed_in})."
174181
return property(_get)

‎diff_diff/duration_did_results.py‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import numpy as np
2424
import pandas as pd
2525

26-
from diff_diff.aggregation import AggregationMixin
26+
from diff_diff.aggregation import AggregationMixin, AggregationResult
2727
from diff_diff.results import _get_significance_stars
2828
from diff_diff.results_base import (
2929
BaseResults,
@@ -359,7 +359,7 @@ class DurationDiDResults(BaseResults, AggregationMixin):
359359
n_draws_invalid_counterfactual: int
360360
pretest: DurationDiDPretestResults
361361

362-
_AGGREGATE_SUPPORTED: ClassVar[Tuple[str, ...]] = ("event_study",)
362+
_AGGREGATE_SUPPORTED: ClassVar[Tuple[str, ...]] = ("simple", "event_study")
363363
_AGGREGATE_BALANCE_E_TYPES: ClassVar[Tuple[str, ...]] = ()
364364

365365
# ------------------------------------------------------------------ #
@@ -545,6 +545,35 @@ def _aggregate_compute(
545545
weights: Optional[str],
546546
balance_e: Optional[int],
547547
) -> Any:
548+
"""Build the ``"simple"`` relay or the ``"event_study"`` container.
549+
550+
``"simple"`` relays the stored headline quintet verbatim (spec section
551+
6: a relay never recomputes). DurationDiD's inference is
552+
bootstrap-only - ``se`` is the centred-bootstrap SD and the interval
553+
the symmetric ``point +/- crit * se`` (REGISTRY: no percentile
554+
intervals), ``t_stat`` the stored analytical z, all NaN when
555+
``inference_status != "ok"`` - so no finite df governs it and the
556+
``df`` column is NaN. ``"event_study"`` builds the unified container.
557+
"""
558+
if level == "simple":
559+
ci_lo, ci_hi = self.conf_int
560+
return AggregationResult(
561+
level="simple",
562+
label=np.array(["overall"], dtype=object),
563+
target=np.array(["att"], dtype=object),
564+
att=np.array([self.att], dtype=float),
565+
se=np.array([self.se], dtype=float),
566+
t_stat=np.array([self.t_stat], dtype=float),
567+
p_value=np.array([self.p_value], dtype=float),
568+
conf_int_lower=np.array([ci_lo], dtype=float),
569+
conf_int_upper=np.array([ci_hi], dtype=float),
570+
n=np.array([float(self.n_units)], dtype=float),
571+
df=None,
572+
alpha=self.alpha,
573+
n_kind="units",
574+
weight=np.array([1.0], dtype=float),
575+
estimator="DurationDiD",
576+
)
548577
if level != "event_study": # pragma: no cover - mixin validates first
549578
raise ValueError(f"Unsupported aggregation method: {level!r}")
550579
n_post = len(self.post_periods)

‎diff_diff/guides/llms-full.txt‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,7 @@ res.f_left, res.f_right # per-side density estimates at the cutoff (who
10791079
res.bandwidths # the rdbwdensity 4-row h-table (None on manual h)
10801080
res.to_dataframe() # tidy left/right/diff table (+ conventional rows with report_all)
10811081
print(res.summary()) # R-style block (Conventional row iff report_all=True)
1082+
res.to_dict() # JSON-serializable scalars (+ nested 'bandwidths' when estimated)
10821083
```
10831084

10841085
Repeated running-variable values are mass-point adjusted with a warning
@@ -1453,7 +1454,7 @@ Key contracts:
14531454
- `results.att_by_period`, `se_by_period`, `conf_int_by_period` (pointwise), `cband_lower/upper` + `cband_crit_value` (simultaneous), `joint_p_value`, `vcov` (P x P bootstrap covariance), `coefficient` (fitted c), `survival_treated/control`, `counterfactual_survival`, `pre_periods`/`pre_period_weights` (realized), `excluded_pre_periods`.
14541455
- Every inference family is fully available or fully withheld: `inference_status` in `ok` / `disabled` (n_bootstrap=0) / `unavailable_invalid_periods` (the CD model extrapolated an invalid counterfactual curve — survival above one or a decreasing cumulative hazard — `curve_status`/`period_status` name the dates; the warning names the applicable remedy: a fitted pre-date violation cannot be repaired by a shorter horizon — change `method` or the fitting window; a later post-date violation admits an explicit refit on the dates before the first invalid post date; a first-post-date violation admits no valid shorter horizon) / `unavailable_failed_draws` (any failed draw; fixed draws, no retries, `bootstrap_failure_reasons` per family) / `unavailable_zero_se`. `n_draws_invalid_counterfactual` counts complete draws whose imputed curve left the domain (not failures; a large share signals weak extrapolation support — shown in `summary()`). `p_value <= alpha` is exactly the band rule (`is_significant`).
14551456
- `results.pretest` (`DurationDiDPretestResults`, a Diagnostic): contrasts of the CD gap / PH ratio at every interior pre-date against the fixed anchor `last_pre_period`, simultaneous band, `statistic`, `p_value`, `reject`, `status` (needs at least three pre-dates). A non-rejection does not establish identification.
1456-
- `results.aggregate('event_study')` returns the unified `EventStudyResults` (event time 0 = first post-treatment date, reference -1, `n_kind='units'`); no pre-treatment ATT rows exist (the pretest is not an event study). Do NOT run `check_parallel_trends` or HonestDiD on this estimator; `DiagnosticReport`/`BusinessReport` reject `DurationDiDResults` by type; `practitioner_next_steps` has a dedicated handler.
1457+
- `results.aggregate('event_study')` returns the unified `EventStudyResults` (event time 0 = first post-treatment date, reference -1, `n_kind='units'`); no pre-treatment ATT rows exist (the pretest is not an event study). `results.aggregate('simple')` relays the stored headline quintet verbatim (`n = n_units`, `df` NaN - bootstrap-only inference). Do NOT run `check_parallel_trends` or HonestDiD on this estimator; `DiagnosticReport`/`BusinessReport` reject `DurationDiDResults` by type; `practitioner_next_steps` has a dedicated handler.
14571458
- Not supported in this version: covariates, staggered adoption, censoring, survey weights, cluster dependence, repeated cross-sections.
14581459

14591460
### TROP
@@ -2275,7 +2276,7 @@ Results container shared by `ChangesInChanges` and `QDiD` (the `method` field is
22752276

22762277
### LWDiDResults
22772278

2278-
Results from `LWDiD.fit()` (both timing modes). Flat-native headline fields `att`, `se`, `t_stat`, `p_value`, `conf_int` (when inference is unavailable - e.g. psm, degenerate designs - `se`/`t_stat`/`p_value` and both CI bounds are jointly NaN while `att` is RETAINED: the fail-closed contract), plus `n_obs`, `n_treated`, `n_control`, `df_inference`, `cluster_name`/`n_clusters`, and fit provenance (`rolling`, `estimation_method`, `vcov_type`, `alpha`, `control_group`, `n_bootstrap`, `seed`, `pscore_trim`, `psm_config`, `inference_basis`). Staggered fits also carry `cohort_effects` and `cohort_time_effects` dicts. Methods: `summary()`, `print_summary()`, `to_dict()`, `to_dataframe()`, `aggregate(level)` with levels `'simple'` (view-relay of the fitted headline -> AggregationResult), `'event_study'` (per-event-time effects -> EventStudyResults), and `'group'` (per-cohort effects -> AggregationResult; staggered fits only - common-timing fits raise), and - common-timing `estimation_method='reg'` fits ONLY - `randomization_test()` and `wild_cluster_bootstrap()` (the latter ADDITIONALLY requires a `cluster=` fit - unclustered fits raise; both REPLAY the fitted collapsed design; no data arguments accepted; other fits reject informatively). NOTE: there is NO `overall_att` or `period_effects` attribute - those surfaces were retired pre-release (unlike the shared staggered pattern above); use `.att` for the overall effect and `aggregate('event_study')` for per-period effects.
2279+
Results from `LWDiD.fit()` (both timing modes). Flat-native headline fields `att`, `se`, `t_stat`, `p_value`, `conf_int` (when inference is unavailable - e.g. psm, degenerate designs - `se`/`t_stat`/`p_value` and both CI bounds are jointly NaN while `att` is RETAINED: the fail-closed contract), plus `n_obs`, `n_treated`, `n_control`, `df_inference`, `cluster_name`/`n_clusters`, and fit provenance (`rolling`, `estimation_method`, `vcov_type`, `alpha`, `control_group`, `n_bootstrap`, `seed`, `pscore_trim`, `psm_config`, `inference_basis`). Staggered fits also carry `cohort_effects` and `cohort_time_effects` dicts. Methods: `summary(alpha=None)` / `print_summary(alpha=None)` (alpha must equal the fit-time alpha - the family-wide guard), `to_dict()`, `to_dataframe(level='simple')` / `to_csv(path, level='simple')` (4.0: aggregate-backed - `aggregate(level).to_dataframe()` plus the `rolling`/`estimation_method`/`vcov_type` columns; `level` in `'simple'` (one headline row, default), `'group'` (per-cohort rows, staggered only), `'event_study'`; columns `conf_int_lower`/`conf_int_upper`, `n` = treated units - `n_obs`/`n_control` live in `to_dict()`), `aggregate(level)` with levels `'simple'` (view-relay of the fitted headline -> AggregationResult), `'event_study'` (per-event-time effects -> EventStudyResults), and `'group'` (per-cohort effects -> AggregationResult; staggered fits only - common-timing fits raise), and - common-timing `estimation_method='reg'` fits ONLY - `randomization_test()` and `wild_cluster_bootstrap()` (the latter ADDITIONALLY requires a `cluster=` fit - unclustered fits raise; both REPLAY the fitted collapsed design; no data arguments accepted; other fits reject informatively). NOTE: there is NO `overall_att` or `period_effects` attribute - those surfaces were retired pre-release (unlike the shared staggered pattern above); use `.att` for the overall effect and `aggregate('event_study')` for per-period effects.
22792280

22802281
### TROPResults
22812282

0 commit comments

Comments
 (0)