Repository navigation
feat(v4): item 7b of the 4.0 cut - SpilloverDiD auto-cluster, uniform cluster=False, cluster metadata (M-080, M-155) - #880
Conversation
… cluster=False, cluster metadata (M-080, M-155) SpilloverDiD auto-clusters at unit when cluster= is omitted - the one default standard error that moves at 4.0 - with TwoWayFixedEffects' carve-outs mirrored: an explicit cluster= wins on every lane, the auto-cluster is dropped on the Conley path and is never injected as a survey PSU. cluster=False reproduces the 3.x observation-level HC1 numbers; results report cluster_name="unit" / n_clusters. Inference stays on the residual df until the df_convention knob lands (new planned row M-154, item 7c). cluster=False is the uniform explicit off-switch (M-080): the plain heteroskedasticity-robust sandwich with residual-df t inference on TwoWayFixedEffects (static and event-study; one-way CR2 Bell-McCaffrey under hc2_bm), SunAbraham, WooldridgeDiD (the OLS within-transform lane gains the absorbed-FE full-K rescale so its SE equals HC1 on the explicit full-dummy design; logit/poisson run the plain QMLE sandwich) and LPDiD (per-horizon HC1, an if_robust unclustered IF variance on the regression-adjustment path, implicit per-observation PSUs under a survey design); an accepted bit-identical no-op on the unit-level-by- construction estimators (CallawaySantAnna incl. its RC auto-PSU path, EfficientDiD incl. hausman_pretest, DMLDiD, ChaisemartinDHaultfoeuille, staggered TripleDifference, LWDiD incl. the sensitivity helpers, HeterogeneousAdoptionDiD, ImputationDiD, TwoStageDiD) and on the 2x2 DifferenceInDifferences / 2x2x2 TripleDifference surfaces; still rejected on StackedDiD. The combinations that would otherwise ship an unverified variance reject it: inference="wild_bootstrap" (DiD/TWFE), WooldridgeDiD(n_bootstrap > 0) and vcov_type="hc2_bm" on SunAbraham / WooldridgeDiD (cluster-only Bell-McCaffrey DOF; the one-way port is a TODO.md row). SunAbraham's pairs bootstrap accepts it as a no-op. The shared contract is utils.validate_cluster (new row M-155): cluster is str | None | False on every estimator exposing it, validated at __init__, through set_params and again at fit(); DMLDiD's pre-4.0 str-only check is generalized and False is normalized locally so it never reaches a column lookup. Metadata: DiD/TWFE analytical fits report n_clusters from the regression's effective cluster count (non-survey fits); SunAbrahamResults gains an appended n_clusters field and reports cluster_name on clustered analytical fits, and its summary() renders the variance-family label on analytical non-survey fits; TWFE's survey-lane auto label is None (the regression received no cluster ids). Tests: new tests/test_v4_cluster_policy.py (the two rows' test_ref) pins the contract roster, the Spillover default and its carve-outs, every c1 off-switch lane (incl. the TWFE and Wooldridge full-dummy HC1 parity, the LPDiD singleton-cluster identity, the saturated-Wooldridge NaN contract and the four rejects), the b/c2 no-op, the metadata lanes and the ledger. test_spillover.py pins its Wave D goldens with cluster=False (the HC1 lane they were captured on) and extends the CR1-routing test to the default; the inference-policy and wild-bootstrap pins follow the new DiD message and the analytical n_clusters; the tutorial-23 drift suite pins the default, the CR1 > HC1 > Conley ordering and the four-row table source. Tutorial 23 section 6 compares CR1 at unit, HC1 (cluster=False) and the two Conley fits. Ledger: M-080 and M-155 -> done, M-154 -> planned (matrix gate counts 137); spec section 7/9, the variance-conventions roster, REGISTRY notes (DiD, TWFE, SunAbraham, WooldridgeDiD, LPDiD, SpilloverDiD, the TWFE knob roster, the category-b classes), the migration guide, llms-full.txt, docs/api/spillover.rst and TODO.md updated.
Overall assessment: ✅ Looks goodNo unmitigated P0 or P1 findings identified in the diff. Executive summary
MethodologyP3 — Documented inference exceptions. Cross-checks against the Registry and committed paper reviews support the changed CR1/HC1 routing, Wooldridge’s absorbed-FE rescale, and LPDiD’s documented IF variance convention. Impact: Spillover retains residual-df inference at this release step; SunAbraham/Wooldridge reject unsupported unclustered Bell–McCaffrey inference. Both are explicitly documented. Code QualityNo actionable findings. Shared validation preserves the raw constructor value while normalizing PerformanceNo actionable findings identified in the changed paths. MaintainabilityNo actionable findings. Parameter introspection through Tech DebtP3 — Follow-ups are tracked. Impact: One-way Bell–McCaffrey support, its remaining external parity check, and tutorial refreshes remain incomplete but explicitly tracked. SecurityNo introduced secrets or security vulnerabilities identified. Documentation/TestsP3 — Qualify Spillover’s blanket residual-df description. Impact: “On every lane” conflicts with the existing survey-df precedence implemented at Validation completed: Python 3.9 syntax checks passed for all 18 changed library modules; extracted validator checks and notebook code parsing passed. Estimator tests and numerical parity remain unverified locally. |
…y-df precedence The SpilloverDiD cluster docstring and the REGISTRY 7b Note said inference uses the residual df "on every lane"; the survey-design df has always taken precedence under survey_design= (spillover.py df_for_inference). Both now say "every non-survey lane" and name the precedence. Documentation only.
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: ✅ Looks goodNo unmitigated P0 or P1 findings identified. The prior documentation finding is resolved. Executive summary
MethodologyReviewed SpilloverDiD, TWFE, Sun–Abraham, WooldridgeDiD and LPDiD against the Registry, docstrings and available committed paper reviews. No undocumented methodology mismatch identified. P3 — Documented inference exceptions; informational. Impact: Spillover retains residual-df inference pending M-154; SunAbraham and WooldridgeDiD reject unsupported unclustered Bell–McCaffrey inference. These are documented policies. Code QualityNo actionable findings. Changed paths normalize PerformanceNo actionable findings in the changed paths. MaintainabilityNo actionable findings. Tech DebtP3 — Remaining follow-ups are tracked. Impact: One-way Bell–McCaffrey support, the event-study external parity check and remaining tutorial refreshes remain explicitly tracked. SecurityNo introduced secrets or security vulnerabilities identified. Documentation/TestsThe prior P3 finding is resolved: The required changelog fragment is present. Validation passed for Python 3.9 syntax across all 18 changed library modules, extracted shared-validator checks and notebook code parsing. Numerical estimator tests could not run because pytest, NumPy, pandas and SciPy are unavailable. |
Summary
docs/v4-design.mdsection 9 item 7, second half; [M-080] as re-scoped + the new shared-contract row [M-155]). Follows 7a (feat(v4): item 7a of the 4.0 cut - flip the df_convention default to "cluster" (M-004..M-006, M-128..M-131) #879).unitwhencluster=is omitted - the one default standard error that moves at 4.0 - mirroringTwoWayFixedEffects' carve-outs (explicitcluster=wins; the auto-cluster is dropped on the Conley path and never injected as a survey PSU).cluster=Falsereproduces the 3.x observation-level HC1 numbers; results reportcluster_name="unit"/n_clusters. Inference stays on the residual df until thedf_conventionknob lands (new planned row [M-154], item 7c = the next PR).cluster=Falseis the uniform explicit off-switch: the plain heteroskedasticity-robust sandwich with residual-df t inference on TWFE (static + event-study; one-way CR2 Bell-McCaffrey underhc2_bm), SunAbraham, WooldridgeDiD (the OLS within-transform lane gains the absorbed-FE full-K rescale so its SE equals HC1 on the explicit full-dummy design; logit/poisson run the plain QMLE sandwich) and LPDiD (per-horizon HC1, anif_robustunclustered IF variance on the regression-adjustment path, implicit per-observation PSUs under a survey design); an accepted bit-identical no-op on the unit-level-by-construction estimators and the 2x2 surfaces; still rejected on StackedDiD. Combinations that would ship an unverified variance reject it:inference="wild_bootstrap"(DiD/TWFE),WooldridgeDiD(n_bootstrap > 0), andvcov_type="hc2_bm"on SunAbraham / WooldridgeDiD (cluster-only BM DOF; the one-way port is a new TODO.md row).diff_diff.utils.validate_cluster([M-155]):clusterisstr | None | Falseon every estimator exposing it, validated at__init__, throughset_paramsand again atfit(); DMLDiD's pre-4.0 str-only check generalized.n_clustersfrom the regression's effective cluster count (non-survey fits);SunAbrahamResultsgains an appendedn_clustersand reportscluster_nameon clustered analytical fits (itssummary()renders the variance-family label on analytical non-survey fits); TWFE's survey-lane auto label isNone(the regression received no cluster ids).done, M-154 ->planned(matrix gate counts 137); spec section 7/9, the variance-conventions roster, REGISTRY notes, the migration guide,llms-full.txt,docs/api/spillover.rstand TODO.md updated. Tutorial 23 section 6 now shows the four-row SE table (CR1 at unit, HC1 viacluster=False, Conley lag 0 / lag 1) and was re-executed.Methodology references (required if estimator / math changes)
cluster=Falseoff-switch (spec section 7, [M-080]; roster indocs/methodology/variance-conventions.md"Default clustering roster"); CR1 Liang-Zeger / HC1 sandwiches; absorbed-FE full-K rescale (linalg._absorbed_fe_vcov_scale, the fixestvcov="hetero"convention); Gardner (2022) GMM first-stage correction on SpilloverDiD's default CR1 lane.df_conventionknob yet; documented REGISTRY Note + TWFE knob-roster entry; [M-154] closes it in 7c).cluster=False+hc2_bmon SunAbraham / WooldridgeDiD is rejected rather than given an unverified one-way BM DOF (REGISTRY Notes, TODO.md row). Each category's position carries a recognized**Note:**in REGISTRY.md.Validation
tests/test_v4_cluster_policy.py(118 tests: contract roster, Spillover default + carve-outs, every c1 off-switch lane incl. the TWFE and Wooldridge full-dummy HC1 parity pins, the LPDiD singleton-cluster identity, the saturated-Wooldridge NaN contract and the four rejects, the b/c2 no-op, metadata lanes, ledger);tests/test_spillover.py(Wave D goldens pinned withcluster=False; CR1-routing test extended to the default);tests/test_t23_spillover_tva_drift.py(default metadata, CR1 > HC1 > Conley ordering, four-row table source pins);tests/test_v4_inference_policy.py,tests/test_wild_bootstrap.py(new DiD message, analyticaln_clusters);tests/test_v4_matrix.py(row counts 137).docs/tutorials/23_spillover_tva.ipynbre-executed (nbmake, Rust backend). Full default suite 15428 passed / 204 skipped; pure-Python backend pass on the new file; CI-pinned mypy clean; py3.9 compile ok.Security / privacy
Changelog
changelog.d/20261029-v4-pr7b-m080-cluster-policy.md)