Skip to content

feat(continuous-did): covariate support (reg + dr) under conditional parallel trends#616

Merged
igerber merged 1 commit into
mainfrom
feature/continuous-did-covariates
Jul 4, 2026
Merged

feat(continuous-did): covariate support (reg + dr) under conditional parallel trends#616
igerber merged 1 commit into
mainfrom
feature/continuous-did-covariates

Conversation

@igerber

@igerber igerber commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add covariates= + estimation_method ∈ {"reg","dr"} to ContinuousDiD for dose-response estimation under conditional parallel trends (E[ΔY(0)|D=d,X] = E[ΔY(0)|D=0,X]). Closes TODO item (a) of the CGBS-2024 extensions row.
  • Each (g,t) cell's control counterfactual becomes covariate-adjusted: reg = outcome regression on controls (ΔY_i − X_i'γ̂); dr (default) = doubly-robust (DRDID drdid_panel), adding a propensity model + scalar augmentation η̄_cont.
  • Composes with analytical SEs, the multiplier bootstrap, and event-study aggregation. reg and dr share the ATT(d) shape and ACRT(d) (point + SE), differing only in the overall_att/ATT(d) level and the doubly-robust SE.
  • Fail-closed (no-silent-failures): missing/non-finite covariates raise; dr propensity-estimation failure raises by default (pscore_fallback="error"). estimation_method="ipw" + covariates and covariates= + survey_design= raise NotImplementedError (documented deferrals — IPW's covariate adjustment is a scalar level shift that can't move the curve shape). The no-covariate path is numerically unchanged.

