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,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
+ )