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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [3.0.0] - 2026-04-07

v3.0 completes the survey support roadmap: all 16 estimators (15 inference-level +
BaconDecomposition diagnostic) now accept `survey_design`. See v2.8.0–v2.9.1 entries
for the full feature history leading to this release.

### Breaking Changes
- **Remove `bootstrap_weight_type` parameter** from CallawaySantAnna — use `bootstrap_weights` instead (deprecated since v1.0.1)
- **Remove TROP `method="twostep"` alias** — use `method="local"` (deprecated since v2.7.2)
- **Remove TROP `method="joint"` alias** — use `method="global"` (deprecated since v2.7.2)

### Upgrading from v2.x
- `CallawaySantAnna(bootstrap_weight_type="mammen")` → `CallawaySantAnna(bootstrap_weights="mammen")`
- `TROP(method="twostep")` → `TROP(method="local")`
- `TROP(method="joint")` → `TROP(method="global")`

### Deprecated
- SyntheticDiD `lambda_reg` and `zeta` parameters formally scheduled for removal in v3.1 — use `zeta_omega`/`zeta_lambda` instead

### Changed
- Internal attribute `bootstrap_weight_type` renamed to `bootstrap_weights` in bootstrap mixin and StaggeredTripleDifference for consistency
- TROP `set_params()` now validates `method` against `("local", "global")` — previously only validated in `__init__`
- Documentation updated: all survey gap notes for WooldridgeDiD removed, ROADMAP Phase 10 items marked shipped

## [2.9.1] - 2026-04-06

### Added
Expand Down
31 changes: 17 additions & 14 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ For past changes and release history, see [CHANGELOG.md](CHANGELOG.md).

---

## Current Status (v2.9.0)
## Current Status (v3.0)

diff-diff is a **production-ready** DiD library with feature parity with R's `did` + `HonestDiD` + `synthdid` ecosystem for core DiD analysis, plus **unique survey support** that no R or Python package matches.

Expand All @@ -28,19 +28,17 @@ diff-diff is a **production-ready** DiD library with feature parity with R's `di

### Survey Support

`SurveyDesign` with strata, PSU, FPC, weight types (pweight/fweight/aweight), lonely PSU handling. 15 of 16 estimators accept `survey_design` (WooldridgeDiD support planned for Phase 10f); design-based variance estimation varies by estimator:
`SurveyDesign` with strata, PSU, FPC, weight types (pweight/fweight/aweight), lonely PSU handling. All 16 estimators accept `survey_design` (15 inference-level + BaconDecomposition diagnostic); design-based variance estimation varies by estimator:

- **TSL variance** (Taylor Series Linearization) with strata + PSU + FPC
- **Replicate weights**: BRR, Fay's BRR, JK1, JKn, SDR — 12 of 16 estimators (not SyntheticDiD, TROP, BaconDecomposition, or WooldridgeDiD)
- **Replicate weights**: BRR, Fay's BRR, JK1, JKn, SDR — 12 of 16 estimators (not SyntheticDiD, TROP, BaconDecomposition, WooldridgeDiD)
- **Survey-aware bootstrap**: multiplier at PSU (IF-based) and Rao-Wu rescaled (resampling-based)
- **DEFF diagnostics**, **subpopulation analysis**, **weight trimming**, **CV on estimates**
- **Repeated cross-sections**: `CallawaySantAnna(panel=False)` for BRFSS, ACS, CPS
- **R cross-validation**: 15 tests against R's `survey` package using NHANES, RECS, and API datasets

