diff-diff 2.1.9__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diff_diff-2.1.9 → diff_diff-2.2.0}/PKG-INFO +1 -1
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/__init__.py +1 -1
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/linalg.py +49 -8
- {diff_diff-2.1.9 → diff_diff-2.2.0}/pyproject.toml +1 -1
- diff_diff-2.2.0/rust/Cargo.lock +1540 -0
- diff_diff-2.2.0/rust/Cargo.toml +34 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/rust/src/bootstrap.rs +3 -3
- {diff_diff-2.1.9 → diff_diff-2.2.0}/rust/src/lib.rs +1 -1
- {diff_diff-2.1.9 → diff_diff-2.2.0}/rust/src/linalg.rs +199 -67
- {diff_diff-2.1.9 → diff_diff-2.2.0}/rust/src/trop.rs +60 -29
- {diff_diff-2.1.9 → diff_diff-2.2.0}/rust/src/weights.rs +5 -5
- diff_diff-2.1.9/rust/Cargo.lock +0 -2321
- diff_diff-2.1.9/rust/Cargo.toml +0 -43
- {diff_diff-2.1.9 → diff_diff-2.2.0}/README.md +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/_backend.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/bacon.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/datasets.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/diagnostics.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/estimators.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/honest_did.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/power.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/prep.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/prep_dgp.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/pretrends.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/results.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/staggered.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/staggered_aggregation.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/staggered_bootstrap.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/staggered_results.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/sun_abraham.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/triple_diff.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/trop.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/twfe.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/utils.py +0 -0
- {diff_diff-2.1.9 → diff_diff-2.2.0}/diff_diff/visualization.py +0 -0
|
@@ -251,10 +251,10 @@ def _solve_ols_rust(
|
|
|
251
251
|
cluster_ids: Optional[np.ndarray] = None,
|
|
252
252
|
return_vcov: bool = True,
|
|
253
253
|
return_fitted: bool = False,
|
|
254
|
-
) -> Union[
|
|
254
|
+
) -> Optional[Union[
|
|
255
255
|
Tuple[np.ndarray, np.ndarray, Optional[np.ndarray]],
|
|
256
256
|
Tuple[np.ndarray, np.ndarray, np.ndarray, Optional[np.ndarray]],
|
|
257
|
-
]:
|
|
257
|
+
]]:
|
|
258
258
|
"""
|
|
259
259
|
Rust backend implementation of solve_ols for full-rank matrices.
|
|
260
260
|
|
|
@@ -296,15 +296,30 @@ def _solve_ols_rust(
|
|
|
296
296
|
Fitted values if return_fitted=True.
|
|
297
297
|
vcov : np.ndarray, optional
|
|
298
298
|
Variance-covariance matrix if return_vcov=True.
|
|
299
|
+
None
|
|
300
|
+
If Rust backend detects numerical instability and caller should
|
|
301
|
+
fall back to Python backend.
|
|
299
302
|
"""
|
|
300
303
|
# Convert cluster_ids to int64 for Rust (handles string/categorical IDs)
|
|
301
304
|
if cluster_ids is not None:
|
|
302
305
|
cluster_ids = _factorize_cluster_ids(cluster_ids)
|
|
303
306
|
|
|
304
|
-
# Call Rust backend
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
307
|
+
# Call Rust backend with fallback on numerical instability
|
|
308
|
+
try:
|
|
309
|
+
coefficients, residuals, vcov = _rust_solve_ols(
|
|
310
|
+
X, y, cluster_ids=cluster_ids, return_vcov=return_vcov
|
|
311
|
+
)
|
|
312
|
+
except ValueError as e:
|
|
313
|
+
error_msg = str(e).lower()
|
|
314
|
+
if "numerically unstable" in error_msg or "singular" in error_msg:
|
|
315
|
+
warnings.warn(
|
|
316
|
+
f"Rust backend detected numerical instability: {e}. "
|
|
317
|
+
"Falling back to Python backend.",
|
|
318
|
+
UserWarning,
|
|
319
|
+
stacklevel=3,
|
|
320
|
+
)
|
|
321
|
+
return None # Signal caller to use Python fallback
|
|
322
|
+
raise
|
|
308
323
|
|
|
309
324
|
# Convert to numpy arrays
|
|
310
325
|
coefficients = np.asarray(coefficients)
|
|
@@ -468,12 +483,15 @@ def solve_ols(
|
|
|
468
483
|
# This saves O(nk²) QR overhead but won't detect rank-deficient matrices
|
|
469
484
|
if skip_rank_check:
|
|
470
485
|
if HAS_RUST_BACKEND and _rust_solve_ols is not None:
|
|
471
|
-
|
|
486
|
+
result = _solve_ols_rust(
|
|
472
487
|
X, y,
|
|
473
488
|
cluster_ids=cluster_ids,
|
|
474
489
|
return_vcov=return_vcov,
|
|
475
490
|
return_fitted=return_fitted,
|
|
476
491
|
)
|
|
492
|
+
if result is not None:
|
|
493
|
+
return result
|
|
494
|
+
# Fall through to NumPy on numerical instability
|
|
477
495
|
# Fall through to Python without rank check (user guarantees full rank)
|
|
478
496
|
return _solve_ols_numpy(
|
|
479
497
|
X, y,
|
|
@@ -499,6 +517,7 @@ def solve_ols(
|
|
|
499
517
|
# Routing strategy:
|
|
500
518
|
# - Full-rank + Rust available → fast Rust backend (SVD-based solve)
|
|
501
519
|
# - Rank-deficient → Python backend (proper NA handling, valid SEs)
|
|
520
|
+
# - Rust numerical instability → Python fallback (via None return)
|
|
502
521
|
# - No Rust → Python backend (works for all cases)
|
|
503
522
|
if HAS_RUST_BACKEND and _rust_solve_ols is not None and not is_rank_deficient:
|
|
504
523
|
result = _solve_ols_rust(
|
|
@@ -508,6 +527,19 @@ def solve_ols(
|
|
|
508
527
|
return_fitted=return_fitted,
|
|
509
528
|
)
|
|
510
529
|
|
|
530
|
+
# Check for None: Rust backend detected numerical instability and
|
|
531
|
+
# signaled us to fall back to Python backend
|
|
532
|
+
if result is None:
|
|
533
|
+
return _solve_ols_numpy(
|
|
534
|
+
X, y,
|
|
535
|
+
cluster_ids=cluster_ids,
|
|
536
|
+
return_vcov=return_vcov,
|
|
537
|
+
return_fitted=return_fitted,
|
|
538
|
+
rank_deficient_action=rank_deficient_action,
|
|
539
|
+
column_names=column_names,
|
|
540
|
+
_precomputed_rank_info=None, # Force fresh rank detection
|
|
541
|
+
)
|
|
542
|
+
|
|
511
543
|
# Check for NaN vcov: Rust SVD may detect rank-deficiency that QR missed
|
|
512
544
|
# for ill-conditioned matrices (QR and SVD have different numerical properties).
|
|
513
545
|
# When this happens, fall back to Python's R-style handling.
|
|
@@ -732,7 +764,7 @@ def compute_robust_vcov(
|
|
|
732
764
|
try:
|
|
733
765
|
return _rust_compute_robust_vcov(X, residuals, cluster_ids_int)
|
|
734
766
|
except ValueError as e:
|
|
735
|
-
# Translate Rust
|
|
767
|
+
# Translate Rust errors to consistent Python error messages or fallback
|
|
736
768
|
error_msg = str(e)
|
|
737
769
|
if "Matrix inversion failed" in error_msg:
|
|
738
770
|
raise ValueError(
|
|
@@ -740,6 +772,15 @@ def compute_robust_vcov(
|
|
|
740
772
|
"This indicates perfect multicollinearity. Check your fixed effects "
|
|
741
773
|
"and covariates for linear dependencies."
|
|
742
774
|
) from e
|
|
775
|
+
if "numerically unstable" in error_msg.lower():
|
|
776
|
+
# Fall back to NumPy on numerical instability (with warning)
|
|
777
|
+
warnings.warn(
|
|
778
|
+
f"Rust backend detected numerical instability: {e}. "
|
|
779
|
+
"Falling back to Python backend for variance computation.",
|
|
780
|
+
UserWarning,
|
|
781
|
+
stacklevel=2,
|
|
782
|
+
)
|
|
783
|
+
return _compute_robust_vcov_numpy(X, residuals, cluster_ids)
|
|
743
784
|
raise
|
|
744
785
|
|
|
745
786
|
# Fallback to NumPy implementation
|