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.
Files changed (30) hide show
  1. {diff_diff-2.0.4 → diff_diff-2.1.1}/PKG-INFO +255 -1
  2. {diff_diff-2.0.4 → diff_diff-2.1.1}/README.md +254 -0
  3. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/__init__.py +9 -1
  4. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/_backend.py +16 -0
  5. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/pretrends.py +104 -11
  6. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/staggered.py +4 -0
  7. diff_diff-2.1.1/diff_diff/trop.py +1703 -0
  8. {diff_diff-2.0.4 → diff_diff-2.1.1}/pyproject.toml +1 -1
  9. {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/Cargo.lock +17 -92
  10. {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/Cargo.toml +1 -1
  11. {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/src/lib.rs +6 -0
  12. diff_diff-2.1.1/rust/src/trop.rs +861 -0
  13. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/bacon.py +0 -0
  14. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/datasets.py +0 -0
  15. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/diagnostics.py +0 -0
  16. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/estimators.py +0 -0
  17. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/honest_did.py +0 -0
  18. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/linalg.py +0 -0
  19. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/power.py +0 -0
  20. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/prep.py +0 -0
  21. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/results.py +0 -0
  22. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/sun_abraham.py +0 -0
  23. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/synthetic_did.py +0 -0
  24. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/triple_diff.py +0 -0
  25. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/twfe.py +0 -0
  26. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/utils.py +0 -0
  27. {diff_diff-2.0.4 → diff_diff-2.1.1}/diff_diff/visualization.py +0 -0
  28. {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/src/bootstrap.rs +0 -0
  29. {diff_diff-2.0.4 → diff_diff-2.1.1}/rust/src/linalg.rs +0 -0
  30. {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.0.4
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.0.4"
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
  ]