Skip to content

feat(duration-did): add DurationDiD estimator (Deaner & Ku 2026 causal duration DiD) - #827

Merged
igerber merged 6 commits into
mainfrom
codex/eval-estimator-8523086f
Sep 7, 2026
Merged

feat(duration-did): add DurationDiD estimator (Deaner & Ku 2026 causal duration DiD)#827
igerber merged 6 commits into
mainfrom
codex/eval-estimator-8523086f

Conversation

@igerber

@igerber igerber commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Summary

Adds DurationDiD for the Deaner & Ku (2026) two-group, common-timing design with a binary absorbing outcome, such as a spell that ends and stays ended. The methodology foundation is docs/methodology/papers/deaner-ku-2026-review.md (#822).

  • method="cd" fits a constant additive gap between untreated hazards; method="ph" fits a constant ratio. The estimator imputes treated counterfactual survival and reports absorption ATT by post-treatment date, with the uniform post-date average as att.
  • fit(pre_periods=..., pre_period_weights=...) selects fitting dates and finite, real, nonnegative weights. The default uses equal weights over eligible pre-treatment dates after the baseline. Configuration is validated again at fit start, including direct attribute mutations.
  • Whole-individual pooled bootstrap inference provides centered pointwise intervals, simultaneous bands and covariance. results.pretest implements the separate fixed-anchor pre-treatment specification test. Invalid counterfactual curves, failed draws or unusable standard errors (including exactly constant bootstrap statistics) withhold the affected inference family and report its status.
  • Categorical unit identifiers ignore unused levels when checking balance, preserving filtered panels across supported pandas versions.
  • Numeric dates retain exact identity across supported integer column dtypes. Spacing checks and elapsed-time subtraction precede float64 inference arithmetic; selectors cannot alias neighboring dates. Complex inputs are rejected before coercion, and result serialization, warnings and practitioner guidance preserve date labels.
  • Integrates with BaseEstimator, BaseResults, summary(), to_dict(), to_dataframe() and aggregate("event_study"). Practitioner guidance explains the hazard restriction and provides an anticipation placebo with an exact preceding anchor.

Methodology references (required if estimator / math changes)

  • Deaner, B., & Ku, H. (2026), Causal Duration Analysis with Diff-in-Diff, arXiv:2405.05220v2: Theorem 1, Equations 3.1–3.4, Appendix B Algorithms 1–2.
  • Implementation choices are recorded as Notes in the DurationDiD section of docs/methodology/REGISTRY.md: the mean-of-ratios PH coefficient supported by Theorem 1 and the authors' code; equal default fitting weights with optional dates/weights; the fixed-anchor pretest; whole-path curve validity; complete-draw failure policies; empirical centered-bootstrap band decisions; and the equally spaced numeric-clock restriction. The printed Equation 3.5 gives the reciprocal under exact PH, so it is not used as written.

Validation

  • Full default suite with the Rust backend: 14,475 passed, 227 skipped, 257 deselected (slow).
  • Both DurationDiD suites, including the slow coverage test: 292 passed, 3 skipped under each of the Python and Rust backends. The three skipped cases require wider longdouble precision than this macOS platform provides; representable-longdouble serialization and guidance tests ran on both backends.
  • The complete DurationDiD suites also pass on Python 3.12 with pandas 2.2.3: 292 passed, 3 skipped. Categorical-unit regressions reproduce the older pandas default failure before the fix, preserve full seeded inference after the fix, and still reject genuinely missing cells.
  • Documentation, guide and tracking contracts: 621 passed, 7 skipped. Sphinx HTML build with warnings treated as errors passed; both standalone API examples executed.
  • Ruff, Black, configured Python 3.10-target mypy (110 source files, pinned typing dependencies), changelog validation and the pre-merge pattern scan passed.

Tests cover paper identities, hand-calculated CD/PH examples, seeded bootstrap covariance and bands, constant-draw withholding with independent family gates, preservation of small nonzero variation, Monte Carlo coverage, parameter mutation, input validation, full-inference invariance under large date translations, exact selectors, tolerance boundaries, JSON, warnings, practitioner code and reporting contracts. The coverage test uses 200 simulated datasets on both backends with the same seeds and acceptance tolerance.

The original five-grid reproduction accepts all three valid grids with identical ATT and SE and rejects both irregular grids. Both self-contained API examples execute successfully. The documentation build also verifies literal statistical notation and unambiguous parameter descriptions.

Security / privacy

Synthetic test data only; no secrets or personal data added.

Changelog

changelog.d/20260906-duration-did.md

Limitations

This initial API requires a balanced panel, common treatment timing, real binary absorbing outcomes and an equally spaced real numeric clock. Covariates, staggered adoption, censoring, survey weights, cluster dependence and repeated cross-sections are outside its scope. Floating dates must be losslessly representable as float64, and elapsed offsets must remain finite and strictly increasing; precision already lost by callers cannot be reconstructed.

DiagnosticReport and BusinessReport admission and a tutorial notebook are tracked in TODO.md; the API page provides executable examples. Numerical validation uses the paper, hand calculations and synthetic DGPs; reference-software numerical parity remains unverified.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Overall Assessment

Blocker — one unmitigated P0 finding.

Executive Summary

  • CD/PH equations, pooled bootstrap, pretest, and NaN gating otherwise match the Registry.
  • P0: fit() does not revalidate mutable estimator parameters.
  • An invalid mutated method silently falls through to PH estimation.
  • Documented deviations and deferred work are correctly tracked.
  • Changelog and broader test coverage are present.

Methodology

P0 — Invalid mutated configuration can silently select PH

  • Location: diff_diff/duration_did.py:L359-L368, diff_diff/duration_did.py:L922-L923
  • Impact: After construction, est.method = "typo" bypasses validation. The numerical core treats every non-"cd" value as PH, potentially returning plausible but mislabeled statistical estimates. Mutated invalid alpha or n_bootstrap values can likewise produce invalid inference or truncation.
  • Concrete fix: Revalidate all constructor parameters at the very start of fit() using _validate_method, _validate_draws, _validate_alpha, and _validate_seed, before reading self.method or validating data.

P3 — Documented methodology choices

  • Impact: Mean-of-ratios PH, fitting weights, fixed-anchor pretest, curve gating, and fixed-draw failure policies intentionally differ from source/software details.
  • Concrete fix: None; each is labeled as a Registry Note and is therefore informational.

Code Quality

No additional findings. Final inference correctly uses safe_inference() and safe_inference_batch().

Performance

No material findings.

Maintainability

No additional findings beyond the missing centralized fit-time configuration revalidation.

Tech Debt

P3 — Tracked limitations

  • Impact: Alternative PH estimators and report-layer admission remain deferred.
  • Concrete fix: None required for this PR; both are tracked in TODO.md.

Security

No findings.

Documentation/Tests

P0 — Missing regression coverage for the blocker

  • Impact: Constructor and set_params() validation tests do not exercise direct attribute mutation before fit().
  • Concrete fix: Add parameterized tests mutating method, alpha, n_bootstrap, and seed, asserting fit() raises before processing data.

P3 — Tutorial deferred

  • Impact: No tutorial notebook ships with the estimator.
  • Concrete fix: None required; tracked in TODO.md. A changelog fragment and executed API examples are present.

Path to Approval

  1. Add fit-start revalidation for every constructor parameter and tests proving invalid direct mutations—including method="typo"—raise instead of entering an estimation branch.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 8fca5c1ebf29e58c272f53a6e75377820ae97166


Overall Assessment

Looks good — no unmitigated P0 or P1 findings.

Executive Summary

  • Prior P0 is resolved: all mutable parameters are revalidated before data access.
  • CD/PH equations, ATT construction, bootstrap, pretest, and NaN gating match the Registry.
  • Intentional methodology deviations are explicitly documented.
  • Deferred items are tracked in TODO.md.
  • Changelog fragment and focused regression tests are present.

Methodology

Resolved — Prior configuration-validation blocker

  • Impact: Invalid direct mutations can no longer silently select PH or corrupt inference.
  • Evidence: diff_diff/duration_did.py:L117-L127, diff_diff/duration_did.py:L936-L940; regression coverage at tests/test_duration_did.py:L156-L186.
  • Concrete fix: None; resolved.

P3 — Documented methodology choices

  • Impact: Mean-of-ratios PH, fitting weights, fixed-anchor pretest, curve gating, and fixed-draw failure policies deliberately differ from source/software alternatives.
  • Evidence: docs/methodology/REGISTRY.md:L5732-L5799.
  • Concrete fix: None required; these are labeled Registry Notes.

Code Quality

No findings. Inference uses the shared safe-inference gates and invalid methods fail closed.

Performance

No material findings.

Maintainability

No findings. Parameter validation and numerical routines are centralized.

Tech Debt

P3 — Tracked limitations

  • Impact: Alternative PH estimators, report-layer admission, and a tutorial remain deferred.
  • Evidence: TODO.md:L60-L61, TODO.md:L100.
  • Concrete fix: None required for this PR.

Security

No findings or apparent secrets.

Documentation/Tests

No findings. The user-visible feature has changelog.d/20260906-duration-did.md, and tests cover methodology identities, mutation validation, bootstrap failures, curve validity, and joint-NaN inference.

Review-environment note: runtime tests could not be executed because pytest and NumPy are not installed; static syntax parsing succeeded.

@igerber
igerber force-pushed the codex/eval-estimator-8523086f branch from 8fca5c1 to d7f5c7f Compare September 7, 2026 18:00
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: d7f5c7fe381cbd36cdc40461af085e7edb631491


Overall Assessment

⚠️ Needs changes — one newly identified P1 edge-case failure.

Executive Summary

  • Prior configuration-validation P0 is resolved.
  • Estimator equations, bootstrap, pretest, and NaN gates match the Registry.
  • One valid categorical-unit input can be falsely rejected as unbalanced.
  • Changelog, documentation, and tracked deferrals are present.

Methodology

P3 — Documented implementation choices

  • Impact: Mean-of-ratios PH, equal default weights, fixed-anchor pretest, and conservative bootstrap failure handling intentionally differ from alternatives.
  • Evidence: docs/methodology/REGISTRY.md:L5850-L5966, docs/methodology/REGISTRY.md:L5980-L6015.
  • Concrete fix: None required; these are explicitly labeled Registry Notes.

Code Quality

P1 [Newly identified] — Unused categorical unit levels trigger a false balance error

  • Impact: On supported pandas versions where groupby(observed=False) is the default, filtering a panel can leave unused categories in a categorical unit column. The unused identifiers receive count zero and the valid balanced panel is rejected as unbalanced.
  • Evidence: diff_diff/duration_did.py:L212-L227.
  • Concrete fix: Use data.groupby(unit, sort=True, observed=True)[time].size() and add a regression test with both groups present plus an unused unit category.

The prior P0 is resolved by fit-start validation at diff_diff/duration_did.py:L998-L1002, covered at tests/test_duration_did.py:L156-L186.

Performance

No findings.

Maintainability

No findings.

Tech Debt

P3 — Tracked limitations

  • Impact: Alternative PH estimators, report admission, and a tutorial remain deferred.
  • Evidence: TODO.md:L66-L67, TODO.md:L107.
  • Concrete fix: None required; properly tracked.

Security

No findings or apparent secrets.

Documentation/Tests

No additional findings. The feature has changelog.d/20260906-duration-did.md. Runtime tests could not be executed because NumPy and pytest are unavailable; static parsing passed.

Path to Approval

  1. Add observed=True to the unit-count groupby.
  2. Add a balanced-panel regression test using a categorical unit column with an unused category.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 80c86d9a277979d6a971fe57f7c57421b36fda7a


Overall Assessment

Looks good — no unmitigated P0 or P1 findings.

Executive Summary

  • The prior categorical-unit P1 is resolved with observed=True and regression coverage.
  • CD/PH equations, bootstrap inference, pretest, and NaN gates match the Methodology Registry.
  • Numerical and finite-sample choices that differ from alternatives are explicitly documented.
  • Changelog, API documentation, methodology tests, and tracked deferrals are present.
  • Runtime tests could not run because NumPy/pytest are unavailable; static parsing passed.

Methodology

P3 — Documented implementation choices

  • Impact: Mean-of-ratios PH estimation, equal default fitting weights, fixed-anchor pretest, whole-path validity checks, and conservative bootstrap failure handling differ from possible alternatives but are intentional.
  • Evidence: diff_diff/duration_did.py:L380-L417, diff_diff/duration_did.py:L630-L709, diff_diff/duration_did.py:L725-L856, docs/methodology/REGISTRY.md:L5980-L6048.
  • Concrete fix: None required; these are explicitly labeled Registry Notes.

Code Quality

No findings. The previous false balance error is fixed at diff_diff/duration_did.py:L217, with categorical and missing-cell regressions at tests/test_duration_did.py:L242-L275.

Performance

No findings.

Maintainability

No findings.

Tech Debt

P3 — Deferred work is tracked

  • Impact: Alternative PH estimators, report admission, and a tutorial remain future work.
  • Evidence: TODO.md:L66-L67, TODO.md:L107.
  • Concrete fix: None required; properly tracked.

Security

No findings or apparent secrets.

Documentation/Tests

No findings. The user-visible feature has changelog.d/20260906-duration-did.md, API documentation, and extensive methodology/edge-case tests. Seven affected Python files passed static AST parsing. Runtime execution was unavailable because the environment lacks NumPy and pytest.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Sep 7, 2026
@igerber
igerber merged commit e54a9c0 into main Sep 7, 2026
39 of 40 checks passed
@igerber
igerber deleted the codex/eval-estimator-8523086f branch September 7, 2026 20:38
@igerber igerber mentioned this pull request Sep 8, 2026
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