Methodology references (required if estimator / math changes)

  • Method name(s): ContinuousDiD covariate adjustment (Callaway, Goodman-Bacon & Sant'Anna 2024). Per-cell OR/DR follows Sant'Anna & Zhao (2020) / the DRDID package.
  • Paper / source link(s): CGBS 2024 (NBER WP 32117); DRDID::reg_did_panel / drdid_panel. See docs/methodology/REGISTRY.md § ContinuousDiD Note Add comprehensive code review for diff-diff library #5 and Key Equations.
  • Intentional deviations (and why): Library extension beyond contdid v0.1.0, which hard-stops on covariates — so there is no external R anchor for the covariate-adjusted dose curve; the scalar overall_att + SE are instead anchored exactly to DRDID, and the curve is validated by reduction + DGP recovery + MC coverage. Propensity trimming uses clip semantics (pscore_trim) vs DRDID's drop-trimming (identical on moderate-overlap data). All documented in REGISTRY Note Add comprehensive code review for diff-diff library #5.

Validation

  • Tests added/updated: tests/test_continuous_did.py (API, params validation, fail-closed guards, results metadata, event-study + bootstrap composition), tests/test_methodology_continuous_did.py (DRDID reg/dr att+SE parity [skip-guarded when R/DRDID absent], R-free NumPy influence-function cross-check at p≥2 [runs in CI], reg-vs-dr ACRT identity, DGP recovery, MC coverage [slow]).
  • Evidence: reg/dr overall_att + SE match DRDID::reg_did_panel / drdid_panel to ~1e-8 (incl. the doubly-robust SE); MC coverage reg 96% / dr 95% under conditional PT; DGP recovery (covariate-adjusted recovers the truth, unconditional is biased). Full ContinuousDiD suite 117 pass; mypy 0-new vs main.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

🤖 Generated with Claude Code

…parallel trends

Add `covariates=` and `estimation_method in {"reg","dr"}` to ContinuousDiD for
dose-response estimation under conditional parallel trends
(E[dY(0)|D=d,X] = E[dY(0)|D=0,X]). Each (g,t) cell's control counterfactual
becomes a covariate-adjusted prediction:
- reg: outcome regression on controls (dY_i - X_i'gamma_hat)
- dr (default): doubly-robust (DRDID drdid_panel), plus a scalar augmentation

Scalar overall_att + SE match DRDID reg_did_panel / drdid_panel to ~1e-8;
analytical, multiplier-bootstrap, and event-study inference all compose with
covariates. reg and dr share the ATT(d) shape and ACRT(d), differing only in
the overall_att/ATT(d) level and the doubly-robust SE.

estimation_method="ipw" with covariates raises NotImplementedError (pure IPW's
covariate adjustment is a scalar level shift and cannot adjust the curve shape);
covariates + survey_design deferred. The no-covariate path is unchanged.

Validated: DRDID parity (reg/dr, skip-guarded), R-free NumPy influence-function
cross-check at p>=2 (in CI), MC coverage (reg 96%, dr 95%), DGP recovery, and
reg-vs-dr ACRT identity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Overall Assessment: ✅ Looks good

Executive Summary

  • No unmitigated P0/P1 findings in the PR diff.
  • Affected method: ContinuousDiD covariate-adjusted dose-response/event-study paths with estimation_method in {"reg", "dr"}.
  • Methodology deviations are documented in docs/methodology/REGISTRY.md Note Add comprehensive code review for diff-diff library #5 and matching TODO tracking, so they are informational under the review rules.
  • Inference paths use safe_inference(); I did not find new inline t-stat/p-value/CI anti-patterns.
  • I could not run tests locally because this environment lacks pytest and numpy.

Methodology

Finding: P3 informational — documented ContinuousDiD covariate-extension deviations
Impact: The PR extends beyond contdid v0.1.0, restricts ipw with covariates, rejects covariates= with survey_design=, and uses clipped propensity scores rather than DRDID drop trimming. These are all documented in docs/methodology/REGISTRY.md:L916-L925 and docs/methodology/REGISTRY.md:L959, with deferred work tracked in TODO.md:L31-L33.
Concrete fix: No action required. Keep these registry/TODO notes in sync with future implementation changes.

Finding: P3 informational — methodology propagation appears consistent
Impact: The covariate-adjusted control counterfactual is applied per (g,t) cell at diff_diff/continuous_did.py:L1281-L1299; reg/dr nuisance IFs are implemented at diff_diff/continuous_did.py:L993-L1191; analytical aggregation scatters the precomputed covariate IFs at diff_diff/continuous_did.py:L1588-L1603; multiplier bootstrap uses the same IFs at diff_diff/continuous_did.py:L1881-L1896. This matches the documented extension strategy.
Concrete fix: No action required.

Code Quality

Finding: P2 — covariates lacks explicit type validation
Impact: fit() converts self.covariates with list(self.covariates) at diff_diff/continuous_did.py:L290-L294. Passing a single string like covariates="x1" will be interpreted as ["x", "1"], producing a confusing missing-column error instead of either accepting "x1" as one covariate or raising a targeted type error.
Concrete fix: In _validate_constrained_params(), reject str/non-sequence covariates with a clear ValueError, or normalize a string to [covariates]; add a small API test for that behavior.

Performance

Finding: None
Impact: The new per-cell OLS/logit work is expected for covariate-adjusted (g,t) estimation, and no avoidable hot-path issue is evident from the diff.
Concrete fix: None.

Maintainability

Finding: P3 — optional fallback path would benefit from a direct regression test
Impact: pscore_fallback="unconditional" is documented and implemented at diff_diff/continuous_did.py:L1046-L1074, but the added tests cover metadata/defaults rather than forcing a propensity failure and asserting the fallback warning/reg-like result.
Concrete fix: Add a deterministic separated/rank-deficient propensity example that asserts default "error" raises and "unconditional" warns and returns finite reg-like output.

Tech Debt

Finding: P3 informational — deferred combinations are properly tracked
Impact: estimation_method="ipw" with covariates and covariates= plus survey_design= are explicitly rejected in code at diff_diff/continuous_did.py:L296-L313 and tracked in TODO.md:L31-L33.
Concrete fix: No action required for this PR.

Security

Finding: None
Impact: No secrets, unsafe file/network operations, or security-sensitive changes were visible in the diff.
Concrete fix: None.

Documentation/Tests

Finding: P3 — time-varying covariate/base-period behavior is documented but not directly tested
Impact: The docstring says covariates are read from each cell’s base period, and the implementation builds a period-indexed covariate cube at diff_diff/continuous_did.py:L937-L943. The new tests use time-invariant covariates, so they do not lock the intended base_period="varying"/"universal" behavior for time-varying X.
Concrete fix: Add one small panel with time-varying x1 where the expected adjusted counterfactual differs depending on the base-period slice.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 4, 2026
@igerber igerber merged commit a0a63c2 into main Jul 4, 2026
35 of 36 checks passed
@igerber igerber deleted the feature/continuous-did-covariates branch July 4, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant