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
1 change: 0 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ Related tracking surfaces:
| Issue | Location | Origin | Effort | Priority |
|-------|----------|--------|--------|----------|
| Consolidate the remaining estimator-entangled DR/logit score variants (`staggered.py::_doubly_robust` + RC twins, `triple_diff.py`, `lwdid.py`, `wooldridge.py`) onto the shared `_dr_scores.py` module, each migration with its own committed oracle capture (the ContinuousDiD lift's two-tier pattern in `tests/test_dr_scores.py`); and add a ridge vcov path to `solve_ridge` if an estimator ever needs analytical ridge inference | `diff_diff/_dr_scores.py` | dml-b0 | Mid | Low |
| hc2/hc2_bm floor `1 - h_ii` at 1e-10 in the shared leverage meat, fabricating finite (if inflated) variances for leverage-one observations - hc3 now fails closed there (LWDiD fix wave) but the pre-existing hc2 family behavior is released surface; decide fail-closed vs keep-floor for hc2/hc2_bm | `diff_diff/linalg.py` | #588 | Quick | Low |
| Numeric between-period cohorts (e.g. `first_treat=4.5` with integer times) are rejected by LWDiD while CallawaySantAnna estimates them and LWDiD's own datetime/Period cohorts map to the next observed period — close the dtype asymmetry by adopting the next-observed-period mapping for numeric cohorts too (contract documented in REGISTRY cohort-encodings Note + `docs/api/lwdid.rst` Input Contract). Lands only after PR #588 merges | `diff_diff/lwdid.py` | #588 | Quick | Low |
| Implement the LW 2026 eq. 7.9/7.10 unit-average cohort estimand (regress per-unit post-average transformed outcomes on `[1, D_g]` vs never-treated) as an alternative to the documented cell-mass `cohort_effects` convention (REGISTRY within-cohort aggregation Note; the two differ on unbalanced panels, where cell-mass weights units by observed post periods). Needs the 7.10 regression + its covariance on the NT path. Lands only after PR #588 merges | `diff_diff/lwdid_staggered.py` | #588 | Quick | Low |
| Expose cell-mass overall ATT (Stata `Post_avg` convention; = CS-simple on balanced panels) as an aggregate extra on LWDiD results — the fit's `.att` is the paper's `tau_omega` (cohort-mean-then-treated-weight, eq. 7.18); the authors' large-N display uses cell-mass weighting instead, and both are legitimate estimands (see the REGISTRY LWDiD Aggregation note). Lands only after PR #588 merges | `diff_diff/lwdid_results.py` | #588 | Quick | Low |
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/20260905-hc2-leverage-one.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### Behavioral Changes
- **HC2 and unweighted, unclustered HC2-BM now fail closed at leverage one:**
an effective observation with hat-matrix leverage at least `1 - 1e-8`
produces a warning and entirely NaN covariance (and requested degrees of
freedom), preserving point estimates while suppressing undefined inference.
Python and Rust agree; over-one leverage no longer substitutes HC1.
Older Rust extensions without the fail-closed HC2 capability use NumPy
for HC2 while retaining their other accelerations.
Weighted and clustered HC2-BM retain their separate CR2 conventions,
including all-ones probability weights.

### Fixed
- **Zero-weight observations do not invalidate HC2/HC3 inference:** excluded
rows contribute zero to the covariance and cannot trigger the leverage
guard. Zero-frequency rows now agree with dropping those rows or expanding
the frequency counts literally.
- **LWDiD uses the shared HC2 covariance guard:** leverage-one regressions
retain their point estimate and unavailable influence contribution while
emitting one covariance warning per regression, without a duplicate local
warning.
10 changes: 5 additions & 5 deletions diff_diff/_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,13 @@
except ImportError:
_rust_batched_ridge_chol_solve = None

# HC2 (leverage-corrected) robust vcov: imported independently for the same
# mixed-version reason as demean_map (a stale extension missing only this
# newer symbol degrades HC2 to the NumPy path without disabling the older
# Rust accelerations).
# HC2 requires the v2 fail-closed leverage contract. An older extension can
# export the original symbol yet return finite covariance at unit leverage,
# so symbol presence alone is insufficient. Import v2 independently: legacy
# extensions use NumPy HC2 while retaining every other Rust acceleration.
try:
from diff_diff._rust_backend import (
compute_robust_vcov_hc2 as _rust_compute_robust_vcov_hc2,
compute_robust_vcov_hc2_v2 as _rust_compute_robust_vcov_hc2,
)
except ImportError:
_rust_compute_robust_vcov_hc2 = None
Expand Down
8 changes: 8 additions & 0 deletions diff_diff/estimators.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,17 @@ class DifferenceInDifferences(BaseEstimator):
(library default). With ``cluster=``, uses CR1 (Liang-Zeger).
- ``"hc2"``: leverage-corrected meat (one-way only). Errors with
``cluster=``; use ``"hc2_bm"`` for clustered Bell-McCaffrey.
Effective leverage ``h_ii >= 1 - 1e-8`` produces a warning and
entirely NaN covariance/inference, retaining point estimates.
- ``"hc2_bm"``: one-way HC2 + Imbens-Kolesar (2016) Satterthwaite DOF;
with ``cluster=``, Pustejovsky-Tipton (2018) CR2 cluster-robust.
``MultiPeriodDiD(cluster=..., vcov_type="hc2_bm")`` is supported and
uses a cluster-aware Bell-McCaffrey contrast DOF for the
post-period-average ATT (see ``_compute_cr2_bm_contrast_dof`` in
``linalg.py`` and the REGISTRY.md note). Weighted CR2-BM
(``survey_design=`` paths) is a separate gate.
Unweighted, unclustered fits share HC2's leverage-one NaN guard;
clustered CR2 and design-based survey inference are separate.
- ``"hc3"``: jackknife-style leverage correction, meat
``e_i^2 / (1 - h_ii)^2`` (one-way only; errors with ``cluster=``).
A leverage-one observation has no defined HC3 variance and the
Expand Down Expand Up @@ -2513,12 +2517,16 @@ class MultiPeriodDiD(DifferenceInDifferences):
(library default). With ``cluster=``, uses CR1 (Liang-Zeger).
- ``"hc2"``: leverage-corrected meat (one-way only). Errors with
``cluster=``; use ``"hc2_bm"`` without cluster for Bell-McCaffrey.
Effective leverage ``h_ii >= 1 - 1e-8`` produces a warning and
entirely NaN covariance/inference, retaining point estimates.
- ``"hc2_bm"``: one-way HC2 + Imbens-Kolesar (2016) Satterthwaite DOF
per coefficient plus a contrast-aware DOF for the post-period-average
ATT. With ``cluster=``, dispatches to Pustejovsky-Tipton (2018)
CR2 cluster-robust with a Bell-McCaffrey Satterthwaite contrast DOF
on the post-period average (see ``cluster`` above for parity
details). Weighted CR2-BM (``survey_design=``) is still gated.
Unweighted, unclustered fits share HC2's leverage-one NaN guard
for covariance and all contrast DOFs, including the average ATT.
- ``"hc3"``: jackknife-style leverage correction, meat
``e_i^2 / (1 - h_ii)^2`` (one-way only; errors with ``cluster=``).
A leverage-one observation has no defined HC3 variance and the
Expand Down
Loading
Loading