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
1 change: 0 additions & 1 deletion .claude/commands/docs-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ The following estimators/features MUST have documentation:
|-----------|---------------|----------------|---------|-------------------|
| DifferenceInDifferences | "DifferenceInDifferences" | "DifferenceInDifferences" | estimators.rst | "Difference-in-Differences" |
| TwoWayFixedEffects | "TwoWayFixedEffects" | "TwoWayFixedEffects" | estimators.rst | "Two-Way Fixed Effects" |
| MultiPeriodDiD | "MultiPeriodDiD" | "MultiPeriodDiD" | estimators.rst | "Multi-Period and Staggered" |
| SyntheticDiD | "SyntheticDiD" | "SyntheticDiD" | estimators.rst | "Synthetic Difference-in-Differences" |
| CallawaySantAnna | "CallawaySantAnna" | "CallawaySantAnna" | staggered.rst | "Multi-Period and Staggered" |
| SunAbraham | "SunAbraham" | "SunAbraham" | staggered.rst | "Multi-Period and Staggered" |
Expand Down
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,10 @@ pytest tests/test_rust_backend.py -v
BaseEstimator (diff_diff/_base.py — shared get_params/set_params mixin)
├── DifferenceInDifferences
│ ├── TwoWayFixedEffects
│ ├── MultiPeriodDiD
│ └── SyntheticDiD
└── every other estimator class (CallawaySantAnna, SunAbraham,
ImputationDiD, TwoStageDiD, TripleDifference, TROP, StackedDiD,
BaconDecomposition, ... — 25 classes total)
BaconDecomposition, ... — 28 classes total)
```
`get_params` introspects the `__init__` signature and `set_params` is
TRANSACTIONAL via probe re-init (`type(self)(**merged)` validates before
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ For rigorous DiD analysis, follow these 8 steps. Skipping diagnostic steps produ
3. **Test parallel trends** - simple 2x2: `check_parallel_trends()`, `equivalence_test_trends()`; staggered: inspect CS event-study pre-period coefficients (generic PT tests are invalid for staggered designs). Insignificant pre-trends do NOT prove PT holds.
4. **Choose estimator** - staggered adoption -> CS/SA/BJS (NOT plain TWFE); few treated units -> SDiD; factor confounding -> TROP; simple 2x2 -> DiD. Run `BaconDecomposition` to diagnose TWFE bias.
5. **Estimate** - `estimator.fit(data, ...)`. Always print the cluster count first and choose inference method based on the result (cluster-robust if >= 50 clusters, wild bootstrap if fewer - for DifferenceInDifferences pass `cluster=`; TwoWayFixedEffects auto-clusters at unit level).
6. **Sensitivity analysis** - `compute_honest_did(results)` for bounds under PT violations (MultiPeriodDiD, CS, or dCDH natively; the TwoWayFixedEffects `event_study=True` surface and a StackedDiD `results.aggregate('event_study')` container also admit - Stacked needs `kappa_pre >= 2`), `run_all_placebo_tests()` for 2x2 falsification, specification comparisons for staggered designs.
6. **Sensitivity analysis** - `compute_honest_did(results)` for bounds under PT violations (the TwoWayFixedEffects `event_study=True` surface, CS, or dCDH natively; a StackedDiD `results.aggregate('event_study')` container also admits - Stacked needs `kappa_pre >= 2`), `run_all_placebo_tests()` for 2x2 falsification, specification comparisons for staggered designs.
7. **Heterogeneity** - CS: `results.aggregate('group')`/`'event_study'` (post-fit, no refit); SA: `results.event_study_effects` / `to_dataframe(level='cohort')`; Stacked: `results.aggregate('event_study')`/`'simple'` post-fit views (surface always computed since 3.9); EDiD: `results.aggregate(...)` post-fit from retained EIFs (3.9); ImputationDiD/TwoStageDiD: `results.aggregate(...)` post-fit from panel-backed kits (3.9); ContinuousDiD: `results.aggregate('dose'/'simple'/'event_study')` post-fit (3.9; dose/simple are views, event_study recomputes); subgroup re-estimation.
8. **Robustness** - compare 2-3 estimators (CS vs SA vs BJS), report with and without covariates (shows whether conditioning drives identification), present pre-trends and sensitivity bounds.

Expand All @@ -99,8 +99,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
## Estimators

- [DifferenceInDifferences](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - basic 2x2 DiD with robust/cluster-robust SEs, wild bootstrap, formula interface, and fixed effects
- [TwoWayFixedEffects](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - panel data DiD with unit and time fixed effects via within-transformation or dummies
- [MultiPeriodDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - event study design with period-specific treatment effects for dynamic analysis (deprecated 3.9 - use TwoWayFixedEffects `event_study=True`)
- [TwoWayFixedEffects](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - panel data DiD with unit and time fixed effects via within-transformation or dummies; event-study mode (`event_study=True`) for period-specific treatment effects
- [CallawaySantAnna](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Callaway & Sant'Anna (2021) group-time ATT estimator for staggered adoption
- [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The most general option for treatments that switch on AND off (see also `LPDiD`/`TROP` `non_absorbing`). Alias `DCDH`.
- [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
Expand Down Expand Up @@ -136,7 +135,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
- [Pre-Trends Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/pretrends.html) - Roth (2022) minimum detectable violation and power curves
- [Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/power.html) - analytical and simulation-based MDE, sample size, power curves for study design
- [MMM Calibration Export](https://diff-diff.readthedocs.io/en/stable/api/mmm.html) - convert experiment results into MMM calibration inputs: PyMC-Marketing lift-test frames and Google Meridian lognormal ROI priors
- Conley spatial HAC SE (`vcov_type="conley"`) on cross-sectional `LinearRegression` / `compute_robust_vcov` plus panel `DifferenceInDifferences` / `MultiPeriodDiD` / `TwoWayFixedEffects` (with `conley_lag_cutoff` for within-unit Bartlett temporal HAC) - Conley (1999) spatial-correlation-aware SEs with parity vs R `conleyreg` on cross-sectional + panel fixtures, optional combined spatial + cluster product kernel via explicit `cluster=`, auto-activating sparse k-d-tree fast path for `n > 5_000`
- Conley spatial HAC SE (`vcov_type="conley"`) on cross-sectional `LinearRegression` / `compute_robust_vcov` plus panel `DifferenceInDifferences` / `TwoWayFixedEffects` (with `conley_lag_cutoff` for within-unit Bartlett temporal HAC) - Conley (1999) spatial-correlation-aware SEs with parity vs R `conleyreg` on cross-sectional + panel fixtures, optional combined spatial + cluster product kernel via explicit `cluster=`, auto-activating sparse k-d-tree fast path for `n > 5_000`

## Survey Support

Expand Down
4 changes: 3 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ generic sparse-FE, QR+SVD rank-detection redundancy, `check_finite` bypass — m

| Issue | Location | Origin | Effort | Priority |
|-------|----------|--------|--------|----------|
| TWFE event-study one-way HC2-BM absorbed-FE R parity against the `mpd_absorbed_fe_did` golden (`clubSandwich::vcovCR(cluster=1:n, type="CR2")`): the within spec with hc2_bm auto-clusters at unit, so the one-way lane needs an explicit singleton-cluster column (`cluster=<row id>` + hc2_bm = CR2 with G=n, PT2018 §3.3) once the G=n CR2 path is verified; the hc2 lane is already pinned by `TestTWFEEventStudyAbsorbedFERParity`, and the MultiPeriodDiD lanes die at the 4.0 cut (PR 4c-ii) | `tests/test_estimators_vcov_type.py` | #v4-4c-i | Mid | Medium |
| TWFE event-study one-way HC2-BM absorbed-FE R parity against the `mpd_absorbed_fe_did` golden (`clubSandwich::vcovCR(cluster=1:n, type="CR2")`): the within spec with hc2_bm auto-clusters at unit, so the one-way lane needs an explicit singleton-cluster column (`cluster=<row id>` + hc2_bm = CR2 with G=n, PT2018 §3.3) once the G=n CR2 path is verified; the hc2 lane is pinned by `TestTWFEEventStudyAbsorbedFERParity` and the clustered hc2_bm per-period lane by `test_twfe_event_study_cluster_hc2_bm_per_period_df_matches_clubsandwich`; the MultiPeriodDiD lanes died at the 4.0 cut (PR 4c-ii) | `tests/test_estimators_vcov_type.py` | #v4-4c-i | Mid | Medium |
| TWFE event-study power profile: `simulate_power`/`simulate_mde`/`simulate_sample_size` have no `_EstimatorProfile` for `TwoWayFixedEffects.fit(event_study=True)` since the MultiPeriodDiD registry entry went with the class (PR 4c-ii); `EventStudyResults` carries no scalar headline, so the profile needs a design (post-period average via the `tests/helpers/event_study_average.py` arithmetic, or a named period) plus its `_SURVEY_FIT_BUILDERS` twin | `diff_diff/power.py` | #v4-4c-ii | Mid | Low |
| Clustered hc2_bm Bell-McCaffrey DOF guard is batch-relative (`linalg._cr2_bm_dof_inner`: a contrast is NaN'd when its scaled max\|B\| falls below 1e-10 of the batch maximum), so the event-study core keeps the otherwise-unused post-period-average contrast column as the anchor to hold the 3.x per-period NaN masks bit-identical (PR 4c-ii, user decision 2026-10-25). Reproduction: `x_nc = treated * 1[period >= 4] + 3e-6 * default_rng(seed).normal(size=n)` as a covariate on the oracle panel flips the mask between `[eye(k), avg]` and `eye(k)` on 20/20 seeds (cond(X) ~ 9e5; 1e-7 never flips, <= 3e-8 rank-drops) - too BLAS-sensitive for a committed oracle, hence the structural pin `test_hc2_bm_contrast_batch_keeps_post_average_anchor`. Decide whether the guard should use a batch-independent anchor (the treatment contrast, or the absolute floor only) and then drop the column | `diff_diff/estimators.py`, `diff_diff/linalg.py` | #v4-4c-ii | Mid | Medium |
| Re-validate the plan-review dual engine on `gpt-6.1-sol` with `tools/plan-review-eval`: re-pin every arm's composite `cli_version` to the live Claude Code + codex CLI pair, add a gpt-6.1-sol arm, run the harness against the gpt-5.6-sol baseline (the 2026-10 swap carried the model over from the code-review campaign 2 verdict without a plan-review campaign). | `tools/plan-review-eval/config/configs.json` | reviewer-eval gpt61sol PR-2 | Heavy | Low |
| TWFE-weights tutorial section: `attgt_weights` (all three `type=` values side by side) + `decompose_twfe_weights` + `plot_twfe_weights` on `mpdta`, either as a section of `02_staggered_did.ipynb` or a new notebook registered in `docs/tutorials/index.rst`; follow-on PR by decision, the feature ships without it | `docs/tutorials/02_staggered_did.ipynb` | #812 | Mid | Medium |
| Geo-experiments tutorial: the SDiD fits on the `generate_factor_data(...)` panel (conversions ~1,500, sigma-hat ~310) hit the Frank-Wolfe iteration cap - the placebo fit warns once and the bootstrap cross-check warns on 100/100 draws - yet the narrative presents the bootstrap SE as a paper-faithful robustness check. Pre-existing on `main` under BOTH backends (surfaced when the notebook outputs were refreshed for the shape-only fit diagnostic); the committed April outputs predate the aggregated non-convergence warning. Either rescale / re-tune the DGP (or raise `max_iter` / loosen `min_decrease`) so the refits converge and re-execute, or label the bootstrap result unreliable and drop it from the "Validity evidence" claim. | `docs/tutorials/18_geo_experiments.ipynb` | local codex review of the SDiD shape-only fit-RMSE PR | Mid | Low |
Expand Down
56 changes: 56 additions & 0 deletions changelog.d/20261025-v4-pr4c-ii-remove-mpd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
### Removed
- **The `MultiPeriodDiD` class, its `MultiPeriodDiDResults` container, the `PeriodEffect`
record and the `EventStudy` alias are gone** (`M-010`, `M-011`, `M-012`, `M-060`; 4.0 cut,
PR 4c-ii). `from diff_diff import MultiPeriodDiD` (and the other three names) raises
`ImportError`. The estimator is `TwoWayFixedEffects().fit(..., event_study=True, spec=,
post_periods=, reference_period=)`, which returns the unified `EventStudyResults` surface
natively: `spec="pooled"` is the removed class's design (bit-exact without `unit=`; with
`unit=` the pooled spec auto-clusters at unit), `spec="within"` is its `absorb=["unit"]`
design; `post_periods=` is required (the midpoint default died with the class) and the
post-period average `avg_att` has no successor field. Pinned field for field against the
pre-refactor capture `tests/fixtures/v4_twfe_event_study_oracles.json`.
- The `pyproject.toml` pytest `filterwarnings` entry that silenced the removed class's
construction `FutureWarning`, the `MultiPeriodDiD` power-simulation profile
(`simulate_power` has no event-study profile yet; TODO row), and the `MultiPeriodDiDResults`
branches of DiagnosticReport / BusinessReport / `practitioner_next_steps` (both reports still
reject `EventStudyResults` by design; the admission TODO row records where the logic lived).

### Breaking Changes
- **`TwoWayFixedEffects.fit(time=)` in static mode now raises `ValueError`** naming `post=`
(`M-083`). Through 3.x `time=` was the deprecated alias for the 0/1 post indicator and
emitted a `FutureWarning` (`M-082`); since 4.0 `time` means the event-study calendar column
only. Supplying both `post=` and `time=` keeps raising the "pass only post=" error.
- **`EventStudyResults.period_effects` is a `FutureWarning` compatibility view** (`M-016`;
removed in 5.0): on a calendar surface it returns the former calendar-keyed dict of
per-period records (`period, effect, se, t_stat, p_value, conf_int, is_significant,
significance_stars`, reference period omitted; `is_significant` is evaluated at the fit's
`alpha`, where the removed record hard-coded 0.05; duplicate labels raise). Relative-time
`aggregate("event_study")` surfaces do not carry it (`AttributeError`, no warning). The
successor is `to_dataframe()` and the columnar arrays.
- The 3.x "default reference_period has changed" `FutureWarning` is no longer emitted
(`M-007`): the event-study mode defaults to the last pre-period silently.
- `simulate_power` and friends read the flat `att`/`se`/`p_value`/`conf_int` names from
unregistered custom estimators' results and raise `TypeError` naming `_EstimatorProfile`
otherwise (the `avg_*` fallback left with the removed container; pass `result_extractor=`
for other shapes).

### Internal
- `DifferenceInDifferences._fit_event_study_core` serves only the TWFE event-study mode
(`post_periods` required by the core; `include_treatment_main` / `cluster_override`
keyword-required); the private payload is pruned to the fields the surface builder reads.
The clustered `hc2_bm` contrast batch keeps the former post-period-average column as the
batch-relative Bell-McCaffrey DOF guard's anchor so per-period NaN masks stay bit-identical
(REGISTRY Note; TODO row for the guard redesign). `build_event_study_surface` opens with an
`isinstance(EventStudyResults)` passthrough.
- Ledger: `M-010`/`M-011`/`M-012` -> `removed`, `M-060`/`M-007`/`M-083` -> `done`, `M-016`
-> `shimmed` on `diff_diff:EventStudyResults.period_effects`, `M-082` keeps `shimmed` with
`warning: null`. Spec item 4c-ii carries the SHIPPED note incl. the three recorded
deviations (anchor column, new live benchmark script, DR/BR branch deletion).
- Tests: the MPD-only tests are deleted or re-fixtured onto `cs_fit` / the TWFE surface
(`tests/test_v4_merge_mpd.py` carries the removal, `M-016` and `M-007` pins and the anchor
pin); the oracle fixture gains two clustered `hc2_bm` lanes captured at `d45d5316` before
the core pruning (its `mpd_twin` records are pruned in PR 4c-ii-b); the clustered
per-period CR2 DOF R/clubSandwich pin and the event-study covariate-collision guard are
migrated to TWFE; variance-conventions `twfe_es_*` rows re-pinned (`tail_df` 5) and the table
regenerated. The methodology/benchmark prose half and the oracle fixture's
`mpd_twin` prune follow in PR 4c-ii-b.
8 changes: 0 additions & 8 deletions diff_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@
)
from diff_diff.estimators import (
DifferenceInDifferences,
MultiPeriodDiD,
SyntheticDiD,
TwoWayFixedEffects,
)
Expand Down Expand Up @@ -233,8 +232,6 @@
)
from diff_diff.results import (
DiDResults,
MultiPeriodDiDResults,
PeriodEffect,
SpilloverDiDResults, # re-export
SyntheticDiDResults,
)
Expand Down Expand Up @@ -321,7 +318,6 @@
# Estimator aliases — short names for convenience
DiD = DifferenceInDifferences
TWFE = TwoWayFixedEffects
EventStudy = MultiPeriodDiD
SDiD = SyntheticDiD
CS = CallawaySantAnna
SA = SunAbraham
Expand All @@ -341,7 +337,6 @@
# Estimators
"DifferenceInDifferences",
"TwoWayFixedEffects",
"MultiPeriodDiD",
"SyntheticDiD",
"CallawaySantAnna",
"ChaisemartinDHaultfoeuille",
Expand All @@ -358,7 +353,6 @@
# Estimator aliases (short names).
"DiD",
"TWFE",
"EventStudy",
"SDiD",
"CS",
"DCDH",
Expand All @@ -374,9 +368,7 @@
"Comparison2x2",
# Results
"DiDResults",
"MultiPeriodDiDResults",
"SyntheticDiDResults",
"PeriodEffect",
"CallawaySantAnnaResults",
"CSBootstrapResults",
"GroupTimeEffect",
Expand Down
5 changes: 3 additions & 2 deletions diff_diff/_deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ def resolve_renamed_kwarg(
Returned when neither name was supplied. Required parameters pass
``NOT_SUPPLIED`` here and re-validate with :func:`require_arg`.
extra : str
Appended to the warning message (e.g. M-082's note that ``time=``
survives as TwoWayFixedEffects' event-study calendar column).
Appended to the warning message (e.g. a note that the old name
survives with a different meaning on the same surface, as M-082's
``time=`` did through 3.x).

Returns
-------
Expand Down
14 changes: 0 additions & 14 deletions diff_diff/_reporting_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,6 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
"reference": ("REGISTRY.md Sec. DifferenceInDifferences / TwoWayFixedEffects"),
}

if name == "MultiPeriodDiDResults":
return {
"name": "average ATT over post-treatment periods (event-study average)",
"definition": (
"The equally-weighted average of the post-treatment event-study "
"coefficients. Each coefficient is an ATT at a single event time "
"relative to treatment onset; the headline averages across "
"post-treatment horizons."
),
"aggregation": "event_study",
"headline_attribute": "avg_att",
"reference": "REGISTRY.md Sec. MultiPeriodDiD",
}

if name == "CallawaySantAnnaResults":
return {
"name": "overall ATT (cohort-size-weighted average of ATT(g,t))",
Expand Down
Loading
Loading