Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Measuring campaign lift? Evaluating a product launch? Rolling out a policy in wa
- [Brand awareness survey tutorial](https://diff-diff.readthedocs.io/en/stable/tutorials/17_brand_awareness_survey.html) - full example with complex survey design, brand funnel analysis, and staggered rollouts
- Have BRFSS/ACS/CPS individual records? Use [`aggregate_survey()`](https://diff-diff.readthedocs.io/en/stable/api/prep.html) to roll respondent-level microdata into a geographic-period panel with inverse-variance precision weights for second-stage DiD

`BusinessReport` and `DiagnosticReport` are experimental preview classes that produce plain-English output and a structured `to_dict()` schema from any fitted result - wording and schema will evolve. See [docs/methodology/REPORTING.md](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REPORTING.md) for usage and stability notes.
`BusinessReport` and `DiagnosticReport` are experimental preview classes that produce plain-English output and a structured `to_dict()` schema from any fitted estimator result except the two rejected by type (`EventStudyResults` surfaces and `DurationDiDResults`) - wording and schema will evolve. See [docs/methodology/REPORTING.md](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REPORTING.md) for usage and stability notes.

## Practitioner Workflow (Baker et al. 2025)

Expand Down Expand Up @@ -122,6 +122,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
- [ChangesInChanges](https://diff-diff.readthedocs.io/en/stable/api/changes_in_changes.html) - Athey & Imbens (2006) nonlinear/distributional DiD for the 2x2 design: full counterfactual distribution and quantile treatment effects via CDF transformation, plus the QDiD comparison estimator via `method="qdid"`; bootstrap inference; R qte parity. Alias `CiC`
- [LWDiD](https://diff-diff.readthedocs.io/en/stable/api/lwdid.html) - Lee & Wooldridge (2025, 2026) rolling-transformation DiD: unit-specific demean/detrend converts panel to cross-section, staggered adoption, `estimation_method` in `reg`/`ipw`/`dr`/`psm` (the papers' RA/IPW/IPWRA plus propensity-score matching), exact small-N inference on the classical collapsed regression
- [DMLDiD](https://diff-diff.readthedocs.io/en/stable/api/dml_did.html) - Chang (2020) double/debiased machine learning DiD: staggered ATT(g,t) with cross-fitted ML nuisance learners (DML2) and Neyman-orthogonal scores, for flexible/high-dimensional covariate adjustment under conditional parallel trends; panel or declared repeated cross sections (`panel=False`); survey/cluster support on both lanes (bad-control lane: panel only, `cluster=` only); Caetano, Callaway, Payne & Sant'Anna (2026) bad-control score via `fit(bad_control=, bad_control_covariates=)`
- [DurationDiD](https://diff-diff.readthedocs.io/en/stable/api/duration_did.html) - Deaner & Ku (2026) causal duration DiD for a binary absorbing outcome (spell ended) in a two-group common-timing design: restricts the groups' untreated hazards (`method="cd"` additive gap or `method="ph"` ratio) instead of outcome levels, imputes the treated counterfactual survival, reports the per-date absorption ATT with whole-individual bootstrap pointwise and simultaneous bands plus a fixed-anchor pre-treatment specification test
- [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html) - Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings

## Diagnostics & Sensitivity
Expand Down
6 changes: 0 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,6 @@ Long-running program, framed as "building toward" rather than with discrete ship

Frontier methods that may graduate to Under Consideration given time and research signals.

### Causal Duration Analysis with DiD

Extends DiD to duration / survival outcomes where standard methods fail (hazard rates, time-to-event). Duration analogue of parallel trends; avoids distributional and hazard-function assumptions.

**Reference**: Deaner & Ku (2025), *AEA Conference Paper*.

### CATT Meta-Learner for Heterogeneous Effects

ML-powered conditional ATT, using a doubly robust meta-learner to discover which units benefit most from treatment.
Expand Down
3 changes: 3 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Related tracking surfaces:
| Remark-4-weighted overall ATT for the bad-control lane (`sum_g sum_{t>=g} [P(G=g given G in G_bar)/(T-g+1)] ATT(g,t)`: cohort mass among the ever-treated divided by the cohort's post-period count); the headline `att` is CS "simple" (REGISTRY DMLDiD Note). | `staggered_aggregation.py` | bad-controls PR-B | Mid | Low |
| Allow empty `Z` on the bad-control lane (the paper's `Z` may be empty; DMLDiD's covariates-required gate currently applies). | `dml_did.py` | bad-controls PR-B | Quick | Low |
| Recover the Supplementary Appendix DGPs 2 / 3 / 5 (nonlinear `W` / `X` terms) on the bad-control ML lane - needs a flexible-learner fixture via the duck-typed learner route (the parametric built-ins are inconsistent there by design, Table S1). | `tests/test_methodology_dml_did.py` | bad-controls PR-B | Mid | Low |
| DurationDiD alternative finite-sample PH estimators (the repaired cumulative-increment slope of Equation 3.5 and the average-hazard slope of Equation 3.6) as an opt-in beside the shipped mean-of-ratios coefficient; they coincide under exact PH and differ under sampling noise (review table, lines 306-311) | `duration_did.py` | #822 | Mid | Low |
| `DiagnosticReport` / `BusinessReport` admission of `DurationDiDResults`: both reject it by type today (their batteries are keyed to mean-outcome parallel-trends diagnostics); admission needs a hazard-restriction check family (the Algorithm 2 pretest, the invalid-curve statuses, per-family bootstrap failure counts) and a narrative for per-date absorption ATTs | `diagnostic_report.py`, `business_report.py` | #822 | Mid | Low |

### Performance

Expand Down Expand Up @@ -102,3 +104,4 @@ generic sparse-FE, QR+SVD rank-detection redundancy, `check_finite` bypass — m
| `trop-silent-drop` criteria regression: the rewritten `criteria.md` dropped a silent-failure catch the OLD criteria had (arm A caught it, B/C missed — campaign-1 A-vs-B contrast). Patch the criteria + re-validate (re-opens the criteria identity, so gated behind a re-validation run). | `.claude/skills/plan-review/criteria.md`, `tools/plan-review-eval/` | campaign-1 verdict | Mid | Medium |
| The Stata `jwdid` parity arms cover BALANCED panels only. `jwdid_alltreated` pins the all-eventually-treated cell set, `N` and SE ratio on the `mpdta` subset (191 units, 955 rows, 764 estimated), but every arm is a balanced frame, so REGISTRY's parity claim is scoped to that case. The comparison-support predicate's UNBALANCED behavior -- where `G_max` may be unobserved at later periods so the closed form `t < G_max - anticipation` does not hold and support is set by whichever cohorts are actually observed -- is verified against the predicate directly, not against Stata. Add an unbalanced `jwdid` arm to close that gap. | `benchmarks/stata/generate_etwfe_cs_golden.do`, `tests/test_etwfe_cs_stata_parity.py` | #729-followup | Mid | Low |
| Bad-controls tutorial section for `docs/tutorials/32_dml_did.ipynb` (PR-C): the `fit(bad_control=)` workflow, the `ATT_X(g,t)` pre-test reading, and the Approach-1 comparison via CS base-period covariates. | `docs/tutorials/32_dml_did.ipynb` | bad-controls PR-B | Quick | Medium |
| DurationDiD tutorial notebook (Appendix C-style simulation showing the sign reversal of standard DiD on an absorbing outcome, CD vs PH, the fitting window, and the pretest), registered in `docs/tutorials/index.rst` with a drift test; deferred from the estimator PR, which relies on the executed API-page examples | `docs/tutorials/` | #822 | Mid | Low |
35 changes: 35 additions & 0 deletions changelog.d/20260906-duration-did.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
### Added
- **DurationDiD estimator** (Deaner & Ku 2026, *Causal Duration Analysis with
Diff-in-Diff*, arXiv:2405.05220v2): two-group, common-timing difference-in-
differences for a binary absorbing outcome. `method="cd"` (constant additive
gap between the groups' untreated hazards) or `method="ph"` (constant hazard
ratio, mean-of-ratios estimator) is fitted on the pre-treatment cumulative
hazards — by default with equal weights over every eligible pre-treatment
date, or on a user window via `fit(pre_periods=..., pre_period_weights=...)`
— with exact numeric date selection and finite real fitting weights.
Integer dates preserve their identity across supported signed/unsigned
dtype ranges: spacings and offsets are subtracted before float64 elapsed
arithmetic, which must remain finite and strictly increasing. Floating
dates must be losslessly representable as float64; complex inputs are
rejected. Result labels, JSON and practitioner guidance retain the same
dates. The treated group's counterfactual survival is imputed from the
control group (Theorem 1). Reports the absorption ATT at every
post-treatment date plus its uniform average as `att`, with the paper's
whole-individual pooled bootstrap (Appendix B Algorithm 1: centered
absolute-deviation pointwise intervals and a simultaneous max-|t| band) and
the Algorithm 2 fixed-anchor pre-treatment specification test
(`results.pretest`, a `DurationDiDPretestResults` diagnostic). Every
inference family is either fully available or fully withheld with a named
`inference_status` (invalid imputed counterfactual curve, failed bootstrap
draws, zero SE); failed draws are never retried or silently dropped.
Unused categorical unit levels are excluded from panel balance checks.
`results.aggregate("event_study")` returns the unified `EventStudyResults`
container (event time 0 = first post-treatment date, reference -1).
Covariates, staggered adoption, censoring, survey and cluster inference are
deferred. `DiagnosticReport` and `BusinessReport` reject `DurationDiDResults`
by type (their batteries are keyed to mean-outcome parallel-trends
diagnostics; admission is tracked in `TODO.md`); `practitioner_next_steps`
gains a DurationDiD handler with a hazard-restriction assumptions step.
A tutorial notebook is deferred (tracked in `TODO.md`), a documented
deviation from the new-estimator documentation checklist; the executed
examples on the API page are the hands-on reference.
6 changes: 6 additions & 0 deletions diff_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@
)
from diff_diff.dml_did import DMLDiD
from diff_diff.dml_did_results import DMLDiDResults
from diff_diff.duration_did import DurationDiD
from diff_diff.duration_did_results import DurationDiDPretestResults, DurationDiDResults
from diff_diff.efficient_did import (
EDiDBootstrapResults,
EfficientDiD,
Expand Down Expand Up @@ -471,6 +473,10 @@ def __getattr__(name: str) -> _Any:
"DMLDiD",
"DMLDiDResults",
"SieveLearner",
# DurationDiD (Deaner & Ku 2026 causal duration DiD)
"DurationDiD",
"DurationDiDResults",
"DurationDiDPretestResults",
# Visualization
"plot_bacon",
"plot_event_study",
Expand Down
30 changes: 27 additions & 3 deletions diff_diff/business_report.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""
BusinessReport — plain-English stakeholder narrative from any diff-diff result.
BusinessReport — plain-English stakeholder narrative from a diff-diff result.

Wraps any of the 16 fitted result types and produces:
Wraps the fitted result types it dispatches on (every scalar estimator
result except the two rejected by type: ``EventStudyResults`` surfaces and
``DurationDiDResults`` — see the constructor's rejection branches) and
produces:

- ``summary()``: a short paragraph block suitable for an email or Slack message.
- ``full_report()``: a multi-section markdown report with headline, assumptions,
Expand Down Expand Up @@ -92,7 +95,11 @@ class BusinessContext:


class BusinessReport:
"""Produce a stakeholder-ready narrative from any diff-diff results object.
"""Produce a stakeholder-ready narrative from a diff-diff results object.

Accepts every scalar estimator result except the two rejected by type:
``EventStudyResults`` surfaces and ``DurationDiDResults`` (see the
constructor's rejection branches).

Parameters
----------
Expand Down Expand Up @@ -203,6 +210,23 @@ def __init__(
"with HonestDiD, PreTrendsPower, or plot_event_study. "
"EventStudyResults admission is tracked in TODO.md."
)
# DurationDiDResults (Deaner & Ku 2026) is rejected BY TYPE: the
# narrative's parallel-trends framing does not describe a
# duration-DiD fit (untreated-hazard restriction on an absorbing
# outcome) and its headline is a uniform average of per-date
# absorption ATTs with a separate hazard pretest. Admission is
# tracked in TODO.md.
from diff_diff.duration_did_results import DurationDiDResults as _DDR

if isinstance(results, _DDR):
raise TypeError(
"BusinessReport does not support DurationDiDResults: the "
"narrative is keyed to mean-outcome parallel-trends estimators, "
"while DurationDiD identifies off an untreated-hazard "
"restriction on an absorbing outcome. Use results.summary(), "
"results.pretest, and results.aggregate('event_study') instead; "
"DurationDiD admission is tracked in TODO.md."
)
# Marked diagnostic results are rejected BY TYPE (spec section
# 3.5, ledger row M-091): BusinessReport's primary input is a
# fitted ESTIMATOR result carrying the canonical inference row.
Expand Down
16 changes: 16 additions & 0 deletions diff_diff/diagnostic_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,22 @@ def __init__(
"the producing estimator). EventStudyResults admission is "
"tracked in TODO.md."
)
# DurationDiDResults (Deaner & Ku 2026) is rejected BY TYPE: the
# battery is keyed to mean-outcome parallel-trends diagnostics, while
# duration DiD identifies off a restriction on the untreated hazards
# of a binary absorbing outcome, so an empty applicability set would
# again produce a zero-check report. Admission is tracked in TODO.md.
from diff_diff.duration_did_results import DurationDiDResults as _DDR

if isinstance(results, _DDR):
raise TypeError(
"DiagnosticReport does not support DurationDiDResults: its "
"checks are keyed to mean-outcome parallel-trends diagnostics, "
"while DurationDiD identifies off an untreated-hazard "
"restriction on an absorbing outcome. Use results.pretest (the "
"Algorithm 2 hazard-restriction test) and results.summary(); "
"DurationDiD admission is tracked in TODO.md."
)
# Marked diagnostic results (spec section 3.5, ledger row M-091)
# are rejected BY TYPE — except Bacon, whose dedicated read-out
# is retained. Before the marker, such inputs silently produced
Expand Down
Loading
Loading