You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(staggered,inference): CS positional base period + absorbed-FE full-K SE parity vs R
Two standard-error parity fixes vs canonical R (SE-audit D3, D4).
D3 - CallawaySantAnna base period is now selected positionally (nearest observed
period) via shared `_select_base_period` / `_valid_periods_for_group` helpers used
across every estimation path (panel fast/vectorized/covariate-reg, repeated
cross-sections, and the universal filters), matching R `did::att_gt` on gapped
(non-consecutive) grids. The pre/post split is corrected to `t < g` (independent of
anticipation; only the post/universal base uses anticipation), verified against a
deparse of `did` 2.5.1 `compute.att_gt`. Consecutive grids are byte-identical to the
old rule; gapped panels now reproduce every R cell (fewer_periods {1,3,4,6},
non_consecutive {1,2,5,7}) instead of NaN-ing cells whose calendar base was
unobserved, and universal no longer materializes the positional base as a fake zero
cell. Aggregated event-study / group SEs on gapped panels are corrected accordingly.
D4 - absorbed-FE (`absorb=`) non-clustered classical/hc1 standard errors now use the
full-K finite-sample scale (`linalg._absorbed_fe_vcov_scale`, a single scalar rescale
of the k_visible vcov; fail-closed to NaN inference when the full-K residual dof is
non-positive), matching `fixest::feols(vcov="iid"/"hetero")` and the explicit
full-dummy oracle. Applies to TwoWayFixedEffects(classical),
DifferenceInDifferences(absorb=), and MultiPeriodDiD(absorb=). Clustered SEs are
unchanged (gated on cluster_ids is None): the k_visible CR1 scale matches fixest for
FE nested in the cluster; non-nested absorbed FE (e.g. time FE with unit clustering)
are a small documented deviation tracked in TODO.md. hc2/hc2_bm (Satterthwaite DOF),
survey vcov, full-dummy fits, and SunAbraham hc1 (auto-clusters) are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178r8ZK6VRiXbaBiknrWjQB
Copy file name to clipboardExpand all lines: TODO.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -110,6 +110,7 @@ exists but parity can't be verified without a local toolchain.
110
110
| 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 |
111
111
|`PreTrendsPower` CS/SA `anticipation=1` R-parity fixture: R `pretrends` has no anticipation parameter, so the Python `_extract_pre_period_params` anticipation filter isn't R-parity-locked. Build a synthetic CS/SA result with `anticipation=1` and assert γ_p matches R's `slope_for_power()`. (Mechanism already covered by MC + full-VCV tests.) |`tests/test_methodology_pretrends.py`, `generate_pretrends_golden.R`| PR-C | Low |
112
112
| Harmonize SunAbraham's HC1 within-transform finite-sample correction with `fixest::sunab()` — SA applies `n/(n-k_dm)`, fixest applies `n/(n-k_total)` (counts absorbed FE); ~1-2% SE difference, documented as a "Deviation from R" and pinned at `atol=5e-3`. Either thread `df_adjustment` or keep as an intentional, R-verified difference. |`sun_abraham.py`, `linalg.py`| follow-up | Low |
113
+
| Absorbed-FE **clustered** CR1 with *non-nested* FE: for `absorb=[FE1,FE2], cluster=FE1` (e.g. `absorb=["unit","time"], cluster="unit"`), `fixest` counts the non-nested FE (time) in the CR1 `(n-1)/(n-k)` finite-sample denominator, but the clustered path uses only `k_visible`. D4 harmonized the *non-clustered* classical/hc1 full-K scale (`_absorbed_fe_vcov_scale`) and left the clustered path unchanged — correct for FE nested in the cluster, a small deviation for non-nested FE (documented in REGISTRY within-transform note). Thread a non-nested `df_adjustment` into the clustered CR1 factor; verify vs `fixest::feols(..., cluster=)`. |`linalg.py`, `estimators.py`| SE-audit D4 | Low |
113
114
| Rust multiplier-bootstrap weight RNG (`generate_bootstrap_weights_batch`) seeds `Xoshiro256PlusPlus::seed_from_u64(seed+i)` per row; audit Python callers (`sdid.py`, `efficient_did_bootstrap.py`, `bootstrap_utils.py`) for parity-test gaps and, where a numpy-canonical equivalent exists, pre-generate in Python and pass through PyO3 (same fix shape as TROP RNG parity #354). |`rust/src/bootstrap.rs`, `bootstrap_utils.py`| follow-up | Medium |
114
115
|`SyntheticDiD` bootstrap cross-language parity anchor vs R `synthdid::vcov(method="bootstrap")` or Julia `Synthdid.jl` (refit-native). Same-library validation is in place; Julia is the cleanest target. Tolerance ~1e-6 (BLAS+RNG paths preclude 1e-10). |`benchmarks/R/`, `benchmarks/julia/`, `tests/`| follow-up | Low |
115
116
| CS R helpers hard-code `xformla = ~1`; no covariate-adjusted R benchmark for the IRLS path. |`tests/test_methodology_callaway.py`|#202| Low |
0 commit comments