nltools 0.6.0.dev0__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.
- nltools/__init__.py +55 -0
- nltools/algorithms/__init__.py +90 -0
- nltools/algorithms/alignment/__init__.py +21 -0
- nltools/algorithms/alignment/procrustes.py +565 -0
- nltools/algorithms/alignment/srm.py +758 -0
- nltools/algorithms/backends.py +1059 -0
- nltools/algorithms/corrections.py +177 -0
- nltools/algorithms/decoding.py +327 -0
- nltools/algorithms/inference/__init__.py +50 -0
- nltools/algorithms/inference/bootstrap.py +1386 -0
- nltools/algorithms/inference/correlation.py +373 -0
- nltools/algorithms/inference/intersubject.py +422 -0
- nltools/algorithms/inference/isc.py +1554 -0
- nltools/algorithms/inference/matrix.py +602 -0
- nltools/algorithms/inference/one_sample.py +288 -0
- nltools/algorithms/inference/random.py +122 -0
- nltools/algorithms/inference/timeseries.py +347 -0
- nltools/algorithms/inference/two_sample.py +212 -0
- nltools/algorithms/inference/utils.py +58 -0
- nltools/algorithms/inference/validation.py +282 -0
- nltools/algorithms/neighborhoods.py +207 -0
- nltools/algorithms/outliers.py +308 -0
- nltools/algorithms/regression.py +83 -0
- nltools/algorithms/signal.py +303 -0
- nltools/algorithms/similarity.py +234 -0
- nltools/algorithms/validation.py +151 -0
- nltools/cross_validation.py +72 -0
- nltools/data/__init__.py +30 -0
- nltools/data/adjacency/__init__.py +875 -0
- nltools/data/adjacency/io.py +111 -0
- nltools/data/adjacency/modeling.py +569 -0
- nltools/data/adjacency/plotting.py +174 -0
- nltools/data/adjacency/state.py +349 -0
- nltools/data/adjacency/stats.py +596 -0
- nltools/data/adjacency/utils.py +79 -0
- nltools/data/atlases/__init__.py +23 -0
- nltools/data/atlases/labeling.py +158 -0
- nltools/data/atlases/loading.py +76 -0
- nltools/data/atlases/registry.py +96 -0
- nltools/data/atlases/reporting.py +456 -0
- nltools/data/braindata/__init__.py +2170 -0
- nltools/data/braindata/analysis.py +1381 -0
- nltools/data/braindata/bootstrap.py +398 -0
- nltools/data/braindata/io.py +896 -0
- nltools/data/braindata/modeling.py +594 -0
- nltools/data/braindata/plotting.py +501 -0
- nltools/data/braindata/prediction.py +1250 -0
- nltools/data/braindata/utils.py +348 -0
- nltools/data/braindata/validation.py +197 -0
- nltools/data/braindata/viewer.js +266 -0
- nltools/data/braindata/viewer.py +770 -0
- nltools/data/combine.py +27 -0
- nltools/data/designmatrix/__init__.py +1032 -0
- nltools/data/designmatrix/append.py +518 -0
- nltools/data/designmatrix/diagnostics.py +248 -0
- nltools/data/designmatrix/io.py +356 -0
- nltools/data/designmatrix/plotting.py +291 -0
- nltools/data/designmatrix/regressors.py +463 -0
- nltools/data/designmatrix/transforms.py +200 -0
- nltools/data/designmatrix/utils.py +350 -0
- nltools/data/ownership.py +129 -0
- nltools/data/results.py +291 -0
- nltools/data/roc/__init__.py +398 -0
- nltools/data/simulator/__init__.py +927 -0
- nltools/data/simulator/haxby.py +124 -0
- nltools/data/validation.py +83 -0
- nltools/datasets.py +218 -0
- nltools/io/__init__.py +10 -0
- nltools/io/events.py +67 -0
- nltools/io/h5.py +246 -0
- nltools/mask.py +403 -0
- nltools/models/__init__.py +11 -0
- nltools/models/glm.py +543 -0
- nltools/models/results.py +49 -0
- nltools/models/ridge.py +1303 -0
- nltools/models/validation.py +26 -0
- nltools/plotting/__init__.py +32 -0
- nltools/plotting/adjacency.py +421 -0
- nltools/plotting/brain.py +669 -0
- nltools/plotting/decomposition.py +111 -0
- nltools/plotting/prediction.py +110 -0
- nltools/resources/covariates_example.csv +161 -0
- nltools/resources/onsets_example.csv +40 -0
- nltools/templates/__init__.py +51 -0
- nltools/templates/config.py +144 -0
- nltools/templates/fetch.py +260 -0
- nltools/templates/matching.py +183 -0
- nltools/templates/paths.py +106 -0
- nltools/templates/registry.py +25 -0
- nltools/utils.py +230 -0
- nltools/version.py +13 -0
- nltools-0.6.0.dev0.dist-info/METADATA +95 -0
- nltools-0.6.0.dev0.dist-info/RECORD +95 -0
- nltools-0.6.0.dev0.dist-info/WHEEL +4 -0
- nltools-0.6.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Similarity metrics and correlation."""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.spatial.distance import cdist
|
|
7
|
+
from scipy.stats import rankdata
|
|
8
|
+
|
|
9
|
+
from .regression import regress
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fisher_r_to_z(r):
|
|
13
|
+
"""Convert correlation coefficients to Fisher z values.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
r (float | np.ndarray): Correlation coefficient(s).
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
np.ndarray: Fisher z-transformed correlation(s).
|
|
20
|
+
"""
|
|
21
|
+
with np.errstate(invalid="ignore"):
|
|
22
|
+
return np.arctanh(r)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def fisher_z_to_r(z):
|
|
26
|
+
"""Convert Fisher z back to a correlation coefficient.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
z (float | np.ndarray): Fisher z value(s).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
np.ndarray: Correlation coefficient(s).
|
|
33
|
+
"""
|
|
34
|
+
return np.tanh(z)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Adapted from the scikit-learn RankSVM example by Fabian Pedregosa and
|
|
38
|
+
# Alexandre Gramfort (BSD licensed).
|
|
39
|
+
def transform_pairwise(X, y):
|
|
40
|
+
"""Transform data into pairwise differences with balanced labels for ranking.
|
|
41
|
+
|
|
42
|
+
Turns an n-class ranking problem into a two-class classification problem:
|
|
43
|
+
every pair of samples with different target values becomes one difference
|
|
44
|
+
row, and signs are flipped so that the -1 and +1 classes are balanced.
|
|
45
|
+
|
|
46
|
+
Reference: Herbrich, R., Graepel, T., & Obermayer, K. "Large Margin Rank
|
|
47
|
+
Boundaries for Ordinal Regression".
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
X (np.ndarray): Data, shape (n_samples, n_features).
|
|
51
|
+
y (np.ndarray): Target labels, shape (n_samples,) or (n_samples, 2). A
|
|
52
|
+
second column groups the samples; pairs from different groups are skipped.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
tuple[np.ndarray, np.ndarray]: `(X_trans, y_trans)`. `X_trans` has shape
|
|
56
|
+
(k, n_features) with one row per retained pair (k is at most
|
|
57
|
+
n_samples * (n_samples - 1) / 2; pairs are formed within groups when
|
|
58
|
+
given). `y_trans` holds the labels in {-1, +1}, shape (k,), or (k, 2)
|
|
59
|
+
with the group in the second column when `y` had two columns.
|
|
60
|
+
"""
|
|
61
|
+
X = np.asarray(X)
|
|
62
|
+
y = np.asarray(y)
|
|
63
|
+
y_ndim = y.ndim
|
|
64
|
+
if y_ndim == 1:
|
|
65
|
+
y = np.c_[y, np.ones(y.shape[0])]
|
|
66
|
+
|
|
67
|
+
# Pre-allocate lists (more efficient than repeated appends)
|
|
68
|
+
X_new = []
|
|
69
|
+
y_new = []
|
|
70
|
+
y_group = []
|
|
71
|
+
|
|
72
|
+
# Use itertools.combinations (necessary for pairwise combinations)
|
|
73
|
+
# Optimize by using pre-allocated lists and vectorized sign operation
|
|
74
|
+
comb = itertools.combinations(range(X.shape[0]), 2)
|
|
75
|
+
for k, (i, j) in enumerate(comb):
|
|
76
|
+
if y[i, 0] == y[j, 0] or y[i, 1] != y[j, 1]:
|
|
77
|
+
# skip if same target or different group
|
|
78
|
+
continue
|
|
79
|
+
X_new.append(X[i] - X[j])
|
|
80
|
+
sign_val = np.sign(y[i, 0] - y[j, 0])
|
|
81
|
+
# output balanced classes
|
|
82
|
+
if sign_val != (-1) ** k:
|
|
83
|
+
sign_val = -sign_val
|
|
84
|
+
X_new[-1] = -X_new[-1]
|
|
85
|
+
y_new.append(sign_val)
|
|
86
|
+
y_group.append(y[i, 1])
|
|
87
|
+
|
|
88
|
+
# Convert to arrays efficiently
|
|
89
|
+
if len(X_new) == 0:
|
|
90
|
+
if y_ndim == 1:
|
|
91
|
+
return np.array([]).reshape(0, X.shape[1]), np.array([])
|
|
92
|
+
return np.array([]).reshape(0, X.shape[1]), np.array([]).reshape(0, 2)
|
|
93
|
+
|
|
94
|
+
X_trans = np.array(X_new)
|
|
95
|
+
if y_ndim == 1:
|
|
96
|
+
return X_trans, np.array(y_new)
|
|
97
|
+
return X_trans, np.column_stack([np.array(y_new), np.array(y_group)])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def compute_similarity(data1, data2, metric="correlation"):
|
|
101
|
+
"""Compute row-wise similarity between two data arrays.
|
|
102
|
+
|
|
103
|
+
The array engine behind `BrainData.similarity`.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
data1 (np.ndarray): First data array, shape (n_samples1, n_features).
|
|
107
|
+
data2 (np.ndarray): Second data array, shape (n_samples2, n_features).
|
|
108
|
+
metric (str): 'correlation' (or 'pearson'), 'spearman' (or
|
|
109
|
+
'rank_correlation'), 'dot_product', or 'cosine'. Defaults to 'correlation'.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
np.ndarray: Similarities of shape (n_samples1, n_samples2), squeezed: a 1D
|
|
113
|
+
array when either input has a single row, a scalar when both do.
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
```python
|
|
117
|
+
data1 = np.random.randn(10, 100)
|
|
118
|
+
data2 = np.random.randn(5, 100)
|
|
119
|
+
sim = compute_similarity(data1, data2, metric="correlation")
|
|
120
|
+
sim.shape # → (10, 5)
|
|
121
|
+
```
|
|
122
|
+
"""
|
|
123
|
+
# Ensure 2D arrays
|
|
124
|
+
data1 = np.atleast_2d(data1)
|
|
125
|
+
data2 = np.atleast_2d(data2)
|
|
126
|
+
|
|
127
|
+
if metric == "dot_product":
|
|
128
|
+
# Vectorized dot product
|
|
129
|
+
if data2.shape[0] == 1:
|
|
130
|
+
out = np.dot(data1, data2.T).squeeze()
|
|
131
|
+
else:
|
|
132
|
+
out = np.dot(data1, data2.T)
|
|
133
|
+
elif metric in ["pearson", "correlation"]:
|
|
134
|
+
# Use np.corrcoef (BLAS-optimized) for Pearson correlation
|
|
135
|
+
stacked = np.vstack([data1, data2])
|
|
136
|
+
corr_matrix = np.corrcoef(stacked)
|
|
137
|
+
n_data1 = data1.shape[0]
|
|
138
|
+
n_data2 = data2.shape[0]
|
|
139
|
+
# Extract correlations between data1 rows and data2 rows
|
|
140
|
+
out = corr_matrix[:n_data1, n_data1 : n_data1 + n_data2]
|
|
141
|
+
out = out.squeeze()
|
|
142
|
+
elif metric in ["spearman", "rank_correlation"]:
|
|
143
|
+
# Spearman correlation: rank-transform then use np.corrcoef
|
|
144
|
+
data1_ranked = np.apply_along_axis(rankdata, axis=1, arr=data1)
|
|
145
|
+
data2_ranked = np.apply_along_axis(rankdata, axis=1, arr=data2)
|
|
146
|
+
stacked = np.vstack([data1_ranked, data2_ranked])
|
|
147
|
+
corr_matrix = np.corrcoef(stacked)
|
|
148
|
+
n_data1 = data1.shape[0]
|
|
149
|
+
n_data2 = data2.shape[0]
|
|
150
|
+
out = corr_matrix[:n_data1, n_data1 : n_data1 + n_data2]
|
|
151
|
+
out = out.squeeze()
|
|
152
|
+
elif metric == "cosine":
|
|
153
|
+
# Use cdist with cosine metric, then convert distance to similarity
|
|
154
|
+
out = cdist(data1, data2, metric="cosine").squeeze()
|
|
155
|
+
out = 1 - out # Convert distance to similarity
|
|
156
|
+
else:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"metric must be one of ['correlation', 'pearson', 'spearman', "
|
|
159
|
+
f"'rank_correlation', 'dot_product', 'cosine'], got '{metric}'"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _compute_multivariate_similarity(y, X, tail=2):
|
|
166
|
+
"""Compute multivariate similarity by regressing one pattern on several.
|
|
167
|
+
|
|
168
|
+
The array engine behind `BrainData.multivariate_similarity`: predicts the
|
|
169
|
+
spatial pattern `y` from a linear combination of the columns of `X` and
|
|
170
|
+
returns the OLS coefficients, t-statistics, p-values, and residuals.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
y (np.ndarray): Target pattern, shape (n_features,).
|
|
174
|
+
X (np.ndarray): Predictor patterns, shape (n_features, n_predictors) (the
|
|
175
|
+
transpose is accepted). An intercept column is always prepended, so
|
|
176
|
+
do not include one.
|
|
177
|
+
tail (int): 2 for two-sided p-values, 1 for an upper-tail test. Defaults to 2.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
dict: Keys 'beta' (coefficients, intercept first, shape (n_predictors + 1,)),
|
|
181
|
+
't' (t-statistics, same shape), 'p' (p-values, same shape), 'df'
|
|
182
|
+
(residual degrees of freedom), 'sigma' (residual standard deviation),
|
|
183
|
+
and 'residual' (residuals, shape (n_features,)).
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
```python
|
|
187
|
+
y = np.random.randn(100)
|
|
188
|
+
X = np.random.randn(100, 5)
|
|
189
|
+
result = _compute_multivariate_similarity(y, X)
|
|
190
|
+
result["beta"].shape # → (6,) 5 predictors + intercept
|
|
191
|
+
```
|
|
192
|
+
"""
|
|
193
|
+
# Ensure y is 1D
|
|
194
|
+
y = np.atleast_1d(y)
|
|
195
|
+
if y.ndim > 1:
|
|
196
|
+
raise ValueError(f"y must be 1D, got shape {y.shape}")
|
|
197
|
+
|
|
198
|
+
# Ensure X is 2D: (n_features, n_predictors)
|
|
199
|
+
X = np.atleast_2d(X)
|
|
200
|
+
if X.ndim != 2:
|
|
201
|
+
raise ValueError(f"X must be 2D, got shape {X.shape}")
|
|
202
|
+
|
|
203
|
+
# Check if X needs to be transposed (handle both orientations)
|
|
204
|
+
if X.shape[0] == y.shape[0]:
|
|
205
|
+
# X is (n_features, n_predictors) - correct orientation
|
|
206
|
+
pass
|
|
207
|
+
elif X.shape[1] == y.shape[0]:
|
|
208
|
+
# X is (n_predictors, n_features) - transpose needed
|
|
209
|
+
X = X.T
|
|
210
|
+
else:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"X must have shape (n_features, n_predictors) or (n_predictors, n_features), "
|
|
213
|
+
f"where n_features={y.shape[0]}, got shape {X.shape}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Add intercept (first column)
|
|
217
|
+
X_with_intercept = np.hstack([np.ones((X.shape[0], 1)), X])
|
|
218
|
+
|
|
219
|
+
b, _, t_out, p, _, res = regress(X_with_intercept, y, tail=tail)
|
|
220
|
+
|
|
221
|
+
n, p_cols = X_with_intercept.shape
|
|
222
|
+
df = n - p_cols
|
|
223
|
+
# Unbiased estimator of residual standard error: sqrt(RSS / df); correct for
|
|
224
|
+
# both intercept and intercept-free models. See GH #287.
|
|
225
|
+
sigma = float(np.sqrt(np.dot(res, res) / df))
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"beta": b,
|
|
229
|
+
"t": t_out,
|
|
230
|
+
"p": p,
|
|
231
|
+
"df": df,
|
|
232
|
+
"sigma": sigma,
|
|
233
|
+
"residual": res,
|
|
234
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""The argument vocabulary every algorithms family shares.
|
|
2
|
+
|
|
3
|
+
`tail` is spoken by inference, alignment and regression alike, and by the two
|
|
4
|
+
facade methods that expose a permutation test (`Roc.summary`,
|
|
5
|
+
`Adjacency.ttest`), so its validator and the p-value computation built on it
|
|
6
|
+
live here rather than inside any one family.
|
|
7
|
+
|
|
8
|
+
Examples:
|
|
9
|
+
```python
|
|
10
|
+
from nltools.algorithms.validation import _validate_tail_parameter
|
|
11
|
+
|
|
12
|
+
_validate_tail_parameter(2) # → 'two'
|
|
13
|
+
_validate_tail_parameter("invalid") # raises ValueError
|
|
14
|
+
```
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _validate_tail_parameter(tail: int | str) -> str:
|
|
21
|
+
"""Validate the public tail vocabulary and normalize to the internal form.
|
|
22
|
+
|
|
23
|
+
The public vocabulary is deliberately two-valued: the *direction* of a
|
|
24
|
+
one-tailed test is fixed by the test's convention, never chosen from the
|
|
25
|
+
data (a data-driven direction would silently halve every p-value). A fixed
|
|
26
|
+
direction across all tests is what keeps multiple-comparison correction
|
|
27
|
+
(FDR, Bonferroni) valid (GH #315).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
tail (int | str): `2` or `'two'` (the default everywhere) for a
|
|
31
|
+
two-tailed test (`|obs|` vs `|null|`); `1` or `'one'` for a
|
|
32
|
+
one-tailed test in the test's canonical positive direction
|
|
33
|
+
(correlation/ISC/similarity > 0, mean > popmean, group1 > group2).
|
|
34
|
+
To test the negative direction, negate your data, swap the groups,
|
|
35
|
+
or flip the contrast.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
str: Normalized internal tail, `'two'` or `'upper'`.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If `tail` is not a valid option (including the removed
|
|
42
|
+
`'upper'`/`'lower'`/`-1` forms).
|
|
43
|
+
"""
|
|
44
|
+
# One mapping table lives in `_normalize_tail_internal`; the public layer
|
|
45
|
+
# only rejects the internal-only directional forms it must not accept.
|
|
46
|
+
if tail not in (-1, "upper", "lower"):
|
|
47
|
+
try:
|
|
48
|
+
return _normalize_tail_internal(tail)
|
|
49
|
+
except ValueError:
|
|
50
|
+
pass
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"tail must be 2|'two' (two-tailed) or 1|'one' (one-tailed, the test's "
|
|
53
|
+
f"positive direction), got {tail!r}. The 'upper'/'lower'/-1 forms were "
|
|
54
|
+
"removed in v0.6.0: to test the negative direction, negate your data, "
|
|
55
|
+
"swap the groups, or flip the contrast."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _normalize_tail_internal(tail: int | str) -> str:
|
|
60
|
+
"""Normalize a tail value to the internal 'two'/'upper'/'lower' form.
|
|
61
|
+
|
|
62
|
+
Accepts BOTH the public v0.6.0 vocabulary (2 or 'two', 1 or 'one') and the
|
|
63
|
+
internal directional forms ('upper', 'lower', -1) that forced-tail call
|
|
64
|
+
sites use directly. Public entry points must validate with the strict
|
|
65
|
+
`_validate_tail_parameter` first — this permissive form exists only so
|
|
66
|
+
`_compute_pvalue` can serve both layers.
|
|
67
|
+
"""
|
|
68
|
+
if tail == 2 or tail == "two":
|
|
69
|
+
return "two"
|
|
70
|
+
if tail == 1 or tail == "one" or tail == "upper":
|
|
71
|
+
return "upper"
|
|
72
|
+
if tail == -1 or tail == "lower":
|
|
73
|
+
return "lower"
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"tail must be 2|'two', 1|'one' (or internal 'upper'/'lower'/-1), got {tail!r}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _compute_pvalue(
|
|
80
|
+
obs_stat: np.ndarray,
|
|
81
|
+
null_dist: np.ndarray,
|
|
82
|
+
tail: int | str = 2,
|
|
83
|
+
) -> np.ndarray:
|
|
84
|
+
"""Calculate p-values from observed statistic and null distribution.
|
|
85
|
+
|
|
86
|
+
Computes the proportion of null-distribution values at least as extreme as
|
|
87
|
+
the observed statistic, with the `(count + 1) / (n_permute + 1)` correction:
|
|
88
|
+
the observed value counts as one draw, so p is never 0 and its minimum is
|
|
89
|
+
`1 / (n_permute + 1)`. Two-tailed tests compare absolute values. The fixed
|
|
90
|
+
per-tail direction keeps multiple-comparison correction (FDR, Bonferroni)
|
|
91
|
+
valid across tests (GH #315).
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
obs_stat (np.ndarray): Observed statistic(s), shape `()` for a scalar or
|
|
95
|
+
`(n_features,)` for multi-feature.
|
|
96
|
+
null_dist (np.ndarray): Null distribution from permutations, shape
|
|
97
|
+
`(n_permute,)` for a single feature or `(n_permute, n_features)`
|
|
98
|
+
for multi-feature.
|
|
99
|
+
tail (int | str): `2` or `'two'` for a two-tailed test (`|obs|` vs
|
|
100
|
+
`|null|`); `1`, `'one'`, or the internal `'upper'` for a one-tailed
|
|
101
|
+
upper test (`null >= obs`, positive effects); the internal `'lower'`
|
|
102
|
+
or `-1` for a one-tailed lower test (`null <= obs`), reserved for
|
|
103
|
+
forced-tail call sites and not part of the public vocabulary.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
np.ndarray: P-value(s) with the same shape as `obs_stat`.
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
```python
|
|
110
|
+
obs_stat = np.array([2.5])
|
|
111
|
+
null_dist = np.random.randn(1000, 1)
|
|
112
|
+
p = _compute_pvalue(obs_stat, null_dist, tail="two")
|
|
113
|
+
0 < p <= 1 # → True
|
|
114
|
+
|
|
115
|
+
# Explicit directions for forced-tail call sites
|
|
116
|
+
p_upper = _compute_pvalue(obs_stat, null_dist, tail="upper")
|
|
117
|
+
p_lower = _compute_pvalue(obs_stat, null_dist, tail="lower")
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
120
|
+
tail_normalized = _normalize_tail_internal(tail)
|
|
121
|
+
|
|
122
|
+
# Ensure inputs are numpy arrays (handles Python float/int scalars)
|
|
123
|
+
obs_stat = np.asarray(obs_stat)
|
|
124
|
+
null_dist = np.asarray(null_dist)
|
|
125
|
+
|
|
126
|
+
# Handle shape differences
|
|
127
|
+
if null_dist.ndim == 1:
|
|
128
|
+
null_dist = null_dist[:, np.newaxis]
|
|
129
|
+
if obs_stat.ndim == 0:
|
|
130
|
+
obs_stat = obs_stat.reshape(1)
|
|
131
|
+
elif obs_stat.ndim == 1:
|
|
132
|
+
obs_stat = obs_stat.reshape(1, -1)
|
|
133
|
+
|
|
134
|
+
n_permute = null_dist.shape[0]
|
|
135
|
+
denom = float(n_permute) + 1.0
|
|
136
|
+
|
|
137
|
+
if tail_normalized == "upper":
|
|
138
|
+
# One-tailed upper: count how many null >= observed
|
|
139
|
+
# Tests for positive effects (H1: statistic > 0)
|
|
140
|
+
numer = np.sum(null_dist >= obs_stat, axis=0) + 1.0
|
|
141
|
+
elif tail_normalized == "lower":
|
|
142
|
+
# One-tailed lower: count how many null <= observed
|
|
143
|
+
# Tests for negative effects (H1: statistic < 0)
|
|
144
|
+
numer = np.sum(null_dist <= obs_stat, axis=0) + 1.0
|
|
145
|
+
else: # tail_normalized == "two"
|
|
146
|
+
# Two-tailed: count how many |null| >= |observed|
|
|
147
|
+
numer = np.sum(np.abs(null_dist) >= np.abs(obs_stat), axis=0) + 1.0
|
|
148
|
+
|
|
149
|
+
p_values = numer / denom
|
|
150
|
+
|
|
151
|
+
return p_values
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Scikit-learn-compatible cross-validation data classes."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["KFoldStratified"]
|
|
4
|
+
|
|
5
|
+
from sklearn.model_selection._split import _BaseKFold
|
|
6
|
+
from sklearn.utils import check_random_state
|
|
7
|
+
from sklearn.utils.validation import check_array
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KFoldStratified(_BaseKFold):
|
|
12
|
+
"""Stratify continuous targets across K-fold cross-validation.
|
|
13
|
+
|
|
14
|
+
Unlike the scikit-learn equivalent, this iterator stratifies continuous data.
|
|
15
|
+
|
|
16
|
+
Provides train/test indices to split data in train test sets. Samples are
|
|
17
|
+
ordered by their continuous target `y` and dealt round-robin into k folds
|
|
18
|
+
so each fold spans the full range of `y`. Each fold is then used as a
|
|
19
|
+
validation set once while the k - 1 remaining folds form the training set.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
n_splits (int): Number of folds. Must be at least 2. Defaults to 3.
|
|
23
|
+
shuffle (bool): Whether to break ties in `y` randomly before dealing samples
|
|
24
|
+
into folds. Default False.
|
|
25
|
+
random_state (int | np.random.RandomState, optional): Seed or RandomState for
|
|
26
|
+
the tie-break shuffle. If None, use the default numpy RNG.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, n_splits=3, *, shuffle=False, random_state=None):
|
|
30
|
+
super().__init__(n_splits=n_splits, shuffle=shuffle, random_state=random_state)
|
|
31
|
+
|
|
32
|
+
def _make_test_folds(self, X, y=None, groups=None):
|
|
33
|
+
y_arr = np.asarray(y).ravel()
|
|
34
|
+
n = len(y_arr)
|
|
35
|
+
if self.shuffle:
|
|
36
|
+
# Sort by y (stratification) but break ties randomly so that
|
|
37
|
+
# shuffle/random_state actually vary the fold assignment. lexsort
|
|
38
|
+
# uses the last key as the primary sort, so y_arr stays primary and
|
|
39
|
+
# the random tiebreak only reorders samples that share a y value.
|
|
40
|
+
rng = check_random_state(self.random_state)
|
|
41
|
+
tiebreak = rng.permutation(n)
|
|
42
|
+
order = np.lexsort((tiebreak, y_arr))
|
|
43
|
+
else:
|
|
44
|
+
order = np.argsort(y_arr, kind="stable")
|
|
45
|
+
test_folds = np.full(n, np.nan)
|
|
46
|
+
for k in range(self.n_splits):
|
|
47
|
+
test_folds[order[np.arange(k, n, self.n_splits)]] = k
|
|
48
|
+
return test_folds
|
|
49
|
+
|
|
50
|
+
def _iter_test_masks(self, X=None, y=None, groups=None):
|
|
51
|
+
test_folds = self._make_test_folds(X, y)
|
|
52
|
+
for i in range(self.n_splits):
|
|
53
|
+
yield test_folds == i
|
|
54
|
+
|
|
55
|
+
def split(self, X, y=None, groups=None):
|
|
56
|
+
"""Generate indices to split data into training and test set.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
X (array-like): Training data of shape `(n_samples, n_features)`. Only
|
|
60
|
+
`y` is needed to generate the splits, so `np.zeros(n_samples)` works
|
|
61
|
+
as a placeholder.
|
|
62
|
+
y (array-like): Continuous target of shape `(n_samples,)`; stratification
|
|
63
|
+
is based on its ordering.
|
|
64
|
+
groups (array-like, optional): Always ignored; exists for sklearn
|
|
65
|
+
compatibility.
|
|
66
|
+
|
|
67
|
+
Yields:
|
|
68
|
+
tuple[np.ndarray, np.ndarray]: `(train, test)` — the training set indices
|
|
69
|
+
and the testing set indices for that split.
|
|
70
|
+
"""
|
|
71
|
+
y = check_array(y, ensure_2d=False, dtype=None)
|
|
72
|
+
return super().split(X, y, groups)
|
nltools/data/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Data classes for neuroimaging analysis.
|
|
2
|
+
|
|
3
|
+
`BrainData` (masked voxel data), `Adjacency` (similarity/distance matrices),
|
|
4
|
+
`DesignMatrix` (regressors), the `Roc` analysis class, the
|
|
5
|
+
`Simulator`/`SimulateGrid` data simulators, and the frozen result records those
|
|
6
|
+
classes return (`Predict`, `BootstrapResult`, `ContrastResult`) plus the
|
|
7
|
+
brain-space configuration record (`BrainSpaceConfig`).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .braindata import BrainData
|
|
11
|
+
from .adjacency import Adjacency
|
|
12
|
+
from .designmatrix import DesignMatrix
|
|
13
|
+
from .results import BootstrapResult, Predict
|
|
14
|
+
from .simulator import Simulator, SimulateGrid
|
|
15
|
+
from .roc import Roc
|
|
16
|
+
from nltools.models.results import ContrastResult
|
|
17
|
+
from nltools.templates.config import BrainSpaceConfig
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Adjacency",
|
|
21
|
+
"BootstrapResult",
|
|
22
|
+
"BrainData",
|
|
23
|
+
"BrainSpaceConfig",
|
|
24
|
+
"ContrastResult",
|
|
25
|
+
"DesignMatrix",
|
|
26
|
+
"Predict",
|
|
27
|
+
"Roc",
|
|
28
|
+
"SimulateGrid",
|
|
29
|
+
"Simulator",
|
|
30
|
+
]
|