Skip to content
Merged
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- **MultiPeriodDiD: Full event-study specification** (BREAKING)
- Treatment × period interactions now created for ALL periods (pre and post),
not just post-treatment
- Pre-period coefficients available for parallel trends assessment
- Default reference period changed from first to last pre-period (e=-1 convention)
with FutureWarning for one release cycle
- `period_effects` dict now contains both pre and post period effects
- `to_dataframe()` includes `is_post` column
- `summary()` output now shows pre-period effects section
- t_stat uses `np.isfinite(se) and se > 0` guard (consistent with other estimators)

### Added
- Time-varying treatment warning when `unit` is provided and treatment varies
within units (guides users toward ever-treated indicator D_i)
- `unit` parameter to `MultiPeriodDiD.fit()` for staggered adoption detection
- `reference_period` and `interaction_indices` attributes on `MultiPeriodDiDResults`
- `pre_period_effects` and `post_period_effects` convenience properties on results
- Pre-period section in `summary()` output with reference period indicator
- `ValueError` when `reference_period` is set to a post-treatment period
- Staggered adoption warning when treatment timing varies across units (with `unit` param)
- Informative KeyError when accessing reference period via `get_effect()`

### Removed
- **TROP `variance_method` parameter** — Jackknife variance estimation removed.
Bootstrap (the only method specified in Athey et al. 2025) is now always used.
The `variance_method` field has also been removed from `TROPResults`.

