diff-diff 1.0.0__tar.gz → 1.2.0__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 (36) hide show
  1. {diff_diff-1.0.0 → diff_diff-1.2.0}/PKG-INFO +332 -6
  2. {diff_diff-1.0.0 → diff_diff-1.2.0}/README.md +331 -5
  3. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/__init__.py +24 -1
  4. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/diagnostics.py +62 -23
  5. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/estimators.py +77 -875
  6. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/honest_did.py +4 -2
  7. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/power.py +1 -1
  8. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/prep.py +1 -1
  9. diff_diff-1.2.0/diff_diff/pretrends.py +1067 -0
  10. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/staggered.py +52 -21
  11. diff_diff-1.2.0/diff_diff/sun_abraham.py +1198 -0
  12. diff_diff-1.2.0/diff_diff/synthetic_did.py +564 -0
  13. diff_diff-1.2.0/diff_diff/twfe.py +344 -0
  14. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/utils.py +1 -1
  15. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/visualization.py +240 -1
  16. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff.egg-info/PKG-INFO +332 -6
  17. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff.egg-info/SOURCES.txt +6 -0
  18. {diff_diff-1.0.0 → diff_diff-1.2.0}/pyproject.toml +1 -1
  19. diff_diff-1.2.0/tests/test_pretrends.py +813 -0
  20. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_staggered.py +9 -3
  21. diff_diff-1.2.0/tests/test_sun_abraham.py +732 -0
  22. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/bacon.py +0 -0
  23. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff/results.py +0 -0
  24. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff.egg-info/dependency_links.txt +0 -0
  25. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff.egg-info/requires.txt +0 -0
  26. {diff_diff-1.0.0 → diff_diff-1.2.0}/diff_diff.egg-info/top_level.txt +0 -0
  27. {diff_diff-1.0.0 → diff_diff-1.2.0}/setup.cfg +0 -0
  28. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_bacon.py +0 -0
  29. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_diagnostics.py +0 -0
  30. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_estimators.py +0 -0
  31. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_honest_did.py +0 -0
  32. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_power.py +0 -0
  33. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_prep.py +0 -0
  34. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_utils.py +0 -0
  35. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_visualization.py +0 -0
  36. {diff_diff-1.0.0 → diff_diff-1.2.0}/tests/test_wild_bootstrap.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: A library for Difference-in-Differences causal inference analysis
5
5
  Author: diff-diff contributors
6
6
  License-Expression: MIT
@@ -105,13 +105,14 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
105
105
  - **Wild cluster bootstrap**: Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights
106
106
  - **Panel data support**: Two-way fixed effects estimator for panel designs
107
107
  - **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
108
- - **Staggered adoption**: Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
108
+ - **Staggered adoption**: Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
109
109
  - **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
110
110
  - **Event study plots**: Publication-ready visualization of treatment effects
111
111
  - **Parallel trends testing**: Multiple methods including equivalence tests
112
112
  - **Goodman-Bacon decomposition**: Diagnose TWFE bias by decomposing into 2x2 comparisons
113
113
  - **Placebo tests**: Comprehensive diagnostics including fake timing, fake group, permutation, and leave-one-out tests
114
114
  - **Honest DiD sensitivity analysis**: Rambachan-Roth (2023) bounds and breakdown analysis for parallel trends violations
115
+ - **Pre-trends power analysis**: Roth (2022) minimum detectable violation (MDV) and power curves for pre-trends tests
115
116
  - **Power analysis**: MDE, sample size, and power calculations for study design; simulation-based power for any estimator
116
117
  - **Data prep utilities**: Helper functions for common data preparation tasks
117
118
 
@@ -122,7 +123,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
122
123
  | Notebook | Description |
123
124
  |----------|-------------|
124
125
  | `01_basic_did.ipynb` | Basic 2x2 DiD, formula interface, covariates, fixed effects, cluster-robust SE, wild bootstrap |
125
- | `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna, group-time effects, aggregation methods, Bacon decomposition |
126
+ | `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna and Sun-Abraham, group-time effects, aggregation methods, Bacon decomposition |
126
127
  | `03_synthetic_did.ipynb` | Synthetic DiD, unit/time weights, inference methods, regularization |
127
128
  | `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
128
129
  | `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
