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,761 @@
|
|
|
1
|
+
"""Kernel density estimation with NumPy/CuPy backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import math
|
|
7
|
+
from statistics import NormalDist
|
|
8
|
+
from typing import Any, Dict, Optional, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from statgpu._base import BaseEstimator
|
|
13
|
+
from statgpu.backends import xp_asarray, xp_empty, xp_maximum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _xp_max(x, **kwargs):
|
|
17
|
+
"""Backend-safe max that returns values only (torch.max returns (values, indices))."""
|
|
18
|
+
# Use amax if available (returns values only, works for torch/cupy/numpy)
|
|
19
|
+
if hasattr(x, 'amax'):
|
|
20
|
+
return x.amax(**kwargs)
|
|
21
|
+
# Fallback: check if max returns (values, indices) tuple
|
|
22
|
+
result = x.max(**kwargs)
|
|
23
|
+
if hasattr(result, 'values'):
|
|
24
|
+
return result.values
|
|
25
|
+
return result
|
|
26
|
+
from statgpu.nonparametric.kernel_smoothing._bandwidth_selection import select_bandwidth
|
|
27
|
+
|
|
28
|
+
from statgpu.nonparametric.kernel_smoothing._kernel_common import (
|
|
29
|
+
_auto_backend_from_device,
|
|
30
|
+
_as_points_2d,
|
|
31
|
+
_as_samples_2d,
|
|
32
|
+
_effective_sample_size,
|
|
33
|
+
_get_xp,
|
|
34
|
+
_kernel_values_from_quad,
|
|
35
|
+
_normalize_kernel_name,
|
|
36
|
+
_normalize_weights,
|
|
37
|
+
_stable_inv_and_det,
|
|
38
|
+
_to_float_scalar,
|
|
39
|
+
_to_numpy,
|
|
40
|
+
_torch_device_from_data,
|
|
41
|
+
_weighted_covariance,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _unit_ball_volume(n_features: int) -> float:
|
|
46
|
+
d = int(n_features)
|
|
47
|
+
if d <= 0:
|
|
48
|
+
raise ValueError("n_features must be a positive integer")
|
|
49
|
+
return float((math.pi ** (0.5 * d)) / math.gamma(0.5 * d + 1.0))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _kernel_norm_const(kernel_name: str, n_features: int) -> float:
|
|
53
|
+
d = int(n_features)
|
|
54
|
+
if kernel_name == "gaussian":
|
|
55
|
+
return float((2.0 * math.pi) ** (-0.5 * d))
|
|
56
|
+
|
|
57
|
+
volume = _unit_ball_volume(d)
|
|
58
|
+
if kernel_name == "rectangular":
|
|
59
|
+
return float(1.0 / volume)
|
|
60
|
+
if kernel_name == "triangular":
|
|
61
|
+
return float((d + 1.0) / volume)
|
|
62
|
+
if kernel_name == "epanechnikov":
|
|
63
|
+
return float((d + 2.0) / (2.0 * volume))
|
|
64
|
+
if kernel_name == "biweight":
|
|
65
|
+
return float(((d + 2.0) * (d + 4.0)) / (8.0 * volume))
|
|
66
|
+
if kernel_name == "triweight":
|
|
67
|
+
return float(((d + 2.0) * (d + 4.0) * (d + 6.0)) / (48.0 * volume))
|
|
68
|
+
if kernel_name == "cosine":
|
|
69
|
+
if d != 1:
|
|
70
|
+
raise ValueError("kernel='cosine' currently supports only 1D samples")
|
|
71
|
+
return 1.0
|
|
72
|
+
if kernel_name == "optcosine":
|
|
73
|
+
if d != 1:
|
|
74
|
+
raise ValueError("kernel='optcosine' currently supports only 1D samples")
|
|
75
|
+
return float(math.pi / 4.0)
|
|
76
|
+
|
|
77
|
+
raise ValueError(f"Unsupported kernel: {kernel_name}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class KernelDensityEstimator(BaseEstimator):
|
|
81
|
+
"""sklearn-style kernel density estimator with class-owned fit/predict API."""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
bandwidth: Union[str, float, int] = "scott",
|
|
87
|
+
weights=None,
|
|
88
|
+
kernel: str = "gaussian",
|
|
89
|
+
backend: str = "auto",
|
|
90
|
+
device: str = "auto",
|
|
91
|
+
n_jobs: Optional[int] = None,
|
|
92
|
+
gpu_memory_cleanup: bool = False,
|
|
93
|
+
):
|
|
94
|
+
super().__init__(device=device, n_jobs=n_jobs)
|
|
95
|
+
self.bandwidth = bandwidth
|
|
96
|
+
self.weights = weights
|
|
97
|
+
self.kernel = kernel
|
|
98
|
+
self.backend = backend
|
|
99
|
+
self.gpu_memory_cleanup = gpu_memory_cleanup
|
|
100
|
+
|
|
101
|
+
def _resolve_backend_name(self, X) -> str:
|
|
102
|
+
name = str(self.backend).strip().lower()
|
|
103
|
+
if name != "auto":
|
|
104
|
+
return name
|
|
105
|
+
return _auto_backend_from_device(self._get_compute_device().value)
|
|
106
|
+
|
|
107
|
+
def fit(self, X, y=None):
|
|
108
|
+
backend_name = self._resolve_backend_name(X)
|
|
109
|
+
xp = _get_xp(backend_name)
|
|
110
|
+
|
|
111
|
+
samples_2d = _as_samples_2d(X, xp)
|
|
112
|
+
n_samples, n_features = int(samples_2d.shape[0]), int(samples_2d.shape[1])
|
|
113
|
+
|
|
114
|
+
device = _torch_device_from_data(samples_2d)
|
|
115
|
+
self._torch_device = device
|
|
116
|
+
weights_1d = _normalize_weights(self.weights, n_samples, xp, device=device, ref_arr=samples_2d)
|
|
117
|
+
n_eff = _effective_sample_size(weights_1d, xp)
|
|
118
|
+
kernel_name = _normalize_kernel_name(self.kernel)
|
|
119
|
+
|
|
120
|
+
data_cov = _weighted_covariance(samples_2d, weights_1d, xp)
|
|
121
|
+
if kernel_name in ("cosine", "optcosine") and n_features != 1:
|
|
122
|
+
raise ValueError(f"kernel='{kernel_name}' currently supports only 1D samples")
|
|
123
|
+
|
|
124
|
+
bw_result = None
|
|
125
|
+
if isinstance(self.bandwidth, str):
|
|
126
|
+
bw_result = select_bandwidth(
|
|
127
|
+
self.bandwidth,
|
|
128
|
+
n_eff=n_eff,
|
|
129
|
+
n_features=n_features,
|
|
130
|
+
samples_2d=samples_2d,
|
|
131
|
+
weights_1d=weights_1d,
|
|
132
|
+
data_cov=data_cov,
|
|
133
|
+
xp=xp,
|
|
134
|
+
enable_r_selectors=True,
|
|
135
|
+
estimator="kde",
|
|
136
|
+
)
|
|
137
|
+
factor = float(bw_result.factor)
|
|
138
|
+
else:
|
|
139
|
+
factor = float(self.bandwidth)
|
|
140
|
+
if (not np.isfinite(factor)) or factor <= 0.0:
|
|
141
|
+
raise ValueError("bandwidth factor must be a finite positive scalar")
|
|
142
|
+
|
|
143
|
+
scaled_cov = data_cov * (factor**2)
|
|
144
|
+
inv_cov, det_cov, stable_cov = _stable_inv_and_det(scaled_cov, xp)
|
|
145
|
+
|
|
146
|
+
kernel_norm_const = _kernel_norm_const(kernel_name, n_features)
|
|
147
|
+
if not np.isfinite(kernel_norm_const) or kernel_norm_const <= 0.0:
|
|
148
|
+
raise ValueError("kernel normalization constant must be finite and positive")
|
|
149
|
+
norm_const = np.sqrt(det_cov) / kernel_norm_const
|
|
150
|
+
|
|
151
|
+
self.samples_ = samples_2d
|
|
152
|
+
self.weights_ = weights_1d
|
|
153
|
+
self.bandwidth_factor_ = factor
|
|
154
|
+
self.bandwidth_info_ = bw_result
|
|
155
|
+
self.covariance_ = stable_cov
|
|
156
|
+
self.inv_covariance_ = inv_cov
|
|
157
|
+
self.norm_const_ = float(norm_const)
|
|
158
|
+
self.inv_norm_const_ = float(1.0 / self.norm_const_)
|
|
159
|
+
self.kernel_ = kernel_name
|
|
160
|
+
self.n_features_ = n_features
|
|
161
|
+
self.n_samples_ = n_samples
|
|
162
|
+
self.backend_ = backend_name
|
|
163
|
+
# Cache these terms for repeated evaluations to avoid recomputation in hot paths.
|
|
164
|
+
self._samples_proj_ = self.samples_ @ self.inv_covariance_
|
|
165
|
+
self._samples_quad_ = xp.sum(self._samples_proj_ * self.samples_, axis=1)
|
|
166
|
+
self._fitted = True
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
def _require_fitted(self) -> None:
|
|
170
|
+
if not self._fitted:
|
|
171
|
+
raise RuntimeError("Estimator not fitted. Call fit() first.")
|
|
172
|
+
|
|
173
|
+
def _cleanup_cuda_memory(self):
|
|
174
|
+
if not self.gpu_memory_cleanup:
|
|
175
|
+
return
|
|
176
|
+
try:
|
|
177
|
+
import cupy as cp
|
|
178
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
179
|
+
cp.get_default_pinned_memory_pool().free_all_blocks()
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
def _cleanup_torch_memory(self):
|
|
184
|
+
if not self.gpu_memory_cleanup:
|
|
185
|
+
return
|
|
186
|
+
try:
|
|
187
|
+
import torch
|
|
188
|
+
torch.cuda.empty_cache()
|
|
189
|
+
torch.cuda.synchronize()
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
def __del__(self):
|
|
194
|
+
try:
|
|
195
|
+
self._cleanup_cuda_memory()
|
|
196
|
+
self._cleanup_torch_memory()
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
def _evaluate_density(self, points_2d, *, batch_size: int, xp):
|
|
201
|
+
n_points = int(points_2d.shape[0])
|
|
202
|
+
n_samples = int(self.samples_.shape[0])
|
|
203
|
+
n_features = int(self.samples_.shape[1])
|
|
204
|
+
|
|
205
|
+
if batch_size <= 0:
|
|
206
|
+
raise ValueError("batch_size must be a positive integer")
|
|
207
|
+
|
|
208
|
+
out = xp_empty((n_points,), xp.float64, xp, ref_arr=points_2d)
|
|
209
|
+
|
|
210
|
+
if n_features == 1:
|
|
211
|
+
samples_1d = self.samples_[:, 0]
|
|
212
|
+
inv_scalar = self.inv_covariance_[0, 0]
|
|
213
|
+
|
|
214
|
+
if xp is np and self.kernel_ == "gaussian" and (n_points * n_samples) <= 8_000_000:
|
|
215
|
+
q_1d = points_2d[:, 0]
|
|
216
|
+
diff = q_1d[:, None] - samples_1d[None, :]
|
|
217
|
+
diff *= diff
|
|
218
|
+
diff *= (-0.5 * inv_scalar)
|
|
219
|
+
np.exp(diff, out=diff)
|
|
220
|
+
out[:] = (diff @ self.weights_) * self.inv_norm_const_
|
|
221
|
+
return out
|
|
222
|
+
|
|
223
|
+
if xp is np and (n_points * n_samples) <= 8_000_000:
|
|
224
|
+
q_1d = points_2d[:, 0]
|
|
225
|
+
diff = q_1d[:, None] - samples_1d[None, :]
|
|
226
|
+
quad = (diff * diff) * inv_scalar
|
|
227
|
+
kernels = _kernel_values_from_quad(quad, self.kernel_, xp)
|
|
228
|
+
out[:] = (kernels @ self.weights_) * self.inv_norm_const_
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
for start in range(0, n_points, int(batch_size)):
|
|
232
|
+
stop = min(start + int(batch_size), n_points)
|
|
233
|
+
q_1d = points_2d[start:stop, 0]
|
|
234
|
+
diff = q_1d[:, None] - samples_1d[None, :]
|
|
235
|
+
if self.kernel_ == "gaussian":
|
|
236
|
+
diff *= diff
|
|
237
|
+
diff *= (-0.5 * inv_scalar)
|
|
238
|
+
xp.exp(diff, out=diff)
|
|
239
|
+
out[start:stop] = (diff @ self.weights_) * self.inv_norm_const_
|
|
240
|
+
else:
|
|
241
|
+
quad = (diff * diff) * inv_scalar
|
|
242
|
+
kernels = _kernel_values_from_quad(quad, self.kernel_, xp)
|
|
243
|
+
out[start:stop] = (kernels @ self.weights_) * self.inv_norm_const_
|
|
244
|
+
return out
|
|
245
|
+
|
|
246
|
+
s_quad = self._samples_quad_
|
|
247
|
+
is_gaussian = self.kernel_ == "gaussian"
|
|
248
|
+
use_log_sum_exp = n_features >= 8
|
|
249
|
+
|
|
250
|
+
for start in range(0, n_points, int(batch_size)):
|
|
251
|
+
stop = min(start + int(batch_size), n_points)
|
|
252
|
+
q = points_2d[start:stop]
|
|
253
|
+
|
|
254
|
+
q_proj = q @ self.inv_covariance_
|
|
255
|
+
q_quad = xp.sum(q_proj * q, axis=1)
|
|
256
|
+
cross = q_proj @ self.samples_.T
|
|
257
|
+
quad = q_quad[:, None] + s_quad[None, :] - 2.0 * cross
|
|
258
|
+
quad = xp_maximum(quad, 0.0, xp)
|
|
259
|
+
|
|
260
|
+
if use_log_sum_exp:
|
|
261
|
+
if is_gaussian:
|
|
262
|
+
log_kernels = -0.5 * quad
|
|
263
|
+
log_kernels_max = _xp_max(log_kernels, axis=1, keepdims=True)
|
|
264
|
+
log_sum = log_kernels_max[:, 0] + xp.log(
|
|
265
|
+
xp.sum(xp.exp(log_kernels - log_kernels_max) * self.weights_[None, :], axis=1)
|
|
266
|
+
)
|
|
267
|
+
out[start:stop] = xp.exp(log_sum) * self.inv_norm_const_
|
|
268
|
+
else:
|
|
269
|
+
kernels = _kernel_values_from_quad(quad, self.kernel_, xp)
|
|
270
|
+
log_sum = self._log_weighted_kernel_sum(kernels, xp)
|
|
271
|
+
out[start:stop] = xp.where(
|
|
272
|
+
xp.isfinite(log_sum),
|
|
273
|
+
xp.exp(log_sum) * self.inv_norm_const_,
|
|
274
|
+
0.0,
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
kernels = _kernel_values_from_quad(quad, self.kernel_, xp)
|
|
278
|
+
out[start:stop] = (kernels @ self.weights_) * self.inv_norm_const_
|
|
279
|
+
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
def _log_weighted_kernel_sum(self, kernels, xp):
|
|
283
|
+
"""Compute row-wise log(weighted kernel sum) with exact zero-density handling.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
kernels : array-like
|
|
288
|
+
Kernel values for each query/sample pair.
|
|
289
|
+
xp : module
|
|
290
|
+
Backend array module used for the computation.
|
|
291
|
+
|
|
292
|
+
Returns
|
|
293
|
+
-------
|
|
294
|
+
array-like
|
|
295
|
+
Per-row log-sum values, or ``-inf`` when all weighted kernel terms are zero.
|
|
296
|
+
"""
|
|
297
|
+
positive_weight_mask = self.weights_[None, :] > 0.0
|
|
298
|
+
positive_term_mask = (kernels > 0.0) & positive_weight_mask
|
|
299
|
+
safe_kernels = xp.where(positive_term_mask, kernels, 1.0)
|
|
300
|
+
safe_weights = xp.where(positive_weight_mask, self.weights_[None, :], 1.0)
|
|
301
|
+
log_terms = xp.where(
|
|
302
|
+
positive_term_mask,
|
|
303
|
+
xp.log(safe_kernels) + xp.log(safe_weights),
|
|
304
|
+
float("-inf"),
|
|
305
|
+
)
|
|
306
|
+
log_terms_max = _xp_max(log_terms, axis=1, keepdims=True)
|
|
307
|
+
finite_rows = xp.isfinite(log_terms_max[:, 0])
|
|
308
|
+
shifted = xp.where(finite_rows[:, None], log_terms - log_terms_max, float("-inf"))
|
|
309
|
+
return xp.where(
|
|
310
|
+
finite_rows,
|
|
311
|
+
log_terms_max[:, 0] + xp.log(xp.sum(xp.exp(shifted), axis=1)),
|
|
312
|
+
float("-inf"),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def pdf(self, points, *, batch_size: int = 1024):
|
|
316
|
+
self._require_fitted()
|
|
317
|
+
xp = _get_xp(self.backend_)
|
|
318
|
+
points_2d = _as_points_2d(points, self.n_features_, xp, ref_arr=self.samples_)
|
|
319
|
+
result = self._evaluate_density(points_2d, batch_size=int(batch_size), xp=xp)
|
|
320
|
+
self._cleanup_cuda_memory()
|
|
321
|
+
self._cleanup_torch_memory()
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
def _evaluate_log_density(self, points_2d, *, batch_size: int, xp):
|
|
325
|
+
"""Evaluate log-density in log domain (avoids underflow for high dimensions)."""
|
|
326
|
+
if batch_size <= 0:
|
|
327
|
+
raise ValueError("batch_size must be a positive integer")
|
|
328
|
+
|
|
329
|
+
n_points = int(points_2d.shape[0])
|
|
330
|
+
|
|
331
|
+
s_quad = self._samples_quad_
|
|
332
|
+
log_norm = math.log(self.inv_norm_const_) if self.inv_norm_const_ > 0.0 else float("-inf")
|
|
333
|
+
is_gaussian = self.kernel_ == "gaussian"
|
|
334
|
+
|
|
335
|
+
out = xp_empty((n_points,), xp.float64, xp, ref_arr=points_2d)
|
|
336
|
+
|
|
337
|
+
for start in range(0, points_2d.shape[0], int(batch_size)):
|
|
338
|
+
stop = min(start + int(batch_size), points_2d.shape[0])
|
|
339
|
+
q = points_2d[start:stop]
|
|
340
|
+
|
|
341
|
+
q_proj = q @ self.inv_covariance_
|
|
342
|
+
q_quad = xp.sum(q_proj * q, axis=1)
|
|
343
|
+
cross = q_proj @ self.samples_.T
|
|
344
|
+
quad = q_quad[:, None] + s_quad[None, :] - 2.0 * cross
|
|
345
|
+
quad = xp_maximum(quad, 0.0, xp)
|
|
346
|
+
|
|
347
|
+
if is_gaussian:
|
|
348
|
+
log_kernels = -0.5 * quad
|
|
349
|
+
log_kernels_max = _xp_max(log_kernels, axis=1, keepdims=True)
|
|
350
|
+
log_kernels_shifted = log_kernels - log_kernels_max
|
|
351
|
+
log_sum = log_kernels_max[:, 0] + xp.log(
|
|
352
|
+
xp.sum(xp.exp(log_kernels_shifted) * self.weights_[None, :], axis=1)
|
|
353
|
+
)
|
|
354
|
+
out[start:stop] = log_sum + log_norm
|
|
355
|
+
else:
|
|
356
|
+
kernels = _kernel_values_from_quad(quad, self.kernel_, xp)
|
|
357
|
+
log_sum = self._log_weighted_kernel_sum(kernels, xp)
|
|
358
|
+
out[start:stop] = xp.where(
|
|
359
|
+
xp.isfinite(log_sum),
|
|
360
|
+
log_sum + log_norm,
|
|
361
|
+
float("-inf"),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return out
|
|
365
|
+
|
|
366
|
+
def logpdf(self, points, *, batch_size: int = 1024):
|
|
367
|
+
self._require_fitted()
|
|
368
|
+
xp = _get_xp(self.backend_)
|
|
369
|
+
points_2d = _as_points_2d(points, self.n_features_, xp, ref_arr=self.samples_)
|
|
370
|
+
result = self._evaluate_log_density(points_2d, batch_size=int(batch_size), xp=xp)
|
|
371
|
+
self._cleanup_cuda_memory()
|
|
372
|
+
self._cleanup_torch_memory()
|
|
373
|
+
return result
|
|
374
|
+
|
|
375
|
+
def __call__(self, points, *, batch_size: int = 1024):
|
|
376
|
+
return self.pdf(points, batch_size=batch_size)
|
|
377
|
+
|
|
378
|
+
def to_numpy_metadata(self):
|
|
379
|
+
self._require_fitted()
|
|
380
|
+
bandwidth_selection = None
|
|
381
|
+
if hasattr(self.bandwidth_info_, "to_dict"):
|
|
382
|
+
bandwidth_selection = self.bandwidth_info_.to_dict()
|
|
383
|
+
return {
|
|
384
|
+
"bandwidth_factor": float(self.bandwidth_factor_),
|
|
385
|
+
"bandwidth_selection": bandwidth_selection,
|
|
386
|
+
"n_samples": int(self.n_samples_),
|
|
387
|
+
"n_features": int(self.n_features_),
|
|
388
|
+
"backend": self.backend_,
|
|
389
|
+
"kernel": self.kernel_,
|
|
390
|
+
"covariance": _to_numpy(self.covariance_),
|
|
391
|
+
"inv_covariance": _to_numpy(self.inv_covariance_),
|
|
392
|
+
"weights": _to_numpy(self.weights_),
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
def predict(self, X):
|
|
396
|
+
return self.pdf(X)
|
|
397
|
+
|
|
398
|
+
def score_samples(self, X):
|
|
399
|
+
return self.logpdf(X)
|
|
400
|
+
|
|
401
|
+
def score(self, X, y=None):
|
|
402
|
+
vals = self.score_samples(X)
|
|
403
|
+
return float(np.mean(_to_numpy(vals)))
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class KDE(KernelDensityEstimator):
|
|
407
|
+
"""Alias class for KernelDensityEstimator."""
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def fit_kde(
|
|
411
|
+
samples,
|
|
412
|
+
*,
|
|
413
|
+
bandwidth: Union[str, float, int] = "scott",
|
|
414
|
+
weights=None,
|
|
415
|
+
kernel: str = "gaussian",
|
|
416
|
+
backend: str = "auto",
|
|
417
|
+
) -> KDE:
|
|
418
|
+
"""Fit a KDE model.
|
|
419
|
+
|
|
420
|
+
Parameters
|
|
421
|
+
----------
|
|
422
|
+
samples : array-like of shape (n_samples,) or (n_samples, n_features)
|
|
423
|
+
Training observations.
|
|
424
|
+
bandwidth : {'scott', 'silverman', 'nrd0', 'nrd', 'ucv', 'bcv', 'sj', 'sj-ste', 'sj-dpi'} or float, default='scott'
|
|
425
|
+
Bandwidth scaling factor mode or explicit positive factor.
|
|
426
|
+
weights : array-like of shape (n_samples,), optional
|
|
427
|
+
Non-negative sample weights. If omitted, uniform weights are used.
|
|
428
|
+
kernel : {'gaussian', 'rectangular', 'triangular', 'epanechnikov',
|
|
429
|
+
'biweight', 'triweight', 'cosine', 'optcosine'}, default='gaussian'
|
|
430
|
+
Kernel function used for density estimation.
|
|
431
|
+
backend : {'auto', 'numpy', 'cupy'}, default='auto'
|
|
432
|
+
Compute backend. 'auto' selects from the estimator's configured
|
|
433
|
+
device/backend rather than inferring from input array types.
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
-------
|
|
437
|
+
KDE
|
|
438
|
+
Fitted KDE object.
|
|
439
|
+
"""
|
|
440
|
+
model = KDE(
|
|
441
|
+
bandwidth=bandwidth,
|
|
442
|
+
weights=weights,
|
|
443
|
+
kernel=kernel,
|
|
444
|
+
backend=backend,
|
|
445
|
+
)
|
|
446
|
+
return model.fit(samples)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def kde_pdf(
|
|
450
|
+
samples,
|
|
451
|
+
points,
|
|
452
|
+
*,
|
|
453
|
+
bandwidth: Union[str, float, int] = "scott",
|
|
454
|
+
weights=None,
|
|
455
|
+
kernel: str = "gaussian",
|
|
456
|
+
backend: str = "auto",
|
|
457
|
+
return_log: bool = False,
|
|
458
|
+
batch_size: int = 1024,
|
|
459
|
+
):
|
|
460
|
+
"""One-shot Gaussian KDE evaluation.
|
|
461
|
+
|
|
462
|
+
This helper fits a KDE model and evaluates it at `points`.
|
|
463
|
+
"""
|
|
464
|
+
model = fit_kde(
|
|
465
|
+
samples,
|
|
466
|
+
bandwidth=bandwidth,
|
|
467
|
+
weights=weights,
|
|
468
|
+
kernel=kernel,
|
|
469
|
+
backend=backend,
|
|
470
|
+
)
|
|
471
|
+
if return_log:
|
|
472
|
+
return model.logpdf(points, batch_size=batch_size)
|
|
473
|
+
return model.pdf(points, batch_size=batch_size)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@dataclass
|
|
477
|
+
class KDEBootstrapResult:
|
|
478
|
+
"""Pointwise bootstrap confidence intervals for KDE estimates."""
|
|
479
|
+
|
|
480
|
+
points: np.ndarray
|
|
481
|
+
estimate: np.ndarray
|
|
482
|
+
lower: np.ndarray
|
|
483
|
+
upper: np.ndarray
|
|
484
|
+
confidence_level: float
|
|
485
|
+
n_resamples: int
|
|
486
|
+
random_state: Optional[int]
|
|
487
|
+
kernel: str
|
|
488
|
+
backend: str
|
|
489
|
+
metadata: Dict[str, Any]
|
|
490
|
+
bootstrap_samples: Optional[np.ndarray] = None
|
|
491
|
+
|
|
492
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
493
|
+
payload = {
|
|
494
|
+
"points": np.asarray(self.points).tolist(),
|
|
495
|
+
"estimate": np.asarray(self.estimate).tolist(),
|
|
496
|
+
"lower": np.asarray(self.lower).tolist(),
|
|
497
|
+
"upper": np.asarray(self.upper).tolist(),
|
|
498
|
+
"confidence_level": float(self.confidence_level),
|
|
499
|
+
"n_resamples": int(self.n_resamples),
|
|
500
|
+
"random_state": self.random_state,
|
|
501
|
+
"kernel": self.kernel,
|
|
502
|
+
"backend": self.backend,
|
|
503
|
+
"metadata": self.metadata,
|
|
504
|
+
}
|
|
505
|
+
if self.bootstrap_samples is not None:
|
|
506
|
+
payload["bootstrap_samples"] = np.asarray(self.bootstrap_samples).tolist()
|
|
507
|
+
return payload
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _validate_confidence_level(confidence_level: float) -> float:
|
|
511
|
+
level = float(confidence_level)
|
|
512
|
+
if level <= 0.0 or level >= 1.0:
|
|
513
|
+
raise ValueError("confidence_level must be in (0, 1)")
|
|
514
|
+
return level
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _validate_n_resamples(n_resamples: int) -> int:
|
|
518
|
+
n = int(n_resamples)
|
|
519
|
+
if n <= 0:
|
|
520
|
+
raise ValueError("n_resamples must be a positive integer")
|
|
521
|
+
return n
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def kde_bootstrap_confidence_interval(
|
|
525
|
+
samples,
|
|
526
|
+
points,
|
|
527
|
+
*,
|
|
528
|
+
bandwidth: Union[str, float, int] = "scott",
|
|
529
|
+
weights=None,
|
|
530
|
+
kernel: str = "gaussian",
|
|
531
|
+
backend: str = "auto",
|
|
532
|
+
n_resamples: int = 200,
|
|
533
|
+
confidence_level: float = 0.95,
|
|
534
|
+
random_state: Optional[int] = None,
|
|
535
|
+
method: str = "percentile",
|
|
536
|
+
return_bootstrap_samples: bool = False,
|
|
537
|
+
batch_size: int = 1024,
|
|
538
|
+
) -> KDEBootstrapResult:
|
|
539
|
+
"""Backward-compatible bootstrap CI wrapper for KDE.
|
|
540
|
+
|
|
541
|
+
This wrapper preserves the original API and delegates to
|
|
542
|
+
``kde_confidence_interval(method='bootstrap')``.
|
|
543
|
+
"""
|
|
544
|
+
return kde_confidence_interval(
|
|
545
|
+
samples,
|
|
546
|
+
points,
|
|
547
|
+
bandwidth=bandwidth,
|
|
548
|
+
weights=weights,
|
|
549
|
+
kernel=kernel,
|
|
550
|
+
backend=backend,
|
|
551
|
+
n_resamples=n_resamples,
|
|
552
|
+
confidence_level=confidence_level,
|
|
553
|
+
random_state=random_state,
|
|
554
|
+
method="bootstrap",
|
|
555
|
+
bootstrap_method=method,
|
|
556
|
+
return_bootstrap_samples=return_bootstrap_samples,
|
|
557
|
+
batch_size=batch_size,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def kde_confidence_interval(
|
|
562
|
+
samples,
|
|
563
|
+
points,
|
|
564
|
+
*,
|
|
565
|
+
bandwidth: Union[str, float, int] = "scott",
|
|
566
|
+
weights=None,
|
|
567
|
+
kernel: str = "gaussian",
|
|
568
|
+
backend: str = "auto",
|
|
569
|
+
n_resamples: int = 200,
|
|
570
|
+
confidence_level: float = 0.95,
|
|
571
|
+
random_state: Optional[int] = None,
|
|
572
|
+
method: str = "normal",
|
|
573
|
+
bootstrap_method: str = "percentile",
|
|
574
|
+
return_bootstrap_samples: bool = False,
|
|
575
|
+
batch_size: int = 1024,
|
|
576
|
+
) -> KDEBootstrapResult:
|
|
577
|
+
"""Estimate pointwise KDE confidence intervals.
|
|
578
|
+
|
|
579
|
+
Supported methods:
|
|
580
|
+
- ``normal``: asymptotic normal approximation (fast path, 1D Gaussian).
|
|
581
|
+
- ``bootstrap``: non-parametric bootstrap percentile intervals.
|
|
582
|
+
"""
|
|
583
|
+
method_name = str(method).strip().lower()
|
|
584
|
+
if method_name not in ("normal", "bootstrap"):
|
|
585
|
+
raise ValueError("method must be one of: 'normal', 'bootstrap'")
|
|
586
|
+
|
|
587
|
+
bootstrap_method_name = str(bootstrap_method).strip().lower()
|
|
588
|
+
if bootstrap_method_name != "percentile":
|
|
589
|
+
raise ValueError("bootstrap_method must be 'percentile'")
|
|
590
|
+
|
|
591
|
+
level = _validate_confidence_level(confidence_level)
|
|
592
|
+
n_boot = _validate_n_resamples(n_resamples)
|
|
593
|
+
model = fit_kde(
|
|
594
|
+
samples,
|
|
595
|
+
bandwidth=bandwidth,
|
|
596
|
+
weights=weights,
|
|
597
|
+
kernel=kernel,
|
|
598
|
+
backend=backend,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
xp = _get_xp(model.backend_)
|
|
602
|
+
points_2d = _as_points_2d(points, model.n_features_, xp)
|
|
603
|
+
estimate = np.asarray(_to_numpy(model.pdf(points_2d, batch_size=batch_size)), dtype=np.float64)
|
|
604
|
+
|
|
605
|
+
points_np = np.asarray(_to_numpy(points_2d), dtype=np.float64)
|
|
606
|
+
if model.n_features_ == 1 and points_np.ndim == 2 and int(points_np.shape[1]) == 1:
|
|
607
|
+
points_np = points_np.reshape(-1)
|
|
608
|
+
|
|
609
|
+
if method_name == "normal":
|
|
610
|
+
if model.n_features_ != 1 or model.kernel_ != "gaussian":
|
|
611
|
+
raise ValueError("method='normal' currently supports only 1D Gaussian KDE")
|
|
612
|
+
|
|
613
|
+
n_eff = float(_effective_sample_size(model.weights_, xp))
|
|
614
|
+
if (not np.isfinite(n_eff)) or n_eff <= 0.0:
|
|
615
|
+
raise ValueError("effective sample size must be finite and positive")
|
|
616
|
+
|
|
617
|
+
cov11 = float(_to_float_scalar(model.covariance_[0, 0]))
|
|
618
|
+
if (not np.isfinite(cov11)) or cov11 <= 0.0:
|
|
619
|
+
raise ValueError("covariance must be positive for normal CI")
|
|
620
|
+
|
|
621
|
+
h = math.sqrt(cov11)
|
|
622
|
+
if (not np.isfinite(h)) or h <= 0.0:
|
|
623
|
+
raise ValueError("bandwidth scale must be positive for normal CI")
|
|
624
|
+
|
|
625
|
+
r_kernel = 1.0 / (2.0 * math.sqrt(math.pi))
|
|
626
|
+
var = np.maximum(estimate, 0.0) * (r_kernel / (n_eff * h))
|
|
627
|
+
var = np.maximum(var, 0.0)
|
|
628
|
+
se = np.sqrt(var)
|
|
629
|
+
|
|
630
|
+
z = float(NormalDist().inv_cdf(0.5 + 0.5 * level))
|
|
631
|
+
lower = np.maximum(estimate - z * se, 0.0)
|
|
632
|
+
upper = estimate + z * se
|
|
633
|
+
|
|
634
|
+
return KDEBootstrapResult(
|
|
635
|
+
points=points_np,
|
|
636
|
+
estimate=estimate,
|
|
637
|
+
lower=lower,
|
|
638
|
+
upper=upper,
|
|
639
|
+
confidence_level=level,
|
|
640
|
+
n_resamples=0,
|
|
641
|
+
random_state=random_state,
|
|
642
|
+
kernel=_normalize_kernel_name(kernel),
|
|
643
|
+
backend=model.backend_,
|
|
644
|
+
metadata={
|
|
645
|
+
"method": method_name,
|
|
646
|
+
"bandwidth": bandwidth,
|
|
647
|
+
"batch_size": int(batch_size),
|
|
648
|
+
"n_features": int(model.n_features_),
|
|
649
|
+
"n_eff": float(n_eff),
|
|
650
|
+
},
|
|
651
|
+
bootstrap_samples=None,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
samples_np = np.asarray(_to_numpy(_as_samples_2d(samples, xp)), dtype=np.float64)
|
|
655
|
+
n_samples = int(samples_np.shape[0])
|
|
656
|
+
|
|
657
|
+
weights_np = np.asarray(_to_numpy(_normalize_weights(weights, n_samples, xp)), dtype=np.float64)
|
|
658
|
+
rng = np.random.default_rng(random_state)
|
|
659
|
+
boot_samples = np.empty((n_boot, estimate.size), dtype=np.float64)
|
|
660
|
+
|
|
661
|
+
use_fast_1d_numpy = (
|
|
662
|
+
(xp is np)
|
|
663
|
+
and (model.n_features_ == 1)
|
|
664
|
+
and (model.kernel_ == "gaussian")
|
|
665
|
+
and np.isfinite(float(model.bandwidth_factor_))
|
|
666
|
+
and (float(model.bandwidth_factor_) > 0.0)
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
if use_fast_1d_numpy:
|
|
670
|
+
samples_1d = samples_np.reshape(-1)
|
|
671
|
+
points_1d = points_np.reshape(-1)
|
|
672
|
+
bw_factor = float(model.bandwidth_factor_)
|
|
673
|
+
sqrt_2pi = math.sqrt(2.0 * math.pi)
|
|
674
|
+
|
|
675
|
+
for i in range(n_boot):
|
|
676
|
+
idx = rng.choice(n_samples, size=n_samples, replace=True, p=weights_np)
|
|
677
|
+
sampled_x = samples_1d[idx]
|
|
678
|
+
sampled_w = weights_np[idx]
|
|
679
|
+
|
|
680
|
+
sampled_w_sum = float(np.sum(sampled_w))
|
|
681
|
+
if sampled_w_sum <= 0.0:
|
|
682
|
+
raise ValueError("bootstrap weights must sum to a positive value")
|
|
683
|
+
sampled_w = sampled_w / sampled_w_sum
|
|
684
|
+
|
|
685
|
+
mean = float(np.sum(sampled_w * sampled_x))
|
|
686
|
+
centered = sampled_x - mean
|
|
687
|
+
denom = 1.0 - float(np.sum(sampled_w * sampled_w))
|
|
688
|
+
if denom <= 1e-15:
|
|
689
|
+
raise ValueError("effective degrees of freedom is too small for covariance estimation")
|
|
690
|
+
|
|
691
|
+
var = float(np.sum(sampled_w * (centered * centered)) / denom)
|
|
692
|
+
if (not np.isfinite(var)) or var <= 0.0:
|
|
693
|
+
var = float(np.finfo(np.float64).tiny)
|
|
694
|
+
jitter = max(var * 1e-12, 1e-12)
|
|
695
|
+
var = var + jitter
|
|
696
|
+
|
|
697
|
+
scaled_var = var * (bw_factor * bw_factor)
|
|
698
|
+
inv_scalar = 1.0 / scaled_var
|
|
699
|
+
inv_norm_const = 1.0 / (math.sqrt(scaled_var) * sqrt_2pi)
|
|
700
|
+
|
|
701
|
+
diff = points_1d[:, None] - sampled_x[None, :]
|
|
702
|
+
diff *= diff
|
|
703
|
+
diff *= (-0.5 * inv_scalar)
|
|
704
|
+
np.exp(diff, out=diff)
|
|
705
|
+
boot_samples[i, :] = (diff @ sampled_w) * inv_norm_const
|
|
706
|
+
else:
|
|
707
|
+
for i in range(n_boot):
|
|
708
|
+
idx = rng.choice(n_samples, size=n_samples, replace=True, p=weights_np)
|
|
709
|
+
sampled_data = samples_np[idx]
|
|
710
|
+
sampled_weights = weights_np[idx]
|
|
711
|
+
sampled_weight_sum = float(np.sum(sampled_weights))
|
|
712
|
+
if sampled_weight_sum <= 0.0:
|
|
713
|
+
raise ValueError("bootstrap weights must sum to a positive value")
|
|
714
|
+
sampled_weights = sampled_weights / sampled_weight_sum
|
|
715
|
+
|
|
716
|
+
sampled_data_backend = sampled_data if xp is np else xp_asarray(sampled_data, dtype=xp.float64, xp=xp, ref_arr=points_2d)
|
|
717
|
+
sampled_weights_backend = sampled_weights if xp is np else xp_asarray(sampled_weights, dtype=xp.float64, xp=xp, ref_arr=points_2d)
|
|
718
|
+
|
|
719
|
+
boot_model = fit_kde(
|
|
720
|
+
sampled_data_backend,
|
|
721
|
+
bandwidth=bandwidth,
|
|
722
|
+
weights=sampled_weights_backend,
|
|
723
|
+
kernel=kernel,
|
|
724
|
+
backend=model.backend_,
|
|
725
|
+
)
|
|
726
|
+
boot_samples[i, :] = np.asarray(_to_numpy(boot_model.pdf(points_2d, batch_size=batch_size)), dtype=np.float64)
|
|
727
|
+
|
|
728
|
+
alpha = 1.0 - level
|
|
729
|
+
lower = np.quantile(boot_samples, alpha / 2.0, axis=0)
|
|
730
|
+
upper = np.quantile(boot_samples, 1.0 - alpha / 2.0, axis=0)
|
|
731
|
+
|
|
732
|
+
return KDEBootstrapResult(
|
|
733
|
+
points=points_np,
|
|
734
|
+
estimate=estimate,
|
|
735
|
+
lower=lower,
|
|
736
|
+
upper=upper,
|
|
737
|
+
confidence_level=level,
|
|
738
|
+
n_resamples=n_boot,
|
|
739
|
+
random_state=random_state,
|
|
740
|
+
kernel=_normalize_kernel_name(kernel),
|
|
741
|
+
backend=model.backend_,
|
|
742
|
+
metadata={
|
|
743
|
+
"method": method_name,
|
|
744
|
+
"bootstrap_method": bootstrap_method_name,
|
|
745
|
+
"bandwidth": bandwidth,
|
|
746
|
+
"batch_size": int(batch_size),
|
|
747
|
+
"n_features": int(model.n_features_),
|
|
748
|
+
},
|
|
749
|
+
bootstrap_samples=boot_samples if return_bootstrap_samples else None,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
__all__ = [
|
|
754
|
+
"KernelDensityEstimator",
|
|
755
|
+
"KDE",
|
|
756
|
+
"KDEBootstrapResult",
|
|
757
|
+
"fit_kde",
|
|
758
|
+
"kde_pdf",
|
|
759
|
+
"kde_confidence_interval",
|
|
760
|
+
"kde_bootstrap_confidence_interval",
|
|
761
|
+
]
|