feat(inference): opt-in df_convention="cluster" knob for clustered t/p/CI#667
Conversation
PR Review ReportOverall Assessment✅ Looks good Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Verification note: |
…p/CI Clustered analytical fits compute t/p/CI at the fitted residual df (n - K_full) while fixest/Stata use the cluster df G - 1 (the documented clustered-CR1 inference-df deviation, REGISTRY sec. TwoWayFixedEffects). df_convention="cluster" on DifferenceInDifferences / TwoWayFixedEffects / MultiPeriodDiD (and the LinearRegression linalg surface) opts into the Stata/fixest convention. Default stays "residual"; flips at v4. - Fallback-level only: survey df and per-coefficient Bell-McCaffrey DOF keep precedence; conley excluded (no documented G-1 reference for the product kernel); inert on unclustered fits; only the reference t-distribution changes (point estimates/SEs/t-stats untouched). - Result metadata: DiDResults/MultiPeriodDiDResults carry df_convention + inference_df (set only when finite and strictly positive), included in to_dict() only when set. - Fail-closed edges hardened along the way (7 local review rounds): G_eff <= 1 returns NaN inference instead of normal-theory fallback (linalg + MPD twins); CR1 cluster validation counts positive-weight clusters for ALL weight types (the fweight carve-out let a degenerate one-effective-cluster SE through); missing (NaN/None) cluster labels are REJECTED at a shared front door across NumPy/Rust/CR2-BM backends (the meat's groupby drops NaN rows, so no count can agree with the sandwich partition - silently wrong SEs, pre-existing). - effective_cluster_count(): factorize+bincount O(n) shared counter. - sklearn-compatible: get_params/set_params round-trip with transactional validation; TWFE/MPD inherit via the DiD base class. TestDfConvention (17 tests): exact t(G-1) tail match on DiD/TWFE/MPD incl. MPD avg_att, byte-identical no-op default, precedence orderings, conley exclusion, NaN-label rejection on all backend routes, fail-closed one-cluster edges, validation + round-trip; both backends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lbd6nqWmg4snvvBmegwqiw
6ed7b0b to
8a1b697
Compare
|
🔁 AI review rerun (requested by @igerber) Head SHA: PR Review ReportOverall Assessment✅ Looks good Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Verification: |
Summary
df_convention="cluster"inference-df knob onDifferenceInDifferences/TwoWayFixedEffects/MultiPeriodDiD(inherited via the DiD base class) and theLinearRegressionlinalg surface: clustered analytical t/p/CI at the Stata/fixest cluster dfG − 1instead of the fitted residual df (the documented clustered-CR1 inference-df deviation). Default stays"residual"; the default flips at v4 (tracked in TODO Deferred → Parked — it moves every clustered p-value/CI).hc2_bm) keep precedence;vcov_type="conley"excluded (no documentedG − 1reference for the Conley+cluster product kernel); inert on unclustered fits. Point estimates, SEs, and t-statistics are untouched — only the reference t-distribution changes.DiDResults/MultiPeriodDiDResultsgaindf_convention+inference_df(the effective df the reported t/p/CI actually used; set only when finite and strictly positive), surfaced into_dict()only when set.G_eff ≤ 1→ NaN inference instead of silent normal-theory fallback (linalg + MPD twins); CR1 cluster validation counts positive-weight clusters for ALL weight types (the fweight carve-out let a degenerate one-effective-cluster CR1 SE through — pre-existing); missing (NaN/None) cluster labels rejected at a shared_validate_cluster_idsfront door across the NumPy, Rust, and CR2-BM backends (the meat's groupby drops NaN-labelled rows, so no cluster count can agree with the sandwich's partition — silently wrong SEs, pre-existing; matches R fixest/Stata, which error on missing cluster values).effective_cluster_count()(factorize + bincount, O(n)).Methodology references (required if estimator / math changes)
G − 1).t(G−1)tail-match tests at rtol 1e-12.Validation
tests/test_estimators_vcov_type.py::TestDfConvention(17 tests): exactt(G−1)p-value/CI match on DiD/TWFE/MPD incl. MPDavg_att; byte-identical no-op default; hc2_bm and survey-df precedence; conley exclusion; NaN-cluster-label rejection on every backend route (solve_ols/rust HC1/CR2-BM/MPD); fail-closed one-effective-cluster edges; transactional set_params validation; get_params round-trip. Both backends (rust + pure Python).Security / privacy
Generated with Claude Code