@@ -574,7 +575,7 @@ Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
574
575
  ### Two-Way Fixed Effects (Panel Data)
575
576
 
576
577
  ```python
577
- from diff_diff.estimators import TwoWayFixedEffects
578
+ from diff_diff import TwoWayFixedEffects
578
579
 
579
580
  twfe = TwoWayFixedEffects()
580
581
  results = twfe.fit(
@@ -797,12 +798,115 @@ results = cs.fit(
797
798
  )
798
799
  ```
799
800
 
801
+ ### Sun-Abraham Interaction-Weighted Estimator
802
+
803
+ The Sun-Abraham (2021) estimator provides an alternative to Callaway-Sant'Anna using an interaction-weighted (IW) regression approach. Running both estimators serves as a useful robustness check—when they agree, results are more credible.
804
+
805
+ ```python
806
+ from diff_diff import SunAbraham
807
+
808
+ # Basic usage
809
+ sa = SunAbraham()
810
+ results = sa.fit(
811
+ panel_data,
812
+ outcome='sales',
813
+ unit='firm_id',
814
+ time='year',
815
+ first_treat='first_treat' # 0 for never-treated, else first treatment year
816
+ )
817
+
818
+ # View results
819
+ results.print_summary()
820
+
821
+ # Event study effects (by relative time to treatment)
822
+ for rel_time, effect in results.event_study_effects.items():
823
+ print(f"e={rel_time}: {effect['effect']:.3f} (SE: {effect['se']:.3f})")
824
+
825
+ # Overall ATT
826
+ print(f"Overall ATT: {results.overall_att:.3f} (SE: {results.overall_se:.3f})")
827
+
828
+ # Cohort weights (how each cohort contributes to each event-time estimate)
829
+ for rel_time, weights in results.cohort_weights.items():
830
+ print(f"e={rel_time}: {weights}")
831
+ ```
832
+
833
+ **Parameters:**
834
+
835
+ ```python
836
+ SunAbraham(
837
+ control_group='never_treated', # or 'not_yet_treated'
838
+ anticipation=0, # Periods before treatment with effects
839
+ alpha=0.05, # Significance level
840
+ cluster=None, # Column for cluster SEs
841
+ n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
842
+ bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
843
+ seed=None # Random seed
844
+ )
845
+ ```
846
+
847
+ **Bootstrap inference:**
848
+
849
+ ```python
850
+ # Bootstrap inference with 999 iterations
851
+ sa = SunAbraham(
852
+ n_bootstrap=999,
853
+ bootstrap_weights='rademacher',
854
+ seed=42
855
+ )
856
+ results = sa.fit(
857
+ data,
858
+ outcome='sales',
859
+ unit='firm_id',
860
+ time='year',
861
+ first_treat='first_treat'
862
+ )
863
+
864
+ # Access bootstrap results
865
+ print(f"Overall ATT: {results.overall_att:.3f}")
866
+ print(f"Bootstrap SE: {results.bootstrap_results.overall_att_se:.3f}")
867
+ print(f"Bootstrap 95% CI: {results.bootstrap_results.overall_att_ci}")
868
+ print(f"Bootstrap p-value: {results.bootstrap_results.overall_att_p_value:.4f}")
869
+ ```
870
+
871
+ **When to use Sun-Abraham vs Callaway-Sant'Anna:**
872
+
873
+ | Aspect | Sun-Abraham | Callaway-Sant'Anna |
874
+ |--------|-------------|-------------------|
875
+ | Approach | Interaction-weighted regression | 2x2 DiD aggregation |
876
+ | Efficiency | More efficient under homogeneous effects | More robust to heterogeneity |
877
+ | Weighting | Weights by cohort share at each relative time | Weights by sample size |
878
+ | Use case | Robustness check, regression-based inference | Primary staggered DiD estimator |
879
+
880
+ **Both estimators should give similar results when:**
881
+ - Treatment effects are relatively homogeneous across cohorts
882
+ - Parallel trends holds
883
+
884
+ **Running both as robustness check:**
885
+
886
+ ```python
887
+ from diff_diff import CallawaySantAnna, SunAbraham
888
+
889
+ # Callaway-Sant'Anna
890
+ cs = CallawaySantAnna()
891
+ cs_results = cs.fit(data, outcome='y', unit='unit', time='time', first_treat='first_treat')
892
+
893
+ # Sun-Abraham
894
+ sa = SunAbraham()
895
+ sa_results = sa.fit(data, outcome='y', unit='unit', time='time', first_treat='first_treat')
896
+
897
+ # Compare
898
+ print(f"Callaway-Sant'Anna ATT: {cs_results.overall_att:.3f}")
899
+ print(f"Sun-Abraham ATT: {sa_results.overall_att:.3f}")
900
+
901
+ # If results differ substantially, investigate heterogeneity
902
+ ```
903
+
800
904
  ### Event Study Visualization
