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.
Files changed (70) hide show
  1. cbps/__init__.py +3462 -0
  2. cbps/constants.py +46 -0
  3. cbps/core/__init__.py +93 -0
  4. cbps/core/cbps_binary.py +1943 -0
  5. cbps/core/cbps_continuous.py +945 -0
  6. cbps/core/cbps_multitreat.py +1123 -0
  7. cbps/core/cbps_optimal.py +507 -0
  8. cbps/core/results.py +1447 -0
  9. cbps/data/Blackwell.csv +571 -0
  10. cbps/data/LaLonde.csv +3213 -0
  11. cbps/data/npcbps_continuous_sim.csv +501 -0
  12. cbps/data/nsw.csv +723 -0
  13. cbps/data/nsw_dw.csv +446 -0
  14. cbps/data/political_ads_urban_niebler.csv +16266 -0
  15. cbps/data/psid_controls.csv +2491 -0
  16. cbps/data/psid_controls2.csv +254 -0
  17. cbps/data/psid_controls3.csv +129 -0
  18. cbps/data/simulation_dgp1_seed12345.csv +201 -0
  19. cbps/data/simulation_dgp2_seed12345.csv +201 -0
  20. cbps/data/simulation_dgp3_seed12345.csv +201 -0
  21. cbps/data/simulation_dgp4_seed12345.csv +201 -0
  22. cbps/datasets/__init__.py +78 -0
  23. cbps/datasets/blackwell.py +112 -0
  24. cbps/datasets/continuous.py +223 -0
  25. cbps/datasets/lalonde.py +272 -0
  26. cbps/datasets/npcbps_sim.py +101 -0
  27. cbps/diagnostics/__init__.py +101 -0
  28. cbps/diagnostics/balance.py +760 -0
  29. cbps/diagnostics/balance_cbmsm_addon.py +162 -0
  30. cbps/diagnostics/continuous_diagnostics.py +259 -0
  31. cbps/diagnostics/normality.py +173 -0
  32. cbps/diagnostics/ocbps_conditions.py +197 -0
  33. cbps/diagnostics/overlap.py +198 -0
  34. cbps/diagnostics/plots.py +1193 -0
  35. cbps/diagnostics/weights_diag.py +205 -0
  36. cbps/highdim/__init__.py +84 -0
  37. cbps/highdim/gmm_loss.py +340 -0
  38. cbps/highdim/hdcbps.py +1078 -0
  39. cbps/highdim/lasso_utils.py +498 -0
  40. cbps/highdim/weight_funcs.py +298 -0
  41. cbps/inference/__init__.py +42 -0
  42. cbps/inference/asyvar.py +621 -0
  43. cbps/inference/vcov_outcome.py +217 -0
  44. cbps/iv/__init__.py +48 -0
  45. cbps/iv/cbiv.py +2603 -0
  46. cbps/logging_config.py +45 -0
  47. cbps/msm/__init__.py +45 -0
  48. cbps/msm/cbmsm.py +1871 -0
  49. cbps/msm/rank_diagnostics.py +112 -0
  50. cbps/nonparametric/__init__.py +58 -0
  51. cbps/nonparametric/cholesky_whitening.py +232 -0
  52. cbps/nonparametric/empirical_likelihood.py +339 -0
  53. cbps/nonparametric/npcbps.py +1036 -0
  54. cbps/nonparametric/taylor_approx.py +207 -0
  55. cbps/py.typed +0 -0
  56. cbps/sklearn/__init__.py +42 -0
  57. cbps/sklearn/estimator.py +378 -0
  58. cbps/utils/__init__.py +82 -0
  59. cbps/utils/formula.py +415 -0
  60. cbps/utils/helpers.py +378 -0
  61. cbps/utils/numerics.py +438 -0
  62. cbps/utils/r_compat.py +109 -0
  63. cbps/utils/validation.py +224 -0
  64. cbps/utils/variance_transform.py +483 -0
  65. cbps/utils/weights.py +586 -0
  66. cbps-0.2.0.dist-info/METADATA +1090 -0
  67. cbps-0.2.0.dist-info/RECORD +70 -0
  68. cbps-0.2.0.dist-info/WHEEL +5 -0
  69. cbps-0.2.0.dist-info/licenses/LICENSE +661 -0
  70. 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