stability-matched 1.0.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.
- stability_matched-1.0.0.dist-info/METADATA +87 -0
- stability_matched-1.0.0.dist-info/RECORD +7 -0
- stability_matched-1.0.0.dist-info/WHEEL +5 -0
- stability_matched-1.0.0.dist-info/entry_points.txt +2 -0
- stability_matched-1.0.0.dist-info/licenses/LICENSE +21 -0
- stability_matched-1.0.0.dist-info/top_level.txt +1 -0
- stability_matched.py +277 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stability-matched
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Feature-selection stability (Nogueira's estimator) with its exact chance level for candidate pools chosen inside each resample
|
|
5
|
+
Author: Hassan Nasiraee, Hajar Abbaszadeh, Parvin Farzanegi
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: feature selection,stability,reproducibility,chance correction,bioinformatics
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy>=1.22
|
|
15
|
+
Requires-Dist: scipy>=1.8
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Provides-Extra: examples
|
|
19
|
+
Requires-Dist: scikit-learn>=1.2; extra == "examples"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# stability-matched
|
|
23
|
+
|
|
24
|
+
Feature-selection stability with its **exact chance level when the candidate
|
|
25
|
+
pool is chosen inside each resample** (for example a variance or univariate
|
|
26
|
+
filter fitted on the training rows).
|
|
27
|
+
|
|
28
|
+
Nogueira's estimator (Nogueira, Sechidis & Brown, JMLR 2018) corrects for
|
|
29
|
+
chance as if every feature could be selected in every resample. When each
|
|
30
|
+
resample first filters its own pool, features that pass every filter are
|
|
31
|
+
favoured even by a selector that picks at random inside its pool, and part of
|
|
32
|
+
the reported stability belongs to the filter. This package gives, in closed
|
|
33
|
+
form and without simulation, the expected value of the estimator under random
|
|
34
|
+
selection of the observed sizes from the observed pools, and the excess over
|
|
35
|
+
it, together with a Monte Carlo test, a variant that keeps co-expression
|
|
36
|
+
modules, and an exact chance level for the agreement between two selectors.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install stability-matched
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Only NumPy and SciPy are required.
|
|
45
|
+
|
|
46
|
+
## Use
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from stability_matched import matrices_from_indices, phi_matched, report
|
|
50
|
+
|
|
51
|
+
# supports[i]: indices selected in resample i; pools[i]: its candidate pool
|
|
52
|
+
Z, P = matrices_from_indices(supports, pools, d)
|
|
53
|
+
print(report(Z, P)) # phi, interval, chance level, excess, p
|
|
54
|
+
res = phi_matched(Z, P, B=999) # the same as a dictionary
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
From the command line, with `Z` and `P` saved as `.npy`, `.csv`, or one
|
|
58
|
+
`.npz` holding arrays `Z` and `P`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
stability-matched selections.npz
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`examples/example_sklearn.py` runs the whole workflow for a cross-validated
|
|
65
|
+
lasso with a variance filter fitted inside each resample.
|
|
66
|
+
|
|
67
|
+
## Functions
|
|
68
|
+
|
|
69
|
+
| Function | Returns |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `phi_stability(Z)` | Nogueira's estimator, asymptotic variance, 95% interval |
|
|
72
|
+
| `phi0_matched(Z, P)` | exact expected estimator under random selection within each resample's pool |
|
|
73
|
+
| `phi0_stratified(Z, P, groups)` | the same, keeping how many features each resample takes from every group (e.g. co-expression module) |
|
|
74
|
+
| `matched_null_test(Z, P, B)` | Monte Carlo p-value against B complete null matrices |
|
|
75
|
+
| `agreement_exact(A, B, P)` | mean Jaccard between two selectors in the same resample and its exact hypergeometric chance level |
|
|
76
|
+
| `phi_matched(Z, P, B)` | all of the above for one matrix |
|
|
77
|
+
| `matrices_from_indices`, `report`, `main` | helpers and command line |
|
|
78
|
+
|
|
79
|
+
Empty supports are kept (they lower stability); a matrix with no defined
|
|
80
|
+
stability returns NaN. Tests: `pytest tests/`.
|
|
81
|
+
|
|
82
|
+
## Cite
|
|
83
|
+
|
|
84
|
+
H. Nasiraee, H. Abbaszadeh, P. Farzanegi. How much of feature-selection
|
|
85
|
+
stability is chance? An exact correction, a re-evaluation of two published
|
|
86
|
+
tools, and a multi-tissue audit. Code: DOI 10.5281/zenodo.22662385. MIT
|
|
87
|
+
licence.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
stability_matched.py,sha256=Ql4nrLtr5EYCBPBnUyIuMv8mQD5lomUOiVNKOKKd77k,11189
|
|
2
|
+
stability_matched-1.0.0.dist-info/licenses/LICENSE,sha256=yG5ruXPBTy8xMFSPdBVtLCYVmHsudlvdxtKY6Q0h12Y,1111
|
|
3
|
+
stability_matched-1.0.0.dist-info/METADATA,sha256=uYSreZO7ECagU9zBgpbY_XBRiSDNI8vYHiYjzjhb1qQ,3535
|
|
4
|
+
stability_matched-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
5
|
+
stability_matched-1.0.0.dist-info/entry_points.txt,sha256=SxdBUn0lvxICJQvkPSTn1Qq868yLqqzTmsv07VAtdm8,61
|
|
6
|
+
stability_matched-1.0.0.dist-info/top_level.txt,sha256=yqdWzJMMAx5A34IW4rK6XbD7gngz1inJP-2PYu8bN0w,18
|
|
7
|
+
stability_matched-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hassan Nasiraee, Hajar Abbaszadeh and Parvin Farzanegi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stability_matched
|
stability_matched.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# stability_matched.py -- stand-alone stability toolkit
|
|
4
|
+
#
|
|
5
|
+
# For a binary selection matrix Z (M resamples x d features) and the
|
|
6
|
+
# candidate-pool matrix P (same shape; P[i, f] = 1 if feature f could be
|
|
7
|
+
# selected in resample i):
|
|
8
|
+
#
|
|
9
|
+
# phi_stability(Z) Nogueira, Sechidis & Brown (2018) estimator,
|
|
10
|
+
# its asymptotic variance and 95% interval
|
|
11
|
+
# phi0_matched(Z, P) exact E[phi] under the matched null: row i a
|
|
12
|
+
# uniform random k_i-subset of pool P_i, rows
|
|
13
|
+
# independent (Proposition 1 of the paper);
|
|
14
|
+
# conditional on the observed k_i and P_i
|
|
15
|
+
# phi0_stratified(Z, P, g) exact E[phi] when row i keeps how many genes
|
|
16
|
+
# it selects from every group g (e.g. a
|
|
17
|
+
# co-expression module) but chooses WHICH genes
|
|
18
|
+
# of that group (within its pool) at random;
|
|
19
|
+
# Proposition 1 with pi_if = c_ig / |g n P_i|
|
|
20
|
+
# matched_null_test(Z, P) Monte Carlo p-value against B complete null
|
|
21
|
+
# matrices, with null quantiles
|
|
22
|
+
# agreement_exact(A, B, P) mean Jaccard between two selectors' supports
|
|
23
|
+
# in the same resample, and its exact
|
|
24
|
+
# hypergeometric chance level
|
|
25
|
+
# phi_matched(Z, P) all of the above in one call
|
|
26
|
+
# matrices_from_indices(supports, pools, d)
|
|
27
|
+
# build Z and P from per-resample index lists
|
|
28
|
+
# report(Z, P) a plain-text report of the above
|
|
29
|
+
#
|
|
30
|
+
# Command line: stability-matched Z.npy P.npy (or python -m
|
|
31
|
+
# stability_matched; also accepts one .npz with arrays Z and P, and .csv)
|
|
32
|
+
#
|
|
33
|
+
# Empty supports are kept as empty rows (they lower p_f and kbar). A matrix
|
|
34
|
+
# in which every row is empty, or every row selects every feature, has no
|
|
35
|
+
# defined stability (zero denominator): NaN is returned, never a number.
|
|
36
|
+
# Tests: test_stability_matched.py (pytest).
|
|
37
|
+
# =============================================================================
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
from scipy import stats
|
|
42
|
+
|
|
43
|
+
__version__ = "1.0.0"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def phi_stability(Z):
|
|
47
|
+
"""Nogueira's estimator with its asymptotic variance (Thm 9)."""
|
|
48
|
+
Z = np.asarray(Z, dtype=float)
|
|
49
|
+
M, d = Z.shape
|
|
50
|
+
k = Z.sum(axis=1)
|
|
51
|
+
kbar = k.mean()
|
|
52
|
+
p = Z.mean(axis=0)
|
|
53
|
+
s2 = M / (M - 1) * p * (1 - p)
|
|
54
|
+
den = (kbar / d) * (1 - kbar / d)
|
|
55
|
+
if den <= 0:
|
|
56
|
+
return {"phi": np.nan, "var": np.nan, "lo": np.nan, "hi": np.nan}
|
|
57
|
+
phi = 1 - s2.mean() / den
|
|
58
|
+
# asymptotic variance, Nogueira et al. (2018), Theorem 9
|
|
59
|
+
phis = np.empty(M)
|
|
60
|
+
for i in range(M):
|
|
61
|
+
phis[i] = (1 / den) * (np.mean(Z[i] * p) - k[i] * kbar / d ** 2
|
|
62
|
+
+ phi / 2 * (2 * k[i] * kbar / d ** 2
|
|
63
|
+
- k[i] / d - kbar / d + 1))
|
|
64
|
+
var = 4 / M ** 2 * np.sum((phis - phis.mean()) ** 2)
|
|
65
|
+
se = np.sqrt(var)
|
|
66
|
+
return {"phi": float(phi), "var": float(var),
|
|
67
|
+
"lo": float(phi - 1.96 * se), "hi": float(phi + 1.96 * se)}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def phi0_matched(Z, P):
|
|
71
|
+
"""Exact expectation of phi under the matched null (Proposition 1)."""
|
|
72
|
+
Z = np.asarray(Z, dtype=float)
|
|
73
|
+
P = np.asarray(P, dtype=bool)
|
|
74
|
+
M, d = Z.shape
|
|
75
|
+
k = Z.sum(axis=1)
|
|
76
|
+
m = P.sum(axis=1)
|
|
77
|
+
pi = np.where(P, (k / np.maximum(m, 1))[:, None], 0.0)
|
|
78
|
+
mu = pi.mean(axis=0)
|
|
79
|
+
v = (pi * (1 - pi)).sum(axis=0) / M ** 2
|
|
80
|
+
es2 = M / (M - 1) * (mu - mu ** 2 - v)
|
|
81
|
+
kb = k.mean()
|
|
82
|
+
den = (kb / d) * (1 - kb / d)
|
|
83
|
+
return float(1 - es2.mean() / den) if den > 0 else np.nan
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _phi0_from_pi(pi, kbar):
|
|
87
|
+
M, d = pi.shape
|
|
88
|
+
mu = pi.mean(axis=0)
|
|
89
|
+
v = (pi * (1 - pi)).sum(axis=0) / M ** 2
|
|
90
|
+
es2 = M / (M - 1) * (mu - mu ** 2 - v)
|
|
91
|
+
den = (kbar / d) * (1 - kbar / d)
|
|
92
|
+
return float(1 - es2.mean() / den) if den > 0 else np.nan
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def phi0_stratified(Z, P, groups):
|
|
96
|
+
"""Exact expectation of phi when, in every row, the number of selected
|
|
97
|
+
features in each group is kept and the features within the group (and
|
|
98
|
+
within the row's pool) are chosen uniformly at random. groups: integer
|
|
99
|
+
label per feature (a feature alone in its group is kept as selected)."""
|
|
100
|
+
Z = np.asarray(Z, dtype=float)
|
|
101
|
+
P = np.asarray(P, dtype=bool)
|
|
102
|
+
g = np.asarray(groups, dtype=int)
|
|
103
|
+
if np.any((Z > 0) & ~P):
|
|
104
|
+
raise ValueError("a selected feature lies outside its pool")
|
|
105
|
+
M, d = Z.shape
|
|
106
|
+
G = g.max() + 1
|
|
107
|
+
pi = np.zeros((M, d))
|
|
108
|
+
for i in range(M):
|
|
109
|
+
a = np.flatnonzero(P[i])
|
|
110
|
+
n_g = np.bincount(g[a], minlength=G)
|
|
111
|
+
c_g = np.bincount(g[a], weights=Z[i, a], minlength=G)
|
|
112
|
+
pi[i, a] = c_g[g[a]] / n_g[g[a]]
|
|
113
|
+
return _phi0_from_pi(pi, Z.sum(axis=1).mean())
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def stratified_null_draws(Z, P, groups, B=999, rng=None):
|
|
117
|
+
rng = np.random.default_rng(rng)
|
|
118
|
+
Z = np.asarray(Z, dtype=bool)
|
|
119
|
+
P = np.asarray(P, dtype=bool)
|
|
120
|
+
g = np.asarray(groups, dtype=int)
|
|
121
|
+
M, d = Z.shape
|
|
122
|
+
out = np.empty(B)
|
|
123
|
+
for b in range(B):
|
|
124
|
+
R = np.zeros((M, d), dtype=np.uint8)
|
|
125
|
+
for i in range(M):
|
|
126
|
+
for grp in np.unique(g[Z[i]]):
|
|
127
|
+
cell = np.flatnonzero((g == grp) & P[i])
|
|
128
|
+
R[i, rng.choice(cell, int((Z[i] & (g == grp)).sum()),
|
|
129
|
+
replace=False)] = 1
|
|
130
|
+
out[b] = phi_stability(R)["phi"]
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def matched_null_draws(Z, P, B=999, rng=None):
|
|
135
|
+
rng = np.random.default_rng(rng)
|
|
136
|
+
Z = np.asarray(Z, dtype=bool)
|
|
137
|
+
P = np.asarray(P, dtype=bool)
|
|
138
|
+
M, d = Z.shape
|
|
139
|
+
k = Z.sum(axis=1)
|
|
140
|
+
pools = [np.flatnonzero(P[i]) for i in range(M)]
|
|
141
|
+
if np.any(Z & ~P):
|
|
142
|
+
raise ValueError("a selected feature lies outside its pool")
|
|
143
|
+
out = np.empty(B)
|
|
144
|
+
for b in range(B):
|
|
145
|
+
R = np.zeros((M, d), dtype=np.uint8)
|
|
146
|
+
for i in range(M):
|
|
147
|
+
if k[i]:
|
|
148
|
+
R[i, rng.choice(pools[i], k[i], replace=False)] = 1
|
|
149
|
+
out[b] = phi_stability(R)["phi"]
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def matched_null_test(Z, P, B=999, rng=None):
|
|
154
|
+
obs = phi_stability(Z)["phi"]
|
|
155
|
+
nd = matched_null_draws(Z, P, B, rng)
|
|
156
|
+
return {"phi": obs, "p_mc": float((1 + np.sum(nd >= obs)) / (B + 1)),
|
|
157
|
+
"null_mean": float(nd.mean()), "null_sd": float(nd.std(ddof=1)),
|
|
158
|
+
"null_q95": float(np.quantile(nd, 0.95)),
|
|
159
|
+
"null_q99": float(np.quantile(nd, 0.99)), "B": B}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _hyper_jaccard(ka, kb, m):
|
|
163
|
+
x = np.arange(max(0, ka + kb - m), min(ka, kb) + 1)
|
|
164
|
+
p = stats.hypergeom.pmf(x, m, ka, kb)
|
|
165
|
+
return float(np.sum(p * x / (ka + kb - x)))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def agreement_exact(A, B, P):
|
|
169
|
+
"""Mean Jaccard of two supports per resample and its exact chance level
|
|
170
|
+
(resamples where either support is empty are skipped)."""
|
|
171
|
+
obs, ch = [], []
|
|
172
|
+
for a, b, p in zip(np.asarray(A, bool), np.asarray(B, bool),
|
|
173
|
+
np.asarray(P, bool)):
|
|
174
|
+
ka, kb = int(a.sum()), int(b.sum())
|
|
175
|
+
if ka == 0 or kb == 0:
|
|
176
|
+
continue
|
|
177
|
+
obs.append((a & b).sum() / (a | b).sum())
|
|
178
|
+
ch.append(_hyper_jaccard(ka, kb, int(p.sum())))
|
|
179
|
+
if not obs:
|
|
180
|
+
return {"agree": np.nan, "agree0": np.nan, "n": 0}
|
|
181
|
+
return {"agree": float(np.mean(obs)), "agree0": float(np.mean(ch)),
|
|
182
|
+
"agree_x": float(np.mean(obs) - np.mean(ch)), "n": len(obs)}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def phi_matched(Z, P, B=999, rng=None):
|
|
186
|
+
"""Phi, its interval, the exact matched chance level, the excess and the
|
|
187
|
+
Monte Carlo test, in one call."""
|
|
188
|
+
st = phi_stability(Z)
|
|
189
|
+
p0 = phi0_matched(Z, P)
|
|
190
|
+
out = {"phi": st["phi"], "phi_lo": st["lo"], "phi_hi": st["hi"],
|
|
191
|
+
"phi0": p0, "phi_x": st["phi"] - p0,
|
|
192
|
+
"empty_share": float(np.mean(np.asarray(Z).sum(axis=1) == 0))}
|
|
193
|
+
if B:
|
|
194
|
+
t = matched_null_test(Z, P, B, rng)
|
|
195
|
+
out.update({"p_mc": t["p_mc"], "null_sd": t["null_sd"],
|
|
196
|
+
"null_q99": t["null_q99"]})
|
|
197
|
+
return out
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def matrices_from_indices(supports, pools=None, d=None):
|
|
201
|
+
"""Selection matrix Z and pool matrix P (M x d, 0/1) from per-resample
|
|
202
|
+
lists of selected feature indices and of candidate-pool indices. With
|
|
203
|
+
pools=None every feature is a candidate in every resample."""
|
|
204
|
+
supports = [np.asarray(s, dtype=int) for s in supports]
|
|
205
|
+
if d is None:
|
|
206
|
+
alls = supports + ([np.asarray(p, dtype=int) for p in pools]
|
|
207
|
+
if pools is not None else [])
|
|
208
|
+
d = int(max((a.max() for a in alls if a.size), default=-1)) + 1
|
|
209
|
+
M = len(supports)
|
|
210
|
+
Z = np.zeros((M, d), dtype=np.uint8)
|
|
211
|
+
P = np.ones((M, d), dtype=np.uint8) if pools is None else \
|
|
212
|
+
np.zeros((M, d), dtype=np.uint8)
|
|
213
|
+
for i, s in enumerate(supports):
|
|
214
|
+
Z[i, s] = 1
|
|
215
|
+
if pools is not None:
|
|
216
|
+
P[i, np.asarray(pools[i], dtype=int)] = 1
|
|
217
|
+
return Z, P
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def report(Z, P, B=999, rng=0):
|
|
221
|
+
"""Plain-text summary: phi with interval, matched chance level, excess
|
|
222
|
+
and Monte Carlo test; empty supports and support sizes."""
|
|
223
|
+
r = phi_matched(Z, P, B=B, rng=rng)
|
|
224
|
+
k = np.asarray(Z).sum(axis=1)
|
|
225
|
+
m = np.asarray(P).sum(axis=1)
|
|
226
|
+
lines = [
|
|
227
|
+
f"resamples M = {len(k)}, features d = {np.asarray(Z).shape[1]}",
|
|
228
|
+
f"support size: mean {k.mean():.1f}, range {k.min()}-{k.max()}; "
|
|
229
|
+
f"empty supports {100 * r['empty_share']:.0f}%",
|
|
230
|
+
f"candidate pool size: mean {m.mean():.1f}, range {m.min()}-{m.max()}",
|
|
231
|
+
f"phi (Nogueira) {r['phi']:.4f} "
|
|
232
|
+
f"[{r['phi_lo']:.4f}, {r['phi_hi']:.4f}]",
|
|
233
|
+
f"matched chance level {r['phi0']:.4f}",
|
|
234
|
+
f"excess over chance {r['phi_x']:+.4f} "
|
|
235
|
+
f"[{r['phi_lo'] - r['phi0']:+.4f}, {r['phi_hi'] - r['phi0']:+.4f}]"]
|
|
236
|
+
if B:
|
|
237
|
+
lines.append(f"Monte Carlo p (B = {B}) {r['p_mc']:.4f} "
|
|
238
|
+
f"(null SD {r['null_sd']:.4f})")
|
|
239
|
+
return "\n".join(lines)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _load(path, key=None):
|
|
243
|
+
path = str(path)
|
|
244
|
+
if path.endswith(".npz"):
|
|
245
|
+
z = np.load(path)
|
|
246
|
+
return z[key] if key else z
|
|
247
|
+
if path.endswith(".csv"):
|
|
248
|
+
return np.loadtxt(path, delimiter=",")
|
|
249
|
+
return np.load(path)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def main(argv=None):
|
|
253
|
+
import argparse
|
|
254
|
+
ap = argparse.ArgumentParser(
|
|
255
|
+
prog="stability-matched",
|
|
256
|
+
description="Selection stability with its exact matched chance level")
|
|
257
|
+
ap.add_argument("Z", help="selection matrix (resamples x features, 0/1): "
|
|
258
|
+
".npy, .csv, or .npz holding arrays Z and P")
|
|
259
|
+
ap.add_argument("P", nargs="?", default=None,
|
|
260
|
+
help="pool matrix of the same shape (omit if every "
|
|
261
|
+
"feature is a candidate in every resample)")
|
|
262
|
+
ap.add_argument("-B", type=int, default=999,
|
|
263
|
+
help="null matrices for the Monte Carlo test (0: none)")
|
|
264
|
+
ap.add_argument("--seed", type=int, default=0)
|
|
265
|
+
a = ap.parse_args(argv)
|
|
266
|
+
if a.Z.endswith(".npz") and a.P is None:
|
|
267
|
+
z = _load(a.Z)
|
|
268
|
+
Z = z["Z"]
|
|
269
|
+
P = z["P"] if "P" in z.files else np.ones_like(Z)
|
|
270
|
+
else:
|
|
271
|
+
Z = _load(a.Z)
|
|
272
|
+
P = _load(a.P) if a.P else np.ones_like(Z)
|
|
273
|
+
print(report(Z, P, B=a.B, rng=a.seed))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
main()
|