diff-diff 1.3.1__tar.gz → 2.0.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 (46) hide show
  1. {diff_diff-1.3.1 → diff_diff-2.0.0}/PKG-INFO +19 -18
  2. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/__init__.py +13 -1
  3. diff_diff-2.0.0/diff_diff/_backend.py +64 -0
  4. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/estimators.py +29 -38
  5. diff_diff-2.0.0/diff_diff/linalg.py +418 -0
  6. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/staggered.py +481 -219
  7. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/sun_abraham.py +2 -2
  8. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/synthetic_did.py +3 -3
  9. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/triple_diff.py +14 -20
  10. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/twfe.py +10 -6
  11. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/utils.py +56 -62
  12. {diff_diff-1.3.1 → diff_diff-2.0.0}/pyproject.toml +14 -5
  13. diff_diff-2.0.0/rust/Cargo.lock +2396 -0
  14. diff_diff-2.0.0/rust/Cargo.toml +39 -0
  15. diff_diff-2.0.0/rust/src/bootstrap.rs +223 -0
  16. diff_diff-2.0.0/rust/src/lib.rs +33 -0
  17. diff_diff-2.0.0/rust/src/linalg.rs +229 -0
  18. diff_diff-2.0.0/rust/src/weights.rs +220 -0
  19. diff_diff-1.3.1/diff_diff.egg-info/PKG-INFO +0 -2255
  20. diff_diff-1.3.1/diff_diff.egg-info/SOURCES.txt +0 -36
  21. diff_diff-1.3.1/diff_diff.egg-info/dependency_links.txt +0 -1
  22. diff_diff-1.3.1/diff_diff.egg-info/requires.txt +0 -14
  23. diff_diff-1.3.1/diff_diff.egg-info/top_level.txt +0 -1
  24. diff_diff-1.3.1/setup.cfg +0 -4
  25. diff_diff-1.3.1/tests/test_bacon.py +0 -679
  26. diff_diff-1.3.1/tests/test_diagnostics.py +0 -674
  27. diff_diff-1.3.1/tests/test_estimators.py +0 -2711
  28. diff_diff-1.3.1/tests/test_honest_did.py +0 -699
  29. diff_diff-1.3.1/tests/test_power.py +0 -691
  30. diff_diff-1.3.1/tests/test_prep.py +0 -794
  31. diff_diff-1.3.1/tests/test_pretrends.py +0 -813
  32. diff_diff-1.3.1/tests/test_staggered.py +0 -1358
  33. diff_diff-1.3.1/tests/test_sun_abraham.py +0 -732
  34. diff_diff-1.3.1/tests/test_triple_diff.py +0 -869
  35. diff_diff-1.3.1/tests/test_utils.py +0 -1270
  36. diff_diff-1.3.1/tests/test_visualization.py +0 -284
  37. diff_diff-1.3.1/tests/test_wild_bootstrap.py +0 -804
  38. {diff_diff-1.3.1 → diff_diff-2.0.0}/README.md +0 -0
  39. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/bacon.py +0 -0
  40. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/diagnostics.py +0 -0
  41. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/honest_did.py +0 -0
  42. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/power.py +0 -0
  43. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/prep.py +0 -0
  44. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/pretrends.py +0 -0
  45. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/results.py +0 -0
  46. {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/visualization.py +0 -0
@@ -1,14 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 1.3.1
4
- Summary: A library for Difference-in-Differences causal inference analysis
5
- Author: diff-diff contributors
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/igerber/diff-diff
8
- Project-URL: Documentation, https://diff-diff.readthedocs.io
9
- Project-URL: Repository, https://github.com/igerber/diff-diff
10
- Project-URL: Issues, https://github.com/igerber/diff-diff/issues
11
- Keywords: causal-inference,difference-in-differences,econometrics,statistics,treatment-effects
3
+ Version: 2.0.0
12
4
  Classifier: Development Status :: 5 - Production/Stable
13
5
  Classifier: Intended Audience :: Science/Research
14
6
  Classifier: Operating System :: OS Independent
@@ -18,20 +10,28 @@ Classifier: Programming Language :: Python :: 3.10
18
10
  Classifier: Programming Language :: Python :: 3.11
19
11
  Classifier: Programming Language :: Python :: 3.12
20
12
  Classifier: Topic :: Scientific/Engineering :: Mathematics
21
- Requires-Python: >=3.9
22
- Description-Content-Type: text/markdown
23
13
  Requires-Dist: numpy>=1.20.0
24
14
  Requires-Dist: pandas>=1.3.0
25
15
  Requires-Dist: scipy>=1.7.0
16
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
17
+ Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
18
+ Requires-Dist: black>=23.0 ; extra == 'dev'
19
+ Requires-Dist: ruff>=0.1.0 ; extra == 'dev'
20
+ Requires-Dist: mypy>=1.0 ; extra == 'dev'
21
+ Requires-Dist: sphinx>=6.0 ; extra == 'docs'
22
+ Requires-Dist: sphinx-rtd-theme>=1.0 ; extra == 'docs'
26
23
  Provides-Extra: dev
27
- Requires-Dist: pytest>=7.0; extra == "dev"
28
- Requires-Dist: pytest-cov>=4.0; extra == "dev"
29
- Requires-Dist: black>=23.0; extra == "dev"
30
- Requires-Dist: ruff>=0.1.0; extra == "dev"
31
- Requires-Dist: mypy>=1.0; extra == "dev"
32
24
  Provides-Extra: docs
33
- Requires-Dist: sphinx>=6.0; extra == "docs"
34
- Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
25
+ Summary: A library for Difference-in-Differences causal inference analysis
26
+ Keywords: causal-inference,difference-in-differences,econometrics,statistics,treatment-effects
27
+ Author: diff-diff contributors
28
+ License-Expression: MIT
29
+ Requires-Python: >=3.9
30
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
31
+ Project-URL: Documentation, https://diff-diff.readthedocs.io
32
+ Project-URL: Homepage, https://github.com/igerber/diff-diff
33
+ Project-URL: Issues, https://github.com/igerber/diff-diff/issues
34
+ Project-URL: Repository, https://github.com/igerber/diff-diff
35
35
 
36
36
  # diff-diff
37
37
 
@@ -2253,3 +2253,4 @@ The `HonestDiD` module implements sensitivity analysis methods for relaxing the
2253
2253
  ## License
2254
2254
 
2255
2255
  MIT License
2256
+
@@ -5,6 +5,16 @@ This library provides sklearn-like estimators for causal inference
5
5
  using the difference-in-differences methodology.
6
6
  """
7
7
 
8
+ # Import backend detection from dedicated module (avoids circular imports)
9
+ from diff_diff._backend import (
10
+ HAS_RUST_BACKEND,
11
+ _rust_bootstrap_weights,
12
+ _rust_compute_robust_vcov,
13
+ _rust_project_simplex,
14
+ _rust_solve_ols,
15
+ _rust_synthetic_weights,
16
+ )
17
+
8
18
  from diff_diff.bacon import (
9
19
  BaconDecomposition,
10
20
  BaconDecompositionResults,
@@ -103,7 +113,7 @@ from diff_diff.visualization import (
103
113
  plot_sensitivity,
104
114
  )
105
115
 
106
- __version__ = "1.3.1"
116
+ __version__ = "2.0.0"
107
117
  __all__ = [
108
118
  # Estimators
109
119
  "DifferenceInDifferences",
@@ -187,4 +197,6 @@ __all__ = [
187
197
  "compute_pretrends_power",
188
198
  "compute_mdv",
189
199
  "plot_pretrends_power",
200
+ # Rust backend
201
+ "HAS_RUST_BACKEND",
190
202
  ]
@@ -0,0 +1,64 @@
1
+ """
2
+ Backend detection and configuration for diff-diff.
3
+
4
+ This module handles:
5
+ 1. Detection of optional Rust backend
6
+ 2. Environment variable configuration (DIFF_DIFF_BACKEND)
7
+ 3. Exports HAS_RUST_BACKEND and Rust function references
8
+
9
+ Other modules should import from here to avoid circular imports with __init__.py.
10
+ """
11
+
12
+ import os
13
+
14
+ # Check for backend override via environment variable
15
+ # DIFF_DIFF_BACKEND can be: 'auto' (default), 'python', or 'rust'
16
+ _backend_env = os.environ.get('DIFF_DIFF_BACKEND', 'auto').lower()
17
+
18
+ # Try to import Rust backend for accelerated operations
19
+ try:
20
+ from diff_diff._rust_backend import (
21
+ generate_bootstrap_weights_batch as _rust_bootstrap_weights,
22
+ compute_synthetic_weights as _rust_synthetic_weights,
23
+ project_simplex as _rust_project_simplex,
24
+ solve_ols as _rust_solve_ols,
25
+ compute_robust_vcov as _rust_compute_robust_vcov,
26
+ )
27
+ _rust_available = True
28
+ except ImportError:
29
+ _rust_available = False
30
+ _rust_bootstrap_weights = None
31
+ _rust_synthetic_weights = None
32
+ _rust_project_simplex = None
33
+ _rust_solve_ols = None
34
+ _rust_compute_robust_vcov = None
35
+
36
+ # Determine final backend based on environment variable and availability
37
+ if _backend_env == 'python':
38
+ # Force pure Python mode - disable Rust even if available
39
+ HAS_RUST_BACKEND = False
40
+ _rust_bootstrap_weights = None
41
+ _rust_synthetic_weights = None
42
+ _rust_project_simplex = None
43
+ _rust_solve_ols = None
44
+ _rust_compute_robust_vcov = None
45
+ elif _backend_env == 'rust':
46
+ # Force Rust mode - fail if not available
47
+ if not _rust_available:
48
+ raise ImportError(
49
+ "DIFF_DIFF_BACKEND=rust but Rust backend is not available. "
50
+ "Install with: pip install diff-diff[rust]"
51
+ )
52
+ HAS_RUST_BACKEND = True
53
+ else:
54
+ # Auto mode - use Rust if available
55
+ HAS_RUST_BACKEND = _rust_available
56
+
57
+ __all__ = [
58
+ 'HAS_RUST_BACKEND',
59
+ '_rust_bootstrap_weights',
60
+ '_rust_synthetic_weights',
61
+ '_rust_project_simplex',
62
+ '_rust_solve_ols',
63
+ '_rust_compute_robust_vcov',
64
+ ]
@@ -17,12 +17,12 @@ from typing import Any, Dict, List, Optional, Tuple
17
17
  import numpy as np
18
18
  import pandas as pd
19
19
 
20
+ from diff_diff.linalg import compute_r_squared, compute_robust_vcov, solve_ols
20
21
  from diff_diff.results import DiDResults, MultiPeriodDiDResults, PeriodEffect
21
22
  from diff_diff.utils import (
22
23
  WildBootstrapResults,
23
24
  compute_confidence_interval,
24
25
  compute_p_value,
25
- compute_robust_se,
26
26
  validate_binary,
27
27
  wild_bootstrap_se,
28
28
  )
@@ -261,8 +261,11 @@ class DifferenceInDifferences:
261
261
  X = np.column_stack([X, dummies[col].values.astype(float)])
262
262
  var_names.append(col)
263
263
 
264
- # Fit OLS
265
- coefficients, residuals, fitted, r_squared = self._fit_ols(X, y)
264
+ # Fit OLS using unified backend
265
+ coefficients, residuals, fitted, vcov = solve_ols(
266
+ X, y, return_fitted=True, return_vcov=False
267
+ )
268
+ r_squared = compute_r_squared(y, residuals)
266
269
 
267
270
  # Extract ATT (coefficient on interaction term)
268
271
  att_idx = 3 # Index of interaction term
@@ -285,13 +288,13 @@ class DifferenceInDifferences:
285
288
  )
286
289
  elif self.cluster is not None:
287
290
  cluster_ids = data[self.cluster].values
288
- vcov = compute_robust_se(X, residuals, cluster_ids)
291
+ vcov = compute_robust_vcov(X, residuals, cluster_ids)
289
292
  se = np.sqrt(vcov[att_idx, att_idx])
290
293
  t_stat = att / se
291
294
  p_value = compute_p_value(t_stat, df=df)
292
295
  conf_int = compute_confidence_interval(att, se, self.alpha, df=df)
293
296
  elif self.robust:
294
- vcov = compute_robust_se(X, residuals)
297
+ vcov = compute_robust_vcov(X, residuals)
295
298
  se = np.sqrt(vcov[att_idx, att_idx])
296
299
  t_stat = att / se
297
300
  p_value = compute_p_value(t_stat, df=df)
@@ -300,7 +303,7 @@ class DifferenceInDifferences:
300
303
  # Classical OLS standard errors
301
304
  n = len(y)
302
305
  k = X.shape[1]
303
- mse = np.sum(residuals ** 2) / (n - k)
306
+ mse = np.sum(residuals**2) / (n - k)
304
307
  # Use solve() instead of inv() for numerical stability
305
308
  # solve(A, B) computes X where AX=B, so this yields (X'X)^{-1} * mse
306
309
  vcov = np.linalg.solve(X.T @ X, mse * np.eye(k))
@@ -352,10 +355,15 @@ class DifferenceInDifferences:
352
355
 
353
356
  return self.results_
354
357
 
355
- def _fit_ols(self, X: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float]:
358
+ def _fit_ols(
359
+ self, X: np.ndarray, y: np.ndarray
360
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float]:
356
361
  """
357
362
  Fit OLS regression.
358
363
 
364
+ This method is kept for backwards compatibility. Internally uses the
365
+ unified solve_ols from diff_diff.linalg for optimized computation.
366
+
359
367
  Parameters
360
368
  ----------
361
369
  X : np.ndarray
@@ -367,32 +375,12 @@ class DifferenceInDifferences:
367
375
  -------
368
376
  tuple
369
377
  (coefficients, residuals, fitted_values, r_squared)
370
-
371
- Raises
372
- ------
373
- ValueError
374
- If design matrix is rank-deficient (perfect multicollinearity).
375
378
  """
376
- # Check for rank deficiency (perfect multicollinearity)
377
- rank = np.linalg.matrix_rank(X)
378
- if rank < X.shape[1]:
379
- raise ValueError(
380
- f"Design matrix is rank-deficient (rank {rank} < {X.shape[1]} columns). "
381
- "This indicates perfect multicollinearity. Check your fixed effects "
382
- "and covariates for linear dependencies."
383
- )
384
-
385
- # Solve normal equations: β = (X'X)^(-1) X'y
386
- coefficients = np.linalg.lstsq(X, y, rcond=None)[0]
387
-
388
- # Compute fitted values and residuals
389
- fitted = X @ coefficients
390
- residuals = y - fitted
391
-
392
- # Compute R-squared
393
- ss_res = np.sum(residuals ** 2)
394
- ss_tot = np.sum((y - np.mean(y)) ** 2)
395
- r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0.0
379
+ # Use unified OLS backend
380
+ coefficients, residuals, fitted, _ = solve_ols(
381
+ X, y, return_fitted=True, return_vcov=False
382
+ )
383
+ r_squared = compute_r_squared(y, residuals)
396
384
 
397
385
  return coefficients, residuals, fitted, r_squared
398
386
 
@@ -442,7 +430,7 @@ class DifferenceInDifferences:
442
430
  t_stat = bootstrap_results.t_stat_original
443
431
 
444
432
  # Also compute vcov for storage (using cluster-robust for consistency)
445
- vcov = compute_robust_se(X, residuals, cluster_ids)
433
+ vcov = compute_robust_vcov(X, residuals, cluster_ids)
446
434
 
447
435
  return se, p_value, conf_int, t_stat, vcov, bootstrap_results
448
436
 
@@ -889,8 +877,11 @@ class MultiPeriodDiD(DifferenceInDifferences):
889
877
  X = np.column_stack([X, dummies[col].values.astype(float)])
890
878
  var_names.append(col)
891
879
 
892
- # Fit OLS
893
- coefficients, residuals, fitted, r_squared = self._fit_ols(X, y)
880
+ # Fit OLS using unified backend
881
+ coefficients, residuals, fitted, _ = solve_ols(
882
+ X, y, return_fitted=True, return_vcov=False
883
+ )
884
+ r_squared = compute_r_squared(y, residuals)
894
885
 
895
886
  # Degrees of freedom
896
887
  df = len(y) - X.shape[1] - n_absorbed_effects
@@ -900,13 +891,13 @@ class MultiPeriodDiD(DifferenceInDifferences):
900
891
  # For now, we use analytical inference even if inference="wild_bootstrap"
901
892
  if self.cluster is not None:
902
893
  cluster_ids = data[self.cluster].values
903
- vcov = compute_robust_se(X, residuals, cluster_ids)
894
+ vcov = compute_robust_vcov(X, residuals, cluster_ids)
904
895
  elif self.robust:
905
- vcov = compute_robust_se(X, residuals)
896
+ vcov = compute_robust_vcov(X, residuals)
906
897
  else:
907
898
  n = len(y)
908
899
  k = X.shape[1]
909
- mse = np.sum(residuals ** 2) / (n - k)
900
+ mse = np.sum(residuals**2) / (n - k)
910
901
  # Use solve() instead of inv() for numerical stability
911
902
  # solve(A, B) computes X where AX=B, so this yields (X'X)^{-1} * mse
912
903
  vcov = np.linalg.solve(X.T @ X, mse * np.eye(k))