801
905
 
802
906
  Create publication-ready event study plots:
803
907
 
804
908
  ```python
805
- from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
909
+ from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham
806
910
 
807
911
  # From MultiPeriodDiD
808
912
  did = MultiPeriodDiD()
@@ -814,7 +918,13 @@ plot_event_study(results, title="Treatment Effects Over Time")
814
918
  cs = CallawaySantAnna()
815
919
  results = cs.fit(data, outcome='y', unit='unit', time='period',
816
920
  first_treat='first_treat', aggregate='event_study')
817
- plot_event_study(results, title="Staggered DiD Event Study")
921
+ plot_event_study(results, title="Staggered DiD Event Study (CS)")
922
+
923
+ # From SunAbraham
924
+ sa = SunAbraham()
925
+ results = sa.fit(data, outcome='y', unit='unit', time='period',
926
+ first_treat='first_treat')
927
+ plot_event_study(results, title="Staggered DiD Event Study (SA)")
818
928
 
819
929
  # From a DataFrame
820
930
  df = pd.DataFrame({
@@ -1147,6 +1257,90 @@ plot_sensitivity(sensitivity, title="Sensitivity to Parallel Trends Violations")
1147
1257
  plot_honest_event_study(event_results, honest_results)
1148
1258
  ```
1149
1259
 
1260
+ ### Pre-Trends Power Analysis (Roth 2022)
1261
+
1262
+ A passing pre-trends test doesn't mean parallel trends holds—it may just mean the test has low power. **Pre-Trends Power Analysis** (Roth 2022) answers: "What violations could my pre-trends test have detected?"
1263
+
1264
+ ```python
1265
+ from diff_diff import PreTrendsPower, MultiPeriodDiD
1266
+
1267
+ # First, fit an event study
1268
+ did = MultiPeriodDiD()
1269
+ event_results = did.fit(
1270
+ data,
1271
+ outcome='outcome',
1272
+ treatment='treated',
1273
+ time='period',
1274
+ post_periods=[5, 6, 7, 8, 9]
1275
+ )
1276
+
1277
+ # Analyze pre-trends test power
1278
+ pt = PreTrendsPower(alpha=0.05, power=0.80)
1279
+ power_results = pt.fit(event_results)
1280
+
1281
+ print(power_results.summary())
1282
+ print(f"Minimum Detectable Violation (MDV): {power_results.mdv:.4f}")
1283
+ print(f"Power to detect violations of size MDV: {power_results.power:.1%}")
1284
+ ```
1285
+
1286
+ **Key concepts:**
1287
+
1288
+ - **Minimum Detectable Violation (MDV)**: Smallest violation magnitude that would be detected with your target power (e.g., 80%). Passing the pre-trends test does NOT rule out violations up to this size.
1289
+ - **Power**: Probability of detecting a violation of given size if it exists.
1290
+ - **Violation types**: Linear trend, constant violation, last-period only, or custom patterns.
1291
+
1292
+ **Power curve visualization:**
1293
+
1294
+ ```python
1295
+ from diff_diff import plot_pretrends_power
1296
+
1297
+ # Generate power curve across violation magnitudes
1298
+ curve = pt.power_curve(event_results)
1299
+
1300
+ # Plot the power curve
1301
+ plot_pretrends_power(curve, title="Pre-Trends Test Power Curve")
1302
+
1303
+ # Or from the curve object directly
1304
+ curve.plot()
1305
+ ```
1306
+
1307
+ **Different violation patterns:**
1308
+
1309
+ ```python
1310
+ # Linear trend violations (default) - most common assumption
1311
+ pt_linear = PreTrendsPower(violation_type='linear')
1312
+
1313
+ # Constant violation in all pre-periods
1314
+ pt_constant = PreTrendsPower(violation_type='constant')
1315
+
1316
+ # Violation only in the last pre-period (sharp break)
1317
+ pt_last = PreTrendsPower(violation_type='last_period')
1318
+
1319
+ # Custom violation pattern
1320
+ custom_weights = np.array([0.1, 0.3, 0.6]) # Increasing violations
1321
+ pt_custom = PreTrendsPower(violation_type='custom', violation_weights=custom_weights)
1322
+ ```
1323
+
1324
+ **Combining with HonestDiD:**
1325
+
1326
+ Pre-trends power analysis and HonestDiD are complementary:
1327
+ 1. **Pre-trends power** tells you what the test could have detected
1328
+ 2. **HonestDiD** tells you how robust your results are to violations
1329
+
1330
+ ```python
1331
+ from diff_diff import HonestDiD, PreTrendsPower
1332
+
1333
+ # If MDV is large relative to your estimated effect, be cautious
1334
+ pt = PreTrendsPower()
1335
+ power_results = pt.fit(event_results)
1336
+ sensitivity = pt.sensitivity_to_honest_did(event_results)
1337
+ print(sensitivity['interpretation'])
1338
+
1339
+ # Use HonestDiD for robust inference
1340
+ honest = HonestDiD(method='relative_magnitude', M=1.0)
1341
+ honest_results = honest.fit(event_results)
1342
+ ```
1343
+
1150
1344
  ### Placebo Tests
1151
1345
 
1152
1346
  Placebo tests help validate the parallel trends assumption by checking whether effects appear where they shouldn't (before treatment or in untreated groups).
@@ -1445,6 +1639,63 @@ SyntheticDiD(
1445
1639
  | `get_unit_weights_df()` | Get unit weights as DataFrame |
1446
1640
  | `get_time_weights_df()` | Get time weights as DataFrame |
1447
1641
 
1642
+ ### SunAbraham
1643
+
1644
+ ```python
1645
+ SunAbraham(
1646
+ control_group='never_treated', # or 'not_yet_treated'
1647
+ anticipation=0, # Periods of anticipation effects
1648
+ alpha=0.05, # Significance level for CIs
1649
+ cluster=None, # Column for cluster-robust SEs
1650
+ n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
1651
+ bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
1652
+ seed=None # Random seed
1653
+ )
1654
+ ```
1655
+
1656
+ **fit() Parameters:**
1657
+
1658
+ | Parameter | Type | Description |
1659
+ |-----------|------|-------------|
1660
+ | `data` | DataFrame | Panel data |
1661
+ | `outcome` | str | Outcome variable column name |
1662
+ | `unit` | str | Unit identifier column |
1663
+ | `time` | str | Time period column |
1664
+ | `first_treat` | str | Column with first treatment period (0 for never-treated) |
1665
+ | `covariates` | list | Covariate column names |
1666
+ | `min_pre_periods` | int | Minimum pre-treatment periods to include |
1667
+ | `min_post_periods` | int | Minimum post-treatment periods to include |
1668
+
1669
+ ### SunAbrahamResults
1670
+
1671
+ **Attributes:**
1672
+
1673
+ | Attribute | Description |
1674
+ |-----------|-------------|
1675
+ | `event_study_effects` | Dict mapping relative time to effect info |
1676
+ | `overall_att` | Overall average treatment effect |
1677
+ | `overall_se` | Standard error of overall ATT |
1678
+ | `overall_t_stat` | T-statistic for overall ATT |
1679
+ | `overall_p_value` | P-value for overall ATT |
1680
+ | `overall_conf_int` | Confidence interval for overall ATT |
1681
+ | `cohort_weights` | Dict mapping relative time to cohort weights |
1682
+ | `groups` | List of treatment cohorts |
1683
+ | `time_periods` | List of all time periods |
1684
+ | `n_obs` | Total number of observations |
1685
+ | `n_treated_units` | Number of ever-treated units |
1686
+ | `n_control_units` | Number of never-treated units |
1687
+ | `is_significant` | Boolean for significance at alpha |
1688
+ | `significance_stars` | String of significance stars |
1689
+ | `bootstrap_results` | SABootstrapResults (if bootstrap enabled) |
1690
+
1691
+ **Methods:**
1692
+
1693
+ | Method | Description |
1694
+ |--------|-------------|
1695
+ | `summary(alpha)` | Get formatted summary string |
1696
+ | `print_summary(alpha)` | Print summary to stdout |
1697
+ | `to_dataframe(level)` | Convert to DataFrame ('event_study' or 'cohort') |
1698
+
1448
1699
  ### HonestDiD
1449
1700
 
1450
1701
  ```python
@@ -1514,6 +1765,81 @@ HonestDiD(
1514
1765
  | `plot(ax)` | Plot sensitivity analysis |
1515
1766
  | `to_dataframe()` | Convert to pandas DataFrame |
1516
1767
 
1768
+ ### PreTrendsPower
1769
+
1770
+ ```python
1771
+ PreTrendsPower(
1772
+ alpha=0.05, # Significance level for pre-trends test
1773
+ power=0.80, # Target power for MDV calculation
1774
+ violation_type='linear', # 'linear', 'constant', 'last_period', 'custom'
1775
+ violation_weights=None # Custom weights (required if violation_type='custom')
1776
+ )
1777
+ ```
1778
+
1779
+ **fit() Parameters:**
1780
+
1781
+ | Parameter | Type | Description |
1782
+ |-----------|------|-------------|
1783
+ | `results` | MultiPeriodDiDResults | Results from event study |
1784
+ | `M` | float | Specific violation magnitude to evaluate |
1785
+
1786
+ **Methods:**
1787
+
1788
+ | Method | Description |
1789
+ |--------|-------------|
1790
+ | `fit(results, M)` | Compute power analysis for given event study |
1791
+ | `power_at(results, M)` | Compute power for specific violation magnitude |
1792
+ | `power_curve(results, M_grid, n_points)` | Compute power across range of M values |
1793
+ | `sensitivity_to_honest_did(results)` | Compare with HonestDiD analysis |
1794
+
1795
+ ### PreTrendsPowerResults
1796
+
1797
+ **Attributes:**
1798
+
1799
+ | Attribute | Description |
1800
+ |-----------|-------------|
1801
+ | `power` | Power to detect the specified violation |
1802
+ | `mdv` | Minimum detectable violation at target power |
1803
+ | `violation_magnitude` | Violation magnitude (M) tested |
1804
+ | `violation_type` | Type of violation pattern |
1805
+ | `alpha` | Significance level |
1806
+ | `target_power` | Target power level |
1807
+ | `n_pre_periods` | Number of pre-treatment periods |
1808
+ | `test_statistic` | Expected test statistic under violation |
1809
+ | `critical_value` | Critical value for pre-trends test |
1810
+ | `noncentrality` | Non-centrality parameter |
1811
+ | `is_informative` | Heuristic check if test is informative |
1812
+ | `power_adequate` | Whether power meets target |
1813
+
1814
+ **Methods:**
1815
+
1816
+ | Method | Description |
1817
+ |--------|-------------|
1818
+ | `summary()` | Get formatted summary string |
1819
+ | `print_summary()` | Print summary to stdout |
1820
+ | `to_dict()` | Convert to dictionary |
1821
+ | `to_dataframe()` | Convert to pandas DataFrame |
1822
+
1823
+ ### PreTrendsPowerCurve
1824
+
1825
+ **Attributes:**
1826
+
1827
+ | Attribute | Description |
1828
+ |-----------|-------------|
1829
+ | `M_values` | Array of violation magnitudes |
1830
+ | `powers` | Array of power values |
1831
+ | `mdv` | Minimum detectable violation |
1832
+ | `alpha` | Significance level |
1833
+ | `target_power` | Target power level |
1834
+ | `violation_type` | Type of violation pattern |
1835
+
1836
+ **Methods:**
1837
+
1838
+ | Method | Description |
1839
+ |--------|-------------|
1840
+ | `plot(ax, show_mdv, show_target)` | Plot power curve |
1841
+ | `to_dataframe()` | Convert to DataFrame with M and power columns |
1842
+
1517
1843
  ### Data Preparation Functions
1518
1844
 
1519
1845
  #### generate_did_data