feat(continuous-did): discrete-treatment saturated regression (treatment_type="discrete")#618
Conversation
…ent_type="discrete")
Adds treatment_type="discrete" to ContinuousDiD for multi-valued / discrete
dose (CGBS 2024 Eq. 4.1). Each distinct dose level gets its own effect
coefficient - ATT(d_j) = mean_{D=d_j}(dY) - control, a per-level 2x2 DiD -
instead of a smoothed B-spline curve; ACRT(d_j) is a finite difference
(forward at the lowest level, backward elsewhere) so ATT/ACRT share the
J-point grid and ACRT^glob stays well-defined.
Implemented as an exact basis swap: the B-spline design/evaluation/derivative
trio is replaced by an indicator/identity/finite-difference trio, and every
downstream quantity is linear in beta, so the analytical-SE, multiplier-
bootstrap, covariate (reg/dr), and survey machinery is reused unchanged and
reduces analytically to the per-level 2x2 DiD standard error
(bread @ psi_bar = ones(J); the common control mean cancels in ACRT via
L.1 = 0). reg/dr share ACRT(d_j) point+SE on the intercept-free basis
(partition-of-unity residual-invariance + L.1 = 0); only the ATT level differs.
Fail-closed edges: multi-cohort heterogeneous dose support raises
NotImplementedError (support-aware aggregation deferred); an off-support dvals
value raises ValueError; a survey-zeroed dose level raises ValueError. The
default treatment_type="continuous" (B-spline) path is numerically unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi
…il-closed) Local review found a P1 blind spot: the fit-time survey-weight guard only checks global positive dose-level support. In a shared-support multi-cohort discrete fit, survey/subpopulation weights can zero out a dose level for one cohort while another keeps it positive — the global guard passes, but that cohort's (g,t) cell then has an all-zero saturated column that solve_ols drops and beta_pred zeroes, silently biasing ATT(d)/ACRT(d) aggregation. Add a per-cell support check in _compute_dose_response_gt: every dose level must have positive effective treated mass in the cell, else raise ValueError before OLS. Covers n_bootstrap>0 too (runs during the initial cell pass). Adds a two-cohort survey test for the cell-level zero-support case and documents the policy in REGISTRY Note #6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi
… n_bootstrap Lock the per-(g,t)-cell zero-support ValueError for both the analytical (n_bootstrap=0) and bootstrap (n_bootstrap>0) survey paths (the guard runs in the initial cell pass, before inference). Closes the optional test-hardening item from local review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi
Overall AssessmentExecutive Summary
MethodologyFinding 1 — P1: Single positive dose level reports
|
…ixes CI P1)
CI review flagged that a single positive dose (J=1) returned ACRT=0, which
contradicts the documented binary identity ACRT=ATT and the paper's Eq. 4.1
d_0=0 discrete setup. The prior forward-difference at the lowest dose was a
deviation; switch to the paper's backward difference on the grid {0, d_1, ...,
d_J} (d_0=0, ATT(0)=0), so ACRT(d_1) = ATT(d_1)/d_1 and a binary D in {0,1}
gives ACRT=ATT. Only the lowest dose's row changes; the j>=2 adjacent backward
differences are unchanged.
Consequence (correct): reg/dr share ACRT(d_j) for j>=2 (the constant DR
augmentation cancels in adjacent differences) but differ at ACRT(d_1) by
eta_cont/d_1 -- d_1 references the fixed baseline ATT(0)=0, which the
augmentation does not shift. The dr influence function now carries the
augmentation variance at d_1 (analytical ACRT(d_1) SE matches the multiplier
bootstrap); applied only on the discrete path, so the validated B-spline
covariate IF is byte-identical. The all-same-dose "ACRT will be 0" warning is
suppressed on the discrete path (single-dose ACRT = ATT(d_1)/d_1 != 0).
Updates REGISTRY Note #5/#6 + Key Equations, continuous-did.md 5.1, CHANGELOG,
llms-full.txt, docstring; adds binary-single-dose and dr-ACRT-analytical-vs-
bootstrap tests; updates the ACRT-boundary / reg-dr / DGP-recovery tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment✅ Looks good — no unmitigated P0/P1 findings. Executive Summary
MethodologyP3 — Informational: documented discrete-path restrictionsLocation: Impact: Multi-cohort heterogeneous dose support raises Concrete fix: None required. Code QualityNo findings. The basis dispatch is localized in PerformanceNo findings. The saturated basis is an indicator matrix over observed dose levels; no concerning new asymptotic cost is introduced for the intended few-level discrete-dose case. MaintainabilityNo findings. Tech DebtP3 — Informational: heterogeneous-support aggregation deferred and trackedLocation: Impact: Support-aware aggregation for cohorts with different observed dose levels is deferred, but the implementation fails closed rather than silently biasing absent levels toward zero. Concrete fix: None required for this PR. SecurityNo findings. The diff does not add risky I/O, credential handling, or secret material. Documentation/TestsNo blocking findings. The prior missing binary/single-dose test is now covered at Verification note: targeted pytest execution was not possible because |
Summary
treatment_type="discrete"toContinuousDiD— a saturated regression for multi-valued / discrete dose (CGBS 2024 Eq. 4.1). Each distinct dose level gets its own effect coefficient (ATT(d_j) = mean_{D=d_j}(ΔY) − control, a per-level 2×2 DiD) instead of a smoothed B-spline curve;ACRT(d_j)is a finite difference (forward at the lowest level, backward elsewhere) so ATT/ACRT share the J-point grid andACRT^globstays well-defined.beta, so the analytical-SE, multiplier-bootstrap, covariate (reg/dr), and survey machinery is reused unchanged and reduces analytically to the per-level 2×2 DiD standard error (bread @ psi_bar = ones(J); the common control mean cancels in ACRT viaL·1 = 0).reg/drshareACRT(d_j)point+SE on the intercept-free basis; only the ATT level differs.NotImplementedError(support-aware aggregation deferred; single-cohort / 2-period / shared-support multi-cohort supported); off-supportdvals→ValueError; a survey/subpopulation-zeroed dose level →ValueError, checked per(g,t)cell (not just globally) so a level zeroed for one cohort cannot silently drop to a zero-coefficient column.treatment_type="continuous"(B-spline) path is numerically unchanged.Methodology references
treatment_type="discrete")docs/methodology/REGISTRY.md§ ContinuousDiD (Note Add multi-period DiD support #6, Key Equations) anddocs/methodology/continuous-did.md§5.1.contdidv0.1.0 acceptstreatment_typein its signature but does not implement the discrete path, so there is no external R anchor. Validation is R-free: exact hand-calc ofATT(d_j)/ACRT/overall_attand the analytical SE vs a direct per-level 2×2 reconstruction, DGP recovery, and MC coverage (analytical + bootstrap). The forward-difference ACRT convention at the lowest dose is a documented boundary choice (no paper/R anchor there), recorded in REGISTRY Note Add multi-period DiD support #6.Validation
tests/test_methodology_continuous_did.py(TestDiscreteSaturated: hand-calc ATT/ACRT/overall + analytical SE, ACRT boundary, DGP recovery, staggered shared-support, MC coverage @slow) andtests/test_continuous_did.py(TestDiscreteSaturatedAPI: dr×discrete ACRT identity, survey×discrete weighted means, exactly-identified boundary, heterogeneous-support/off-support/survey-zero-level/per-cell guards, params round-trip, continuous default unchanged).ATT(d_j)andACRT(d_j)under a discrete-dose DGP.Security / privacy
🤖 Generated with Claude Code
https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi