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
88 changes: 88 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **TripleDifference serves both DDD designs** (v4 program Phase 3(b); ledger
rows [M-013] shimmed, [M-064]): `TripleDifference().fit(..., unit=, time=,
first_treat=, partition=)` estimates the staggered-adoption DDD design that
`StaggeredTripleDifference` used to own, while the existing
`fit(df, outcome, group, partition, post)` call keeps serving the 2x2x2
design unchanged. `first_treat=` selects the engine; mixing the two
parameter sets raises rather than guessing. This mirrors the reference
implementation, whose `triplediff::ddd()` also serves both designs from one
signature.
- The estimation cores are UNCHANGED - both classes now share one relocated
engine, so the staggered numbers are identical by construction (pinned
bit-exactly, including per-`(g,t)` tables and seeded bootstrap draws).
- The staggered-only fit params (`unit`, `first_treat`, `aggregate`,
`balance_e`) are keyword-only; every existing positional slot, `time=`
included, is unchanged.
- The constructor gains `control_group`, `anticipation`, `base_period`,
`n_bootstrap`, `bootstrap_weights`, `seed` and `cband`. `control_group`
takes the underscored `"not_yet_treated"`/`"never_treated"`.
- `time=` keeps both meanings without ambiguity: the calendar column in
staggered mode (no warning), the deprecated alias for `post=` in 2x2x2
mode (row [M-031], warns).
- 2x2x2 mode returns `TripleDifferenceResults` and staggered mode returns
`StaggeredTripleDiffResults`; the containers unify at 4.0 (row [M-014]),
so downstream consumers are unaffected.

### Changed
- **`TripleDifference(pscore_trim=)` is now validated** (ledger row [M-142]):
values outside `(0, 0.5)` raise instead of being accepted. The value feeds
`np.clip(pscore, trim, 1 - trim)`, so `pscore_trim=0` silently disabled the
overlap guard that keeps the `1/(1-p)` IPW/DR weights finite, and
`>= 0.5` inverted the clip bounds.
- **The single-PSU bootstrap warning now names the estimator that was fit.**
It lives in the mixin shared by `CallawaySantAnna` and both DDD classes and
was hard-coded to `"CallawaySantAnna bootstrap ..."`, so a DDD fit failing
closed on a degenerate design pointed diagnosis at the wrong estimator.
`CallawaySantAnna`'s own message is byte-identical to before.
- **Degenerate enabling cohorts are now reported** (both DDD staggered
surfaces). A positive `first_treat` cohort whose units are all
`partition == 0` identifies no `ATT(g,t)` and contributes to no aggregate,
but was still counted in `results.groups`/`n_groups` with no warning naming
it — so the estimate silently covered fewer cohorts than the metadata
claimed. A `UserWarning` now names the cohort, and `groups` reflects the
cohorts that actually produced a `(g, t)` cell. **Estimates are unchanged**;
this is a reporting fix.
- **Negative `first_treat` cohort values now raise** (both `TripleDifference`
staggered mode and the deprecated `StaggeredTripleDifference`, which share the
engine). Never-treated units must be `0`, or `+inf` (recoded to `0` with a
warning, unchanged); treated cohorts must be positive period labels. A unit
encoded with the common `-1` never-treated convention previously belonged to
neither the treated nor the comparison population: it still counted toward
`n_obs` but entered no ATT comparison, so the fit returned a plausible finite
estimate for a silently different population. **This is a bug fix that changes
results:** such a fit now raises instead of returning a number. Fits using
`0`/`+inf` are byte-for-byte unchanged.
- **`anticipation` is validated** as a non-negative integer (`bool` rejected)
via the shared `utils.validate_anticipation`, on `TripleDifference` at
construction and in the staggered engine for both surfaces. The window feeds
both the base-period rule and the not-yet-treated threshold, so a negative
value would have made the universal base period an already-treated period and
admitted cohorts treated at the evaluation period as clean controls - neither
visible in the output. On the deprecated class, construction still succeeds
and `fit()` raises: an identification guard, not a signature change.
- **`cluster=` raises in staggered DDD mode** on the merged surface, steering
to `n_bootstrap > 0` (unit-level clustering via the multiplier bootstrap).
Cluster-robust analytical SEs are not implemented for the staggered engine;
the deprecated `StaggeredTripleDifference` keeps its 3.x behavior of
accepting, warning and ignoring. 2x2x2 mode is unaffected (Liang-Zeger CR1).
- **Power analysis rejects a staggered-configured `TripleDifference`** at
`simulate_power`, `simulate_mde` and `simulate_sample_size`. Both registered
DDD data generators produce 2x2x2 data, so a staggered configuration would
have been simulated under the wrong design. Staggered-DDD power is not
supported yet. "Staggered-configured" means a non-default `control_group`,
`anticipation`, `base_period` or `n_bootstrap`; `bootstrap_weights`, `seed`
and `cband` are accepted, matching `fit()`, since they are inert without
`n_bootstrap > 0`.

