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.
Files changed (168) hide show
  1. statgpu/__init__.py +174 -0
  2. statgpu/_base.py +544 -0
  3. statgpu/_config.py +127 -0
  4. statgpu/anova/__init__.py +5 -0
  5. statgpu/anova/_oneway.py +194 -0
  6. statgpu/backends/__init__.py +83 -0
  7. statgpu/backends/_array_ops.py +529 -0
  8. statgpu/backends/_base.py +184 -0
  9. statgpu/backends/_cupy.py +453 -0
  10. statgpu/backends/_factory.py +65 -0
  11. statgpu/backends/_gpu_inference_cupy.py +214 -0
  12. statgpu/backends/_gpu_inference_torch.py +422 -0
  13. statgpu/backends/_numpy.py +324 -0
  14. statgpu/backends/_torch.py +685 -0
  15. statgpu/backends/_torch_safe.py +47 -0
  16. statgpu/backends/_utils.py +423 -0
  17. statgpu/core/__init__.py +10 -0
  18. statgpu/core/formula/__init__.py +33 -0
  19. statgpu/core/formula/_design.py +99 -0
  20. statgpu/core/formula/_parser.py +191 -0
  21. statgpu/core/formula/_terms.py +70 -0
  22. statgpu/core/formula/tests/__init__.py +0 -0
  23. statgpu/core/formula/tests/test_parser.py +194 -0
  24. statgpu/covariance/__init__.py +6 -0
  25. statgpu/covariance/_empirical.py +310 -0
  26. statgpu/covariance/_shrinkage.py +248 -0
  27. statgpu/cross_validation/__init__.py +31 -0
  28. statgpu/cross_validation/_base.py +410 -0
  29. statgpu/cross_validation/_engine.py +167 -0
  30. statgpu/diagnostics/__init__.py +7 -0
  31. statgpu/diagnostics/_regression_diagnostics.py +188 -0
  32. statgpu/feature_selection/__init__.py +24 -0
  33. statgpu/feature_selection/_knockoff.py +870 -0
  34. statgpu/feature_selection/_knockoff_utils.py +1003 -0
  35. statgpu/feature_selection/_stepwise.py +300 -0
  36. statgpu/glm_core/__init__.py +81 -0
  37. statgpu/glm_core/_base.py +202 -0
  38. statgpu/glm_core/_family.py +362 -0
  39. statgpu/glm_core/_fused.py +149 -0
  40. statgpu/glm_core/_gamma.py +111 -0
  41. statgpu/glm_core/_inverse_gaussian.py +62 -0
  42. statgpu/glm_core/_irls.py +561 -0
  43. statgpu/glm_core/_logistic.py +82 -0
  44. statgpu/glm_core/_negative_binomial.py +68 -0
  45. statgpu/glm_core/_poisson.py +60 -0
  46. statgpu/glm_core/_solver_legacy.py +100 -0
  47. statgpu/glm_core/_squared.py +53 -0
  48. statgpu/glm_core/_tweedie.py +74 -0
  49. statgpu/inference/__init__.py +239 -0
  50. statgpu/inference/_distributions_backend.py +2610 -0
  51. statgpu/inference/_multiple_testing.py +391 -0
  52. statgpu/inference/_resampling.py +1400 -0
  53. statgpu/inference/_results.py +265 -0
  54. statgpu/linear_model/__init__.py +75 -0
  55. statgpu/linear_model/_gaussian_inference.py +306 -0
  56. statgpu/linear_model/_glm_base.py +1261 -0
  57. statgpu/linear_model/_ordered_logit.py +52 -0
  58. statgpu/linear_model/_ordered_probit.py +50 -0
  59. statgpu/linear_model/_stats.py +170 -0
  60. statgpu/linear_model/cv/__init__.py +13 -0
  61. statgpu/linear_model/cv/_elasticnet_cv.py +892 -0
  62. statgpu/linear_model/cv/_lasso_cv.py +253 -0
  63. statgpu/linear_model/cv/_logistic_cv.py +895 -0
  64. statgpu/linear_model/cv/_ridge_cv.py +1160 -0
  65. statgpu/linear_model/legacy/__init__.py +1 -0
  66. statgpu/linear_model/legacy/_distributions_legacy_gpu.py +340 -0
  67. statgpu/linear_model/legacy/_elasticnet_legacy.py +936 -0
  68. statgpu/linear_model/legacy/_lasso_legacy.py +4876 -0
  69. statgpu/linear_model/legacy/_penalized_legacy.py +1174 -0
  70. statgpu/linear_model/legacy/_ridge_legacy.py +863 -0
  71. statgpu/linear_model/legacy/_solver_legacy.py +104 -0
  72. statgpu/linear_model/penalized/__init__.py +25 -0
  73. statgpu/linear_model/penalized/_base.py +437 -0
  74. statgpu/linear_model/penalized/_fit_mixin.py +1877 -0
  75. statgpu/linear_model/penalized/_inference_mixin.py +1179 -0
  76. statgpu/linear_model/penalized/_penalized_cv.py +2699 -0
  77. statgpu/linear_model/penalized/_penalized_gamma.py +86 -0
  78. statgpu/linear_model/penalized/_penalized_inverse_gaussian.py +62 -0
  79. statgpu/linear_model/penalized/_penalized_linear.py +236 -0
  80. statgpu/linear_model/penalized/_penalized_logistic.py +100 -0
  81. statgpu/linear_model/penalized/_penalized_negative_binomial.py +65 -0
  82. statgpu/linear_model/penalized/_penalized_poisson.py +62 -0
  83. statgpu/linear_model/penalized/_penalized_tweedie.py +65 -0
  84. statgpu/linear_model/penalized/_predict_mixin.py +182 -0
  85. statgpu/linear_model/wrappers/__init__.py +31 -0
  86. statgpu/linear_model/wrappers/_adaptive_lasso.py +63 -0
  87. statgpu/linear_model/wrappers/_elasticnet.py +75 -0
  88. statgpu/linear_model/wrappers/_gamma.py +67 -0
  89. statgpu/linear_model/wrappers/_inverse_gaussian.py +47 -0
  90. statgpu/linear_model/wrappers/_lasso.py +2124 -0
  91. statgpu/linear_model/wrappers/_linear.py +1127 -0
  92. statgpu/linear_model/wrappers/_logistic.py +1435 -0
  93. statgpu/linear_model/wrappers/_mcp.py +58 -0
  94. statgpu/linear_model/wrappers/_negative_binomial.py +58 -0
  95. statgpu/linear_model/wrappers/_poisson.py +48 -0
  96. statgpu/linear_model/wrappers/_ridge.py +166 -0
  97. statgpu/linear_model/wrappers/_scad.py +58 -0
  98. statgpu/linear_model/wrappers/_tweedie.py +57 -0
  99. statgpu/metrics/__init__.py +21 -0
  100. statgpu/metrics/_classification.py +591 -0
  101. statgpu/nonparametric/__init__.py +50 -0
  102. statgpu/nonparametric/kernel_methods/__init__.py +25 -0
  103. statgpu/nonparametric/kernel_methods/_kernels.py +246 -0
  104. statgpu/nonparametric/kernel_methods/_krr.py +234 -0
  105. statgpu/nonparametric/kernel_methods/_krr_cv.py +380 -0
  106. statgpu/nonparametric/kernel_smoothing/__init__.py +39 -0
  107. statgpu/nonparametric/kernel_smoothing/_bandwidth_selection.py +1083 -0
  108. statgpu/nonparametric/kernel_smoothing/_kde.py +761 -0
  109. statgpu/nonparametric/kernel_smoothing/_kernel_common.py +348 -0
  110. statgpu/nonparametric/kernel_smoothing/_kernel_regression.py +748 -0
  111. statgpu/nonparametric/splines/__init__.py +5 -0
  112. statgpu/nonparametric/splines/_bspline_basis.py +336 -0
  113. statgpu/nonparametric/splines/_penalized.py +349 -0
  114. statgpu/panel/__init__.py +19 -0
  115. statgpu/panel/_covariance.py +140 -0
  116. statgpu/panel/_fixed_effects.py +420 -0
  117. statgpu/panel/_random_effects.py +385 -0
  118. statgpu/panel/_utils.py +482 -0
  119. statgpu/penalties/__init__.py +139 -0
  120. statgpu/penalties/_adaptive_l1.py +313 -0
  121. statgpu/penalties/_base.py +261 -0
  122. statgpu/penalties/_categories.py +39 -0
  123. statgpu/penalties/_elasticnet.py +98 -0
  124. statgpu/penalties/_group_lasso.py +678 -0
  125. statgpu/penalties/_group_mcp.py +553 -0
  126. statgpu/penalties/_group_scad.py +605 -0
  127. statgpu/penalties/_l1.py +107 -0
  128. statgpu/penalties/_l2.py +77 -0
  129. statgpu/penalties/_mcp.py +237 -0
  130. statgpu/penalties/_scad.py +260 -0
  131. statgpu/semiparametric/__init__.py +5 -0
  132. statgpu/semiparametric/_gam.py +401 -0
  133. statgpu/solvers/__init__.py +24 -0
  134. statgpu/solvers/_admm.py +241 -0
  135. statgpu/solvers/_constants.py +15 -0
  136. statgpu/solvers/_convergence.py +6 -0
  137. statgpu/solvers/_fista.py +436 -0
  138. statgpu/solvers/_fista_bb.py +513 -0
  139. statgpu/solvers/_fista_lla.py +541 -0
  140. statgpu/solvers/_lbfgs.py +206 -0
  141. statgpu/solvers/_newton.py +149 -0
  142. statgpu/solvers/_utils.py +277 -0
  143. statgpu/survival/__init__.py +14 -0
  144. statgpu/survival/_cox.py +3974 -0
  145. statgpu/survival/_cox_breslow_triton_kernel.py +106 -0
  146. statgpu/survival/_cox_cv.py +1159 -0
  147. statgpu/survival/_cox_efron_cuda.py +1280 -0
  148. statgpu/survival/_cox_efron_triton.py +359 -0
  149. statgpu/unsupervised/__init__.py +29 -0
  150. statgpu/unsupervised/_agglomerative.py +307 -0
  151. statgpu/unsupervised/_dbscan.py +263 -0
  152. statgpu/unsupervised/_dbscan_cpu.pyx +125 -0
  153. statgpu/unsupervised/_gmm.py +332 -0
  154. statgpu/unsupervised/_incremental_pca.py +176 -0
  155. statgpu/unsupervised/_kmeans.py +261 -0
  156. statgpu/unsupervised/_minibatch_kmeans.py +299 -0
  157. statgpu/unsupervised/_minibatch_nmf.py +252 -0
  158. statgpu/unsupervised/_nmf.py +190 -0
  159. statgpu/unsupervised/_pca.py +189 -0
  160. statgpu/unsupervised/_truncated_svd.py +132 -0
  161. statgpu/unsupervised/_tsne.py +192 -0
  162. statgpu/unsupervised/_umap.py +224 -0
  163. statgpu/unsupervised/_utils.py +134 -0
  164. statgpu-0.1.0.dist-info/METADATA +245 -0
  165. statgpu-0.1.0.dist-info/RECORD +168 -0
  166. statgpu-0.1.0.dist-info/WHEEL +5 -0
  167. statgpu-0.1.0.dist-info/licenses/LICENSE +199 -0
  168. 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)