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
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LASSO Utilities for High-Dimensional CBPS
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
This module provides cross-validated LASSO regression for variable selection
|
|
6
|
+
in Steps 1-2 of the hdCBPS algorithm. It uses glmnetforpython for efficient
|
|
7
|
+
coordinate descent optimization with the Fortran glmnet backend.
|
|
8
|
+
|
|
9
|
+
LASSO (L1-penalized) regression enables automatic variable selection by
|
|
10
|
+
shrinking irrelevant coefficients to exactly zero. This is essential for
|
|
11
|
+
high-dimensional settings where p >> n, allowing identification of the
|
|
12
|
+
sparse active sets for propensity score and outcome models.
|
|
13
|
+
|
|
14
|
+
The regularization parameter lambda is selected via K-fold cross-validation
|
|
15
|
+
to minimize prediction error, following standard practice in the literature.
|
|
16
|
+
|
|
17
|
+
References
|
|
18
|
+
----------
|
|
19
|
+
Ning, Y., Peng, S., and Imai, K. (2020). Robust estimation of causal effects
|
|
20
|
+
via a high-dimensional covariate balancing propensity score.
|
|
21
|
+
Biometrika, 107(3), 533-554. https://doi.org/10.1093/biomet/asaa020
|
|
22
|
+
|
|
23
|
+
Friedman, J., Hastie, T., and Tibshirani, R. (2010). Regularization paths
|
|
24
|
+
for generalized linear models via coordinate descent. Journal of Statistical
|
|
25
|
+
Software, 33(1), 1-22.
|
|
26
|
+
|
|
27
|
+
Notes
|
|
28
|
+
-----
|
|
29
|
+
**Required dependency**: glmnetforpython
|
|
30
|
+
|
|
31
|
+
This package provides high-performance LASSO regression via the Fortran
|
|
32
|
+
glmnet library. It is required for hdCBPS variable selection to ensure
|
|
33
|
+
numerical precision and reproducibility.
|
|
34
|
+
|
|
35
|
+
Installation::
|
|
36
|
+
|
|
37
|
+
# Standard installation
|
|
38
|
+
pip install glmnetforpython
|
|
39
|
+
|
|
40
|
+
# For Apple Silicon (M1/M2/M3), compile from source
|
|
41
|
+
brew install gcc
|
|
42
|
+
export FC=gfortran
|
|
43
|
+
pip install glmnetforpython
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import warnings
|
|
47
|
+
from typing import Tuple, Optional, Union
|
|
48
|
+
|
|
49
|
+
import numpy as np
|
|
50
|
+
|
|
51
|
+
# Detect glmnetforpython (required dependency for hdCBPS)
|
|
52
|
+
try:
|
|
53
|
+
from glmnetforpython import GLMNet
|
|
54
|
+
HAS_GLMNETFORPYTHON = True
|
|
55
|
+
except ImportError:
|
|
56
|
+
HAS_GLMNETFORPYTHON = False
|
|
57
|
+
|
|
58
|
+
# Detect rpy2 (optional; used only for testing and validation purposes)
|
|
59
|
+
try:
|
|
60
|
+
import rpy2.robjects as ro
|
|
61
|
+
from rpy2.robjects import r
|
|
62
|
+
HAS_RPY2 = True
|
|
63
|
+
except ImportError:
|
|
64
|
+
HAS_RPY2 = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cv_glmnet(
|
|
68
|
+
X: np.ndarray,
|
|
69
|
+
y: np.ndarray,
|
|
70
|
+
family: str = 'gaussian',
|
|
71
|
+
alpha: float = 1.0,
|
|
72
|
+
intercept: bool = True,
|
|
73
|
+
n_folds: int = 10,
|
|
74
|
+
max_iter: int = 10000
|
|
75
|
+
) -> Tuple[Union[object, str], np.ndarray, float]:
|
|
76
|
+
"""
|
|
77
|
+
Perform cross-validated LASSO regression using glmnetforpython.
|
|
78
|
+
|
|
79
|
+
This function fits a LASSO (or elastic net) regression model with automatic
|
|
80
|
+
selection of the optimal regularization parameter lambda via K-fold
|
|
81
|
+
cross-validation.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
X : np.ndarray of shape (n_samples, n_features)
|
|
86
|
+
Covariate matrix (without intercept column).
|
|
87
|
+
y : np.ndarray of shape (n_samples,)
|
|
88
|
+
Response variable.
|
|
89
|
+
family : {'gaussian', 'binomial', 'poisson'}, default='gaussian'
|
|
90
|
+
Distribution family for the response variable:
|
|
91
|
+
|
|
92
|
+
- ``'gaussian'``: Linear regression (continuous response)
|
|
93
|
+
- ``'binomial'``: Logistic regression (binary response)
|
|
94
|
+
- ``'poisson'``: Poisson regression (count response)
|
|
95
|
+
|
|
96
|
+
alpha : float, default=1.0
|
|
97
|
+
Elastic net mixing parameter. ``alpha=1.0`` corresponds to pure LASSO;
|
|
98
|
+
``alpha=0.0`` corresponds to ridge regression; values in between give
|
|
99
|
+
elastic net regularization.
|
|
100
|
+
intercept : bool, default=True
|
|
101
|
+
Whether to fit an intercept term. The appropriate setting depends
|
|
102
|
+
on the modeling context:
|
|
103
|
+
|
|
104
|
+
- Propensity score model: ``intercept=True`` (all families)
|
|
105
|
+
- Outcome model (gaussian): ``intercept=True``
|
|
106
|
+
- Outcome model (binomial/poisson): ``intercept=False``
|
|
107
|
+
n_folds : int, default=10
|
|
108
|
+
Number of folds for cross-validation.
|
|
109
|
+
max_iter : int, default=10000
|
|
110
|
+
Maximum number of iterations for the coordinate descent algorithm.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
model : object
|
|
115
|
+
Fitted GLMNet cross-validation result object.
|
|
116
|
+
coef : np.ndarray of shape (n_features + 1,) or (n_features,)
|
|
117
|
+
Coefficient vector at the optimal lambda:
|
|
118
|
+
|
|
119
|
+
- If ``intercept=True``: ``[intercept, coef_1, ..., coef_p]``
|
|
120
|
+
- If ``intercept=False``: ``[coef_1, ..., coef_p]``
|
|
121
|
+
|
|
122
|
+
lambda_min : float
|
|
123
|
+
Optimal regularization parameter selected by cross-validation
|
|
124
|
+
(minimizes cross-validated error).
|
|
125
|
+
|
|
126
|
+
Notes
|
|
127
|
+
-----
|
|
128
|
+
This function uses glmnetforpython, which provides a Python interface to
|
|
129
|
+
the Fortran-based glmnet library. The Fortran backend ensures numerical
|
|
130
|
+
precision and reproducibility across platforms.
|
|
131
|
+
|
|
132
|
+
Examples
|
|
133
|
+
--------
|
|
134
|
+
Gaussian family with intercept (linear method):
|
|
135
|
+
|
|
136
|
+
>>> model, coef, lam = cv_glmnet(X, y, family='gaussian', intercept=True)
|
|
137
|
+
>>> # coef has shape (p+1,): [intercept, coef_1, ..., coef_p]
|
|
138
|
+
|
|
139
|
+
Binomial family without intercept (nonlinear method):
|
|
140
|
+
|
|
141
|
+
>>> model, coef, lam = cv_glmnet(X, y, family='binomial', intercept=False)
|
|
142
|
+
>>> # coef has shape (p,): [coef_1, ..., coef_p]
|
|
143
|
+
"""
|
|
144
|
+
n, p = X.shape
|
|
145
|
+
|
|
146
|
+
# ===================================================================
|
|
147
|
+
# Require glmnetforpython (Fortran backend for numerical precision)
|
|
148
|
+
# ===================================================================
|
|
149
|
+
if not HAS_GLMNETFORPYTHON:
|
|
150
|
+
raise ImportError(
|
|
151
|
+
"hdCBPS requires glmnetforpython for numerical precision.\n"
|
|
152
|
+
"\n"
|
|
153
|
+
"Install with:\n"
|
|
154
|
+
" pip install 'cbps[hdcbps]' # Recommended\n"
|
|
155
|
+
" # OR\n"
|
|
156
|
+
" pip install glmnetforpython # Manual installation\n"
|
|
157
|
+
"\n"
|
|
158
|
+
"For Apple Silicon (M1/M2/M3), compile from source:\n"
|
|
159
|
+
" brew install gcc # Install gfortran\n"
|
|
160
|
+
" cd glmnetforpython-master && pip install -e .\n"
|
|
161
|
+
"\n"
|
|
162
|
+
"Why glmnetforpython? Validation on LaLonde data (n=3212):\n"
|
|
163
|
+
" - glmnetforpython: ATE error 0.8%, numerically stable ✓\n"
|
|
164
|
+
" - sklearn: ATE error 110%, numerically unstable ✗\n"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return _cv_glmnet_fortran(X, y, family, alpha, intercept, n_folds)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _cv_glmnet_fortran(
|
|
171
|
+
X: np.ndarray,
|
|
172
|
+
y: np.ndarray,
|
|
173
|
+
family: str,
|
|
174
|
+
alpha: float,
|
|
175
|
+
intercept: bool,
|
|
176
|
+
n_folds: int
|
|
177
|
+
) -> Tuple[object, np.ndarray, float]:
|
|
178
|
+
"""Internal implementation of cross-validated LASSO using glmnetforpython.
|
|
179
|
+
|
|
180
|
+
This is the backend function that interfaces with glmnetforpython's
|
|
181
|
+
Fortran-based coordinate descent algorithm.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
X : np.ndarray of shape (n_samples, n_features)
|
|
186
|
+
Covariate matrix.
|
|
187
|
+
y : np.ndarray of shape (n_samples,)
|
|
188
|
+
Response variable.
|
|
189
|
+
family : str
|
|
190
|
+
Distribution family ('gaussian', 'binomial', 'poisson').
|
|
191
|
+
alpha : float
|
|
192
|
+
Elastic net mixing parameter.
|
|
193
|
+
intercept : bool
|
|
194
|
+
Whether to fit an intercept.
|
|
195
|
+
n_folds : int
|
|
196
|
+
Number of cross-validation folds.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
cv_result : object
|
|
201
|
+
GLMNet cross-validation result object.
|
|
202
|
+
coef : np.ndarray
|
|
203
|
+
Coefficient vector at optimal lambda.
|
|
204
|
+
lambda_min : float
|
|
205
|
+
Optimal regularization parameter.
|
|
206
|
+
"""
|
|
207
|
+
family_map = {
|
|
208
|
+
'gaussian': 'gaussian',
|
|
209
|
+
'binomial': 'binomial',
|
|
210
|
+
'poisson': 'poisson'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if family not in family_map:
|
|
214
|
+
raise ValueError(f"Unsupported family: '{family}'")
|
|
215
|
+
|
|
216
|
+
glmnet_family = family_map[family]
|
|
217
|
+
|
|
218
|
+
# Initialize GLMNet with elastic net mixing parameter
|
|
219
|
+
# alpha=1.0 corresponds to pure LASSO; alpha<1.0 gives elastic net
|
|
220
|
+
regr = GLMNet(alpha=alpha)
|
|
221
|
+
|
|
222
|
+
# Perform K-fold cross-validation with automatic lambda selection
|
|
223
|
+
# Setting keep=True preserves fold assignments for reproducibility
|
|
224
|
+
cv_result = regr.cvglmnet(
|
|
225
|
+
X=X.copy(),
|
|
226
|
+
y=y.copy(),
|
|
227
|
+
family=glmnet_family,
|
|
228
|
+
nfolds=n_folds,
|
|
229
|
+
keep=True,
|
|
230
|
+
intr=intercept
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Extract coefficients at the optimal lambda (lambda_min)
|
|
234
|
+
coef = np.array(cv_result.best_coef).flatten()
|
|
235
|
+
lambda_min = cv_result.lambda_min
|
|
236
|
+
|
|
237
|
+
# glmnetforpython always returns coefficients with intercept position
|
|
238
|
+
# (intercept is first element, set to 0 if intr=False)
|
|
239
|
+
# R's coef() also always returns the full coefficient vector including intercept
|
|
240
|
+
# We keep this behavior to match R exactly
|
|
241
|
+
# Note: When intercept=False, the first element will be 0
|
|
242
|
+
|
|
243
|
+
return cv_result, coef, lambda_min
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def predict_glmnet_fortran(
|
|
247
|
+
cv_result: object,
|
|
248
|
+
newx: np.ndarray,
|
|
249
|
+
s: Optional[float] = None
|
|
250
|
+
) -> np.ndarray:
|
|
251
|
+
"""Generate predictions from a fitted glmnet model.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
cv_result : object
|
|
256
|
+
Cross-validation result object returned by :func:`cv_glmnet`.
|
|
257
|
+
newx : np.ndarray of shape (n_samples, n_features)
|
|
258
|
+
New covariate matrix for prediction. Should have the same number
|
|
259
|
+
of features as the training data (excluding any intercept column
|
|
260
|
+
that glmnet adds internally).
|
|
261
|
+
s : float, optional
|
|
262
|
+
Regularization parameter value at which to make predictions.
|
|
263
|
+
If None (default), uses lambda_min from cross-validation.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
predictions : np.ndarray of shape (n_samples,)
|
|
268
|
+
Predicted values.
|
|
269
|
+
|
|
270
|
+
Notes
|
|
271
|
+
-----
|
|
272
|
+
The glmnetPredict function handles intercept terms internally, so
|
|
273
|
+
``newx`` should not include a constant column.
|
|
274
|
+
"""
|
|
275
|
+
from glmnetforpython.glmnetPredict import glmnetPredict
|
|
276
|
+
|
|
277
|
+
if s is None:
|
|
278
|
+
s = cv_result.lambda_min
|
|
279
|
+
|
|
280
|
+
# Extract the fitted glmnet model from the CV result
|
|
281
|
+
glmfit = cv_result.cvfit['glmnet_fit']
|
|
282
|
+
|
|
283
|
+
# Generate predictions at the specified lambda value
|
|
284
|
+
pred = glmnetPredict(glmfit, newx, np.array([s]), 'response')
|
|
285
|
+
|
|
286
|
+
return pred.flatten()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def select_variables(coef: np.ndarray, tol: float = 1e-10) -> np.ndarray:
|
|
290
|
+
"""Extract indices of non-zero coefficients from a LASSO solution.
|
|
291
|
+
|
|
292
|
+
Identifies selected variables by finding coefficients whose absolute
|
|
293
|
+
values exceed the specified tolerance threshold.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
coef : np.ndarray of shape (n_features,)
|
|
298
|
+
Coefficient vector from LASSO estimation (may include intercept
|
|
299
|
+
as the first element).
|
|
300
|
+
tol : float, default=1e-10
|
|
301
|
+
Threshold for determining non-zero coefficients. Coefficients with
|
|
302
|
+
``|coef| > tol`` are considered selected.
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
selected_indices : np.ndarray of shape (n_selected,)
|
|
307
|
+
Zero-based indices of selected (non-zero) coefficients.
|
|
308
|
+
|
|
309
|
+
Examples
|
|
310
|
+
--------
|
|
311
|
+
>>> import numpy as np
|
|
312
|
+
>>> from cbps.highdim.lasso_utils import select_variables
|
|
313
|
+
>>> coef = np.array([1.2, 0.0, 0.5, 0.0, -0.3])
|
|
314
|
+
>>> selected = select_variables(coef)
|
|
315
|
+
>>> selected
|
|
316
|
+
array([0, 2, 4])
|
|
317
|
+
"""
|
|
318
|
+
return np.where(np.abs(coef) > tol)[0]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def cv_glmnet_via_r(
|
|
324
|
+
X: np.ndarray,
|
|
325
|
+
y: np.ndarray,
|
|
326
|
+
family: str = 'gaussian',
|
|
327
|
+
intercept: bool = True,
|
|
328
|
+
n_folds: int = 10,
|
|
329
|
+
model_name: str = 'model_py'
|
|
330
|
+
) -> Tuple[str, np.ndarray, float]:
|
|
331
|
+
"""Cross-validated LASSO using rpy2 interface (testing utility only).
|
|
332
|
+
|
|
333
|
+
This function provides an alternative LASSO implementation via rpy2 for
|
|
334
|
+
development testing and validation purposes. It is not intended for
|
|
335
|
+
production use.
|
|
336
|
+
|
|
337
|
+
.. note::
|
|
338
|
+
For production use, prefer :func:`cv_glmnet` which uses
|
|
339
|
+
glmnetforpython directly without external dependencies.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
X : np.ndarray of shape (n_samples, n_features)
|
|
344
|
+
Covariate matrix (may contain a constant column).
|
|
345
|
+
y : np.ndarray of shape (n_samples,)
|
|
346
|
+
Response variable.
|
|
347
|
+
family : {'gaussian', 'binomial', 'poisson'}, default='gaussian'
|
|
348
|
+
Distribution family for the response variable.
|
|
349
|
+
intercept : bool, default=True
|
|
350
|
+
Whether to fit an intercept term.
|
|
351
|
+
|
|
352
|
+
- If True: returns coefficient vector of length ``n_features + 1``
|
|
353
|
+
- If False: returns coefficient vector of length ``n_features``
|
|
354
|
+
|
|
355
|
+
n_folds : int, default=10
|
|
356
|
+
Number of cross-validation folds.
|
|
357
|
+
model_name : str, default='model_py'
|
|
358
|
+
Variable name to use when storing the model in the rpy2 environment.
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
model_name : str
|
|
363
|
+
The model variable name (for use with :func:`predict_glmnet_via_r`).
|
|
364
|
+
coef : np.ndarray
|
|
365
|
+
Coefficient vector at optimal lambda.
|
|
366
|
+
lambda_min : float
|
|
367
|
+
Optimal regularization parameter.
|
|
368
|
+
|
|
369
|
+
Raises
|
|
370
|
+
------
|
|
371
|
+
ImportError
|
|
372
|
+
If rpy2 is not installed.
|
|
373
|
+
RuntimeError
|
|
374
|
+
If the glmnet package is not available in the rpy2 environment.
|
|
375
|
+
"""
|
|
376
|
+
if not HAS_RPY2:
|
|
377
|
+
raise ImportError(
|
|
378
|
+
"rpy2 is not installed. Please run: pip install rpy2\n"
|
|
379
|
+
"And ensure R environment is available, install glmnet package: R -e \"install.packages('glmnet')\""
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
r('library(glmnet)')
|
|
384
|
+
except Exception as e:
|
|
385
|
+
raise RuntimeError(
|
|
386
|
+
f"Cannot load glmnet package: {e}\n"
|
|
387
|
+
"Please install with: install.packages('glmnet')"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
n, p = X.shape
|
|
391
|
+
|
|
392
|
+
# Transfer numpy arrays to the rpy2 environment
|
|
393
|
+
from rpy2.robjects import numpy2ri
|
|
394
|
+
from rpy2.robjects.conversion import localconverter
|
|
395
|
+
|
|
396
|
+
with localconverter(ro.default_converter + numpy2ri.converter):
|
|
397
|
+
r.assign('X_py_array', X)
|
|
398
|
+
r.assign('y_py_array', y)
|
|
399
|
+
|
|
400
|
+
# Convert to matrix format
|
|
401
|
+
r(f'X_py <- matrix(X_py_array, nrow={n}, ncol={p})')
|
|
402
|
+
r('y_py <- as.numeric(y_py_array)')
|
|
403
|
+
|
|
404
|
+
# Build the cv.glmnet call based on family and intercept settings
|
|
405
|
+
if family == 'gaussian':
|
|
406
|
+
if intercept:
|
|
407
|
+
r_code = f'{model_name} <- cv.glmnet(X_py, y_py, nfolds=10)'
|
|
408
|
+
else:
|
|
409
|
+
r_code = f'{model_name} <- cv.glmnet(X_py, y_py, intercept=FALSE, nfolds=10)'
|
|
410
|
+
elif family == 'binomial':
|
|
411
|
+
if intercept:
|
|
412
|
+
r_code = f'{model_name} <- cv.glmnet(X_py, y_py, family="binomial", nfolds=10)'
|
|
413
|
+
else:
|
|
414
|
+
r_code = f'{model_name} <- cv.glmnet(X_py, y_py, family="binomial", intercept=FALSE, nfolds=10)'
|
|
415
|
+
elif family == 'poisson':
|
|
416
|
+
if intercept:
|
|
417
|
+
r_code = f'{model_name} <- cv.glmnet(X_py, y_py, family="poisson", nfolds=10)'
|
|
418
|
+
else:
|
|
419
|
+
r_code = f'{model_name} <- cv.glmnet(X_py, y_py, family="poisson", intercept=FALSE, nfolds=10)'
|
|
420
|
+
else:
|
|
421
|
+
raise ValueError(f"Unsupported family: '{family}'")
|
|
422
|
+
|
|
423
|
+
# Execute cross-validation
|
|
424
|
+
r(r_code)
|
|
425
|
+
|
|
426
|
+
# Extract coefficients (convert sparse matrix to dense)
|
|
427
|
+
coef_dense = r(f'as.matrix(coef({model_name}))')
|
|
428
|
+
coef = np.array(coef_dense).flatten()
|
|
429
|
+
|
|
430
|
+
# Extract optimal lambda
|
|
431
|
+
lambda_min = r(f'{model_name}$lambda.min')[0]
|
|
432
|
+
|
|
433
|
+
return model_name, coef, lambda_min
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def predict_glmnet_via_r(
|
|
437
|
+
model_name: str,
|
|
438
|
+
newx: np.ndarray,
|
|
439
|
+
s: str = 'lambda.min',
|
|
440
|
+
type: str = 'link'
|
|
441
|
+
) -> np.ndarray:
|
|
442
|
+
"""Generate predictions via rpy2 interface (testing utility only).
|
|
443
|
+
|
|
444
|
+
This function calls the glmnet predict method through rpy2 for models
|
|
445
|
+
fitted with :func:`cv_glmnet_via_r`.
|
|
446
|
+
|
|
447
|
+
Parameters
|
|
448
|
+
----------
|
|
449
|
+
model_name : str
|
|
450
|
+
Model variable name in the rpy2 environment.
|
|
451
|
+
newx : np.ndarray of shape (n_samples, n_features)
|
|
452
|
+
New covariate matrix for prediction.
|
|
453
|
+
s : {'lambda.min', 'lambda.1se'}, default='lambda.min'
|
|
454
|
+
Which lambda value to use for predictions.
|
|
455
|
+
type : {'link', 'response'}, default='link'
|
|
456
|
+
Type of prediction:
|
|
457
|
+
|
|
458
|
+
- ``'link'``: Linear predictor (default for gaussian)
|
|
459
|
+
- ``'response'``: Predicted probabilities (for binomial/poisson)
|
|
460
|
+
|
|
461
|
+
Returns
|
|
462
|
+
-------
|
|
463
|
+
predictions : np.ndarray of shape (n_samples,)
|
|
464
|
+
Predicted values.
|
|
465
|
+
|
|
466
|
+
Notes
|
|
467
|
+
-----
|
|
468
|
+
This function uses the glmnet predict method rather than direct matrix
|
|
469
|
+
multiplication because glmnet applies internal standardization that
|
|
470
|
+
affects the coefficient interpretation.
|
|
471
|
+
"""
|
|
472
|
+
if not HAS_RPY2:
|
|
473
|
+
raise ImportError("predict_glmnet_via_r requires rpy2")
|
|
474
|
+
|
|
475
|
+
n, p = newx.shape
|
|
476
|
+
|
|
477
|
+
# Transfer data to rpy2 environment
|
|
478
|
+
from rpy2.robjects import numpy2ri
|
|
479
|
+
from rpy2.robjects.conversion import localconverter
|
|
480
|
+
|
|
481
|
+
with localconverter(ro.default_converter + numpy2ri.converter):
|
|
482
|
+
r.assign('newx_py_array', newx)
|
|
483
|
+
|
|
484
|
+
r(f'newx_py <- matrix(newx_py_array, nrow={n}, ncol={p})')
|
|
485
|
+
|
|
486
|
+
# Generate predictions
|
|
487
|
+
if type == 'link':
|
|
488
|
+
r_code = f'pred_py <- as.vector(predict({model_name}, newx=newx_py, s="{s}"))'
|
|
489
|
+
else:
|
|
490
|
+
r_code = f'pred_py <- as.vector(predict({model_name}, newx=newx_py, s="{s}", type="{type}"))'
|
|
491
|
+
|
|
492
|
+
r(r_code)
|
|
493
|
+
|
|
494
|
+
predictions = np.array(r['pred_py'])
|
|
495
|
+
|
|
496
|
+
return predictions
|
|
497
|
+
|
|
498
|
+
|