### Deprecated
- **`StaggeredTripleDifference` and its `SDDD` alias** (ledger rows [M-013],
[M-064]): deprecated in 3.9, removed in 4.0. Migration:
`StaggeredTripleDifference(...).fit(df, outcome, unit, time, first_treat,
eligibility)` becomes `TripleDifference(...).fit(df, outcome, unit=...,
time=..., first_treat=..., partition=...)`. Two vocabulary changes:
`eligibility=` is named `partition=`, and `control_group` takes
`"not_yet_treated"`/`"never_treated"` instead of R's compact
`"notyettreated"`/`"nevertreated"`. The deprecated class keeps its 3.x
parameter names and values until removal, so existing code keeps working
(with a `FutureWarning`) until 4.0.

- **TwoWayFixedEffects event-study mode** (v4 program Phase 3(a); ledger
row [M-010] shimmed): `TWFE().fit(..., event_study=True, time="period",
spec="within"|"pooled", reference_period=None, post_periods=[...])`
Expand Down
6 changes: 3 additions & 3 deletions DEFERRED.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ exists but parity can't be verified without a local toolchain.

| Issue | Location | PR | Priority |
|-------|----------|----|----------|
| `StaggeredTripleDifference` R cross-validation: CSV fixtures not committed (gitignored); tests skip without local R + `triplediff`. Commit fixtures or generate deterministically. | `tests/test_methodology_staggered_triple_diff.py` | #245 | Medium |
| `StaggeredTripleDifference` R parity: benchmark only tests the no-covariate path (`xformla=~1`). Add covariate-adjusted scenarios + aggregation-SE parity assertions. | `benchmarks/R/benchmark_staggered_triplediff.R` | #245 | Medium |
| `StaggeredTripleDifference` per-cohort group-effect SEs include WIF (conservative vs R's `wif=NULL`); documented in REGISTRY. Could override the mixin for an exact R match (verification needs R `triplediff`). | `staggered_triple_diff.py` | #245 | Low |
| `StaggeredTripleDifference` R cross-validation (the engine is shared with `TripleDifference`'s staggered mode since 3(b), so this covers both surfaces): CSV fixtures not committed (gitignored); tests skip without local R + `triplediff`. Commit fixtures or generate deterministically. | `tests/test_methodology_staggered_triple_diff.py` | #245 | Medium |
| Staggered DDD R parity (both surfaces - one shared engine since 3(b)): benchmark only tests the no-covariate path (`xformla=~1`). Add covariate-adjusted scenarios + aggregation-SE parity assertions. | `benchmarks/R/benchmark_staggered_triplediff.R` | #245 | Medium |
| Staggered DDD per-cohort group-effect SEs include WIF (both surfaces - one shared engine since 3(b)) (conservative vs R's `wif=NULL`); documented in REGISTRY. Could override the mixin for an exact R match (verification needs R `triplediff`). | `_staggered_triple_diff_engine.py` | #245 | Low |
| **WooldridgeDiD follow-up cluster** (PR-B Stage D/E fail-closed surfaces; re-enable after R/Stata validation): <br>• QMLE sandwich uses `aweight` cluster adjustment `(G/(G-1))·(n-1)/(n-k)` vs Stata's `G/(G-1)` (conservative); add a `qmle` weight type if Stata goldens confirm a material difference (`wooldridge.py`, `linalg.py`). <br>• response-scale APE / log-link coefficient bridge for R `etwfe(family=poisson|logit)` cell-level parity — needs `emfx()` APE extraction or link-inversion with baseline-mean adjustment (`generate_wooldridge_golden.R`, `test_methodology_wooldridge.py`). <br>• `aggregate(weights="cohort_share")` on survey-weighted fits: `_n_g_per_cohort` uses raw `unit.nunique()`; implement design-weighted unit totals per cohort (paper W2025 §7) and lift the `ValueError` gate (`wooldridge.py`, `wooldridge_results.py`). <br>• unconditional inference for `cohort_share` accounting for ω̂_g sampling uncertainty (W2025 §7.5); currently NaN-closed (`wooldridge_results.py`). <br>• `cohort_trends=True × survey_design` and `× control_group="never_treated"` raise `NotImplementedError` (unvalidated TSL variance / trend columns spanned jointly by the placebo cells and the unit FE, which absorb the cohort indicator and recover the omitted reference) (`wooldridge.py`). <br>• ~~Stata `jwdid` golden-value `TestReferenceValues`~~ RESOLVED: the golden ships four arms pinned by `tests/test_etwfe_cs_stata_parity.py` (no `TestReferenceValues` symbol was ever added). The QMLE bullet above REMAINS OPEN -- every golden arm is linear `jwdid`, so no QMLE cluster-SE reference exists, and SEs are pinned only as a ratio. | `wooldridge.py`, `wooldridge_results.py`, `linalg.py`, benchmarks | #216 · PR-B | Med-Low |
| Extend `WooldridgeDiD` `method ∈ {logit, poisson}` with `vcov_type ∈ {classical, hc2, hc2_bm}`: composing HC2 leverage + Bell-McCaffrey DOF with the QMLE pseudo-residual sandwich needs derivation + R parity vs `clubSandwich::vcovCR(glm, type="CR2")`. Rejected at `__init__`. | `wooldridge.py` | follow-up | Medium |
| Multi-constraint CR2 parallel-trends test (AHT/HTZ) for `hc2_bm` fits: DiagnosticReport's PT check routes `vcov_type="hc2_bm"` sources to Bonferroni over the BM-adjusted per-row p-values because the generic chi-square joint Wald would discard the CR2 small-sample correction (see REPORTING.md "hc2_bm parallel-trends policy"). The proper joint test is the AHT/HTZ Wald with a Satterthwaite-style denominator df over the pre-period contrast block; needs derivation for the stacked/pooled WLS-CR2 layout + parity vs `clubSandwich::Wald_test(..., test="HTZ")`. | `diagnostic_report.py`, `linalg.py` | vcov/df round-trip PR | Low |
Expand Down
2 changes: 1 addition & 1 deletion METHODOLOGY_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ These three are feature deferrals (paper-supported extensions that the library h

| Field | Value |
|-------|-------|
| Module | `staggered_triple_diff.py`, `staggered_triple_diff_results.py` |
| Module | `_staggered_triple_diff_engine.py` (the shared engine), `staggered_triple_diff.py` (deprecated class surface), `staggered_triple_diff_results.py` |
| Primary Reference | Ortiz-Villavicencio & Sant'Anna (2025) — same paper as TripleDifference, staggered case |
| R Reference | `triplediff::ddd(panel=TRUE)` + `agg_ddd()` (per `benchmarks/R/benchmark_staggered_triplediff.R`) |
| Status | **Complete** |
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,14 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
- [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html) - Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover on near-control units; handles non-staggered and staggered timing; supports survey-design variance under `survey_design=` for HC1 / CR1 (Wave E.1 Binder TSL) and Conley (Wave E.2 panel-aware stratified-Conley sandwich on per-period PSU totals; extended in Wave E.2 follow-up to `conley_lag_cutoff > 0` via panel-block composition with within-PSU serial Bartlett HAC — `lag>0` requires an effective PSU via explicit `survey_design.psu` or injected `cluster=<col>`); `SurveyDesign.subpopulation()` preserves full-design `n_psu` / `df_survey` via zero-padded scores (Wave E.3, R `svyrecvar(subset())` form)
- [SyntheticDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - Synthetic DiD combining standard DiD and synthetic control for few treated units
- [SyntheticControl](https://diff-diff.readthedocs.io/en/stable/api/synthetic_control.html) - Abadie, Diamond & Hainmueller (2010) classic synthetic control for a single treated unit (donor-weight counterfactual, nested/cv/inverse-variance/custom V; in-space placebo permutation inference via `in_space_placebo()`, plus ADH-2015 `leave_one_out()` + `in_time_placebo()` robustness, Firpo-Possebom (2018) test-inversion confidence sets, and Chernozhukov-Wüthrich-Zhu (2021) conformal inference)
- [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html) - triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility
- [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html) - triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility; serves both the 2x2x2 and the staggered-adoption design from one signature (`fit(..., first_treat=)` selects the staggered engine)
- [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html) - Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves
- [HeterogeneousAdoptionDiD](https://diff-diff.readthedocs.io/en/stable/api/had.html) - de Chaisemartin, Ciccia, D'Haultfœuille & Knau (2026) for designs where **no unit remains untreated**; local-linear estimator at the dose support boundary returning Weighted Average Slope (WAS) on Design 1' (`d̲ = 0` / QUG) or `WAS_{d̲}` on Design 1 (`d̲ > 0`, continuous-near-d̲ or mass-point), with a multi-period event-study extension (last-treatment cohort, pointwise CIs). **Panel-only** in this release - repeated cross-sections rejected by the validator. Alias `HAD`.
- [RegressionDiscontinuity](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Calonico, Cattaneo & Titiunik (2014) sharp, fuzzy, AND covariate-adjusted regression discontinuity with robust bias-corrected inference and rdrobust-parity bandwidth selection (all 10 selectors, mass-point handling; fuzzy via `takeup=` with a first-stage block and weak-identification warning; covariates via `covariates=` - CCFT 2019, same estimand, covariate-aware bandwidths). Canonical `att` is the bias-corrected estimate with a coherent robust CI (rdrobust's printed headline is `att_conventional`). Alias `RDD`.
- [StackedDiD](https://diff-diff.readthedocs.io/en/stable/api/stacked_did.html) - Wing, Freedman & Hollingsworth (2024) stacked DiD with Q-weights and sub-experiments; optional covariate balancing (Ustyuzhanin 2026)
- [EfficientDiD](https://diff-diff.readthedocs.io/en/stable/api/efficient_did.html) - Chen, Sant'Anna & Xie (2025) efficient DiD with optimal weighting for tighter SEs
- [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html) - Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
- [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference) - Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT
- [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference) - Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT (deprecated 3.9 - use `TripleDifference` with `first_treat=`)
- [WooldridgeDiD](https://diff-diff.readthedocs.io/en/stable/api/wooldridge_etwfe.html) - Wooldridge (2023, 2025) ETWFE: saturated OLS, logit/Poisson QMLE (ASF-based ATT). Alias `ETWFE`.
- [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html) - Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting), variance- or equally-weighted ATT, for absorbing or non-absorbing (reversible) treatment
- [ChangesInChanges](https://diff-diff.readthedocs.io/en/stable/api/changes_in_changes.html) - Athey & Imbens (2006) nonlinear/distributional DiD for the 2x2 design: full counterfactual distribution and quantile treatment effects via CDF transformation, plus the QDiD comparison estimator; bootstrap inference; R qte parity. Alias `CiC`
Expand Down
Loading