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,596 @@
|
|
|
1
|
+
"""Provide standalone statistical functions for Adjacency matrices.
|
|
2
|
+
|
|
3
|
+
Each function takes an Adjacency instance as its first argument (`adj`).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _similarity(
|
|
10
|
+
adj,
|
|
11
|
+
data,
|
|
12
|
+
plot=False,
|
|
13
|
+
method="2d",
|
|
14
|
+
n_permute=5000,
|
|
15
|
+
metric="spearman",
|
|
16
|
+
include_diag=False,
|
|
17
|
+
nan_policy="omit",
|
|
18
|
+
tail=2,
|
|
19
|
+
return_null=False,
|
|
20
|
+
n_jobs=-1,
|
|
21
|
+
random_state=None,
|
|
22
|
+
*,
|
|
23
|
+
progress_bar: bool = False,
|
|
24
|
+
):
|
|
25
|
+
"""Calculate similarity between two Adjacency matrices.
|
|
26
|
+
|
|
27
|
+
The default uses Spearman correlation and a permutation test.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
adj (Adjacency): Adjacency instance.
|
|
31
|
+
data (Adjacency | np.ndarray): Adjacency to compare against, or a 1-D array the
|
|
32
|
+
same size as `adj.data`.
|
|
33
|
+
plot (bool): If True, plot stacked adjacency matrices. Default False.
|
|
34
|
+
method (str | None): Permutation scheme, `'1d'`, `'2d'`, or None (no
|
|
35
|
+
permutation test).
|
|
36
|
+
n_permute (int): Number of permutations. Default 5000.
|
|
37
|
+
metric (str): `'spearman'`, `'pearson'`, or `'kendall'`.
|
|
38
|
+
include_diag (bool): Only applies to `'directed'` matrices with `method=None`
|
|
39
|
+
or `method='1d'`. Default False (self-similarity is uninformative).
|
|
40
|
+
Symmetric matrices never store the diagonal, so this flag is a no-op
|
|
41
|
+
for them.
|
|
42
|
+
nan_policy (str): How to handle NaN values on the 1-D paths
|
|
43
|
+
(`method='1d'` or `method=None`): `'omit'` removes NaN pairwise before
|
|
44
|
+
computing the correlation (default), `'propagate'` lets NaN flow
|
|
45
|
+
through, `'raise'` errors if any NaN is present. `method='2d'` raises
|
|
46
|
+
on any NaN whatever the policy.
|
|
47
|
+
tail (int | str): `2`/`'two'` (two-tailed, default) or `1`/`'one'` (one-tailed, positive direction).
|
|
48
|
+
return_null (bool): If True, also return the null distribution. Default False.
|
|
49
|
+
n_jobs (int): Number of parallel jobs. -1 means all cores. Default -1.
|
|
50
|
+
random_state (int, optional): Random seed for reproducibility.
|
|
51
|
+
progress_bar (bool): If True, show a progress bar. Default False.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
dict | list[dict]: A correlation result dict with keys 'correlation'
|
|
55
|
+
and 'p', or a list of these dicts for a stack.
|
|
56
|
+
"""
|
|
57
|
+
from nltools.data.adjacency import Adjacency
|
|
58
|
+
from nltools.algorithms.inference import (
|
|
59
|
+
correlation_permutation_test,
|
|
60
|
+
matrix_permutation_test,
|
|
61
|
+
)
|
|
62
|
+
from nltools.plotting import _plot_stacked_adjacency
|
|
63
|
+
|
|
64
|
+
if nan_policy not in ("omit", "propagate", "raise"):
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"nan_policy must be 'omit', 'propagate', or 'raise', got {nan_policy!r}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _handle_nans(arr1, arr2, nan_policy):
|
|
70
|
+
"""Apply `nan_policy` to the 1-D inputs; reject NaN outright for 2-D."""
|
|
71
|
+
arr1 = np.asarray(arr1)
|
|
72
|
+
arr2 = np.asarray(arr2)
|
|
73
|
+
|
|
74
|
+
if not np.any(np.isnan(arr1)) and not np.any(np.isnan(arr2)):
|
|
75
|
+
return arr1, arr2
|
|
76
|
+
|
|
77
|
+
if arr1.ndim == 2:
|
|
78
|
+
# The matrix permutation shuffles whole rows and columns together,
|
|
79
|
+
# so no policy makes a 2-D correlation over NaN edges meaningful.
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"Input contains NaN values, which method='2d' cannot handle. "
|
|
82
|
+
"Use method='1d' (or method=None), which masks NaN pairwise, "
|
|
83
|
+
"or remove the NaN values before calling similarity()."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if nan_policy == "raise":
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"Input contains NaN values. Use nan_policy='omit' to ignore them "
|
|
89
|
+
"or nan_policy='propagate' to allow NaN in results."
|
|
90
|
+
)
|
|
91
|
+
if nan_policy == "propagate":
|
|
92
|
+
return arr1, arr2
|
|
93
|
+
|
|
94
|
+
mask = ~(np.isnan(arr1) | np.isnan(arr2))
|
|
95
|
+
if not np.any(mask):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"All values are NaN after pairwise removal. Cannot compute similarity."
|
|
98
|
+
)
|
|
99
|
+
return arr1[mask], arr2[mask]
|
|
100
|
+
|
|
101
|
+
data1 = adj.copy()
|
|
102
|
+
if not isinstance(data, Adjacency):
|
|
103
|
+
data2 = Adjacency(data)
|
|
104
|
+
else:
|
|
105
|
+
data2 = data.copy()
|
|
106
|
+
|
|
107
|
+
if method is None:
|
|
108
|
+
n_permute = 0
|
|
109
|
+
similarity_func = correlation_permutation_test
|
|
110
|
+
elif method == "1d":
|
|
111
|
+
similarity_func = correlation_permutation_test
|
|
112
|
+
elif method == "2d":
|
|
113
|
+
similarity_func = matrix_permutation_test
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError("method must be ['1d','2d', or None']")
|
|
116
|
+
|
|
117
|
+
def _convert_data_similarity(
|
|
118
|
+
data, permutation_method=None, include_diag=include_diag
|
|
119
|
+
):
|
|
120
|
+
"""Convert data to the representation required for similarity."""
|
|
121
|
+
if (permutation_method is None) or (permutation_method == "1d"):
|
|
122
|
+
if not include_diag and (not data.issymmetric):
|
|
123
|
+
d = data.squareform()
|
|
124
|
+
data = d[~np.eye(d.shape[0]).astype(bool)]
|
|
125
|
+
else:
|
|
126
|
+
data = data.data
|
|
127
|
+
elif permutation_method == "2d":
|
|
128
|
+
if not data.issymmetric:
|
|
129
|
+
raise TypeError(
|
|
130
|
+
f"data must be symmetric to do {permutation_method} permutation"
|
|
131
|
+
)
|
|
132
|
+
data = data.squareform()
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError("permutation_method must be ['1d','2d', or None']")
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
if adj.is_single_matrix:
|
|
138
|
+
if plot:
|
|
139
|
+
_plot_stacked_adjacency(adj, data)
|
|
140
|
+
arr1 = _convert_data_similarity(data1, permutation_method=method)
|
|
141
|
+
arr2 = _convert_data_similarity(data2, permutation_method=method)
|
|
142
|
+
arr1, arr2 = _handle_nans(arr1, arr2, nan_policy)
|
|
143
|
+
return similarity_func(
|
|
144
|
+
arr1,
|
|
145
|
+
arr2,
|
|
146
|
+
metric=metric,
|
|
147
|
+
n_permute=n_permute,
|
|
148
|
+
tail=tail,
|
|
149
|
+
return_null=return_null,
|
|
150
|
+
n_jobs=n_jobs,
|
|
151
|
+
random_state=random_state,
|
|
152
|
+
progress_bar=progress_bar,
|
|
153
|
+
)
|
|
154
|
+
if plot:
|
|
155
|
+
import matplotlib.pyplot as plt
|
|
156
|
+
|
|
157
|
+
_, a = plt.subplots(len(adj))
|
|
158
|
+
for i in a:
|
|
159
|
+
_plot_stacked_adjacency(adj, data, ax=i)
|
|
160
|
+
results = []
|
|
161
|
+
arr2_base = _convert_data_similarity(data2, permutation_method=method)
|
|
162
|
+
for x in adj:
|
|
163
|
+
arr1 = _convert_data_similarity(x, permutation_method=method)
|
|
164
|
+
arr1_clean, arr2_clean = _handle_nans(arr1, arr2_base, nan_policy)
|
|
165
|
+
results.append(
|
|
166
|
+
similarity_func(
|
|
167
|
+
arr1_clean,
|
|
168
|
+
arr2_clean,
|
|
169
|
+
metric=metric,
|
|
170
|
+
n_permute=n_permute,
|
|
171
|
+
tail=tail,
|
|
172
|
+
return_null=return_null,
|
|
173
|
+
n_jobs=n_jobs,
|
|
174
|
+
random_state=random_state,
|
|
175
|
+
progress_bar=progress_bar,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
return results
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _r_to_z(adj):
|
|
182
|
+
"""Apply Fisher's r to z transformation to each element of the data object.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
adj (Adjacency): Adjacency instance.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Adjacency: New Adjacency with z-transformed values.
|
|
189
|
+
"""
|
|
190
|
+
from nltools.algorithms.similarity import fisher_r_to_z
|
|
191
|
+
|
|
192
|
+
out = adj.copy()
|
|
193
|
+
out.data = fisher_r_to_z(out.data)
|
|
194
|
+
return out
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _z_to_r(adj):
|
|
198
|
+
"""Convert z score back into r value for each element of data object.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
adj (Adjacency): Adjacency instance.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Adjacency: New Adjacency with r values.
|
|
205
|
+
"""
|
|
206
|
+
from nltools.algorithms.similarity import fisher_z_to_r
|
|
207
|
+
|
|
208
|
+
out = adj.copy()
|
|
209
|
+
out.data = fisher_z_to_r(out.data)
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _threshold(adj, *, upper=None, lower=None, binarize=False):
|
|
214
|
+
"""Threshold an Adjacency instance.
|
|
215
|
+
|
|
216
|
+
Provide upper and lower values or percentages to perform two-sided
|
|
217
|
+
thresholding. Binarize will return a mask image respecting thresholds if
|
|
218
|
+
provided, otherwise respecting every non-zero value.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
adj (Adjacency): Adjacency instance.
|
|
222
|
+
upper (float | str, optional): Upper cutoff. A string such as `'95%'` is
|
|
223
|
+
interpreted as a percentile; None for one-sided thresholding.
|
|
224
|
+
lower (float | str, optional): Lower cutoff. A string such as `'5%'` is
|
|
225
|
+
interpreted as a percentile; None for one-sided thresholding.
|
|
226
|
+
binarize (bool): Return a binarized matrix respecting the thresholds if
|
|
227
|
+
provided, otherwise binarize on every non-zero value. Default False.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Adjacency: Thresholded Adjacency instance.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
b = adj.copy()
|
|
234
|
+
if isinstance(upper, str) and upper[-1] == "%":
|
|
235
|
+
upper = np.percentile(b.data, float(upper[:-1]))
|
|
236
|
+
if isinstance(lower, str) and lower[-1] == "%":
|
|
237
|
+
lower = np.percentile(b.data, float(lower[:-1]))
|
|
238
|
+
|
|
239
|
+
if upper is not None and lower is not None:
|
|
240
|
+
b.data[(b.data < upper) & (b.data > lower)] = 0
|
|
241
|
+
elif upper is not None:
|
|
242
|
+
b.data[b.data < upper] = 0
|
|
243
|
+
elif lower is not None:
|
|
244
|
+
b.data[b.data > lower] = 0
|
|
245
|
+
if binarize:
|
|
246
|
+
b.data[b.data != 0] = 1
|
|
247
|
+
return b
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _ttest(
|
|
251
|
+
adj,
|
|
252
|
+
*,
|
|
253
|
+
popmean=0.0,
|
|
254
|
+
permutation=False,
|
|
255
|
+
n_permute=5000,
|
|
256
|
+
tail=2,
|
|
257
|
+
return_null=False,
|
|
258
|
+
n_jobs=-1,
|
|
259
|
+
random_state=None,
|
|
260
|
+
progress_bar=False,
|
|
261
|
+
):
|
|
262
|
+
"""Run a one-sample t-test across stacked matrices.
|
|
263
|
+
|
|
264
|
+
Tests every stored edge against `popmean` across the matrices in the stack.
|
|
265
|
+
Delegates the statistics to the shared one-sample contract in
|
|
266
|
+
`nltools.algorithms.inference.one_sample`.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
adj (Adjacency): Stack of two or more matrices with the same node order
|
|
270
|
+
and storage kind.
|
|
271
|
+
popmean (float): Population mean to test against. Default 0.0.
|
|
272
|
+
permutation (bool): If True, take p from a sign-flip permutation test on
|
|
273
|
+
`matrices - popmean`. The reported `t` stays the observed parametric
|
|
274
|
+
statistic. Default False.
|
|
275
|
+
n_permute (int): Number of permutations, used only when
|
|
276
|
+
`permutation=True`. Default 5000.
|
|
277
|
+
tail (int | str): `2`/`'two'` (two-tailed, default) or `1`/`'one'`
|
|
278
|
+
(one-tailed: mean > `popmean`).
|
|
279
|
+
return_null (bool): If True, also return the permutation null. Has no
|
|
280
|
+
effect on the parametric path, which computes no null. Default False.
|
|
281
|
+
n_jobs (int): Number of parallel jobs. Default -1 (all cores).
|
|
282
|
+
random_state (int, optional): Random seed for reproducibility.
|
|
283
|
+
progress_bar (bool): If True, show a progress bar. Default False.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
dict: `'mean'`, `'t'`, `'z'` and `'p'` as independent single-matrix
|
|
287
|
+
`Adjacency` results that retain the node count, storage kind
|
|
288
|
+
(including directed) and shared node labels, with matrix metadata
|
|
289
|
+
cleared. `'mean'` is the edgewise mean minus `popmean`; `'t'` is the
|
|
290
|
+
observed one-sample t-statistic on both paths; `'p'` is parametric,
|
|
291
|
+
or the empirical sign-flip p-value when `permutation=True`; `'z'` is
|
|
292
|
+
the tail-aware normal score of `p`. With `permutation=True` and
|
|
293
|
+
`return_null=True` the dict also holds `'null_dist'`, an owned
|
|
294
|
+
`(n_permute, n_edges)` array of centered means in flat storage
|
|
295
|
+
order and in the units of `'mean'`. Maps are unthresholded. Apply a
|
|
296
|
+
cutoff or a multiple-comparison correction afterwards.
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
ValueError: If `adj` holds fewer than two matrices.
|
|
300
|
+
"""
|
|
301
|
+
import polars as pl
|
|
302
|
+
|
|
303
|
+
from nltools.algorithms.inference.one_sample import _one_sample_statistics
|
|
304
|
+
|
|
305
|
+
from .state import _common_labels, _result
|
|
306
|
+
|
|
307
|
+
if adj.is_single_matrix or adj.data.shape[0] < 2:
|
|
308
|
+
raise ValueError(
|
|
309
|
+
"t-test requires multiple matrices (got fewer than 2). "
|
|
310
|
+
"Stack matrices into a single Adjacency first."
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
stats = _one_sample_statistics(
|
|
314
|
+
adj.data,
|
|
315
|
+
popmean=popmean,
|
|
316
|
+
permutation=permutation,
|
|
317
|
+
n_permute=n_permute,
|
|
318
|
+
tail=tail,
|
|
319
|
+
return_null=return_null,
|
|
320
|
+
n_jobs=n_jobs,
|
|
321
|
+
random_state=random_state,
|
|
322
|
+
progress_bar=progress_bar,
|
|
323
|
+
)
|
|
324
|
+
labels = _common_labels(adj)
|
|
325
|
+
results = {
|
|
326
|
+
key: _result(adj, stats[key], labels=labels, Y=pl.DataFrame())
|
|
327
|
+
for key in ("mean", "t", "z", "p")
|
|
328
|
+
}
|
|
329
|
+
if "null_dist" in stats:
|
|
330
|
+
results["null_dist"] = stats["null_dist"]
|
|
331
|
+
return results
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _label_distance_long(adj, labels):
|
|
335
|
+
"""Build long-format within/between distance arrays for a labelled adjacency.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
dict: Keys ``Distance`` (1-D float array), ``Type`` (1-D object array of
|
|
339
|
+
"Within"/"Between"), ``Group`` (1-D array of label values).
|
|
340
|
+
"""
|
|
341
|
+
distance = np.asarray(adj.squareform())
|
|
342
|
+
labels = np.asarray(labels)
|
|
343
|
+
if len(labels) != distance.shape[0]:
|
|
344
|
+
raise ValueError("Labels must be same length as distance matrix")
|
|
345
|
+
|
|
346
|
+
dist_parts, type_parts, group_parts = [], [], []
|
|
347
|
+
for i in np.unique(labels):
|
|
348
|
+
mask_i = labels == i
|
|
349
|
+
sub = distance[np.ix_(mask_i, mask_i)]
|
|
350
|
+
within_vals = sub[np.triu_indices(mask_i.sum(), k=1)]
|
|
351
|
+
between_vals = distance[np.ix_(mask_i, ~mask_i)].ravel()
|
|
352
|
+
|
|
353
|
+
dist_parts.append(within_vals)
|
|
354
|
+
type_parts.append(np.full(within_vals.shape, "Within", dtype=object))
|
|
355
|
+
group_parts.append(np.full(within_vals.shape, i))
|
|
356
|
+
|
|
357
|
+
dist_parts.append(between_vals)
|
|
358
|
+
type_parts.append(np.full(between_vals.shape, "Between", dtype=object))
|
|
359
|
+
group_parts.append(np.full(between_vals.shape, i))
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
"Distance": np.concatenate(dist_parts),
|
|
363
|
+
"Type": np.concatenate(type_parts),
|
|
364
|
+
"Group": np.concatenate(group_parts),
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _label_distance_inputs(adj, labels):
|
|
369
|
+
"""Return the square distance matrix and node labels the label plots take.
|
|
370
|
+
|
|
371
|
+
Both label-distance plots are drawn by `nltools.plotting.adjacency` from a
|
|
372
|
+
square matrix and a label vector, so the squareform, the stored-label
|
|
373
|
+
fallback, and the single-matrix rule live here once.
|
|
374
|
+
"""
|
|
375
|
+
from copy import deepcopy
|
|
376
|
+
|
|
377
|
+
if not adj.is_single_matrix:
|
|
378
|
+
raise ValueError("This function only works on single adjacency matrices.")
|
|
379
|
+
|
|
380
|
+
distance = adj.squareform()
|
|
381
|
+
if labels is None:
|
|
382
|
+
labels = np.array(deepcopy(adj.labels))
|
|
383
|
+
labels = np.asarray(labels)
|
|
384
|
+
if len(labels) != distance.shape[0]:
|
|
385
|
+
raise ValueError("Labels must be same length as distance matrix")
|
|
386
|
+
return distance, labels
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _plot_label_distance( # nosemgrep: kwargs-internal-forwarding # forwards to seaborn via _plot_mean_label_distance
|
|
390
|
+
adj, labels=None, ax=None, *, permutation_test=False, n_permute=5000, **kwargs
|
|
391
|
+
):
|
|
392
|
+
"""Create a violin plot of within- and between-label distances.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
adj (Adjacency): Adjacency instance (must be a single matrix).
|
|
396
|
+
labels (np.ndarray, optional): Group label per node; defaults to `adj.labels`.
|
|
397
|
+
ax (matplotlib.axes.Axes, optional): Axis to draw on.
|
|
398
|
+
permutation_test (bool): Run a two-sample permutation test of within
|
|
399
|
+
against between distance for each group. Default False.
|
|
400
|
+
n_permute (int): Number of permutations for the test. Default 5000.
|
|
401
|
+
**kwargs (dict): Forwarded to `seaborn.violinplot`, plus `fontsize` for
|
|
402
|
+
the axis label and title (default 18).
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
pl.DataFrame | tuple[pl.DataFrame, dict]: The long-format frame with
|
|
406
|
+
columns `Distance`, `Type`, `Group`, or `(long_df, stats)` when
|
|
407
|
+
`permutation_test=True`, where `stats` maps each group label to its
|
|
408
|
+
permutation-test result.
|
|
409
|
+
"""
|
|
410
|
+
from nltools.plotting import _plot_mean_label_distance
|
|
411
|
+
|
|
412
|
+
distance, labels = _label_distance_inputs(adj, labels)
|
|
413
|
+
return _plot_mean_label_distance(
|
|
414
|
+
distance,
|
|
415
|
+
labels,
|
|
416
|
+
ax=ax,
|
|
417
|
+
permutation_test=permutation_test,
|
|
418
|
+
n_permute=n_permute,
|
|
419
|
+
**kwargs,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _plot_between_label_distance( # nosemgrep: kwargs-internal-forwarding # forwards to seaborn via plot_between_label_distance
|
|
424
|
+
adj, *, labels=None, ax=None, permutation_test=True, n_permute=5000, **kwargs
|
|
425
|
+
):
|
|
426
|
+
"""Create a heatmap of the average distance between every pair of labels.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
adj (Adjacency): Adjacency instance (must be a single matrix).
|
|
430
|
+
labels (np.ndarray, optional): Group label per node; defaults to `adj.labels`.
|
|
431
|
+
ax (matplotlib.axes.Axes, optional): Axis to draw on.
|
|
432
|
+
permutation_test (bool): Also compute the mean-difference and p-value
|
|
433
|
+
matrices from a two-sample permutation test. Default True.
|
|
434
|
+
n_permute (int): Number of permutations for the test. Default 5000.
|
|
435
|
+
**kwargs (dict): Forwarded to `seaborn.heatmap`.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
tuple[pl.DataFrame, ...]: `(long_df, within_mean_df)` without the
|
|
439
|
+
permutation test, or `(long_df, within_mean_df, mean_diff_df, p_df)`
|
|
440
|
+
with it.
|
|
441
|
+
"""
|
|
442
|
+
from nltools.plotting import _plot_between_label_distance as _plot_between
|
|
443
|
+
|
|
444
|
+
distance, labels = _label_distance_inputs(adj, labels)
|
|
445
|
+
return _plot_between(
|
|
446
|
+
distance,
|
|
447
|
+
labels,
|
|
448
|
+
ax=ax,
|
|
449
|
+
permutation_test=permutation_test,
|
|
450
|
+
n_permute=n_permute,
|
|
451
|
+
**kwargs,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _stats_label_distance(
|
|
456
|
+
adj, *, labels=None, n_permute=5000, n_jobs=-1, progress_bar=False
|
|
457
|
+
):
|
|
458
|
+
"""Calculate permutation tests on within and between label distance.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
adj (Adjacency): Adjacency instance (must be a single matrix).
|
|
462
|
+
labels (np.ndarray, optional): Group label per node; defaults to `adj.labels`.
|
|
463
|
+
n_permute (int): Number of permutations to run. Default 5000.
|
|
464
|
+
n_jobs (int): Number of parallel jobs. Default -1 (all cores).
|
|
465
|
+
progress_bar (bool): If True, show a progress bar. Default False.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
dict: Per-group within-vs-between distance differences and p-values, keyed by
|
|
469
|
+
group label.
|
|
470
|
+
"""
|
|
471
|
+
from copy import deepcopy
|
|
472
|
+
|
|
473
|
+
from nltools.algorithms.inference import two_sample_permutation_test
|
|
474
|
+
|
|
475
|
+
if not adj.is_single_matrix:
|
|
476
|
+
raise ValueError("This function only works on single adjacency matrices.")
|
|
477
|
+
|
|
478
|
+
if labels is None:
|
|
479
|
+
labels = deepcopy(adj.labels)
|
|
480
|
+
|
|
481
|
+
long = _label_distance_long(adj, labels)
|
|
482
|
+
distances = long["Distance"]
|
|
483
|
+
types = long["Type"]
|
|
484
|
+
groups = long["Group"]
|
|
485
|
+
|
|
486
|
+
stats = {}
|
|
487
|
+
for i in np.unique(groups):
|
|
488
|
+
within = distances[(groups == i) & (types == "Within")]
|
|
489
|
+
between = distances[(groups == i) & (types == "Between")]
|
|
490
|
+
stats[str(i)] = two_sample_permutation_test(
|
|
491
|
+
within,
|
|
492
|
+
between,
|
|
493
|
+
n_permute=n_permute,
|
|
494
|
+
n_jobs=n_jobs,
|
|
495
|
+
progress_bar=progress_bar,
|
|
496
|
+
)
|
|
497
|
+
return stats
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _plot_silhouette(
|
|
501
|
+
adj,
|
|
502
|
+
*,
|
|
503
|
+
labels=None,
|
|
504
|
+
ax=None,
|
|
505
|
+
permutation_test=True,
|
|
506
|
+
n_permute=5000,
|
|
507
|
+
colors=None,
|
|
508
|
+
figsize=(6, 4),
|
|
509
|
+
):
|
|
510
|
+
"""Create a silhouette plot.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
adj (Adjacency): Adjacency instance (must be a single matrix).
|
|
514
|
+
labels (np.ndarray, optional): Cluster/group label per node; defaults to
|
|
515
|
+
`adj.labels`.
|
|
516
|
+
ax (matplotlib.axes.Axes, optional): Axis to draw on.
|
|
517
|
+
permutation_test (bool): Whether to run a permutation test. Default True.
|
|
518
|
+
n_permute (int): Number of permutations for the test. Default 5000.
|
|
519
|
+
colors (list, optional): RGB triplets, one per cluster. Default: seaborn
|
|
520
|
+
`'hls'` palette.
|
|
521
|
+
figsize (tuple): Figure size. Default (6, 4).
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
pl.DataFrame: Columns `label` and `mean_silhouette`, plus `p` when
|
|
525
|
+
`permutation_test=True`.
|
|
526
|
+
"""
|
|
527
|
+
from copy import deepcopy
|
|
528
|
+
|
|
529
|
+
from nltools.plotting import _plot_silhouette as _plot_silhouette
|
|
530
|
+
|
|
531
|
+
distance = adj.squareform()
|
|
532
|
+
|
|
533
|
+
if labels is None:
|
|
534
|
+
labels = np.array(deepcopy(adj.labels))
|
|
535
|
+
else:
|
|
536
|
+
if len(labels) != distance.shape[0]:
|
|
537
|
+
raise ValueError("Labels must be same length as distance matrix")
|
|
538
|
+
|
|
539
|
+
return _plot_silhouette(
|
|
540
|
+
distance,
|
|
541
|
+
np.asarray(labels),
|
|
542
|
+
ax=ax,
|
|
543
|
+
permutation_test=permutation_test,
|
|
544
|
+
n_permute=n_permute,
|
|
545
|
+
colors=colors,
|
|
546
|
+
figsize=figsize,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _cluster_summary(adj, *, clusters=None, summary="mean", scope="within"):
|
|
551
|
+
"""Provide summaries of clusters within Adjacency matrices.
|
|
552
|
+
|
|
553
|
+
Computes the mean/median of within- or between-cluster values. Requires a
|
|
554
|
+
list of cluster ids indicating the cluster of each row/column.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
adj (Adjacency): Adjacency instance.
|
|
558
|
+
clusters (list): Cluster label for each row/column.
|
|
559
|
+
summary (str | None): Central tendency, `'mean'` or `'median'`. If None,
|
|
560
|
+
return all values instead of a summary.
|
|
561
|
+
scope (str): Summarize `'within'` cluster or `'between'` clusters.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
dict: Per-cluster summaries keyed by cluster label.
|
|
565
|
+
"""
|
|
566
|
+
if summary not in ["mean", "median", None]:
|
|
567
|
+
raise ValueError("summary must be ['mean','median', None]")
|
|
568
|
+
|
|
569
|
+
distance = np.asarray(adj.squareform())
|
|
570
|
+
clusters = np.asarray(clusters)
|
|
571
|
+
|
|
572
|
+
if len(clusters) != distance.shape[0]:
|
|
573
|
+
raise ValueError("Cluster labels must be same length as distance matrix")
|
|
574
|
+
|
|
575
|
+
out = {}
|
|
576
|
+
for i in list(set(clusters.tolist())):
|
|
577
|
+
mask_i = clusters == i
|
|
578
|
+
if scope == "within":
|
|
579
|
+
within_vals = distance[np.ix_(mask_i, mask_i)][
|
|
580
|
+
np.triu_indices(mask_i.sum(), k=1)
|
|
581
|
+
]
|
|
582
|
+
if summary == "mean":
|
|
583
|
+
out[i] = float(np.mean(within_vals))
|
|
584
|
+
elif summary == "median":
|
|
585
|
+
out[i] = float(np.median(within_vals))
|
|
586
|
+
else:
|
|
587
|
+
out[i] = within_vals
|
|
588
|
+
elif scope == "between":
|
|
589
|
+
between_block = distance[np.ix_(mask_i, ~mask_i)]
|
|
590
|
+
if summary == "mean":
|
|
591
|
+
out[i] = float(np.mean(between_block))
|
|
592
|
+
elif summary == "median":
|
|
593
|
+
out[i] = float(np.median(between_block))
|
|
594
|
+
else:
|
|
595
|
+
out[i] = between_block
|
|
596
|
+
return out
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Shared helpers for Adjacency submodules.
|
|
2
|
+
|
|
3
|
+
These are internal utilities used by the facade and submodules — not part of the
|
|
4
|
+
public API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _perform_arithmetic(adj, y, op, op_name, reverse=False):
|
|
14
|
+
"""Perform arithmetic operation with validation.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
adj: Adjacency instance (left operand unless *reverse* is True).
|
|
18
|
+
y: Operand (scalar or Adjacency).
|
|
19
|
+
op: Callable that performs the operation on arrays.
|
|
20
|
+
op_name: Name of operation for error messages.
|
|
21
|
+
reverse: If True, reverse operand order (y op adj).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Adjacency: New instance with result.
|
|
25
|
+
"""
|
|
26
|
+
new = deepcopy(adj)
|
|
27
|
+
if isinstance(y, (int, np.integer, float, np.floating)):
|
|
28
|
+
if reverse:
|
|
29
|
+
new.data = op(y, new.data)
|
|
30
|
+
else:
|
|
31
|
+
new.data = op(new.data, y)
|
|
32
|
+
else:
|
|
33
|
+
# Import here to avoid circular import at module level
|
|
34
|
+
from . import Adjacency
|
|
35
|
+
|
|
36
|
+
if isinstance(y, Adjacency):
|
|
37
|
+
from .state import _validate_compatible
|
|
38
|
+
|
|
39
|
+
_validate_compatible(adj, y, labels=True)
|
|
40
|
+
if adj.shape != y.shape:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"Both Adjacency() instances need to be the same shape."
|
|
43
|
+
)
|
|
44
|
+
if reverse:
|
|
45
|
+
new.data = op(y.data, new.data)
|
|
46
|
+
else:
|
|
47
|
+
new.data = op(new.data, y.data)
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError(f"Can only {op_name} int, float, or Adjacency")
|
|
50
|
+
return new
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _apply_stat(adj, func, axis=0):
|
|
54
|
+
"""Apply a statistical function along an axis.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
adj: Adjacency instance.
|
|
58
|
+
func: Numpy function to apply (e.g., np.nanmean).
|
|
59
|
+
axis: Axis along which to apply function. 0 for across matrices,
|
|
60
|
+
1 for across upper triangle elements.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
float | Adjacency | np.ndarray: A float for a single matrix; an Adjacency
|
|
64
|
+
when `axis=0` with multiple matrices; an array when `axis=1` with
|
|
65
|
+
multiple matrices.
|
|
66
|
+
"""
|
|
67
|
+
if axis not in (0, 1):
|
|
68
|
+
raise ValueError(f"axis must be 0 or 1, got {axis}")
|
|
69
|
+
if adj.is_single_matrix:
|
|
70
|
+
return func(adj.data)
|
|
71
|
+
from .state import _common_labels, _result
|
|
72
|
+
|
|
73
|
+
if axis == 0:
|
|
74
|
+
return _result(
|
|
75
|
+
adj, func(adj.data, axis=axis), labels=_common_labels(adj), Y=pl.DataFrame()
|
|
76
|
+
)
|
|
77
|
+
if axis == 1:
|
|
78
|
+
return func(adj.data, axis=axis)
|
|
79
|
+
raise ValueError(f"axis must be 0 or 1, got {axis}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Atlas registry, lazy loading, and coordinate labeling.
|
|
2
|
+
|
|
3
|
+
Atlases are hosted at `huggingface.co/datasets/nltools/niftis` under
|
|
4
|
+
`atlases/` and fetched on first use via `fetch_resource`, then cached locally.
|
|
5
|
+
|
|
6
|
+
The labeling logic was adapted from
|
|
7
|
+
[atlasreader](https://github.com/miykael/atlasreader) (BSD-3-Clause); please
|
|
8
|
+
cite it when using these tools.
|
|
9
|
+
|
|
10
|
+
References:
|
|
11
|
+
Notter, M. P., Gale, D., Herholz, P., Markello, R., Notter-Bielser, M.-L., &
|
|
12
|
+
Whittingstall, K. (2019). AtlasReader: A Python package to generate
|
|
13
|
+
coordinate tables, region labels, and informative figures from statistical
|
|
14
|
+
MRI images. *Journal of Open Source Software*, 4(34), 1257.
|
|
15
|
+
https://doi.org/10.21105/joss.01257
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Internal package: these imports are re-exports for the rest of nltools, not
|
|
19
|
+
# an advertised surface, so there is no `__all__` to mark them as used.
|
|
20
|
+
from .labeling import label_coords # noqa: F401
|
|
21
|
+
from .loading import _Atlas, load_atlas # noqa: F401
|
|
22
|
+
from .registry import list_atlases # noqa: F401
|
|
23
|
+
from .reporting import _ClusterReport, _cluster_report_data # noqa: F401
|