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,1127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Linear regression with full statistical inference and GPU support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = ["LinearRegression"]
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy import stats
|
|
10
|
+
from time import perf_counter
|
|
11
|
+
|
|
12
|
+
from statgpu._base import BaseEstimator
|
|
13
|
+
from statgpu._config import Device
|
|
14
|
+
from statgpu.backends import _get_torch_device_str
|
|
15
|
+
from statgpu.inference._results import GaussianInferenceResult
|
|
16
|
+
from statgpu.linear_model._gaussian_inference import (
|
|
17
|
+
compute_gaussian_inference,
|
|
18
|
+
validate_cov_type,
|
|
19
|
+
validate_hac_maxlags,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_formula_if_provided(formula, data, X, y):
|
|
24
|
+
"""Parse formula+data or fall back to raw arrays. Returns (y, X, info)."""
|
|
25
|
+
if formula is not None:
|
|
26
|
+
from statgpu.core.formula import parse_formula
|
|
27
|
+
return parse_formula(formula, data)
|
|
28
|
+
y = np.asarray(y)
|
|
29
|
+
if y.ndim == 2 and y.shape[1] == 1:
|
|
30
|
+
y = y.ravel()
|
|
31
|
+
return y, np.asarray(X), None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LinearRegression(BaseEstimator):
|
|
35
|
+
"""
|
|
36
|
+
Ordinary least squares linear regression with GPU acceleration
|
|
37
|
+
and full statistical inference (R/statsmodels style).
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
fit_intercept : bool, default=True
|
|
42
|
+
Whether to calculate the intercept.
|
|
43
|
+
device : str or Device, default='auto'
|
|
44
|
+
Computation device: 'cpu', 'cuda', or 'auto'.
|
|
45
|
+
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
coef_ : ndarray of shape (n_features,)
|
|
49
|
+
Estimated coefficients.
|
|
50
|
+
intercept_ : float
|
|
51
|
+
Independent term.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
fit_intercept: bool = True,
|
|
57
|
+
device: Union[str, Device] = Device.AUTO,
|
|
58
|
+
n_jobs: Optional[int] = None,
|
|
59
|
+
compute_inference: bool = True,
|
|
60
|
+
gpu_memory_cleanup: bool = False,
|
|
61
|
+
cov_type: str = "nonrobust",
|
|
62
|
+
hac_maxlags: Optional[int] = None,
|
|
63
|
+
):
|
|
64
|
+
super().__init__(device=device, n_jobs=n_jobs)
|
|
65
|
+
self.fit_intercept = fit_intercept
|
|
66
|
+
self.compute_inference = compute_inference
|
|
67
|
+
self.gpu_memory_cleanup = bool(gpu_memory_cleanup)
|
|
68
|
+
self.cov_type = validate_cov_type(cov_type)
|
|
69
|
+
self.hac_maxlags = validate_hac_maxlags(hac_maxlags)
|
|
70
|
+
self.coef_ = None
|
|
71
|
+
self.intercept_ = None
|
|
72
|
+
|
|
73
|
+
# Internal storage for inference
|
|
74
|
+
self._X_design = None
|
|
75
|
+
self._y = None
|
|
76
|
+
self._resid = None
|
|
77
|
+
self._scale = None
|
|
78
|
+
self._nobs = None
|
|
79
|
+
self._df_resid = None
|
|
80
|
+
self._params = None
|
|
81
|
+
self._bse = None
|
|
82
|
+
self._tvalues = None
|
|
83
|
+
self._pvalues = None
|
|
84
|
+
self._conf_int = None
|
|
85
|
+
self._inference_result = None
|
|
86
|
+
self._is_multi_output = False
|
|
87
|
+
self._hac_mixed_precision_preference = {}
|
|
88
|
+
self._feature_names = None
|
|
89
|
+
self._design_info = None
|
|
90
|
+
self._formula_has_intercept = None
|
|
91
|
+
|
|
92
|
+
def _clear_inference_result(self):
|
|
93
|
+
self._bse = None
|
|
94
|
+
self._tvalues = None
|
|
95
|
+
self._pvalues = None
|
|
96
|
+
self._conf_int = None
|
|
97
|
+
self._inference_result = None
|
|
98
|
+
|
|
99
|
+
def _cleanup_cuda_memory(self):
|
|
100
|
+
"""Best-effort CuPy memory pool cleanup."""
|
|
101
|
+
if not self.gpu_memory_cleanup:
|
|
102
|
+
return
|
|
103
|
+
try:
|
|
104
|
+
import cupy as cp
|
|
105
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
106
|
+
cp.get_default_pinned_memory_pool().free_all_blocks()
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def _resolve_hac_maxlags(self, n_obs: int) -> int:
|
|
111
|
+
"""Resolve HAC lag count with a Newey-West style default rule."""
|
|
112
|
+
if n_obs <= 1:
|
|
113
|
+
return 0
|
|
114
|
+
if self.hac_maxlags is None:
|
|
115
|
+
maxlags = int(np.floor(4.0 * (n_obs / 100.0) ** (2.0 / 9.0)))
|
|
116
|
+
else:
|
|
117
|
+
maxlags = int(self.hac_maxlags)
|
|
118
|
+
return max(0, min(maxlags, n_obs - 1))
|
|
119
|
+
|
|
120
|
+
def _benchmark_hac_numpy_kernel(
|
|
121
|
+
self,
|
|
122
|
+
scores: np.ndarray,
|
|
123
|
+
maxlags: int,
|
|
124
|
+
use_mixed_precision: bool,
|
|
125
|
+
) -> float:
|
|
126
|
+
"""Benchmark a tiny HAC kernel to choose the faster precision path."""
|
|
127
|
+
probe_maxlags = min(maxlags, 2)
|
|
128
|
+
if use_mixed_precision:
|
|
129
|
+
scores32 = scores.astype(np.float32, copy=False)
|
|
130
|
+
t0 = perf_counter()
|
|
131
|
+
meat = (scores32.T @ scores32).astype(np.float64)
|
|
132
|
+
for lag in range(1, probe_maxlags + 1):
|
|
133
|
+
weight = 1.0 - (lag / (maxlags + 1.0))
|
|
134
|
+
gamma = scores32[lag:].T @ scores32[:-lag]
|
|
135
|
+
meat = meat + float(weight) * (gamma + gamma.T).astype(np.float64)
|
|
136
|
+
_ = float(meat[0, 0])
|
|
137
|
+
return perf_counter() - t0
|
|
138
|
+
|
|
139
|
+
t0 = perf_counter()
|
|
140
|
+
meat = scores.T @ scores
|
|
141
|
+
for lag in range(1, probe_maxlags + 1):
|
|
142
|
+
weight = 1.0 - (lag / (maxlags + 1.0))
|
|
143
|
+
gamma = scores[lag:].T @ scores[:-lag]
|
|
144
|
+
meat = meat + weight * (gamma + gamma.T)
|
|
145
|
+
_ = float(meat[0, 0])
|
|
146
|
+
return perf_counter() - t0
|
|
147
|
+
|
|
148
|
+
def _should_use_mixed_precision_hac_numpy(self, scores: np.ndarray, maxlags: int) -> bool:
|
|
149
|
+
"""Choose HAC precision path adaptively and cache by problem shape."""
|
|
150
|
+
n_obs = int(scores.shape[0])
|
|
151
|
+
n_features = int(scores.shape[1])
|
|
152
|
+
if not (scores.dtype == np.float64 and n_obs >= 4096 and n_features <= 64):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
if n_obs < 32768:
|
|
156
|
+
n_bucket = "small"
|
|
157
|
+
elif n_obs < 65536:
|
|
158
|
+
n_bucket = "medium"
|
|
159
|
+
else:
|
|
160
|
+
n_bucket = "large"
|
|
161
|
+
|
|
162
|
+
key = (n_features, int(min(maxlags, 8)), n_bucket)
|
|
163
|
+
cached = self._hac_mixed_precision_preference.get(key)
|
|
164
|
+
if cached is not None:
|
|
165
|
+
return bool(cached)
|
|
166
|
+
|
|
167
|
+
probe_cap = 12288 if n_bucket != "large" else 24576
|
|
168
|
+
probe_n = min(n_obs, probe_cap)
|
|
169
|
+
if probe_n <= maxlags + 16:
|
|
170
|
+
self._hac_mixed_precision_preference[key] = True
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
probe_scores = np.asarray(scores[:probe_n], dtype=np.float64, order="C")
|
|
174
|
+
try:
|
|
175
|
+
# Warmup to reduce one-time BLAS startup noise.
|
|
176
|
+
self._benchmark_hac_numpy_kernel(probe_scores, maxlags, use_mixed_precision=True)
|
|
177
|
+
self._benchmark_hac_numpy_kernel(probe_scores, maxlags, use_mixed_precision=False)
|
|
178
|
+
mixed_time = self._benchmark_hac_numpy_kernel(
|
|
179
|
+
probe_scores, maxlags, use_mixed_precision=True
|
|
180
|
+
)
|
|
181
|
+
float64_time = self._benchmark_hac_numpy_kernel(
|
|
182
|
+
probe_scores, maxlags, use_mixed_precision=False
|
|
183
|
+
)
|
|
184
|
+
# Keep mixed path only if it clears a small speed margin.
|
|
185
|
+
use_mixed = mixed_time <= 0.95 * float64_time
|
|
186
|
+
except Exception:
|
|
187
|
+
use_mixed = True
|
|
188
|
+
|
|
189
|
+
self._hac_mixed_precision_preference[key] = use_mixed
|
|
190
|
+
return use_mixed
|
|
191
|
+
|
|
192
|
+
def _hac_meat_numpy(self, scores: np.ndarray) -> np.ndarray:
|
|
193
|
+
"""Bartlett-kernel HAC meat from per-observation score matrix."""
|
|
194
|
+
n_obs = int(scores.shape[0])
|
|
195
|
+
maxlags = self._resolve_hac_maxlags(n_obs)
|
|
196
|
+
weights = 1.0 - (np.arange(1, maxlags + 1, dtype=float) / (maxlags + 1.0))
|
|
197
|
+
|
|
198
|
+
# Adaptive mixed precision: select per-shape path by quick local probe,
|
|
199
|
+
# then cache the decision to avoid recurring benchmark overhead.
|
|
200
|
+
use_mixed_precision = self._should_use_mixed_precision_hac_numpy(scores, maxlags)
|
|
201
|
+
|
|
202
|
+
if use_mixed_precision:
|
|
203
|
+
scores32 = scores.astype(np.float32, copy=False)
|
|
204
|
+
meat = (scores32.T @ scores32).astype(np.float64)
|
|
205
|
+
if maxlags == 0:
|
|
206
|
+
return meat
|
|
207
|
+
for lag, weight in enumerate(weights, start=1):
|
|
208
|
+
gamma = scores32[lag:].T @ scores32[:-lag]
|
|
209
|
+
meat = meat + float(weight) * (gamma + gamma.T).astype(np.float64)
|
|
210
|
+
return meat
|
|
211
|
+
|
|
212
|
+
meat = scores.T @ scores
|
|
213
|
+
if maxlags == 0:
|
|
214
|
+
return meat
|
|
215
|
+
for lag, weight in enumerate(weights, start=1):
|
|
216
|
+
gamma = scores[lag:].T @ scores[:-lag]
|
|
217
|
+
meat = meat + weight * (gamma + gamma.T)
|
|
218
|
+
return meat
|
|
219
|
+
|
|
220
|
+
def _hac_meat_cupy(self, scores):
|
|
221
|
+
"""CuPy Bartlett-kernel HAC meat from per-observation score matrix."""
|
|
222
|
+
import cupy as cp
|
|
223
|
+
|
|
224
|
+
n_obs = int(scores.shape[0])
|
|
225
|
+
meat = scores.T @ scores
|
|
226
|
+
maxlags = self._resolve_hac_maxlags(n_obs)
|
|
227
|
+
if maxlags == 0:
|
|
228
|
+
return meat
|
|
229
|
+
for lag in range(1, maxlags + 1):
|
|
230
|
+
weight = 1.0 - (lag / (maxlags + 1.0))
|
|
231
|
+
gamma = scores[lag:].T @ scores[:-lag]
|
|
232
|
+
meat = meat + weight * (gamma + gamma.T)
|
|
233
|
+
return meat
|
|
234
|
+
|
|
235
|
+
def _robust_covariance_numpy(self, X: np.ndarray, resid: np.ndarray, XtX_inv: np.ndarray) -> np.ndarray:
|
|
236
|
+
"""Compute robust/HAC covariance matrix for OLS-like score equations."""
|
|
237
|
+
n, k = X.shape
|
|
238
|
+
e = np.asarray(resid, dtype=float).reshape(-1)
|
|
239
|
+
|
|
240
|
+
if self.cov_type == "hac":
|
|
241
|
+
scores = X * e[:, np.newaxis]
|
|
242
|
+
meat = self._hac_meat_numpy(scores)
|
|
243
|
+
return XtX_inv @ meat @ XtX_inv
|
|
244
|
+
|
|
245
|
+
if self.cov_type in ("hc2", "hc3"):
|
|
246
|
+
leverage = np.einsum("ij,jk,ik->i", X, XtX_inv, X)
|
|
247
|
+
leverage = np.clip(leverage, 0.0, 1.0 - 1e-12)
|
|
248
|
+
if self.cov_type == "hc2":
|
|
249
|
+
e2 = (e ** 2) / (1.0 - leverage)
|
|
250
|
+
else:
|
|
251
|
+
e2 = (e ** 2) / ((1.0 - leverage) ** 2)
|
|
252
|
+
else:
|
|
253
|
+
e2 = e ** 2
|
|
254
|
+
|
|
255
|
+
Xw = X * e2[:, np.newaxis]
|
|
256
|
+
meat = X.T @ Xw
|
|
257
|
+
cov_params = XtX_inv @ meat @ XtX_inv
|
|
258
|
+
if self.cov_type == "hc1" and n > k:
|
|
259
|
+
cov_params *= (n / (n - k))
|
|
260
|
+
return cov_params
|
|
261
|
+
|
|
262
|
+
def _robust_covariance_cupy(self, X, resid, XtX_inv):
|
|
263
|
+
"""Compute robust/HAC covariance matrix for OLS-like score equations on GPU."""
|
|
264
|
+
import cupy as cp
|
|
265
|
+
|
|
266
|
+
n, k = X.shape
|
|
267
|
+
e = resid.reshape(-1)
|
|
268
|
+
|
|
269
|
+
if self.cov_type == "hac":
|
|
270
|
+
scores = X * e[:, cp.newaxis]
|
|
271
|
+
meat = self._hac_meat_cupy(scores)
|
|
272
|
+
return XtX_inv @ meat @ XtX_inv
|
|
273
|
+
|
|
274
|
+
if self.cov_type in ("hc2", "hc3"):
|
|
275
|
+
leverage = cp.einsum("ij,jk,ik->i", X, XtX_inv, X)
|
|
276
|
+
leverage = cp.clip(leverage, 0.0, 1.0 - 1e-12)
|
|
277
|
+
if self.cov_type == "hc2":
|
|
278
|
+
e2 = cp.square(e) / (1.0 - leverage)
|
|
279
|
+
else:
|
|
280
|
+
e2 = cp.square(e) / cp.square(1.0 - leverage)
|
|
281
|
+
else:
|
|
282
|
+
e2 = cp.square(e)
|
|
283
|
+
|
|
284
|
+
Xw = X * e2[:, cp.newaxis]
|
|
285
|
+
meat = X.T @ Xw
|
|
286
|
+
cov_params = XtX_inv @ meat @ XtX_inv
|
|
287
|
+
if self.cov_type == "hc1" and n > k:
|
|
288
|
+
cov_params = cov_params * (n / (n - k))
|
|
289
|
+
return cov_params
|
|
290
|
+
|
|
291
|
+
def fit(self, X=None, y=None, sample_weight=None, formula=None, data=None):
|
|
292
|
+
"""Fit linear model.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
X : array-like or None
|
|
297
|
+
Predictor matrix. Required if ``formula`` is None.
|
|
298
|
+
y : array-like or None
|
|
299
|
+
Response vector. Required if ``formula`` is None.
|
|
300
|
+
sample_weight : array-like or None
|
|
301
|
+
Sample weights.
|
|
302
|
+
formula : str or None
|
|
303
|
+
R-style formula string (e.g. ``"y ~ x1 + x2"``). Mutually
|
|
304
|
+
exclusive with ``X``/``y``.
|
|
305
|
+
data : pd.DataFrame or None
|
|
306
|
+
DataFrame used with ``formula`` for column lookup.
|
|
307
|
+
"""
|
|
308
|
+
self._clear_inference_result()
|
|
309
|
+
|
|
310
|
+
# Handle formula interface
|
|
311
|
+
_orig_fit_intercept = self.fit_intercept
|
|
312
|
+
if formula is not None:
|
|
313
|
+
if data is None:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
"formula was provided but data is None. "
|
|
316
|
+
"Pass data=your_dataframe when using formula."
|
|
317
|
+
)
|
|
318
|
+
y_arr, X_arr, design_info = _parse_formula_if_provided(
|
|
319
|
+
formula, data, None, None
|
|
320
|
+
)
|
|
321
|
+
self._design_info = design_info
|
|
322
|
+
formula_column_names = list(design_info.column_names)
|
|
323
|
+
self._formula_has_intercept = "Intercept" in formula_column_names
|
|
324
|
+
self._feature_names = [name for name in formula_column_names if name != "Intercept"]
|
|
325
|
+
if self._formula_has_intercept:
|
|
326
|
+
intercept_idx = formula_column_names.index("Intercept")
|
|
327
|
+
# Drop the intercept column — let the fitting methods handle it
|
|
328
|
+
X_arr = np.delete(X_arr, intercept_idx, axis=1)
|
|
329
|
+
self.fit_intercept = True
|
|
330
|
+
else:
|
|
331
|
+
# Formula syntax owns intercept semantics, matching statsmodels/R.
|
|
332
|
+
self.fit_intercept = False
|
|
333
|
+
else:
|
|
334
|
+
if X is None or y is None:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
"Either formula+data or X+y must be provided."
|
|
337
|
+
)
|
|
338
|
+
self._feature_names = None
|
|
339
|
+
self._design_info = None
|
|
340
|
+
self._formula_has_intercept = None
|
|
341
|
+
y_arr = np.asarray(y)
|
|
342
|
+
if y_arr.ndim == 2 and y_arr.shape[1] == 1:
|
|
343
|
+
y_arr = y_arr.ravel()
|
|
344
|
+
X_arr = np.asarray(X)
|
|
345
|
+
|
|
346
|
+
self.fit_intercept = _orig_fit_intercept
|
|
347
|
+
# Store y (may be CuPy/Torch array, convert later for CPU)
|
|
348
|
+
self._y = y_arr
|
|
349
|
+
|
|
350
|
+
# Get backend - support explicit torch backend selection
|
|
351
|
+
backend = self._get_backend(backend="auto")
|
|
352
|
+
backend_name = backend.name
|
|
353
|
+
|
|
354
|
+
X_arr = self._to_array(X_arr, backend=backend_name)
|
|
355
|
+
y_arr = self._to_array(y_arr, backend=backend_name)
|
|
356
|
+
self._is_multi_output = y_arr.ndim > 1 and y_arr.shape[1] > 1
|
|
357
|
+
|
|
358
|
+
device = self._get_compute_device()
|
|
359
|
+
|
|
360
|
+
# Route to appropriate backend
|
|
361
|
+
if backend_name == "torch":
|
|
362
|
+
self._fit_torch(X_arr, y_arr, sample_weight)
|
|
363
|
+
elif backend_name == "cupy":
|
|
364
|
+
self._fit_gpu(X_arr, y_arr, sample_weight)
|
|
365
|
+
else:
|
|
366
|
+
self._fit_cpu(X_arr, y_arr, sample_weight)
|
|
367
|
+
|
|
368
|
+
# Convert y to numpy for diagnostics if needed
|
|
369
|
+
if hasattr(self._y, 'get'): # CuPy
|
|
370
|
+
self._y = self._y.get()
|
|
371
|
+
elif hasattr(self._y, 'cpu'): # Torch
|
|
372
|
+
self._y = self._y.cpu().numpy()
|
|
373
|
+
else:
|
|
374
|
+
self._y = np.asarray(self._y)
|
|
375
|
+
|
|
376
|
+
# GPU single-output inference is computed in _fit_gpu/_fit_torch().
|
|
377
|
+
# Multi-output GPU inference is not implemented yet; do not fall back to
|
|
378
|
+
# the NumPy inference path when the user selected a GPU backend.
|
|
379
|
+
if self.compute_inference and self._is_multi_output and device in (Device.CUDA, Device.TORCH):
|
|
380
|
+
raise NotImplementedError(
|
|
381
|
+
"Multi-output LinearRegression inference is not implemented for "
|
|
382
|
+
f"device='{device.value}'. Set compute_inference=False or use device='cpu'."
|
|
383
|
+
)
|
|
384
|
+
if self.compute_inference and device == Device.CPU:
|
|
385
|
+
self._compute_inference()
|
|
386
|
+
self._fitted = True
|
|
387
|
+
return self
|
|
388
|
+
|
|
389
|
+
def _fit_cpu(self, X, y, sample_weight=None):
|
|
390
|
+
"""Fit using CPU."""
|
|
391
|
+
X = np.asarray(X)
|
|
392
|
+
y = np.asarray(y)
|
|
393
|
+
|
|
394
|
+
n_samples, n_features = X.shape
|
|
395
|
+
self._nobs = n_samples
|
|
396
|
+
|
|
397
|
+
if sample_weight is not None:
|
|
398
|
+
sample_weight = np.asarray(sample_weight)
|
|
399
|
+
sqrt_sw = np.sqrt(sample_weight)
|
|
400
|
+
X = X * sqrt_sw[:, np.newaxis]
|
|
401
|
+
y = y * sqrt_sw
|
|
402
|
+
|
|
403
|
+
if self.fit_intercept:
|
|
404
|
+
self._X_design = np.column_stack([np.ones(n_samples, dtype=X.dtype), X])
|
|
405
|
+
else:
|
|
406
|
+
self._X_design = X.copy()
|
|
407
|
+
|
|
408
|
+
if y.ndim == 1:
|
|
409
|
+
y = y.reshape(-1, 1)
|
|
410
|
+
|
|
411
|
+
coef, _, _, _ = np.linalg.lstsq(self._X_design, y, rcond=None)
|
|
412
|
+
|
|
413
|
+
if self.fit_intercept:
|
|
414
|
+
if coef.shape[1] > 1:
|
|
415
|
+
self.intercept_ = coef[0, :].copy()
|
|
416
|
+
self.coef_ = coef[1:, :].T
|
|
417
|
+
self._params = coef.copy()
|
|
418
|
+
else:
|
|
419
|
+
coef_1d = coef[:, 0]
|
|
420
|
+
self.intercept_ = float(coef_1d[0])
|
|
421
|
+
self.coef_ = coef_1d[1:]
|
|
422
|
+
self._params = coef_1d.copy()
|
|
423
|
+
else:
|
|
424
|
+
if coef.shape[1] > 1:
|
|
425
|
+
self.intercept_ = np.zeros(coef.shape[1], dtype=coef.dtype)
|
|
426
|
+
self.coef_ = coef.T
|
|
427
|
+
self._params = coef.copy()
|
|
428
|
+
else:
|
|
429
|
+
self.intercept_ = 0.0
|
|
430
|
+
self.coef_ = coef[:, 0].copy()
|
|
431
|
+
self._params = self.coef_.copy()
|
|
432
|
+
|
|
433
|
+
y_pred = self._X_design @ coef
|
|
434
|
+
self._resid = y - y_pred
|
|
435
|
+
if self._resid.shape[1] == 1:
|
|
436
|
+
self._resid = self._resid[:, 0]
|
|
437
|
+
self._df_resid = n_samples - (n_features + (1 if self.fit_intercept else 0))
|
|
438
|
+
|
|
439
|
+
if self._df_resid > 0:
|
|
440
|
+
if np.asarray(self._resid).ndim == 1:
|
|
441
|
+
self._scale = np.sum(self._resid ** 2) / self._df_resid
|
|
442
|
+
else:
|
|
443
|
+
self._scale = np.sum(self._resid ** 2, axis=0) / self._df_resid
|
|
444
|
+
else:
|
|
445
|
+
self._scale = np.nan
|
|
446
|
+
|
|
447
|
+
def _fit_gpu(self, X, y, sample_weight=None):
|
|
448
|
+
"""Fit using GPU with FULL GPU computation (including inference)."""
|
|
449
|
+
import cupy as cp
|
|
450
|
+
from statgpu.backends._gpu_inference_cupy import (
|
|
451
|
+
compute_inference_gpu,
|
|
452
|
+
compute_r2_gpu,
|
|
453
|
+
compute_aic_bic_gpu,
|
|
454
|
+
compute_f_stat_gpu,
|
|
455
|
+
)
|
|
456
|
+
from statgpu.inference._distributions_backend import norm
|
|
457
|
+
|
|
458
|
+
n_samples, n_features = X.shape
|
|
459
|
+
self._nobs = n_samples
|
|
460
|
+
|
|
461
|
+
# Ensure CuPy arrays
|
|
462
|
+
X = cp.asarray(X)
|
|
463
|
+
y = cp.asarray(y)
|
|
464
|
+
|
|
465
|
+
if sample_weight is not None:
|
|
466
|
+
sample_weight = cp.asarray(sample_weight)
|
|
467
|
+
sqrt_sw = cp.sqrt(sample_weight)
|
|
468
|
+
X = X * sqrt_sw[:, cp.newaxis]
|
|
469
|
+
y = y * sqrt_sw
|
|
470
|
+
|
|
471
|
+
if self.fit_intercept:
|
|
472
|
+
X_design = cp.column_stack([cp.ones(n_samples, dtype=X.dtype), X])
|
|
473
|
+
else:
|
|
474
|
+
X_design = X
|
|
475
|
+
|
|
476
|
+
if y.ndim == 1:
|
|
477
|
+
y = y.reshape(-1, 1)
|
|
478
|
+
|
|
479
|
+
# Use normal equations: (X'X)^-1 X'y
|
|
480
|
+
XtX = X_design.T @ X_design
|
|
481
|
+
Xty = X_design.T @ y
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
# Cholesky decomposition
|
|
485
|
+
L = cp.linalg.cholesky(XtX)
|
|
486
|
+
tmp = cp.linalg.solve_triangular(L, Xty, lower=True)
|
|
487
|
+
coef = cp.linalg.solve_triangular(L.T, tmp, lower=False)
|
|
488
|
+
except Exception:
|
|
489
|
+
coef = cp.linalg.solve(XtX, Xty)
|
|
490
|
+
|
|
491
|
+
# Compute predictions and residuals on GPU
|
|
492
|
+
y_pred = X_design @ coef
|
|
493
|
+
resid = y - y_pred
|
|
494
|
+
|
|
495
|
+
# Compute scale on GPU
|
|
496
|
+
df_resid = n_samples - (n_features + (1 if self.fit_intercept else 0))
|
|
497
|
+
if df_resid > 0:
|
|
498
|
+
if y.shape[1] > 1:
|
|
499
|
+
scale = cp.sum(resid ** 2, axis=0) / df_resid
|
|
500
|
+
else:
|
|
501
|
+
scale = cp.sum(resid ** 2) / df_resid
|
|
502
|
+
else:
|
|
503
|
+
if y.shape[1] > 1:
|
|
504
|
+
scale = cp.full((y.shape[1],), cp.nan, dtype=y.dtype)
|
|
505
|
+
else:
|
|
506
|
+
scale = cp.nan
|
|
507
|
+
|
|
508
|
+
# Compute inference-related statistics only when requested.
|
|
509
|
+
if self.compute_inference and not self._is_multi_output:
|
|
510
|
+
coef_flat = coef.flatten()
|
|
511
|
+
if self.cov_type == "nonrobust":
|
|
512
|
+
self._bse_gpu, self._tvalues_gpu, self._pvalues_gpu, self._conf_int_gpu = \
|
|
513
|
+
compute_inference_gpu(X_design, resid, scale, df_resid, coef_flat)
|
|
514
|
+
else:
|
|
515
|
+
XtX_cov = X_design.T @ X_design
|
|
516
|
+
try:
|
|
517
|
+
XtX_inv = cp.linalg.inv(XtX_cov)
|
|
518
|
+
except Exception:
|
|
519
|
+
XtX_inv = cp.linalg.pinv(XtX_cov)
|
|
520
|
+
cov_params = self._robust_covariance_cupy(X_design, resid, XtX_inv)
|
|
521
|
+
self._bse_gpu = cp.sqrt(cp.maximum(cp.diag(cov_params), 0.0))
|
|
522
|
+
self._tvalues_gpu = coef_flat / (self._bse_gpu + 1e-30)
|
|
523
|
+
self._pvalues_gpu = cp.minimum(1.0, 2.0 * norm.sf(cp.abs(self._tvalues_gpu)))
|
|
524
|
+
z_crit = norm.ppf(0.975)
|
|
525
|
+
self._conf_int_gpu = cp.stack([
|
|
526
|
+
coef_flat - z_crit * self._bse_gpu,
|
|
527
|
+
coef_flat + z_crit * self._bse_gpu,
|
|
528
|
+
], axis=1)
|
|
529
|
+
|
|
530
|
+
# R-squared on GPU
|
|
531
|
+
self._rsquared_gpu = compute_r2_gpu(y, resid)
|
|
532
|
+
|
|
533
|
+
# AIC/BIC on GPU
|
|
534
|
+
k = n_features + (1 if self.fit_intercept else 0)
|
|
535
|
+
scale_mle = cp.sum(resid ** 2) / n_samples
|
|
536
|
+
self._aic_gpu, self._bic_gpu = compute_aic_bic_gpu(n_samples, k, scale_mle)
|
|
537
|
+
|
|
538
|
+
# F-statistic on GPU
|
|
539
|
+
self._fvalue_gpu, self._f_pvalue = compute_f_stat_gpu(y, resid, X_design, df_resid)
|
|
540
|
+
|
|
541
|
+
# Single transfer to CPU at the end
|
|
542
|
+
coef_np = coef.get()
|
|
543
|
+
resid_np = resid.get()
|
|
544
|
+
if y.shape[1] > 1:
|
|
545
|
+
scale_np = scale.get()
|
|
546
|
+
else:
|
|
547
|
+
scale_np = float(scale.get()) if not cp.isnan(scale) else np.nan
|
|
548
|
+
X_design_np = X_design.get()
|
|
549
|
+
|
|
550
|
+
if self.compute_inference and not self._is_multi_output:
|
|
551
|
+
# Transfer inference results
|
|
552
|
+
self._bse = self._bse_gpu.get()
|
|
553
|
+
self._tvalues = self._tvalues_gpu.get()
|
|
554
|
+
self._pvalues = self._pvalues_gpu.get()
|
|
555
|
+
self._conf_int = self._conf_int_gpu.get()
|
|
556
|
+
|
|
557
|
+
# Store results
|
|
558
|
+
if self.fit_intercept:
|
|
559
|
+
if coef_np.shape[1] > 1:
|
|
560
|
+
self.intercept_ = coef_np[0, :].copy()
|
|
561
|
+
self.coef_ = coef_np[1:, :].T
|
|
562
|
+
self._params = coef_np.copy()
|
|
563
|
+
else:
|
|
564
|
+
self.intercept_ = float(coef_np[0, 0])
|
|
565
|
+
self.coef_ = coef_np[1:, 0]
|
|
566
|
+
self._params = coef_np[:, 0]
|
|
567
|
+
else:
|
|
568
|
+
if coef_np.shape[1] > 1:
|
|
569
|
+
self.intercept_ = np.zeros(coef_np.shape[1], dtype=coef_np.dtype)
|
|
570
|
+
self.coef_ = coef_np.T
|
|
571
|
+
self._params = coef_np.copy()
|
|
572
|
+
else:
|
|
573
|
+
self.intercept_ = 0.0
|
|
574
|
+
self.coef_ = coef_np[:, 0]
|
|
575
|
+
self._params = coef_np[:, 0]
|
|
576
|
+
|
|
577
|
+
self._X_design = X_design_np
|
|
578
|
+
if resid_np.shape[1] == 1:
|
|
579
|
+
self._resid = resid_np[:, 0]
|
|
580
|
+
else:
|
|
581
|
+
self._resid = resid_np
|
|
582
|
+
self._df_resid = df_resid
|
|
583
|
+
self._scale = scale_np
|
|
584
|
+
if self.compute_inference and not self._is_multi_output:
|
|
585
|
+
self._wrap_gaussian_inference_result()
|
|
586
|
+
|
|
587
|
+
# Release large temporary GPU tensors early.
|
|
588
|
+
try:
|
|
589
|
+
del X_design
|
|
590
|
+
except Exception:
|
|
591
|
+
pass
|
|
592
|
+
try:
|
|
593
|
+
del resid
|
|
594
|
+
except Exception:
|
|
595
|
+
pass
|
|
596
|
+
try:
|
|
597
|
+
del XtX
|
|
598
|
+
except Exception:
|
|
599
|
+
pass
|
|
600
|
+
try:
|
|
601
|
+
del Xty
|
|
602
|
+
except Exception:
|
|
603
|
+
pass
|
|
604
|
+
try:
|
|
605
|
+
del coef
|
|
606
|
+
except Exception:
|
|
607
|
+
pass
|
|
608
|
+
self._cleanup_cuda_memory()
|
|
609
|
+
|
|
610
|
+
def _cleanup_torch_memory(self):
|
|
611
|
+
"""Best-effort Torch memory cleanup."""
|
|
612
|
+
if not self.gpu_memory_cleanup:
|
|
613
|
+
return
|
|
614
|
+
try:
|
|
615
|
+
import torch
|
|
616
|
+
if torch.cuda.is_available():
|
|
617
|
+
torch.cuda.empty_cache()
|
|
618
|
+
except Exception:
|
|
619
|
+
pass
|
|
620
|
+
|
|
621
|
+
def _hac_meat_torch(self, scores):
|
|
622
|
+
"""Torch Bartlett-kernel HAC meat from per-observation score matrix."""
|
|
623
|
+
import torch
|
|
624
|
+
|
|
625
|
+
n_obs = int(scores.shape[0])
|
|
626
|
+
meat = scores.T @ scores
|
|
627
|
+
maxlags = self._resolve_hac_maxlags(n_obs)
|
|
628
|
+
if maxlags == 0:
|
|
629
|
+
return meat
|
|
630
|
+
for lag in range(1, maxlags + 1):
|
|
631
|
+
weight = 1.0 - (lag / (maxlags + 1.0))
|
|
632
|
+
gamma = scores[lag:].T @ scores[:-lag]
|
|
633
|
+
meat = meat + weight * (gamma + gamma.T)
|
|
634
|
+
return meat
|
|
635
|
+
|
|
636
|
+
def _robust_covariance_torch(self, X, resid, XtX_inv, device=None):
|
|
637
|
+
"""Compute robust/HAC covariance matrix for OLS-like score equations on Torch GPU."""
|
|
638
|
+
import torch
|
|
639
|
+
|
|
640
|
+
n, k = X.shape
|
|
641
|
+
e = resid.reshape(-1)
|
|
642
|
+
|
|
643
|
+
if device is None:
|
|
644
|
+
device = 'cuda' if X.is_cuda else 'cpu'
|
|
645
|
+
|
|
646
|
+
if self.cov_type == "hac":
|
|
647
|
+
# HAC requires temporal ordering - compute score matrix and apply Bartlett kernel
|
|
648
|
+
scores = X * e[:, None]
|
|
649
|
+
meat = self._hac_meat_torch(scores)
|
|
650
|
+
return XtX_inv @ meat @ XtX_inv
|
|
651
|
+
|
|
652
|
+
if self.cov_type in ("hc2", "hc3"):
|
|
653
|
+
leverage = torch.einsum("ij,jk,ik->i", X, XtX_inv, X)
|
|
654
|
+
leverage = torch.clamp(leverage, 0.0, 1.0 - 1e-12)
|
|
655
|
+
if self.cov_type == "hc2":
|
|
656
|
+
e2 = torch.square(e) / (1.0 - leverage)
|
|
657
|
+
else:
|
|
658
|
+
e2 = torch.square(e) / torch.square(1.0 - leverage)
|
|
659
|
+
else:
|
|
660
|
+
e2 = torch.square(e)
|
|
661
|
+
|
|
662
|
+
Xw = X * e2[:, None]
|
|
663
|
+
meat = X.T @ Xw
|
|
664
|
+
cov_params = XtX_inv @ meat @ XtX_inv
|
|
665
|
+
if self.cov_type == "hc1" and n > k:
|
|
666
|
+
cov_params = cov_params * (n / (n - k))
|
|
667
|
+
return cov_params
|
|
668
|
+
|
|
669
|
+
def _fit_torch(self, X, y, sample_weight=None):
|
|
670
|
+
"""Fit using Torch GPU with FULL GPU computation (including inference)."""
|
|
671
|
+
import torch
|
|
672
|
+
from statgpu.backends._gpu_inference_torch import (
|
|
673
|
+
compute_inference_torch,
|
|
674
|
+
compute_r2_torch,
|
|
675
|
+
compute_aic_bic_torch,
|
|
676
|
+
compute_f_stat_torch,
|
|
677
|
+
)
|
|
678
|
+
from statgpu.inference._distributions_backend import norm
|
|
679
|
+
|
|
680
|
+
n_samples, n_features = X.shape
|
|
681
|
+
self._nobs = n_samples
|
|
682
|
+
|
|
683
|
+
# Ensure Torch tensors on correct device
|
|
684
|
+
# Note: Device.TORCH.value is 'torch', but Torch expects 'cuda' or 'cpu'
|
|
685
|
+
torch_device = _get_torch_device_str()
|
|
686
|
+
if not isinstance(X, torch.Tensor):
|
|
687
|
+
X = torch.from_numpy(np.asarray(X)).to(torch_device)
|
|
688
|
+
if not isinstance(y, torch.Tensor):
|
|
689
|
+
y = torch.from_numpy(np.asarray(y)).to(torch_device)
|
|
690
|
+
|
|
691
|
+
if X.dtype != torch.float64:
|
|
692
|
+
X = X.to(torch.float64)
|
|
693
|
+
if y.dtype != torch.float64:
|
|
694
|
+
y = y.to(torch.float64)
|
|
695
|
+
|
|
696
|
+
if sample_weight is not None:
|
|
697
|
+
if not isinstance(sample_weight, torch.Tensor):
|
|
698
|
+
sample_weight = torch.from_numpy(np.asarray(sample_weight)).to(torch_device)
|
|
699
|
+
if sample_weight.dtype != torch.float64:
|
|
700
|
+
sample_weight = sample_weight.to(torch.float64)
|
|
701
|
+
sqrt_sw = torch.sqrt(sample_weight)
|
|
702
|
+
X = X * sqrt_sw[:, None]
|
|
703
|
+
y = y * sqrt_sw
|
|
704
|
+
|
|
705
|
+
if self.fit_intercept:
|
|
706
|
+
X_design = torch.cat([torch.ones(n_samples, 1, dtype=X.dtype, device=torch_device), X], dim=1)
|
|
707
|
+
else:
|
|
708
|
+
X_design = X.clone()
|
|
709
|
+
|
|
710
|
+
if y.ndim == 1:
|
|
711
|
+
y = y.reshape(-1, 1)
|
|
712
|
+
|
|
713
|
+
# Use normal equations: (X'X)^-1 X'y
|
|
714
|
+
XtX = X_design.T @ X_design
|
|
715
|
+
Xty = X_design.T @ y
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
# Cholesky decomposition
|
|
719
|
+
L = torch.linalg.cholesky(XtX)
|
|
720
|
+
# Solve L @ tmp = Xty (L is lower triangular)
|
|
721
|
+
tmp = torch.linalg.solve_triangular(L, Xty, upper=False)
|
|
722
|
+
# Solve L.T @ coef = tmp (L.T is upper triangular)
|
|
723
|
+
coef = torch.linalg.solve_triangular(L.T, tmp, upper=True)
|
|
724
|
+
except Exception:
|
|
725
|
+
coef = torch.linalg.solve(XtX, Xty)
|
|
726
|
+
|
|
727
|
+
# Compute predictions and residuals on Torch
|
|
728
|
+
y_pred = X_design @ coef
|
|
729
|
+
resid = y - y_pred
|
|
730
|
+
|
|
731
|
+
# Compute scale on Torch
|
|
732
|
+
df_resid = n_samples - (n_features + (1 if self.fit_intercept else 0))
|
|
733
|
+
if df_resid > 0:
|
|
734
|
+
if y.shape[1] > 1:
|
|
735
|
+
scale = torch.sum(resid ** 2, dim=0) / df_resid
|
|
736
|
+
else:
|
|
737
|
+
scale = torch.sum(resid ** 2) / df_resid
|
|
738
|
+
else:
|
|
739
|
+
if y.shape[1] > 1:
|
|
740
|
+
scale = torch.full((y.shape[1],), float('nan'), dtype=y.dtype, device=torch_device)
|
|
741
|
+
else:
|
|
742
|
+
scale = torch.tensor(float('nan'), dtype=y.dtype, device=torch_device)
|
|
743
|
+
|
|
744
|
+
# Compute inference-related statistics only when requested.
|
|
745
|
+
if self.compute_inference and not self._is_multi_output:
|
|
746
|
+
coef_flat = coef.flatten()
|
|
747
|
+
if self.cov_type == "nonrobust":
|
|
748
|
+
self._bse_gpu, self._tvalues_gpu, self._pvalues_gpu, self._conf_int_gpu = \
|
|
749
|
+
compute_inference_torch(X_design, resid, scale, df_resid, coef_flat, cov_type="nonrobust", device=torch_device)
|
|
750
|
+
else:
|
|
751
|
+
XtX_cov = X_design.T @ X_design
|
|
752
|
+
try:
|
|
753
|
+
XtX_inv = torch.linalg.inv(XtX_cov)
|
|
754
|
+
except Exception:
|
|
755
|
+
XtX_inv = torch.linalg.pinv(XtX_cov)
|
|
756
|
+
cov_params = self._robust_covariance_torch(X_design, resid, XtX_inv, device=torch_device)
|
|
757
|
+
self._bse_gpu = torch.sqrt(torch.clamp(torch.diag(cov_params), 0.0))
|
|
758
|
+
self._tvalues_gpu = coef_flat / (self._bse_gpu + 1e-30)
|
|
759
|
+
self._pvalues_gpu = torch.clamp(2.0 * norm.sf(torch.abs(self._tvalues_gpu), device=torch_device), 0.0, 1.0)
|
|
760
|
+
z_crit = norm.ppf(0.975, device=torch_device)
|
|
761
|
+
self._conf_int_gpu = torch.stack([
|
|
762
|
+
coef_flat - z_crit * self._bse_gpu,
|
|
763
|
+
coef_flat + z_crit * self._bse_gpu,
|
|
764
|
+
], dim=1)
|
|
765
|
+
|
|
766
|
+
# R-squared on Torch
|
|
767
|
+
self._rsquared_gpu = compute_r2_torch(y, resid)
|
|
768
|
+
|
|
769
|
+
# AIC/BIC on Torch
|
|
770
|
+
k = n_features + (1 if self.fit_intercept else 0)
|
|
771
|
+
scale_mle = torch.sum(resid ** 2) / n_samples
|
|
772
|
+
self._aic_gpu, self._bic_gpu = compute_aic_bic_torch(n_samples, k, scale_mle, device=torch_device)
|
|
773
|
+
|
|
774
|
+
# F-statistic on Torch
|
|
775
|
+
self._fvalue_gpu, self._f_pvalue = compute_f_stat_torch(y, resid, X_design, df_resid, device=torch_device)
|
|
776
|
+
|
|
777
|
+
# Single transfer to CPU at the end
|
|
778
|
+
coef_np = coef.detach().cpu().numpy()
|
|
779
|
+
resid_np = resid.detach().cpu().numpy()
|
|
780
|
+
if y.shape[1] > 1:
|
|
781
|
+
scale_np = scale.detach().cpu().numpy()
|
|
782
|
+
else:
|
|
783
|
+
scale_val = scale.detach().cpu().item()
|
|
784
|
+
scale_np = float(scale_val) if not np.isnan(scale_val) else np.nan
|
|
785
|
+
X_design_np = X_design.detach().cpu().numpy()
|
|
786
|
+
|
|
787
|
+
if self.compute_inference and not self._is_multi_output:
|
|
788
|
+
# Transfer inference results
|
|
789
|
+
self._bse = self._bse_gpu.detach().cpu().numpy()
|
|
790
|
+
self._tvalues = self._tvalues_gpu.detach().cpu().numpy()
|
|
791
|
+
self._pvalues = self._pvalues_gpu.detach().cpu().numpy()
|
|
792
|
+
self._conf_int = self._conf_int_gpu.detach().cpu().numpy()
|
|
793
|
+
|
|
794
|
+
# Store results
|
|
795
|
+
if self.fit_intercept:
|
|
796
|
+
if coef_np.shape[1] > 1:
|
|
797
|
+
self.intercept_ = coef_np[0, :].copy()
|
|
798
|
+
self.coef_ = coef_np[1:, :].T
|
|
799
|
+
self._params = coef_np.copy()
|
|
800
|
+
else:
|
|
801
|
+
self.intercept_ = float(coef_np[0, 0])
|
|
802
|
+
self.coef_ = coef_np[1:, 0].copy() # Ensure 1D array
|
|
803
|
+
self._params = coef_np[:, 0].copy()
|
|
804
|
+
else:
|
|
805
|
+
if coef_np.shape[1] > 1:
|
|
806
|
+
self.intercept_ = np.zeros(coef_np.shape[1], dtype=coef_np.dtype)
|
|
807
|
+
self.coef_ = coef_np.T
|
|
808
|
+
self._params = coef_np.copy()
|
|
809
|
+
else:
|
|
810
|
+
self.intercept_ = 0.0
|
|
811
|
+
self.coef_ = coef_np[:, 0].copy() # Ensure 1D array
|
|
812
|
+
self._params = coef_np[:, 0].copy()
|
|
813
|
+
|
|
814
|
+
self._X_design = X_design_np
|
|
815
|
+
if resid_np.shape[1] == 1:
|
|
816
|
+
self._resid = resid_np[:, 0]
|
|
817
|
+
else:
|
|
818
|
+
self._resid = resid_np
|
|
819
|
+
self._df_resid = df_resid
|
|
820
|
+
self._scale = scale_np
|
|
821
|
+
if self.compute_inference and not self._is_multi_output:
|
|
822
|
+
self._wrap_gaussian_inference_result()
|
|
823
|
+
|
|
824
|
+
# Release large temporary Torch tensors early.
|
|
825
|
+
try:
|
|
826
|
+
del X_design
|
|
827
|
+
except Exception:
|
|
828
|
+
pass
|
|
829
|
+
try:
|
|
830
|
+
del resid
|
|
831
|
+
except Exception:
|
|
832
|
+
pass
|
|
833
|
+
try:
|
|
834
|
+
del XtX
|
|
835
|
+
except Exception:
|
|
836
|
+
pass
|
|
837
|
+
try:
|
|
838
|
+
del Xty
|
|
839
|
+
except Exception:
|
|
840
|
+
pass
|
|
841
|
+
try:
|
|
842
|
+
del coef
|
|
843
|
+
except Exception:
|
|
844
|
+
pass
|
|
845
|
+
self._cleanup_torch_memory()
|
|
846
|
+
|
|
847
|
+
def _compute_inference(self):
|
|
848
|
+
"""Compute standard errors, t-stats, p-values."""
|
|
849
|
+
result = compute_gaussian_inference(
|
|
850
|
+
self._X_design,
|
|
851
|
+
self._params,
|
|
852
|
+
self._resid,
|
|
853
|
+
self._scale,
|
|
854
|
+
self._df_resid,
|
|
855
|
+
self.cov_type,
|
|
856
|
+
hac_maxlags=self.hac_maxlags,
|
|
857
|
+
)
|
|
858
|
+
if result is None:
|
|
859
|
+
self._clear_inference_result()
|
|
860
|
+
return
|
|
861
|
+
result.feature_names = self._inference_feature_names()
|
|
862
|
+
result.apply_to(self)
|
|
863
|
+
|
|
864
|
+
def _inference_feature_names(self):
|
|
865
|
+
if self._feature_names is not None:
|
|
866
|
+
names = list(self._feature_names)
|
|
867
|
+
if self.fit_intercept:
|
|
868
|
+
names.insert(0, "(Intercept)")
|
|
869
|
+
return names
|
|
870
|
+
if self.coef_ is None:
|
|
871
|
+
return None
|
|
872
|
+
n_features = int(np.asarray(self.coef_).shape[-1])
|
|
873
|
+
if self.fit_intercept:
|
|
874
|
+
return ["(Intercept)"] + [f"x{i+1}" for i in range(n_features)]
|
|
875
|
+
return [f"x{i+1}" for i in range(n_features)]
|
|
876
|
+
|
|
877
|
+
def _wrap_gaussian_inference_result(self):
|
|
878
|
+
method = "classical" if self.cov_type == "nonrobust" else "sandwich"
|
|
879
|
+
distribution = "t" if self.cov_type == "nonrobust" else "normal"
|
|
880
|
+
result = GaussianInferenceResult(
|
|
881
|
+
params=self._params,
|
|
882
|
+
bse=self._bse,
|
|
883
|
+
statistic=self._tvalues,
|
|
884
|
+
pvalues=self._pvalues,
|
|
885
|
+
conf_int=self._conf_int,
|
|
886
|
+
cov_type=self.cov_type,
|
|
887
|
+
distribution=distribution,
|
|
888
|
+
df=self._df_resid,
|
|
889
|
+
method=method,
|
|
890
|
+
feature_names=self._inference_feature_names(),
|
|
891
|
+
metadata={"alpha": 0.05},
|
|
892
|
+
)
|
|
893
|
+
result.apply_to(self)
|
|
894
|
+
|
|
895
|
+
@property
|
|
896
|
+
def rsquared(self):
|
|
897
|
+
"""R-squared."""
|
|
898
|
+
if self._y is None or self._resid is None:
|
|
899
|
+
return None
|
|
900
|
+
y_mean = np.mean(self._y)
|
|
901
|
+
ss_tot = np.sum((self._y - y_mean) ** 2)
|
|
902
|
+
ss_res = np.sum(self._resid ** 2)
|
|
903
|
+
return 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
904
|
+
|
|
905
|
+
@property
|
|
906
|
+
def rsquared_adj(self):
|
|
907
|
+
"""Adjusted R-squared."""
|
|
908
|
+
if self._nobs is None:
|
|
909
|
+
return None
|
|
910
|
+
r2 = self.rsquared
|
|
911
|
+
k = int(self._X_design.shape[1] - (1 if self.fit_intercept else 0))
|
|
912
|
+
return 1 - (1 - r2) * (self._nobs - 1) / self._df_resid
|
|
913
|
+
|
|
914
|
+
@property
|
|
915
|
+
def fvalue(self):
|
|
916
|
+
"""F-statistic."""
|
|
917
|
+
if self._y is None or self._resid is None:
|
|
918
|
+
return None
|
|
919
|
+
y_mean = np.mean(self._y)
|
|
920
|
+
ss_tot = np.sum((self._y - y_mean) ** 2)
|
|
921
|
+
ss_res = np.sum(self._resid ** 2)
|
|
922
|
+
ss_reg = ss_tot - ss_res
|
|
923
|
+
k = int(self._X_design.shape[1] - (1 if self.fit_intercept else 0))
|
|
924
|
+
if k == 0 or ss_res <= 0:
|
|
925
|
+
return np.inf
|
|
926
|
+
return (ss_reg / k) / (ss_res / self._df_resid)
|
|
927
|
+
|
|
928
|
+
@property
|
|
929
|
+
def f_pvalue(self):
|
|
930
|
+
"""p-value for F-statistic."""
|
|
931
|
+
fv = self.fvalue
|
|
932
|
+
if fv is None or fv == np.inf:
|
|
933
|
+
return 1.0
|
|
934
|
+
k = int(self._X_design.shape[1] - (1 if self.fit_intercept else 0))
|
|
935
|
+
return 1 - stats.f.cdf(fv, k, self._df_resid)
|
|
936
|
+
|
|
937
|
+
@property
|
|
938
|
+
def aic(self):
|
|
939
|
+
"""Akaike Information Criterion."""
|
|
940
|
+
if self._is_multi_output:
|
|
941
|
+
return None
|
|
942
|
+
if self._nobs is None or self._scale is None:
|
|
943
|
+
return None
|
|
944
|
+
if np.any(np.isnan(self._scale)):
|
|
945
|
+
return None
|
|
946
|
+
# AIC = -2 * log-likelihood + 2 * k
|
|
947
|
+
return -2 * self.llf + 2 * len(self._params)
|
|
948
|
+
|
|
949
|
+
@property
|
|
950
|
+
def bic(self):
|
|
951
|
+
"""Bayesian Information Criterion."""
|
|
952
|
+
if self._is_multi_output:
|
|
953
|
+
return None
|
|
954
|
+
if self._nobs is None or self._scale is None:
|
|
955
|
+
return None
|
|
956
|
+
if np.any(np.isnan(self._scale)):
|
|
957
|
+
return None
|
|
958
|
+
n = self._nobs
|
|
959
|
+
k = len(self._params)
|
|
960
|
+
# BIC = -2 * log-likelihood + k * log(n)
|
|
961
|
+
return -2 * self.llf + k * np.log(n)
|
|
962
|
+
|
|
963
|
+
@property
|
|
964
|
+
def llf(self):
|
|
965
|
+
"""Log-likelihood (matches statsmodels/R)."""
|
|
966
|
+
if self._nobs is None or self._resid is None:
|
|
967
|
+
return None
|
|
968
|
+
n = self._nobs
|
|
969
|
+
# Use MLE estimate of sigma^2 = RSS/n (not RSS/df_resid)
|
|
970
|
+
sigma2_mle = np.sum(self._resid ** 2) / n
|
|
971
|
+
# LL = -n/2 * log(2*pi*sigma2_mle) - n/2
|
|
972
|
+
return -n/2 * np.log(2 * np.pi * sigma2_mle) - n/2
|
|
973
|
+
|
|
974
|
+
def summary(self):
|
|
975
|
+
"""Print summary table similar to R's summary(lm())."""
|
|
976
|
+
if not self._fitted:
|
|
977
|
+
raise RuntimeError("Model has not been fitted yet.")
|
|
978
|
+
|
|
979
|
+
if not self.compute_inference:
|
|
980
|
+
raise RuntimeError(
|
|
981
|
+
"compute_inference=False: summary/inference statistics are not available. "
|
|
982
|
+
"Re-fit with compute_inference=True (default)."
|
|
983
|
+
)
|
|
984
|
+
if self._is_multi_output:
|
|
985
|
+
raise RuntimeError("summary() is only available for single-output linear regression.")
|
|
986
|
+
if self._bse is None or self._pvalues is None or self._conf_int is None:
|
|
987
|
+
raise RuntimeError(
|
|
988
|
+
"Inference statistics are not available for the current fit. "
|
|
989
|
+
"This can happen when residual degrees of freedom are non-positive."
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
# Build feature names
|
|
993
|
+
if self._feature_names is not None:
|
|
994
|
+
feature_names = list(self._feature_names)
|
|
995
|
+
if self.fit_intercept:
|
|
996
|
+
feature_names.insert(0, '(Intercept)')
|
|
997
|
+
elif self.fit_intercept:
|
|
998
|
+
feature_names = ['(Intercept)'] + [f'x{i+1}' for i in range(len(self.coef_))]
|
|
999
|
+
else:
|
|
1000
|
+
feature_names = [f'x{i+1}' for i in range(len(self.coef_))]
|
|
1001
|
+
|
|
1002
|
+
print("=" * 80)
|
|
1003
|
+
print(" Linear Regression Results")
|
|
1004
|
+
print("=" * 80)
|
|
1005
|
+
print(f"Covariance Type: {self.cov_type:>15}")
|
|
1006
|
+
print(f"No. Observations: {self._nobs:>15}")
|
|
1007
|
+
print(f"Degrees of Freedom: {self._df_resid:>15}")
|
|
1008
|
+
print(f"R-squared: {self.rsquared:>15.4f}")
|
|
1009
|
+
print(f"Adj. R-squared: {self.rsquared_adj:>15.4f}")
|
|
1010
|
+
print(f"F-statistic: {self.fvalue:>15.4f}")
|
|
1011
|
+
print(f"Prob (F-statistic): {self.f_pvalue:>15.4e}")
|
|
1012
|
+
print(f"Log-Likelihood: {self.llf:>15.4f}")
|
|
1013
|
+
print(f"AIC: {self.aic:>15.4f}")
|
|
1014
|
+
print(f"BIC: {self.bic:>15.4f}")
|
|
1015
|
+
print("-" * 80)
|
|
1016
|
+
print(f"{'':<15} {'coef':>12} {'std err':>12} {'t':>10} {'P>|t|':>10} {'[0.025':>12} {'0.975]':>12}")
|
|
1017
|
+
print("-" * 80)
|
|
1018
|
+
|
|
1019
|
+
for i, name in enumerate(feature_names):
|
|
1020
|
+
print(f"{name:<15} {self._params[i]:>12.4f} {self._bse[i]:>12.4f} "
|
|
1021
|
+
f"{self._tvalues[i]:>10.3f} {self._pvalues[i]:>10.4f} "
|
|
1022
|
+
f"{self._conf_int[i, 0]:>12.4f} {self._conf_int[i, 1]:>12.4f}")
|
|
1023
|
+
|
|
1024
|
+
print("=" * 80)
|
|
1025
|
+
|
|
1026
|
+
def predict(self, X):
|
|
1027
|
+
"""Predict using the linear model.
|
|
1028
|
+
|
|
1029
|
+
Parameters
|
|
1030
|
+
----------
|
|
1031
|
+
X : array-like or pd.DataFrame
|
|
1032
|
+
If a DataFrame is passed and the model was trained with a formula,
|
|
1033
|
+
the design matrix is automatically built using the stored
|
|
1034
|
+
``design_info``.
|
|
1035
|
+
|
|
1036
|
+
Returns
|
|
1037
|
+
-------
|
|
1038
|
+
predictions : ndarray
|
|
1039
|
+
"""
|
|
1040
|
+
self._check_is_fitted()
|
|
1041
|
+
|
|
1042
|
+
# If model was trained with formula and X is a DataFrame,
|
|
1043
|
+
# rebuild the design matrix using the stored design_info.
|
|
1044
|
+
if self._design_info is not None:
|
|
1045
|
+
import pandas as pd
|
|
1046
|
+
if isinstance(X, pd.DataFrame):
|
|
1047
|
+
from statgpu.core.formula import FormulaParser
|
|
1048
|
+
# Reconstruct parser from design_info
|
|
1049
|
+
parser = FormulaParser.__new__(FormulaParser)
|
|
1050
|
+
parser._design_info = self._design_info
|
|
1051
|
+
parser.formula = None
|
|
1052
|
+
X = parser.transform(X)
|
|
1053
|
+
# Drop intercept column to match the fitting path
|
|
1054
|
+
col_names = list(self._design_info.column_names)
|
|
1055
|
+
if self._formula_has_intercept and "Intercept" in col_names:
|
|
1056
|
+
intercept_idx = col_names.index("Intercept")
|
|
1057
|
+
X = np.delete(X, intercept_idx, axis=1)
|
|
1058
|
+
else:
|
|
1059
|
+
X = np.asarray(X)
|
|
1060
|
+
else:
|
|
1061
|
+
X = np.asarray(X)
|
|
1062
|
+
|
|
1063
|
+
device = self._get_compute_device()
|
|
1064
|
+
if device == Device.CUDA:
|
|
1065
|
+
import cupy as cp
|
|
1066
|
+
|
|
1067
|
+
X_gpu = cp.asarray(self._to_array(X, Device.CUDA))
|
|
1068
|
+
coef_gpu = cp.asarray(self.coef_)
|
|
1069
|
+
intercept_gpu = cp.asarray(self.intercept_, dtype=coef_gpu.dtype)
|
|
1070
|
+
if coef_gpu.ndim == 2:
|
|
1071
|
+
return X_gpu @ coef_gpu.T + intercept_gpu
|
|
1072
|
+
return X_gpu @ coef_gpu + intercept_gpu
|
|
1073
|
+
if device == Device.TORCH:
|
|
1074
|
+
import torch
|
|
1075
|
+
|
|
1076
|
+
X_torch = self._to_array(X, Device.TORCH, backend="torch").to(torch.float64)
|
|
1077
|
+
coef_torch = torch.as_tensor(self.coef_, dtype=X_torch.dtype, device=X_torch.device)
|
|
1078
|
+
intercept_torch = torch.as_tensor(
|
|
1079
|
+
self.intercept_, dtype=X_torch.dtype, device=X_torch.device
|
|
1080
|
+
)
|
|
1081
|
+
if coef_torch.ndim == 2:
|
|
1082
|
+
return X_torch @ coef_torch.T + intercept_torch
|
|
1083
|
+
return X_torch @ coef_torch + intercept_torch
|
|
1084
|
+
X = self._to_array(X, Device.CPU)
|
|
1085
|
+
X = np.asarray(X)
|
|
1086
|
+
if np.asarray(self.coef_).ndim == 2:
|
|
1087
|
+
return X @ self.coef_.T + self.intercept_
|
|
1088
|
+
return X @ self.coef_ + self.intercept_
|
|
1089
|
+
|
|
1090
|
+
def score(self, X, y):
|
|
1091
|
+
"""Return R^2 score."""
|
|
1092
|
+
y_pred = self.predict(X)
|
|
1093
|
+
device = self._get_compute_device()
|
|
1094
|
+
if device == Device.CUDA:
|
|
1095
|
+
import cupy as cp
|
|
1096
|
+
|
|
1097
|
+
yb = cp.asarray(self._to_array(y, Device.CUDA))
|
|
1098
|
+
if y_pred.ndim == 1:
|
|
1099
|
+
ss_res = cp.sum((yb - y_pred) ** 2)
|
|
1100
|
+
ss_tot = cp.sum((yb - cp.mean(yb)) ** 2)
|
|
1101
|
+
return float((1 - ss_res / ss_tot).item()) if float(ss_tot.item()) > 0 else 0.0
|
|
1102
|
+
ss_res = cp.sum((yb - y_pred) ** 2, axis=0)
|
|
1103
|
+
ss_tot = cp.sum((yb - cp.mean(yb, axis=0)) ** 2, axis=0)
|
|
1104
|
+
r2 = cp.where(ss_tot > 0, 1 - ss_res / ss_tot, 0.0)
|
|
1105
|
+
return float(cp.mean(r2).item())
|
|
1106
|
+
if device == Device.TORCH:
|
|
1107
|
+
import torch
|
|
1108
|
+
|
|
1109
|
+
yb = self._to_array(y, Device.TORCH, backend="torch").to(y_pred.dtype)
|
|
1110
|
+
if y_pred.ndim == 1:
|
|
1111
|
+
ss_res = torch.sum((yb - y_pred) ** 2)
|
|
1112
|
+
ss_tot = torch.sum((yb - torch.mean(yb)) ** 2)
|
|
1113
|
+
return float((1 - ss_res / ss_tot).item()) if float(ss_tot.item()) > 0 else 0.0
|
|
1114
|
+
ss_res = torch.sum((yb - y_pred) ** 2, dim=0)
|
|
1115
|
+
ss_tot = torch.sum((yb - torch.mean(yb, dim=0)) ** 2, dim=0)
|
|
1116
|
+
r2 = torch.where(ss_tot > 0, 1 - ss_res / ss_tot, torch.zeros_like(ss_tot))
|
|
1117
|
+
return float(torch.mean(r2).item())
|
|
1118
|
+
y_pred = np.asarray(y_pred)
|
|
1119
|
+
y = self._to_numpy(y)
|
|
1120
|
+
if y_pred.ndim == 1:
|
|
1121
|
+
ss_res = np.sum((y - y_pred) ** 2)
|
|
1122
|
+
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
|
1123
|
+
return 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
1124
|
+
ss_res = np.sum((y - y_pred) ** 2, axis=0)
|
|
1125
|
+
ss_tot = np.sum((y - np.mean(y, axis=0)) ** 2, axis=0)
|
|
1126
|
+
r2 = np.where(ss_tot > 0, 1 - ss_res / ss_tot, 0.0)
|
|
1127
|
+
return float(np.mean(r2))
|