diff-diff 2.7.1__tar.gz → 2.7.2__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 (52) hide show
  1. {diff_diff-2.7.1 → diff_diff-2.7.2}/PKG-INFO +35 -7
  2. {diff_diff-2.7.1 → diff_diff-2.7.2}/README.md +24 -3
  3. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/__init__.py +1 -1
  4. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/_backend.py +16 -16
  5. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/datasets.py +3 -3
  6. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/staggered.py +30 -13
  7. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/staggered_results.py +2 -2
  8. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/trop.py +70 -53
  9. {diff_diff-2.7.1 → diff_diff-2.7.2}/pyproject.toml +20 -3
  10. {diff_diff-2.7.1 → diff_diff-2.7.2}/rust/Cargo.lock +1 -1
  11. {diff_diff-2.7.1 → diff_diff-2.7.2}/rust/Cargo.toml +1 -1
  12. {diff_diff-2.7.1 → diff_diff-2.7.2}/rust/src/lib.rs +4 -4
  13. {diff_diff-2.7.1 → diff_diff-2.7.2}/rust/src/trop.rs +16 -12
  14. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/bacon.py +0 -0
  15. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/bootstrap_utils.py +0 -0
  16. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/continuous_did.py +0 -0
  17. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/continuous_did_bspline.py +0 -0
  18. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/continuous_did_results.py +0 -0
  19. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/diagnostics.py +0 -0
  20. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/efficient_did.py +0 -0
  21. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/efficient_did_bootstrap.py +0 -0
  22. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/efficient_did_results.py +0 -0
  23. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/efficient_did_weights.py +0 -0
  24. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/estimators.py +0 -0
  25. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/honest_did.py +0 -0
  26. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/imputation.py +0 -0
  27. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/imputation_bootstrap.py +0 -0
  28. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/imputation_results.py +0 -0
  29. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/linalg.py +0 -0
  30. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/power.py +0 -0
  31. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/prep.py +0 -0
  32. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/prep_dgp.py +0 -0
  33. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/pretrends.py +0 -0
  34. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/results.py +0 -0
  35. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/stacked_did.py +0 -0
  36. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/stacked_did_results.py +0 -0
  37. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/staggered_aggregation.py +0 -0
  38. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/staggered_bootstrap.py +0 -0
  39. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/sun_abraham.py +0 -0
  40. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/synthetic_did.py +0 -0
  41. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/triple_diff.py +0 -0
  42. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/trop_results.py +0 -0
  43. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/twfe.py +0 -0
  44. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/two_stage.py +0 -0
  45. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/two_stage_bootstrap.py +0 -0
  46. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/two_stage_results.py +0 -0
  47. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/utils.py +0 -0
  48. {diff_diff-2.7.1 → diff_diff-2.7.2}/diff_diff/visualization.py +0 -0
  49. {diff_diff-2.7.1 → diff_diff-2.7.2}/rust/build.rs +0 -0
  50. {diff_diff-2.7.1 → diff_diff-2.7.2}/rust/src/bootstrap.rs +0 -0
  51. {diff_diff-2.7.1 → diff_diff-2.7.2}/rust/src/linalg.rs +0 -0
  52. {diff_diff-2.7.1 → diff_diff-2.7.2}/rust/src/weights.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 2.7.1
3
+ Version: 2.7.2
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -11,6 +11,8 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Topic :: Scientific/Engineering :: Mathematics
14
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
+ Classifier: Topic :: Scientific/Engineering
14
16
  Requires-Dist: numpy>=1.20.0
15
17
  Requires-Dist: pandas>=1.3.0
16
18
  Requires-Dist: scipy>=1.7.0
@@ -21,12 +23,17 @@ Requires-Dist: black>=23.0 ; extra == 'dev'
21
23
  Requires-Dist: ruff>=0.1.0 ; extra == 'dev'
22
24
  Requires-Dist: mypy>=1.0 ; extra == 'dev'
23
25
  Requires-Dist: maturin>=1.4,<2.0 ; extra == 'dev'
26
+ Requires-Dist: matplotlib>=3.5 ; extra == 'dev'
24
27
  Requires-Dist: sphinx>=6.0 ; extra == 'docs'
25
- Requires-Dist: sphinx-rtd-theme>=1.0 ; extra == 'docs'
28
+ Requires-Dist: pydata-sphinx-theme>=0.15 ; extra == 'docs'
29
+ Requires-Dist: sphinxext-opengraph>=0.9 ; extra == 'docs'
30
+ Requires-Dist: sphinx-sitemap>=2.5 ; extra == 'docs'
31
+ Requires-Dist: nbsphinx>=0.9 ; extra == 'docs'
32
+ Requires-Dist: matplotlib>=3.5 ; extra == 'docs'
26
33
  Provides-Extra: dev
27
34
  Provides-Extra: docs
28
- Summary: A library for Difference-in-Differences causal inference analysis
29
- Keywords: causal-inference,difference-in-differences,econometrics,statistics,treatment-effects
35
+ Summary: Difference-in-Differences causal inference with sklearn-like API. Callaway-Sant'Anna, Synthetic DiD, Honest DiD, event studies, parallel trends.
36
+ Keywords: causal-inference,difference-in-differences,econometrics,statistics,treatment-effects,event-study,staggered-adoption,parallel-trends,synthetic-control,panel-data,did,twfe,callaway-santanna,honest-did,sensitivity-analysis
30
37
  Author: diff-diff contributors
31
38
  License-Expression: MIT
32
39
  Requires-Python: >=3.9, <3.14
@@ -38,6 +45,12 @@ Project-URL: Repository, https://github.com/igerber/diff-diff
38
45
 
39
46
  # diff-diff
40
47
 