See [Survey Design Support](docs/choosing_estimator.rst#survey-design-support) for the full compatibility matrix, and [survey-roadmap.md](docs/survey-roadmap.md) for implementation details.

**Gap**: WooldridgeDiD does not yet accept `survey_design`. Planned for Phase 10f.

### Infrastructure

- Optional Rust backend for accelerated computation
Expand All @@ -50,24 +48,29 @@ See [Survey Design Support](docs/choosing_estimator.rst#survey-design-support) f

---

## Active Work: Survey Academic Credibility (Phase 10)
## Survey Academic Credibility (Phase 10)

Before broadly announcing survey capability, we are establishing the theoretical
and empirical foundation needed for credibility with practitioners and
methodologists. See [survey-roadmap.md](docs/survey-roadmap.md) for detailed specs.
Phase 10 established the theoretical and empirical foundation for survey support
credibility. See [survey-roadmap.md](docs/survey-roadmap.md) for detailed specs.

| Item | Priority | Status |
|------|----------|--------|
| **10a.** Theory document (`survey-theory.md`) | HIGH | Not started |
| **10b.** Research-grade survey DGP (enhance `generate_survey_did_data`) | HIGH | Not started |
| **10c.** Expand R validation (ImputationDiD, StackedDiD, SunAbraham, TripleDifference) | HIGH | Not started |
| **10d.** Tutorial: flat-weight vs design-based comparison | HIGH | Not started — depends on 10b |
| **10a.** Theory document (`survey-theory.md`) | HIGH | ✅ Shipped (v2.9.1) |
| **10b.** Research-grade survey DGP (enhance `generate_survey_did_data`) | HIGH | ✅ Shipped (v2.9.1) |
| **10c.** Expand R validation (ImputationDiD, StackedDiD, SunAbraham, TripleDifference) | HIGH | ✅ Shipped (v2.9.1) |
| **10d.** Tutorial: flat-weight vs design-based comparison | HIGH | ✅ Shipped (v2.9.1) |
| **10e.** Position paper / arXiv preprint | MEDIUM | Not started — depends on 10b |
| **10f.** WooldridgeDiD survey support (OLS + logit + Poisson) | MEDIUM | Not started |
| **10f.** WooldridgeDiD survey support (OLS + logit + Poisson) | MEDIUM | ✅ Shipped (v2.9.0) |
| **10g.** Practitioner guidance: when does survey design matter? | LOW | Not started |

---

## Future: Survey Aggregation Helper

**`survey_aggregate()` helper function** for the microdata-to-panel workflow. Bridges individual-level survey data (BRFSS, ACS, CPS) collected as repeated cross-sections to geographic-level (state, city) panel DiD. Computes design-based cell means and precision weights that estimators can consume directly.

---

## Future Estimators

### de Chaisemartin-D'Haultfouille Estimator
Expand Down
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ Mypy reports 0 errors. All mixin `attr-defined` errors resolved via

Deprecated parameters still present for backward compatibility:

- `bootstrap_weight_type` in `CallawaySantAnna` (`staggered.py`)
- Deprecated in favor of `bootstrap_weights` parameter
- Remove in next major version (v3.0)
- `lambda_reg` and `zeta` in `SyntheticDiD` (`synthetic_did.py`)
- Deprecated in favor of `zeta_omega`/`zeta_lambda` parameters
- Remove in v3.1

---

Expand Down
2 changes: 1 addition & 1 deletion diff_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@
EDiD = EfficientDiD
ETWFE = WooldridgeDiD

__version__ = "2.9.1"
__version__ = "3.0.0"
__all__ = [
# Estimators
"DifferenceInDifferences",
Expand Down
21 changes: 1 addition & 20 deletions diff_diff/staggered.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,6 @@ class CallawaySantAnna(
- "rademacher": +1/-1 with equal probability (standard choice)
- "mammen": Two-point distribution (asymptotically valid, matches skewness)
- "webb": Six-point distribution (recommended when n_clusters < 20)
bootstrap_weight_type : str, optional
.. deprecated:: 1.0.1
Use ``bootstrap_weights`` instead. Will be removed in v3.0.
seed : int, optional
Random seed for reproducibility.
rank_deficient_action : str, default="warn"
Expand Down Expand Up @@ -293,7 +290,6 @@ def __init__(
cluster: Optional[str] = None,
n_bootstrap: int = 0,
bootstrap_weights: Optional[str] = None,
bootstrap_weight_type: Optional[str] = None,
seed: Optional[int] = None,
rank_deficient_action: str = "warn",
base_period: str = "varying",
Expand Down Expand Up @@ -323,18 +319,7 @@ def __init__(
f"pscore_fallback must be 'error' or 'unconditional', " f"got '{pscore_fallback}'"
)

# Handle bootstrap_weight_type deprecation
if bootstrap_weight_type is not None:
warnings.warn(
"bootstrap_weight_type is deprecated and will be removed in v3.0. "
"Use bootstrap_weights instead.",
DeprecationWarning,
stacklevel=2,
)
if bootstrap_weights is None:
bootstrap_weights = bootstrap_weight_type

# Default to rademacher if neither specified
# Default to rademacher if not specified
if bootstrap_weights is None:
bootstrap_weights = "rademacher"

Expand Down Expand Up @@ -362,8 +347,6 @@ def __init__(
self.cluster = cluster
self.n_bootstrap = n_bootstrap
self.bootstrap_weights = bootstrap_weights
# Keep bootstrap_weight_type for backward compatibility
self.bootstrap_weight_type = bootstrap_weights
self.seed = seed
self.rank_deficient_action = rank_deficient_action
self.base_period = base_period
Expand Down Expand Up @@ -3881,8 +3864,6 @@ def get_params(self) -> Dict[str, Any]:
"cluster": self.cluster,
"n_bootstrap": self.n_bootstrap,
"bootstrap_weights": self.bootstrap_weights,
# Deprecated but kept for backward compatibility
"bootstrap_weight_type": self.bootstrap_weight_type,
"seed": self.seed,
"rank_deficient_action": self.rank_deficient_action,
"base_period": self.base_period,
Expand Down
8 changes: 4 additions & 4 deletions diff_diff/staggered_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class CallawaySantAnnaBootstrapMixin:

# Type hints for attributes accessed from the main class
n_bootstrap: int
bootstrap_weight_type: str
bootstrap_weights: str
alpha: float
seed: Optional[int]
anticipation: int
Expand Down Expand Up @@ -329,7 +329,7 @@ def _run_multiplier_bootstrap(
if _use_survey_bootstrap:
# PSU-level multiplier weights
psu_weights, psu_ids = _generate_survey_multiplier_weights_batch(
self.n_bootstrap, resolved_survey_unit, self.bootstrap_weight_type, rng
self.n_bootstrap, resolved_survey_unit, self.bootstrap_weights, rng
)
# Build unit → PSU column map
if resolved_survey_unit.psu is not None:
Expand All @@ -348,7 +348,7 @@ def _run_multiplier_bootstrap(
else:
# Standard unit-level weights (no survey or weights-only)
all_bootstrap_weights = _generate_bootstrap_weights_batch(
self.n_bootstrap, n_units, self.bootstrap_weight_type, rng
self.n_bootstrap, n_units, self.bootstrap_weights, rng
)

# Vectorized bootstrap ATT(g,t) computation
Expand Down Expand Up @@ -534,7 +534,7 @@ def _run_multiplier_bootstrap(

return CSBootstrapResults(
n_bootstrap=self.n_bootstrap,
weight_type=self.bootstrap_weight_type,
weight_type=self.bootstrap_weights,
alpha=self.alpha,
overall_att_se=overall_se,
overall_att_ci=overall_ci,
Expand Down
3 changes: 1 addition & 2 deletions diff_diff/staggered_triple_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ def __init__(
self.base_period = base_period
self.n_bootstrap = n_bootstrap
self.bootstrap_weights = bootstrap_weights
self.bootstrap_weight_type = bootstrap_weights
self.seed = seed
self.cband = cband
self.pscore_trim = pscore_trim
Expand Down Expand Up @@ -186,7 +185,7 @@ def set_params(self, **params) -> "StaggeredTripleDifference":
raise ValueError(f"Unknown parameter: {key}")
setattr(self, key, value)
if "bootstrap_weights" in params:
self.bootstrap_weight_type = params["bootstrap_weights"]
self.bootstrap_weights = params["bootstrap_weights"]
return self

# ------------------------------------------------------------------
Expand Down
7 changes: 4 additions & 3 deletions diff_diff/synthetic_did.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,14 @@ def __init__(
warnings.warn(
"lambda_reg is deprecated and ignored. Regularization is now "
"auto-computed from data. Use zeta_omega to override unit weight "
"regularization.",
"regularization. Will be removed in v3.1.",
DeprecationWarning,
stacklevel=2,
)
if zeta is not None:
warnings.warn(
"zeta is deprecated and ignored. Use zeta_lambda to override "
"time weight regularization.",
"time weight regularization. Will be removed in v3.1.",
DeprecationWarning,
stacklevel=2,
)
Expand Down Expand Up @@ -1124,7 +1124,8 @@ def set_params(self, **params) -> "SyntheticDiD":
for key, value in params.items():
if key in _deprecated:
warnings.warn(
f"{key} is deprecated and ignored. Use zeta_omega/zeta_lambda " f"instead.",
f"{key} is deprecated and ignored. Use zeta_omega/zeta_lambda "
f"instead. Will be removed in v3.1.",
DeprecationWarning,
stacklevel=2,
)
Expand Down
41 changes: 4 additions & 37 deletions diff_diff/trop.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,6 @@ class TROP(TROPLocalMixin, TROPGlobalMixin):
ATT is the mean of these effects. For the paper's full
per-treated-cell estimator, use ``method='local'``.

- 'twostep': Deprecated alias for 'local'. Will be removed in v3.0.

- 'joint': Deprecated alias for 'global'. Will be removed in v3.0.

lambda_time_grid : list, optional
Grid of time weight decay parameters. 0.0 = uniform weights (disabled).
Must not contain inf. Default: [0, 0.1, 0.5, 1, 2, 5].
Expand Down Expand Up @@ -140,26 +136,9 @@ def __init__(
seed: Optional[int] = None,
):
# Validate method parameter
# 'local'/'global' are preferred; 'twostep'/'joint' are deprecated aliases
valid_methods = ("local", "twostep", "joint", "global")
valid_methods = ("local", "global")
if method not in valid_methods:
raise ValueError(f"method must be one of {valid_methods}, got '{method}'")
if method == "twostep":
warnings.warn(
"method='twostep' is deprecated and will be removed in v3.0. "
"Use method='local' instead.",
FutureWarning,
stacklevel=2,
)
method = "local"
if method == "joint":
warnings.warn(
"method='joint' is deprecated and will be removed in v3.0. "
"Use method='global' instead.",
FutureWarning,
stacklevel=2,
)
method = "global"
self.method = method

# Default grids from paper
Expand Down Expand Up @@ -913,22 +892,10 @@ def get_params(self) -> Dict[str, Any]:
def set_params(self, **params) -> "TROP":
"""Set estimator parameters."""
for key, value in params.items():
if key == "method" and value == "twostep":
warnings.warn(
"method='twostep' is deprecated and will be removed in "
"v3.0. Use method='local' instead.",
FutureWarning,
stacklevel=2,
)
value = "local"
if key == "method" and value == "joint":
warnings.warn(
"method='joint' is deprecated and will be removed in "
"v3.0. Use method='global' instead.",
FutureWarning,
stacklevel=2,
if key == "method" and value not in ("local", "global"):
raise ValueError(
f"method must be one of ('local', 'global'), got '{value}'"
)
value = "global"
if hasattr(self, key):
setattr(self, key, value)
else:
Expand Down
2 changes: 1 addition & 1 deletion docs/api/_autosummary/diff_diff.CallawaySantAnna.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
.. autosummary::

~CallawaySantAnna.n_bootstrap
~CallawaySantAnna.bootstrap_weight_type
~CallawaySantAnna.bootstrap_weights
~CallawaySantAnna.alpha
~CallawaySantAnna.seed
~CallawaySantAnna.anticipation
Expand Down
4 changes: 0 additions & 4 deletions docs/api/trop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,6 @@ For the paper's full per-treated-cell estimator (Algorithm 2), use
The global method is **faster** (single optimization vs N_treated optimizations).
Treatment effects are **heterogeneous** per-observation residuals; ATT is their mean.

``method='twostep'`` is a deprecated alias for ``method='local'`` and will be
removed in v3.0. ``method='joint'`` is a deprecated alias for ``method='global'``
and will be removed in v3.0.

.. list-table::
:header-rows: 1
:widths: 20 40 40
Expand Down
10 changes: 5 additions & 5 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> A Python library for Difference-in-Differences causal inference analysis. Provides sklearn-like estimators with statsmodels-style output for econometric analysis.

- Version: 2.9.1
- Version: 3.0.0
- Repository: https://github.com/igerber/diff-diff
- License: MIT
- Dependencies: numpy, pandas, scipy (no statsmodels dependency)
Expand Down Expand Up @@ -589,7 +589,7 @@ Triply Robust Panel estimator (Athey, Imbens, Qu & Viviano 2025). Combines nucle

```python
TROP(
method: str = "twostep", # "twostep" or "global" (or deprecated "joint")
method: str = "local", # "local" or "global"
lambda_time_grid: list[float] = None, # Time weight decay grid [0, 0.1, 0.5, 1, 2, 5]
lambda_unit_grid: list[float] = None, # Unit weight decay grid [0, 0.1, 0.5, 1, 2, 5]
lambda_nn_grid: list[float] = None, # Nuclear norm grid [0, 0.01, 0.1, 1, 10]
Expand Down Expand Up @@ -618,7 +618,7 @@ trop.fit(
```python
from diff_diff import TROP

trop = TROP(method='twostep', seed=42)
trop = TROP(method='local', seed=42)
results = trop.fit(data, outcome='outcome', treatment='treated',
unit='unit', time='period')
results.print_summary()
Expand Down Expand Up @@ -1525,7 +1525,7 @@ clear_cache()

## Survey Support

All estimators except `WooldridgeDiD` accept an optional `survey_design` parameter in `fit()`. Pass a `SurveyDesign` object to get design-based variance estimation. (WooldridgeDiD survey support is planned for Phase 10f.)
All estimators accept an optional `survey_design` parameter in `fit()`. Pass a `SurveyDesign` object to get design-based variance estimation.

```python
from diff_diff import SurveyDesign, CallawaySantAnna
Expand Down Expand Up @@ -1576,7 +1576,7 @@ sd_female, data_female = sd.subpopulation(data, mask=lambda df: df['sex'] == 'F'
- Repeated cross-sections: `CallawaySantAnna(panel=False)`
- Compatibility matrix: see `docs/choosing_estimator.rst` Survey Design Support section

No R or Python package offers design-based variance estimation for modern heterogeneity-robust DiD estimators. (`WooldridgeDiD` does not yet accept `survey_design` — planned for Phase 10f.)
No R or Python package offers design-based variance estimation for modern heterogeneity-robust DiD estimators.

## Linear Algebra Helpers

Expand Down
Loading
Loading