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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- **Narrative docs migrated off the deprecated fit-time `aggregate=`** (the
3.9 M-020 family; TODO "fit-time aggregate= teachings" sweep, RST half):
`choosing_estimator.rst`, `python_comparison.rst` and `r_comparison.rst` now
teach post-fit `results.aggregate(type=...)` (the r_comparison block maps
1:1 onto R's three `aggte()` calls; `'all'` has no post-fit counterpart),
and `troubleshooting.rst`'s event-study-plot remedy is rewritten to a
`base_period="universal"` fit whose `aggregate('event_study')` container is
plotted directly - the container carries its own `is_reference` marking, so
the example inspects the actual reference row(s) rather than hard-coding
`reference_period=-1` (under the varying default every pre-period point is
an estimated effect with no common anchor, and on gapped grids even the
universal base can anchor at an event time other than -1).
The troubleshooting bootstrap passage deliberately KEEPS fit-time
`aggregate='event_study'` with a comment naming it the documented until-4.0
exception (post-fit recompute levels raise on a bootstrapped fit;
`'simple'` relays). The two rewritten blocks that previously died on
allowlisted `NameError`s (`r_comparison:block2`, `troubleshooting:block8`)
are now self-contained and removed from `_CONTEXT_DEPENDENT_SNIPPETS`, so
`tests/test_doc_snippets.py` actually executes them.

## [3.9.0] - 2026-08-10

The 3.9 shim release of the 4.0 API-unification program (`docs/v4-design.md`):
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ generic sparse-FE, QR+SVD rank-detection redundancy, `check_finite` bypass — m
|-------|----------|--------|--------|----------|
| Committed `fixest::feols` event-study golden for TWFE `event_study=True` (within + pooled specs, unbalanced + covariate panels, matched CR1 cluster convention, per-period effects + vcov block) - the in-suite gates are shared-core cross-checks (TWFE-within == MPD-absorb, pooled == MPD bit-exact), so a defect common to the shared core would pass; the live-R harness (`benchmarks/R/benchmark_multiperiod.R`, `feols(y ~ treated * time_f \| unit)`) validated the within design in `docs/benchmarks.rst` but is not a committed regression test - follow the `fixest_did_twfe_golden.json` committed-golden pattern (pytest.skip when absent) | `tests/test_fixest_did_twfe_parity.py`, `benchmarks/R/` | 3(a) R2 | Mid | Medium |
| Type-blind `n_bootstrap` acceptance in already-validated estimators - HAD bool (`isinstance(..., int)` passes `True`, runs as 1 replicate), dCDH bool+float (its bare `< 0` check passes both `True` and `2.5`), TROP float (`2.5` passes the `>= 2` floor), SyntheticDiD float under all three variance methods + bool/negative under jackknife (its floor check is skipped there) - align these local checks with the `utils.validate_n_bootstrap` type guard (M-081 kept them out of the sweep: it scoped to previously-UNvalidated estimators only) | `diff_diff/had.py`, `diff_diff/chaisemartin_dhaultfoeuille.py`, `diff_diff/trop.py`, `diff_diff/synthetic_did.py` | 2(d) PR-B | Quick | Low |
| Fit-time `aggregate=` teachings persist across the docs and tutorials (M-020 family, removed at 4.0); migrate to post-fit `results.aggregate(...)`. Re-scoped 2026-08-09 while shipping the migration guide - this is NOT a quick sweep. Narrative docs: `troubleshooting.rst:216`/`:245` both sit on the `n_bootstrap=999` fit at `:213`, and post-fit event-study aggregation **raises `NotImplementedError` on a bootstrapped fit** (`staggered_results.py:323`), so those two need a decision about what to teach before any edit; `choosing_estimator.rst:252` and `python_comparison.rst:416` use the analytical default and can migrate freely; `r_comparison.rst:119-127` needs its `results.event_study_effects`/`.group_effects` reads rebound to the `aggregate()` return values in the same edit (the fields stay `None` after post-fit aggregation). API pages: `docs/api/triple_diff.rst:56`, plus `business_report.rst:77` and `diagnostic_report.rst:59`, which **cannot** migrate today because both report consumers read the raw `event_study_effects` field; `had.rst:164` and `continuous_did.rst:137` are prose references only. Tutorials: **28 executable code-cell sites across 9 notebooks** (`02_staggered_did` 7, `09_real_world_examples` 6, `16_survey_did` 6, `26_composition_drift_calibration` 3, `21_had_pretest_workflow` 2, and one each in `08_triple_diff`, `16_wooldridge_etwfe`, `17_brand_awareness_survey`, `24_staggered_vs_collapsed_power`) - all nbmake-executed; `14_continuous_did` and `15_efficient_did` match only in markdown prose | `docs/troubleshooting.rst`, `docs/choosing_estimator.rst`, `docs/python_comparison.rst`, `docs/r_comparison.rst`, `docs/api/*.rst`, `docs/tutorials/*.ipynb` | 2(b) PR-4 | Heavy | Medium |
| Fit-time `aggregate=` teachings persist in the tutorials (M-020 family, removed at 4.0); migrate to post-fit `results.aggregate(...)`. The NARRATIVE-RST half is DONE (this row's original scope, re-scoped 2026-08-09): `choosing_estimator.rst`, `python_comparison.rst`, `r_comparison.rst` migrated; `troubleshooting.rst`'s bootstrap passage deliberately keeps fit-time `aggregate=` as the documented until-4.0 exception (post-fit recompute levels **raise `NotImplementedError` on a bootstrapped fit**, `staggered_results.py:323`). Remaining: **the notebook half** - executable code-cell sites across 9 notebooks (`02_staggered_did` 7, `09_real_world_examples` 6, `16_survey_did` 6, `26_composition_drift_calibration` 3, `21_had_pretest_workflow` 2, and one each in `08_triple_diff`, `16_wooldridge_etwfe`, `17_brand_awareness_survey`, `24_staggered_vs_collapsed_power`) - all nbmake-executed; `14_continuous_did`/`15_efficient_did` match only in markdown prose; bootstrapped fits keep the documented fit-time exception and 08's staggered DDD is canonical (M-140/M-141). API pages stay per their blockers: `docs/api/triple_diff.rst:56` (canonical DDD), `business_report.rst:77`/`diagnostic_report.rst:59` (report consumers read the raw `event_study_effects` field); `had.rst:164`/`continuous_did.rst:137` are prose-only and already correct | `docs/tutorials/*.ipynb`, `docs/api/*.rst` | 2(b) PR-4 | Heavy | Medium |
| Evaluate adding the `BaseEstimator` param surface (get_params/set_params) to the exported classes that never had it - `PowerAnalysis`, `LinearRegression`, `BusinessReport`, `DiagnosticReport`, `TWFEWeightsResult` (a NEW public surface, deliberately out of the 2(c)-i pure-refactor scope; `LinearRegression` is the one `fit`-bearing class excluded from the contract suite's roster-completeness test). | `diff_diff/linalg.py`, `diff_diff/power.py` | mixin PR | Mid | Low |
| Tighten the mypy suppressions that back the enforced-zero posture: burn down `prep_dgp`'s per-module `[index]` override (needs a None-vs-array restructure that preserves the seeded RNG stream), and evaluate re-enabling the globally disabled codes (`arg-type`, `return-value`, `var-annotated`, `assignment`) one at a time — `assignment` alone hid several real annotation drifts found during the 2026-07 triage. | `pyproject.toml` `[tool.mypy]`, `diff_diff/prep_dgp.py` | lint-CI | Mid | Low |
| MMM interop follow-up: Meridian `roi_calibration_period` mask builder - accept the MMM's time index + channel order and emit the boolean `(n_media_times, n_media_channels)` mask so `.to_code()` scopes the prior to the experiment window automatically (today the caller passes a mask expression / `full_model_window=True`). | `diff_diff/mmm.py` | mmm-interop | Quick | Low |
Expand Down
8 changes: 3 additions & 5 deletions docs/choosing_estimator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,9 @@ This is the recommended estimator for most applied work with staggered adoption.
# Overall ATT
print(f"Overall ATT: {results.overall_att:.3f}")

# Event study aggregation
es = cs.fit(data, outcome='y', unit='unit_id',
time='period', first_treat='first_treat',
covariates=['x1', 'x2'], aggregate='event_study')
event_study_df = es.to_dataframe('event_study')
# Event study aggregation (post-fit - no refit needed)
es = results.aggregate('event_study')
event_study_df = es.to_dataframe()

Reversible (Non-Absorbing) Treatment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
5 changes: 2 additions & 3 deletions docs/python_comparison.rst
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,9 @@ Staggered DiD (Callaway-Sant'Anna)
unit='unit',
time='time',
first_treat='first_treat',
covariates=['x1', 'x2'],
aggregate='event_study'
covariates=['x1', 'x2']
)
event_study = results.event_study_effects
event_study = results.aggregate('event_study')

.. code-block:: python

Expand Down
13 changes: 7 additions & 6 deletions docs/r_comparison.rst
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,14 @@ staggered DiD. Here's how to translate common operations:

.. code-block:: python

# Python (R's aggte() has two counterparts: the fit-time aggregate= shown here,
# deprecated in 3.9, and post-fit results.aggregate(type=), which supersedes it)
# Python (post-fit results.aggregate(type=) is aggte()'s counterpart;
# R's "dynamic" is spelled "event_study")
cs = CallawaySantAnna()
results = cs.fit(data, outcome='Y', time='period', unit='id',
first_treat='G', aggregate='all')
overall_att = results.overall_att # Simple aggregation
event_study = results.event_study_effects # Dynamic
by_group = results.group_effects # By cohort
first_treat='G')
agg_simple = results.aggregate('simple')
agg_dynamic = results.aggregate('event_study')
agg_group = results.aggregate('group')

R ``HonestDiD`` Package → diff-diff
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
32 changes: 21 additions & 11 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,11 @@ Staggered Adoption Issues
# Check cohort sizes
print(data.groupby('first_treat')['unit_id'].nunique())

# Use bootstrap for better inference
# Use bootstrap for better inference. On a BOOTSTRAPPED fit, post-fit
# results.aggregate('event_study') raises - the percentile draws are not
# retained - so the deprecated fit-time aggregate= remains the documented
# route for this case until 4.0. (results.aggregate('simple') relays the
# stored bootstrap inference and works on any fit.)
cs = CallawaySantAnna(n_bootstrap=999)
results = cs.fit(data, outcome='y', unit='unit_id',
time='period', first_treat='first_treat',
Expand All @@ -231,19 +235,25 @@ Visualization Issues

.. code-block:: python

from diff_diff import plot_event_study
from diff_diff import CallawaySantAnna, plot_event_study

# Check your results first
print(results.period_effects) # or results.event_study_effects
# For CallawaySantAnna, aggregate to an event study post-fit, then plot.
# base_period="universal" gives the event study explicit reference row(s),
# marked in the container - under the default varying base every
# pre-treatment point is an estimated effect with no common anchor, so
# renormalizing a plot around one would silently shift every point.
cs = CallawaySantAnna(base_period='universal')
results = cs.fit(data, outcome='y', unit='unit_id',
time='period', first_treat='first_treat')
es = results.aggregate('event_study')

# Specify reference period explicitly
plot_event_study(results, reference_period=-1)
# Inspect the actual reference row(s) - on gapped period grids the
# positional base can sit at an event time other than -1
print(es.to_dataframe().query("is_reference"))

# For CallawaySantAnna, fit with aggregate='event_study'
results = cs.fit(data, outcome='y', unit='unit_id',
time='period', first_treat='first_treat',
aggregate='event_study')
plot_event_study(results)
# Plot the container - it carries its own reference; no manual
# reference_period override is needed (or safe to hard-code)
plot_event_study(es)

"Plot doesn't show in Jupyter"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 0 additions & 2 deletions tests/test_doc_snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,11 +413,9 @@ def _restore_datasets_module():
"python_comparison:block5",
"quickstart:block3",
"quickstart:block9",
"r_comparison:block2",
"r_comparison:block3",
"r_comparison:block4",
"r_comparison:block7",
"troubleshooting:block8",
}


Expand Down
Loading