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
@@ -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
+