diff-diff 2.0.4__tar.gz → 2.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diff_diff-2.0.4 → diff_diff-2.1.1}/PKG-INFO +255 -1
- {diff_diff-2.0.4 → diff_diff-2.1.1}/README.md +254 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/__init__.py +9 -1
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/_backend.py +16 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/pretrends.py +104 -11
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/staggered.py +4 -0
- diff_diff-2.1.1/diff_diff/trop.py +1703 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/pyproject.toml +1 -1
- {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/Cargo.lock +17 -92
- {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/Cargo.toml +1 -1
- {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/src/lib.rs +6 -0
- diff_diff-2.1.1/rust/src/trop.rs +861 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/bacon.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/datasets.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/diagnostics.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/estimators.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/honest_did.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/linalg.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/power.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/prep.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/results.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/sun_abraham.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/triple_diff.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/twfe.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/utils.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/visualization.py +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/src/bootstrap.rs +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/src/linalg.rs +0 -0
- {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/src/weights.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diff-diff
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Operating System :: OS Independent
|
|
@@ -108,6 +108,7 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
108
108
|
- **Staggered adoption**: Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
|
|
109
109
|
- **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
|
|
110
110
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
111
|
+
- **Triply Robust Panel (TROP)**: Factor-adjusted DiD with synthetic weights (Athey et al. 2025)
|
|
111
112
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
112
113
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
113
114
|
- **Goodman-Bacon decomposition**: Diagnose TWFE bias by decomposing into 2x2 comparisons
|
|
@@ -133,6 +134,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
133
134
|
| `07_pretrends_power.ipynb` | Pre-trends power analysis (Roth 2022), MDV, power curves |
|
|
134
135
|
| `08_triple_diff.ipynb` | Triple Difference (DDD) estimation with proper covariate handling |
|
|
135
136
|
| `09_real_world_examples.ipynb` | Real-world data examples (Card-Krueger, Castle Doctrine, Divorce Laws) |
|
|
137
|
+
| `10_trop.ipynb` | Triply Robust Panel (TROP) estimation with factor model adjustment |
|
|
136
138
|
|
|
137
139
|
## Data Preparation
|
|
138
140
|
|
|
@@ -1150,6 +1152,179 @@ SyntheticDiD(
|
|
|
1150
1152
|
)
|
|
1151
1153
|
```
|
|
1152
1154
|
|
|
1155
|
+
### Triply Robust Panel (TROP)
|
|
1156
|
+
|
|
1157
|
+
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.
|
|
1158
|
+
|
|
1159
|
+
TROP combines three robustness components:
|
|
1160
|
+
1. **Nuclear norm regularized factor model**: Estimates interactive fixed effects L_it via soft-thresholding
|
|
1161
|
+
2. **Exponential distance-based unit weights**: ω_j = exp(-λ_unit × distance(j,i))
|
|
1162
|
+
3. **Exponential time decay weights**: θ_s = exp(-λ_time × |s-t|)
|
|
1163
|
+
|
|
1164
|
+
Tuning parameters are selected via leave-one-out cross-validation (LOOCV).
|
|
1165
|
+
|
|
1166
|
+
```python
|
|
1167
|
+
from diff_diff import TROP, trop
|
|
1168
|
+
|
|
1169
|
+
# Fit TROP model with automatic tuning via LOOCV
|
|
1170
|
+
trop_est = TROP(
|
|
1171
|
+
lambda_time_grid=[0.0, 0.5, 1.0, 2.0], # Time decay grid
|
|
1172
|
+
lambda_unit_grid=[0.0, 0.5, 1.0, 2.0], # Unit distance grid
|
|
1173
|
+
lambda_nn_grid=[0.0, 0.1, 1.0], # Nuclear norm grid
|
|
1174
|
+
n_bootstrap=200
|
|
1175
|
+
)
|
|
1176
|
+
results = trop_est.fit(
|
|
1177
|
+
panel_data,
|
|
1178
|
+
outcome='gdp_growth',
|
|
1179
|
+
treatment='treated',
|
|
1180
|
+
unit='state',
|
|
1181
|
+
time='year',
|
|
1182
|
+
post_periods=[2015, 2016, 2017, 2018]
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
# View results
|
|
1186
|
+
results.print_summary()
|
|
1187
|
+
print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
|
|
1188
|
+
print(f"Effective rank: {results.effective_rank:.2f}")
|
|
1189
|
+
|
|
1190
|
+
# Selected tuning parameters
|
|
1191
|
+
print(f"λ_time: {results.lambda_time:.2f}")
|
|
1192
|
+
print(f"λ_unit: {results.lambda_unit:.2f}")
|
|
1193
|
+
print(f"λ_nn: {results.lambda_nn:.2f}")
|
|
1194
|
+
|
|
1195
|
+
# Examine unit effects
|
|
1196
|
+
unit_effects = results.get_unit_effects_df()
|
|
1197
|
+
print(unit_effects.head(10))
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
Output:
|
|
1201
|
+
```
|
|
1202
|
+
===========================================================================
|
|
1203
|
+
Triply Robust Panel (TROP) Estimation Results
|
|
1204
|
+
Athey, Imbens, Qu & Viviano (2025)
|
|
1205
|
+
===========================================================================
|
|
1206
|
+
|
|
1207
|
+
Observations: 500
|
|
1208
|
+
Treated units: 1
|
|
1209
|
+
Control units: 49
|
|
1210
|
+
Treated observations: 4
|
|
1211
|
+
Pre-treatment periods: 6
|
|
1212
|
+
Post-treatment periods: 4
|
|
1213
|
+
|
|
1214
|
+
---------------------------------------------------------------------------
|
|
1215
|
+
Tuning Parameters (selected via LOOCV)
|
|
1216
|
+
---------------------------------------------------------------------------
|
|
1217
|
+
Lambda (time decay): 1.0000
|
|
1218
|
+
Lambda (unit distance): 0.5000
|
|
1219
|
+
Lambda (nuclear norm): 0.1000
|
|
1220
|
+
Effective rank: 2.35
|
|
1221
|
+
LOOCV score: 0.012345
|
|
1222
|
+
Variance method: bootstrap
|
|
1223
|
+
Bootstrap replications: 200
|
|
1224
|
+
|
|
1225
|
+
---------------------------------------------------------------------------
|
|
1226
|
+
Parameter Estimate Std. Err. t-stat P>|t|
|
|
1227
|
+
---------------------------------------------------------------------------
|
|
1228
|
+
ATT 2.5000 0.3892 6.424 0.0000 ***
|
|
1229
|
+
---------------------------------------------------------------------------
|
|
1230
|
+
|
|
1231
|
+
95% Confidence Interval: [1.7372, 3.2628]
|
|
1232
|
+
|
|
1233
|
+
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
1234
|
+
===========================================================================
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
#### When to Use TROP Over Synthetic DiD
|
|
1238
|
+
|
|
1239
|
+
Use TROP when you suspect **factor structure** in the data—unobserved confounders that affect outcomes differently across units and time:
|
|
1240
|
+
|
|
1241
|
+
| Scenario | Use SDID | Use TROP |
|
|
1242
|
+
|----------|----------|----------|
|
|
1243
|
+
| Simple parallel trends | ✓ | ✓ |
|
|
1244
|
+
| Unobserved factors (e.g., economic cycles) | May be biased | ✓ |
|
|
1245
|
+
| Strong unit-time interactions | May be biased | ✓ |
|
|
1246
|
+
| Low-dimensional confounding | ✓ | ✓ |
|
|
1247
|
+
|
|
1248
|
+
**Example scenarios where TROP excels:**
|
|
1249
|
+
- Regional economic shocks that affect states differently based on industry composition
|
|
1250
|
+
- Global trends that impact countries differently based on their economic structure
|
|
1251
|
+
- Common factors in financial data (market risk, interest rates, etc.)
|
|
1252
|
+
|
|
1253
|
+
**How TROP works:**
|
|
1254
|
+
|
|
1255
|
+
1. **Factor estimation**: Estimates interactive fixed effects L_it using nuclear norm regularization (encourages low-rank structure)
|
|
1256
|
+
2. **Unit weights**: Exponential distance-based weighting ω_j = exp(-λ_unit × d(j,i)) where d(j,i) is the RMSE of outcome differences
|
|
1257
|
+
3. **Time weights**: Exponential decay weighting θ_s = exp(-λ_time × |s-t|) based on proximity to treatment
|
|
1258
|
+
4. **ATT computation**: τ = Y_it - α_i - β_t - L_it for treated observations
|
|
1259
|
+
|
|
1260
|
+
```python
|
|
1261
|
+
# Compare TROP vs SDID under factor confounding
|
|
1262
|
+
from diff_diff import SyntheticDiD
|
|
1263
|
+
|
|
1264
|
+
# Synthetic DiD (may be biased with factors)
|
|
1265
|
+
sdid = SyntheticDiD()
|
|
1266
|
+
sdid_results = sdid.fit(data, outcome='y', treatment='treated',
|
|
1267
|
+
unit='unit', time='time', post_periods=[5,6,7])
|
|
1268
|
+
|
|
1269
|
+
# TROP (accounts for factors)
|
|
1270
|
+
trop_est = TROP() # Uses default grids with LOOCV selection
|
|
1271
|
+
trop_results = trop_est.fit(data, outcome='y', treatment='treated',
|
|
1272
|
+
unit='unit', time='time', post_periods=[5,6,7])
|
|
1273
|
+
|
|
1274
|
+
print(f"SDID estimate: {sdid_results.att:.3f}")
|
|
1275
|
+
print(f"TROP estimate: {trop_results.att:.3f}")
|
|
1276
|
+
print(f"Effective rank: {trop_results.effective_rank:.2f}")
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
**Tuning parameter grids:**
|
|
1280
|
+
|
|
1281
|
+
```python
|
|
1282
|
+
# Custom tuning grids (searched via LOOCV)
|
|
1283
|
+
trop = TROP(
|
|
1284
|
+
lambda_time_grid=[0.0, 0.1, 0.5, 1.0, 2.0, 5.0], # Time decay
|
|
1285
|
+
lambda_unit_grid=[0.0, 0.1, 0.5, 1.0, 2.0, 5.0], # Unit distance
|
|
1286
|
+
lambda_nn_grid=[0.0, 0.01, 0.1, 1.0, 10.0] # Nuclear norm
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
# Fixed tuning parameters (skip LOOCV search)
|
|
1290
|
+
trop = TROP(
|
|
1291
|
+
lambda_time_grid=[1.0], # Single value = fixed
|
|
1292
|
+
lambda_unit_grid=[1.0], # Single value = fixed
|
|
1293
|
+
lambda_nn_grid=[0.1] # Single value = fixed
|
|
1294
|
+
)
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
**Parameters:**
|
|
1298
|
+
|
|
1299
|
+
```python
|
|
1300
|
+
TROP(
|
|
1301
|
+
lambda_time_grid=None, # Time decay grid (default: [0, 0.1, 0.5, 1, 2, 5])
|
|
1302
|
+
lambda_unit_grid=None, # Unit distance grid (default: [0, 0.1, 0.5, 1, 2, 5])
|
|
1303
|
+
lambda_nn_grid=None, # Nuclear norm grid (default: [0, 0.01, 0.1, 1, 10])
|
|
1304
|
+
max_iter=100, # Max iterations for factor estimation
|
|
1305
|
+
tol=1e-6, # Convergence tolerance
|
|
1306
|
+
alpha=0.05, # Significance level
|
|
1307
|
+
variance_method='bootstrap', # 'bootstrap' or 'jackknife'
|
|
1308
|
+
n_bootstrap=200, # Bootstrap replications
|
|
1309
|
+
seed=None # Random seed
|
|
1310
|
+
)
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
**Convenience function:**
|
|
1314
|
+
|
|
1315
|
+
```python
|
|
1316
|
+
# One-liner estimation with default tuning grids
|
|
1317
|
+
results = trop(
|
|
1318
|
+
data,
|
|
1319
|
+
outcome='y',
|
|
1320
|
+
treatment='treated',
|
|
1321
|
+
unit='unit',
|
|
1322
|
+
time='time',
|
|
1323
|
+
post_periods=[5, 6, 7],
|
|
1324
|
+
n_bootstrap=200
|
|
1325
|
+
)
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1153
1328
|
## Working with Results
|
|
1154
1329
|
|
|
1155
1330
|
### Export Results
|
|
@@ -1715,6 +1890,74 @@ SyntheticDiD(
|
|
|
1715
1890
|
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
1716
1891
|
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
1717
1892
|
|
|
1893
|
+
### TROP
|
|
1894
|
+
|
|
1895
|
+
```python
|
|
1896
|
+
TROP(
|
|
1897
|
+
lambda_time_grid=None, # Time decay grid (default: [0, 0.1, 0.5, 1, 2, 5])
|
|
1898
|
+
lambda_unit_grid=None, # Unit distance grid (default: [0, 0.1, 0.5, 1, 2, 5])
|
|
1899
|
+
lambda_nn_grid=None, # Nuclear norm grid (default: [0, 0.01, 0.1, 1, 10])
|
|
1900
|
+
max_iter=100, # Max iterations for factor estimation
|
|
1901
|
+
tol=1e-6, # Convergence tolerance
|
|
1902
|
+
alpha=0.05, # Significance level for CIs
|
|
1903
|
+
variance_method='bootstrap', # 'bootstrap' or 'jackknife'
|
|
1904
|
+
n_bootstrap=200, # Bootstrap/jackknife iterations
|
|
1905
|
+
seed=None # Random seed
|
|
1906
|
+
)
|
|
1907
|
+
```
|
|
1908
|
+
|
|
1909
|
+
**fit() Parameters:**
|
|
1910
|
+
|
|
1911
|
+
| Parameter | Type | Description |
|
|
1912
|
+
|-----------|------|-------------|
|
|
1913
|
+
| `data` | DataFrame | Panel data |
|
|
1914
|
+
| `outcome` | str | Outcome variable column name |
|
|
1915
|
+
| `treatment` | str | Treatment indicator column (0/1) |
|
|
1916
|
+
| `unit` | str | Unit identifier column |
|
|
1917
|
+
| `time` | str | Time period column |
|
|
1918
|
+
| `post_periods` | list | List of post-treatment period values |
|
|
1919
|
+
|
|
1920
|
+
### TROPResults
|
|
1921
|
+
|
|
1922
|
+
**Attributes:**
|
|
1923
|
+
|
|
1924
|
+
| Attribute | Description |
|
|
1925
|
+
|-----------|-------------|
|
|
1926
|
+
| `att` | Average Treatment effect on the Treated |
|
|
1927
|
+
| `se` | Standard error (bootstrap or jackknife) |
|
|
1928
|
+
| `t_stat` | T-statistic |
|
|
1929
|
+
| `p_value` | P-value |
|
|
1930
|
+
| `conf_int` | Confidence interval |
|
|
1931
|
+
| `n_obs` | Number of observations |
|
|
1932
|
+
| `n_treated` | Number of treated units |
|
|
1933
|
+
| `n_control` | Number of control units |
|
|
1934
|
+
| `n_treated_obs` | Number of treated unit-time observations |
|
|
1935
|
+
| `unit_effects` | Dict mapping unit IDs to fixed effects |
|
|
1936
|
+
| `time_effects` | Dict mapping periods to fixed effects |
|
|
1937
|
+
| `treatment_effects` | Dict mapping (unit, time) to individual effects |
|
|
1938
|
+
| `lambda_time` | Selected time decay parameter |
|
|
1939
|
+
| `lambda_unit` | Selected unit distance parameter |
|
|
1940
|
+
| `lambda_nn` | Selected nuclear norm parameter |
|
|
1941
|
+
| `factor_matrix` | Low-rank factor matrix L (n_periods x n_units) |
|
|
1942
|
+
| `effective_rank` | Effective rank of factor matrix |
|
|
1943
|
+
| `loocv_score` | LOOCV score for selected parameters |
|
|
1944
|
+
| `pre_periods` | List of pre-treatment periods |
|
|
1945
|
+
| `post_periods` | List of post-treatment periods |
|
|
1946
|
+
| `variance_method` | Variance estimation method |
|
|
1947
|
+
| `bootstrap_distribution` | Bootstrap distribution (if bootstrap) |
|
|
1948
|
+
|
|
1949
|
+
**Methods:**
|
|
1950
|
+
|
|
1951
|
+
| Method | Description |
|
|
1952
|
+
|--------|-------------|
|
|
1953
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
1954
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
1955
|
+
| `to_dict()` | Convert to dictionary |
|
|
1956
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
1957
|
+
| `get_unit_effects_df()` | Get unit fixed effects as DataFrame |
|
|
1958
|
+
| `get_time_effects_df()` | Get time fixed effects as DataFrame |
|
|
1959
|
+
| `get_treatment_effects_df()` | Get individual treatment effects as DataFrame |
|
|
1960
|
+
|
|
1718
1961
|
### SunAbraham
|
|
1719
1962
|
|
|
1720
1963
|
```python
|
|
@@ -2189,6 +2432,17 @@ This library implements methods from the following scholarly works:
|
|
|
2189
2432
|
|
|
2190
2433
|
- **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)
|
|
2191
2434
|
|
|
2435
|
+
### Triply Robust Panel (TROP)
|
|
2436
|
+
|
|
2437
|
+
- **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)
|
|
2438
|
+
|
|
2439
|
+
This paper introduces the TROP estimator which combines three robustness components:
|
|
2440
|
+
- **Factor model adjustment**: Low-rank factor structure via SVD removes unobserved confounders
|
|
2441
|
+
- **Unit weights**: Synthetic control style weighting for optimal comparison
|
|
2442
|
+
- **Time weights**: SDID style time weighting for informative pre-periods
|
|
2443
|
+
|
|
2444
|
+
TROP is particularly useful when there are unobserved time-varying confounders with a factor structure that affect different units differently over time.
|
|
2445
|
+
|
|
2192
2446
|
### Triple Difference (DDD)
|
|
2193
2447
|
|
|
2194
2448
|
- **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)
|
|
@@ -73,6 +73,7 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
73
73
|
- **Staggered adoption**: Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
|
|
74
74
|
- **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
|
|
75
75
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
76
|
+
- **Triply Robust Panel (TROP)**: Factor-adjusted DiD with synthetic weights (Athey et al. 2025)
|
|
76
77
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
77
78
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
78
79
|
- **Goodman-Bacon decomposition**: Diagnose TWFE bias by decomposing into 2x2 comparisons
|
|
@@ -98,6 +99,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
98
99
|
| `07_pretrends_power.ipynb` | Pre-trends power analysis (Roth 2022), MDV, power curves |
|
|
99
100
|
| `08_triple_diff.ipynb` | Triple Difference (DDD) estimation with proper covariate handling |
|
|
100
101
|
| `09_real_world_examples.ipynb` | Real-world data examples (Card-Krueger, Castle Doctrine, Divorce Laws) |
|
|
102
|
+
| `10_trop.ipynb` | Triply Robust Panel (TROP) estimation with factor model adjustment |
|
|
101
103
|
|
|
102
104
|
## Data Preparation
|
|
103
105
|
|
|
@@ -1115,6 +1117,179 @@ SyntheticDiD(
|
|
|
1115
1117
|
)
|
|
1116
1118
|
```
|
|
1117
1119
|
|
|
1120
|
+
### Triply Robust Panel (TROP)
|
|
1121
|
+
|
|
1122
|
+
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.
|
|
1123
|
+
|
|
1124
|
+
TROP combines three robustness components:
|
|
1125
|
+
1. **Nuclear norm regularized factor model**: Estimates interactive fixed effects L_it via soft-thresholding
|
|
1126
|
+
2. **Exponential distance-based unit weights**: ω_j = exp(-λ_unit × distance(j,i))
|
|
1127
|
+
3. **Exponential time decay weights**: θ_s = exp(-λ_time × |s-t|)
|
|
1128
|
+
|
|
1129
|
+
Tuning parameters are selected via leave-one-out cross-validation (LOOCV).
|
|
1130
|
+
|
|
1131
|
+
```python
|
|
1132
|
+
from diff_diff import TROP, trop
|
|
1133
|
+
|
|
1134
|
+
# Fit TROP model with automatic tuning via LOOCV
|
|
1135
|
+
trop_est = TROP(
|
|
1136
|
+
lambda_time_grid=[0.0, 0.5, 1.0, 2.0], # Time decay grid
|
|
1137
|
+
lambda_unit_grid=[0.0, 0.5, 1.0, 2.0], # Unit distance grid
|
|
1138
|
+
lambda_nn_grid=[0.0, 0.1, 1.0], # Nuclear norm grid
|
|
1139
|
+
n_bootstrap=200
|
|
1140
|
+
)
|
|
1141
|
+
results = trop_est.fit(
|
|
1142
|
+
panel_data,
|
|
1143
|
+
outcome='gdp_growth',
|
|
1144
|
+
treatment='treated',
|
|
1145
|
+
unit='state',
|
|
1146
|
+
time='year',
|
|
1147
|
+
post_periods=[2015, 2016, 2017, 2018]
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
# View results
|
|
1151
|
+
results.print_summary()
|
|
1152
|
+
print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
|
|
1153
|
+
print(f"Effective rank: {results.effective_rank:.2f}")
|
|
1154
|
+
|
|
1155
|
+
# Selected tuning parameters
|
|
1156
|
+
print(f"λ_time: {results.lambda_time:.2f}")
|
|
1157
|
+
print(f"λ_unit: {results.lambda_unit:.2f}")
|
|
1158
|
+
print(f"λ_nn: {results.lambda_nn:.2f}")
|
|
1159
|
+
|
|
1160
|
+
# Examine unit effects
|
|
1161
|
+
unit_effects = results.get_unit_effects_df()
|
|
1162
|
+
print(unit_effects.head(10))
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
Output:
|
|
1166
|
+
```
|
|
1167
|
+
===========================================================================
|
|
1168
|
+
Triply Robust Panel (TROP) Estimation Results
|
|
1169
|
+
Athey, Imbens, Qu & Viviano (2025)
|
|
1170
|
+
===========================================================================
|
|
1171
|
+
|
|
1172
|
+
Observations: 500
|
|
1173
|
+
Treated units: 1
|
|
1174
|
+
Control units: 49
|
|
1175
|
+
Treated observations: 4
|
|
1176
|
+
Pre-treatment periods: 6
|
|
1177
|
+
Post-treatment periods: 4
|
|
1178
|
+
|
|
1179
|
+
---------------------------------------------------------------------------
|
|
1180
|
+
Tuning Parameters (selected via LOOCV)
|
|
1181
|
+
---------------------------------------------------------------------------
|
|
1182
|
+
Lambda (time decay): 1.0000
|
|
1183
|
+
Lambda (unit distance): 0.5000
|
|
1184
|
+
Lambda (nuclear norm): 0.1000
|
|
1185
|
+
Effective rank: 2.35
|
|
1186
|
+
LOOCV score: 0.012345
|
|
1187
|
+
Variance method: bootstrap
|
|
1188
|
+
Bootstrap replications: 200
|
|
1189
|
+
|
|
1190
|
+
---------------------------------------------------------------------------
|
|
1191
|
+
Parameter Estimate Std. Err. t-stat P>|t|
|
|
1192
|
+
---------------------------------------------------------------------------
|
|
1193
|
+
ATT 2.5000 0.3892 6.424 0.0000 ***
|
|
1194
|
+
---------------------------------------------------------------------------
|
|
1195
|
+
|
|
1196
|
+
95% Confidence Interval: [1.7372, 3.2628]
|
|
1197
|
+
|
|
1198
|
+
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
1199
|
+
===========================================================================
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
#### When to Use TROP Over Synthetic DiD
|
|
1203
|
+
|
|
1204
|
+
Use TROP when you suspect **factor structure** in the data—unobserved confounders that affect outcomes differently across units and time:
|
|
1205
|
+
|
|
1206
|
+
| Scenario | Use SDID | Use TROP |
|
|
1207
|
+
|----------|----------|----------|
|
|
1208
|
+
| Simple parallel trends | ✓ | ✓ |
|
|
1209
|
+
| Unobserved factors (e.g., economic cycles) | May be biased | ✓ |
|
|
1210
|
+
| Strong unit-time interactions | May be biased | ✓ |
|
|
1211
|
+
| Low-dimensional confounding | ✓ | ✓ |
|
|
1212
|
+
|
|
1213
|
+
**Example scenarios where TROP excels:**
|
|
1214
|
+
- Regional economic shocks that affect states differently based on industry composition
|
|
1215
|
+
- Global trends that impact countries differently based on their economic structure
|
|
1216
|
+
- Common factors in financial data (market risk, interest rates, etc.)
|
|
1217
|
+
|
|
1218
|
+
**How TROP works:**
|
|
1219
|
+
|
|
1220
|
+
1. **Factor estimation**: Estimates interactive fixed effects L_it using nuclear norm regularization (encourages low-rank structure)
|
|
1221
|
+
2. **Unit weights**: Exponential distance-based weighting ω_j = exp(-λ_unit × d(j,i)) where d(j,i) is the RMSE of outcome differences
|
|
1222
|
+
3. **Time weights**: Exponential decay weighting θ_s = exp(-λ_time × |s-t|) based on proximity to treatment
|
|
1223
|
+
4. **ATT computation**: τ = Y_it - α_i - β_t - L_it for treated observations
|
|
1224
|
+
|
|
1225
|
+
```python
|
|
1226
|
+
# Compare TROP vs SDID under factor confounding
|
|
1227
|
+
from diff_diff import SyntheticDiD
|
|
1228
|
+
|
|
1229
|
+
# Synthetic DiD (may be biased with factors)
|
|
1230
|
+
sdid = SyntheticDiD()
|
|
1231
|
+
sdid_results = sdid.fit(data, outcome='y', treatment='treated',
|
|
1232
|
+
unit='unit', time='time', post_periods=[5,6,7])
|
|
1233
|
+
|
|
1234
|
+
# TROP (accounts for factors)
|
|
1235
|
+
trop_est = TROP() # Uses default grids with LOOCV selection
|
|
1236
|
+
trop_results = trop_est.fit(data, outcome='y', treatment='treated',
|
|
1237
|
+
unit='unit', time='time', post_periods=[5,6,7])
|
|
1238
|
+
|
|
1239
|
+
print(f"SDID estimate: {sdid_results.att:.3f}")
|
|
1240
|
+
print(f"TROP estimate: {trop_results.att:.3f}")
|
|
1241
|
+
print(f"Effective rank: {trop_results.effective_rank:.2f}")
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
**Tuning parameter grids:**
|
|
1245
|
+
|
|
1246
|
+
```python
|
|
1247
|
+
# Custom tuning grids (searched via LOOCV)
|
|
1248
|
+
trop = TROP(
|
|
1249
|
+
lambda_time_grid=[0.0, 0.1, 0.5, 1.0, 2.0, 5.0], # Time decay
|
|
1250
|
+
lambda_unit_grid=[0.0, 0.1, 0.5, 1.0, 2.0, 5.0], # Unit distance
|
|
1251
|
+
lambda_nn_grid=[0.0, 0.01, 0.1, 1.0, 10.0] # Nuclear norm
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
# Fixed tuning parameters (skip LOOCV search)
|
|
1255
|
+
trop = TROP(
|
|
1256
|
+
lambda_time_grid=[1.0], # Single value = fixed
|
|
1257
|
+
lambda_unit_grid=[1.0], # Single value = fixed
|
|
1258
|
+
lambda_nn_grid=[0.1] # Single value = fixed
|
|
1259
|
+
)
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
**Parameters:**
|
|
1263
|
+
|
|
1264
|
+
```python
|
|
1265
|
+
TROP(
|
|
1266
|
+
lambda_time_grid=None, # Time decay grid (default: [0, 0.1, 0.5, 1, 2, 5])
|
|
1267
|
+
lambda_unit_grid=None, # Unit distance grid (default: [0, 0.1, 0.5, 1, 2, 5])
|
|
1268
|
+
lambda_nn_grid=None, # Nuclear norm grid (default: [0, 0.01, 0.1, 1, 10])
|
|
1269
|
+
max_iter=100, # Max iterations for factor estimation
|
|
1270
|
+
tol=1e-6, # Convergence tolerance
|
|
1271
|
+
alpha=0.05, # Significance level
|
|
1272
|
+
variance_method='bootstrap', # 'bootstrap' or 'jackknife'
|
|
1273
|
+
n_bootstrap=200, # Bootstrap replications
|
|
1274
|
+
seed=None # Random seed
|
|
1275
|
+
)
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
**Convenience function:**
|
|
1279
|
+
|
|
1280
|
+
```python
|
|
1281
|
+
# One-liner estimation with default tuning grids
|
|
1282
|
+
results = trop(
|
|
1283
|
+
data,
|
|
1284
|
+
outcome='y',
|
|
1285
|
+
treatment='treated',
|
|
1286
|
+
unit='unit',
|
|
1287
|
+
time='time',
|
|
1288
|
+
post_periods=[5, 6, 7],
|
|
1289
|
+
n_bootstrap=200
|
|
1290
|
+
)
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1118
1293
|
## Working with Results
|
|
1119
1294
|
|
|
1120
1295
|
### Export Results
|
|
@@ -1680,6 +1855,74 @@ SyntheticDiD(
|
|
|
1680
1855
|
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
1681
1856
|
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
1682
1857
|
|
|
1858
|
+
### TROP
|
|
1859
|
+
|
|
1860
|
+
```python
|
|
1861
|
+
TROP(
|
|
1862
|
+
lambda_time_grid=None, # Time decay grid (default: [0, 0.1, 0.5, 1, 2, 5])
|
|
1863
|
+
lambda_unit_grid=None, # Unit distance grid (default: [0, 0.1, 0.5, 1, 2, 5])
|
|
1864
|
+
lambda_nn_grid=None, # Nuclear norm grid (default: [0, 0.01, 0.1, 1, 10])
|
|
1865
|
+
max_iter=100, # Max iterations for factor estimation
|
|
1866
|
+
tol=1e-6, # Convergence tolerance
|
|
1867
|
+
alpha=0.05, # Significance level for CIs
|
|
1868
|
+
variance_method='bootstrap', # 'bootstrap' or 'jackknife'
|
|
1869
|
+
n_bootstrap=200, # Bootstrap/jackknife iterations
|
|
1870
|
+
seed=None # Random seed
|
|
1871
|
+
)
|
|
1872
|
+
```
|
|
1873
|
+
|
|
1874
|
+
**fit() Parameters:**
|
|
1875
|
+
|
|
1876
|
+
| Parameter | Type | Description |
|
|
1877
|
+
|-----------|------|-------------|
|
|
1878
|
+
| `data` | DataFrame | Panel data |
|
|
1879
|
+
| `outcome` | str | Outcome variable column name |
|
|
1880
|
+
| `treatment` | str | Treatment indicator column (0/1) |
|
|
1881
|
+
| `unit` | str | Unit identifier column |
|
|
1882
|
+
| `time` | str | Time period column |
|
|
1883
|
+
| `post_periods` | list | List of post-treatment period values |
|
|
1884
|
+
|
|
1885
|
+
### TROPResults
|
|
1886
|
+
|
|
1887
|
+
**Attributes:**
|
|
1888
|
+
|
|
1889
|
+
| Attribute | Description |
|
|
1890
|
+
|-----------|-------------|
|
|
1891
|
+
| `att` | Average Treatment effect on the Treated |
|
|
1892
|
+
| `se` | Standard error (bootstrap or jackknife) |
|
|
1893
|
+
| `t_stat` | T-statistic |
|
|
1894
|
+
| `p_value` | P-value |
|
|
1895
|
+
| `conf_int` | Confidence interval |
|
|
1896
|
+
| `n_obs` | Number of observations |
|
|
1897
|
+
| `n_treated` | Number of treated units |
|
|
1898
|
+
| `n_control` | Number of control units |
|
|
1899
|
+
| `n_treated_obs` | Number of treated unit-time observations |
|
|
1900
|
+
| `unit_effects` | Dict mapping unit IDs to fixed effects |
|
|
1901
|
+
| `time_effects` | Dict mapping periods to fixed effects |
|
|
1902
|
+
| `treatment_effects` | Dict mapping (unit, time) to individual effects |
|
|
1903
|
+
| `lambda_time` | Selected time decay parameter |
|
|
1904
|
+
| `lambda_unit` | Selected unit distance parameter |
|
|
1905
|
+
| `lambda_nn` | Selected nuclear norm parameter |
|
|
1906
|
+
| `factor_matrix` | Low-rank factor matrix L (n_periods x n_units) |
|
|
1907
|
+
| `effective_rank` | Effective rank of factor matrix |
|
|
1908
|
+
| `loocv_score` | LOOCV score for selected parameters |
|
|
1909
|
+
| `pre_periods` | List of pre-treatment periods |
|
|
1910
|
+
| `post_periods` | List of post-treatment periods |
|
|
1911
|
+
| `variance_method` | Variance estimation method |
|
|
1912
|
+
| `bootstrap_distribution` | Bootstrap distribution (if bootstrap) |
|
|
1913
|
+
|
|
1914
|
+
**Methods:**
|
|
1915
|
+
|
|
1916
|
+
| Method | Description |
|
|
1917
|
+
|--------|-------------|
|
|
1918
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
1919
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
1920
|
+
| `to_dict()` | Convert to dictionary |
|
|
1921
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
1922
|
+
| `get_unit_effects_df()` | Get unit fixed effects as DataFrame |
|
|
1923
|
+
| `get_time_effects_df()` | Get time fixed effects as DataFrame |
|
|
1924
|
+
| `get_treatment_effects_df()` | Get individual treatment effects as DataFrame |
|
|
1925
|
+
|
|
1683
1926
|
### SunAbraham
|
|
1684
1927
|
|
|
1685
1928
|
```python
|
|
@@ -2154,6 +2397,17 @@ This library implements methods from the following scholarly works:
|
|
|
2154
2397
|
|
|
2155
2398
|
- **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)
|
|
2156
2399
|
|
|
2400
|
+
### Triply Robust Panel (TROP)
|
|
2401
|
+
|
|
2402
|
+
- **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)
|
|
2403
|
+
|
|
2404
|
+
This paper introduces the TROP estimator which combines three robustness components:
|
|
2405
|
+
- **Factor model adjustment**: Low-rank factor structure via SVD removes unobserved confounders
|
|
2406
|
+
- **Unit weights**: Synthetic control style weighting for optimal comparison
|
|
2407
|
+
- **Time weights**: SDID style time weighting for informative pre-periods
|
|
2408
|
+
|
|
2409
|
+
TROP is particularly useful when there are unobserved time-varying confounders with a factor structure that affect different units differently over time.
|
|
2410
|
+
|
|
2157
2411
|
### Triple Difference (DDD)
|
|
2158
2412
|
|
|
2159
2413
|
- **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)
|
|
@@ -100,6 +100,11 @@ from diff_diff.triple_diff import (
|
|
|
100
100
|
TripleDifferenceResults,
|
|
101
101
|
triple_difference,
|
|
102
102
|
)
|
|
103
|
+
from diff_diff.trop import (
|
|
104
|
+
TROP,
|
|
105
|
+
TROPResults,
|
|
106
|
+
trop,
|
|
107
|
+
)
|
|
103
108
|
from diff_diff.utils import (
|
|
104
109
|
WildBootstrapResults,
|
|
105
110
|
check_parallel_trends,
|
|
@@ -126,7 +131,7 @@ from diff_diff.datasets import (
|
|
|
126
131
|
load_mpdta,
|
|
127
132
|
)
|
|
128
133
|
|
|
129
|
-
__version__ = "2.
|
|
134
|
+
__version__ = "2.1.1"
|
|
130
135
|
__all__ = [
|
|
131
136
|
# Estimators
|
|
132
137
|
"DifferenceInDifferences",
|
|
@@ -136,6 +141,7 @@ __all__ = [
|
|
|
136
141
|
"CallawaySantAnna",
|
|
137
142
|
"SunAbraham",
|
|
138
143
|
"TripleDifference",
|
|
144
|
+
"TROP",
|
|
139
145
|
# Bacon Decomposition
|
|
140
146
|
"BaconDecomposition",
|
|
141
147
|
"BaconDecompositionResults",
|
|
@@ -154,6 +160,8 @@ __all__ = [
|
|
|
154
160
|
"SABootstrapResults",
|
|
155
161
|
"TripleDifferenceResults",
|
|
156
162
|
"triple_difference",
|
|
163
|
+
"TROPResults",
|
|
164
|
+
"trop",
|
|
157
165
|
# Visualization
|
|
158
166
|
"plot_event_study",
|
|
159
167
|
"plot_group_effects",
|
|
@@ -23,6 +23,10 @@ try:
|
|
|
23
23
|
project_simplex as _rust_project_simplex,
|
|
24
24
|
solve_ols as _rust_solve_ols,
|
|
25
25
|
compute_robust_vcov as _rust_compute_robust_vcov,
|
|
26
|
+
# TROP estimator acceleration
|
|
27
|
+
compute_unit_distance_matrix as _rust_unit_distance_matrix,
|
|
28
|
+
loocv_grid_search as _rust_loocv_grid_search,
|
|
29
|
+
bootstrap_trop_variance as _rust_bootstrap_trop_variance,
|
|
26
30
|
)
|
|
27
31
|
_rust_available = True
|
|
28
32
|
except ImportError:
|
|
@@ -32,6 +36,10 @@ except ImportError:
|
|
|
32
36
|
_rust_project_simplex = None
|
|
33
37
|
_rust_solve_ols = None
|
|
34
38
|
_rust_compute_robust_vcov = None
|
|
39
|
+
# TROP estimator acceleration
|
|
40
|
+
_rust_unit_distance_matrix = None
|
|
41
|
+
_rust_loocv_grid_search = None
|
|
42
|
+
_rust_bootstrap_trop_variance = None
|
|
35
43
|
|
|
36
44
|
# Determine final backend based on environment variable and availability
|
|
37
45
|
if _backend_env == 'python':
|
|
@@ -42,6 +50,10 @@ if _backend_env == 'python':
|
|
|
42
50
|
_rust_project_simplex = None
|
|
43
51
|
_rust_solve_ols = None
|
|
44
52
|
_rust_compute_robust_vcov = None
|
|
53
|
+
# TROP estimator acceleration
|
|
54
|
+
_rust_unit_distance_matrix = None
|
|
55
|
+
_rust_loocv_grid_search = None
|
|
56
|
+
_rust_bootstrap_trop_variance = None
|
|
45
57
|
elif _backend_env == 'rust':
|
|
46
58
|
# Force Rust mode - fail if not available
|
|
47
59
|
if not _rust_available:
|
|
@@ -61,4 +73,8 @@ __all__ = [
|
|
|
61
73
|
'_rust_project_simplex',
|
|
62
74
|
'_rust_solve_ols',
|
|
63
75
|
'_rust_compute_robust_vcov',
|
|
76
|
+
# TROP estimator acceleration
|
|
77
|
+
'_rust_unit_distance_matrix',
|
|
78
|
+
'_rust_loocv_grid_search',
|
|
79
|
+
'_rust_bootstrap_trop_variance',
|
|
64
80
|
]
|