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,391 @@
|
|
|
1
|
+
"""Multiple-testing utilities (FDR/FWER p-value adjustments)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from statgpu.backends import get_backend, _resolve_backend, _to_float_scalar
|
|
10
|
+
from statgpu.inference._distributions_backend import chi2, norm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _to_bool_scalar(x) -> bool:
|
|
14
|
+
if hasattr(x, "item"):
|
|
15
|
+
return bool(x.item())
|
|
16
|
+
return bool(x)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_axis_index(axis, ndim):
|
|
20
|
+
try:
|
|
21
|
+
return int(np._core.numeric.normalize_axis_index(axis, ndim))
|
|
22
|
+
except AttributeError:
|
|
23
|
+
return int(np.core.numeric.normalize_axis_index(axis, ndim))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_METHOD_ALIASES = {
|
|
27
|
+
"bh": "bh",
|
|
28
|
+
"fdr_bh": "bh",
|
|
29
|
+
"benjamini-hochberg": "bh",
|
|
30
|
+
"benjamini_hochberg": "bh",
|
|
31
|
+
"by": "by",
|
|
32
|
+
"fdr_by": "by",
|
|
33
|
+
"benjamini-yekutieli": "by",
|
|
34
|
+
"benjamini_yekutieli": "by",
|
|
35
|
+
"holm": "holm",
|
|
36
|
+
"holm-bonferroni": "holm",
|
|
37
|
+
"holm_bonferroni": "holm",
|
|
38
|
+
"bonferroni": "bonferroni",
|
|
39
|
+
"bonf": "bonferroni",
|
|
40
|
+
"hochberg": "hochberg",
|
|
41
|
+
"fdr_hochberg": "hochberg",
|
|
42
|
+
"step_up": "hochberg",
|
|
43
|
+
"stepup": "hochberg",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_COMBINE_METHOD_ALIASES = {
|
|
48
|
+
"fisher": "fisher",
|
|
49
|
+
"fisher-combination": "fisher",
|
|
50
|
+
"fisher_combination": "fisher",
|
|
51
|
+
"cauchy": "cauchy",
|
|
52
|
+
"cauchy-combination": "cauchy",
|
|
53
|
+
"cauchy_combination": "cauchy",
|
|
54
|
+
"acat": "cauchy",
|
|
55
|
+
"stouffer": "stouffer",
|
|
56
|
+
"z-test": "stouffer",
|
|
57
|
+
"ztest": "stouffer",
|
|
58
|
+
"weighted_z": "stouffer",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _normalize_method(method: str) -> str:
|
|
63
|
+
key = str(method).strip().lower()
|
|
64
|
+
if key not in _METHOD_ALIASES:
|
|
65
|
+
allowed = sorted(set(_METHOD_ALIASES.values()))
|
|
66
|
+
raise ValueError(f"Unknown method='{method}'. Supported methods: {allowed}")
|
|
67
|
+
return _METHOD_ALIASES[key]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _normalize_combine_method(method: str) -> str:
|
|
71
|
+
key = str(method).strip().lower()
|
|
72
|
+
if key not in _COMBINE_METHOD_ALIASES:
|
|
73
|
+
allowed = sorted(set(_COMBINE_METHOD_ALIASES.values()))
|
|
74
|
+
raise ValueError(f"Unknown method='{method}'. Supported methods: {allowed}")
|
|
75
|
+
return _COMBINE_METHOD_ALIASES[key]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validate_alpha(alpha: float) -> float:
|
|
79
|
+
alpha_f = float(alpha)
|
|
80
|
+
if alpha_f <= 0.0 or alpha_f >= 1.0:
|
|
81
|
+
raise ValueError("alpha must be in (0, 1)")
|
|
82
|
+
return alpha_f
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _validate_1d_pvalues(pvalues, backend):
|
|
86
|
+
p = backend.asarray(pvalues, dtype=backend.float64).reshape(-1)
|
|
87
|
+
if _to_bool_scalar(backend.xp.any(~backend.xp.isfinite(p))):
|
|
88
|
+
raise ValueError("pvalues must be finite")
|
|
89
|
+
if _to_bool_scalar(backend.xp.any((p < 0.0) | (p > 1.0))):
|
|
90
|
+
raise ValueError("pvalues must be within [0, 1]")
|
|
91
|
+
return p
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_pvalues_array(arr, backend):
|
|
95
|
+
p = backend.asarray(arr, dtype=backend.float64)
|
|
96
|
+
if _to_bool_scalar(backend.xp.any(~backend.xp.isfinite(p))):
|
|
97
|
+
raise ValueError("pvalues must be finite")
|
|
98
|
+
if _to_bool_scalar(backend.xp.any((p < 0.0) | (p > 1.0))):
|
|
99
|
+
raise ValueError("pvalues must be within [0, 1]")
|
|
100
|
+
return p
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _adjust_1d_pvalues(pvalues_1d, method: str, backend):
|
|
104
|
+
m = int(pvalues_1d.shape[0])
|
|
105
|
+
if m == 0:
|
|
106
|
+
return backend.asarray([], dtype=backend.float64)
|
|
107
|
+
|
|
108
|
+
order = backend.xp.argsort(pvalues_1d)
|
|
109
|
+
p_sorted = pvalues_1d[order]
|
|
110
|
+
|
|
111
|
+
if method == "bonferroni":
|
|
112
|
+
adj_sorted = backend.minimum(p_sorted * m, 1.0)
|
|
113
|
+
elif method == "holm":
|
|
114
|
+
factors = m - backend.arange(m, dtype=backend.float64)
|
|
115
|
+
raw = factors * p_sorted
|
|
116
|
+
adj_sorted = backend.cummax(raw)
|
|
117
|
+
adj_sorted = backend.minimum(adj_sorted, 1.0)
|
|
118
|
+
elif method == "bh":
|
|
119
|
+
ranks = backend.arange(1.0, m + 1.0)
|
|
120
|
+
raw = p_sorted * m / ranks
|
|
121
|
+
adj_sorted = backend.flip(backend.cummin(backend.flip(raw, 0)), 0)
|
|
122
|
+
adj_sorted = backend.minimum(adj_sorted, 1.0)
|
|
123
|
+
elif method == "by":
|
|
124
|
+
ranks = backend.arange(1.0, m + 1.0)
|
|
125
|
+
c_m = backend.xp.sum(1.0 / ranks)
|
|
126
|
+
raw = p_sorted * m * c_m / ranks
|
|
127
|
+
adj_sorted = backend.flip(backend.cummin(backend.flip(raw, 0)), 0)
|
|
128
|
+
adj_sorted = backend.minimum(adj_sorted, 1.0)
|
|
129
|
+
elif method == "hochberg":
|
|
130
|
+
factors = backend.arange(m, 0, -1, dtype=backend.float64)
|
|
131
|
+
raw = p_sorted * factors
|
|
132
|
+
adj_sorted = backend.flip(backend.cummin(backend.flip(raw, 0)), 0)
|
|
133
|
+
adj_sorted = backend.minimum(adj_sorted, 1.0)
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(f"Unsupported normalized method: {method}")
|
|
136
|
+
|
|
137
|
+
adj = backend.xp.empty_like(adj_sorted)
|
|
138
|
+
adj[order] = adj_sorted
|
|
139
|
+
return adj
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_weights(weights, m: int, backend):
|
|
143
|
+
if weights is None:
|
|
144
|
+
return backend.full((m,), 1.0 / m, dtype=backend.float64)
|
|
145
|
+
|
|
146
|
+
w = backend.asarray(weights, dtype=backend.float64).reshape(-1)
|
|
147
|
+
if int(w.shape[0]) != int(m):
|
|
148
|
+
raise ValueError("weights must be 1D and have the same length as the combine axis")
|
|
149
|
+
if _to_bool_scalar(backend.xp.any(~backend.xp.isfinite(w))):
|
|
150
|
+
raise ValueError("weights must be finite")
|
|
151
|
+
if _to_bool_scalar(backend.xp.any(w < 0.0)):
|
|
152
|
+
raise ValueError("weights must be non-negative")
|
|
153
|
+
|
|
154
|
+
w_sum = backend.xp.sum(w)
|
|
155
|
+
if _to_float_scalar(w_sum) <= 0.0:
|
|
156
|
+
raise ValueError("weights must sum to a positive value")
|
|
157
|
+
return w / w_sum
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _combine_1d_fisher(pvalues_1d, backend):
|
|
163
|
+
p = _validate_1d_pvalues(pvalues_1d, backend)
|
|
164
|
+
m = int(p.shape[0])
|
|
165
|
+
if m == 0:
|
|
166
|
+
raise ValueError("pvalues must contain at least one value")
|
|
167
|
+
|
|
168
|
+
# Avoid log(0) while keeping statistical meaning for very small p-values.
|
|
169
|
+
eps = np.finfo(np.float64).tiny
|
|
170
|
+
p_safe = backend.xp.clip(backend.astype(p, backend.float64), eps, 1.0)
|
|
171
|
+
statistic = -2.0 * backend.xp.sum(backend.xp.log(p_safe))
|
|
172
|
+
|
|
173
|
+
pvalue = chi2.sf(statistic, df=2 * m)
|
|
174
|
+
return backend.astype(statistic, backend.float64), backend.astype(pvalue, backend.float64)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _combine_1d_cauchy(pvalues_1d, weights, backend):
|
|
178
|
+
p = _validate_1d_pvalues(pvalues_1d, backend)
|
|
179
|
+
m = int(p.shape[0])
|
|
180
|
+
if m == 0:
|
|
181
|
+
raise ValueError("pvalues must contain at least one value")
|
|
182
|
+
|
|
183
|
+
w = _validate_weights(weights, m, backend)
|
|
184
|
+
|
|
185
|
+
eps = np.finfo(np.float64).eps
|
|
186
|
+
p_safe = backend.xp.clip(backend.astype(p, backend.float64), eps, 1.0 - eps)
|
|
187
|
+
statistic = backend.xp.sum(w * backend.xp.tan((0.5 - p_safe) * np.pi))
|
|
188
|
+
pvalue = 0.5 - backend.xp.arctan(statistic) / np.pi
|
|
189
|
+
pvalue = backend.xp.clip(pvalue, 0.0, 1.0)
|
|
190
|
+
return backend.astype(statistic, backend.float64), backend.astype(pvalue, backend.float64)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _combine_1d_stouffer(pvalues_1d, weights, backend):
|
|
194
|
+
p = _validate_1d_pvalues(pvalues_1d, backend)
|
|
195
|
+
m = int(p.shape[0])
|
|
196
|
+
if m == 0:
|
|
197
|
+
raise ValueError("pvalues must contain at least one value")
|
|
198
|
+
|
|
199
|
+
w = _validate_weights(weights, m, backend)
|
|
200
|
+
|
|
201
|
+
eps = np.finfo(np.float64).eps
|
|
202
|
+
p_safe = backend.xp.clip(backend.astype(p, backend.float64), eps, 1.0 - eps)
|
|
203
|
+
z_scores = norm.ppf(1.0 - p_safe)
|
|
204
|
+
z = backend.xp.sum(w * z_scores) / backend.xp.sqrt(backend.xp.sum(w * w))
|
|
205
|
+
pvalue = norm.sf(z)
|
|
206
|
+
pvalue = backend.xp.clip(pvalue, 0.0, 1.0)
|
|
207
|
+
return backend.astype(z, backend.float64), backend.astype(pvalue, backend.float64)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _combine_1d_pvalues(pvalues_1d, method: str, weights, backend):
|
|
211
|
+
if method == "fisher":
|
|
212
|
+
if weights is not None:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"weights are only supported for method='cauchy' or method='stouffer'"
|
|
215
|
+
)
|
|
216
|
+
return _combine_1d_fisher(pvalues_1d, backend)
|
|
217
|
+
if method == "cauchy":
|
|
218
|
+
return _combine_1d_cauchy(pvalues_1d, weights, backend)
|
|
219
|
+
if method == "stouffer":
|
|
220
|
+
return _combine_1d_stouffer(pvalues_1d, weights, backend)
|
|
221
|
+
raise ValueError(f"Unsupported normalized combine method: {method}")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def adjust_pvalues(
|
|
225
|
+
pvalues,
|
|
226
|
+
method: str = "bh",
|
|
227
|
+
alpha: float = 0.05,
|
|
228
|
+
axis: Optional[int] = None,
|
|
229
|
+
backend: str = "auto",
|
|
230
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
231
|
+
"""
|
|
232
|
+
Adjust p-values for multiple testing.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
pvalues : array-like
|
|
237
|
+
Raw p-values.
|
|
238
|
+
method : str, default='bh'
|
|
239
|
+
One of: 'bh', 'by', 'holm', 'bonferroni', 'hochberg'.
|
|
240
|
+
Common aliases are accepted (e.g., 'fdr_bh', 'bonf', 'step_up').
|
|
241
|
+
alpha : float, default=0.05
|
|
242
|
+
Rejection threshold in (0, 1).
|
|
243
|
+
axis : int or None, default=None
|
|
244
|
+
Axis along which to adjust p-values.
|
|
245
|
+
If None, adjusts over all values flattened.
|
|
246
|
+
backend : {'auto', 'numpy', 'cupy', 'torch'}, default='auto'
|
|
247
|
+
Compute backend. ``'auto'`` infers from input array type.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
reject : ndarray of bool
|
|
252
|
+
Rejection mask for adjusted p-values at level ``alpha``.
|
|
253
|
+
pvalues_adjusted : ndarray of float
|
|
254
|
+
Adjusted p-values with same shape as input.
|
|
255
|
+
"""
|
|
256
|
+
method_n = _normalize_method(method)
|
|
257
|
+
alpha_f = _validate_alpha(alpha)
|
|
258
|
+
backend_name = _resolve_backend(backend, pvalues)
|
|
259
|
+
backend = get_backend(backend_name)
|
|
260
|
+
|
|
261
|
+
arr = backend.asarray(pvalues, dtype=backend.float64)
|
|
262
|
+
|
|
263
|
+
if axis is None:
|
|
264
|
+
flat = _validate_1d_pvalues(arr, backend)
|
|
265
|
+
adj_flat = _adjust_1d_pvalues(flat, method_n, backend)
|
|
266
|
+
reject_flat = adj_flat <= alpha_f
|
|
267
|
+
return reject_flat.reshape(arr.shape), adj_flat.reshape(arr.shape)
|
|
268
|
+
|
|
269
|
+
if arr.ndim == 0:
|
|
270
|
+
raise ValueError("axis must be None for scalar pvalues")
|
|
271
|
+
|
|
272
|
+
axis_n = _normalize_axis_index(axis, arr.ndim)
|
|
273
|
+
moved = backend.xp.moveaxis(arr, axis_n, -1)
|
|
274
|
+
matrix = moved.reshape(-1, moved.shape[-1])
|
|
275
|
+
|
|
276
|
+
adj_matrix = backend.xp.empty_like(matrix, dtype=backend.float64)
|
|
277
|
+
reject_matrix = backend.xp.empty_like(matrix, dtype=bool)
|
|
278
|
+
|
|
279
|
+
for i in range(matrix.shape[0]):
|
|
280
|
+
row = _validate_1d_pvalues(matrix[i], backend)
|
|
281
|
+
adj_row = _adjust_1d_pvalues(row, method_n, backend)
|
|
282
|
+
adj_matrix[i] = adj_row
|
|
283
|
+
reject_matrix[i] = adj_row <= alpha_f
|
|
284
|
+
|
|
285
|
+
adj_moved = adj_matrix.reshape(moved.shape)
|
|
286
|
+
reject_moved = reject_matrix.reshape(moved.shape)
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
backend.xp.moveaxis(reject_moved, -1, axis_n),
|
|
290
|
+
backend.xp.moveaxis(adj_moved, -1, axis_n),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def combine_pvalues(
|
|
295
|
+
pvalues,
|
|
296
|
+
method: str = "fisher",
|
|
297
|
+
weights=None,
|
|
298
|
+
axis: Optional[int] = None,
|
|
299
|
+
backend: str = "auto",
|
|
300
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
301
|
+
"""
|
|
302
|
+
Combine p-values into a global p-value.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
pvalues : array-like
|
|
307
|
+
Raw p-values to combine.
|
|
308
|
+
method : {'fisher', 'cauchy', 'stouffer'}, default='fisher'
|
|
309
|
+
Combination method. Aliases accepted (e.g., 'acat').
|
|
310
|
+
weights : array-like, optional
|
|
311
|
+
Optional non-negative weights for method='cauchy' or 'stouffer'.
|
|
312
|
+
axis : int or None, default=None
|
|
313
|
+
Axis along which to combine p-values. If None, flattens all values.
|
|
314
|
+
backend : {'auto', 'numpy', 'cupy', 'torch'}, default='auto'
|
|
315
|
+
Compute backend. ``'auto'`` infers from input array type.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
statistic : ndarray or scalar
|
|
320
|
+
Combined test statistic.
|
|
321
|
+
pvalue : ndarray or scalar
|
|
322
|
+
Combined p-value(s).
|
|
323
|
+
"""
|
|
324
|
+
method_n = _normalize_combine_method(method)
|
|
325
|
+
backend_name = _resolve_backend(backend, pvalues)
|
|
326
|
+
backend = get_backend(backend_name)
|
|
327
|
+
|
|
328
|
+
arr = backend.asarray(pvalues, dtype=backend.float64)
|
|
329
|
+
|
|
330
|
+
if axis is None:
|
|
331
|
+
flat = _validate_1d_pvalues(arr, backend)
|
|
332
|
+
return _combine_1d_pvalues(flat, method_n, weights, backend)
|
|
333
|
+
|
|
334
|
+
if arr.ndim == 0:
|
|
335
|
+
raise ValueError("axis must be None for scalar pvalues")
|
|
336
|
+
|
|
337
|
+
arr = _validate_pvalues_array(arr, backend)
|
|
338
|
+
axis_n = _normalize_axis_index(axis, arr.ndim)
|
|
339
|
+
moved = backend.xp.moveaxis(arr, axis_n, -1)
|
|
340
|
+
m = int(moved.shape[-1])
|
|
341
|
+
if m == 0:
|
|
342
|
+
raise ValueError("pvalues must contain at least one value")
|
|
343
|
+
|
|
344
|
+
if method_n == "fisher":
|
|
345
|
+
if weights is not None:
|
|
346
|
+
raise ValueError("weights are only supported for method='cauchy' or method='stouffer'")
|
|
347
|
+
eps = np.finfo(np.float64).tiny
|
|
348
|
+
p_safe = backend.xp.clip(backend.astype(moved, backend.float64), eps, 1.0)
|
|
349
|
+
statistics = -2.0 * backend.xp.sum(backend.xp.log(p_safe), axis=-1)
|
|
350
|
+
pvals = chi2.sf(statistics, df=2 * m)
|
|
351
|
+
return backend.astype(statistics, backend.float64), backend.astype(pvals, backend.float64)
|
|
352
|
+
|
|
353
|
+
if method_n == "cauchy":
|
|
354
|
+
w = _validate_weights(weights, m, backend)
|
|
355
|
+
eps = np.finfo(np.float64).eps
|
|
356
|
+
p_safe = backend.xp.clip(backend.astype(moved, backend.float64), eps, 1.0 - eps)
|
|
357
|
+
w_shape = (1,) * (p_safe.ndim - 1) + (m,)
|
|
358
|
+
statistics = backend.xp.sum(w.reshape(w_shape) * backend.xp.tan((0.5 - p_safe) * np.pi), axis=-1)
|
|
359
|
+
pvals = 0.5 - backend.xp.arctan(statistics) / np.pi
|
|
360
|
+
pvals = backend.xp.clip(pvals, 0.0, 1.0)
|
|
361
|
+
return backend.astype(statistics, backend.float64), backend.astype(pvals, backend.float64)
|
|
362
|
+
|
|
363
|
+
if method_n == "stouffer":
|
|
364
|
+
w = _validate_weights(weights, m, backend)
|
|
365
|
+
eps = np.finfo(np.float64).eps
|
|
366
|
+
p_safe = backend.xp.clip(backend.astype(moved, backend.float64), eps, 1.0 - eps)
|
|
367
|
+
z_scores = norm.ppf(1.0 - p_safe)
|
|
368
|
+
w_shape = (1,) * (p_safe.ndim - 1) + (m,)
|
|
369
|
+
statistics = backend.xp.sum(w.reshape(w_shape) * z_scores, axis=-1) / backend.xp.sqrt(backend.xp.sum(w * w))
|
|
370
|
+
pvals = norm.sf(statistics)
|
|
371
|
+
pvals = backend.xp.clip(pvals, 0.0, 1.0)
|
|
372
|
+
return backend.astype(statistics, backend.float64), backend.astype(pvals, backend.float64)
|
|
373
|
+
|
|
374
|
+
raise ValueError(f"Unsupported normalized combine method: {method_n}")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def multipletests(
|
|
378
|
+
pvalues,
|
|
379
|
+
alpha: float = 0.05,
|
|
380
|
+
method: str = "bh",
|
|
381
|
+
axis: Optional[int] = None,
|
|
382
|
+
backend: str = "auto",
|
|
383
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
384
|
+
"""Alias compatible with common scientific naming."""
|
|
385
|
+
return adjust_pvalues(
|
|
386
|
+
pvalues,
|
|
387
|
+
method=method,
|
|
388
|
+
alpha=alpha,
|
|
389
|
+
axis=axis,
|
|
390
|
+
backend=backend,
|
|
391
|
+
)
|