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.
- {diff_diff-1.3.1 → diff_diff-2.0.0}/PKG-INFO +19 -18
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/__init__.py +13 -1
- diff_diff-2.0.0/diff_diff/_backend.py +64 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/estimators.py +29 -38
- diff_diff-2.0.0/diff_diff/linalg.py +418 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/staggered.py +481 -219
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/sun_abraham.py +2 -2
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/synthetic_did.py +3 -3
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/triple_diff.py +14 -20
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/twfe.py +10 -6
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/utils.py +56 -62
- {diff_diff-1.3.1 → diff_diff-2.0.0}/pyproject.toml +14 -5
- diff_diff-2.0.0/rust/Cargo.lock +2396 -0
- diff_diff-2.0.0/rust/Cargo.toml +39 -0
- diff_diff-2.0.0/rust/src/bootstrap.rs +223 -0
- diff_diff-2.0.0/rust/src/lib.rs +33 -0
- diff_diff-2.0.0/rust/src/linalg.rs +229 -0
- diff_diff-2.0.0/rust/src/weights.rs +220 -0
- diff_diff-1.3.1/diff_diff.egg-info/PKG-INFO +0 -2255
- diff_diff-1.3.1/diff_diff.egg-info/SOURCES.txt +0 -36
- diff_diff-1.3.1/diff_diff.egg-info/dependency_links.txt +0 -1
- diff_diff-1.3.1/diff_diff.egg-info/requires.txt +0 -14
- diff_diff-1.3.1/diff_diff.egg-info/top_level.txt +0 -1
- diff_diff-1.3.1/setup.cfg +0 -4
- diff_diff-1.3.1/tests/test_bacon.py +0 -679
- diff_diff-1.3.1/tests/test_diagnostics.py +0 -674
- diff_diff-1.3.1/tests/test_estimators.py +0 -2711
- diff_diff-1.3.1/tests/test_honest_did.py +0 -699
- diff_diff-1.3.1/tests/test_power.py +0 -691
- diff_diff-1.3.1/tests/test_prep.py +0 -794
- diff_diff-1.3.1/tests/test_pretrends.py +0 -813
- diff_diff-1.3.1/tests/test_staggered.py +0 -1358
- diff_diff-1.3.1/tests/test_sun_abraham.py +0 -732
- diff_diff-1.3.1/tests/test_triple_diff.py +0 -869
- diff_diff-1.3.1/tests/test_utils.py +0 -1270
- diff_diff-1.3.1/tests/test_visualization.py +0 -284
- diff_diff-1.3.1/tests/test_wild_bootstrap.py +0 -804
- {diff_diff-1.3.1 → diff_diff-2.0.0}/README.md +0 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/bacon.py +0 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/diagnostics.py +0 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/honest_did.py +0 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/power.py +0 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/prep.py +0 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/pretrends.py +0 -0
- {diff_diff-1.3.1 → diff_diff-2.0.0}/diff_diff/results.py +0 -0
- {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:
|
|
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
|
-
|
|
34
|
-
|
|
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__ = "
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
-
#
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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 =
|
|
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,
|
|
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 =
|
|
894
|
+
vcov = compute_robust_vcov(X, residuals, cluster_ids)
|
|
904
895
|
elif self.robust:
|
|
905
|
-
vcov =
|
|
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
|
|
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))
|