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
30 changes: 30 additions & 0 deletions changelog.d/20261018-v4-pr1-conformance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
### Added
- **`LWDiDResults.summary(alpha=None)` / `print_summary(alpha=None)`** join the family-wide
summary-alpha guard (ledger row `M-146`): a value other than the fit-time `alpha` raises
`ValueError` instead of being ignored; the methods previously took no `alpha` at all.
- **`LWDiDResults.to_dataframe(level=...)` / `to_csv(path, level=...)`** gain the uniform
`level` selector (`"simple"` default, `"group"`, `"event_study"`) - see Breaking Changes.
- **`DurationDiDResults.aggregate("simple")`**: a verbatim relay of the stored headline
quintet (`n = n_units`, `df` NaN - inference is bootstrap-only), joining the spec section 6
`simple` contract every aggregation adopter honours.
- **`RDDensityTestResult.to_dict()`**: JSON-serializable scalars with native casts (NaN
inference passes through), the `*_conventional` block `None` unless `report_all=True`, and
the bandwidth-selection table nested under `bandwidths` only when it was estimated.
- `diff_diff._deprecation.deprecated_field_property` takes `removed_in=` so the warning and the
generated docstring name the row's actual removal release.

### Deprecated
- **`LWDiDResults.pvalue` and `LWDiDResults.ci`** (ledger rows `M-151`, `M-152`): the
3.10-born alias properties for `p_value` / `conf_int` now emit a `FutureWarning` and are
removed in 5.0. Read `.p_value` / `.conf_int`.

### Breaking Changes
- **`LWDiDResults.to_dataframe()` is reshaped onto the shared results schema** (ledger row
`M-153`): each `level` returns `aggregate(level).to_dataframe()` - columns `level, label,
target, att, se, t_stat, p_value, conf_int_lower, conf_int_upper, n, weight, df` (the
event-study schema for `level="event_study"`) - plus LWDiD's `rolling`, `estimation_method`
and `vcov_type` columns. The default `"simple"` is ONE headline row; the former per-cohort
rows are `level="group"` (without the old trailing `"Overall"` row). The `term` / `cohort`
column is now `label`, `n_treated` is the schema's `n`, `n_obs` and `n_control` are no longer
in the frame (both stay in `to_dict()`), and `ci_lower` / `ci_upper` are
`conf_int_lower` / `conf_int_upper`. `to_csv()` follows and takes the same `level`.
15 changes: 11 additions & 4 deletions diff_diff/_deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,24 +151,31 @@ def require_arg(qualname: str, name: str, value: Any) -> None:
raise TypeError(f"{qualname}() missing required argument: '{name}'")


def deprecated_field_property(cls_name: str, old: str, new: str) -> property:
def deprecated_field_property(
cls_name: str, old: str, new: str, *, removed_in: str = "4.0"
) -> property:
"""Build the read-only warning alias for a renamed results field.

Assigned WITHOUT an annotation in the dataclass body, so it stays a
plain descriptor (never a ``__dataclass_fields__`` entry). Wording
follows the ``SyntheticDiDResults`` renamed-field alias precedent
(M-003, ``diff_diff/results.py``), with the FutureWarning category the
ledger declares for the M-094/M-095/M-114 rows.
ledger declares for the M-094/M-095/M-114 rows. ``removed_in`` names the
release the ledger row schedules the removal for (``"4.0"`` for the 3.9
rename sweep; ``"5.0"`` for aliases deprecated AT 4.0, e.g. the LWDiD
``pvalue``/``ci`` rows M-151/M-152) so the warning and the generated
docstring never contradict the row.
"""

def _get(self: Any) -> Any:
warnings.warn(
f"{cls_name}.{old} is deprecated; use {new} instead. " "Will be removed in 4.0.",
f"{cls_name}.{old} is deprecated; use {new} instead. "
f"Will be removed in {removed_in}.",
FutureWarning,
stacklevel=2,
)
return getattr(self, new)

_get.__name__ = old
_get.__doc__ = f"Deprecated alias for :attr:`{new}` (removed in 4.0)."
_get.__doc__ = f"Deprecated alias for :attr:`{new}` (removed in {removed_in})."
return property(_get)
33 changes: 31 additions & 2 deletions diff_diff/duration_did_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import numpy as np
import pandas as pd

from diff_diff.aggregation import AggregationMixin
from diff_diff.aggregation import AggregationMixin, AggregationResult
from diff_diff.results import _get_significance_stars
from diff_diff.results_base import (
BaseResults,
Expand Down Expand Up @@ -359,7 +359,7 @@ class DurationDiDResults(BaseResults, AggregationMixin):
n_draws_invalid_counterfactual: int
pretest: DurationDiDPretestResults

_AGGREGATE_SUPPORTED: ClassVar[Tuple[str, ...]] = ("event_study",)
_AGGREGATE_SUPPORTED: ClassVar[Tuple[str, ...]] = ("simple", "event_study")
_AGGREGATE_BALANCE_E_TYPES: ClassVar[Tuple[str, ...]] = ()

# ------------------------------------------------------------------ #
Expand Down Expand Up @@ -545,6 +545,35 @@ def _aggregate_compute(
weights: Optional[str],
balance_e: Optional[int],
) -> Any:
"""Build the ``"simple"`` relay or the ``"event_study"`` container.

``"simple"`` relays the stored headline quintet verbatim (spec section
6: a relay never recomputes). DurationDiD's inference is
bootstrap-only - ``se`` is the centred-bootstrap SD and the interval
the symmetric ``point +/- crit * se`` (REGISTRY: no percentile
intervals), ``t_stat`` the stored analytical z, all NaN when
``inference_status != "ok"`` - so no finite df governs it and the
``df`` column is NaN. ``"event_study"`` builds the unified container.
"""
if level == "simple":
ci_lo, ci_hi = self.conf_int
return AggregationResult(
level="simple",
label=np.array(["overall"], dtype=object),
target=np.array(["att"], dtype=object),
att=np.array([self.att], dtype=float),
se=np.array([self.se], dtype=float),
t_stat=np.array([self.t_stat], dtype=float),
p_value=np.array([self.p_value], dtype=float),
conf_int_lower=np.array([ci_lo], dtype=float),
conf_int_upper=np.array([ci_hi], dtype=float),
n=np.array([float(self.n_units)], dtype=float),
df=None,
alpha=self.alpha,
n_kind="units",
weight=np.array([1.0], dtype=float),
estimator="DurationDiD",
)
if level != "event_study": # pragma: no cover - mixin validates first
raise ValueError(f"Unsupported aggregation method: {level!r}")
n_post = len(self.post_periods)
Expand Down
5 changes: 3 additions & 2 deletions diff_diff/guides/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,7 @@ res.f_left, res.f_right # per-side density estimates at the cutoff (who
res.bandwidths # the rdbwdensity 4-row h-table (None on manual h)
res.to_dataframe() # tidy left/right/diff table (+ conventional rows with report_all)
print(res.summary()) # R-style block (Conventional row iff report_all=True)
res.to_dict() # JSON-serializable scalars (+ nested 'bandwidths' when estimated)
```

Repeated running-variable values are mass-point adjusted with a warning
Expand Down Expand Up @@ -1453,7 +1454,7 @@ Key contracts:
- `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`.
- 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`).
- `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.
- `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.
- `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.
- Not supported in this version: covariates, staggered adoption, censoring, survey weights, cluster dependence, repeated cross-sections.

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

### LWDiDResults

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.
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.

### TROPResults

Expand Down
Loading
Loading