feat(rdd): covariate-adjusted RD - covariates= on fit() with rdrobust 4.0.0 parity (CCFT 2019)#691
Conversation
… 4.0.0 parity (CCFT 2019) Adds the CCFT 2019 additive common-coefficient covariate adjustment to RegressionDiscontinuity (sharp AND fuzzy), parity-targeting R rdrobust 4.0.0 end-to-end. The estimand is unchanged (precision only, unlike the DiD estimators' conditional-parallel-trends covariates role); bandwidths are covariate-aware (Z threads into every pilot of all three selector chains with a per-pilot partialled gamma); collinear covariates are dropped with a warning naming them under covs_drop=True (R's exact dqrdc2 rank/pivot semantics incl. the name-length column sort, ported directly; covs_drop=False is a deterministic strict error). Degenerate covariate adjustment is GUARDED, not reproduced (documented Deviation from R): R's ginv(tol=1e-20) inverts a float-noise singular value on constant covariates / full dummy sets, silently returning platform-dependent estimates; diff-diff excludes per-column degeneracies (a constant covariate reproduces the fit without it bit-for-bit), applies a scale-invariant stabilized cut for rank-deficient sets (a full dummy set reproduces the drop-one-category fit - span invariance), and warns naming the columns. Well-posed systems match R at machine precision. Surfaces: covariates= (fit-time, library-wide name), covs_drop= constructor knob, name-keyed covariate_coefficients / first_stage_covariate_coefficients + covariates/covariates_dropped/ covs_drop result echoes, covariate-adjusted summary() banner. Goldens 23 -> 32 configs (9 covariate configs incl. msetwo/cercomb2 chain pins, collinear-drop, ties, fuzzy, fuzzy-sharpbw; all 23 pre-existing configs reproduced exactly on regeneration; worst covariate-config disagreement 6.2e-12). R-free anchors: the CCFT 2019 partial-out identity tau_adj = tau_unadj - gamma' tau_Z exact at common manual bandwidths (both conventional and bias-corrected rows), span/order invariance, CI shrinkage. Docs: REGISTRY covariate subsection + deviation notes, CCFT-2019/2017 review checkbox flips, api rst, llms guides, README, CHANGELOG, references, choosing-estimator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4
|
Overall Assessment ✅ Looks good. No unmitigated P0/P1 findings. Executive Summary
Methodology
Code Quality No findings. The new public parameter is validated and included in Performance No findings. Covariates add expected matrix work to bandwidth pilots and estimation; no avoidable repeated high-cost path stood out in the changed logic. Maintainability No findings. The R source anchors and Registry cross-references are explicit, and the covariate-specific logic is centralized in Tech Debt No untracked blocker found. The deferred covariate-balance helper is documented in the Registry; broader RD v1 seams remain documented. Security No findings. I did not see secrets or unsafe external-input behavior in the changed code paths. Documentation/Tests
|
CI round 1 (Linux/Windows/pure-python; macOS passed): the two constant-covariate exclusion tests asserted EXACT equality between the fit with the excluded constant column and the fit without it. The equality is exact mathematically (the excluded column's gamma row is exactly 0) but only roundoff-level numerically: the response matrix still carries the excluded column, and BLAS matmul kernels differ with matrix shape across platforms (bit-equal on Accelerate, last-ULP differences on OpenBLAS/Windows). Asserts relaxed to rel=1e-12 and the "bit-for-bit" claims in the port docstring / REGISTRY / CHANGELOG softened to "to floating-point roundoff". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment ✅ Looks good. No unmitigated P0/P1 findings. Executive Summary
Methodology
No undocumented methodology mismatch found for covariate-aware bandwidths or fuzzy covariate variance construction. References: Code Quality No findings. Performance No findings. The added matrix work is expected for covariate-aware pilots and estimation; I did not see an avoidable repeated high-cost path in the changed logic. References: Maintainability No findings. The R-source anchors and Registry notes are explicit, and the covariate-specific numerical logic is centralized in Tech Debt No untracked blocker found. RD v1 seams, including cluster/weights, weak-IV-robust fuzzy inference, and the packaged covariate-balance helper, are documented as follow-ups. Reference: Security No findings. I did not see secrets or unsafe external-input behavior in the changed paths. Documentation/Tests
|
Summary
RegressionDiscontinuity(sharp AND fuzzy) viafit(..., covariates=[...])(R'scovs=) - additive common-coefficient specification with a pooled-across-sides gamma; the estimand is UNCHANGED (precision only, explicitly contrasted with the DiD estimators' conditional-parallel-trendscovariatesrole), and the covariate-balance placebo recipe (fit each covariate as the outcome) is documented in the module docstring and REGISTRY.covs_drop=Trueconstructor knob (R name/default): redundant covariates are dropped via a direct port of LINPACK dqrdc2's rank/pivot loop (R's exactqr(z, tol=1e-7)semantics, incl. the name-length column sort - order never leaks: every user-facing covariate surface is name-keyed and order-invariance is tested) with a warning NAMING the dropped columns;covs_drop=Falseis a deterministic strict error.ginv(tol=1e-20)inverts a float-noise singular value on constant covariates / full one-hot dummy sets, silently returning platform-dependent estimates (28% cross-implementation gamma spread, ~0.5% tau shifts observed). diff-diff excludes per-column degeneracies (a constant covariate reproduces the fit without it to numerical precision), applies a scale-invariant stabilized cut for rank-deficient sets (a full dummy set reproduces the drop-one-category fit - span invariance), and warns once per fit naming the columns; well-posed systems solve with the samepinv(rcond=1e-20)semantics as R and match at machine precision.covariate_coefficients(+ fuzzyfirst_stage_covariate_coefficients) nuisance-gamma echoes,covariates/covariates_dropped/covs_dropconfig echoes, covariate-adjustedsummary()banner mirroring R's rdmodel string.Methodology references (required if estimator / math changes)
docs/methodology/papers/calonico-cattaneo-farrell-titiunik-2019-review.md); parity source: CRAN rdrobust 4.0.0 (tarball sha256 pinned indiff_diff/_rdrobust_port.py)docs/methodology/REGISTRY.md(RegressionDiscontinuity section) - named-column collinearity warning (R's is generic), rank-0 fail-closed error, deterministiccovs_drop=Falsestrict mode (R'schol()failure is roundoff-dependent), and the guarded degenerate-adjustment solve above (R's behavior there is platform-noise, i.e. irreproducible even R-to-R across BLAS builds)Validation
tests/test_rdrobust_port.py(TestCovsPortGoldenParity - 9 configs incl. gamma matrices and per-side biases; TestCovsPortValidation; TestCovsDropFun - dqrdc2 rank/pivot unit pins vs live-Rqr()incl. the tiny-scaled-column trap; TestCovsDegenerateGuard),tests/test_rdd_parity.py(covariate configs + gamma pins; config count lock 23 -> 32),tests/test_rdd_methodology.py(TestCovariates - the CCFT 2019 partial-out identitytau_adj = tau_unadj - gamma' tau_ZEXACT at common manual bandwidths for both conventional and bias-corrected rows, span/order invariance, CI shrinkage, degenerate-guard contracts),tests/test_rdd.py(TestCovariatesAPI)Security / privacy
Generated with Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4