### Fixed
- HonestDiD: filter non-finite period effects from MultiPeriodDiD results
(prevents NaN propagation into sensitivity bounds; raises ValueError
when no finite pre- or post-period effects remain)
- HonestDiD VCV extraction: now uses interaction sub-VCV instead of full regression VCV
(via `interaction_indices` period → column index mapping)
- MultiPeriodDiD: `avg_se` guard now checks `np.isfinite()` (matches per-period pattern;
prevents `avg_t_stat=0` / `avg_p_value=1` when variance is infinite)
- HonestDiD: extraction now uses explicit pre-then-post ordering instead of sorted period
labels (prevents misclassification when period labels don't sort chronologically)

## [2.2.0] - 2026-01-27

### Added
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ cross-platform compilation - no OpenBLAS or Intel MKL installation required.

- **`diff_diff/estimators.py`** - Core estimator classes implementing DiD methods:
- `DifferenceInDifferences` - Basic 2x2 DiD with formula or column-name interface
- `MultiPeriodDiD` - Event-study style DiD with period-specific treatment effects
- `MultiPeriodDiD` - Full event-study DiD with treatment × period interactions for ALL periods (pre and post). Supports `unit` parameter for staggered adoption detection. Default reference period is last pre-period (e=-1 convention). Pre-period coefficients enable parallel trends assessment. `interaction_indices` maps periods to VCV column indices for robust sub-VCV extraction in HonestDiD/PreTrendsPower.
- Re-exports `TwoWayFixedEffects` and `SyntheticDiD` for backward compatibility

- **`diff_diff/twfe.py`** - Two-Way Fixed Effects estimator:
Expand Down
45 changes: 29 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,31 +561,37 @@ results = twfe.fit(

### Multi-Period DiD (Event Study)

For settings with multiple pre- and post-treatment periods:
For settings with multiple pre- and post-treatment periods. Estimates treatment × period
interactions for ALL periods (pre and post), enabling parallel trends assessment:

```python
from diff_diff import MultiPeriodDiD

# Fit with multiple time periods
# Fit full event study with pre and post period effects
did = MultiPeriodDiD()
results = did.fit(
panel_data,
outcome='sales',
treatment='treated',
time='period',
post_periods=[3, 4, 5], # Periods 3-5 are post-treatment
reference_period=0 # Reference period for comparison
reference_period=2, # Last pre-period (e=-1 convention)
unit='unit_id', # Optional: warns if staggered adoption detected
)

# View period-specific treatment effects
for period, effect in results.period_effects.items():
print(f"Period {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
# Pre-period effects test parallel trends (should be ≈ 0)
for period, effect in results.pre_period_effects.items():
print(f"Pre {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")

# Post-period effects estimate dynamic treatment effects
for period, effect in results.post_period_effects.items():
print(f"Post {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")

# View average treatment effect across post-periods
print(f"Average ATT: {results.avg_att:.3f}")
print(f"Average SE: {results.avg_se:.3f}")

# Full summary with all period effects
# Full summary with pre and post period effects
results.print_summary()
```

Expand Down Expand Up @@ -951,10 +957,10 @@ Create publication-ready event study plots:
```python
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham

# From MultiPeriodDiD
# From MultiPeriodDiD (full event study with pre and post period effects)
did = MultiPeriodDiD()
results = did.fit(data, outcome='y', treatment='treated',
time='period', post_periods=[3, 4, 5])
time='period', post_periods=[3, 4, 5], reference_period=2)
plot_event_study(results, title="Treatment Effects Over Time")

# From CallawaySantAnna (with event study aggregation)
Expand Down Expand Up @@ -1413,14 +1419,15 @@ Pre-trends tests have low power and can exacerbate bias. **Honest DiD** (Rambach
```python
from diff_diff import HonestDiD, MultiPeriodDiD

# First, fit a standard event study
# First, fit a full event study (pre + post period effects)
did = MultiPeriodDiD()
event_results = did.fit(
data,
outcome='outcome',
treatment='treated',
time='period',
post_periods=[5, 6, 7, 8, 9]
post_periods=[5, 6, 7, 8, 9],
reference_period=4, # Last pre-period (e=-1 convention)
)

# Compute honest bounds with relative magnitudes restriction
Expand Down Expand Up @@ -1488,14 +1495,15 @@ A passing pre-trends test doesn't mean parallel trends holds—it may just mean
```python
from diff_diff import PreTrendsPower, MultiPeriodDiD

# First, fit an event study
# First, fit a full event study
did = MultiPeriodDiD()
event_results = did.fit(
data,
outcome='outcome',
treatment='treated',
time='period',
post_periods=[5, 6, 7, 8, 9]
post_periods=[5, 6, 7, 8, 9],
reference_period=4,
)

# Analyze pre-trends test power
Expand Down Expand Up @@ -1764,23 +1772,28 @@ MultiPeriodDiD(
| `covariates` | list | Linear control variables |
| `fixed_effects` | list | Categorical FE columns (creates dummies) |
| `absorb` | list | High-dimensional FE (within-transformation) |
| `reference_period` | any | Omitted period for time dummies |
| `reference_period` | any | Omitted period (default: last pre-period, e=-1 convention) |
| `unit` | str | Unit identifier column (for staggered adoption warning) |

### MultiPeriodDiDResults

**Attributes:**

| Attribute | Description |
|-----------|-------------|
| `period_effects` | Dict mapping periods to PeriodEffect objects |
| `avg_att` | Average ATT across post-treatment periods |
| `period_effects` | Dict mapping periods to PeriodEffect objects (pre and post, excluding reference) |
| `avg_att` | Average ATT across post-treatment periods only |
| `avg_se` | Standard error of average ATT |
| `avg_t_stat` | T-statistic for average ATT |
| `avg_p_value` | P-value for average ATT |
| `avg_conf_int` | Confidence interval for average ATT |
| `n_obs` | Number of observations |
| `pre_periods` | List of pre-treatment periods |
| `post_periods` | List of post-treatment periods |
| `reference_period` | The omitted reference period (coefficient = 0 by construction) |
| `interaction_indices` | Dict mapping period → column index in VCV (for sub-VCV extraction) |
| `pre_period_effects` | Property: pre-period effects only (for parallel trends assessment) |
| `post_period_effects` | Property: post-period effects only |

**Methods:**

Expand Down
Loading