48
+ [![PyPI version](https://img.shields.io/pypi/v/diff-diff.svg)](https://pypi.org/project/diff-diff/)
49
+ [![Python versions](https://img.shields.io/pypi/pyversions/diff-diff.svg)](https://pypi.org/project/diff-diff/)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
51
+ [![Downloads](https://img.shields.io/pypi/dm/diff-diff.svg)](https://pypi.org/project/diff-diff/)
52
+ [![Documentation](https://readthedocs.org/projects/diff-diff/badge/?version=stable)](https://diff-diff.readthedocs.io/en/stable/)
53
+
41
54
  A Python library for Difference-in-Differences (DiD) causal inference analysis with an sklearn-like API and statsmodels-style outputs.
42
55
 
43
56
  ## Installation
@@ -1555,7 +1568,7 @@ trop = TROP(
1555
1568
 
1556
1569
  ```python
1557
1570
  TROP(
1558
- method='twostep', # Estimation method: 'twostep' (default) or 'joint'
1571
+ method='local', # Estimation method: 'local' (default) or 'global'
1559
1572
  lambda_time_grid=None, # Time decay grid (default: [0, 0.1, 0.5, 1, 2, 5])
1560
1573
  lambda_unit_grid=None, # Unit distance grid (default: [0, 0.1, 0.5, 1, 2, 5])
1561
1574
  lambda_nn_grid=None, # Nuclear norm grid (default: [0, 0.01, 0.1, 1, 10])
@@ -1568,8 +1581,8 @@ TROP(
1568
1581
  ```
1569
1582
 
1570
1583
  **Estimation methods:**
1571
- - `'twostep'` (default): Per-observation model fitting following Algorithm 2 of the paper. Computes observation-specific weights and fits a model for each treated observation, then averages the individual treatment effects. More flexible but computationally intensive.
1572
- - `'joint'`: Joint weighted least squares optimization. Estimates a single scalar treatment effect τ along with fixed effects and optional low-rank factor adjustment. Faster but assumes homogeneous treatment effects.
1584
+ - `'local'` (default): Per-observation model fitting following Algorithm 2 of the paper. Computes observation-specific weights and fits a model for each treated observation, then averages the individual treatment effects. More flexible but computationally intensive.
1585
+ - `'global'`: Global weighted least squares optimization. Fits a single model on control observations with global weights, then computes per-observation treatment effects as residuals. Faster but uses global rather than observation-specific weights.
1573
1586
 
1574
1587
  **Convenience function:**
1575
1588
 
@@ -2947,6 +2960,21 @@ The `HonestDiD` module implements sensitivity analysis methods for relaxing the
2947
2960
 
2948
2961
  - **Cunningham, S. (2021).** *Causal Inference: The Mixtape*. Yale University Press. [https://mixtape.scunning.com/](https://mixtape.scunning.com/)
2949
2962
 
2963
+ ## Citing diff-diff
2964
+
2965
+ If you use diff-diff in your research, please cite it:
2966
+
2967
+ ```bibtex
2968
+ @software{diff_diff,
2969
+ title = {diff-diff: Difference-in-Differences Causal Inference for Python},
2970
+ author = {{diff-diff contributors}},
2971
+ url = {https://github.com/igerber/diff-diff},
2972
+ license = {MIT},
2973
+ }
2974
+ ```
2975
+
2976
+ See [`CITATION.cff`](CITATION.cff) for the full citation metadata.
2977
+
2950
2978
  ## License
2951
2979
 
2952
2980
  MIT License
@@ -1,5 +1,11 @@
1
1
  # diff-diff
2
2
 
3
+ [![PyPI version](https://img.shields.io/pypi/v/diff-diff.svg)](https://pypi.org/project/diff-diff/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/diff-diff.svg)](https://pypi.org/project/diff-diff/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+ [![Downloads](https://img.shields.io/pypi/dm/diff-diff.svg)](https://pypi.org/project/diff-diff/)
7
+ [![Documentation](https://readthedocs.org/projects/diff-diff/badge/?version=stable)](https://diff-diff.readthedocs.io/en/stable/)
8
+
3
9
  A Python library for Difference-in-Differences (DiD) causal inference analysis with an sklearn-like API and statsmodels-style outputs.
4
10
 
5
11
  ## Installation
@@ -1517,7 +1523,7 @@ trop = TROP(
1517
1523
 
1518
1524
  ```python
1519
1525
  TROP(
1520
- method='twostep', # Estimation method: 'twostep' (default) or 'joint'
1526
+ method='local', # Estimation method: 'local' (default) or 'global'
1521
1527
  lambda_time_grid=None, # Time decay grid (default: [0, 0.1, 0.5, 1, 2, 5])
1522
1528
  lambda_unit_grid=None, # Unit distance grid (default: [0, 0.1, 0.5, 1, 2, 5])
1523
1529
  lambda_nn_grid=None, # Nuclear norm grid (default: [0, 0.01, 0.1, 1, 10])
@@ -1530,8 +1536,8 @@ TROP(
1530
1536
  ```
1531
1537
 
1532
1538
  **Estimation methods:**
1533
- - `'twostep'` (default): Per-observation model fitting following Algorithm 2 of the paper. Computes observation-specific weights and fits a model for each treated observation, then averages the individual treatment effects. More flexible but computationally intensive.
1534
- - `'joint'`: Joint weighted least squares optimization. Estimates a single scalar treatment effect τ along with fixed effects and optional low-rank factor adjustment. Faster but assumes homogeneous treatment effects.
1539
+ - `'local'` (default): Per-observation model fitting following Algorithm 2 of the paper. Computes observation-specific weights and fits a model for each treated observation, then averages the individual treatment effects. More flexible but computationally intensive.
1540
+ - `'global'`: Global weighted least squares optimization. Fits a single model on control observations with global weights, then computes per-observation treatment effects as residuals. Faster but uses global rather than observation-specific weights.
1535
1541
 
1536
1542
  **Convenience function:**
1537
1543
 
@@ -2909,6 +2915,21 @@ The `HonestDiD` module implements sensitivity analysis methods for relaxing the
2909
2915
 
2910
2916
  - **Cunningham, S. (2021).** *Causal Inference: The Mixtape*. Yale University Press. [https://mixtape.scunning.com/](https://mixtape.scunning.com/)
2911
2917
 
2918
+ ## Citing diff-diff
2919
+
2920
+ If you use diff-diff in your research, please cite it:
2921
+
2922
+ ```bibtex
2923
+ @software{diff_diff,
2924
+ title = {diff-diff: Difference-in-Differences Causal Inference for Python},
2925
+ author = {{diff-diff contributors}},
2926
+ url = {https://github.com/igerber/diff-diff},
2927
+ license = {MIT},
2928
+ }
2929
+ ```
2930
+
2931
+ See [`CITATION.cff`](CITATION.cff) for the full citation metadata.
2932
+
2912
2933
  ## License
2913
2934
 
2914
2935
  MIT License
@@ -179,7 +179,7 @@ Stacked = StackedDiD
179
179
  Bacon = BaconDecomposition
180
180
  EDiD = EfficientDiD
181
181
 
182
- __version__ = "2.7.1"
182
+ __version__ = "2.7.2"
183
183
  __all__ = [
184
184
  # Estimators
185
185
  "DifferenceInDifferences",
@@ -23,13 +23,13 @@ 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 (twostep method)
26
+ # TROP estimator acceleration (local method)
27
27
  compute_unit_distance_matrix as _rust_unit_distance_matrix,
28
28
  loocv_grid_search as _rust_loocv_grid_search,
29
29
  bootstrap_trop_variance as _rust_bootstrap_trop_variance,
30
- # TROP estimator acceleration (joint method)
31
- loocv_grid_search_joint as _rust_loocv_grid_search_joint,
32
- bootstrap_trop_variance_joint as _rust_bootstrap_trop_variance_joint,
30
+ # TROP estimator acceleration (global method)
31
+ loocv_grid_search_global as _rust_loocv_grid_search_global,
32
+ bootstrap_trop_variance_global as _rust_bootstrap_trop_variance_global,
33
33
  # SDID weights (Frank-Wolfe matching R's synthdid)
34
34
  compute_sdid_unit_weights as _rust_sdid_unit_weights,
35
35
  compute_time_weights as _rust_compute_time_weights,
@@ -46,13 +46,13 @@ except ImportError:
46
46
  _rust_project_simplex = None
47
47
  _rust_solve_ols = None
48
48
  _rust_compute_robust_vcov = None
49
- # TROP estimator acceleration (twostep method)
49
+ # TROP estimator acceleration (local method)
50
50
  _rust_unit_distance_matrix = None
51
51
  _rust_loocv_grid_search = None
52
52
  _rust_bootstrap_trop_variance = None
53
- # TROP estimator acceleration (joint method)
54
- _rust_loocv_grid_search_joint = None
55
- _rust_bootstrap_trop_variance_joint = None
53
+ # TROP estimator acceleration (global method)
54
+ _rust_loocv_grid_search_global = None
55
+ _rust_bootstrap_trop_variance_global = None
56
56
  # SDID weights (Frank-Wolfe matching R's synthdid)
57
57
  _rust_sdid_unit_weights = None
58
58
  _rust_compute_time_weights = None
@@ -69,13 +69,13 @@ if _backend_env == 'python':
69
69
  _rust_project_simplex = None
70
70
  _rust_solve_ols = None
71
71
  _rust_compute_robust_vcov = None
72
- # TROP estimator acceleration (twostep method)
72
+ # TROP estimator acceleration (local method)
73
73
  _rust_unit_distance_matrix = None
74
74
  _rust_loocv_grid_search = None
75
75
  _rust_bootstrap_trop_variance = None
76
- # TROP estimator acceleration (joint method)
77
- _rust_loocv_grid_search_joint = None
78
- _rust_bootstrap_trop_variance_joint = None
76
+ # TROP estimator acceleration (global method)
77
+ _rust_loocv_grid_search_global = None
78
+ _rust_bootstrap_trop_variance_global = None
79
79
  # SDID weights (Frank-Wolfe matching R's synthdid)
80
80
  _rust_sdid_unit_weights = None
81
81
  _rust_compute_time_weights = None
@@ -118,13 +118,13 @@ __all__ = [
118
118
  '_rust_project_simplex',
119
119
  '_rust_solve_ols',
120
120
  '_rust_compute_robust_vcov',
121
- # TROP estimator acceleration (twostep method)
121
+ # TROP estimator acceleration (local method)
122
122
  '_rust_unit_distance_matrix',
123
123
  '_rust_loocv_grid_search',
124
124
  '_rust_bootstrap_trop_variance',
125
- # TROP estimator acceleration (joint method)
126
- '_rust_loocv_grid_search_joint',
127
- '_rust_bootstrap_trop_variance_joint',
125
+ # TROP estimator acceleration (global method)
126
+ '_rust_loocv_grid_search_global',
127
+ '_rust_bootstrap_trop_variance_global',
128
128
  # SDID weights (Frank-Wolfe matching R's synthdid)
129
129
  '_rust_sdid_unit_weights',
130
130
  '_rust_compute_time_weights',
@@ -266,7 +266,7 @@ def load_castle_doctrine(force_download: bool = False) -> pd.DataFrame:
266
266
  ... outcome="homicide_rate",
267
267
  ... unit="state",
268
268
  ... time="year",
269
- ... cohort="first_treat"
269
+ ... first_treat="first_treat"
270
270
  ... )
271
271
  """
272
272
  url = "https://raw.githubusercontent.com/causaldata/causal_datasets/main/castle/castle.csv"
@@ -412,7 +412,7 @@ def load_divorce_laws(force_download: bool = False) -> pd.DataFrame:
412
412
  ... outcome="divorce_rate",
413
413
  ... unit="state",
414
414
  ... time="year",
415
- ... cohort="first_treat"
415
+ ... first_treat="first_treat"
416
416
  ... )
417
417
  """
418
418
  # Try to load from causaldata repository
@@ -571,7 +571,7 @@ def load_mpdta(force_download: bool = False) -> pd.DataFrame:
571
571
  ... outcome="lemp",
572
572
  ... unit="countyreal",
573
573
  ... time="year",
574
- ... cohort="first_treat"
574
+ ... first_treat="first_treat"
575
575
  ... )
576
576
  """
577
577
  # mpdta is available from the did package documentation
@@ -450,10 +450,12 @@ class CallawaySantAnna(
450
450
  if self.control_group == "never_treated":
451
451
  control_mask = never_treated_mask
452
452
  else: # not_yet_treated
453
- # Not yet treated at time t: never-treated OR (first_treat > t AND not cohort g)
454
- # Must exclude cohort g since they are the treated group for this ATT(g,t)
453
+ # Not yet treated at BOTH time t and the base period:
454
+ # Controls must be untreated at whichever is later, otherwise
455
+ # their outcome at the base period is contaminated by treatment.
456
+ nyt_threshold = max(t, base_period_val) + self.anticipation
455
457
  control_mask = never_treated_mask | (
456
- (unit_cohorts > t + self.anticipation) & (unit_cohorts != g)
458
+ (unit_cohorts > nyt_threshold) & (unit_cohorts != g)
457
459
  )
458
460
 
459
461
  # Extract outcomes for base and post periods
@@ -616,21 +618,23 @@ class CallawaySantAnna(
616
618
  if base_period_val not in period_to_col or t not in period_to_col:
617
619
  continue
618
620
 
619
- tasks.append((g, t, period_to_col[base_period_val], period_to_col[t]))
621
+ tasks.append((g, t, period_to_col[base_period_val], period_to_col[t], base_period_val))
620
622
 
621
623
  # Process all tasks
622
624
  atts = []
623
625
  ses = []
624
626
  task_keys = []
625
627
 
626
- for g, t, base_col, post_col in tasks:
628
+ for g, t, base_col, post_col, base_period_val in tasks:
627
629
  treated_mask = cohort_masks[g]
628
630
 
629
631
  if self.control_group == "never_treated":
630
632
  control_mask = never_treated_mask
631
633
  else:
634
+ # Controls must be untreated at both t and base_period_val
635
+ nyt_threshold = max(t, base_period_val) + self.anticipation
632
636
  control_mask = never_treated_mask | (
633
- (unit_cohorts > t + self.anticipation) & (unit_cohorts != g)
637
+ (unit_cohorts > nyt_threshold) & (unit_cohorts != g)
634
638
  )
635
639
 
636
640
  y_base = outcome_matrix[:, base_col]
@@ -786,10 +790,10 @@ class CallawaySantAnna(
786
790
  if self.control_group == "never_treated":
787
791
  control_mask = never_treated_mask
788
792
  else:
789
- # For not_yet_treated, control_key includes t
790
- ref_t = first_t
793
+ # Controls must be untreated at both t and base_period_val
794
+ nyt_threshold = max(first_t, base_period_val) + self.anticipation
791
795
  control_mask = never_treated_mask | (
792
- (unit_cohorts > ref_t + self.anticipation) & (unit_cohorts != first_g)
796
+ (unit_cohorts > nyt_threshold) & (unit_cohorts != first_g)
793
797
  )
794
798
 
795
799
  # For balanced panels, valid_mask is all True so control_valid = control_mask
@@ -846,13 +850,15 @@ class CallawaySantAnna(
846
850
  cho = None
847
851
 
848
852
  # Process each (g, t) pair in this group
849
- for g, t, _, base_col, post_col in tasks:
853
+ for g, t, bp_val, base_col, post_col in tasks:
850
854
  treated_mask = cohort_masks[g]
851
855
 
852
856
  # Recompute control mask for not_yet_treated (varies by g, t)
853
857
  if self.control_group == "not_yet_treated":
858
+ # Controls must be untreated at both t and base period
859
+ nyt_threshold = max(t, bp_val) + self.anticipation
854
860
  control_mask = never_treated_mask | (
855
- (unit_cohorts > t + self.anticipation) & (unit_cohorts != g)
861
+ (unit_cohorts > nyt_threshold) & (unit_cohorts != g)
856
862
  )
857
863
 
858
864
  y_base = outcome_matrix[:, base_col]
@@ -1127,8 +1133,19 @@ class CallawaySantAnna(
1127
1133
  n_treated_units = (unit_info[first_treat] > 0).sum()
1128
1134
  n_control_units = (unit_info["_never_treated"]).sum()
1129
1135
 
1130
- if n_control_units == 0:
1131
- raise ValueError("No never-treated units found. Check 'first_treat' column.")
1136
+ if n_control_units == 0 and self.control_group == "never_treated":
1137
+ raise ValueError(
1138
+ "No never-treated units found. Check 'first_treat' column. "
1139
+ "Use control_group='not_yet_treated' if all units are eventually treated."
1140
+ )
1141
+ if n_control_units == 0 and self.control_group == "not_yet_treated":
1142
+ # With not_yet_treated, controls are units not yet treated at each
1143
+ # (g, t) pair — never-treated units are not required.
1144
+ if len(treatment_groups) < 2:
1145
+ raise ValueError(
1146
+ "not_yet_treated control group requires at least 2 treatment "
1147
+ "cohorts when there are no never-treated units."
1148
+ )
1132
1149
 
1133
1150
  # Pre-compute data structures for efficient ATT(g,t) computation
1134
1151
  precomputed = self._precompute_structures(
@@ -88,7 +88,7 @@ class CallawaySantAnnaResults:
88
88
  n_treated_units : int
89
89
  Number of ever-treated units.
90
90
  n_control_units : int
91
- Number of never-treated units.
91
+ Number of never-treated units (excludes not-yet-treated dynamic controls).
92
92
  event_study_effects : dict, optional
93
93
  Effects aggregated by relative time (event study).
94
94
  group_effects : dict, optional
@@ -152,7 +152,7 @@ class CallawaySantAnnaResults:
152
152
  "",
153
153
  f"{'Total observations:':<30} {self.n_obs:>10}",
154
154
  f"{'Treated units:':<30} {self.n_treated_units:>10}",
155
- f"{'Control units:':<30} {self.n_control_units:>10}",
155
+ f"{'Never-treated units:':<30} {self.n_control_units:>10}",
156
156
  f"{'Treatment cohorts:':<30} {len(self.groups):>10}",
157
157
  f"{'Time periods:':<30} {len(self.time_periods):>10}",
158
158
  f"{'Control group:':<30} {self.control_group:>10}",
@@ -31,8 +31,8 @@ from diff_diff._backend import (
31
31
  _rust_unit_distance_matrix,
32
32
  _rust_loocv_grid_search,
33
33
  _rust_bootstrap_trop_variance,
34
- _rust_loocv_grid_search_joint,
35
- _rust_bootstrap_trop_variance_joint,
34
+ _rust_loocv_grid_search_global,
35
+ _rust_bootstrap_trop_variance_global,
36
36
  )
37
37
  from diff_diff.trop_results import (
38
38
  _LAMBDA_INF,
@@ -63,10 +63,10 @@ class TROP:
63
63
 
64
64
  Parameters
65
65
  ----------
66
- method : str, default='twostep'
66
+ method : str, default='local'
67
67
  Estimation method to use:
68
68
 
69
- - 'twostep': Per-observation model fitting following Algorithm 2 of
69
+ - 'local': Per-observation model fitting following Algorithm 2 of
70
70
  Athey et al. (2025). Computes observation-specific weights and fits
71
71
  a model for each treated observation, averaging the individual
72
72
  treatment effects. More flexible but computationally intensive.
@@ -77,10 +77,11 @@ class TROP:
77
77
  treatment effects as residuals:
78
78
  tau_it = Y_it - mu - alpha_i - beta_t - L_it for treated cells.
79
79
  ATT is the mean of these effects. For the paper's full
80
- per-treated-cell estimator, use ``method='twostep'``.
80
+ per-treated-cell estimator, use ``method='local'``.
81
81
 
82
- - 'joint': Deprecated alias for 'global'. Will be removed in a
83
- future version.
82
+ - 'twostep': Deprecated alias for 'local'. Will be removed in v3.0.
83
+
84
+ - 'joint': Deprecated alias for 'global'. Will be removed in v3.0.
84
85
 
85
86
  lambda_time_grid : list, optional
86
87
  Grid of time weight decay parameters. 0.0 = uniform weights (disabled).
@@ -138,7 +139,7 @@ class TROP:
138
139
 
139
140
  def __init__(
140
141
  self,
141
- method: str = "twostep",
142
+ method: str = "local",
142
143
  lambda_time_grid: Optional[List[float]] = None,
143
144
  lambda_unit_grid: Optional[List[float]] = None,
144
145
  lambda_nn_grid: Optional[List[float]] = None,
@@ -149,16 +150,24 @@ class TROP:
149
150
  seed: Optional[int] = None,
150
151
  ):
151
152
  # Validate method parameter
152
- # 'global' is the preferred name; 'joint' is a deprecated alias
153
- valid_methods = ("twostep", "joint", "global")
153
+ # 'local'/'global' are preferred; 'twostep'/'joint' are deprecated aliases
154
+ valid_methods = ("local", "twostep", "joint", "global")
154
155
  if method not in valid_methods:
155
156
  raise ValueError(
156
157
  f"method must be one of {valid_methods}, got '{method}'"
157
158
  )
159
+ if method == "twostep":
160
+ warnings.warn(
161
+ "method='twostep' is deprecated and will be removed in v3.0. "
162
+ "Use method='local' instead.",
163
+ FutureWarning,
164
+ stacklevel=2,
165
+ )
166
+ method = "local"
158
167
  if method == "joint":
159
168
  warnings.warn(
160
- "method='joint' is deprecated and will be removed in a future "
161
- "version. Use method='global' instead.",
169
+ "method='joint' is deprecated and will be removed in v3.0. "
170
+ "Use method='global' instead.",
162
171
  FutureWarning,
163
172
  stacklevel=2,
164
173
  )
@@ -556,7 +565,7 @@ class TROP:
556
565
  # Joint estimation method
557
566
  # =========================================================================
558
567
 
559
- def _compute_joint_weights(
568
+ def _compute_global_weights(
560
569
  self,
561
570
  Y: np.ndarray,
562
571
  D: np.ndarray,
@@ -567,7 +576,7 @@ class TROP:
567
576
  n_periods: int,
568
577
  ) -> np.ndarray:
569
578
  """
570
- Compute distance-based weights for joint estimation.
579
+ Compute distance-based weights for global estimation.
571
580
 
572
581
  Following the reference implementation, weights are computed based on:
573
582
  - Time distance: distance to center of treated block
@@ -655,7 +664,7 @@ class TROP:
655
664
 
656
665
  return delta
657
666
 
658
- def _solve_joint_model(
667
+ def _solve_global_model(
659
668
  self,
660
669
  Y: np.ndarray,
661
670
  delta: np.ndarray,
@@ -668,10 +677,10 @@ class TROP:
668
677
  """
669
678
  n_periods, n_units = Y.shape
670
679
  if lambda_nn >= 1e10:
671
- mu, alpha, beta = self._solve_joint_no_lowrank(Y, delta)
680
+ mu, alpha, beta = self._solve_global_no_lowrank(Y, delta)
672
681
  L = np.zeros((n_periods, n_units))
673
682
  else:
674
- mu, alpha, beta, L = self._solve_joint_with_lowrank(
683
+ mu, alpha, beta, L = self._solve_global_with_lowrank(
675
684
  Y, delta, lambda_nn, self.max_iter, self.tol
676
685
  )
677
686
  return mu, alpha, beta, L
@@ -718,7 +727,7 @@ class TROP:
718
727
 
719
728
  return att, treatment_effects, tau_values
720
729
 
721
- def _loocv_score_joint(
730
+ def _loocv_score_global(
722
731
  self,
723
732
  Y: np.ndarray,
724
733
  D: np.ndarray,
@@ -731,12 +740,12 @@ class TROP:
731
740
  n_periods: int,
732
741
  ) -> float:
733
742
  """
734
- Compute LOOCV score for joint method with specific parameter combination.
743
+ Compute LOOCV score for global method with specific parameter combination.
735
744
 
736
745
  Following paper's Equation 5:
737
746
  Q(λ) = Σ_{j,s: D_js=0} [τ̂_js^loocv(λ)]²
738
747
 
739
- For joint method, we exclude each control observation, fit the joint model
748
+ For global method, we exclude each control observation, fit the global model
740
749
  on remaining data, and compute the pseudo-treatment effect at the excluded obs.
741
750
 
742
751
  Parameters
@@ -766,7 +775,7 @@ class TROP:
766
775
  LOOCV score (sum of squared pseudo-treatment effects).
767
776
  """
768
777
  # Compute global weights (same for all LOOCV iterations)
769
- delta = self._compute_joint_weights(
778
+ delta = self._compute_global_weights(
770
779
  Y, D, lambda_time, lambda_unit, treated_periods, n_units, n_periods
771
780
  )
772
781
 
@@ -779,7 +788,7 @@ class TROP:
779
788
  delta_ex[t_ex, i_ex] = 0.0
780
789
 
781
790
  try:
782
- mu, alpha, beta, L = self._solve_joint_model(Y, delta_ex, lambda_nn)
791
+ mu, alpha, beta, L = self._solve_global_model(Y, delta_ex, lambda_nn)
783
792
 
784
793
  # Pseudo treatment effect: τ = Y - μ - α - β - L
785
794
  if np.isfinite(Y[t_ex, i_ex]):
@@ -796,7 +805,7 @@ class TROP:
796
805
 
797
806
  return tau_sq_sum
798
807
 
799
- def _solve_joint_no_lowrank(
808
+ def _solve_global_no_lowrank(
800
809
  self,
801
810
  Y: np.ndarray,
802
811
  delta: np.ndarray,
@@ -806,7 +815,7 @@ class TROP:
806
815
 
807
816
  Solves: min Σ (1-W)*δ_{it}(Y_{it} - μ - α_i - β_t)²
808
817
 
809
- The (1-W) masking is already applied to delta by _compute_joint_weights,
818
+ The (1-W) masking is already applied to delta by _compute_global_weights,
810
819
  so treated observations have zero weight and do not affect the fit.
811
820
 
812
821
  Parameters
@@ -880,7 +889,7 @@ class TROP:
880
889
 
881
890
  return float(mu), alpha, beta
882
891
 
883
- def _solve_joint_with_lowrank(
892
+ def _solve_global_with_lowrank(
884
893
  self,
885
894
  Y: np.ndarray,
886
895
  delta: np.ndarray,
@@ -893,7 +902,7 @@ class TROP:
893
902
 
894
903
  Solves: min Σ (1-W)*δ_{it}(Y_{it} - μ - α_i - β_t - L_{it})² + λ_nn||L||_*
895
904
 
896
- The (1-W) masking is already applied to delta by _compute_joint_weights,
905
+ The (1-W) masking is already applied to delta by _compute_global_weights,
897
906
  so treated observations have zero weight and do not affect the fit.
898
907
 
899
908
  Parameters
@@ -942,7 +951,7 @@ class TROP:
942
951
 
943
952
  # Step 1: Fix L, solve for (mu, alpha, beta)
944
953
  Y_adj = Y_safe - L
945
- mu, alpha, beta = self._solve_joint_no_lowrank(Y_adj, delta_masked)
954
+ mu, alpha, beta = self._solve_global_no_lowrank(Y_adj, delta_masked)
946
955
 
947
956
  # Step 2: Fix (mu, alpha, beta), update L with FISTA acceleration
948
957
  R = Y_safe - mu - alpha[np.newaxis, :] - beta[:, np.newaxis]
@@ -981,11 +990,11 @@ class TROP:
981
990
 
982
991
  # Final re-solve with converged L (match Rust behavior)
983
992
  Y_adj = Y_safe - L
984
- mu, alpha, beta = self._solve_joint_no_lowrank(Y_adj, delta_masked)
993
+ mu, alpha, beta = self._solve_global_no_lowrank(Y_adj, delta_masked)
985
994
 
986
995
  return mu, alpha, beta, L
987
996
 
988
- def _fit_joint(
997
+ def _fit_global(
989
998
  self,
990
999
  data: pd.DataFrame,
991
1000
  outcome: str,
@@ -1024,10 +1033,10 @@ class TROP:
1024
1033
  (fixed `treated_periods` across resamples). The treatment timing is
1025
1034
  inferred from the data once and held constant for all bootstrap
1026
1035
  iterations. For staggered adoption designs where treatment timing varies
1027
- across units, use `method="twostep"` which computes observation-specific
1036
+ across units, use `method="local"` which computes observation-specific
1028
1037
  weights that naturally handle heterogeneous timing.
1029
1038
  """
1030
- # Data setup (same as twostep method)
1039
+ # Data setup (same as local method)
1031
1040
  all_units = sorted(data[unit].unique())
1032
1041
  all_periods = sorted(data[time].unique())
1033
1042
 
@@ -1097,7 +1106,7 @@ class TROP:
1097
1106
  if n_pre_periods < 2:
1098
1107
  raise ValueError("Need at least 2 pre-treatment periods")
1099
1108
 
1100
- # Check for staggered adoption (joint method requires simultaneous treatment)
1109
+ # Check for staggered adoption (global method requires simultaneous treatment)
1101
1110
  # Use only observed periods (skip missing) to avoid false positives on unbalanced panels
1102
1111
  first_treat_by_unit = []
1103
1112
  for i in treated_unit_idx:
@@ -1115,7 +1124,7 @@ class TROP:
1115
1124
  raise ValueError(
1116
1125
  f"method='global' requires simultaneous treatment adoption, but your data "
1117
1126
  f"shows staggered adoption (units first treated at periods {unique_starts}). "
1118
- f"Use method='twostep' which properly handles staggered adoption designs."
1127
+ f"Use method='local' which properly handles staggered adoption designs."
1119
1128
  )
1120
1129
 
1121
1130
  # LOOCV grid search for tuning parameters
@@ -1124,7 +1133,7 @@ class TROP:
1124
1133
  best_score = np.inf
1125
1134
  control_mask = D == 0
1126
1135
 
1127
- if HAS_RUST_BACKEND and _rust_loocv_grid_search_joint is not None:
1136
+ if HAS_RUST_BACKEND and _rust_loocv_grid_search_global is not None:
1128
1137
  try:
1129
1138
  # Prepare inputs for Rust function
1130
1139
  control_mask_u8 = control_mask.astype(np.uint8)
@@ -1133,7 +1142,7 @@ class TROP:
1133
1142
  lambda_unit_arr = np.array(self.lambda_unit_grid, dtype=np.float64)
1134
1143
  lambda_nn_arr = np.array(self.lambda_nn_grid, dtype=np.float64)
1135
1144
 
1136
- result = _rust_loocv_grid_search_joint(
1145
+ result = _rust_loocv_grid_search_global(
1137
1146
  Y, D.astype(np.float64), control_mask_u8,
1138
1147
  lambda_time_arr, lambda_unit_arr, lambda_nn_arr,
1139
1148
  self.max_iter, self.tol,
@@ -1170,7 +1179,7 @@ class TROP:
1170
1179
  except Exception as e:
1171
1180
  # Fall back to Python implementation on error
1172
1181
  logger.debug(
1173
- "Rust LOOCV grid search (joint) failed, falling back to Python: %s", e
1182
+ "Rust LOOCV grid search (global) failed, falling back to Python: %s", e
1174
1183
  )
1175
1184
  best_lambda = None
1176
1185
  best_score = np.inf
@@ -1193,7 +1202,7 @@ class TROP:
1193
1202
  ln = 1e10 if np.isinf(lambda_nn_val) else lambda_nn_val
1194
1203
 
1195
1204
  try:
1196
- score = self._loocv_score_joint(
1205
+ score = self._loocv_score_global(
1197
1206
  Y, D, control_obs, lt, lu, ln,
1198
1207
  treated_periods, n_units, n_periods
1199
1208
  )
@@ -1223,11 +1232,11 @@ class TROP:
1223
1232
  lambda_nn = 1e10
1224
1233
 
1225
1234
  # Compute final weights and fit
1226
- delta = self._compute_joint_weights(
1235
+ delta = self._compute_global_weights(
1227
1236
  Y, D, lambda_time, lambda_unit, treated_periods, n_units, n_periods
1228
1237
  )
1229
1238
 
1230
- mu, alpha, beta, L = self._solve_joint_model(Y, delta, lambda_nn)
1239
+ mu, alpha, beta, L = self._solve_global_model(Y, delta, lambda_nn)
1231
1240
 
1232
1241
  # Post-hoc tau extraction (per paper Eq. 2)
1233
1242
  att, treatment_effects, tau_values = self._extract_posthoc_tau(
@@ -1258,7 +1267,7 @@ class TROP:
1258
1267
  # Bootstrap variance estimation
1259
1268
  effective_lambda = (lambda_time, lambda_unit, lambda_nn)
1260
1269
 
1261
- se, bootstrap_dist = self._bootstrap_variance_joint(
1270
+ se, bootstrap_dist = self._bootstrap_variance_global(
1262
1271
  data, outcome, treatment, unit, time,
1263
1272
  effective_lambda, treated_periods
1264
1273
  )
@@ -1300,7 +1309,7 @@ class TROP:
1300
1309
  self.is_fitted_ = True
1301
1310
  return self.results_
1302
1311
 
1303
- def _bootstrap_variance_joint(
1312
+ def _bootstrap_variance_global(
1304
1313
  self,
1305
1314
  data: pd.DataFrame,
1306
1315
  outcome: str,
@@ -1311,7 +1320,7 @@ class TROP:
1311
1320
  treated_periods: int,
1312
1321
  ) -> Tuple[float, np.ndarray]:
1313
1322
  """
1314
- Compute bootstrap standard error for joint method.
1323
+ Compute bootstrap standard error for global method.
1315
1324
 
1316
1325
  Uses Rust backend when available for parallel bootstrap (5-15x speedup).
1317
1326
 
@@ -1340,7 +1349,7 @@ class TROP:
1340
1349
  lambda_time, lambda_unit, lambda_nn = optimal_lambda
1341
1350
 
1342
1351
  # Try Rust backend for parallel bootstrap (5-15x speedup)
1343
- if HAS_RUST_BACKEND and _rust_bootstrap_trop_variance_joint is not None:
1352
+ if HAS_RUST_BACKEND and _rust_bootstrap_trop_variance_global is not None:
1344
1353
  try:
1345
1354
  # Create matrices for Rust function
1346
1355
  all_units = sorted(data[unit].unique())
@@ -1359,7 +1368,7 @@ class TROP:
1359
1368
  .values
1360
1369
  )
1361
1370
 
1362
- bootstrap_estimates, se = _rust_bootstrap_trop_variance_joint(
1371
+ bootstrap_estimates, se = _rust_bootstrap_trop_variance_global(
1363
1372
  Y, D,
1364
1373
  lambda_time, lambda_unit, lambda_nn,
1365
1374
  self.n_bootstrap, self.max_iter, self.tol,
@@ -1378,7 +1387,7 @@ class TROP:
1378
1387
 
1379
1388
  except Exception as e:
1380
1389
  logger.debug(
1381
- "Rust bootstrap (joint) failed, falling back to Python: %s", e
1390
+ "Rust bootstrap (global) failed, falling back to Python: %s", e
1382
1391
  )
1383
1392
 
1384
1393
  # Python fallback implementation
@@ -1419,7 +1428,7 @@ class TROP:
1419
1428
  ], ignore_index=True)
1420
1429
 
1421
1430
  try:
1422
- tau = self._fit_joint_with_fixed_lambda(
1431
+ tau = self._fit_global_with_fixed_lambda(
1423
1432
  boot_data, outcome, treatment, unit, time,
1424
1433
  optimal_lambda, treated_periods
1425
1434
  )
@@ -1441,7 +1450,7 @@ class TROP:
1441
1450
  se = np.std(bootstrap_estimates, ddof=1)
1442
1451
  return float(se), bootstrap_estimates
1443
1452
 
1444
- def _fit_joint_with_fixed_lambda(
1453
+ def _fit_global_with_fixed_lambda(
1445
1454
  self,
1446
1455
  data: pd.DataFrame,
1447
1456
  outcome: str,
@@ -1478,12 +1487,12 @@ class TROP:
1478
1487
  )
1479
1488
 
1480
1489
  # Compute weights (includes (1-W) masking)
1481
- delta = self._compute_joint_weights(
1490
+ delta = self._compute_global_weights(
1482
1491
  Y, D, lambda_time, lambda_unit, treated_periods, n_units, n_periods
1483
1492
  )
1484
1493
 
1485
1494
  # Fit model on control data and extract post-hoc tau
1486
- mu, alpha, beta, L = self._solve_joint_model(Y, delta, lambda_nn)
1495
+ mu, alpha, beta, L = self._solve_global_model(Y, delta, lambda_nn)
1487
1496
  att, _, _ = self._extract_posthoc_tau(Y, D, mu, alpha, beta, L)
1488
1497
  return att
1489
1498
 
@@ -1541,9 +1550,9 @@ class TROP:
1541
1550
 
1542
1551
  # Dispatch based on estimation method
1543
1552
  if self.method == "global":
1544
- return self._fit_joint(data, outcome, treatment, unit, time)
1553
+ return self._fit_global(data, outcome, treatment, unit, time)
1545
1554
 
1546
- # Below is the twostep method (default)
1555
+ # Below is the local method (default)
1547
1556
  # Get unique units and periods
1548
1557
  all_units = sorted(data[unit].unique())
1549
1558
  all_periods = sorted(data[time].unique())
@@ -2660,10 +2669,18 @@ class TROP:
2660
2669
  def set_params(self, **params) -> "TROP":
2661
2670
  """Set estimator parameters."""
2662
2671
  for key, value in params.items():
2672
+ if key == "method" and value == "twostep":
2673
+ warnings.warn(
2674
+ "method='twostep' is deprecated and will be removed in "
2675
+ "v3.0. Use method='local' instead.",
2676
+ FutureWarning,
2677
+ stacklevel=2,
2678
+ )
2679
+ value = "local"
2663
2680
  if key == "method" and value == "joint":
2664
2681
  warnings.warn(
2665
- "method='joint' is deprecated and will be removed in a "
2666
- "future version. Use method='global' instead.",
2682
+ "method='joint' is deprecated and will be removed in "
2683
+ "v3.0. Use method='global' instead.",
2667
2684
  FutureWarning,
2668
2685
  stacklevel=2,
2669
2686
  )
@@ -4,8 +4,8 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "diff-diff"
7
- version = "2.7.1"
8
- description = "A library for Difference-in-Differences causal inference analysis"
7
+ version = "2.7.2"
8
+ description = "Difference-in-Differences causal inference with sklearn-like API. Callaway-Sant'Anna, Synthetic DiD, Honest DiD, event studies, parallel trends."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.9,<3.14"
@@ -18,6 +18,16 @@ keywords = [
18
18
  "econometrics",
19
19
  "statistics",
20
20
  "treatment-effects",
21
+ "event-study",
22
+ "staggered-adoption",
23
+ "parallel-trends",
24
+ "synthetic-control",
25
+ "panel-data",
26
+ "did",
27
+ "twfe",
28
+ "callaway-santanna",
29
+ "honest-did",
30
+ "sensitivity-analysis",
21
31
  ]
22
32
  classifiers = [
23
33
  "Development Status :: 5 - Production/Stable",
@@ -30,6 +40,8 @@ classifiers = [
30
40
  "Programming Language :: Python :: 3.12",
31
41
  "Programming Language :: Python :: 3.13",
32
42
  "Topic :: Scientific/Engineering :: Mathematics",
43
+ "Topic :: Scientific/Engineering :: Information Analysis",
44
+ "Topic :: Scientific/Engineering",
33
45
  ]
34
46
  dependencies = [
35
47
  "numpy>=1.20.0",
@@ -46,10 +58,15 @@ dev = [
46
58
  "ruff>=0.1.0",
47
59
  "mypy>=1.0",
48
60
  "maturin>=1.4,<2.0",
61
+ "matplotlib>=3.5",
49
62
  ]
50
63
  docs = [
51
64
  "sphinx>=6.0",
52
- "sphinx-rtd-theme>=1.0",
65
+ "pydata-sphinx-theme>=0.15",
66
+ "sphinxext-opengraph>=0.9",
67
+ "sphinx-sitemap>=2.5",
68
+ "nbsphinx>=0.9",
69
+ "matplotlib>=3.5",
53
70
  ]
54
71
 
55
72
  [project.urls]
@@ -197,7 +197,7 @@ checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8"
197
197
 
198
198
  [[package]]
199
199
  name = "diff_diff_rust"
200
- version = "2.7.1"
200
+ version = "2.7.2"
201
201
  dependencies = [
202
202
  "blas-src",
203
203
  "faer",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "diff_diff_rust"
3
- version = "2.7.1"
3
+ version = "2.7.2"
4
4
  edition = "2021"
5
5
  description = "Rust backend for diff-diff DiD library"
6
6
  license = "MIT"
@@ -42,14 +42,14 @@ fn _rust_backend(m: &Bound<'_, PyModule>) -> PyResult<()> {
42
42
  m.add_function(wrap_pyfunction!(linalg::solve_ols, m)?)?;
43
43
  m.add_function(wrap_pyfunction!(linalg::compute_robust_vcov, m)?)?;
44
44
 
45
- // TROP estimator acceleration (twostep method)
45
+ // TROP estimator acceleration (local method)
46
46
  m.add_function(wrap_pyfunction!(trop::compute_unit_distance_matrix, m)?)?;
47
47
  m.add_function(wrap_pyfunction!(trop::loocv_grid_search, m)?)?;
48
48
  m.add_function(wrap_pyfunction!(trop::bootstrap_trop_variance, m)?)?;
49
49
 
50
- // TROP estimator acceleration (joint method)
51
- m.add_function(wrap_pyfunction!(trop::loocv_grid_search_joint, m)?)?;
52
- m.add_function(wrap_pyfunction!(trop::bootstrap_trop_variance_joint, m)?)?;
50
+ // TROP estimator acceleration (global method)
51
+ m.add_function(wrap_pyfunction!(trop::loocv_grid_search_global, m)?)?;
52
+ m.add_function(wrap_pyfunction!(trop::bootstrap_trop_variance_global, m)?)?;
53
53
 
54
54
  // Diagnostics
55
55
  m.add_function(wrap_pyfunction!(rust_backend_info, m)?)?;
@@ -1081,12 +1081,16 @@ pub fn bootstrap_trop_variance<'py>(
1081
1081
  }
1082
1082
 
1083
1083
  // ============================================================================
1084
- // Joint method implementation
1084
+ // Global method implementation
1085
+ //
1086
+ // Note: Only the #[pyfunction] exports were renamed (joint → global) to match
1087
+ // the Python public API. The private Rust helpers below retain their original
1088
+ // `*_joint*` names to keep the Rust-only rename scope minimal.
1085
1089
  // ============================================================================
1086
1090
 
1087
- /// Compute global weights for joint method estimation.
1091
+ /// Compute global weights for global method estimation.
1088
1092
  ///
1089
- /// Unlike twostep (which computes per-observation weights), joint uses global
1093
+ /// Unlike local (which computes per-observation weights), global uses
1090
1094
  /// weights based on:
1091
1095
  /// - Time weights: distance to center of treated block
1092
1096
  /// - Unit weights: RMSE to average treated trajectory over pre-periods
@@ -1196,7 +1200,7 @@ fn compute_joint_weights(
1196
1200
  delta
1197
1201
  }
1198
1202
 
1199
- /// Solve joint TWFE via weighted least squares (no low-rank, no tau).
1203
+ /// Solve global TWFE via weighted least squares (no low-rank, no tau).
1200
1204
  ///
1201
1205
  /// Minimizes: min Σ δ_{it}(Y_{it} - μ - α_i - β_t)²
1202
1206
  ///
@@ -1315,7 +1319,7 @@ fn solve_joint_no_lowrank(
1315
1319
  Some((mu, alpha, beta))
1316
1320
  }
1317
1321
 
1318
- /// Solve joint TWFE + low-rank via alternating minimization (no tau).
1322
+ /// Solve global TWFE + low-rank via alternating minimization (no tau).
1319
1323
  ///
1320
1324
  /// Minimizes: min Σ δ_{it}(Y_{it} - μ - α_i - β_t - L_{it})² + λ_nn||L||_*
1321
1325
  ///
@@ -1422,12 +1426,12 @@ fn solve_joint_with_lowrank(
1422
1426
  Some((mu, alpha, beta, l))
1423
1427
  }
1424
1428
 
1425
- /// Compute LOOCV score for joint method with specific parameter combination.
1429
+ /// Compute LOOCV score for global method with specific parameter combination.
1426
1430
  ///
1427
1431
  /// Following paper's Equation 5:
1428
1432
  /// Q(λ) = Σ_{j,s: D_js=0} [τ̂_js^loocv(λ)]²
1429
1433
  ///
1430
- /// For joint method, we exclude each control observation, fit the joint model
1434
+ /// For global method, we exclude each control observation, fit the global model
1431
1435
  /// on remaining data, and compute the pseudo-treatment effect at the excluded obs.
1432
1436
  ///
1433
1437
  /// # Returns
@@ -1502,7 +1506,7 @@ fn loocv_score_joint(
1502
1506
  }
1503
1507
  }
1504
1508
 
1505
- /// Perform LOOCV grid search for joint method using parallel grid search.
1509
+ /// Perform LOOCV grid search for global method using parallel grid search.
1506
1510
  ///
1507
1511
  /// Evaluates all combinations of (lambda_time, lambda_unit, lambda_nn) in parallel
1508
1512
  /// and returns the combination with lowest LOOCV score.
@@ -1522,7 +1526,7 @@ fn loocv_score_joint(
1522
1526
  #[pyfunction]
1523
1527
  #[pyo3(signature = (y, d, control_mask, lambda_time_grid, lambda_unit_grid, lambda_nn_grid, max_iter, tol))]
1524
1528
  #[allow(clippy::too_many_arguments)]
1525
- pub fn loocv_grid_search_joint<'py>(
1529
+ pub fn loocv_grid_search_global<'py>(
1526
1530
  _py: Python<'py>,
1527
1531
  y: PyReadonlyArray2<'py, f64>,
1528
1532
  d: PyReadonlyArray2<'py, f64>,
@@ -1630,7 +1634,7 @@ pub fn loocv_grid_search_joint<'py>(
1630
1634
  Ok((best_lt, best_lu, best_ln, best_score, n_valid, n_attempted, first_failed))
1631
1635
  }
1632
1636
 
1633
- /// Compute bootstrap variance estimation for TROP joint method in parallel.
1637
+ /// Compute bootstrap variance estimation for TROP global method in parallel.
1634
1638
  ///
1635
1639
  /// Performs unit-level block bootstrap, parallelizing across bootstrap iterations.
1636
1640
  /// Uses stratified sampling to preserve treated/control unit ratio.
@@ -1651,7 +1655,7 @@ pub fn loocv_grid_search_joint<'py>(
1651
1655
  #[pyfunction]
1652
1656
  #[pyo3(signature = (y, d, lambda_time, lambda_unit, lambda_nn, n_bootstrap, max_iter, tol, seed))]
1653
1657
  #[allow(clippy::too_many_arguments)]
1654
- pub fn bootstrap_trop_variance_joint<'py>(
1658
+ pub fn bootstrap_trop_variance_global<'py>(
1655
1659
  py: Python<'py>,
1656
1660
  y: PyReadonlyArray2<'py, f64>,
1657
1661
  d: PyReadonlyArray2<'py, f64>,
@@ -1737,7 +1741,7 @@ pub fn bootstrap_trop_variance_joint<'py>(
1737
1741
  }
1738
1742
  }
1739
1743
 
1740
- // Compute weights and fit joint model
1744
+ // Compute weights and fit global model
1741
1745
  let delta = compute_joint_weights(
1742
1746
  &y_boot.view(),
1743
1747
  &d_boot.view(),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes