structboost 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.
- structboost/__init__.py +94 -0
- structboost/_annotation.py +266 -0
- structboost/_boosting.py +552 -0
- structboost/_decoder.py +109 -0
- structboost/_encoder.py +88 -0
- structboost/_explorer.py +842 -0
- structboost/_io.py +302 -0
- structboost/_model.py +3483 -0
- structboost/_persistence.py +326 -0
- structboost/_plotting.py +301 -0
- structboost/_simulation.py +867 -0
- structboost/_stability.py +412 -0
- structboost/_types.py +393 -0
- structboost/_utils.py +509 -0
- structboost/py.typed +0 -0
- structboost-0.1.0.dist-info/METADATA +219 -0
- structboost-0.1.0.dist-info/RECORD +19 -0
- structboost-0.1.0.dist-info/WHEEL +4 -0
- structboost-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Stability selection for boosting-based gene selection.
|
|
2
|
+
|
|
3
|
+
Implements Meinshausen & Bühlmann (2010) stability selection wrapped around
|
|
4
|
+
``allboost``. The selector is re-run on many random subsamples of the cells and a
|
|
5
|
+
gene is called *stable* for a latent dimension when it is selected in at least a
|
|
6
|
+
fraction ``threshold`` of the subsamples. This turns a single, seed-dependent
|
|
7
|
+
gene list into a per-gene selection frequency plus a bound on the expected number
|
|
8
|
+
of false selections.
|
|
9
|
+
|
|
10
|
+
Following the paper, subsamples are drawn of size ``floor(subsample_frac * n)``
|
|
11
|
+
**without replacement** (``subsample_frac=0.5`` is the value the theory is derived
|
|
12
|
+
for). Bootstrap resampling is deliberately not offered: sampling with replacement
|
|
13
|
+
breaks the exchangeability the error bound relies on.
|
|
14
|
+
|
|
15
|
+
Reference
|
|
16
|
+
---------
|
|
17
|
+
Meinshausen, N. & Bühlmann, P. (2010). Stability selection. *Journal of the Royal
|
|
18
|
+
Statistical Society: Series B*, 72(4), 417-473.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import warnings
|
|
24
|
+
from collections.abc import Iterable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
from numpy.typing import NDArray
|
|
29
|
+
|
|
30
|
+
from ._boosting import allboost
|
|
31
|
+
|
|
32
|
+
#: Boolean mask array. Named rather than spelled ``NDArray[np.bool_]`` inline:
|
|
33
|
+
#: the trailing underscore in ``np.bool_`` is valid Python but reads as reference
|
|
34
|
+
#: syntax to the documentation builder, which then reports a broken target on
|
|
35
|
+
#: every page that renders the annotation.
|
|
36
|
+
BoolArray = NDArray[np.bool_]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _mandatory_gene_counts(
|
|
40
|
+
mandatory_features: NDArray[np.intp] | list[NDArray[np.intp]] | None,
|
|
41
|
+
latent_dim: int,
|
|
42
|
+
p_genes: int,
|
|
43
|
+
) -> NDArray[np.float64]:
|
|
44
|
+
"""Per-dimension count of forced features that fall in the gene columns.
|
|
45
|
+
|
|
46
|
+
Mirrors ``allboost``'s ``mandatory_features`` shapes: ``None``, a single array
|
|
47
|
+
applied to every target, or one array per target. Only indices ``< p_genes``
|
|
48
|
+
are counted (nuisance columns beyond the genes are already excluded elsewhere).
|
|
49
|
+
"""
|
|
50
|
+
counts = np.zeros(latent_dim, dtype=np.float64)
|
|
51
|
+
if mandatory_features is None:
|
|
52
|
+
return counts
|
|
53
|
+
if isinstance(mandatory_features, list):
|
|
54
|
+
for j, mand in enumerate(mandatory_features):
|
|
55
|
+
counts[j] = int((np.asarray(mand) < p_genes).sum())
|
|
56
|
+
else:
|
|
57
|
+
counts[:] = int((np.asarray(mandatory_features) < p_genes).sum())
|
|
58
|
+
return counts
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _coefficient_statistics(
|
|
62
|
+
coef_sum: NDArray[np.float64],
|
|
63
|
+
coef_sq_sum: NDArray[np.float64],
|
|
64
|
+
positive_count: NDArray[np.float64],
|
|
65
|
+
counts: NDArray[np.float64],
|
|
66
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
67
|
+
"""Conditional mean, sd and sign consistency from streaming accumulators.
|
|
68
|
+
|
|
69
|
+
All three condition on the runs in which an entry was actually selected, so an
|
|
70
|
+
entry selected twice out of a hundred reports the spread of those two draws
|
|
71
|
+
rather than being diluted by ninety-eight zeros.
|
|
72
|
+
"""
|
|
73
|
+
safe = np.maximum(counts, 1.0)
|
|
74
|
+
cond_mean = np.divide(coef_sum, safe, out=np.zeros_like(coef_sum), where=counts > 0)
|
|
75
|
+
variance = np.maximum(coef_sq_sum / safe - cond_mean**2, 0.0)
|
|
76
|
+
cond_sd = np.where(counts > 1, np.sqrt(variance), 0.0)
|
|
77
|
+
modal = np.maximum(positive_count, counts - positive_count)
|
|
78
|
+
sign_consistency = np.divide(modal, safe, out=np.ones_like(coef_sum), where=counts > 0)
|
|
79
|
+
return cond_mean, cond_sd, sign_consistency
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class StabilitySelectionResult:
|
|
84
|
+
"""Result of :func:`structboost.stability_selection`.
|
|
85
|
+
|
|
86
|
+
Attributes
|
|
87
|
+
----------
|
|
88
|
+
frequency
|
|
89
|
+
Per-gene, per-dimension selection frequency, shape ``(n_genes, latent_dim)``.
|
|
90
|
+
Entry ``(g, j)`` is the fraction of subsamples in which gene ``g`` received
|
|
91
|
+
a nonzero coefficient for latent dimension ``j``.
|
|
92
|
+
stable_support
|
|
93
|
+
Boolean mask ``frequency >= threshold``, shape ``(n_genes, latent_dim)``.
|
|
94
|
+
threshold
|
|
95
|
+
The selection-frequency threshold ``pi`` used for ``stable_support``.
|
|
96
|
+
avg_selected
|
|
97
|
+
Mean number of genes selected per subsample, per dimension, shape
|
|
98
|
+
``(latent_dim,)``. This is the ``q`` in the error bound below.
|
|
99
|
+
expected_false_positives
|
|
100
|
+
Meinshausen-Bühlmann upper bound on the expected number of falsely stable
|
|
101
|
+
genes per dimension, ``E[V] <= q^2 / ((2*threshold - 1) * p)``, shape
|
|
102
|
+
``(latent_dim,)``. Here ``q`` is the average number of *competitively*
|
|
103
|
+
selected genes and ``p`` the number of candidate genes; forced (mandatory)
|
|
104
|
+
genes are excluded from both, since they are always selected by
|
|
105
|
+
construction and are not candidates. ``NaN`` when ``threshold <= 0.5`` (the
|
|
106
|
+
bound is undefined there) or when every gene is mandatory. The bound holds
|
|
107
|
+
under the paper's exchangeability and "no worse than random guessing"
|
|
108
|
+
assumptions, which real data may violate; treat it as a guideline, not a
|
|
109
|
+
guarantee.
|
|
110
|
+
n_subsamples
|
|
111
|
+
Number of subsamples drawn. ``0`` for ``mode="iteration"``, which draws no
|
|
112
|
+
subsamples.
|
|
113
|
+
subsample_frac
|
|
114
|
+
Fraction of cells in each subsample. ``NaN`` for ``mode="iteration"``.
|
|
115
|
+
mode
|
|
116
|
+
Which resampling scheme produced the frequencies.
|
|
117
|
+
|
|
118
|
+
``"subsample"``
|
|
119
|
+
Meinshausen-Bühlmann cell subsampling with the model frozen. Targets
|
|
120
|
+
variance under *cell resampling*. ``frequency`` is per latent dimension
|
|
121
|
+
and ``expected_false_positives`` carries the paper's error bound.
|
|
122
|
+
``"iteration"``
|
|
123
|
+
Selection frequency across additional *training iterations* of a single
|
|
124
|
+
fit. Targets variance from the optimizer's position on its loss plateau,
|
|
125
|
+
which is a different — and on measured data, larger — source of
|
|
126
|
+
instability. ``frequency`` is per latent dimension, with each
|
|
127
|
+
iteration's dimensions matched to the fitted model's before counting —
|
|
128
|
+
see ``dim_match_quality``. ``expected_false_positives`` is ``NaN``:
|
|
129
|
+
training iterations are neither independent nor exchangeable, so the
|
|
130
|
+
Meinshausen-Bühlmann bound does not apply and no error control is
|
|
131
|
+
claimed.
|
|
132
|
+
n_iterations
|
|
133
|
+
Number of training iterations averaged over. ``0`` for ``mode="subsample"``.
|
|
134
|
+
coefficient_cond_mean
|
|
135
|
+
Shape ``(n_genes, latent_dim)``, or ``None`` when coefficients were not
|
|
136
|
+
collected. Mean coefficient over the runs in which each entry was
|
|
137
|
+
*selected* — reliability-blind but scale-preserving, since its expectation
|
|
138
|
+
equals the per-run coefficient. Feeds :meth:`stable_encoder`.
|
|
139
|
+
coefficient_sd
|
|
140
|
+
Standard deviation of the same conditional distribution. This is a
|
|
141
|
+
**spread, not a standard error**: subsample runs share half their cells by
|
|
142
|
+
construction and iteration runs are autocorrelated, so the effective sample
|
|
143
|
+
size is far below ``n_runs`` and ``sd / sqrt(n_runs)`` would be badly
|
|
144
|
+
overconfident. Reporting it as a precision would require a block bootstrap.
|
|
145
|
+
sign_consistency
|
|
146
|
+
Fraction of selecting runs in which each coefficient took its modal sign.
|
|
147
|
+
A gene selected every time with a sign that flips is not stable, which a
|
|
148
|
+
selection frequency alone cannot reveal. Measured at ~0.999 on real data,
|
|
149
|
+
so in practice a guard rather than a headline.
|
|
150
|
+
dim_match_quality
|
|
151
|
+
``mode="iteration"`` only: mean absolute cosine similarity between each
|
|
152
|
+
iteration's latent dimensions and the fitted model's, after optimal
|
|
153
|
+
matching. Per-dimension frequencies are only meaningful when dimensions
|
|
154
|
+
keep their identity across iterations, and this quantifies that rather than
|
|
155
|
+
assuming it. Near 1 means the anchoring held; low values mean the
|
|
156
|
+
per-dimension split should not be trusted and ``frequency.max(axis=1)``
|
|
157
|
+
(the flat union) is the safer readout. ``NaN`` for ``mode="subsample"``.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
frequency: NDArray[np.float64]
|
|
161
|
+
stable_support: BoolArray
|
|
162
|
+
threshold: float
|
|
163
|
+
avg_selected: NDArray[np.float64]
|
|
164
|
+
expected_false_positives: NDArray[np.float64]
|
|
165
|
+
n_subsamples: int
|
|
166
|
+
subsample_frac: float
|
|
167
|
+
mode: str = "subsample"
|
|
168
|
+
n_iterations: int = 0
|
|
169
|
+
dim_match_quality: float = float("nan")
|
|
170
|
+
coefficient_cond_mean: NDArray[np.float64] | None = None
|
|
171
|
+
coefficient_sd: NDArray[np.float64] | None = None
|
|
172
|
+
sign_consistency: NDArray[np.float64] | None = None
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def n_runs(self) -> int:
|
|
176
|
+
"""Number of resampling runs, whichever mode produced this result."""
|
|
177
|
+
return self.n_iterations if self.mode == "iteration" else self.n_subsamples
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def coefficient_mean(self) -> NDArray[np.float64] | None:
|
|
181
|
+
"""Mean coefficient over *all* runs, counting unselected runs as zero.
|
|
182
|
+
|
|
183
|
+
Equals ``coefficient_cond_mean * frequency``, so a gene selected in 40% of
|
|
184
|
+
runs is shrunk to 40% of its typical weight — shrinkage proportional to
|
|
185
|
+
reliability. Statistically the more principled estimator, but it changes the
|
|
186
|
+
latent scale the decoder was trained against, which is why
|
|
187
|
+
:meth:`stable_encoder` does not offer it.
|
|
188
|
+
"""
|
|
189
|
+
if self.coefficient_cond_mean is None:
|
|
190
|
+
return None
|
|
191
|
+
return self.coefficient_cond_mean * self.frequency
|
|
192
|
+
|
|
193
|
+
def stable_encoder(self) -> NDArray[np.float64]:
|
|
194
|
+
"""Aggregate the per-run coefficients into a more reproducible encoder.
|
|
195
|
+
|
|
196
|
+
Among stably selected entries the run-to-run coefficient spread is large
|
|
197
|
+
(CV ~0.34 median on measured data), so the *support* of a fit is more
|
|
198
|
+
reproducible than its *weights*. This returns the conditional mean
|
|
199
|
+
coefficient restricted to the stable support: the frequency threshold
|
|
200
|
+
decides *which* genes, the conditional mean decides *how much*.
|
|
201
|
+
|
|
202
|
+
Measured on simulated data against exact ground truth, with the decoder
|
|
203
|
+
left untouched, it improves on the fitted encoder on every axis that
|
|
204
|
+
matters for selection:
|
|
205
|
+
|
|
206
|
+
================== ========== ========= ======
|
|
207
|
+
encoder precision FDR genes
|
|
208
|
+
================== ========== ========= ======
|
|
209
|
+
fitted 0.62 0.38 156
|
|
210
|
+
this **0.74** **0.26** 116
|
|
211
|
+
================== ========== ========= ======
|
|
212
|
+
|
|
213
|
+
and raises reconstruction from 55% to 59% of the linear ceiling. The same
|
|
214
|
+
ordering holds under batch conditioning (precision 0.754 vs 0.691, FDR
|
|
215
|
+
0.246 vs 0.309). Because the latent scale is preserved, the result can be
|
|
216
|
+
installed with :meth:`BAE.apply_encoder` **without refitting the decoder**.
|
|
217
|
+
|
|
218
|
+
To trade precision for recall, lower ``threshold`` when calling
|
|
219
|
+
:meth:`BAE.stability_selection` rather than reaching for a different
|
|
220
|
+
estimator — the threshold *is* that dial. An unmasked conditional mean is
|
|
221
|
+
simply this with ``threshold`` at zero, and it was measured to be a worse
|
|
222
|
+
selector at every setting tried (precision 0.47 unconditioned, 0.56
|
|
223
|
+
conditioned) as well as numerically fragile on batch-dominated fits, where
|
|
224
|
+
it produced a reconstruction worse than predicting zero.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
ndarray of shape ``(n_genes, latent_dim)``
|
|
229
|
+
Same orientation as ``frequency`` and ``adata.varm``.
|
|
230
|
+
|
|
231
|
+
Raises
|
|
232
|
+
------
|
|
233
|
+
ValueError
|
|
234
|
+
If coefficients were not collected for this result.
|
|
235
|
+
"""
|
|
236
|
+
if self.coefficient_cond_mean is None:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
"coefficients were not collected for this result; "
|
|
239
|
+
"stable_encoder() needs a result produced by BAE.stability_selection"
|
|
240
|
+
)
|
|
241
|
+
return np.where(self.stable_support, self.coefficient_cond_mean, 0.0)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def stability_selection(
|
|
245
|
+
sourcemat: NDArray[np.floating],
|
|
246
|
+
targetmat: NDArray[np.floating],
|
|
247
|
+
*,
|
|
248
|
+
n_genes: int | None = None,
|
|
249
|
+
mandatory_features: NDArray[np.intp] | list[NDArray[np.intp]] | None = None,
|
|
250
|
+
mandatory_ridge: float | NDArray[np.floating] = 0.0,
|
|
251
|
+
n_subsamples: int = 100,
|
|
252
|
+
subsample_frac: float = 0.5,
|
|
253
|
+
threshold: float = 0.7,
|
|
254
|
+
stepno: int = 20,
|
|
255
|
+
nu: float = 0.1,
|
|
256
|
+
csf: float = 0.9,
|
|
257
|
+
independent: bool = True,
|
|
258
|
+
seed: int | None = None,
|
|
259
|
+
verbose: bool = False,
|
|
260
|
+
) -> StabilitySelectionResult:
|
|
261
|
+
"""Run stability selection over subsamples of ``allboost``.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
sourcemat
|
|
266
|
+
Predictor matrix, shape ``(n_samples, n_features)``. May include trailing
|
|
267
|
+
nuisance columns; only the first ``n_genes`` columns are scored.
|
|
268
|
+
targetmat
|
|
269
|
+
Target matrix, shape ``(n_samples, latent_dim)``. For BAE these are the
|
|
270
|
+
functional-gradient targets ``z*`` (see :meth:`BAE.stability_selection`),
|
|
271
|
+
not the latent codes themselves — the codes are a sparse linear function of
|
|
272
|
+
the already-selected genes, which makes selecting them nearly circular.
|
|
273
|
+
n_genes
|
|
274
|
+
Number of leading columns of ``sourcemat`` that are genes. Frequencies are
|
|
275
|
+
reported only for these. Defaults to all columns.
|
|
276
|
+
mandatory_features, mandatory_ridge
|
|
277
|
+
Forwarded to :func:`allboost`, so the resampled problem matches the one the
|
|
278
|
+
encoder solved (e.g. batch nuisance regressors). Mandatory columns beyond
|
|
279
|
+
``n_genes`` never appear in the reported frequencies.
|
|
280
|
+
n_subsamples
|
|
281
|
+
Number of subsamples (``B``). More reduces Monte-Carlo noise in the
|
|
282
|
+
frequencies; cost is linear.
|
|
283
|
+
subsample_frac
|
|
284
|
+
Fraction of cells per subsample, drawn without replacement. ``0.5`` is the
|
|
285
|
+
value Meinshausen-Bühlmann derive the error bound for; other values give
|
|
286
|
+
frequencies but weaken the bound's justification.
|
|
287
|
+
threshold
|
|
288
|
+
Stability threshold ``pi`` in ``(0.5, 1]``. Genes selected in at least this
|
|
289
|
+
fraction of subsamples form the stable support. Must exceed ``0.5`` for the
|
|
290
|
+
error bound to be defined.
|
|
291
|
+
stepno, nu, csf, independent
|
|
292
|
+
Boosting hyperparameters, forwarded to :func:`allboost`. Use the same
|
|
293
|
+
values the encoder was fitted with.
|
|
294
|
+
seed
|
|
295
|
+
Seed for the subsampling RNG.
|
|
296
|
+
verbose
|
|
297
|
+
Show a progress bar over the subsamples. Defaults to False so that
|
|
298
|
+
calling this function directly stays silent;
|
|
299
|
+
:meth:`structboost.BAE.stability_selection` passes its own ``verbose``
|
|
300
|
+
through.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
StabilitySelectionResult
|
|
305
|
+
"""
|
|
306
|
+
sourcemat = np.asarray(sourcemat, dtype=np.float64)
|
|
307
|
+
targetmat = np.asarray(targetmat, dtype=np.float64)
|
|
308
|
+
n = sourcemat.shape[0]
|
|
309
|
+
if targetmat.shape[0] != n:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"sourcemat and targetmat must share n_samples, got {n} and {targetmat.shape[0]}"
|
|
312
|
+
)
|
|
313
|
+
latent_dim = targetmat.shape[1]
|
|
314
|
+
p_genes = sourcemat.shape[1] if n_genes is None else int(n_genes)
|
|
315
|
+
if not 1 <= p_genes <= sourcemat.shape[1]:
|
|
316
|
+
raise ValueError(f"n_genes must be in [1, {sourcemat.shape[1]}], got {p_genes}")
|
|
317
|
+
if n_subsamples < 1:
|
|
318
|
+
raise ValueError(f"n_subsamples must be >= 1, got {n_subsamples}")
|
|
319
|
+
if not 0.0 < subsample_frac < 1.0:
|
|
320
|
+
raise ValueError(f"subsample_frac must be in (0, 1), got {subsample_frac}")
|
|
321
|
+
if not 0.0 < threshold <= 1.0:
|
|
322
|
+
raise ValueError(f"threshold must be in (0, 1], got {threshold}")
|
|
323
|
+
|
|
324
|
+
sub_n = int(np.floor(subsample_frac * n))
|
|
325
|
+
if sub_n < 2:
|
|
326
|
+
raise ValueError(
|
|
327
|
+
f"subsample_frac={subsample_frac} gives {sub_n} cells; too few to fit. "
|
|
328
|
+
"Increase subsample_frac or use more cells."
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
rng = np.random.default_rng(seed)
|
|
332
|
+
counts = np.zeros((p_genes, latent_dim), dtype=np.float64)
|
|
333
|
+
selected_per_subsample = np.zeros(latent_dim, dtype=np.float64)
|
|
334
|
+
# Streaming accumulators: the full coefficient trace would be
|
|
335
|
+
# n_subsamples x p_genes x latent_dim floats, which is infeasible at scale.
|
|
336
|
+
# Signs need no alignment here because every subsample fits the same targets.
|
|
337
|
+
coef_sum = np.zeros((p_genes, latent_dim), dtype=np.float64)
|
|
338
|
+
coef_sq_sum = np.zeros((p_genes, latent_dim), dtype=np.float64)
|
|
339
|
+
positive_count = np.zeros((p_genes, latent_dim), dtype=np.float64)
|
|
340
|
+
|
|
341
|
+
# tqdm ships in the `[bae]` extra while this module is part of the NumPy-only
|
|
342
|
+
# core, so it is imported lazily *and* only when a bar was actually asked for.
|
|
343
|
+
# Importing it unconditionally would make a quiet call fail on a core install.
|
|
344
|
+
runs: Iterable[int] = range(n_subsamples)
|
|
345
|
+
if verbose:
|
|
346
|
+
from tqdm import tqdm
|
|
347
|
+
|
|
348
|
+
runs = tqdm(runs, desc="Stability selection (subsample)", unit="run")
|
|
349
|
+
|
|
350
|
+
for _ in runs:
|
|
351
|
+
idx = rng.choice(n, size=sub_n, replace=False)
|
|
352
|
+
# A fresh covcache per subsample is mandatory: the Gram matrix depends on
|
|
353
|
+
# the rows, so the full-data cache would silently produce wrong updates.
|
|
354
|
+
betamat = allboost(
|
|
355
|
+
sourcemat[idx],
|
|
356
|
+
targetmat[idx],
|
|
357
|
+
mandatory_features=mandatory_features,
|
|
358
|
+
mandatory_ridge=mandatory_ridge,
|
|
359
|
+
stepno=stepno,
|
|
360
|
+
nu=nu,
|
|
361
|
+
csf=csf,
|
|
362
|
+
independent=independent,
|
|
363
|
+
)
|
|
364
|
+
beta_genes = betamat[:, :p_genes].T # (n_genes, latent_dim)
|
|
365
|
+
selected = np.abs(betamat[:, :p_genes]) > 0 # (latent_dim, n_genes)
|
|
366
|
+
counts += selected.T
|
|
367
|
+
selected_per_subsample += selected.sum(axis=1)
|
|
368
|
+
coef_sum += beta_genes
|
|
369
|
+
coef_sq_sum += beta_genes**2
|
|
370
|
+
positive_count += beta_genes > 0
|
|
371
|
+
|
|
372
|
+
frequency = counts / n_subsamples
|
|
373
|
+
avg_selected = selected_per_subsample / n_subsamples
|
|
374
|
+
stable_support = frequency >= threshold
|
|
375
|
+
|
|
376
|
+
if threshold > 0.5:
|
|
377
|
+
# Forced (mandatory) genes are selected in every subsample by construction:
|
|
378
|
+
# their frequency-1.0 is not evidence of stability, and they are not
|
|
379
|
+
# candidates competing for selection. Exclude them from both the average
|
|
380
|
+
# model size q and the candidate pool p so the bound reflects the genes
|
|
381
|
+
# that were actually selected competitively.
|
|
382
|
+
n_forced = _mandatory_gene_counts(mandatory_features, latent_dim, p_genes)
|
|
383
|
+
q_eff = np.maximum(avg_selected - n_forced, 0.0)
|
|
384
|
+
p_eff = p_genes - n_forced
|
|
385
|
+
expected_false_positives = np.where(
|
|
386
|
+
p_eff > 0, q_eff**2 / ((2.0 * threshold - 1.0) * p_eff), np.nan
|
|
387
|
+
)
|
|
388
|
+
else:
|
|
389
|
+
warnings.warn(
|
|
390
|
+
"threshold <= 0.5: the Meinshausen-Bühlmann error bound is undefined, "
|
|
391
|
+
"so expected_false_positives is NaN. Use a threshold in (0.5, 1].",
|
|
392
|
+
UserWarning,
|
|
393
|
+
stacklevel=2,
|
|
394
|
+
)
|
|
395
|
+
expected_false_positives = np.full(latent_dim, np.nan)
|
|
396
|
+
|
|
397
|
+
cond_mean, cond_sd, sign_consistency = _coefficient_statistics(
|
|
398
|
+
coef_sum, coef_sq_sum, positive_count, counts
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return StabilitySelectionResult(
|
|
402
|
+
frequency=frequency,
|
|
403
|
+
stable_support=stable_support,
|
|
404
|
+
threshold=float(threshold),
|
|
405
|
+
avg_selected=avg_selected,
|
|
406
|
+
expected_false_positives=expected_false_positives,
|
|
407
|
+
n_subsamples=int(n_subsamples),
|
|
408
|
+
subsample_frac=float(subsample_frac),
|
|
409
|
+
coefficient_cond_mean=cond_mean,
|
|
410
|
+
coefficient_sd=cond_sd,
|
|
411
|
+
sign_consistency=sign_consistency,
|
|
412
|
+
)
|