statgpu 0.1.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.
- statgpu/__init__.py +174 -0
- statgpu/_base.py +544 -0
- statgpu/_config.py +127 -0
- statgpu/anova/__init__.py +5 -0
- statgpu/anova/_oneway.py +194 -0
- statgpu/backends/__init__.py +83 -0
- statgpu/backends/_array_ops.py +529 -0
- statgpu/backends/_base.py +184 -0
- statgpu/backends/_cupy.py +453 -0
- statgpu/backends/_factory.py +65 -0
- statgpu/backends/_gpu_inference_cupy.py +214 -0
- statgpu/backends/_gpu_inference_torch.py +422 -0
- statgpu/backends/_numpy.py +324 -0
- statgpu/backends/_torch.py +685 -0
- statgpu/backends/_torch_safe.py +47 -0
- statgpu/backends/_utils.py +423 -0
- statgpu/core/__init__.py +10 -0
- statgpu/core/formula/__init__.py +33 -0
- statgpu/core/formula/_design.py +99 -0
- statgpu/core/formula/_parser.py +191 -0
- statgpu/core/formula/_terms.py +70 -0
- statgpu/core/formula/tests/__init__.py +0 -0
- statgpu/core/formula/tests/test_parser.py +194 -0
- statgpu/covariance/__init__.py +6 -0
- statgpu/covariance/_empirical.py +310 -0
- statgpu/covariance/_shrinkage.py +248 -0
- statgpu/cross_validation/__init__.py +31 -0
- statgpu/cross_validation/_base.py +410 -0
- statgpu/cross_validation/_engine.py +167 -0
- statgpu/diagnostics/__init__.py +7 -0
- statgpu/diagnostics/_regression_diagnostics.py +188 -0
- statgpu/feature_selection/__init__.py +24 -0
- statgpu/feature_selection/_knockoff.py +870 -0
- statgpu/feature_selection/_knockoff_utils.py +1003 -0
- statgpu/feature_selection/_stepwise.py +300 -0
- statgpu/glm_core/__init__.py +81 -0
- statgpu/glm_core/_base.py +202 -0
- statgpu/glm_core/_family.py +362 -0
- statgpu/glm_core/_fused.py +149 -0
- statgpu/glm_core/_gamma.py +111 -0
- statgpu/glm_core/_inverse_gaussian.py +62 -0
- statgpu/glm_core/_irls.py +561 -0
- statgpu/glm_core/_logistic.py +82 -0
- statgpu/glm_core/_negative_binomial.py +68 -0
- statgpu/glm_core/_poisson.py +60 -0
- statgpu/glm_core/_solver_legacy.py +100 -0
- statgpu/glm_core/_squared.py +53 -0
- statgpu/glm_core/_tweedie.py +74 -0
- statgpu/inference/__init__.py +239 -0
- statgpu/inference/_distributions_backend.py +2610 -0
- statgpu/inference/_multiple_testing.py +391 -0
- statgpu/inference/_resampling.py +1400 -0
- statgpu/inference/_results.py +265 -0
- statgpu/linear_model/__init__.py +75 -0
- statgpu/linear_model/_gaussian_inference.py +306 -0
- statgpu/linear_model/_glm_base.py +1261 -0
- statgpu/linear_model/_ordered_logit.py +52 -0
- statgpu/linear_model/_ordered_probit.py +50 -0
- statgpu/linear_model/_stats.py +170 -0
- statgpu/linear_model/cv/__init__.py +13 -0
- statgpu/linear_model/cv/_elasticnet_cv.py +892 -0
- statgpu/linear_model/cv/_lasso_cv.py +253 -0
- statgpu/linear_model/cv/_logistic_cv.py +895 -0
- statgpu/linear_model/cv/_ridge_cv.py +1160 -0
- statgpu/linear_model/legacy/__init__.py +1 -0
- statgpu/linear_model/legacy/_distributions_legacy_gpu.py +340 -0
- statgpu/linear_model/legacy/_elasticnet_legacy.py +936 -0
- statgpu/linear_model/legacy/_lasso_legacy.py +4876 -0
- statgpu/linear_model/legacy/_penalized_legacy.py +1174 -0
- statgpu/linear_model/legacy/_ridge_legacy.py +863 -0
- statgpu/linear_model/legacy/_solver_legacy.py +104 -0
- statgpu/linear_model/penalized/__init__.py +25 -0
- statgpu/linear_model/penalized/_base.py +437 -0
- statgpu/linear_model/penalized/_fit_mixin.py +1877 -0
- statgpu/linear_model/penalized/_inference_mixin.py +1179 -0
- statgpu/linear_model/penalized/_penalized_cv.py +2699 -0
- statgpu/linear_model/penalized/_penalized_gamma.py +86 -0
- statgpu/linear_model/penalized/_penalized_inverse_gaussian.py +62 -0
- statgpu/linear_model/penalized/_penalized_linear.py +236 -0
- statgpu/linear_model/penalized/_penalized_logistic.py +100 -0
- statgpu/linear_model/penalized/_penalized_negative_binomial.py +65 -0
- statgpu/linear_model/penalized/_penalized_poisson.py +62 -0
- statgpu/linear_model/penalized/_penalized_tweedie.py +65 -0
- statgpu/linear_model/penalized/_predict_mixin.py +182 -0
- statgpu/linear_model/wrappers/__init__.py +31 -0
- statgpu/linear_model/wrappers/_adaptive_lasso.py +63 -0
- statgpu/linear_model/wrappers/_elasticnet.py +75 -0
- statgpu/linear_model/wrappers/_gamma.py +67 -0
- statgpu/linear_model/wrappers/_inverse_gaussian.py +47 -0
- statgpu/linear_model/wrappers/_lasso.py +2124 -0
- statgpu/linear_model/wrappers/_linear.py +1127 -0
- statgpu/linear_model/wrappers/_logistic.py +1435 -0
- statgpu/linear_model/wrappers/_mcp.py +58 -0
- statgpu/linear_model/wrappers/_negative_binomial.py +58 -0
- statgpu/linear_model/wrappers/_poisson.py +48 -0
- statgpu/linear_model/wrappers/_ridge.py +166 -0
- statgpu/linear_model/wrappers/_scad.py +58 -0
- statgpu/linear_model/wrappers/_tweedie.py +57 -0
- statgpu/metrics/__init__.py +21 -0
- statgpu/metrics/_classification.py +591 -0
- statgpu/nonparametric/__init__.py +50 -0
- statgpu/nonparametric/kernel_methods/__init__.py +25 -0
- statgpu/nonparametric/kernel_methods/_kernels.py +246 -0
- statgpu/nonparametric/kernel_methods/_krr.py +234 -0
- statgpu/nonparametric/kernel_methods/_krr_cv.py +380 -0
- statgpu/nonparametric/kernel_smoothing/__init__.py +39 -0
- statgpu/nonparametric/kernel_smoothing/_bandwidth_selection.py +1083 -0
- statgpu/nonparametric/kernel_smoothing/_kde.py +761 -0
- statgpu/nonparametric/kernel_smoothing/_kernel_common.py +348 -0
- statgpu/nonparametric/kernel_smoothing/_kernel_regression.py +748 -0
- statgpu/nonparametric/splines/__init__.py +5 -0
- statgpu/nonparametric/splines/_bspline_basis.py +336 -0
- statgpu/nonparametric/splines/_penalized.py +349 -0
- statgpu/panel/__init__.py +19 -0
- statgpu/panel/_covariance.py +140 -0
- statgpu/panel/_fixed_effects.py +420 -0
- statgpu/panel/_random_effects.py +385 -0
- statgpu/panel/_utils.py +482 -0
- statgpu/penalties/__init__.py +139 -0
- statgpu/penalties/_adaptive_l1.py +313 -0
- statgpu/penalties/_base.py +261 -0
- statgpu/penalties/_categories.py +39 -0
- statgpu/penalties/_elasticnet.py +98 -0
- statgpu/penalties/_group_lasso.py +678 -0
- statgpu/penalties/_group_mcp.py +553 -0
- statgpu/penalties/_group_scad.py +605 -0
- statgpu/penalties/_l1.py +107 -0
- statgpu/penalties/_l2.py +77 -0
- statgpu/penalties/_mcp.py +237 -0
- statgpu/penalties/_scad.py +260 -0
- statgpu/semiparametric/__init__.py +5 -0
- statgpu/semiparametric/_gam.py +401 -0
- statgpu/solvers/__init__.py +24 -0
- statgpu/solvers/_admm.py +241 -0
- statgpu/solvers/_constants.py +15 -0
- statgpu/solvers/_convergence.py +6 -0
- statgpu/solvers/_fista.py +436 -0
- statgpu/solvers/_fista_bb.py +513 -0
- statgpu/solvers/_fista_lla.py +541 -0
- statgpu/solvers/_lbfgs.py +206 -0
- statgpu/solvers/_newton.py +149 -0
- statgpu/solvers/_utils.py +277 -0
- statgpu/survival/__init__.py +14 -0
- statgpu/survival/_cox.py +3974 -0
- statgpu/survival/_cox_breslow_triton_kernel.py +106 -0
- statgpu/survival/_cox_cv.py +1159 -0
- statgpu/survival/_cox_efron_cuda.py +1280 -0
- statgpu/survival/_cox_efron_triton.py +359 -0
- statgpu/unsupervised/__init__.py +29 -0
- statgpu/unsupervised/_agglomerative.py +307 -0
- statgpu/unsupervised/_dbscan.py +263 -0
- statgpu/unsupervised/_dbscan_cpu.pyx +125 -0
- statgpu/unsupervised/_gmm.py +332 -0
- statgpu/unsupervised/_incremental_pca.py +176 -0
- statgpu/unsupervised/_kmeans.py +261 -0
- statgpu/unsupervised/_minibatch_kmeans.py +299 -0
- statgpu/unsupervised/_minibatch_nmf.py +252 -0
- statgpu/unsupervised/_nmf.py +190 -0
- statgpu/unsupervised/_pca.py +189 -0
- statgpu/unsupervised/_truncated_svd.py +132 -0
- statgpu/unsupervised/_tsne.py +192 -0
- statgpu/unsupervised/_umap.py +224 -0
- statgpu/unsupervised/_utils.py +134 -0
- statgpu-0.1.0.dist-info/METADATA +245 -0
- statgpu-0.1.0.dist-info/RECORD +168 -0
- statgpu-0.1.0.dist-info/WHEEL +5 -0
- statgpu-0.1.0.dist-info/licenses/LICENSE +199 -0
- statgpu-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optimized Ridge regression with GPU support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy import stats
|
|
10
|
+
|
|
11
|
+
from statgpu._base import BaseEstimator
|
|
12
|
+
from statgpu._config import Device
|
|
13
|
+
from statgpu.backends import _LINALG_ERRORS, _get_torch_device_str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _RidgeLegacy(BaseEstimator):
|
|
17
|
+
"""
|
|
18
|
+
Legacy Ridge implementation (superseded by V9 wrapper below).
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
alpha : float, default=1.0
|
|
23
|
+
Regularization strength; must be a positive float.
|
|
24
|
+
fit_intercept : bool, default=True
|
|
25
|
+
Whether to calculate the intercept.
|
|
26
|
+
device : str or Device, default='auto'
|
|
27
|
+
Computation device: 'cpu', 'cuda', or 'auto'.
|
|
28
|
+
n_jobs : int or None, default=None
|
|
29
|
+
Number of parallel jobs.
|
|
30
|
+
gpu_memory_cleanup : bool, default=False
|
|
31
|
+
Whether to free CuPy memory pool after fitting.
|
|
32
|
+
compute_inference : bool, default=True
|
|
33
|
+
Whether to compute standard errors, t-stats, p-values and CI.
|
|
34
|
+
cov_type : str, default='nonrobust'
|
|
35
|
+
Covariance estimator for inference. One of:
|
|
36
|
+
``'nonrobust'`` (classical), ``'hc0'`` (White HC0), ``'hc1'`` (HC1),
|
|
37
|
+
``'hc2'`` (leverage-adjusted HC2), ``'hc3'`` (jackknife-style HC3),
|
|
38
|
+
or ``'hac'`` (Newey-West HAC with Bartlett kernel).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
alpha: float = 1.0,
|
|
44
|
+
fit_intercept: bool = True,
|
|
45
|
+
device: Union[str, Device] = Device.AUTO,
|
|
46
|
+
n_jobs: Optional[int] = None,
|
|
47
|
+
gpu_memory_cleanup: bool = False,
|
|
48
|
+
compute_inference: bool = True,
|
|
49
|
+
cov_type: str = "nonrobust",
|
|
50
|
+
hac_maxlags: Optional[int] = None,
|
|
51
|
+
):
|
|
52
|
+
super().__init__(device=device, n_jobs=n_jobs)
|
|
53
|
+
self.alpha = alpha
|
|
54
|
+
self.fit_intercept = fit_intercept
|
|
55
|
+
self.gpu_memory_cleanup = bool(gpu_memory_cleanup)
|
|
56
|
+
self.compute_inference = compute_inference
|
|
57
|
+
self.cov_type = cov_type.lower()
|
|
58
|
+
if self.cov_type not in ("nonrobust", "hc0", "hc1", "hc2", "hc3", "hac"):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"cov_type must be one of: 'nonrobust', 'hc0', 'hc1', 'hc2', 'hc3', 'hac'"
|
|
61
|
+
)
|
|
62
|
+
if hac_maxlags is not None and int(hac_maxlags) < 0:
|
|
63
|
+
raise ValueError("hac_maxlags must be a non-negative integer or None")
|
|
64
|
+
self.hac_maxlags = None if hac_maxlags is None else int(hac_maxlags)
|
|
65
|
+
self.coef_ = None
|
|
66
|
+
self.intercept_ = None
|
|
67
|
+
self._X_design = None
|
|
68
|
+
self._y = None
|
|
69
|
+
self._resid = None
|
|
70
|
+
self._scale = None
|
|
71
|
+
self._nobs = None
|
|
72
|
+
self._df_resid = None
|
|
73
|
+
self._params = None
|
|
74
|
+
self._bse = None
|
|
75
|
+
self._tvalues = None
|
|
76
|
+
self._pvalues = None
|
|
77
|
+
self._conf_int = None
|
|
78
|
+
|
|
79
|
+
def _cleanup_cuda_memory(self):
|
|
80
|
+
"""Best-effort CuPy memory pool cleanup."""
|
|
81
|
+
if not self.gpu_memory_cleanup:
|
|
82
|
+
return
|
|
83
|
+
try:
|
|
84
|
+
import cupy as cp
|
|
85
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
86
|
+
cp.get_default_pinned_memory_pool().free_all_blocks()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def _resolve_hac_maxlags(self, n_obs: int) -> int:
|
|
91
|
+
"""Resolve HAC lag count with a Newey-West style default rule."""
|
|
92
|
+
if n_obs <= 1:
|
|
93
|
+
return 0
|
|
94
|
+
if self.hac_maxlags is None:
|
|
95
|
+
maxlags = int(np.floor(4.0 * (n_obs / 100.0) ** (2.0 / 9.0)))
|
|
96
|
+
else:
|
|
97
|
+
maxlags = int(self.hac_maxlags)
|
|
98
|
+
return max(0, min(maxlags, n_obs - 1))
|
|
99
|
+
|
|
100
|
+
def _hac_meat_numpy(self, scores: np.ndarray) -> np.ndarray:
|
|
101
|
+
"""Bartlett-kernel HAC meat from per-observation score matrix."""
|
|
102
|
+
n_obs = int(scores.shape[0])
|
|
103
|
+
meat = scores.T @ scores
|
|
104
|
+
maxlags = self._resolve_hac_maxlags(n_obs)
|
|
105
|
+
if maxlags == 0:
|
|
106
|
+
return meat
|
|
107
|
+
for lag in range(1, maxlags + 1):
|
|
108
|
+
weight = 1.0 - (lag / (maxlags + 1.0))
|
|
109
|
+
gamma = scores[lag:].T @ scores[:-lag]
|
|
110
|
+
meat = meat + weight * (gamma + gamma.T)
|
|
111
|
+
return meat
|
|
112
|
+
|
|
113
|
+
def _hac_meat_cupy(self, scores):
|
|
114
|
+
"""CuPy Bartlett-kernel HAC meat from per-observation score matrix."""
|
|
115
|
+
import cupy as cp
|
|
116
|
+
|
|
117
|
+
n_obs = int(scores.shape[0])
|
|
118
|
+
meat = scores.T @ scores
|
|
119
|
+
maxlags = self._resolve_hac_maxlags(n_obs)
|
|
120
|
+
if maxlags == 0:
|
|
121
|
+
return meat
|
|
122
|
+
for lag in range(1, maxlags + 1):
|
|
123
|
+
weight = 1.0 - (lag / (maxlags + 1.0))
|
|
124
|
+
gamma = scores[lag:].T @ scores[:-lag]
|
|
125
|
+
meat = meat + weight * (gamma + gamma.T)
|
|
126
|
+
return meat
|
|
127
|
+
|
|
128
|
+
def _robust_covariance_numpy(self, X: np.ndarray, resid: np.ndarray, XtX_inv: np.ndarray) -> np.ndarray:
|
|
129
|
+
"""Compute robust/HAC covariance matrix for Ridge score equations."""
|
|
130
|
+
n, k = X.shape
|
|
131
|
+
e = np.asarray(resid, dtype=float).reshape(-1)
|
|
132
|
+
|
|
133
|
+
if self.cov_type == "hac":
|
|
134
|
+
scores = X * e[:, np.newaxis]
|
|
135
|
+
meat = self._hac_meat_numpy(scores)
|
|
136
|
+
return XtX_inv @ meat @ XtX_inv
|
|
137
|
+
|
|
138
|
+
if self.cov_type in ("hc2", "hc3"):
|
|
139
|
+
leverage = np.einsum("ij,jk,ik->i", X, XtX_inv, X)
|
|
140
|
+
leverage = np.clip(leverage, 0.0, 1.0 - 1e-12)
|
|
141
|
+
if self.cov_type == "hc2":
|
|
142
|
+
e2 = (e ** 2) / (1.0 - leverage)
|
|
143
|
+
else:
|
|
144
|
+
e2 = (e ** 2) / ((1.0 - leverage) ** 2)
|
|
145
|
+
else:
|
|
146
|
+
e2 = e ** 2
|
|
147
|
+
|
|
148
|
+
Xw = X * e2[:, np.newaxis]
|
|
149
|
+
meat = X.T @ Xw
|
|
150
|
+
cov_params = XtX_inv @ meat @ XtX_inv
|
|
151
|
+
if self.cov_type == "hc1" and n > k:
|
|
152
|
+
cov_params *= n / (n - k)
|
|
153
|
+
return cov_params
|
|
154
|
+
|
|
155
|
+
def _robust_covariance_cupy(self, X, resid, XtX_inv):
|
|
156
|
+
"""Compute robust/HAC covariance matrix for Ridge score equations on GPU."""
|
|
157
|
+
import cupy as cp
|
|
158
|
+
|
|
159
|
+
n, k = X.shape
|
|
160
|
+
e = resid.reshape(-1)
|
|
161
|
+
|
|
162
|
+
if self.cov_type == "hac":
|
|
163
|
+
scores = X * e[:, cp.newaxis]
|
|
164
|
+
meat = self._hac_meat_cupy(scores)
|
|
165
|
+
return XtX_inv @ meat @ XtX_inv
|
|
166
|
+
|
|
167
|
+
if self.cov_type in ("hc2", "hc3"):
|
|
168
|
+
leverage = cp.einsum("ij,jk,ik->i", X, XtX_inv, X)
|
|
169
|
+
leverage = cp.clip(leverage, 0.0, 1.0 - 1e-12)
|
|
170
|
+
if self.cov_type == "hc2":
|
|
171
|
+
e2 = cp.square(e) / (1.0 - leverage)
|
|
172
|
+
else:
|
|
173
|
+
e2 = cp.square(e) / cp.square(1.0 - leverage)
|
|
174
|
+
else:
|
|
175
|
+
e2 = cp.square(e)
|
|
176
|
+
|
|
177
|
+
Xw = X * e2[:, cp.newaxis]
|
|
178
|
+
meat = X.T @ Xw
|
|
179
|
+
cov_params = XtX_inv @ meat @ XtX_inv
|
|
180
|
+
if self.cov_type == "hc1" and n > k:
|
|
181
|
+
cov_params = cov_params * (n / (n - k))
|
|
182
|
+
return cov_params
|
|
183
|
+
|
|
184
|
+
def fit(self, X, y, sample_weight=None):
|
|
185
|
+
"""Fit Ridge regression model."""
|
|
186
|
+
# Store y (may be CuPy/Torch array, convert later)
|
|
187
|
+
self._y = y
|
|
188
|
+
|
|
189
|
+
# Get backend - support explicit torch backend selection
|
|
190
|
+
backend = self._get_backend(backend="auto")
|
|
191
|
+
backend_name = backend.name
|
|
192
|
+
|
|
193
|
+
X_arr = self._to_array(X, backend=backend_name)
|
|
194
|
+
y_arr = self._to_array(y, backend=backend_name)
|
|
195
|
+
|
|
196
|
+
device = self._get_compute_device()
|
|
197
|
+
|
|
198
|
+
# Route to appropriate backend
|
|
199
|
+
if backend_name == "torch":
|
|
200
|
+
self._fit_torch(X_arr, y_arr, sample_weight)
|
|
201
|
+
elif backend_name == "cupy":
|
|
202
|
+
self._fit_gpu(X_arr, y_arr, sample_weight)
|
|
203
|
+
else:
|
|
204
|
+
self._fit_cpu(X_arr, y_arr, sample_weight)
|
|
205
|
+
|
|
206
|
+
# Now convert y to numpy for diagnostics
|
|
207
|
+
if hasattr(self._y, 'get'): # CuPy
|
|
208
|
+
self._y = self._y.get()
|
|
209
|
+
elif hasattr(self._y, 'cpu'): # Torch
|
|
210
|
+
self._y = self._y.cpu().numpy()
|
|
211
|
+
else:
|
|
212
|
+
self._y = np.asarray(self._y)
|
|
213
|
+
|
|
214
|
+
# GPU path already computes inference on-device in _fit_gpu/_fit_torch().
|
|
215
|
+
if self.compute_inference and device == Device.CPU:
|
|
216
|
+
self._compute_inference()
|
|
217
|
+
self._fitted = True
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
def _fit_cpu(self, X, y, sample_weight=None):
|
|
221
|
+
"""Fit using CPU with optimized memory usage."""
|
|
222
|
+
X = np.asarray(X)
|
|
223
|
+
y = np.asarray(y)
|
|
224
|
+
n_samples, n_features = X.shape
|
|
225
|
+
self._nobs = n_samples
|
|
226
|
+
|
|
227
|
+
if sample_weight is not None:
|
|
228
|
+
sample_weight = np.asarray(sample_weight)
|
|
229
|
+
sqrt_sw = np.sqrt(sample_weight)
|
|
230
|
+
X = X * sqrt_sw[:, np.newaxis]
|
|
231
|
+
y = y * sqrt_sw
|
|
232
|
+
|
|
233
|
+
if self.fit_intercept:
|
|
234
|
+
X_mean = np.mean(X, axis=0)
|
|
235
|
+
y_mean = np.mean(y)
|
|
236
|
+
# Avoid creating full X_centered (n×p) matrix when computing XtX/Xty.
|
|
237
|
+
# Use the centering formula: X_centered.T @ X_centered = X.T@X - n*outer(mean)
|
|
238
|
+
# This reduces memory from O(n*p) to O(p²).
|
|
239
|
+
XtX = X.T @ X
|
|
240
|
+
XtX -= n_samples * np.outer(X_mean, X_mean)
|
|
241
|
+
Xty = X.T @ y
|
|
242
|
+
Xty -= n_samples * X_mean * y_mean
|
|
243
|
+
else:
|
|
244
|
+
y_mean = 0.0
|
|
245
|
+
XtX = X.T @ X
|
|
246
|
+
Xty = X.T @ y
|
|
247
|
+
|
|
248
|
+
if Xty.ndim == 1:
|
|
249
|
+
Xty = Xty.reshape(-1, 1)
|
|
250
|
+
|
|
251
|
+
I = np.eye(n_features)
|
|
252
|
+
XtX_reg = XtX + self.alpha * I
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
coef = np.linalg.solve(XtX_reg, Xty)
|
|
256
|
+
except np.linalg.LinAlgError:
|
|
257
|
+
coef = np.linalg.lstsq(XtX_reg, Xty, rcond=None)[0]
|
|
258
|
+
|
|
259
|
+
coef = coef.flatten()
|
|
260
|
+
|
|
261
|
+
# Only build design matrix and compute residuals when inference is needed
|
|
262
|
+
if self.fit_intercept:
|
|
263
|
+
self.intercept_ = float(y_mean - X_mean @ coef)
|
|
264
|
+
self.coef_ = coef
|
|
265
|
+
self._params = np.concatenate([[self.intercept_], self.coef_])
|
|
266
|
+
else:
|
|
267
|
+
self.intercept_ = 0.0
|
|
268
|
+
self.coef_ = coef
|
|
269
|
+
self._params = self.coef_.copy()
|
|
270
|
+
|
|
271
|
+
self._df_resid = n_samples - (n_features + (1 if self.fit_intercept else 0))
|
|
272
|
+
|
|
273
|
+
if self.compute_inference:
|
|
274
|
+
if self.fit_intercept:
|
|
275
|
+
self._X_design = np.column_stack([np.ones(n_samples, dtype=X.dtype), X])
|
|
276
|
+
else:
|
|
277
|
+
self._X_design = X.copy()
|
|
278
|
+
y_pred = self._X_design @ self._params
|
|
279
|
+
self._resid = self._y - y_pred
|
|
280
|
+
if self._df_resid > 0:
|
|
281
|
+
self._scale = np.sum(self._resid ** 2) / self._df_resid
|
|
282
|
+
else:
|
|
283
|
+
self._scale = np.nan
|
|
284
|
+
else:
|
|
285
|
+
self._X_design = None
|
|
286
|
+
self._resid = None
|
|
287
|
+
self._scale = np.nan
|
|
288
|
+
|
|
289
|
+
def _fit_gpu(self, X, y, sample_weight=None):
|
|
290
|
+
"""Fit using GPU (optimized)."""
|
|
291
|
+
import cupy as cp
|
|
292
|
+
|
|
293
|
+
n_samples, n_features = X.shape
|
|
294
|
+
self._nobs = n_samples
|
|
295
|
+
|
|
296
|
+
# Ensure CuPy arrays
|
|
297
|
+
X = cp.asarray(X)
|
|
298
|
+
y = cp.asarray(y)
|
|
299
|
+
|
|
300
|
+
if sample_weight is not None:
|
|
301
|
+
sample_weight = cp.asarray(sample_weight)
|
|
302
|
+
sqrt_sw = cp.sqrt(sample_weight)
|
|
303
|
+
X = X * sqrt_sw[:, np.newaxis]
|
|
304
|
+
y = y * sqrt_sw
|
|
305
|
+
|
|
306
|
+
if self.fit_intercept:
|
|
307
|
+
X_mean = cp.mean(X, axis=0)
|
|
308
|
+
y_mean = cp.mean(y)
|
|
309
|
+
X_centered = X - X_mean
|
|
310
|
+
y_centered = y - y_mean
|
|
311
|
+
else:
|
|
312
|
+
X_centered = X
|
|
313
|
+
y_mean = cp.array(0.0)
|
|
314
|
+
|
|
315
|
+
if y.ndim == 1:
|
|
316
|
+
y_centered = y_centered.reshape(-1, 1)
|
|
317
|
+
|
|
318
|
+
# Ridge closed-form
|
|
319
|
+
XtX = X_centered.T @ X_centered
|
|
320
|
+
Xty = X_centered.T @ y_centered
|
|
321
|
+
|
|
322
|
+
I = cp.eye(n_features)
|
|
323
|
+
XtX_reg = XtX + self.alpha * I
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Cholesky for better performance
|
|
327
|
+
L = cp.linalg.cholesky(XtX_reg)
|
|
328
|
+
tmp = cp.linalg.solve_triangular(L, Xty, lower=True)
|
|
329
|
+
coef = cp.linalg.solve_triangular(L.T, tmp, lower=False)
|
|
330
|
+
except _LINALG_ERRORS:
|
|
331
|
+
coef = cp.linalg.solve(XtX_reg, Xty)
|
|
332
|
+
|
|
333
|
+
# Keep on GPU for residuals
|
|
334
|
+
if self.fit_intercept:
|
|
335
|
+
X_design = cp.column_stack([cp.ones(n_samples, dtype=X.dtype), X])
|
|
336
|
+
coef_full = cp.concatenate([y_mean - X_mean @ coef, coef.flatten()])
|
|
337
|
+
else:
|
|
338
|
+
X_design = X
|
|
339
|
+
coef_full = coef.flatten()
|
|
340
|
+
|
|
341
|
+
y_pred = X_design @ coef_full
|
|
342
|
+
resid = y - y_pred
|
|
343
|
+
|
|
344
|
+
df_resid = n_samples - (n_features + (1 if self.fit_intercept else 0))
|
|
345
|
+
if df_resid > 0:
|
|
346
|
+
scale = cp.sum(resid ** 2) / df_resid
|
|
347
|
+
else:
|
|
348
|
+
scale = cp.nan
|
|
349
|
+
|
|
350
|
+
# Compute ALL statistics on GPU
|
|
351
|
+
from statgpu.backends._gpu_inference_cupy import compute_inference_gpu, compute_r2_gpu, compute_aic_bic_gpu, compute_f_stat_gpu
|
|
352
|
+
from statgpu.inference._distributions_backend import norm
|
|
353
|
+
|
|
354
|
+
if self.compute_inference:
|
|
355
|
+
if self.cov_type == "nonrobust":
|
|
356
|
+
self._bse_gpu, self._tvalues_gpu, self._pvalues_gpu, self._conf_int_gpu = \
|
|
357
|
+
compute_inference_gpu(X_design, resid, scale, df_resid, coef_full)
|
|
358
|
+
else:
|
|
359
|
+
XtX_cov = X_design.T @ X_design
|
|
360
|
+
# Apply ridge penalty excluding the intercept column
|
|
361
|
+
k_design = X_design.shape[1]
|
|
362
|
+
penalty_diag = cp.ones(k_design, dtype=cp.float64) * self.alpha
|
|
363
|
+
if self.fit_intercept:
|
|
364
|
+
penalty_diag[0] = 0.0 # no penalty on the intercept term
|
|
365
|
+
XtX_pen = XtX_cov + cp.diag(penalty_diag)
|
|
366
|
+
try:
|
|
367
|
+
XtX_inv = cp.linalg.inv(XtX_pen)
|
|
368
|
+
except Exception:
|
|
369
|
+
XtX_inv = cp.linalg.pinv(XtX_pen)
|
|
370
|
+
cov_params = self._robust_covariance_cupy(X_design, resid, XtX_inv)
|
|
371
|
+
self._bse_gpu = cp.sqrt(cp.maximum(cp.diag(cov_params), 0.0))
|
|
372
|
+
self._tvalues_gpu = coef_full / (self._bse_gpu + 1e-30)
|
|
373
|
+
self._pvalues_gpu = cp.minimum(1.0, 2.0 * norm.sf(cp.abs(self._tvalues_gpu)))
|
|
374
|
+
z_crit = norm.ppf(0.975)
|
|
375
|
+
self._conf_int_gpu = cp.stack([
|
|
376
|
+
coef_full - z_crit * self._bse_gpu,
|
|
377
|
+
coef_full + z_crit * self._bse_gpu,
|
|
378
|
+
], axis=1)
|
|
379
|
+
|
|
380
|
+
self._rsquared_gpu = compute_r2_gpu(y, resid)
|
|
381
|
+
|
|
382
|
+
k = n_features + (1 if self.fit_intercept else 0)
|
|
383
|
+
scale_mle = cp.sum(resid ** 2) / n_samples
|
|
384
|
+
self._aic_gpu, self._bic_gpu = compute_aic_bic_gpu(n_samples, k, scale_mle)
|
|
385
|
+
|
|
386
|
+
self._fvalue_gpu, self._f_pvalue = compute_f_stat_gpu(y, resid, X_design, df_resid)
|
|
387
|
+
|
|
388
|
+
# Single transfer to CPU at the end
|
|
389
|
+
coef_full_np = coef_full.get()
|
|
390
|
+
resid_np = resid.get()
|
|
391
|
+
scale_float = float(scale.get()) if not cp.isnan(scale) else np.nan
|
|
392
|
+
X_design_np = X_design.get()
|
|
393
|
+
|
|
394
|
+
# Transfer inference results
|
|
395
|
+
if self.compute_inference:
|
|
396
|
+
self._bse = self._bse_gpu.get()
|
|
397
|
+
self._tvalues = self._tvalues_gpu.get()
|
|
398
|
+
self._pvalues = self._pvalues_gpu.get()
|
|
399
|
+
self._conf_int = self._conf_int_gpu.get()
|
|
400
|
+
|
|
401
|
+
# Store
|
|
402
|
+
if self.fit_intercept:
|
|
403
|
+
self.intercept_ = float(coef_full_np[0])
|
|
404
|
+
self.coef_ = coef_full_np[1:]
|
|
405
|
+
self._params = coef_full_np
|
|
406
|
+
else:
|
|
407
|
+
self.intercept_ = 0.0
|
|
408
|
+
self.coef_ = coef_full_np
|
|
409
|
+
self._params = coef_full_np
|
|
410
|
+
|
|
411
|
+
self._X_design = X_design_np
|
|
412
|
+
self._resid = resid_np
|
|
413
|
+
self._df_resid = df_resid
|
|
414
|
+
self._scale = scale_float
|
|
415
|
+
|
|
416
|
+
# Release large temporary GPU tensors early.
|
|
417
|
+
try:
|
|
418
|
+
del X_design
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
try:
|
|
422
|
+
del resid
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
try:
|
|
426
|
+
del XtX
|
|
427
|
+
except Exception:
|
|
428
|
+
pass
|
|
429
|
+
try:
|
|
430
|
+
del Xty
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
433
|
+
try:
|
|
434
|
+
del XtX_reg
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
437
|
+
self._cleanup_cuda_memory()
|
|
438
|
+
|
|
439
|
+
def _cleanup_torch_memory(self):
|
|
440
|
+
"""Best-effort Torch CUDA memory cleanup."""
|
|
441
|
+
if not self.gpu_memory_cleanup:
|
|
442
|
+
return
|
|
443
|
+
try:
|
|
444
|
+
import torch
|
|
445
|
+
if torch.cuda.is_available():
|
|
446
|
+
torch.cuda.empty_cache()
|
|
447
|
+
torch.cuda.synchronize()
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
|
|
451
|
+
def _robust_covariance_torch(self, X, resid, XtX_inv):
|
|
452
|
+
"""Compute robust/HAC covariance matrix for Ridge score equations on Torch GPU."""
|
|
453
|
+
import torch
|
|
454
|
+
|
|
455
|
+
n, k = X.shape
|
|
456
|
+
e = resid.reshape(-1)
|
|
457
|
+
|
|
458
|
+
if self.cov_type == "hac":
|
|
459
|
+
scores = X * e[:, None]
|
|
460
|
+
meat = self._hac_meat_torch(scores)
|
|
461
|
+
return XtX_inv @ meat @ XtX_inv
|
|
462
|
+
|
|
463
|
+
if self.cov_type in ("hc2", "hc3"):
|
|
464
|
+
leverage = torch.einsum("ij,jk,ik->i", X, XtX_inv, X)
|
|
465
|
+
leverage = torch.clamp(leverage, 0.0, 1.0 - 1e-12)
|
|
466
|
+
if self.cov_type == "hc2":
|
|
467
|
+
e2 = torch.square(e) / (1.0 - leverage)
|
|
468
|
+
else:
|
|
469
|
+
e2 = torch.square(e) / torch.square(1.0 - leverage)
|
|
470
|
+
else:
|
|
471
|
+
e2 = torch.square(e)
|
|
472
|
+
|
|
473
|
+
Xw = X * e2[:, None]
|
|
474
|
+
meat = X.T @ Xw
|
|
475
|
+
cov_params = XtX_inv @ meat @ XtX_inv
|
|
476
|
+
if self.cov_type == "hc1" and n > k:
|
|
477
|
+
cov_params = cov_params * (n / (n - k))
|
|
478
|
+
return cov_params
|
|
479
|
+
|
|
480
|
+
def _hac_meat_torch(self, scores):
|
|
481
|
+
"""Torch Bartlett-kernel HAC meat from per-observation score matrix."""
|
|
482
|
+
import torch
|
|
483
|
+
|
|
484
|
+
n_obs = int(scores.shape[0])
|
|
485
|
+
meat = scores.T @ scores
|
|
486
|
+
maxlags = self._resolve_hac_maxlags(n_obs)
|
|
487
|
+
if maxlags == 0:
|
|
488
|
+
return meat
|
|
489
|
+
for lag in range(1, maxlags + 1):
|
|
490
|
+
weight = 1.0 - (lag / (maxlags + 1.0))
|
|
491
|
+
gamma = scores[lag:].T @ scores[:-lag]
|
|
492
|
+
meat = meat + weight * (gamma + gamma.T)
|
|
493
|
+
return meat
|
|
494
|
+
|
|
495
|
+
def _fit_torch(self, X, y, sample_weight=None):
|
|
496
|
+
"""Fit using Torch GPU."""
|
|
497
|
+
import torch
|
|
498
|
+
from statgpu.backends._gpu_inference_torch import (
|
|
499
|
+
compute_inference_torch,
|
|
500
|
+
compute_r2_torch,
|
|
501
|
+
compute_aic_bic_torch,
|
|
502
|
+
compute_f_stat_torch,
|
|
503
|
+
)
|
|
504
|
+
from statgpu.inference._distributions_backend import norm
|
|
505
|
+
|
|
506
|
+
# Note: Device.TORCH.value is 'torch', but Torch expects 'cuda' or 'cpu'
|
|
507
|
+
torch_device = _get_torch_device_str()
|
|
508
|
+
|
|
509
|
+
n_samples, n_features = X.shape
|
|
510
|
+
self._nobs = n_samples
|
|
511
|
+
|
|
512
|
+
# Ensure Torch tensors on GPU
|
|
513
|
+
if not isinstance(X, torch.Tensor):
|
|
514
|
+
X = torch.from_numpy(X).to(torch_device)
|
|
515
|
+
if not isinstance(y, torch.Tensor):
|
|
516
|
+
y = torch.from_numpy(y).to(torch_device)
|
|
517
|
+
if y.dtype != torch.float64:
|
|
518
|
+
y = y.to(torch.float64)
|
|
519
|
+
if X.dtype != torch.float64:
|
|
520
|
+
X = X.to(torch.float64)
|
|
521
|
+
|
|
522
|
+
if sample_weight is not None:
|
|
523
|
+
if not isinstance(sample_weight, torch.Tensor):
|
|
524
|
+
sample_weight = torch.from_numpy(sample_weight).to(torch_device)
|
|
525
|
+
sqrt_sw = torch.sqrt(sample_weight)
|
|
526
|
+
X = X * sqrt_sw[:, None]
|
|
527
|
+
y = y * sqrt_sw
|
|
528
|
+
|
|
529
|
+
if self.fit_intercept:
|
|
530
|
+
X_mean = torch.mean(X, axis=0)
|
|
531
|
+
y_mean = torch.mean(y)
|
|
532
|
+
X_centered = X - X_mean
|
|
533
|
+
y_centered = y - y_mean
|
|
534
|
+
else:
|
|
535
|
+
X_centered = X
|
|
536
|
+
y_mean = torch.tensor(0.0, device=torch_device)
|
|
537
|
+
|
|
538
|
+
if y.ndim == 1:
|
|
539
|
+
y_centered = y_centered.reshape(-1, 1)
|
|
540
|
+
|
|
541
|
+
# Ridge closed-form
|
|
542
|
+
XtX = X_centered.T @ X_centered
|
|
543
|
+
Xty = X_centered.T @ y_centered
|
|
544
|
+
|
|
545
|
+
I = torch.eye(n_features, dtype=torch.float64, device=torch_device)
|
|
546
|
+
XtX_reg = XtX + self.alpha * I
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
# Cholesky for better performance
|
|
550
|
+
L = torch.linalg.cholesky(XtX_reg)
|
|
551
|
+
tmp = torch.linalg.solve_triangular(L, Xty, upper=False)
|
|
552
|
+
coef = torch.linalg.solve_triangular(L.T, tmp, upper=True)
|
|
553
|
+
except _LINALG_ERRORS:
|
|
554
|
+
coef = torch.linalg.solve(XtX_reg, Xty)
|
|
555
|
+
|
|
556
|
+
# Keep on GPU for residuals
|
|
557
|
+
if self.fit_intercept:
|
|
558
|
+
X_design = torch.cat([torch.ones(n_samples, 1, dtype=torch.float64, device=torch_device), X], dim=1)
|
|
559
|
+
intercept_coef = y_mean - X_mean @ coef
|
|
560
|
+
coef_full = torch.cat([intercept_coef.reshape(-1), coef.flatten()])
|
|
561
|
+
else:
|
|
562
|
+
X_design = X
|
|
563
|
+
coef_full = coef.flatten()
|
|
564
|
+
|
|
565
|
+
y_pred = X_design @ coef_full
|
|
566
|
+
resid = y - y_pred
|
|
567
|
+
|
|
568
|
+
df_resid = n_samples - (n_features + (1 if self.fit_intercept else 0))
|
|
569
|
+
if df_resid > 0:
|
|
570
|
+
scale = torch.sum(resid ** 2) / df_resid
|
|
571
|
+
else:
|
|
572
|
+
scale = torch.tensor(float('nan'), dtype=torch.float64, device=torch_device)
|
|
573
|
+
|
|
574
|
+
# Compute ALL statistics on GPU
|
|
575
|
+
if self.compute_inference:
|
|
576
|
+
if self.cov_type == "nonrobust":
|
|
577
|
+
self._bse_gpu, self._tvalues_gpu, self._pvalues_gpu, self._conf_int_gpu = \
|
|
578
|
+
compute_inference_torch(X_design, resid, scale, df_resid, coef_full, device=torch_device)
|
|
579
|
+
else:
|
|
580
|
+
XtX_cov = X_design.T @ X_design
|
|
581
|
+
# Apply ridge penalty excluding the intercept column
|
|
582
|
+
k_design = X_design.shape[1]
|
|
583
|
+
penalty_diag = torch.ones(k_design, dtype=torch.float64, device=torch_device) * self.alpha
|
|
584
|
+
if self.fit_intercept:
|
|
585
|
+
penalty_diag[0] = 0.0 # no penalty on the intercept term
|
|
586
|
+
XtX_pen = XtX_cov + torch.diag(penalty_diag)
|
|
587
|
+
try:
|
|
588
|
+
XtX_inv = torch.linalg.inv(XtX_pen)
|
|
589
|
+
except Exception:
|
|
590
|
+
XtX_inv = torch.linalg.pinv(XtX_pen)
|
|
591
|
+
cov_params = self._robust_covariance_torch(X_design, resid, XtX_inv)
|
|
592
|
+
self._bse_gpu = torch.sqrt(torch.clamp(torch.diag(cov_params), 0.0))
|
|
593
|
+
self._tvalues_gpu = coef_full / (self._bse_gpu + 1e-30)
|
|
594
|
+
self._pvalues_gpu = torch.minimum(torch.tensor(1.0, device=torch_device), 2.0 * norm.sf(torch.abs(self._tvalues_gpu), device=torch_device))
|
|
595
|
+
z_crit = norm.ppf(0.975, device=torch_device)
|
|
596
|
+
self._conf_int_gpu = torch.stack([
|
|
597
|
+
coef_full - z_crit * self._bse_gpu,
|
|
598
|
+
coef_full + z_crit * self._bse_gpu,
|
|
599
|
+
], dim=1)
|
|
600
|
+
|
|
601
|
+
self._rsquared_gpu = compute_r2_torch(y, resid)
|
|
602
|
+
|
|
603
|
+
k = n_features + (1 if self.fit_intercept else 0)
|
|
604
|
+
scale_mle = torch.sum(resid ** 2) / n_samples
|
|
605
|
+
self._aic_gpu, self._bic_gpu = compute_aic_bic_torch(n_samples, k, scale_mle, device=torch_device)
|
|
606
|
+
|
|
607
|
+
self._fvalue_gpu, self._f_pvalue = compute_f_stat_torch(y, resid, X_design, df_resid, device=torch_device)
|
|
608
|
+
|
|
609
|
+
# Single transfer to CPU at the end
|
|
610
|
+
coef_full_np = coef_full.cpu().numpy()
|
|
611
|
+
resid_np = resid.cpu().numpy()
|
|
612
|
+
scale_float = float(scale.cpu().numpy()) if not torch.isnan(scale) else np.nan
|
|
613
|
+
X_design_np = X_design.cpu().numpy()
|
|
614
|
+
|
|
615
|
+
# Transfer inference results
|
|
616
|
+
if self.compute_inference:
|
|
617
|
+
self._bse = self._bse_gpu.cpu().numpy()
|
|
618
|
+
self._tvalues = self._tvalues_gpu.cpu().numpy()
|
|
619
|
+
self._pvalues = self._pvalues_gpu.cpu().numpy()
|
|
620
|
+
self._conf_int = self._conf_int_gpu.cpu().numpy()
|
|
621
|
+
|
|
622
|
+
# Store
|
|
623
|
+
if self.fit_intercept:
|
|
624
|
+
self.intercept_ = float(coef_full_np[0])
|
|
625
|
+
self.coef_ = coef_full_np[1:]
|
|
626
|
+
self._params = coef_full_np
|
|
627
|
+
else:
|
|
628
|
+
self.intercept_ = 0.0
|
|
629
|
+
self.coef_ = coef_full_np
|
|
630
|
+
self._params = coef_full_np
|
|
631
|
+
|
|
632
|
+
self._X_design = X_design_np
|
|
633
|
+
self._resid = resid_np
|
|
634
|
+
self._df_resid = df_resid
|
|
635
|
+
self._scale = scale_float
|
|
636
|
+
|
|
637
|
+
# Release large temporary GPU tensors early.
|
|
638
|
+
try:
|
|
639
|
+
del X_design
|
|
640
|
+
except Exception:
|
|
641
|
+
pass
|
|
642
|
+
try:
|
|
643
|
+
del resid
|
|
644
|
+
except Exception:
|
|
645
|
+
pass
|
|
646
|
+
try:
|
|
647
|
+
del XtX
|
|
648
|
+
except Exception:
|
|
649
|
+
pass
|
|
650
|
+
try:
|
|
651
|
+
del Xty
|
|
652
|
+
except Exception:
|
|
653
|
+
pass
|
|
654
|
+
try:
|
|
655
|
+
del XtX_reg
|
|
656
|
+
except Exception:
|
|
657
|
+
pass
|
|
658
|
+
self._cleanup_torch_memory()
|
|
659
|
+
|
|
660
|
+
def _compute_inference(self):
|
|
661
|
+
"""Compute standard errors, t-stats, p-values, and CIs."""
|
|
662
|
+
if self._X_design is None or self._scale is None or np.isnan(self._scale):
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
X = self._X_design
|
|
666
|
+
n = X.shape[0]
|
|
667
|
+
k = X.shape[1]
|
|
668
|
+
|
|
669
|
+
# Build the penalized bread (X'X + alpha·P)^{-1} where the penalty
|
|
670
|
+
# matrix P excludes the intercept column (if fit_intercept is True).
|
|
671
|
+
# This ensures SE/t/p are consistent with the ridge fit rather than OLS.
|
|
672
|
+
XtX = X.T @ X
|
|
673
|
+
penalty_diag = np.ones(k) * self.alpha
|
|
674
|
+
if self.fit_intercept:
|
|
675
|
+
penalty_diag[0] = 0.0 # no penalty on the intercept term
|
|
676
|
+
XtX_pen = XtX + np.diag(penalty_diag)
|
|
677
|
+
try:
|
|
678
|
+
XtX_inv = np.linalg.inv(XtX_pen)
|
|
679
|
+
except np.linalg.LinAlgError:
|
|
680
|
+
XtX_inv = np.linalg.pinv(XtX_pen)
|
|
681
|
+
|
|
682
|
+
alpha = 0.05
|
|
683
|
+
|
|
684
|
+
if self.cov_type == "nonrobust":
|
|
685
|
+
cov_params = self._scale * XtX_inv
|
|
686
|
+
self._bse = np.sqrt(np.diag(cov_params))
|
|
687
|
+
self._tvalues = self._params / (self._bse + 1e-30)
|
|
688
|
+
self._pvalues = 2 * (1 - stats.t.cdf(np.abs(self._tvalues), self._df_resid))
|
|
689
|
+
t_crit = stats.t.ppf(1 - alpha / 2, self._df_resid)
|
|
690
|
+
self._conf_int = np.column_stack([
|
|
691
|
+
self._params - t_crit * self._bse,
|
|
692
|
+
self._params + t_crit * self._bse,
|
|
693
|
+
])
|
|
694
|
+
else:
|
|
695
|
+
cov_params = self._robust_covariance_numpy(X, self._resid, XtX_inv)
|
|
696
|
+
self._bse = np.sqrt(np.maximum(np.diag(cov_params), 0.0))
|
|
697
|
+
self._tvalues = self._params / (self._bse + 1e-30)
|
|
698
|
+
# Robust path uses large-sample normal approximation.
|
|
699
|
+
self._pvalues = 2 * (1 - stats.norm.cdf(np.abs(self._tvalues)))
|
|
700
|
+
z_crit = stats.norm.ppf(1 - alpha / 2)
|
|
701
|
+
self._conf_int = np.column_stack([
|
|
702
|
+
self._params - z_crit * self._bse,
|
|
703
|
+
self._params + z_crit * self._bse,
|
|
704
|
+
])
|
|
705
|
+
|
|
706
|
+
def predict(self, X):
|
|
707
|
+
"""Predict."""
|
|
708
|
+
self._check_is_fitted()
|
|
709
|
+
device = self._get_compute_device()
|
|
710
|
+
if device == Device.CUDA:
|
|
711
|
+
import cupy as cp
|
|
712
|
+
|
|
713
|
+
X_gpu = cp.asarray(self._to_array(X, Device.CUDA))
|
|
714
|
+
coef_gpu = cp.asarray(self.coef_)
|
|
715
|
+
intercept_gpu = cp.asarray(self.intercept_, dtype=coef_gpu.dtype)
|
|
716
|
+
return X_gpu @ coef_gpu + intercept_gpu
|
|
717
|
+
if device == Device.TORCH:
|
|
718
|
+
import torch
|
|
719
|
+
|
|
720
|
+
X_torch = self._to_array(X, Device.TORCH, backend="torch").to(torch.float64)
|
|
721
|
+
coef_torch = torch.as_tensor(self.coef_, dtype=X_torch.dtype, device=X_torch.device)
|
|
722
|
+
intercept_torch = torch.as_tensor(
|
|
723
|
+
self.intercept_, dtype=X_torch.dtype, device=X_torch.device
|
|
724
|
+
)
|
|
725
|
+
return X_torch @ coef_torch + intercept_torch
|
|
726
|
+
X = self._to_array(X, Device.CPU)
|
|
727
|
+
X = np.asarray(X)
|
|
728
|
+
return X @ self.coef_ + self.intercept_
|
|
729
|
+
|
|
730
|
+
def score(self, X, y):
|
|
731
|
+
"""R² score."""
|
|
732
|
+
y_pred = self.predict(X)
|
|
733
|
+
device = self._get_compute_device()
|
|
734
|
+
if device == Device.CUDA:
|
|
735
|
+
import cupy as cp
|
|
736
|
+
|
|
737
|
+
yb = cp.asarray(self._to_array(y, Device.CUDA))
|
|
738
|
+
ss_res = cp.sum((yb - y_pred) ** 2)
|
|
739
|
+
ss_tot = cp.sum((yb - cp.mean(yb)) ** 2)
|
|
740
|
+
return float((1 - ss_res / ss_tot).item()) if float(ss_tot.item()) > 0 else 0.0
|
|
741
|
+
if device == Device.TORCH:
|
|
742
|
+
import torch
|
|
743
|
+
|
|
744
|
+
yb = self._to_array(y, Device.TORCH, backend="torch").to(y_pred.dtype)
|
|
745
|
+
ss_res = torch.sum((yb - y_pred) ** 2)
|
|
746
|
+
ss_tot = torch.sum((yb - torch.mean(yb)) ** 2)
|
|
747
|
+
return float((1 - ss_res / ss_tot).item()) if float(ss_tot.item()) > 0 else 0.0
|
|
748
|
+
y_pred = np.asarray(y_pred)
|
|
749
|
+
y = self._to_numpy(y)
|
|
750
|
+
ss_res = np.sum((y - y_pred) ** 2)
|
|
751
|
+
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
|
752
|
+
return 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
753
|
+
|
|
754
|
+
@property
|
|
755
|
+
def rsquared(self):
|
|
756
|
+
"""R-squared."""
|
|
757
|
+
if self._y is None or self._resid is None:
|
|
758
|
+
return None
|
|
759
|
+
y_mean = np.mean(self._y)
|
|
760
|
+
ss_tot = np.sum((self._y - y_mean) ** 2)
|
|
761
|
+
ss_res = np.sum(self._resid ** 2)
|
|
762
|
+
return 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
763
|
+
|
|
764
|
+
@property
|
|
765
|
+
def rsquared_adj(self):
|
|
766
|
+
"""Adjusted R-squared."""
|
|
767
|
+
if self._nobs is None or self._X_design is None:
|
|
768
|
+
return None
|
|
769
|
+
r2 = self.rsquared
|
|
770
|
+
if r2 is None:
|
|
771
|
+
return None
|
|
772
|
+
k = int(self._X_design.shape[1] - (1 if self.fit_intercept else 0))
|
|
773
|
+
return 1 - (1 - r2) * (self._nobs - 1) / self._df_resid
|
|
774
|
+
|
|
775
|
+
@property
|
|
776
|
+
def fvalue(self):
|
|
777
|
+
"""F-statistic."""
|
|
778
|
+
if self._y is None or self._resid is None or self._X_design is None:
|
|
779
|
+
return None
|
|
780
|
+
y_mean = np.mean(self._y)
|
|
781
|
+
ss_tot = np.sum((self._y - y_mean) ** 2)
|
|
782
|
+
ss_res = np.sum(self._resid ** 2)
|
|
783
|
+
ss_reg = ss_tot - ss_res
|
|
784
|
+
k = int(self._X_design.shape[1] - (1 if self.fit_intercept else 0))
|
|
785
|
+
if k == 0 or ss_res <= 0:
|
|
786
|
+
return np.inf
|
|
787
|
+
return (ss_reg / k) / (ss_res / self._df_resid)
|
|
788
|
+
|
|
789
|
+
@property
|
|
790
|
+
def f_pvalue(self):
|
|
791
|
+
"""p-value for F-statistic."""
|
|
792
|
+
fv = self.fvalue
|
|
793
|
+
if fv is None or fv == np.inf:
|
|
794
|
+
return 1.0
|
|
795
|
+
k = int(self._X_design.shape[1] - (1 if self.fit_intercept else 0))
|
|
796
|
+
return 1 - stats.f.cdf(fv, k, self._df_resid)
|
|
797
|
+
|
|
798
|
+
@property
|
|
799
|
+
def llf(self):
|
|
800
|
+
"""Log-likelihood (Gaussian MLE)."""
|
|
801
|
+
if self._nobs is None or self._resid is None:
|
|
802
|
+
return None
|
|
803
|
+
n = self._nobs
|
|
804
|
+
sigma2_mle = np.sum(self._resid ** 2) / n
|
|
805
|
+
return -n / 2 * np.log(2 * np.pi * sigma2_mle) - n / 2
|
|
806
|
+
|
|
807
|
+
@property
|
|
808
|
+
def aic(self):
|
|
809
|
+
"""Akaike Information Criterion."""
|
|
810
|
+
if self._nobs is None or self._scale is None or np.isnan(self._scale):
|
|
811
|
+
return None
|
|
812
|
+
return -2 * self.llf + 2 * len(self._params)
|
|
813
|
+
|
|
814
|
+
@property
|
|
815
|
+
def bic(self):
|
|
816
|
+
"""Bayesian Information Criterion."""
|
|
817
|
+
if self._nobs is None or self._scale is None or np.isnan(self._scale):
|
|
818
|
+
return None
|
|
819
|
+
n = self._nobs
|
|
820
|
+
k = len(self._params)
|
|
821
|
+
return -2 * self.llf + k * np.log(n)
|
|
822
|
+
|
|
823
|
+
def summary(self):
|
|
824
|
+
"""Print summary table similar to R's summary(lm())."""
|
|
825
|
+
if not self._fitted:
|
|
826
|
+
raise RuntimeError("Model has not been fitted yet.")
|
|
827
|
+
if not self.compute_inference:
|
|
828
|
+
raise RuntimeError(
|
|
829
|
+
"compute_inference=False: summary/inference statistics are not available. "
|
|
830
|
+
"Re-fit with compute_inference=True (default)."
|
|
831
|
+
)
|
|
832
|
+
if self._bse is None:
|
|
833
|
+
raise RuntimeError("Inference statistics are not available.")
|
|
834
|
+
|
|
835
|
+
if self.fit_intercept:
|
|
836
|
+
feature_names = ['(Intercept)'] + [f'x{i+1}' for i in range(len(self.coef_))]
|
|
837
|
+
else:
|
|
838
|
+
feature_names = [f'x{i+1}' for i in range(len(self.coef_))]
|
|
839
|
+
|
|
840
|
+
print("=" * 80)
|
|
841
|
+
print(" Ridge Regression Results")
|
|
842
|
+
print("=" * 80)
|
|
843
|
+
print(f"Alpha (L2 penalty): {self.alpha:>15.4f}")
|
|
844
|
+
print(f"Covariance Type: {self.cov_type:>15}")
|
|
845
|
+
print(f"No. Observations: {self._nobs:>15}")
|
|
846
|
+
print(f"Degrees of Freedom: {self._df_resid:>15}")
|
|
847
|
+
print(f"R-squared: {self.rsquared:>15.4f}")
|
|
848
|
+
print(f"Adj. R-squared: {self.rsquared_adj:>15.4f}")
|
|
849
|
+
print(f"F-statistic: {self.fvalue:>15.4f}")
|
|
850
|
+
print(f"Prob (F-statistic): {self.f_pvalue:>15.4e}")
|
|
851
|
+
print(f"Log-Likelihood: {self.llf:>15.4f}")
|
|
852
|
+
print(f"AIC: {self.aic:>15.4f}")
|
|
853
|
+
print(f"BIC: {self.bic:>15.4f}")
|
|
854
|
+
print("-" * 80)
|
|
855
|
+
print(f"{'':<15} {'coef':>12} {'std err':>12} {'t':>10} {'P>|t|':>10} {'[0.025':>12} {'0.975]':>12}")
|
|
856
|
+
print("-" * 80)
|
|
857
|
+
|
|
858
|
+
for i, name in enumerate(feature_names):
|
|
859
|
+
print(f"{name:<15} {self._params[i]:>12.4f} {self._bse[i]:>12.4f} "
|
|
860
|
+
f"{self._tvalues[i]:>10.3f} {self._pvalues[i]:>10.4f} "
|
|
861
|
+
f"{self._conf_int[i, 0]:>12.4f} {self._conf_int[i, 1]:>12.4f}")
|
|
862
|
+
|
|
863
|
+
print("=" * 80)
|