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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ 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).

## [2.1.0] - 2026-01-17

### Added
- **Triply Robust Panel (TROP) estimator** implementing Athey, Imbens, Qu & Viviano (2025)
- `TROP` class combining three robustness components:
- Factor model adjustment via SVD (removes unobserved confounders with factor structure)
- Synthetic control style unit weights
- SDID style time weights
- `TROPResults` dataclass with ATT, factors, loadings, unit/time weights
- `trop()` convenience function for quick estimation
- Automatic rank selection methods: cross-validation (`'cv'`), information criterion (`'ic'`), elbow detection (`'elbow'`)
- Bootstrap and placebo-based variance estimation
- Full integration with existing infrastructure (exports in `__init__.py`, sklearn-compatible API)
- Tutorial notebook: `docs/tutorials/10_trop.ipynb`
- Comprehensive test suite: `tests/test_trop.py`

**Reference**: Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2025). "Triply Robust Panel Estimators." *Working Paper*. [arXiv:2508.21536](https://arxiv.org/abs/2508.21536)

## [2.0.3] - 2026-01-17

### Changed
Expand Down Expand Up @@ -392,6 +410,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `to_dict()` and `to_dataframe()` export methods
- `is_significant` and `significance_stars` properties

[2.1.0]: https://github.com/igerber/diff-diff/compare/v2.0.3...v2.1.0
[2.0.3]: https://github.com/igerber/diff-diff/compare/v2.0.2...v2.0.3
[2.0.2]: https://github.com/igerber/diff-diff/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/igerber/diff-diff/compare/v2.0.0...v2.0.1
Expand Down
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ pytest tests/test_rust_backend.py -v
- Regression adjustment, IPW, and doubly robust estimation methods
- Proper covariate handling (unlike naive DDD implementations)

- **`diff_diff/trop.py`** - Triply Robust Panel (TROP) estimator (v2.1.0):
- `TROP` - Athey, Imbens, Qu & Viviano (2025) estimator with factor model adjustment
- `TROPResults` - Results with ATT, factors, loadings, unit/time weights
- `trop()` - Convenience function for quick estimation
- Three robustness components: factor adjustment, unit weights, time weights
- Automatic rank selection via cross-validation, information criterion, or elbow detection
- Bootstrap and placebo-based variance estimation

- **`diff_diff/bacon.py`** - Goodman-Bacon decomposition for TWFE diagnostics:
- `BaconDecomposition` - Decompose TWFE into weighted 2x2 comparisons (Goodman-Bacon 2021)
- `BaconDecompositionResults` - Results with comparison weights and estimates by type
Expand Down Expand Up @@ -250,6 +258,7 @@ See `docs/performance-plan.md` for full optimization details and `docs/benchmark
- `07_pretrends_power.ipynb` - Pre-trends power analysis (Roth 2022), MDV, power curves
- `08_triple_diff.ipynb` - Triple Difference (DDD) estimation with proper covariate handling
- `09_real_world_examples.ipynb` - Real-world data examples (Card-Krueger, Castle Doctrine, Divorce Laws)
- `10_trop.ipynb` - Triply Robust Panel (TROP) estimation with factor model adjustment

### Benchmarks

Expand Down Expand Up @@ -282,6 +291,7 @@ Tests mirror the source modules:
- `tests/test_staggered.py` - Tests for CallawaySantAnna
- `tests/test_sun_abraham.py` - Tests for SunAbraham interaction-weighted estimator
- `tests/test_triple_diff.py` - Tests for Triple Difference (DDD) estimator
- `tests/test_trop.py` - Tests for Triply Robust Panel (TROP) estimator
- `tests/test_bacon.py` - Tests for Goodman-Bacon decomposition
- `tests/test_linalg.py` - Tests for unified OLS backend, robust variance estimation, LinearRegression helper, and InferenceResult
- `tests/test_utils.py` - Tests for parallel trends, robust SE, synthetic weights
Expand Down
254 changes: 254 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
- **Staggered adoption**: Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
- **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
- **Triply Robust Panel (TROP)**: Factor-adjusted DiD with synthetic weights (Athey et al. 2025)
- **Event study plots**: Publication-ready visualization of treatment effects
- **Parallel trends testing**: Multiple methods including equivalence tests
- **Goodman-Bacon decomposition**: Diagnose TWFE bias by decomposing into 2x2 comparisons
Expand All @@ -98,6 +99,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
| `07_pretrends_power.ipynb` | Pre-trends power analysis (Roth 2022), MDV, power curves |
| `08_triple_diff.ipynb` | Triple Difference (DDD) estimation with proper covariate handling |
| `09_real_world_examples.ipynb` | Real-world data examples (Card-Krueger, Castle Doctrine, Divorce Laws) |
| `10_trop.ipynb` | Triply Robust Panel (TROP) estimation with factor model adjustment |

## Data Preparation

Expand Down Expand Up @@ -1115,6 +1117,179 @@ SyntheticDiD(
)
```

### Triply Robust Panel (TROP)

TROP (Athey, Imbens, Qu & Viviano 2025) extends Synthetic DiD by adding interactive fixed effects (factor model) adjustment. It's particularly useful when there are unobserved time-varying confounders with a factor structure that could bias standard DiD or SDID estimates.

TROP combines three robustness components:
1. **Nuclear norm regularized factor model**: Estimates interactive fixed effects L_it via soft-thresholding
2. **Exponential distance-based unit weights**: ω_j = exp(-λ_unit × distance(j,i))
3. **Exponential time decay weights**: θ_s = exp(-λ_time × |s-t|)

Tuning parameters are selected via leave-one-out cross-validation (LOOCV).

```python
from diff_diff import TROP, trop

# Fit TROP model with automatic tuning via LOOCV
trop_est = TROP(
lambda_time_grid=[0.0, 0.5, 1.0, 2.0], # Time decay grid
lambda_unit_grid=[0.0, 0.5, 1.0, 2.0], # Unit distance grid
lambda_nn_grid=[0.0, 0.1, 1.0], # Nuclear norm grid
n_bootstrap=200
)
results = trop_est.fit(
panel_data,
outcome='gdp_growth',
treatment='treated',
unit='state',
time='year',
post_periods=[2015, 2016, 2017, 2018]
)

# View results
results.print_summary()
print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
print(f"Effective rank: {results.effective_rank:.2f}")

# Selected tuning parameters
print(f"λ_time: {results.lambda_time:.2f}")
print(f"λ_unit: {results.lambda_unit:.2f}")
print(f"λ_nn: {results.lambda_nn:.2f}")

# Examine unit effects
unit_effects = results.get_unit_effects_df()
print(unit_effects.head(10))
```

Output:
```
===========================================================================
Triply Robust Panel (TROP) Estimation Results
Athey, Imbens, Qu & Viviano (2025)
===========================================================================

Observations: 500
Treated units: 1
Control units: 49
Treated observations: 4
Pre-treatment periods: 6
Post-treatment periods: 4

---------------------------------------------------------------------------
Tuning Parameters (selected via LOOCV)
---------------------------------------------------------------------------
Lambda (time decay): 1.0000
Lambda (unit distance): 0.5000
Lambda (nuclear norm): 0.1000
Effective rank: 2.35
LOOCV score: 0.012345
Variance method: bootstrap
Bootstrap replications: 200

---------------------------------------------------------------------------
Parameter Estimate Std. Err. t-stat P>|t|
---------------------------------------------------------------------------
ATT 2.5000 0.3892 6.424 0.0000 ***
---------------------------------------------------------------------------

95% Confidence Interval: [1.7372, 3.2628]

Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
===========================================================================
```

#### When to Use TROP Over Synthetic DiD

Use TROP when you suspect **factor structure** in the data—unobserved confounders that affect outcomes differently across units and time:

| Scenario | Use SDID | Use TROP |
|----------|----------|----------|
| Simple parallel trends | ✓ | ✓ |
| Unobserved factors (e.g., economic cycles) | May be biased | ✓ |
| Strong unit-time interactions | May be biased | ✓ |
| Low-dimensional confounding | ✓ | ✓ |

**Example scenarios where TROP excels:**
- Regional economic shocks that affect states differently based on industry composition
- Global trends that impact countries differently based on their economic structure
- Common factors in financial data (market risk, interest rates, etc.)

**How TROP works:**

1. **Factor estimation**: Estimates interactive fixed effects L_it using nuclear norm regularization (encourages low-rank structure)
2. **Unit weights**: Exponential distance-based weighting ω_j = exp(-λ_unit × d(j,i)) where d(j,i) is the RMSE of outcome differences
3. **Time weights**: Exponential decay weighting θ_s = exp(-λ_time × |s-t|) based on proximity to treatment
4. **ATT computation**: τ = Y_it - α_i - β_t - L_it for treated observations

```python
# Compare TROP vs SDID under factor confounding
from diff_diff import SyntheticDiD

# Synthetic DiD (may be biased with factors)
sdid = SyntheticDiD()
sdid_results = sdid.fit(data, outcome='y', treatment='treated',
unit='unit', time='time', post_periods=[5,6,7])

# TROP (accounts for factors)
trop_est = TROP() # Uses default grids with LOOCV selection
trop_results = trop_est.fit(data, outcome='y', treatment='treated',
unit='unit', time='time', post_periods=[5,6,7])

print(f"SDID estimate: {sdid_results.att:.3f}")
print(f"TROP estimate: {trop_results.att:.3f}")
print(f"Effective rank: {trop_results.effective_rank:.2f}")
```

**Tuning parameter grids:**

```python
# Custom tuning grids (searched via LOOCV)
trop = TROP(
lambda_time_grid=[0.0, 0.1, 0.5, 1.0, 2.0, 5.0], # Time decay
lambda_unit_grid=[0.0, 0.1, 0.5, 1.0, 2.0, 5.0], # Unit distance
lambda_nn_grid=[0.0, 0.01, 0.1, 1.0, 10.0] # Nuclear norm
)

# Fixed tuning parameters (skip LOOCV search)
trop = TROP(
lambda_time_grid=[1.0], # Single value = fixed
lambda_unit_grid=[1.0], # Single value = fixed
lambda_nn_grid=[0.1] # Single value = fixed
)
```

**Parameters:**

```python
TROP(
lambda_time_grid=None, # Time decay grid (default: [0, 0.1, 0.5, 1, 2, 5])
lambda_unit_grid=None, # Unit distance grid (default: [0, 0.1, 0.5, 1, 2, 5])
lambda_nn_grid=None, # Nuclear norm grid (default: [0, 0.01, 0.1, 1, 10])
max_iter=100, # Max iterations for factor estimation
tol=1e-6, # Convergence tolerance
alpha=0.05, # Significance level
variance_method='bootstrap', # 'bootstrap' or 'jackknife'
n_bootstrap=200, # Bootstrap replications
seed=None # Random seed
)
```

**Convenience function:**

```python
# One-liner estimation with default tuning grids
results = trop(
data,
outcome='y',
treatment='treated',
unit='unit',
time='time',
post_periods=[5, 6, 7],
n_bootstrap=200
)
```

## Working with Results

### Export Results
Expand Down Expand Up @@ -1680,6 +1855,74 @@ SyntheticDiD(
| `get_unit_weights_df()` | Get unit weights as DataFrame |
| `get_time_weights_df()` | Get time weights as DataFrame |

### TROP

```python
TROP(
lambda_time_grid=None, # Time decay grid (default: [0, 0.1, 0.5, 1, 2, 5])
lambda_unit_grid=None, # Unit distance grid (default: [0, 0.1, 0.5, 1, 2, 5])
lambda_nn_grid=None, # Nuclear norm grid (default: [0, 0.01, 0.1, 1, 10])
max_iter=100, # Max iterations for factor estimation
tol=1e-6, # Convergence tolerance
alpha=0.05, # Significance level for CIs
variance_method='bootstrap', # 'bootstrap' or 'jackknife'
n_bootstrap=200, # Bootstrap/jackknife iterations
seed=None # Random seed
)
```

**fit() Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `data` | DataFrame | Panel data |
| `outcome` | str | Outcome variable column name |
| `treatment` | str | Treatment indicator column (0/1) |
| `unit` | str | Unit identifier column |
| `time` | str | Time period column |
| `post_periods` | list | List of post-treatment period values |

### TROPResults

**Attributes:**

| Attribute | Description |
|-----------|-------------|
| `att` | Average Treatment effect on the Treated |
| `se` | Standard error (bootstrap or jackknife) |
| `t_stat` | T-statistic |
| `p_value` | P-value |
| `conf_int` | Confidence interval |
| `n_obs` | Number of observations |
| `n_treated` | Number of treated units |
| `n_control` | Number of control units |
| `n_treated_obs` | Number of treated unit-time observations |
| `unit_effects` | Dict mapping unit IDs to fixed effects |
| `time_effects` | Dict mapping periods to fixed effects |
| `treatment_effects` | Dict mapping (unit, time) to individual effects |
| `lambda_time` | Selected time decay parameter |
| `lambda_unit` | Selected unit distance parameter |
| `lambda_nn` | Selected nuclear norm parameter |
| `factor_matrix` | Low-rank factor matrix L (n_periods x n_units) |
| `effective_rank` | Effective rank of factor matrix |
| `loocv_score` | LOOCV score for selected parameters |
| `pre_periods` | List of pre-treatment periods |
| `post_periods` | List of post-treatment periods |
| `variance_method` | Variance estimation method |
| `bootstrap_distribution` | Bootstrap distribution (if bootstrap) |

**Methods:**

| Method | Description |
|--------|-------------|
| `summary(alpha)` | Get formatted summary string |
| `print_summary(alpha)` | Print summary to stdout |
| `to_dict()` | Convert to dictionary |
| `to_dataframe()` | Convert to pandas DataFrame |
| `get_unit_effects_df()` | Get unit fixed effects as DataFrame |
| `get_time_effects_df()` | Get time fixed effects as DataFrame |
| `get_treatment_effects_df()` | Get individual treatment effects as DataFrame |

### SunAbraham

```python
Expand Down Expand Up @@ -2154,6 +2397,17 @@ This library implements methods from the following scholarly works:

- **Arkhangelsky, D., Athey, S., Hirshberg, D. A., Imbens, G. W., & Wager, S. (2021).** "Synthetic Difference-in-Differences." *American Economic Review*, 111(12), 4088-4118. [https://doi.org/10.1257/aer.20190159](https://doi.org/10.1257/aer.20190159)

### Triply Robust Panel (TROP)

- **Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2025).** "Triply Robust Panel Estimators." *Working Paper*. [https://arxiv.org/abs/2508.21536](https://arxiv.org/abs/2508.21536)

This paper introduces the TROP estimator which combines three robustness components:
- **Factor model adjustment**: Low-rank factor structure via SVD removes unobserved confounders
- **Unit weights**: Synthetic control style weighting for optimal comparison
- **Time weights**: SDID style time weighting for informative pre-periods

TROP is particularly useful when there are unobserved time-varying confounders with a factor structure that affect different units differently over time.

### Triple Difference (DDD)

- **Ortiz-Villavicencio, M., & Sant'Anna, P. H. C. (2025).** "Better Understanding Triple Differences Estimators." *Working Paper*. [https://arxiv.org/abs/2505.09942](https://arxiv.org/abs/2505.09942)
Expand Down
Binary file added TROP-ref/2508.21536v2.pdf
Binary file not shown.
10 changes: 9 additions & 1 deletion diff_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@
TripleDifferenceResults,
triple_difference,
)
from diff_diff.trop import (
TROP,
TROPResults,
trop,
)
from diff_diff.utils import (
WildBootstrapResults,
check_parallel_trends,
Expand All @@ -126,7 +131,7 @@
load_mpdta,
)

__version__ = "2.0.4"
__version__ = "2.1.0"
__all__ = [
# Estimators
"DifferenceInDifferences",
Expand All @@ -136,6 +141,7 @@
"CallawaySantAnna",
"SunAbraham",
"TripleDifference",
"TROP",
# Bacon Decomposition
"BaconDecomposition",
"BaconDecompositionResults",
Expand All @@ -154,6 +160,8 @@
"SABootstrapResults",
"TripleDifferenceResults",
"triple_difference",
"TROPResults",
"trop",
# Visualization
"plot_event_study",
"plot_group_effects",
Expand Down
Loading