Skip to content

feat(rdd): covariate-adjusted RD - covariates= on fit() with rdrobust 4.0.0 parity (CCFT 2019)#691

Open
igerber wants to merge 1 commit into
mainfrom
rdd-covariates
Open

feat(rdd): covariate-adjusted RD - covariates= on fit() with rdrobust 4.0.0 parity (CCFT 2019)#691
igerber wants to merge 1 commit into
mainfrom
rdd-covariates

Conversation

@igerber

@igerber igerber commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds CCFT (2019) covariate adjustment to RegressionDiscontinuity (sharp AND fuzzy) via fit(..., covariates=[...]) (R's covs=) - additive common-coefficient specification with a pooled-across-sides gamma; the estimand is UNCHANGED (precision only, explicitly contrasted with the DiD estimators' conditional-parallel-trends covariates role), and the covariate-balance placebo recipe (fit each covariate as the outcome) is documented in the module docstring and REGISTRY.
  • Bandwidths are covariate-aware: Z stacks into every pilot fit of all three selector chains with a per-pilot partialled gamma and extended delta vectors (fuzzy+covariates composes the ratio machinery with the length-(2+dZ) variance vector of rdrobust.R:722).
  • covs_drop=True constructor knob (R name/default): redundant covariates are dropped via a direct port of LINPACK dqrdc2's rank/pivot loop (R's exact qr(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=False is a deterministic strict error.
  • Degenerate covariate adjustment is GUARDED, not reproduced (documented Deviation from R in REGISTRY): R's 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 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 at 1e-15), and warns once per fit naming the columns; well-posed systems solve with the same pinv(rcond=1e-20) semantics as R and match at machine precision.
  • Results surface: name-keyed covariate_coefficients (+ fuzzy first_stage_covariate_coefficients) nuisance-gamma echoes, covariates/covariates_dropped/covs_drop config echoes, covariate-adjusted summary() banner mirroring R's rdmodel string.

Methodology references (required if estimator / math changes)

  • Method name(s): Covariate-adjusted sharp/fuzzy regression discontinuity (additive common-coefficient adjustment)
  • Paper / source link(s): Calonico, Cattaneo, Farrell & Titiunik (2019), "Regression Discontinuity Designs Using Covariates", REStat 101(3), 442-451, https://doi.org/10.1162/rest_a_00760 (review on file: docs/methodology/papers/calonico-cattaneo-farrell-titiunik-2019-review.md); parity source: CRAN rdrobust 4.0.0 (tarball sha256 pinned in diff_diff/_rdrobust_port.py)
  • Any intentional deviations from the source (and why): all labeled in docs/methodology/REGISTRY.md (RegressionDiscontinuity section) - named-column collinearity warning (R's is generic), rank-0 fail-closed error, deterministic covs_drop=False strict mode (R's chol() 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 added/updated: tests/test_rdrobust_port.py (TestCovsPortGoldenParity - 9 configs incl. gamma matrices and per-side biases; TestCovsPortValidation; TestCovsDropFun - dqrdc2 rank/pivot unit pins vs live-R qr() 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 identity tau_adj = tau_unadj - gamma' tau_Z EXACT at common manual bandwidths for both conventional and bias-corrected rows, span/order invariance, CI shrinkage, degenerate-guard contracts), tests/test_rdd.py (TestCovariatesAPI)
  • Backtest / simulation / notebook evidence (if applicable): golden regeneration reproduced all 23 pre-existing configs EXACTLY; the 9 new covariate configs match installed rdrobust 4.0.0 at worst 6.2e-12 relative

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Generated with Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4

… 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
@github-actions

Copy link
Copy Markdown

Overall Assessment

✅ Looks good. No unmitigated P0/P1 findings.

Executive Summary

  • Affected methods: covariate-adjusted sharp/fuzzy RegressionDiscontinuity, plus rdbwselect() / rdrobust_fit() covariate paths.
  • The implementation matches the Registry’s CCFT 2019 contract: common additive gamma, unchanged estimand, covariate-aware bandwidths, and fuzzy extended variance vector.
  • Inference uses safe_inference() for main and first-stage rows; I did not find new inline inference or partial-NaN guard anti-patterns.
  • covs_drop is propagated through constructor params, bandwidth selection, estimation, results, and tests.
  • Local tests were not runnable in this environment because pytest is not installed.

Methodology

  • Severity: P3 informational
    Impact: Covariate balance is required for consistency, and the automatic packaged balance helper remains deferred. This is documented in the module docstring and Registry with the prescribed placebo recipe, so it is not a blocker.
    Concrete fix: No required fix for this PR; future work can add the packaged helper described in the Registry.
    References: diff_diff/rdd.py:L22-L44, docs/methodology/REGISTRY.md:L3651-L3692, docs/methodology/REGISTRY.md:L3867-L3874.

  • Severity: P3 informational
    Impact: The guarded degenerate covariate solve intentionally deviates from R’s platform-sensitive ginv(tol=1e-20) behavior. This is documented as a Registry deviation and implemented with warnings/strict errors, so it is not a defect.
    Concrete fix: No required fix.
    References: diff_diff/_rdrobust_port.py:L417-L506, docs/methodology/REGISTRY.md:L3823-L3866.

Code Quality

No findings. The new public parameter is validated and included in get_params() / set_params(), and covariates are validated before model assembly.
References: diff_diff/rdd.py:L645-L715, diff_diff/rdd.py:L784-L807.

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.
References: diff_diff/_rdrobust_port.py:L557-L720, diff_diff/_rdrobust_port.py:L1059-L1105.

Maintainability

No findings. The R source anchors and Registry cross-references are explicit, and the covariate-specific logic is centralized in _covs_gamma() / _covs_entry_drop().
References: diff_diff/_rdrobust_port.py:L37-L64, diff_diff/_rdrobust_port.py:L417-L543.

Tech Debt

No untracked blocker found. The deferred covariate-balance helper is documented in the Registry; broader RD v1 seams remain documented.
References: docs/methodology/REGISTRY.md:L3867-L3874.

Security

No findings. I did not see secrets or unsafe external-input behavior in the changed code paths.

Documentation/Tests

  • Severity: P3 informational
    Impact: I could not run the RD test subset locally because pytest is unavailable in this environment (python -m pytest ... failed with No module named pytest).
    Concrete fix: Run the RD test subset in CI or an environment with test dependencies installed.
    References: tests/test_rdd.py, tests/test_rdd_methodology.py, tests/test_rdd_parity.py, tests/test_rdrobust_port.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant