diff-diff 2.2.0__tar.gz → 2.2.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 (34) hide show
  1. {diff_diff-2.2.0 → diff_diff-2.2.1}/PKG-INFO +33 -22
  2. {diff_diff-2.2.0 → diff_diff-2.2.1}/README.md +31 -21
  3. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/__init__.py +1 -1
  4. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/diagnostics.py +3 -3
  5. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/estimators.py +156 -42
  6. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/honest_did.py +158 -147
  7. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/pretrends.py +89 -151
  8. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/results.py +164 -88
  9. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/sun_abraham.py +6 -6
  10. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/triple_diff.py +2 -2
  11. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/trop.py +80 -325
  12. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/visualization.py +206 -213
  13. {diff_diff-2.2.0 → diff_diff-2.2.1}/pyproject.toml +2 -1
  14. {diff_diff-2.2.0 → diff_diff-2.2.1}/rust/Cargo.lock +29 -29
  15. {diff_diff-2.2.0 → diff_diff-2.2.1}/rust/Cargo.toml +1 -1
  16. {diff_diff-2.2.0 → diff_diff-2.2.1}/rust/src/trop.rs +149 -136
  17. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/_backend.py +0 -0
  18. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/bacon.py +0 -0
  19. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/datasets.py +0 -0
  20. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/linalg.py +0 -0
  21. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/power.py +0 -0
  22. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/prep.py +0 -0
  23. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/prep_dgp.py +0 -0
  24. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/staggered.py +0 -0
  25. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/staggered_aggregation.py +0 -0
  26. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/staggered_bootstrap.py +0 -0
  27. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/staggered_results.py +0 -0
  28. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/synthetic_did.py +0 -0
  29. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/twfe.py +0 -0
  30. {diff_diff-2.2.0 → diff_diff-2.2.1}/diff_diff/utils.py +0 -0
  31. {diff_diff-2.2.0 → diff_diff-2.2.1}/rust/src/bootstrap.rs +0 -0
  32. {diff_diff-2.2.0 → diff_diff-2.2.1}/rust/src/lib.rs +0 -0
  33. {diff_diff-2.2.0 → diff_diff-2.2.1}/rust/src/linalg.rs +0 -0
  34. {diff_diff-2.2.0 → diff_diff-2.2.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.2.0
3
+ Version: 2.2.1
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -18,6 +18,7 @@ Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
18
18
  Requires-Dist: black>=23.0 ; extra == 'dev'
19
19
  Requires-Dist: ruff>=0.1.0 ; extra == 'dev'
20
20
  Requires-Dist: mypy>=1.0 ; extra == 'dev'
21
+ Requires-Dist: maturin>=1.4,<2.0 ; extra == 'dev'
21
22
  Requires-Dist: sphinx>=6.0 ; extra == 'docs'
22
23
  Requires-Dist: sphinx-rtd-theme>=1.0 ; extra == 'docs'
23
24
  Provides-Extra: dev
@@ -596,12 +597,13 @@ results = twfe.fit(
596
597
 
597
598
  ### Multi-Period DiD (Event Study)
598
599
 
599
- For settings with multiple pre- and post-treatment periods:
600
+ For settings with multiple pre- and post-treatment periods. Estimates treatment × period
601
+ interactions for ALL periods (pre and post), enabling parallel trends assessment:
600
602
 
601
603
  ```python
602
604
  from diff_diff import MultiPeriodDiD
603
605
 
604
- # Fit with multiple time periods
606
+ # Fit full event study with pre and post period effects
605
607
  did = MultiPeriodDiD()
606
608
  results = did.fit(
607
609
  panel_data,
@@ -609,18 +611,23 @@ results = did.fit(
609
611
  treatment='treated',
610
612
  time='period',
611
613
  post_periods=[3, 4, 5], # Periods 3-5 are post-treatment
612
- reference_period=0 # Reference period for comparison
614
+ reference_period=2, # Last pre-period (e=-1 convention)
615
+ unit='unit_id', # Optional: warns if staggered adoption detected
613
616
  )
614
617
 
615
- # View period-specific treatment effects
616
- for period, effect in results.period_effects.items():
617
- print(f"Period {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
618
+ # Pre-period effects test parallel trends (should be ≈ 0)
619
+ for period, effect in results.pre_period_effects.items():
620
+ print(f"Pre {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
621
+
622
+ # Post-period effects estimate dynamic treatment effects
623
+ for period, effect in results.post_period_effects.items():
624
+ print(f"Post {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
618
625
 
619
626
  # View average treatment effect across post-periods
620
627
  print(f"Average ATT: {results.avg_att:.3f}")
621
628
  print(f"Average SE: {results.avg_se:.3f}")
622
629
 
623
- # Full summary with all period effects
630
+ # Full summary with pre and post period effects
624
631
  results.print_summary()
625
632
  ```
626
633
 
@@ -986,10 +993,10 @@ Create publication-ready event study plots:
986
993
  ```python
987
994
  from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham
988
995
 
989
- # From MultiPeriodDiD
996
+ # From MultiPeriodDiD (full event study with pre and post period effects)
990
997
  did = MultiPeriodDiD()
991
998
  results = did.fit(data, outcome='y', treatment='treated',
992
- time='period', post_periods=[3, 4, 5])
999
+ time='period', post_periods=[3, 4, 5], reference_period=2)
993
1000
  plot_event_study(results, title="Treatment Effects Over Time")
994
1001
 
995
1002
  # From CallawaySantAnna (with event study aggregation)
@@ -1309,7 +1316,6 @@ TROP(
1309
1316
  max_iter=100, # Max iterations for factor estimation
1310
1317
  tol=1e-6, # Convergence tolerance
1311
1318
  alpha=0.05, # Significance level
1312
- variance_method='bootstrap', # 'bootstrap' or 'jackknife'
1313
1319
  n_bootstrap=200, # Bootstrap replications
1314
1320
  seed=None # Random seed
1315
1321
  )
@@ -1449,14 +1455,15 @@ Pre-trends tests have low power and can exacerbate bias. **Honest DiD** (Rambach
1449
1455
  ```python
1450
1456
  from diff_diff import HonestDiD, MultiPeriodDiD
1451
1457
 
1452
- # First, fit a standard event study
1458
+ # First, fit a full event study (pre + post period effects)
1453
1459
  did = MultiPeriodDiD()
1454
1460
  event_results = did.fit(
1455
1461
  data,
1456
1462
  outcome='outcome',
1457
1463
  treatment='treated',
1458
1464
  time='period',
1459
- post_periods=[5, 6, 7, 8, 9]
1465
+ post_periods=[5, 6, 7, 8, 9],
1466
+ reference_period=4, # Last pre-period (e=-1 convention)
1460
1467
  )
1461
1468
 
1462
1469
  # Compute honest bounds with relative magnitudes restriction
@@ -1524,14 +1531,15 @@ A passing pre-trends test doesn't mean parallel trends holds—it may just mean
1524
1531
  ```python
1525
1532
  from diff_diff import PreTrendsPower, MultiPeriodDiD
1526
1533
 
1527
- # First, fit an event study
1534
+ # First, fit a full event study
1528
1535
  did = MultiPeriodDiD()
1529
1536
  event_results = did.fit(
1530
1537
  data,
1531
1538
  outcome='outcome',
1532
1539
  treatment='treated',
1533
1540
  time='period',
1534
- post_periods=[5, 6, 7, 8, 9]
1541
+ post_periods=[5, 6, 7, 8, 9],
1542
+ reference_period=4,
1535
1543
  )
1536
1544
 
1537
1545
  # Analyze pre-trends test power
@@ -1800,7 +1808,8 @@ MultiPeriodDiD(
1800
1808
  | `covariates` | list | Linear control variables |
1801
1809
  | `fixed_effects` | list | Categorical FE columns (creates dummies) |
1802
1810
  | `absorb` | list | High-dimensional FE (within-transformation) |
1803
- | `reference_period` | any | Omitted period for time dummies |
1811
+ | `reference_period` | any | Omitted period (default: last pre-period, e=-1 convention) |
1812
+ | `unit` | str | Unit identifier column (for staggered adoption warning) |
1804
1813
 
1805
1814
  ### MultiPeriodDiDResults
1806
1815
 
@@ -1808,8 +1817,8 @@ MultiPeriodDiD(
1808
1817
 
1809
1818
  | Attribute | Description |
1810
1819
  |-----------|-------------|
1811
- | `period_effects` | Dict mapping periods to PeriodEffect objects |
1812
- | `avg_att` | Average ATT across post-treatment periods |
1820
+ | `period_effects` | Dict mapping periods to PeriodEffect objects (pre and post, excluding reference) |
1821
+ | `avg_att` | Average ATT across post-treatment periods only |
1813
1822
  | `avg_se` | Standard error of average ATT |
1814
1823
  | `avg_t_stat` | T-statistic for average ATT |
1815
1824
  | `avg_p_value` | P-value for average ATT |
@@ -1817,6 +1826,10 @@ MultiPeriodDiD(
1817
1826
  | `n_obs` | Number of observations |
1818
1827
  | `pre_periods` | List of pre-treatment periods |
1819
1828
  | `post_periods` | List of post-treatment periods |
1829
+ | `reference_period` | The omitted reference period (coefficient = 0 by construction) |
1830
+ | `interaction_indices` | Dict mapping period → column index in VCV (for sub-VCV extraction) |
1831
+ | `pre_period_effects` | Property: pre-period effects only (for parallel trends assessment) |
1832
+ | `post_period_effects` | Property: post-period effects only |
1820
1833
 
1821
1834
  **Methods:**
1822
1835
 
@@ -1909,8 +1922,7 @@ TROP(
1909
1922
  max_iter=100, # Max iterations for factor estimation
1910
1923
  tol=1e-6, # Convergence tolerance
1911
1924
  alpha=0.05, # Significance level for CIs
1912
- variance_method='bootstrap', # 'bootstrap' or 'jackknife'
1913
- n_bootstrap=200, # Bootstrap/jackknife iterations
1925
+ n_bootstrap=200, # Bootstrap replications
1914
1926
  seed=None # Random seed
1915
1927
  )
1916
1928
  ```
@@ -1934,7 +1946,7 @@ Note: TROP infers treatment periods from the treatment indicator column. The tre
1934
1946
  | Attribute | Description |
1935
1947
  |-----------|-------------|
1936
1948
  | `att` | Average Treatment effect on the Treated |
1937
- | `se` | Standard error (bootstrap or jackknife) |
1949
+ | `se` | Standard error (bootstrap) |
1938
1950
  | `t_stat` | T-statistic |
1939
1951
  | `p_value` | P-value |
1940
1952
  | `conf_int` | Confidence interval |
@@ -1953,7 +1965,6 @@ Note: TROP infers treatment periods from the treatment indicator column. The tre
1953
1965
  | `loocv_score` | LOOCV score for selected parameters |
1954
1966
  | `n_pre_periods` | Number of pre-treatment periods |
1955
1967
  | `n_post_periods` | Number of post-treatment periods |
1956
- | `variance_method` | Variance estimation method |
1957
1968
  | `bootstrap_distribution` | Bootstrap distribution (if bootstrap) |
1958
1969
 
1959
1970
  **Methods:**
@@ -561,12 +561,13 @@ results = twfe.fit(
561
561
 
562
562
  ### Multi-Period DiD (Event Study)
563
563
 
564
- For settings with multiple pre- and post-treatment periods:
564
+ For settings with multiple pre- and post-treatment periods. Estimates treatment × period
565
+ interactions for ALL periods (pre and post), enabling parallel trends assessment:
565
566
 
566
567
  ```python
567
568
  from diff_diff import MultiPeriodDiD
568
569
 
569
- # Fit with multiple time periods
570
+ # Fit full event study with pre and post period effects
570
571
  did = MultiPeriodDiD()
571
572
  results = did.fit(
572
573
  panel_data,
@@ -574,18 +575,23 @@ results = did.fit(
574
575
  treatment='treated',
575
576
  time='period',
576
577
  post_periods=[3, 4, 5], # Periods 3-5 are post-treatment
577
- reference_period=0 # Reference period for comparison
578
+ reference_period=2, # Last pre-period (e=-1 convention)
579
+ unit='unit_id', # Optional: warns if staggered adoption detected
578
580
  )
579
581
 
580
- # View period-specific treatment effects
581
- for period, effect in results.period_effects.items():
582
- print(f"Period {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
582
+ # Pre-period effects test parallel trends (should be ≈ 0)
583
+ for period, effect in results.pre_period_effects.items():
584
+ print(f"Pre {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
585
+
586
+ # Post-period effects estimate dynamic treatment effects
587
+ for period, effect in results.post_period_effects.items():
588
+ print(f"Post {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
583
589
 
584
590
  # View average treatment effect across post-periods
585
591
  print(f"Average ATT: {results.avg_att:.3f}")
586
592
  print(f"Average SE: {results.avg_se:.3f}")
587
593
 
588
- # Full summary with all period effects
594
+ # Full summary with pre and post period effects
589
595
  results.print_summary()
590
596
  ```
591
597
 
@@ -951,10 +957,10 @@ Create publication-ready event study plots:
951
957
  ```python
952
958
  from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham
953
959
 
954
- # From MultiPeriodDiD
960
+ # From MultiPeriodDiD (full event study with pre and post period effects)
955
961
  did = MultiPeriodDiD()
956
962
  results = did.fit(data, outcome='y', treatment='treated',
957
- time='period', post_periods=[3, 4, 5])
963
+ time='period', post_periods=[3, 4, 5], reference_period=2)
958
964
  plot_event_study(results, title="Treatment Effects Over Time")
959
965
 
960
966
  # From CallawaySantAnna (with event study aggregation)
@@ -1274,7 +1280,6 @@ TROP(
1274
1280
  max_iter=100, # Max iterations for factor estimation
1275
1281
  tol=1e-6, # Convergence tolerance
1276
1282
  alpha=0.05, # Significance level
1277
- variance_method='bootstrap', # 'bootstrap' or 'jackknife'
1278
1283
  n_bootstrap=200, # Bootstrap replications
1279
1284
  seed=None # Random seed
1280
1285
  )
@@ -1414,14 +1419,15 @@ Pre-trends tests have low power and can exacerbate bias. **Honest DiD** (Rambach
1414
1419
  ```python
1415
1420
  from diff_diff import HonestDiD, MultiPeriodDiD
1416
1421
 
1417
- # First, fit a standard event study
1422
+ # First, fit a full event study (pre + post period effects)
1418
1423
  did = MultiPeriodDiD()
1419
1424
  event_results = did.fit(
1420
1425
  data,
1421
1426
  outcome='outcome',
1422
1427
  treatment='treated',
1423
1428
  time='period',
1424
- post_periods=[5, 6, 7, 8, 9]
1429
+ post_periods=[5, 6, 7, 8, 9],
1430
+ reference_period=4, # Last pre-period (e=-1 convention)
1425
1431
  )
1426
1432
 
1427
1433
  # Compute honest bounds with relative magnitudes restriction
@@ -1489,14 +1495,15 @@ A passing pre-trends test doesn't mean parallel trends holds—it may just mean
1489
1495
  ```python
1490
1496
  from diff_diff import PreTrendsPower, MultiPeriodDiD
1491
1497
 
1492
- # First, fit an event study
1498
+ # First, fit a full event study
1493
1499
  did = MultiPeriodDiD()
1494
1500
  event_results = did.fit(
1495
1501
  data,
1496
1502
  outcome='outcome',
1497
1503
  treatment='treated',
1498
1504
  time='period',
1499
- post_periods=[5, 6, 7, 8, 9]
1505
+ post_periods=[5, 6, 7, 8, 9],
1506
+ reference_period=4,
1500
1507
  )
1501
1508
 
1502
1509
  # Analyze pre-trends test power
@@ -1765,7 +1772,8 @@ MultiPeriodDiD(
1765
1772
  | `covariates` | list | Linear control variables |
1766
1773
  | `fixed_effects` | list | Categorical FE columns (creates dummies) |
1767
1774
  | `absorb` | list | High-dimensional FE (within-transformation) |
1768
- | `reference_period` | any | Omitted period for time dummies |
1775
+ | `reference_period` | any | Omitted period (default: last pre-period, e=-1 convention) |
1776
+ | `unit` | str | Unit identifier column (for staggered adoption warning) |
1769
1777
 
1770
1778
  ### MultiPeriodDiDResults
1771
1779
 
@@ -1773,8 +1781,8 @@ MultiPeriodDiD(
1773
1781
 
1774
1782
  | Attribute | Description |
1775
1783
  |-----------|-------------|
1776
- | `period_effects` | Dict mapping periods to PeriodEffect objects |
1777
- | `avg_att` | Average ATT across post-treatment periods |
1784
+ | `period_effects` | Dict mapping periods to PeriodEffect objects (pre and post, excluding reference) |
1785
+ | `avg_att` | Average ATT across post-treatment periods only |
1778
1786
  | `avg_se` | Standard error of average ATT |
1779
1787
  | `avg_t_stat` | T-statistic for average ATT |
1780
1788
  | `avg_p_value` | P-value for average ATT |
@@ -1782,6 +1790,10 @@ MultiPeriodDiD(
1782
1790
  | `n_obs` | Number of observations |
1783
1791
  | `pre_periods` | List of pre-treatment periods |
1784
1792
  | `post_periods` | List of post-treatment periods |
1793
+ | `reference_period` | The omitted reference period (coefficient = 0 by construction) |
1794
+ | `interaction_indices` | Dict mapping period → column index in VCV (for sub-VCV extraction) |
1795
+ | `pre_period_effects` | Property: pre-period effects only (for parallel trends assessment) |
1796
+ | `post_period_effects` | Property: post-period effects only |
1785
1797
 
1786
1798
  **Methods:**
1787
1799
 
@@ -1874,8 +1886,7 @@ TROP(
1874
1886
  max_iter=100, # Max iterations for factor estimation
1875
1887
  tol=1e-6, # Convergence tolerance
1876
1888
  alpha=0.05, # Significance level for CIs
1877
- variance_method='bootstrap', # 'bootstrap' or 'jackknife'
1878
- n_bootstrap=200, # Bootstrap/jackknife iterations
1889
+ n_bootstrap=200, # Bootstrap replications
1879
1890
  seed=None # Random seed
1880
1891
  )
1881
1892
  ```
@@ -1899,7 +1910,7 @@ Note: TROP infers treatment periods from the treatment indicator column. The tre
1899
1910
  | Attribute | Description |
1900
1911
  |-----------|-------------|
1901
1912
  | `att` | Average Treatment effect on the Treated |
1902
- | `se` | Standard error (bootstrap or jackknife) |
1913
+ | `se` | Standard error (bootstrap) |
1903
1914
  | `t_stat` | T-statistic |
1904
1915
  | `p_value` | P-value |
1905
1916
  | `conf_int` | Confidence interval |
@@ -1918,7 +1929,6 @@ Note: TROP infers treatment periods from the treatment indicator column. The tre
1918
1929
  | `loocv_score` | LOOCV score for selected parameters |
1919
1930
  | `n_pre_periods` | Number of pre-treatment periods |
1920
1931
  | `n_post_periods` | Number of post-treatment periods |
1921
- | `variance_method` | Variance estimation method |
1922
1932
  | `bootstrap_distribution` | Bootstrap distribution (if bootstrap) |
1923
1933
 
1924
1934
  **Methods:**
@@ -136,7 +136,7 @@ from diff_diff.datasets import (
136
136
  load_mpdta,
137
137
  )
138
138
 
139
- __version__ = "2.2.0"
139
+ __version__ = "2.2.1"
140
140
  __all__ = [
141
141
  # Estimators
142
142
  "DifferenceInDifferences",
@@ -662,7 +662,7 @@ def permutation_test(
662
662
  ci_upper = np.percentile(valid_effects, (1 - alpha / 2) * 100)
663
663
 
664
664
  # T-stat from original estimate
665
- t_stat = original_att / se if se > 0 else 0.0
665
+ t_stat = original_att / se if np.isfinite(se) and se > 0 else np.nan
666
666
 
667
667
  return PlaceboTestResults(
668
668
  test_type="permutation",
@@ -783,14 +783,14 @@ def leave_one_out_test(
783
783
  # Statistics of LOO distribution
784
784
  mean_effect = np.mean(valid_effects)
785
785
  se = np.std(valid_effects, ddof=1) if len(valid_effects) > 1 else 0.0
786
- t_stat = mean_effect / se if se > 0 else 0.0
786
+ t_stat = mean_effect / se if np.isfinite(se) and se > 0 else np.nan
787
787
 
788
788
  # Use t-distribution for p-value
789
789
  df = len(valid_effects) - 1 if len(valid_effects) > 1 else 1
790
790
  p_value = compute_p_value(t_stat, df=df)
791
791
 
792
792
  # CI
793
- conf_int = compute_confidence_interval(mean_effect, se, alpha, df=df)
793
+ conf_int = compute_confidence_interval(mean_effect, se, alpha, df=df) if np.isfinite(se) and se > 0 else (np.nan, np.nan)
794
794
 
795
795
  return PlaceboTestResults(
796
796
  test_type="leave_one_out",