cbps 0.2.0__py3-none-any.whl
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.
- cbps/__init__.py +3462 -0
- cbps/constants.py +46 -0
- cbps/core/__init__.py +93 -0
- cbps/core/cbps_binary.py +1943 -0
- cbps/core/cbps_continuous.py +945 -0
- cbps/core/cbps_multitreat.py +1123 -0
- cbps/core/cbps_optimal.py +507 -0
- cbps/core/results.py +1447 -0
- cbps/data/Blackwell.csv +571 -0
- cbps/data/LaLonde.csv +3213 -0
- cbps/data/npcbps_continuous_sim.csv +501 -0
- cbps/data/nsw.csv +723 -0
- cbps/data/nsw_dw.csv +446 -0
- cbps/data/political_ads_urban_niebler.csv +16266 -0
- cbps/data/psid_controls.csv +2491 -0
- cbps/data/psid_controls2.csv +254 -0
- cbps/data/psid_controls3.csv +129 -0
- cbps/data/simulation_dgp1_seed12345.csv +201 -0
- cbps/data/simulation_dgp2_seed12345.csv +201 -0
- cbps/data/simulation_dgp3_seed12345.csv +201 -0
- cbps/data/simulation_dgp4_seed12345.csv +201 -0
- cbps/datasets/__init__.py +78 -0
- cbps/datasets/blackwell.py +112 -0
- cbps/datasets/continuous.py +223 -0
- cbps/datasets/lalonde.py +272 -0
- cbps/datasets/npcbps_sim.py +101 -0
- cbps/diagnostics/__init__.py +101 -0
- cbps/diagnostics/balance.py +760 -0
- cbps/diagnostics/balance_cbmsm_addon.py +162 -0
- cbps/diagnostics/continuous_diagnostics.py +259 -0
- cbps/diagnostics/normality.py +173 -0
- cbps/diagnostics/ocbps_conditions.py +197 -0
- cbps/diagnostics/overlap.py +198 -0
- cbps/diagnostics/plots.py +1193 -0
- cbps/diagnostics/weights_diag.py +205 -0
- cbps/highdim/__init__.py +84 -0
- cbps/highdim/gmm_loss.py +340 -0
- cbps/highdim/hdcbps.py +1078 -0
- cbps/highdim/lasso_utils.py +498 -0
- cbps/highdim/weight_funcs.py +298 -0
- cbps/inference/__init__.py +42 -0
- cbps/inference/asyvar.py +621 -0
- cbps/inference/vcov_outcome.py +217 -0
- cbps/iv/__init__.py +48 -0
- cbps/iv/cbiv.py +2603 -0
- cbps/logging_config.py +45 -0
- cbps/msm/__init__.py +45 -0
- cbps/msm/cbmsm.py +1871 -0
- cbps/msm/rank_diagnostics.py +112 -0
- cbps/nonparametric/__init__.py +58 -0
- cbps/nonparametric/cholesky_whitening.py +232 -0
- cbps/nonparametric/empirical_likelihood.py +339 -0
- cbps/nonparametric/npcbps.py +1036 -0
- cbps/nonparametric/taylor_approx.py +207 -0
- cbps/py.typed +0 -0
- cbps/sklearn/__init__.py +42 -0
- cbps/sklearn/estimator.py +378 -0
- cbps/utils/__init__.py +82 -0
- cbps/utils/formula.py +415 -0
- cbps/utils/helpers.py +378 -0
- cbps/utils/numerics.py +438 -0
- cbps/utils/r_compat.py +109 -0
- cbps/utils/validation.py +224 -0
- cbps/utils/variance_transform.py +483 -0
- cbps/utils/weights.py +586 -0
- cbps-0.2.0.dist-info/METADATA +1090 -0
- cbps-0.2.0.dist-info/RECORD +70 -0
- cbps-0.2.0.dist-info/WHEEL +5 -0
- cbps-0.2.0.dist-info/licenses/LICENSE +661 -0
- cbps-0.2.0.dist-info/top_level.txt +1 -0
cbps/utils/numerics.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Numerical Linear Algebra Utilities
|
|
3
|
+
|
|
4
|
+
This module provides numerically stable implementations of matrix operations
|
|
5
|
+
commonly used in CBPS estimation, including pseudoinverses, matrix rank
|
|
6
|
+
computation, and symmetry utilities.
|
|
7
|
+
|
|
8
|
+
The pseudoinverse functions implement tolerance-based singular value truncation
|
|
9
|
+
to handle rank-deficient matrices that arise in high-dimensional settings or
|
|
10
|
+
with collinear covariates.
|
|
11
|
+
|
|
12
|
+
Functions
|
|
13
|
+
---------
|
|
14
|
+
r_ginv_like
|
|
15
|
+
Moore-Penrose pseudoinverse with configurable tolerance.
|
|
16
|
+
pinv_match_r
|
|
17
|
+
Pseudoinverse using NumPy with matched tolerance.
|
|
18
|
+
pinv_symmetric_psd
|
|
19
|
+
Specialized pseudoinverse for symmetric positive semi-definite matrices.
|
|
20
|
+
numeric_rank
|
|
21
|
+
Effective numerical rank via singular value decomposition.
|
|
22
|
+
symmetrize
|
|
23
|
+
Force matrix symmetry by averaging with transpose.
|
|
24
|
+
max_asymmetry
|
|
25
|
+
Measure of matrix asymmetry (infinity norm of A - A.T).
|
|
26
|
+
is_symmetric
|
|
27
|
+
Check if matrix is symmetric within tolerance.
|
|
28
|
+
|
|
29
|
+
References
|
|
30
|
+
----------
|
|
31
|
+
Golub, G. H. and Van Loan, C. F. (2013). Matrix Computations (4th ed.).
|
|
32
|
+
Johns Hopkins University Press.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import warnings
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
import scipy.linalg as la
|
|
39
|
+
from typing import Optional, Tuple, Dict
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def r_ginv_with_diagnostics(
|
|
43
|
+
X: np.ndarray,
|
|
44
|
+
tol: Optional[float] = None,
|
|
45
|
+
warn_threshold: float = 1e12,
|
|
46
|
+
) -> Tuple[np.ndarray, Dict]:
|
|
47
|
+
"""Compute Moore-Penrose pseudoinverse with condition number diagnostics.
|
|
48
|
+
|
|
49
|
+
Matches R MASS::ginv() tolerance (sqrt(eps) * max(singular values)) but
|
|
50
|
+
adds condition number monitoring that R lacks. Does NOT apply
|
|
51
|
+
regularization (no theoretical support in Imai & Ratkovic 2014/2015).
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
X : np.ndarray
|
|
56
|
+
Matrix to pseudoinvert.
|
|
57
|
+
tol : float or None
|
|
58
|
+
SVD truncation tolerance. None = sqrt(eps) * max(singular values),
|
|
59
|
+
matching the default used in the internal ``_r_ginv`` helper.
|
|
60
|
+
warn_threshold : float, default=1e12
|
|
61
|
+
Condition number threshold for issuing a warning.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
X_pinv : np.ndarray
|
|
66
|
+
Pseudoinverse of X.
|
|
67
|
+
diagnostics : dict
|
|
68
|
+
Contains:
|
|
69
|
+
- ``condition_number`` (float): ratio of largest to smallest retained
|
|
70
|
+
singular value (inf if matrix is effectively singular).
|
|
71
|
+
- ``effective_rank`` (int): number of singular values above tolerance.
|
|
72
|
+
- ``tolerance`` (float): the tolerance actually used.
|
|
73
|
+
|
|
74
|
+
Warns
|
|
75
|
+
-----
|
|
76
|
+
UserWarning
|
|
77
|
+
When condition number exceeds *warn_threshold*.
|
|
78
|
+
"""
|
|
79
|
+
X = np.asarray(X, dtype=float)
|
|
80
|
+
# SVD (reduced)
|
|
81
|
+
U, s, Vt = la.svd(X, full_matrices=False, lapack_driver='gesdd')
|
|
82
|
+
|
|
83
|
+
# Tolerance: matches R MASS::ginv default – sqrt(eps) * max(s)
|
|
84
|
+
if tol is None:
|
|
85
|
+
eps = np.finfo(float).eps
|
|
86
|
+
tol_value = np.sqrt(eps) * (s[0] if len(s) > 0 else 0.0)
|
|
87
|
+
else:
|
|
88
|
+
tol_value = tol
|
|
89
|
+
|
|
90
|
+
# Determine retained singular values
|
|
91
|
+
positive = s > max(tol_value, 0.0)
|
|
92
|
+
effective_rank = int(np.sum(positive))
|
|
93
|
+
|
|
94
|
+
# Condition number: ratio of max to min retained singular value
|
|
95
|
+
if effective_rank == 0:
|
|
96
|
+
condition_number = float('inf')
|
|
97
|
+
elif effective_rank == 1:
|
|
98
|
+
condition_number = float('inf') # effectively rank-1
|
|
99
|
+
else:
|
|
100
|
+
s_retained = s[positive]
|
|
101
|
+
condition_number = float(s_retained[0] / s_retained[-1])
|
|
102
|
+
|
|
103
|
+
# Compute pseudoinverse with the same logic as _r_ginv in cbps_binary
|
|
104
|
+
if len(s) == 0 or s[0] < np.finfo(float).eps:
|
|
105
|
+
X_pinv = np.zeros((X.shape[1], X.shape[0]))
|
|
106
|
+
elif np.all(positive):
|
|
107
|
+
X_pinv = (Vt.T / s) @ U.T
|
|
108
|
+
elif not np.any(positive):
|
|
109
|
+
X_pinv = np.zeros((X.shape[1], X.shape[0]))
|
|
110
|
+
else:
|
|
111
|
+
V_pos = Vt[positive].T
|
|
112
|
+
s_pos = s[positive]
|
|
113
|
+
U_pos = U[:, positive]
|
|
114
|
+
X_pinv = (V_pos / s_pos) @ U_pos.T
|
|
115
|
+
|
|
116
|
+
diagnostics = {
|
|
117
|
+
'condition_number': condition_number,
|
|
118
|
+
'effective_rank': effective_rank,
|
|
119
|
+
'tolerance': tol_value,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Emit warning for ill-conditioned matrices
|
|
123
|
+
if condition_number > warn_threshold:
|
|
124
|
+
warnings.warn(
|
|
125
|
+
f"Matrix is ill-conditioned: condition number = {condition_number:.2e} "
|
|
126
|
+
f"(threshold = {warn_threshold:.2e}). "
|
|
127
|
+
f"Effective rank = {effective_rank}/{min(X.shape)}. "
|
|
128
|
+
f"Results may be numerically unreliable. "
|
|
129
|
+
f"Consider checking for collinear covariates.",
|
|
130
|
+
UserWarning,
|
|
131
|
+
stacklevel=2,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return X_pinv, diagnostics
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def r_ginv_like(X: np.ndarray, tol: Optional[float] = None) -> np.ndarray:
|
|
138
|
+
"""
|
|
139
|
+
Compute Moore-Penrose pseudoinverse with tolerance-based truncation.
|
|
140
|
+
|
|
141
|
+
Uses SVD decomposition with a threshold rule for singular value truncation:
|
|
142
|
+
singular values below the tolerance are set to zero in the inversion.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
X : np.ndarray
|
|
147
|
+
Input matrix to pseudo-invert, shape (m, n).
|
|
148
|
+
tol : float, optional
|
|
149
|
+
Absolute tolerance for singular value truncation.
|
|
150
|
+
If None, uses: max(m, n) * max(singular_values) * machine_epsilon.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
np.ndarray
|
|
155
|
+
Pseudoinverse of X, shape (n, m).
|
|
156
|
+
|
|
157
|
+
Notes
|
|
158
|
+
-----
|
|
159
|
+
The tolerance rule follows the standard numerical convention:
|
|
160
|
+
|
|
161
|
+
tol = max(dim(X)) * sigma_max * eps
|
|
162
|
+
|
|
163
|
+
where sigma_max is the largest singular value and eps is machine epsilon.
|
|
164
|
+
This ensures robustness against numerical rank deficiency.
|
|
165
|
+
|
|
166
|
+
Examples
|
|
167
|
+
--------
|
|
168
|
+
>>> import numpy as np
|
|
169
|
+
>>> X = np.array([[1, 2], [3, 4], [5, 6]])
|
|
170
|
+
>>> X_pinv = r_ginv_like(X)
|
|
171
|
+
>>> # Verify pseudoinverse property: X @ X_pinv @ X ≈ X
|
|
172
|
+
>>> assert np.allclose(X @ X_pinv @ X, X, atol=1e-10)
|
|
173
|
+
"""
|
|
174
|
+
X = np.asarray(X)
|
|
175
|
+
# Compute SVD once to control tolerance exactly and avoid SciPy defaults
|
|
176
|
+
U, s, Vt = la.svd(X, full_matrices=False, lapack_driver='gesdd')
|
|
177
|
+
if tol is None:
|
|
178
|
+
eps = np.finfo(X.dtype if np.issubdtype(X.dtype, np.floating) else np.float64).eps
|
|
179
|
+
tol = max(X.shape) * s.max(initial=0.0) * eps
|
|
180
|
+
# Invert with truncation
|
|
181
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
182
|
+
s_inv = np.where(s > tol, 1.0 / s, 0.0)
|
|
183
|
+
return (Vt.T * s_inv) @ U.T
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def r_ginv_rcond(X: np.ndarray) -> float:
|
|
187
|
+
"""
|
|
188
|
+
Compute the relative condition number for pseudoinverse truncation.
|
|
189
|
+
|
|
190
|
+
Converts the absolute tolerance rule to a relative cutoff suitable for
|
|
191
|
+
NumPy/SciPy pinv functions.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
X : np.ndarray
|
|
196
|
+
Input matrix for which to compute rcond.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
float
|
|
201
|
+
Relative condition number: max(dim(X)) * machine_epsilon.
|
|
202
|
+
|
|
203
|
+
Notes
|
|
204
|
+
-----
|
|
205
|
+
The relationship between absolute and relative tolerances is:
|
|
206
|
+
|
|
207
|
+
absolute_tol = rcond * sigma_max
|
|
208
|
+
rcond = max(m, n) * eps
|
|
209
|
+
|
|
210
|
+
where sigma_max is the largest singular value.
|
|
211
|
+
|
|
212
|
+
Examples
|
|
213
|
+
--------
|
|
214
|
+
>>> import numpy as np
|
|
215
|
+
>>> X = np.random.randn(100, 10)
|
|
216
|
+
>>> rcond = r_ginv_rcond(X)
|
|
217
|
+
>>> assert rcond > 0
|
|
218
|
+
"""
|
|
219
|
+
X = np.asarray(X)
|
|
220
|
+
eps = np.finfo(X.dtype if np.issubdtype(X.dtype, np.floating) else np.float64).eps
|
|
221
|
+
return max(X.shape) * eps
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def pinv_match_r(X: np.ndarray) -> np.ndarray:
|
|
225
|
+
"""
|
|
226
|
+
Compute pseudoinverse using NumPy with standard tolerance.
|
|
227
|
+
|
|
228
|
+
A convenience wrapper around numpy.linalg.pinv that applies
|
|
229
|
+
the standard tolerance rule for singular value truncation.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
X : np.ndarray
|
|
234
|
+
Input matrix to pseudo-invert.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
np.ndarray
|
|
239
|
+
Pseudoinverse of X.
|
|
240
|
+
|
|
241
|
+
See Also
|
|
242
|
+
--------
|
|
243
|
+
r_ginv_like : Direct SVD-based implementation with custom tolerance.
|
|
244
|
+
r_ginv_rcond : Computes the rcond value used here.
|
|
245
|
+
|
|
246
|
+
Examples
|
|
247
|
+
--------
|
|
248
|
+
>>> import numpy as np
|
|
249
|
+
>>> X = np.array([[1, 2], [3, 4]])
|
|
250
|
+
>>> X_pinv = pinv_match_r(X)
|
|
251
|
+
>>> assert np.allclose(X @ X_pinv @ X, X, atol=1e-10)
|
|
252
|
+
"""
|
|
253
|
+
return np.linalg.pinv(np.asarray(X), rcond=r_ginv_rcond(X))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def pinv_symmetric_psd(X: np.ndarray, tol: Optional[float] = None) -> np.ndarray:
|
|
257
|
+
"""
|
|
258
|
+
Compute pseudoinverse for symmetric positive semi-definite matrices.
|
|
259
|
+
|
|
260
|
+
Uses eigenvalue decomposition instead of SVD, exploiting symmetry for
|
|
261
|
+
improved numerical stability and efficiency. Small or negative eigenvalues
|
|
262
|
+
(arising from numerical noise) are clipped to zero.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
X : np.ndarray
|
|
267
|
+
Symmetric input matrix to pseudo-invert, shape (n, n).
|
|
268
|
+
tol : float, optional
|
|
269
|
+
Absolute tolerance for eigenvalue truncation.
|
|
270
|
+
If None, uses: n * max(eigenvalues) * machine_epsilon.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
np.ndarray
|
|
275
|
+
Symmetric pseudoinverse of X, shape (n, n).
|
|
276
|
+
|
|
277
|
+
Notes
|
|
278
|
+
-----
|
|
279
|
+
For a symmetric matrix X = Q Λ Q^T, the pseudoinverse is:
|
|
280
|
+
|
|
281
|
+
X^+ = Q Λ^+ Q^T
|
|
282
|
+
|
|
283
|
+
where Λ^+ has diagonal entries 1/λ_i for λ_i > tol, and 0 otherwise.
|
|
284
|
+
|
|
285
|
+
The input matrix is symmetrized as 0.5*(X + X^T) before decomposition
|
|
286
|
+
to handle minor floating-point asymmetries.
|
|
287
|
+
|
|
288
|
+
Examples
|
|
289
|
+
--------
|
|
290
|
+
>>> import numpy as np
|
|
291
|
+
>>> # Create a symmetric positive definite matrix
|
|
292
|
+
>>> A = np.array([[4, 2], [2, 3]])
|
|
293
|
+
>>> A_pinv = pinv_symmetric_psd(A)
|
|
294
|
+
>>> assert np.allclose(A @ A_pinv @ A, A, atol=1e-10)
|
|
295
|
+
>>> assert np.allclose(A_pinv, A_pinv.T, atol=1e-14) # Result is symmetric
|
|
296
|
+
"""
|
|
297
|
+
X = np.asarray(X)
|
|
298
|
+
# Symmetrize defensively to counter FP drift
|
|
299
|
+
X = 0.5 * (X + X.T)
|
|
300
|
+
# Eigen-decomposition for symmetric matrices
|
|
301
|
+
w, Q = la.eigh(X)
|
|
302
|
+
if tol is None:
|
|
303
|
+
eps = np.finfo(X.dtype if np.issubdtype(X.dtype, np.floating) else np.float64).eps
|
|
304
|
+
tol = max(X.shape) * float(np.max(w, initial=0.0)) * eps
|
|
305
|
+
# Invert with clipping
|
|
306
|
+
w_inv = np.where(w > tol, 1.0 / w, 0.0)
|
|
307
|
+
return (Q * w_inv) @ Q.T
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def numeric_rank(X: np.ndarray, tol: Optional[float] = None) -> int:
|
|
311
|
+
"""
|
|
312
|
+
Compute effective numerical rank via singular value decomposition.
|
|
313
|
+
|
|
314
|
+
The numerical rank counts singular values exceeding the tolerance threshold,
|
|
315
|
+
providing a robust measure of matrix rank that accounts for floating-point
|
|
316
|
+
precision limitations.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
X : np.ndarray
|
|
321
|
+
Input matrix, shape (m, n).
|
|
322
|
+
tol : float, optional
|
|
323
|
+
Absolute tolerance for singular value truncation.
|
|
324
|
+
If None, uses: max(m, n) * max(singular_values) * machine_epsilon.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
int
|
|
329
|
+
Number of singular values exceeding the tolerance.
|
|
330
|
+
|
|
331
|
+
Notes
|
|
332
|
+
-----
|
|
333
|
+
Unlike numpy.linalg.matrix_rank, this function uses a tolerance rule
|
|
334
|
+
that scales with the matrix dimensions, providing more consistent
|
|
335
|
+
behavior across different problem sizes.
|
|
336
|
+
|
|
337
|
+
Examples
|
|
338
|
+
--------
|
|
339
|
+
>>> import numpy as np
|
|
340
|
+
>>> # Full rank matrix
|
|
341
|
+
>>> X = np.array([[1, 2], [3, 4]])
|
|
342
|
+
>>> assert numeric_rank(X) == 2
|
|
343
|
+
>>>
|
|
344
|
+
>>> # Rank deficient matrix
|
|
345
|
+
>>> Y = np.array([[1, 2], [2, 4]]) # Second row is 2x first row
|
|
346
|
+
>>> assert numeric_rank(Y) == 1
|
|
347
|
+
"""
|
|
348
|
+
X = np.asarray(X)
|
|
349
|
+
s = la.svd(X, compute_uv=False, lapack_driver='gesdd')
|
|
350
|
+
if tol is None:
|
|
351
|
+
eps = np.finfo(X.dtype if np.issubdtype(X.dtype, np.floating) else np.float64).eps
|
|
352
|
+
tol = max(X.shape) * s.max(initial=0.0) * eps
|
|
353
|
+
return int(np.sum(s > tol))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def symmetrize(A: np.ndarray) -> np.ndarray:
|
|
357
|
+
"""
|
|
358
|
+
Force matrix symmetry by averaging with its transpose.
|
|
359
|
+
|
|
360
|
+
Computes 0.5 * (A + A^T), which projects any square matrix onto
|
|
361
|
+
the space of symmetric matrices.
|
|
362
|
+
|
|
363
|
+
Parameters
|
|
364
|
+
----------
|
|
365
|
+
A : np.ndarray
|
|
366
|
+
Square matrix, shape (n, n).
|
|
367
|
+
|
|
368
|
+
Returns
|
|
369
|
+
-------
|
|
370
|
+
np.ndarray
|
|
371
|
+
Symmetric matrix, shape (n, n).
|
|
372
|
+
|
|
373
|
+
Examples
|
|
374
|
+
--------
|
|
375
|
+
>>> import numpy as np
|
|
376
|
+
>>> A = np.array([[1, 2], [3, 4]])
|
|
377
|
+
>>> A_sym = symmetrize(A)
|
|
378
|
+
>>> assert np.allclose(A_sym, A_sym.T)
|
|
379
|
+
>>> assert np.allclose(A_sym, [[1, 2.5], [2.5, 4]])
|
|
380
|
+
"""
|
|
381
|
+
A = np.asarray(A)
|
|
382
|
+
return 0.5 * (A + A.T)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def max_asymmetry(A: np.ndarray) -> float:
|
|
386
|
+
"""
|
|
387
|
+
Compute the maximum asymmetry of a matrix.
|
|
388
|
+
|
|
389
|
+
Returns the infinity norm of (A - A^T), measuring how far
|
|
390
|
+
the matrix deviates from perfect symmetry.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
A : np.ndarray
|
|
395
|
+
Square matrix, shape (n, n).
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
float
|
|
400
|
+
Maximum absolute difference: max|A_ij - A_ji|.
|
|
401
|
+
|
|
402
|
+
Examples
|
|
403
|
+
--------
|
|
404
|
+
>>> import numpy as np
|
|
405
|
+
>>> A = np.array([[1, 2], [2.001, 4]])
|
|
406
|
+
>>> asym = max_asymmetry(A)
|
|
407
|
+
>>> assert np.isclose(asym, 0.001)
|
|
408
|
+
"""
|
|
409
|
+
A = np.asarray(A)
|
|
410
|
+
return float(np.max(np.abs(A - A.T)))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def is_symmetric(A: np.ndarray, atol: float = 1e-12) -> bool:
|
|
414
|
+
"""
|
|
415
|
+
Check if a matrix is symmetric within tolerance.
|
|
416
|
+
|
|
417
|
+
Parameters
|
|
418
|
+
----------
|
|
419
|
+
A : np.ndarray
|
|
420
|
+
Square matrix to check, shape (n, n).
|
|
421
|
+
atol : float, default=1e-12
|
|
422
|
+
Absolute tolerance for asymmetry.
|
|
423
|
+
|
|
424
|
+
Returns
|
|
425
|
+
-------
|
|
426
|
+
bool
|
|
427
|
+
True if max|A_ij - A_ji| <= atol.
|
|
428
|
+
|
|
429
|
+
Examples
|
|
430
|
+
--------
|
|
431
|
+
>>> import numpy as np
|
|
432
|
+
>>> A = np.array([[1, 2], [2, 4]])
|
|
433
|
+
>>> assert is_symmetric(A)
|
|
434
|
+
>>>
|
|
435
|
+
>>> B = np.array([[1, 2], [3, 4]])
|
|
436
|
+
>>> assert not is_symmetric(B)
|
|
437
|
+
"""
|
|
438
|
+
return max_asymmetry(A) <= atol
|
cbps/utils/r_compat.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
rpy2 and pandas 2.x Compatibility Utilities
|
|
3
|
+
|
|
4
|
+
This module provides compatibility patches for using rpy2 with pandas 2.x,
|
|
5
|
+
where the deprecated ``DataFrame.iteritems()`` and ``Series.iteritems()``
|
|
6
|
+
methods were removed. The rpy2 pandas2ri converter relies on these methods,
|
|
7
|
+
causing AttributeError in pandas 2.x environments.
|
|
8
|
+
|
|
9
|
+
This module is primarily used for cross-validation testing and is not
|
|
10
|
+
required for normal CBPS functionality.
|
|
11
|
+
|
|
12
|
+
Usage
|
|
13
|
+
-----
|
|
14
|
+
Call ``ensure_rpy2_compatibility()`` before importing rpy2::
|
|
15
|
+
|
|
16
|
+
from cbps.utils.r_compat import ensure_rpy2_compatibility
|
|
17
|
+
ensure_rpy2_compatibility()
|
|
18
|
+
|
|
19
|
+
import rpy2.robjects as ro
|
|
20
|
+
from rpy2.robjects import pandas2ri
|
|
21
|
+
pandas2ri.activate()
|
|
22
|
+
|
|
23
|
+
Notes
|
|
24
|
+
-----
|
|
25
|
+
The compatibility patch maps ``iteritems()`` to ``items()``, which is the
|
|
26
|
+
pandas 2.x replacement. This patch is idempotent and safe to call multiple
|
|
27
|
+
times.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import pandas as pd
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ensure_rpy2_compatibility():
|
|
34
|
+
"""
|
|
35
|
+
Apply compatibility patches for rpy2 with pandas 2.x.
|
|
36
|
+
|
|
37
|
+
Maps the removed ``iteritems()`` methods to ``items()`` on both
|
|
38
|
+
DataFrame and Series classes. This is required because rpy2's
|
|
39
|
+
pandas2ri converter uses these deprecated methods.
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
- Idempotent: safe to call multiple times
|
|
44
|
+
- No effect on pandas 1.x (where iteritems() exists)
|
|
45
|
+
- Applied at the class level to DataFrame and Series
|
|
46
|
+
|
|
47
|
+
Examples
|
|
48
|
+
--------
|
|
49
|
+
>>> from cbps.utils.r_compat import ensure_rpy2_compatibility
|
|
50
|
+
>>> ensure_rpy2_compatibility()
|
|
51
|
+
>>> # rpy2 can now be safely imported
|
|
52
|
+
"""
|
|
53
|
+
# Check if patch is needed (pandas 2.x lacks iteritems)
|
|
54
|
+
if not hasattr(pd.DataFrame, 'iteritems'):
|
|
55
|
+
# Add DataFrame.iteritems as an alias for items
|
|
56
|
+
pd.DataFrame.iteritems = pd.DataFrame.items
|
|
57
|
+
|
|
58
|
+
if not hasattr(pd.Series, 'iteritems'):
|
|
59
|
+
# Add Series.iteritems as an alias for items
|
|
60
|
+
pd.Series.iteritems = pd.Series.items
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_rpy2_available():
|
|
64
|
+
"""
|
|
65
|
+
Check rpy2 availability and apply compatibility patches.
|
|
66
|
+
|
|
67
|
+
Attempts to import rpy2 and the CBPS package from the R environment.
|
|
68
|
+
Automatically applies pandas 2.x compatibility patches before import.
|
|
69
|
+
|
|
70
|
+
This function is primarily used for internal testing and validation.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
available : bool
|
|
75
|
+
True if rpy2 and required packages are available.
|
|
76
|
+
components : tuple of (robjects, pandas2ri, cbps_package) or None
|
|
77
|
+
If available, returns the imported rpy2 components.
|
|
78
|
+
If not available, returns None.
|
|
79
|
+
|
|
80
|
+
Examples
|
|
81
|
+
--------
|
|
82
|
+
>>> from cbps.utils.r_compat import check_rpy2_available
|
|
83
|
+
>>> available, components = check_rpy2_available()
|
|
84
|
+
>>> if available:
|
|
85
|
+
... ro, pandas2ri, cbps_pkg = components
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
# Apply compatibility patches first
|
|
89
|
+
ensure_rpy2_compatibility()
|
|
90
|
+
|
|
91
|
+
# Attempt to import rpy2
|
|
92
|
+
import rpy2.robjects as ro
|
|
93
|
+
from rpy2.robjects import pandas2ri
|
|
94
|
+
from rpy2.robjects.packages import importr
|
|
95
|
+
|
|
96
|
+
# Activate pandas conversion
|
|
97
|
+
pandas2ri.activate()
|
|
98
|
+
|
|
99
|
+
# Attempt to import CBPS package
|
|
100
|
+
cbps_r = importr('CBPS')
|
|
101
|
+
|
|
102
|
+
return True, (ro, pandas2ri, cbps_r)
|
|
103
|
+
|
|
104
|
+
except ImportError as e:
|
|
105
|
+
# rpy2 or required packages not installed
|
|
106
|
+
return False, None
|
|
107
|
+
except Exception as e:
|
|
108
|
+
# Other initialization errors
|
|
109
|
+
return False, None
|