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,2170 @@
|
|
|
1
|
+
"""Represent brain image data with the BrainData class."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, overload
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from nibabel import Nifti1Image
|
|
13
|
+
from sklearn.base import BaseEstimator
|
|
14
|
+
from sklearn.model_selection import BaseCrossValidator
|
|
15
|
+
|
|
16
|
+
from nltools.data.atlases import _Atlas, _ClusterReport
|
|
17
|
+
from nltools.data.designmatrix import DesignMatrix
|
|
18
|
+
from nltools.data.results import Predict
|
|
19
|
+
|
|
20
|
+
from .utils import _check_brain_data, _coalesced_gc
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BrainData:
|
|
24
|
+
"""Represent neuroimaging data as vectors instead of three-dimensional matrices.
|
|
25
|
+
|
|
26
|
+
Each image is flattened to its in-mask voxels, so a stack of images is a 2D
|
|
27
|
+
``(n_images, n_voxels)`` array. This representation makes it easier to perform
|
|
28
|
+
data manipulation and analyses.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
data (None | BrainData | list | str | Path | Nifti1Image | np.ndarray):
|
|
32
|
+
Neuroimaging data. Accepts ``None`` (an empty BrainData), another
|
|
33
|
+
BrainData, a list of BrainData objects or file paths, a file path to
|
|
34
|
+
``.nii``/``.nii.gz``/``.h5``/``.hdf5``, a nibabel ``Nifti1Image``, a URL
|
|
35
|
+
to download from, or a numpy array (1D ``(n_voxels,)`` for a single image
|
|
36
|
+
or 2D ``(n_images, n_voxels)`` for a stack). Array input requires
|
|
37
|
+
``mask``, whose in-mask voxel count must match the array's last axis.
|
|
38
|
+
mask (None | Nifti1Image | str | Path): Brain mask. ``None`` uses the MNI
|
|
39
|
+
template; otherwise a nibabel ``Nifti1Image``, a file path to a mask
|
|
40
|
+
file, or a template name string like ``'2mm-MNI152-2009c'`` (version:
|
|
41
|
+
``'fsl'`` for default/, ``'a'`` for nilearn/, ``'c'`` for fmriprep/).
|
|
42
|
+
masker (nilearn masker | None): nilearn masker object (e.g. ROI or
|
|
43
|
+
searchlight extractor). Default ``None`` loads data as voxels.
|
|
44
|
+
Y (DataFrame | np.ndarray | str | None): Optional per-image target/label
|
|
45
|
+
values, stored as a polars DataFrame (``.Y``). Default ``None``. If
|
|
46
|
+
``data`` is a BrainData with a ``.Y``, that value is inherited when this
|
|
47
|
+
is ``None``.
|
|
48
|
+
X (DataFrame | np.ndarray | str | None): Optional per-image design/feature
|
|
49
|
+
values, stored as a polars DataFrame (``.X``). Default ``None``. If
|
|
50
|
+
``data`` is a BrainData with an ``.X``, that value is inherited when this
|
|
51
|
+
is ``None``.
|
|
52
|
+
h5_compression (str): Compression filter used when writing HDF5
|
|
53
|
+
(``.h5``/``.hdf5``) output, ``'gzip'`` (default) or ``'lzf'``.
|
|
54
|
+
verbose (bool): Emit informational messages during loading and other
|
|
55
|
+
operations. Default ``False``.
|
|
56
|
+
resample (bool): Whether to automatically resample data to mask space.
|
|
57
|
+
If ``True`` (default), data is resampled to match the mask's spatial
|
|
58
|
+
characteristics. If ``False``, data must already be in mask space.
|
|
59
|
+
interpolation (str): Interpolation method for resampling. ``'auto'``
|
|
60
|
+
(default) detects based on data type — ``'nearest'`` for discrete data
|
|
61
|
+
like atlases/masks and ``'continuous'`` for stat maps; ``'nearest'``
|
|
62
|
+
(nearest-neighbor, preserves discrete values), ``'linear'`` (linear
|
|
63
|
+
interpolation), or ``'continuous'`` (higher-order spline, use for stat
|
|
64
|
+
maps).
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
data (np.ndarray): In-mask voxel values, shape ``(n_voxels,)`` for a single
|
|
68
|
+
image or ``(n_images, n_voxels)`` for a stack.
|
|
69
|
+
mask (Nifti1Image): The brain mask every image is flattened against.
|
|
70
|
+
masker (nilearn masker | None): Masker used to extract data, or ``None``
|
|
71
|
+
when data are plain voxels.
|
|
72
|
+
verbose (bool): Whether informational messages are emitted.
|
|
73
|
+
X (pl.DataFrame): Design matrix / per-image covariates (possibly empty).
|
|
74
|
+
Y (pl.DataFrame): Per-image targets (possibly empty).
|
|
75
|
+
dtype (np.dtype): Data type of ``data``.
|
|
76
|
+
is_empty (bool): Whether ``data`` holds no elements.
|
|
77
|
+
shape (tuple[int, ...]): Images-by-voxels shape of ``data``.
|
|
78
|
+
size (int): Total number of elements in ``data`` (numpy convention).
|
|
79
|
+
|
|
80
|
+
Note:
|
|
81
|
+
The mask decides the grid. With no ``mask``, nltools uses the bundled
|
|
82
|
+
MNI template at the brain space's current resolution; data on a grid no
|
|
83
|
+
bundled template matches — 4 mm, say — is resampled to the closest
|
|
84
|
+
bundled 1/2/3 mm template and a `ResamplingWarning` names the fallback.
|
|
85
|
+
To keep the native resolution, pass ``mask`` with a mask in the data's
|
|
86
|
+
own space.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
data=None,
|
|
92
|
+
*,
|
|
93
|
+
Y=None,
|
|
94
|
+
X=None,
|
|
95
|
+
mask=None,
|
|
96
|
+
masker=None,
|
|
97
|
+
h5_compression="gzip",
|
|
98
|
+
verbose=False,
|
|
99
|
+
resample=True,
|
|
100
|
+
interpolation="auto",
|
|
101
|
+
):
|
|
102
|
+
from .io import (
|
|
103
|
+
_initialize_mask,
|
|
104
|
+
_load_from_brain_data,
|
|
105
|
+
_load_from_file,
|
|
106
|
+
_load_from_h5,
|
|
107
|
+
_load_from_list,
|
|
108
|
+
_load_from_url,
|
|
109
|
+
)
|
|
110
|
+
from .validation import _validate_data_type
|
|
111
|
+
|
|
112
|
+
# Initialize attributes
|
|
113
|
+
self._h5_compression = h5_compression
|
|
114
|
+
self.verbose = verbose
|
|
115
|
+
self._resample = resample
|
|
116
|
+
self._interpolation = interpolation
|
|
117
|
+
valid_interpolations = ("auto", "nearest", "linear", "continuous")
|
|
118
|
+
if self._interpolation not in valid_interpolations:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"interpolation must be one of {valid_interpolations}, "
|
|
121
|
+
f"got '{self._interpolation}'"
|
|
122
|
+
)
|
|
123
|
+
self.masker = masker
|
|
124
|
+
self._labels = None
|
|
125
|
+
|
|
126
|
+
# Initialize mask
|
|
127
|
+
_initialize_mask(self, mask)
|
|
128
|
+
|
|
129
|
+
# Initialize data based on type
|
|
130
|
+
data_type = _validate_data_type(data)
|
|
131
|
+
|
|
132
|
+
if data_type == "none":
|
|
133
|
+
self.data = np.array([])
|
|
134
|
+
elif data_type == "brain_data":
|
|
135
|
+
_load_from_brain_data(self, data, mask)
|
|
136
|
+
elif data_type == "h5":
|
|
137
|
+
_load_from_h5(self, data, mask)
|
|
138
|
+
return
|
|
139
|
+
elif data_type == "list":
|
|
140
|
+
_load_from_list(self, data)
|
|
141
|
+
elif data_type == "url":
|
|
142
|
+
_load_from_url(self, data)
|
|
143
|
+
elif data_type in ["file", "nibabel"]:
|
|
144
|
+
_load_from_file(self, data)
|
|
145
|
+
elif data_type == "array":
|
|
146
|
+
# Raw numpy array path. Requires an explicit mask because without
|
|
147
|
+
# one we can't map the flat voxel axis to 3D space. Accepts 1D
|
|
148
|
+
# (n_voxels,) for a single image or 2D (n_images, n_voxels) for a
|
|
149
|
+
# stack. Values are stored as-is; users are expected to have
|
|
150
|
+
# already applied any scaling they want.
|
|
151
|
+
if mask is None:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"Constructing BrainData from a numpy array requires an "
|
|
154
|
+
"explicit mask — pass mask=<path|Nifti1Image> that matches "
|
|
155
|
+
"the array's voxel axis."
|
|
156
|
+
)
|
|
157
|
+
arr = np.asarray(data)
|
|
158
|
+
if arr.ndim not in (1, 2):
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"numpy array input must be 1D (n_voxels,) or 2D "
|
|
161
|
+
f"(n_images, n_voxels); got shape {arr.shape}"
|
|
162
|
+
)
|
|
163
|
+
n_voxels_mask = int((self.mask.get_fdata() > 0).sum())
|
|
164
|
+
if arr.shape[-1] != n_voxels_mask:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"numpy array last axis ({arr.shape[-1]}) must match the "
|
|
167
|
+
f"number of in-mask voxels ({n_voxels_mask})."
|
|
168
|
+
)
|
|
169
|
+
self.data = arr
|
|
170
|
+
|
|
171
|
+
# Collapse extra trailing dimensions, but preserve samples dimension for list inputs
|
|
172
|
+
if self.data is not None and self.data.ndim > 1 and data_type != "list":
|
|
173
|
+
if 1 in self.data.shape:
|
|
174
|
+
self.data = self.data.squeeze()
|
|
175
|
+
|
|
176
|
+
# Set X and Y. Invariant: .X and .Y are always polars DataFrames
|
|
177
|
+
# (possibly empty). Assignment goes through the property setter,
|
|
178
|
+
# which pipes through _validate_frame for pandas/numpy/csv ingress.
|
|
179
|
+
if X is not None:
|
|
180
|
+
self.X = X
|
|
181
|
+
elif data_type == "brain_data" and hasattr(data, "X"):
|
|
182
|
+
self.X = data.X
|
|
183
|
+
else:
|
|
184
|
+
self.X = None
|
|
185
|
+
|
|
186
|
+
if Y is not None:
|
|
187
|
+
self.Y = Y
|
|
188
|
+
elif data_type == "brain_data" and hasattr(data, "Y"):
|
|
189
|
+
self.Y = data.Y
|
|
190
|
+
else:
|
|
191
|
+
self.Y = None
|
|
192
|
+
|
|
193
|
+
# =========================================================================
|
|
194
|
+
# Dunders (alphabetical)
|
|
195
|
+
# =========================================================================
|
|
196
|
+
|
|
197
|
+
def __add__(self, y):
|
|
198
|
+
"""Add to BrainData."""
|
|
199
|
+
from .utils import _perform_arithmetic
|
|
200
|
+
|
|
201
|
+
return _perform_arithmetic(self, y, np.add, "add")
|
|
202
|
+
|
|
203
|
+
def __copy__(self):
|
|
204
|
+
"""Create an independent snapshot of all data and fitted state."""
|
|
205
|
+
from ..ownership import _copy_complete
|
|
206
|
+
|
|
207
|
+
return _copy_complete(self)
|
|
208
|
+
|
|
209
|
+
def __deepcopy__(self, memo):
|
|
210
|
+
"""Create an independent snapshot of all data and fitted state."""
|
|
211
|
+
from ..ownership import _copy_complete
|
|
212
|
+
|
|
213
|
+
return _copy_complete(self, memo)
|
|
214
|
+
|
|
215
|
+
def __eq__(self, other):
|
|
216
|
+
"""Check equality between BrainData."""
|
|
217
|
+
if not isinstance(other, BrainData):
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
eq_data = np.all(self.data == other.data)
|
|
221
|
+
eq_X = self.X.equals(other.X)
|
|
222
|
+
eq_Y = self.Y.equals(other.Y)
|
|
223
|
+
|
|
224
|
+
if self.mask is None and other.mask is None:
|
|
225
|
+
eq_mask = True
|
|
226
|
+
elif self.mask is None or other.mask is None:
|
|
227
|
+
eq_mask = False
|
|
228
|
+
elif hasattr(self.mask, "dataobj") and hasattr(other.mask, "dataobj"):
|
|
229
|
+
eq_mask = (
|
|
230
|
+
self.mask.shape == other.mask.shape
|
|
231
|
+
and np.array_equal(self.mask.affine, other.mask.affine)
|
|
232
|
+
and np.array_equal(
|
|
233
|
+
np.asanyarray(self.mask.dataobj),
|
|
234
|
+
np.asanyarray(other.mask.dataobj),
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
eq_mask = self.mask == other.mask
|
|
239
|
+
|
|
240
|
+
return eq_data and eq_X and eq_Y and eq_mask
|
|
241
|
+
|
|
242
|
+
def __getitem__(self, index):
|
|
243
|
+
from .utils import _result_from_selection
|
|
244
|
+
|
|
245
|
+
return _result_from_selection(self, index)
|
|
246
|
+
|
|
247
|
+
def __iadd__(self, y):
|
|
248
|
+
"""In-place addition (+=)."""
|
|
249
|
+
from .utils import _perform_arithmetic
|
|
250
|
+
|
|
251
|
+
return _perform_arithmetic(self, y, np.add, "add", inplace=True)
|
|
252
|
+
|
|
253
|
+
def __imul__(self, y):
|
|
254
|
+
"""In-place multiplication (*=)."""
|
|
255
|
+
from .utils import _perform_arithmetic
|
|
256
|
+
|
|
257
|
+
return _perform_arithmetic(self, y, np.multiply, "multiply", inplace=True)
|
|
258
|
+
|
|
259
|
+
def __isub__(self, y):
|
|
260
|
+
"""In-place subtraction (-=)."""
|
|
261
|
+
from .utils import _perform_arithmetic
|
|
262
|
+
|
|
263
|
+
return _perform_arithmetic(self, y, np.subtract, "subtract", inplace=True)
|
|
264
|
+
|
|
265
|
+
def __iter__(self):
|
|
266
|
+
for x in range(len(self)):
|
|
267
|
+
yield self[x]
|
|
268
|
+
|
|
269
|
+
def __itruediv__(self, y):
|
|
270
|
+
"""In-place true division (/=)."""
|
|
271
|
+
from .utils import _perform_arithmetic
|
|
272
|
+
|
|
273
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
274
|
+
return _perform_arithmetic(self, y, np.divide, "divide", inplace=True)
|
|
275
|
+
|
|
276
|
+
def __len__(self):
|
|
277
|
+
return self.shape[0]
|
|
278
|
+
|
|
279
|
+
def __mul__(self, y):
|
|
280
|
+
"""Multiply BrainData."""
|
|
281
|
+
from .utils import _perform_arithmetic
|
|
282
|
+
|
|
283
|
+
return _perform_arithmetic(self, y, np.multiply, "multiply")
|
|
284
|
+
|
|
285
|
+
def __radd__(self, y):
|
|
286
|
+
"""Right add to BrainData."""
|
|
287
|
+
from .utils import _perform_arithmetic
|
|
288
|
+
|
|
289
|
+
return _perform_arithmetic(self, y, np.add, "add")
|
|
290
|
+
|
|
291
|
+
def __repr__(self):
|
|
292
|
+
mask_filename = self.mask.get_filename()
|
|
293
|
+
mask_display = os.path.basename(mask_filename) if mask_filename else "None"
|
|
294
|
+
|
|
295
|
+
if hasattr(self, "_voxel_resolution") and self._voxel_resolution is not None:
|
|
296
|
+
if np.allclose(self._voxel_resolution, self._voxel_resolution[0]):
|
|
297
|
+
resolution_str = f"{self._voxel_resolution[0]:.1f}mm"
|
|
298
|
+
else:
|
|
299
|
+
resolution_str = (
|
|
300
|
+
f"{self._voxel_resolution[0]:.1f}x"
|
|
301
|
+
f"{self._voxel_resolution[1]:.1f}x"
|
|
302
|
+
f"{self._voxel_resolution[2]:.1f}mm"
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
resolution_str = "unknown"
|
|
306
|
+
|
|
307
|
+
space_str = getattr(self, "_space", "unknown")
|
|
308
|
+
|
|
309
|
+
return f"{self.__class__.__module__}.{self.__class__.__name__}(data={self.shape}, resolution={resolution_str}, space={space_str}, mask={mask_display})"
|
|
310
|
+
|
|
311
|
+
def __rmul__(self, y):
|
|
312
|
+
"""Right multiply BrainData."""
|
|
313
|
+
from .utils import _perform_arithmetic
|
|
314
|
+
|
|
315
|
+
return _perform_arithmetic(self, y, np.multiply, "multiply")
|
|
316
|
+
|
|
317
|
+
def __rsub__(self, y):
|
|
318
|
+
"""Right subtract from BrainData."""
|
|
319
|
+
from .utils import _perform_arithmetic
|
|
320
|
+
|
|
321
|
+
return _perform_arithmetic(self, y, np.subtract, "subtract", reverse=True)
|
|
322
|
+
|
|
323
|
+
def __setitem__(self, index, value):
|
|
324
|
+
import polars as pl
|
|
325
|
+
from .utils import _clear_fit_state
|
|
326
|
+
|
|
327
|
+
if not isinstance(value, BrainData):
|
|
328
|
+
raise ValueError(
|
|
329
|
+
"Make sure the value you are trying to set is a BrainData() instance."
|
|
330
|
+
)
|
|
331
|
+
new_data = self.data.copy()
|
|
332
|
+
new_data[index, :] = value.data
|
|
333
|
+
new_y = None
|
|
334
|
+
if not value.Y.is_empty():
|
|
335
|
+
if self.Y.is_empty():
|
|
336
|
+
raise ValueError("Cannot set Y values: self.Y is empty.")
|
|
337
|
+
arr = self.Y.to_numpy()
|
|
338
|
+
arr[index] = value.Y.to_numpy()
|
|
339
|
+
new_y = pl.DataFrame(arr, schema=self.Y.columns)
|
|
340
|
+
new_X = None
|
|
341
|
+
if not value.X.is_empty():
|
|
342
|
+
if self.X.is_empty():
|
|
343
|
+
raise ValueError("Cannot set X values: self.X is empty.")
|
|
344
|
+
if self.X.shape[1] != value.X.shape[1]:
|
|
345
|
+
raise ValueError("Make sure self.X is the same size as value.X.")
|
|
346
|
+
arr = self.X.to_numpy()
|
|
347
|
+
arr[index] = value.X.to_numpy()
|
|
348
|
+
new_X = pl.DataFrame(arr, schema=self.X.columns)
|
|
349
|
+
|
|
350
|
+
_clear_fit_state(self)
|
|
351
|
+
self.data = new_data
|
|
352
|
+
if new_y is not None:
|
|
353
|
+
self.Y = new_y
|
|
354
|
+
if new_X is not None:
|
|
355
|
+
self.X = new_X
|
|
356
|
+
|
|
357
|
+
def __sub__(self, y):
|
|
358
|
+
"""Subtract from BrainData."""
|
|
359
|
+
from .utils import _perform_arithmetic
|
|
360
|
+
|
|
361
|
+
return _perform_arithmetic(self, y, np.subtract, "subtract")
|
|
362
|
+
|
|
363
|
+
def __truediv__(self, y):
|
|
364
|
+
"""Divide BrainData."""
|
|
365
|
+
from .utils import _perform_arithmetic
|
|
366
|
+
|
|
367
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
368
|
+
return _perform_arithmetic(self, y, np.divide, "divide")
|
|
369
|
+
|
|
370
|
+
# =========================================================================
|
|
371
|
+
# Properties (alphabetical)
|
|
372
|
+
# =========================================================================
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def dtype(self):
|
|
376
|
+
"""Get data type of BrainData.data."""
|
|
377
|
+
return self.data.dtype
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def is_empty(self) -> bool:
|
|
381
|
+
"""Check if BrainData.data is empty."""
|
|
382
|
+
if isinstance(self.data, np.ndarray):
|
|
383
|
+
return self.data.size == 0
|
|
384
|
+
if isinstance(self.data, list):
|
|
385
|
+
return len(self.data) == 0
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def shape(self):
|
|
390
|
+
"""Get images by voxels shape."""
|
|
391
|
+
return self.data.shape
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def size(self):
|
|
395
|
+
"""Total number of elements in BrainData.data (numpy convention)."""
|
|
396
|
+
return self.data.size
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def X(self):
|
|
400
|
+
"""Design matrix / per-image covariates as a polars DataFrame."""
|
|
401
|
+
return self._X
|
|
402
|
+
|
|
403
|
+
@X.setter
|
|
404
|
+
def X(self, value):
|
|
405
|
+
from ..validation import _validate_frame
|
|
406
|
+
|
|
407
|
+
self._X = _validate_frame(value, frame_type="X")
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def Y(self):
|
|
411
|
+
"""Per-image targets as a polars DataFrame."""
|
|
412
|
+
return self._Y
|
|
413
|
+
|
|
414
|
+
@Y.setter
|
|
415
|
+
def Y(self, value):
|
|
416
|
+
from ..validation import _validate_frame
|
|
417
|
+
|
|
418
|
+
self._Y = _validate_frame(value, frame_type="Y")
|
|
419
|
+
|
|
420
|
+
# =========================================================================
|
|
421
|
+
# Public methods (alphabetical)
|
|
422
|
+
# =========================================================================
|
|
423
|
+
|
|
424
|
+
@_coalesced_gc()
|
|
425
|
+
def align(
|
|
426
|
+
self,
|
|
427
|
+
target,
|
|
428
|
+
method="procrustes",
|
|
429
|
+
axis=0,
|
|
430
|
+
*,
|
|
431
|
+
spatial_scale: str = "whole_brain",
|
|
432
|
+
roi_mask=None,
|
|
433
|
+
):
|
|
434
|
+
"""Align BrainData instance to target object using functional alignment.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
target (BrainData): Object to align to.
|
|
438
|
+
method (str): Alignment method: ``'probabilistic_srm'``,
|
|
439
|
+
``'deterministic_srm'``, or ``'procrustes'``. Default ``'procrustes'``.
|
|
440
|
+
axis (int): Axis to align on. Default 0.
|
|
441
|
+
spatial_scale (str): ``'whole_brain'`` (default) or ``'roi'``
|
|
442
|
+
(per-parcel transforms + reassembly, requires `roi_mask`).
|
|
443
|
+
roi_mask (BrainData | Nifti1Image | str | Path | None): Atlas image
|
|
444
|
+
used when ``spatial_scale='roi'``.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
dict: ``'transformed'``, ``'transformation_matrix'`` and
|
|
448
|
+
``'common_model'``, plus ``'disparity'`` and ``'scale'`` for
|
|
449
|
+
``method='procrustes'``. A value is a `BrainData` when its
|
|
450
|
+
columns are a voxel axis matching the mask it carries, and a
|
|
451
|
+
raw `np.ndarray` otherwise. ``'procrustes'`` therefore returns
|
|
452
|
+
all three as independently owned `BrainData`, with float
|
|
453
|
+
``'disparity'`` and ``'scale'``. The SRM methods return
|
|
454
|
+
``'transformed'`` ``(n_images, n_features)`` and
|
|
455
|
+
``'common_model'`` ``(n_model_rows, n_features)`` as raw
|
|
456
|
+
`np.ndarray`, since both span the common model's feature axis
|
|
457
|
+
rather than voxels, and ``'transformation_matrix'`` as a
|
|
458
|
+
`BrainData` of ``n_features`` voxel maps. With ``axis=1`` the
|
|
459
|
+
transformation matrix spans images on its column axis for
|
|
460
|
+
either method, so it is a raw `np.ndarray` too. With
|
|
461
|
+
``spatial_scale='roi'`` the result also carries
|
|
462
|
+
``'roi_labels'``, ``'transformed'`` is one stitched
|
|
463
|
+
`BrainData`, ``'transformation_matrix'`` and ``'common_model'``
|
|
464
|
+
are dicts keyed by atlas label whose values follow the same
|
|
465
|
+
rule on that parcel's mask, and ``'disparity'`` and
|
|
466
|
+
``'scale'`` are one-per-parcel arrays.
|
|
467
|
+
|
|
468
|
+
Raises:
|
|
469
|
+
ValueError: If a value that must be returned as a `BrainData` has a
|
|
470
|
+
column count other than the mask support — for example a
|
|
471
|
+
``'procrustes'`` target with more voxels than the source, which
|
|
472
|
+
zero-pads the source data to the target's width.
|
|
473
|
+
|
|
474
|
+
Examples:
|
|
475
|
+
```python
|
|
476
|
+
# Hyperalign using procrustes transform
|
|
477
|
+
out = data.align(target, method='procrustes')
|
|
478
|
+
|
|
479
|
+
# Align using shared response model
|
|
480
|
+
out = data.align(target, method='probabilistic_srm')
|
|
481
|
+
|
|
482
|
+
# Project procrustes-aligned data back into original voxel space
|
|
483
|
+
original = np.dot(
|
|
484
|
+
out['transformed'].data, out['transformation_matrix'].data.T
|
|
485
|
+
)
|
|
486
|
+
```
|
|
487
|
+
"""
|
|
488
|
+
if spatial_scale == "roi":
|
|
489
|
+
from .analysis import _align_per_roi
|
|
490
|
+
|
|
491
|
+
return _align_per_roi(
|
|
492
|
+
self, target, method=method, axis=axis, roi_mask=roi_mask
|
|
493
|
+
)
|
|
494
|
+
if spatial_scale != "whole_brain":
|
|
495
|
+
raise ValueError(
|
|
496
|
+
f"spatial_scale must be one of {{'whole_brain', 'roi'}}, "
|
|
497
|
+
f"got {spatial_scale!r}"
|
|
498
|
+
)
|
|
499
|
+
from .analysis import _align
|
|
500
|
+
|
|
501
|
+
return _align(self, target, method=method, axis=axis)
|
|
502
|
+
|
|
503
|
+
def append(self, data, *, ignore_attrs=False):
|
|
504
|
+
"""Append data to BrainData instance.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
data (BrainData): BrainData instance to append.
|
|
508
|
+
ignore_attrs (bool): Clear both X and Y on the result when True.
|
|
509
|
+
Otherwise, each metadata frame must be empty on both inputs or
|
|
510
|
+
have compatible columns on both inputs. Default False.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
BrainData: Independently owned data with concatenated row metadata.
|
|
514
|
+
|
|
515
|
+
Raises:
|
|
516
|
+
ValueError: Metadata is present on only one input or has incompatible columns.
|
|
517
|
+
"""
|
|
518
|
+
from .utils import _result_from_rows
|
|
519
|
+
from .validation import _validate_append_shapes
|
|
520
|
+
import polars as pl
|
|
521
|
+
|
|
522
|
+
data = _check_brain_data(data)
|
|
523
|
+
if self.is_empty:
|
|
524
|
+
return _result_from_rows(
|
|
525
|
+
data,
|
|
526
|
+
data.data,
|
|
527
|
+
X=None if ignore_attrs else data.X,
|
|
528
|
+
Y=None if ignore_attrs else data.Y,
|
|
529
|
+
)
|
|
530
|
+
_validate_append_shapes(self.shape, data.shape)
|
|
531
|
+
frames = []
|
|
532
|
+
for name in ("X", "Y"):
|
|
533
|
+
left, right = getattr(self, name), getattr(data, name)
|
|
534
|
+
if ignore_attrs or (left.is_empty() and right.is_empty()):
|
|
535
|
+
frames.append(None)
|
|
536
|
+
elif left.is_empty() or right.is_empty() or left.columns != right.columns:
|
|
537
|
+
raise ValueError(
|
|
538
|
+
f"append requires compatible {name} metadata on both operands"
|
|
539
|
+
)
|
|
540
|
+
else:
|
|
541
|
+
try:
|
|
542
|
+
frames.append(pl.concat([left, right], how="vertical_relaxed"))
|
|
543
|
+
except pl.exceptions.SchemaError as error:
|
|
544
|
+
raise ValueError(
|
|
545
|
+
f"append requires compatible {name} metadata schemas"
|
|
546
|
+
) from error
|
|
547
|
+
return _result_from_rows(
|
|
548
|
+
self, np.vstack([self.data, data.data]), X=frames[0], Y=frames[1]
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
@_coalesced_gc()
|
|
552
|
+
def apply_mask(self, mask):
|
|
553
|
+
"""Restrict the data to a mask's support, leaving the grid unchanged.
|
|
554
|
+
|
|
555
|
+
The mask must be a single three-dimensional image on the same grid and
|
|
556
|
+
with the same affine as this object. A mismatch raises: resample the
|
|
557
|
+
mask or the data with `resample()` first, rather than relying on an
|
|
558
|
+
implicit resample here.
|
|
559
|
+
|
|
560
|
+
Support is every voxel of `mask` greater than zero, and the mask defines
|
|
561
|
+
the result's voxel axis on its own. Where it reaches past this object's
|
|
562
|
+
current support the result gains those voxels with zero values, so a
|
|
563
|
+
mask larger than `self.mask` widens the array rather than intersecting
|
|
564
|
+
with it.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
mask (BrainData | Nifti1Image | str | Path): Mask to apply.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
BrainData: Masked BrainData object.
|
|
571
|
+
|
|
572
|
+
Raises:
|
|
573
|
+
ValueError: If the mask is not a single 3-D image, or its shape or
|
|
574
|
+
affine differs from this object's.
|
|
575
|
+
TypeError: If `mask` is not a BrainData, nibabel image, or file path.
|
|
576
|
+
"""
|
|
577
|
+
from .analysis import _apply_mask
|
|
578
|
+
|
|
579
|
+
return _apply_mask(self, mask)
|
|
580
|
+
|
|
581
|
+
def astype(self, dtype):
|
|
582
|
+
"""Cast BrainData.data as type.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
dtype (np.dtype | type | str): Datatype to convert to.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
BrainData: BrainData instance with new datatype.
|
|
589
|
+
"""
|
|
590
|
+
from .utils import _result_from_array
|
|
591
|
+
|
|
592
|
+
out = _result_from_array(self, self.data.astype(dtype), rows="preserve")
|
|
593
|
+
return out
|
|
594
|
+
|
|
595
|
+
def bootstrap(
|
|
596
|
+
self,
|
|
597
|
+
statistic,
|
|
598
|
+
*,
|
|
599
|
+
X=None,
|
|
600
|
+
X_test=None,
|
|
601
|
+
n_samples=5000,
|
|
602
|
+
confidence_level=0.95,
|
|
603
|
+
device="cpu",
|
|
604
|
+
memory_budget_gb=None,
|
|
605
|
+
return_samples=False,
|
|
606
|
+
n_jobs=-1,
|
|
607
|
+
random_state=None,
|
|
608
|
+
progress_bar: bool = False,
|
|
609
|
+
):
|
|
610
|
+
"""Bootstrap a statistic and its uncertainty, on CPU workers or a GPU.
|
|
611
|
+
|
|
612
|
+
Resamples rows with replacement and aggregates the replicates as they
|
|
613
|
+
complete, into a running Welford variance plus just enough retained
|
|
614
|
+
order statistics per output element to reproduce the exact percentile
|
|
615
|
+
interval. What the run holds is that retained tail — about
|
|
616
|
+
``(1 - confidence_level)`` of the replicates per element — plus one
|
|
617
|
+
dispatch window, rather than all ``n_samples`` maps. This is
|
|
618
|
+
memory-efficient, not constant-memory: the tail still grows with
|
|
619
|
+
``n_samples``, and ``return_samples=True`` keeps the whole
|
|
620
|
+
distribution.
|
|
621
|
+
|
|
622
|
+
A Ridge bootstrap resamples the training features you pass as ``X``
|
|
623
|
+
together with ``self.data``, using the same row indices for every
|
|
624
|
+
feature space, and refits with the fitted model's selected ``alpha_``
|
|
625
|
+
— and, for a banded model, its ``feature_space_weights_`` — held fixed.
|
|
626
|
+
It never reruns cross-validation or the banded random search. Fitting
|
|
627
|
+
keeps no hidden copy of the training features, so ``X`` is required
|
|
628
|
+
even when the same features were passed to `fit`.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
statistic (str): Statistic to bootstrap. Basic aggregates:
|
|
632
|
+
``'mean'``, ``'median'``, ``'std'``, ``'sum'``, ``'min'``,
|
|
633
|
+
``'max'`` — each the corresponding NumPy reduction over rows,
|
|
634
|
+
with ``'std'`` at ``ddof=0``. Model statistics (require a
|
|
635
|
+
fitted `_Ridge`): ``'weights'`` or ``'predict'``.
|
|
636
|
+
X (np.ndarray | Mapping[str, np.ndarray] | None): Training features
|
|
637
|
+
in their original row order — a matrix for ordinary Ridge, a
|
|
638
|
+
mapping with exactly the fitted feature-space names for banded
|
|
639
|
+
Ridge. Required by both model statistics; rejected by the basic
|
|
640
|
+
ones.
|
|
641
|
+
X_test (np.ndarray | Mapping[str, np.ndarray] | None): Evaluation
|
|
642
|
+
features for ``statistic='predict'``, in the same structure as
|
|
643
|
+
``X``. Any row count is allowed.
|
|
644
|
+
n_samples (int): Number of bootstrap replicates, at least two.
|
|
645
|
+
Default 5000.
|
|
646
|
+
confidence_level (float): Confidence level of the reported
|
|
647
|
+
interval, strictly between zero and one. Default 0.95. The
|
|
648
|
+
bounds are the central percentile interval by linear
|
|
649
|
+
interpolation, and they are elementwise marginal: the nominal
|
|
650
|
+
level applies separately to each voxel, feature, or test row,
|
|
651
|
+
with no simultaneous-coverage claim. A different level needs a
|
|
652
|
+
new run unless ``return_samples=True`` kept the distribution.
|
|
653
|
+
device (str): Compute device for the Ridge refits: ``'cpu'``
|
|
654
|
+
(default) or ``'gpu'`` (PyTorch on CUDA/MPS, or an error when
|
|
655
|
+
neither is available). Basic statistics reject ``'gpu'``.
|
|
656
|
+
memory_budget_gb (float | None): Working-memory budget in GB. It
|
|
657
|
+
governs the output preflight and CPU-worker planning for every
|
|
658
|
+
statistic, and GPU batch sizing for the Ridge ones. ``None``
|
|
659
|
+
(default) measures the device.
|
|
660
|
+
return_samples (bool): Retain and return every replicate. Default
|
|
661
|
+
False. It changes retention only, never interval semantics.
|
|
662
|
+
n_jobs (int): CPU worker ceiling. -1 (default) means all cores; the
|
|
663
|
+
planner may use fewer.
|
|
664
|
+
random_state (int | None): Random seed for reproducibility.
|
|
665
|
+
progress_bar (bool): If True, show a progress bar. Default False.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
BootstrapResult: ``estimate`` (the statistic on the unresampled
|
|
669
|
+
full sample — for ``'weights'`` the fitted coefficients, for
|
|
670
|
+
``'predict'`` the full-data model at ``X_test``),
|
|
671
|
+
``standard_error`` (the ``ddof=1`` deviation across
|
|
672
|
+
replicates), ``ci_lower`` and ``ci_upper``, all `BrainData` of
|
|
673
|
+
identical shape, plus ``samples`` as a NumPy array with the
|
|
674
|
+
bootstrap axis first when ``return_samples=True``.
|
|
675
|
+
|
|
676
|
+
Raises:
|
|
677
|
+
ValueError: If `statistic` is unknown, a basic statistic is given
|
|
678
|
+
``X``, ``X_test`` or ``device='gpu'``, a Ridge statistic is
|
|
679
|
+
missing its features, the fitted model is not a `_Ridge`, an
|
|
680
|
+
argument is out of range, or the retained output cannot fit the
|
|
681
|
+
memory budget.
|
|
682
|
+
|
|
683
|
+
Examples:
|
|
684
|
+
```python
|
|
685
|
+
boot = brain.bootstrap('mean', n_samples=1000)
|
|
686
|
+
boot.estimate.plot()
|
|
687
|
+
|
|
688
|
+
brain.fit(model='ridge', X=features, ridge_alpha=1.0)
|
|
689
|
+
boot = brain.bootstrap('weights', X=features, n_samples=1000)
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
Note:
|
|
693
|
+
This is an IID row bootstrap. Rows must be exchangeable for the
|
|
694
|
+
interval to be meaningful; it implements no grouped, clustered,
|
|
695
|
+
stratified, or block resampling, so an autocorrelated fMRI time
|
|
696
|
+
series must not be treated as IID rows.
|
|
697
|
+
|
|
698
|
+
Note:
|
|
699
|
+
Whatever the run will hold is checked *before* it resamples: if it
|
|
700
|
+
does not fit, `bootstrap` says so and names the
|
|
701
|
+
``memory_budget_gb`` override, and it never quietly shrinks the
|
|
702
|
+
run. A whole-brain 95% interval at ``n_samples=5000`` needs roughly
|
|
703
|
+
0.5 GB for the retained tail; ``return_samples=True`` keeps every
|
|
704
|
+
draw and costs the full ``n_samples x output_size``.
|
|
705
|
+
"""
|
|
706
|
+
from .bootstrap import _bootstrap
|
|
707
|
+
|
|
708
|
+
return _bootstrap(
|
|
709
|
+
self,
|
|
710
|
+
statistic,
|
|
711
|
+
X=X,
|
|
712
|
+
X_test=X_test,
|
|
713
|
+
n_samples=n_samples,
|
|
714
|
+
confidence_level=confidence_level,
|
|
715
|
+
device=device,
|
|
716
|
+
memory_budget_gb=memory_budget_gb,
|
|
717
|
+
return_samples=return_samples,
|
|
718
|
+
n_jobs=n_jobs,
|
|
719
|
+
random_state=random_state,
|
|
720
|
+
progress_bar=progress_bar,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
def compute_contrasts(self, contrasts, *, inference=False):
|
|
724
|
+
"""Compute contrasts on a fitted GLM.
|
|
725
|
+
|
|
726
|
+
Call after ``fit(model='glm', X=design)``. The fitted `_Glm` owns
|
|
727
|
+
contrast parsing and inference; this method forwards each definition
|
|
728
|
+
unchanged and wraps the results as `BrainData` maps.
|
|
729
|
+
|
|
730
|
+
A contrast is a **string** naming design columns with optional
|
|
731
|
+
coefficients (``"conditionA - conditionB"``, ``"2*A - B - C"``) or a
|
|
732
|
+
**numeric vector** with one weight per column (``[1, -1, 0, 0]``). A
|
|
733
|
+
**mapping** of names to those forms computes several at once and is the
|
|
734
|
+
only batch form.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
contrasts (str | array-like | Mapping): One contrast definition, or
|
|
738
|
+
a mapping of names to definitions.
|
|
739
|
+
inference (bool): If True, return `ContrastResult` records carrying
|
|
740
|
+
effect, variance, standard error, t-statistic, z-score,
|
|
741
|
+
one-sided p-value, and degrees of freedom. Default False.
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
BrainData | ContrastResult | dict: An effect map for one contrast,
|
|
745
|
+
or a `ContrastResult` of maps when ``inference=True``; a
|
|
746
|
+
dictionary with the same keys for a mapping.
|
|
747
|
+
|
|
748
|
+
Raises:
|
|
749
|
+
RuntimeError: If no model has been fitted.
|
|
750
|
+
ValueError: If the fitted model is not a `_Glm`, or a contrast is
|
|
751
|
+
invalid (see `_Glm.compute_contrasts`).
|
|
752
|
+
|
|
753
|
+
Examples:
|
|
754
|
+
```python
|
|
755
|
+
brain.fit(model='glm', X=design)
|
|
756
|
+
|
|
757
|
+
# Effect maps — what a second-level model consumes
|
|
758
|
+
effect = brain.compute_contrasts("conditionA - conditionB")
|
|
759
|
+
effects = brain.compute_contrasts({
|
|
760
|
+
"A_vs_B": "conditionA - conditionB",
|
|
761
|
+
"avg": [0, 0.5, 0.5],
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
# First-level inference
|
|
765
|
+
result = brain.compute_contrasts("conditionA - conditionB", inference=True)
|
|
766
|
+
result.statistic.plot(threshold=3.09)
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
Note:
|
|
770
|
+
Contrast p-values are one-sided, following the nilearn/SPM
|
|
771
|
+
directional-contrast convention; negate the contrast to test the
|
|
772
|
+
other direction.
|
|
773
|
+
"""
|
|
774
|
+
from .modeling import _compute_contrasts
|
|
775
|
+
|
|
776
|
+
return _compute_contrasts(self, contrasts, inference=inference)
|
|
777
|
+
|
|
778
|
+
def copy(self):
|
|
779
|
+
"""Create an independent snapshot of a BrainData instance.
|
|
780
|
+
|
|
781
|
+
Data, metadata, mask state, and any fitted model/results are copied.
|
|
782
|
+
Mutating either object after copying does not affect the other.
|
|
783
|
+
Python's `copy.copy()` and `copy.deepcopy()` have the same semantics.
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
BrainData: An independent copy, including fitted state.
|
|
787
|
+
"""
|
|
788
|
+
from ..ownership import _copy_complete
|
|
789
|
+
|
|
790
|
+
return _copy_complete(self)
|
|
791
|
+
|
|
792
|
+
def create_empty(self):
|
|
793
|
+
"""Create a copy of BrainData with empty data array.
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
BrainData: A copy of this object with an empty data array.
|
|
797
|
+
"""
|
|
798
|
+
from .utils import _result_from_array
|
|
799
|
+
|
|
800
|
+
out = _result_from_array(self, np.array([]), rows="clear")
|
|
801
|
+
return out
|
|
802
|
+
|
|
803
|
+
@_coalesced_gc() # nosemgrep: kwargs-internal-forwarding # forwards to the sklearn decomposition estimator
|
|
804
|
+
def decompose(self, *, method="pca", axis="voxels", n_components=None, **kwargs):
|
|
805
|
+
"""Decompose BrainData object.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
method (str): Decomposition algorithm: ``'pca'``, ``'ica'``, ``'nnmf'``,
|
|
809
|
+
``'fa'``, ``'dictionary'``, or ``'kernelpca'``. Default ``'pca'``.
|
|
810
|
+
axis (str): Dimension to decompose: ``'voxels'`` (default) or ``'images'``.
|
|
811
|
+
n_components (int | None): Number of components. If ``None`` then retain
|
|
812
|
+
as many as possible.
|
|
813
|
+
**kwargs (dict): Forwarded to the underlying sklearn decomposition
|
|
814
|
+
estimator.
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
dict: A dictionary of decomposition parameters.
|
|
818
|
+
"""
|
|
819
|
+
from .analysis import _decompose
|
|
820
|
+
|
|
821
|
+
return _decompose(
|
|
822
|
+
self,
|
|
823
|
+
method=method,
|
|
824
|
+
axis=axis,
|
|
825
|
+
n_components=n_components,
|
|
826
|
+
**kwargs,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
def detrend(self, method="linear"):
|
|
830
|
+
"""Remove linear trend from each voxel.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
method (str): Type of detrending: ``'linear'`` (default) or ``'constant'``.
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
BrainData: Detrended BrainData instance.
|
|
837
|
+
"""
|
|
838
|
+
from .analysis import _detrend_data
|
|
839
|
+
|
|
840
|
+
return _detrend_data(self, method=method)
|
|
841
|
+
|
|
842
|
+
@_coalesced_gc() # nosemgrep: kwargs-internal-forwarding # forwards to scipy.spatial.distance.cdist via analysis.distance
|
|
843
|
+
def distance( # nosemgrep: kwargs-internal-forwarding # forwards to scipy.spatial.distance.cdist
|
|
844
|
+
self,
|
|
845
|
+
metric="euclidean",
|
|
846
|
+
*,
|
|
847
|
+
spatial_scale: str = "whole_brain",
|
|
848
|
+
roi_mask=None,
|
|
849
|
+
radius: float = 10.0,
|
|
850
|
+
**kwargs,
|
|
851
|
+
):
|
|
852
|
+
"""Calculate distance between images within a BrainData() instance.
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
metric (str): Distance metric — any ``scipy.spatial.distance`` metric
|
|
856
|
+
supported by ``cdist``. Default ``'euclidean'``.
|
|
857
|
+
spatial_scale (str): One of ``'whole_brain'`` (default), ``'roi'``, or
|
|
858
|
+
``'searchlight'``. ``'whole_brain'`` returns a single
|
|
859
|
+
pairwise distance ``Adjacency`` between images. ``'roi'``
|
|
860
|
+
requires ``roi_mask`` and returns a stacked ``Adjacency``
|
|
861
|
+
with one RDM per sorted nonzero atlas label present inside the
|
|
862
|
+
source mask after nearest-neighbor resampling. `'searchlight'`
|
|
863
|
+
returns one RDM per source-mask voxel in mask order.
|
|
864
|
+
roi_mask (BrainData | Nifti1Image | str | Path | None): Atlas image
|
|
865
|
+
for ``spatial_scale='roi'``.
|
|
866
|
+
radius (float): Searchlight radius in mm. Default 10.0.
|
|
867
|
+
**kwargs (dict): Additional metric options forwarded to
|
|
868
|
+
``scipy.spatial.distance.cdist`` (e.g. ``p`` for minkowski).
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
Adjacency: Single pairwise distance matrix for ``'whole_brain'``;
|
|
872
|
+
ordinary stack for `'roi'` / `'searchlight'`. Map per-matrix values
|
|
873
|
+
externally using `roi_to_brain_from_atlas` with the aligned atlas
|
|
874
|
+
and sorted surviving ROI labels, or `nilearn.masking.unmask`
|
|
875
|
+
with the source mask for searchlights. Subset the mapping whenever
|
|
876
|
+
selecting matrices from the returned stack.
|
|
877
|
+
"""
|
|
878
|
+
from .analysis import _distance
|
|
879
|
+
|
|
880
|
+
return _distance(
|
|
881
|
+
self,
|
|
882
|
+
metric=metric,
|
|
883
|
+
spatial_scale=spatial_scale,
|
|
884
|
+
roi_mask=roi_mask,
|
|
885
|
+
radius=radius,
|
|
886
|
+
**kwargs,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
@_coalesced_gc()
|
|
890
|
+
def extract_roi(self, mask, method="mean", n_components=None):
|
|
891
|
+
"""Extract activity from mask or ROI atlas using NiftiLabelsMasker.
|
|
892
|
+
|
|
893
|
+
The mask may be binary (a single ROI) or a labeled atlas (one value per
|
|
894
|
+
region, extracting from every ROI at once). Unlike `apply_mask`, this
|
|
895
|
+
is an extraction convenience: `mask` is resampled onto this object's
|
|
896
|
+
own grid with nearest-neighbor interpolation before extracting, so it
|
|
897
|
+
need not already share this object's grid. That is a grid change by
|
|
898
|
+
header affine, not a spatial normalization, so an atlas's parcel
|
|
899
|
+
boundaries are approximate unless the data are already in the atlas's
|
|
900
|
+
space.
|
|
901
|
+
|
|
902
|
+
Args:
|
|
903
|
+
mask (BrainData | Nifti1Image | str | Path): Binary mask or labeled
|
|
904
|
+
atlas to extract from, on any grid.
|
|
905
|
+
method (str): Extraction method: ``'mean'`` (default), ``'median'``, or
|
|
906
|
+
``'pca'``.
|
|
907
|
+
n_components (int | None): Number of components to return when
|
|
908
|
+
``method='pca'``.
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
float | np.ndarray: For a binary mask, a scalar (single image) or 1D
|
|
912
|
+
array (multiple images). For a labeled atlas, a 1D array (single
|
|
913
|
+
image), a 2D array of ROIs x images (multiple images), or the PCA
|
|
914
|
+
components array when ``method='pca'``.
|
|
915
|
+
|
|
916
|
+
Raises:
|
|
917
|
+
ValueError: If, after resampling onto this object's grid, `mask`
|
|
918
|
+
has no overlap with it.
|
|
919
|
+
|
|
920
|
+
Examples:
|
|
921
|
+
```python
|
|
922
|
+
roi_values = brain.extract_roi(binary_mask)
|
|
923
|
+
atlas_values = brain.extract_roi(atlas_mask)
|
|
924
|
+
components = brain.extract_roi(mask, method='pca', n_components=5)
|
|
925
|
+
```
|
|
926
|
+
"""
|
|
927
|
+
from .analysis import _extract_roi
|
|
928
|
+
|
|
929
|
+
return _extract_roi(self, mask, method=method, n_components=n_components)
|
|
930
|
+
|
|
931
|
+
def filter( # nosemgrep: kwargs-internal-forwarding # forwards to nilearn.signal.clean
|
|
932
|
+
self, *, sampling_freq=None, high_pass=None, low_pass=None, **kwargs
|
|
933
|
+
):
|
|
934
|
+
"""Apply a Butterworth filter to data (wraps `nilearn.signal.clean`).
|
|
935
|
+
|
|
936
|
+
Note:
|
|
937
|
+
Unlike nilearn's default, does not detrend or standardize. Pass
|
|
938
|
+
detrend=True or standardize=True via kwargs to enable.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
sampling_freq (float | None): Sampling frequency in hertz (i.e. 1 / TR).
|
|
942
|
+
high_pass (float | None): High-pass cutoff frequency in hertz.
|
|
943
|
+
low_pass (float | None): Low-pass cutoff frequency in hertz.
|
|
944
|
+
**kwargs (dict): Additional arguments passed to ``nilearn.signal.clean``.
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
BrainData: Filtered BrainData instance.
|
|
948
|
+
"""
|
|
949
|
+
from .analysis import _filter_data
|
|
950
|
+
|
|
951
|
+
return _filter_data(
|
|
952
|
+
self,
|
|
953
|
+
sampling_freq=sampling_freq,
|
|
954
|
+
high_pass=high_pass,
|
|
955
|
+
low_pass=low_pass,
|
|
956
|
+
**kwargs,
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
def find_spikes(
|
|
960
|
+
self,
|
|
961
|
+
global_spike_cutoff=3,
|
|
962
|
+
diff_spike_cutoff=3,
|
|
963
|
+
*,
|
|
964
|
+
TR: float | None = None,
|
|
965
|
+
sampling_freq: float | None = None,
|
|
966
|
+
):
|
|
967
|
+
"""Identify spikes from Time Series Data.
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
global_spike_cutoff (int or None): cutoff to identify spikes in global signal
|
|
971
|
+
in standard deviations, or None to skip.
|
|
972
|
+
diff_spike_cutoff (int or None): cutoff to identify spikes in average frame
|
|
973
|
+
difference in standard deviations, or None to skip.
|
|
974
|
+
TR: Repetition time in seconds. Sets the returned DesignMatrix's
|
|
975
|
+
sampling_freq for downstream `.append(...)` / `.convolve()`.
|
|
976
|
+
Pass exactly one of `TR` or `sampling_freq`.
|
|
977
|
+
sampling_freq: Sampling frequency in Hz (= 1/TR). See `TR`.
|
|
978
|
+
|
|
979
|
+
Returns:
|
|
980
|
+
DesignMatrix: One indicator column per detected spike TR, with all
|
|
981
|
+
spike columns pre-marked as confounds. A TR flagged by both
|
|
982
|
+
detectors yields a single column (named `global_spike*`); the
|
|
983
|
+
colliding detections are bitwise identical, so only the retained
|
|
984
|
+
name differs.
|
|
985
|
+
"""
|
|
986
|
+
from .analysis import _find_spikes_data
|
|
987
|
+
|
|
988
|
+
return _find_spikes_data(
|
|
989
|
+
self,
|
|
990
|
+
global_spike_cutoff=global_spike_cutoff,
|
|
991
|
+
diff_spike_cutoff=diff_spike_cutoff,
|
|
992
|
+
TR=TR,
|
|
993
|
+
sampling_freq=sampling_freq,
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
@_coalesced_gc() # nosemgrep: kwargs-internal-forwarding # forwards model params to the nilearn/sklearn estimator via modeling.fit
|
|
997
|
+
def fit(
|
|
998
|
+
self,
|
|
999
|
+
model="glm",
|
|
1000
|
+
*,
|
|
1001
|
+
X=None,
|
|
1002
|
+
ridge_alpha=1.0,
|
|
1003
|
+
ridge_cv=None,
|
|
1004
|
+
ridge_search_iterations=100,
|
|
1005
|
+
ridge_dirichlet_concentration=(0.1, 1.0),
|
|
1006
|
+
ridge_device="cpu",
|
|
1007
|
+
ridge_memory_budget_gb=None,
|
|
1008
|
+
ridge_per_target_alpha=True,
|
|
1009
|
+
ridge_prefer_conservative_alpha=False,
|
|
1010
|
+
ridge_progress_bar=False,
|
|
1011
|
+
glm_noise_model="ols",
|
|
1012
|
+
glm_bins=100,
|
|
1013
|
+
glm_n_jobs=1,
|
|
1014
|
+
inplace=True,
|
|
1015
|
+
random_state=None,
|
|
1016
|
+
):
|
|
1017
|
+
"""Fit a model to brain imaging data.
|
|
1018
|
+
|
|
1019
|
+
``self.data`` is always the response. The fitted estimator and its
|
|
1020
|
+
results are stored for later use with `predict` and, for a GLM,
|
|
1021
|
+
`compute_contrasts`.
|
|
1022
|
+
|
|
1023
|
+
Every model-specific option carries a ``glm_`` or ``ridge_`` prefix
|
|
1024
|
+
naming the estimator it configures; ``random_state`` keeps its bare
|
|
1025
|
+
name because both estimators accept it. Supplying a non-default option
|
|
1026
|
+
belonging to the estimator ``model`` did not select raises
|
|
1027
|
+
`ValueError`.
|
|
1028
|
+
|
|
1029
|
+
`fit` does not preprocess the response. Compose `scale` and
|
|
1030
|
+
`standardize` before calling it when you want them, so the fitted
|
|
1031
|
+
object stays in the response space you supplied.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
model (str): ``'glm'`` (default) or ``'ridge'``.
|
|
1035
|
+
X (DesignMatrix | array-like | Mapping): A precomputed
|
|
1036
|
+
`DesignMatrix` for a GLM; a feature matrix for ridge, or a
|
|
1037
|
+
mapping of feature-space names to matrices for banded ridge.
|
|
1038
|
+
Required.
|
|
1039
|
+
ridge_alpha (float | Sequence[float]): Ridge only. A positive
|
|
1040
|
+
scalar fits a fixed α and requires ``ridge_cv=None``; a
|
|
1041
|
+
sequence selects α by cross-validation and requires
|
|
1042
|
+
``ridge_cv``. Default 1.0.
|
|
1043
|
+
ridge_cv (int | sklearn splitter | None): Ridge only.
|
|
1044
|
+
Cross-validation specification; ``int`` → unshuffled
|
|
1045
|
+
``KFold(cv)``. Generators are rejected. Default None.
|
|
1046
|
+
ridge_search_iterations (int): Ridge only, banded. Sampled
|
|
1047
|
+
feature-space weight vectors. Default 100.
|
|
1048
|
+
ridge_dirichlet_concentration (float | Sequence[float]): Ridge
|
|
1049
|
+
only, banded. Dirichlet concentration for those candidate
|
|
1050
|
+
weights. Default ``(0.1, 1.0)``.
|
|
1051
|
+
ridge_device (str): Ridge only. ``'cpu'`` (default) or ``'gpu'``.
|
|
1052
|
+
ridge_memory_budget_gb (float | None): Ridge only. Working-memory
|
|
1053
|
+
budget in GB for the solver's internal batching. Default None
|
|
1054
|
+
(measure the device).
|
|
1055
|
+
ridge_per_target_alpha (bool): Ridge only. Select α per voxel
|
|
1056
|
+
(default True) or one shared α.
|
|
1057
|
+
ridge_prefer_conservative_alpha (bool): Ridge only. Select the
|
|
1058
|
+
largest α within one standard deviation of the best score.
|
|
1059
|
+
Default False.
|
|
1060
|
+
ridge_progress_bar (bool): Ridge only. Show a progress bar over the
|
|
1061
|
+
banded search. Default False.
|
|
1062
|
+
glm_noise_model (str): GLM only. ``'ols'`` (default) or ``'arN'``
|
|
1063
|
+
for Nilearn's autoregressive model of order N.
|
|
1064
|
+
glm_bins (int): GLM only. Nilearn's discretization of the estimated
|
|
1065
|
+
AR coefficients. Default 100.
|
|
1066
|
+
glm_n_jobs (int): GLM only. CPUs Nilearn uses for autoregressive
|
|
1067
|
+
groups; the default OLS fit does not use this path. Default 1.
|
|
1068
|
+
inplace (bool): If True (default), mutate self and return self. If
|
|
1069
|
+
False, fit and return an independent `BrainData` copy while
|
|
1070
|
+
leaving every part of self untouched.
|
|
1071
|
+
random_state (int | None): Seed shared by both estimators.
|
|
1072
|
+
|
|
1073
|
+
Returns:
|
|
1074
|
+
BrainData: Self when ``inplace=True``; otherwise an independently
|
|
1075
|
+
owned fitted copy.
|
|
1076
|
+
|
|
1077
|
+
Note:
|
|
1078
|
+
A GLM fit attaches ``model_``, ``glm_betas`` (one map per design
|
|
1079
|
+
column), ``glm_residual``, ``glm_predicted``, and ``glm_r2``.
|
|
1080
|
+
``glm_r2`` is Nilearn's whitened variance ratio: conventional
|
|
1081
|
+
R-squared for an OLS fit whose design has an intercept, and a
|
|
1082
|
+
pseudo-R-squared in the whitened space for an autoregressive one.
|
|
1083
|
+
A GLM fit does not compute eager per-regressor t, p, or
|
|
1084
|
+
standard-error maps: ask for them one contrast at a time with
|
|
1085
|
+
``compute_contrasts(..., inference=True)``, which uses the full
|
|
1086
|
+
per-voxel parameter covariance and is therefore correct for
|
|
1087
|
+
contrasts spanning several regressors.
|
|
1088
|
+
|
|
1089
|
+
Note:
|
|
1090
|
+
A rank-deficient design fires `DesignMatrixWarning`. It describes
|
|
1091
|
+
your design, not a bug in nltools: a duplicated regressor, an
|
|
1092
|
+
intercept added twice, a condition that never occurs in this run.
|
|
1093
|
+
The fit falls back to a pseudo-inverse, so contrasts touching the
|
|
1094
|
+
dependent columns are not interpretable. Fix the design —
|
|
1095
|
+
``DesignMatrix.clean()`` handles the common case.
|
|
1096
|
+
|
|
1097
|
+
Examples:
|
|
1098
|
+
```python
|
|
1099
|
+
brain_data.fit(model='glm', X=design)
|
|
1100
|
+
effect = brain_data.compute_contrasts('conditionA - conditionB')
|
|
1101
|
+
|
|
1102
|
+
fitted = brain_data.fit(
|
|
1103
|
+
model='ridge', ridge_alpha=1.0, X=features, inplace=False
|
|
1104
|
+
)
|
|
1105
|
+
```
|
|
1106
|
+
"""
|
|
1107
|
+
from .modeling import _fit
|
|
1108
|
+
|
|
1109
|
+
return _fit(
|
|
1110
|
+
self,
|
|
1111
|
+
model=model,
|
|
1112
|
+
X=X,
|
|
1113
|
+
ridge_alpha=ridge_alpha,
|
|
1114
|
+
ridge_cv=ridge_cv,
|
|
1115
|
+
ridge_search_iterations=ridge_search_iterations,
|
|
1116
|
+
ridge_dirichlet_concentration=ridge_dirichlet_concentration,
|
|
1117
|
+
ridge_device=ridge_device,
|
|
1118
|
+
ridge_memory_budget_gb=ridge_memory_budget_gb,
|
|
1119
|
+
ridge_per_target_alpha=ridge_per_target_alpha,
|
|
1120
|
+
ridge_prefer_conservative_alpha=ridge_prefer_conservative_alpha,
|
|
1121
|
+
ridge_progress_bar=ridge_progress_bar,
|
|
1122
|
+
glm_noise_model=glm_noise_model,
|
|
1123
|
+
glm_bins=glm_bins,
|
|
1124
|
+
glm_n_jobs=glm_n_jobs,
|
|
1125
|
+
inplace=inplace,
|
|
1126
|
+
random_state=random_state,
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
def mean(self, axis=0):
|
|
1130
|
+
"""Get mean of each voxel or image.
|
|
1131
|
+
|
|
1132
|
+
Args:
|
|
1133
|
+
axis (int): 0 = across images (default, returns BrainData),
|
|
1134
|
+
1 = within images (returns array).
|
|
1135
|
+
|
|
1136
|
+
Returns:
|
|
1137
|
+
float | np.ndarray | BrainData: Mean values.
|
|
1138
|
+
"""
|
|
1139
|
+
from .utils import _apply_func
|
|
1140
|
+
|
|
1141
|
+
return _apply_func(self, np.mean, axis)
|
|
1142
|
+
|
|
1143
|
+
def median(self, axis=0):
|
|
1144
|
+
"""Get median of each voxel or image.
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
axis (int): 0 = across images (default, returns BrainData),
|
|
1148
|
+
1 = within images (returns array).
|
|
1149
|
+
|
|
1150
|
+
Returns:
|
|
1151
|
+
float | np.ndarray | BrainData: Median values.
|
|
1152
|
+
"""
|
|
1153
|
+
from .utils import _apply_func
|
|
1154
|
+
|
|
1155
|
+
return _apply_func(self, np.median, axis)
|
|
1156
|
+
|
|
1157
|
+
def multivariate_similarity(self, images, tail=2):
|
|
1158
|
+
"""Predict a BrainData spatial distribution from a linear combination.
|
|
1159
|
+
|
|
1160
|
+
The predictors may be other BrainData instances or nibabel images.
|
|
1161
|
+
|
|
1162
|
+
Args:
|
|
1163
|
+
images (BrainData | Nifti1Image | list): Predictor image(s) — a
|
|
1164
|
+
BrainData stack of weight maps or nibabel images.
|
|
1165
|
+
tail (int | str): ``2`` or ``'two'`` for two-tailed (default); ``1`` or
|
|
1166
|
+
``'one'`` for one-tailed (positive direction) regression p-values.
|
|
1167
|
+
|
|
1168
|
+
Returns:
|
|
1169
|
+
dict: Regression statistics as BrainData instances, keyed
|
|
1170
|
+
`'beta'`, `'t'`, `'p'`, `'df'`, `'residual'`.
|
|
1171
|
+
"""
|
|
1172
|
+
from .analysis import _multivariate_similarity
|
|
1173
|
+
|
|
1174
|
+
return _multivariate_similarity(self, images, tail=tail)
|
|
1175
|
+
|
|
1176
|
+
def plot( # nosemgrep: kwargs-internal-forwarding # forwards to nilearn plotting functions
|
|
1177
|
+
self,
|
|
1178
|
+
*,
|
|
1179
|
+
method="glass",
|
|
1180
|
+
upper=None,
|
|
1181
|
+
lower=None,
|
|
1182
|
+
threshold=None,
|
|
1183
|
+
view="z",
|
|
1184
|
+
cut_coords=None,
|
|
1185
|
+
cmap=None,
|
|
1186
|
+
bg_img=None,
|
|
1187
|
+
ax=None,
|
|
1188
|
+
figsize=(8, 6),
|
|
1189
|
+
title=None,
|
|
1190
|
+
colorbar=True,
|
|
1191
|
+
save=None,
|
|
1192
|
+
stat="mean",
|
|
1193
|
+
limit=3,
|
|
1194
|
+
**kwargs,
|
|
1195
|
+
):
|
|
1196
|
+
"""Plot BrainData instance using nilearn visualization or matplotlib.
|
|
1197
|
+
|
|
1198
|
+
Args:
|
|
1199
|
+
method (str): Visualization type: 'glass', 'slices', 'timeseries', 'histogram'
|
|
1200
|
+
upper (str/float, optional): Upper threshold.
|
|
1201
|
+
lower (str/float, optional): Lower threshold.
|
|
1202
|
+
threshold (float | str, optional): Absolute transparency cutoff.
|
|
1203
|
+
Percentile strings resolve over finite, nonzero magnitudes.
|
|
1204
|
+
view (str): For ``method="slices"``, any non-empty combination of
|
|
1205
|
+
``"x"``, ``"y"``, ``"z"`` (e.g. ``"xyz"``, ``"xz"``, ``"y"``).
|
|
1206
|
+
Default: ``"z"``.
|
|
1207
|
+
cut_coords (list or dict, optional): Cut coordinates for
|
|
1208
|
+
multi-slice views. Takes precedence over ``view``-based
|
|
1209
|
+
defaults. Either a list matching ``len(view)`` or a dict
|
|
1210
|
+
keyed by axis letter.
|
|
1211
|
+
cmap (str, optional): Colormap name. Defaults are sign-aware.
|
|
1212
|
+
bg_img (str/nibabel image, optional): Background image.
|
|
1213
|
+
ax (matplotlib.axes.Axes, optional): Matplotlib axis.
|
|
1214
|
+
figsize (tuple, optional): default figure size if no axis (8, 6)
|
|
1215
|
+
title (str, optional): Plot title.
|
|
1216
|
+
colorbar (bool): Whether to show colorbar. Default: True.
|
|
1217
|
+
save (str, optional): Path to save figure(s).
|
|
1218
|
+
stat (str): Statistic for timeseries plots. Default: 'mean'.
|
|
1219
|
+
limit (int): Maximum number of images to render when this
|
|
1220
|
+
BrainData contains multiple maps and ``method`` is
|
|
1221
|
+
``"glass"`` or ``"slices"``. Default: 3. Warns when more
|
|
1222
|
+
images exist than ``limit``.
|
|
1223
|
+
**kwargs (dict): Additional arguments passed to nilearn plot functions.
|
|
1224
|
+
|
|
1225
|
+
Returns:
|
|
1226
|
+
matplotlib.figure.Figure | list[matplotlib.figure.Figure]: A single
|
|
1227
|
+
figure for single-image data; a list of figures for multi-image
|
|
1228
|
+
data with `method` in `{"glass", "slices"}` (one per image for
|
|
1229
|
+
glass; one per image-and-view pair for slices).
|
|
1230
|
+
"""
|
|
1231
|
+
from .plotting import _plot_brain
|
|
1232
|
+
|
|
1233
|
+
return _plot_brain(
|
|
1234
|
+
self,
|
|
1235
|
+
method=method,
|
|
1236
|
+
upper=upper,
|
|
1237
|
+
lower=lower,
|
|
1238
|
+
threshold=threshold,
|
|
1239
|
+
view=view,
|
|
1240
|
+
cut_coords=cut_coords,
|
|
1241
|
+
cmap=cmap,
|
|
1242
|
+
bg_img=bg_img,
|
|
1243
|
+
ax=ax,
|
|
1244
|
+
figsize=figsize,
|
|
1245
|
+
title=title,
|
|
1246
|
+
colorbar=colorbar,
|
|
1247
|
+
save=save,
|
|
1248
|
+
stat=stat,
|
|
1249
|
+
limit=limit,
|
|
1250
|
+
**kwargs,
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
def plot_flatmap(
|
|
1254
|
+
self,
|
|
1255
|
+
*,
|
|
1256
|
+
threshold=None,
|
|
1257
|
+
cmap=None,
|
|
1258
|
+
vmax=None,
|
|
1259
|
+
vmin=None,
|
|
1260
|
+
template="fsaverage5",
|
|
1261
|
+
transparency="auto",
|
|
1262
|
+
colorbar=True,
|
|
1263
|
+
figsize=(12, 6),
|
|
1264
|
+
title=None,
|
|
1265
|
+
save=None,
|
|
1266
|
+
):
|
|
1267
|
+
"""Plot brain data on cortical flatmap.
|
|
1268
|
+
|
|
1269
|
+
Args:
|
|
1270
|
+
threshold (float | str, optional): Absolute cutoff or percentile string.
|
|
1271
|
+
cmap (str, optional): Matplotlib colormap. Defaults are sign-aware.
|
|
1272
|
+
vmax (float, optional): Maximum value; inferred from displayed data.
|
|
1273
|
+
vmin (float, optional): Minimum value; inferred from displayed data.
|
|
1274
|
+
template (str): Freesurfer surface resolution. Default: 'fsaverage5'.
|
|
1275
|
+
transparency (BrainData, Nifti1Image, str, or "auto"): Binary mask
|
|
1276
|
+
used to render vertices outside the mask as transparent.
|
|
1277
|
+
``"auto"`` (default) uses the instance's ``.mask``; pass
|
|
1278
|
+
``None`` to disable masking.
|
|
1279
|
+
colorbar (bool): Show colorbar. Default: True.
|
|
1280
|
+
figsize (tuple): Figure size as (width, height). Default: (12, 6).
|
|
1281
|
+
title (str, optional): Figure title.
|
|
1282
|
+
save (str, optional): File path to save figure.
|
|
1283
|
+
|
|
1284
|
+
Returns:
|
|
1285
|
+
matplotlib.figure.Figure: The rendered figure.
|
|
1286
|
+
"""
|
|
1287
|
+
from nltools.plotting import _plot_flatmap
|
|
1288
|
+
|
|
1289
|
+
return _plot_flatmap(
|
|
1290
|
+
self,
|
|
1291
|
+
threshold=threshold,
|
|
1292
|
+
cmap=cmap,
|
|
1293
|
+
vmax=vmax,
|
|
1294
|
+
vmin=vmin,
|
|
1295
|
+
template=template,
|
|
1296
|
+
transparency=transparency,
|
|
1297
|
+
colorbar=colorbar,
|
|
1298
|
+
figsize=figsize,
|
|
1299
|
+
title=title,
|
|
1300
|
+
save=save,
|
|
1301
|
+
)
|
|
1302
|
+
|
|
1303
|
+
def plot_surf(
|
|
1304
|
+
self,
|
|
1305
|
+
*,
|
|
1306
|
+
hemi="both",
|
|
1307
|
+
view="montage",
|
|
1308
|
+
surface="pial",
|
|
1309
|
+
template="fsaverage5",
|
|
1310
|
+
threshold=None,
|
|
1311
|
+
cmap=None,
|
|
1312
|
+
vmin=None,
|
|
1313
|
+
vmax=None,
|
|
1314
|
+
transparency="auto",
|
|
1315
|
+
colorbar=True,
|
|
1316
|
+
figsize=(10, 8),
|
|
1317
|
+
title=None,
|
|
1318
|
+
save=None,
|
|
1319
|
+
):
|
|
1320
|
+
"""Render this BrainData on fsaverage surfaces as a tight 2×2 montage.
|
|
1321
|
+
|
|
1322
|
+
Facade over `plot_surf`. See that function's docstring for the full
|
|
1323
|
+
argument reference. Notable defaults: ``surface="pial"``,
|
|
1324
|
+
``transparency="auto"`` (uses this instance's ``.mask``).
|
|
1325
|
+
|
|
1326
|
+
Returns:
|
|
1327
|
+
matplotlib.figure.Figure: The rendered figure.
|
|
1328
|
+
"""
|
|
1329
|
+
from nltools.plotting import _plot_surf
|
|
1330
|
+
|
|
1331
|
+
return _plot_surf(
|
|
1332
|
+
self,
|
|
1333
|
+
hemi=hemi,
|
|
1334
|
+
view=view,
|
|
1335
|
+
surface=surface,
|
|
1336
|
+
template=template,
|
|
1337
|
+
threshold=threshold,
|
|
1338
|
+
cmap=cmap,
|
|
1339
|
+
vmin=vmin,
|
|
1340
|
+
vmax=vmax,
|
|
1341
|
+
transparency=transparency,
|
|
1342
|
+
colorbar=colorbar,
|
|
1343
|
+
figsize=figsize,
|
|
1344
|
+
title=title,
|
|
1345
|
+
save=save,
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
def iplot( # nosemgrep: kwargs-internal-forwarding # forwards to new Niivue(opts)
|
|
1349
|
+
self,
|
|
1350
|
+
*,
|
|
1351
|
+
view: str = "ortho",
|
|
1352
|
+
threshold: "float | str | None" = None,
|
|
1353
|
+
lower: "float | str | None" = None,
|
|
1354
|
+
upper: "float | str | None" = None,
|
|
1355
|
+
autoscale: bool = True,
|
|
1356
|
+
symmetric: bool | Literal["auto"] = "auto",
|
|
1357
|
+
cmap: "str | None" = None,
|
|
1358
|
+
bg_img: "str | bool | None" = None,
|
|
1359
|
+
atlas: "str | _Atlas | None" = None,
|
|
1360
|
+
opacity: float = 1.0,
|
|
1361
|
+
outline: float = 0.0,
|
|
1362
|
+
colorbar: bool = True,
|
|
1363
|
+
controls: bool = True,
|
|
1364
|
+
**kwargs,
|
|
1365
|
+
):
|
|
1366
|
+
"""Interactive WebGL brain viewer powered by niivue.
|
|
1367
|
+
|
|
1368
|
+
Renders inline in a live kernel (Jupyter, marimo) with
|
|
1369
|
+
live windowing (right-drag to set the threshold/contrast), slice
|
|
1370
|
+
scrolling, native 4D frame scrubbing, true 3D rendering, a stat-map
|
|
1371
|
+
colorbar, and optional nltools-atlas overlays. Static-built docs (plain
|
|
1372
|
+
Markdown) are not interactive; use `plot` there.
|
|
1373
|
+
|
|
1374
|
+
Returns a `_NiivueViewer` widget. By default (``controls=True``) it
|
|
1375
|
+
renders an in-widget threshold slider above the viewer; the window is
|
|
1376
|
+
reactive through the ``cal_min`` / ``cal_max`` traits. Pass
|
|
1377
|
+
``controls=False`` to hide the slider (right-drag windowing still
|
|
1378
|
+
works).
|
|
1379
|
+
|
|
1380
|
+
Thresholding uses positive and negative display limbs. ``cal_min`` is
|
|
1381
|
+
the magnitude floor and ``cal_max`` the positive saturation point;
|
|
1382
|
+
niivue receives the negative endpoints explicitly. Both are
|
|
1383
|
+
**magnitudes**, and the floor is always strictly positive — a floor of
|
|
1384
|
+
zero would admit every zero-valued voxel (everything outside the mask)
|
|
1385
|
+
and paint the whole volume — so ``lower`` / ``threshold`` values at or
|
|
1386
|
+
below zero are taken as their magnitude and raised to the slider's
|
|
1387
|
+
smallest step. By default, mixed maps use symmetric limbs while each
|
|
1388
|
+
sign in a one-sided map determines its own ceiling. The window is
|
|
1389
|
+
computed in Python, and the two controls show the shared floor and
|
|
1390
|
+
positive-limb ceiling.
|
|
1391
|
+
|
|
1392
|
+
Args:
|
|
1393
|
+
view: ``"ortho"`` (default), ``"axial"``, ``"coronal"``,
|
|
1394
|
+
``"sagittal"``, or ``"render"`` (3D volume render).
|
|
1395
|
+
``"surface"`` is no longer supported — use ``"render"`` or
|
|
1396
|
+
`plot_flatmap` / `plot_surf`.
|
|
1397
|
+
threshold: Convenience symmetric magnitude floor (→ ``cal_min``).
|
|
1398
|
+
Accepts a percentile string (``"95%"``) resolved over the
|
|
1399
|
+
finite nonzero magnitudes, consistent with `threshold`.
|
|
1400
|
+
lower: Window floor as a magnitude (→ ``cal_min``). Overrides
|
|
1401
|
+
``threshold``. Accepts a percentile string. A value at or
|
|
1402
|
+
below zero is raised to the slider's smallest step.
|
|
1403
|
+
upper: Window ceiling as a magnitude (→ ``cal_max``). Overrides
|
|
1404
|
+
``threshold``. Accepts a percentile string.
|
|
1405
|
+
autoscale: Robust default window for the edges not set above.
|
|
1406
|
+
``True`` (default): ceiling at the 98th percentile of the
|
|
1407
|
+
finite nonzero magnitudes — a couple of outlier voxels no
|
|
1408
|
+
longer wash out the whole map — and an epsilon floor, never
|
|
1409
|
+
above the smallest nonzero magnitude, so zeros render
|
|
1410
|
+
transparent and every real voxel stays visible (threshold up
|
|
1411
|
+
from there). ``False``: the raw magnitude range, from one
|
|
1412
|
+
slider step above zero to the largest absolute value. For a
|
|
1413
|
+
custom percentile window pass
|
|
1414
|
+
``lower``/``upper`` (e.g. ``lower="60%", upper="98%"``).
|
|
1415
|
+
symmetric: ``"auto"`` (default) mirrors mixed-signed maps but lets
|
|
1416
|
+
each sign in a one-sided map determine its own ceiling. ``True``
|
|
1417
|
+
always mirrors; ``False`` scales positive and negative limbs
|
|
1418
|
+
independently.
|
|
1419
|
+
cmap: niivue colormap for the positive limb. The default uses
|
|
1420
|
+
niivue's red positive and blue negative palettes. Common
|
|
1421
|
+
matplotlib names are auto-mapped with a warning.
|
|
1422
|
+
bg_img: ``None``/``True`` auto-loads the matching MNI template
|
|
1423
|
+
when the data is in standard space (else none); ``False``
|
|
1424
|
+
disables the background; a path string uses that image.
|
|
1425
|
+
atlas: Atlas overlay — a registry name (e.g. ``"aal"``), a
|
|
1426
|
+
loaded atlas record, or ``None``. Deterministic atlases
|
|
1427
|
+
only; probabilistic atlases raise.
|
|
1428
|
+
opacity: Stat-map (and filled-atlas) opacity in ``0..1``.
|
|
1429
|
+
outline: ``> 0`` draws atlas region boundaries of that width
|
|
1430
|
+
(stat map stays visible); ``0`` draws filled regions.
|
|
1431
|
+
colorbar: Show the stat-map colorbar (default ``True``). An
|
|
1432
|
+
explicit ``is_colorbar`` kwarg overrides this.
|
|
1433
|
+
controls: Render an in-widget threshold slider above the viewer
|
|
1434
|
+
(default ``True``). ``False`` hides it; the viewer still
|
|
1435
|
+
supports niivue's right-drag windowing. No extra dependency
|
|
1436
|
+
either way — the slider is native to the widget frontend.
|
|
1437
|
+
**kwargs (dict): Passed as niivue options. ``height`` configures
|
|
1438
|
+
the canvas and ``is_colorbar`` overrides ``colorbar``.
|
|
1439
|
+
|
|
1440
|
+
Returns:
|
|
1441
|
+
_NiivueViewer: An `anywidget.AnyWidget` whose threshold window is
|
|
1442
|
+
reactive via the `cal_min` and `cal_max` traits.
|
|
1443
|
+
|
|
1444
|
+
Raises:
|
|
1445
|
+
TypeError: If ``autoscale`` is not a bool or ``symmetric`` is not
|
|
1446
|
+
``True``, ``False``, or ``"auto"``.
|
|
1447
|
+
"""
|
|
1448
|
+
from .viewer import _build_viewer, _compute_display_window
|
|
1449
|
+
|
|
1450
|
+
window = _compute_display_window(
|
|
1451
|
+
self.data,
|
|
1452
|
+
autoscale=autoscale,
|
|
1453
|
+
threshold=threshold,
|
|
1454
|
+
lower=lower,
|
|
1455
|
+
upper=upper,
|
|
1456
|
+
symmetric=symmetric,
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
return _build_viewer(
|
|
1460
|
+
self,
|
|
1461
|
+
view=view,
|
|
1462
|
+
window=window,
|
|
1463
|
+
cmap=cmap,
|
|
1464
|
+
atlas=atlas,
|
|
1465
|
+
bg_img=bg_img,
|
|
1466
|
+
opacity=opacity,
|
|
1467
|
+
outline=outline,
|
|
1468
|
+
colorbar=colorbar,
|
|
1469
|
+
controls=controls,
|
|
1470
|
+
niivue_opts=kwargs,
|
|
1471
|
+
)
|
|
1472
|
+
|
|
1473
|
+
@overload
|
|
1474
|
+
def predict(
|
|
1475
|
+
self,
|
|
1476
|
+
*,
|
|
1477
|
+
X: "DesignMatrix | np.ndarray | Mapping[str, np.ndarray]",
|
|
1478
|
+
y: None = None,
|
|
1479
|
+
estimator: "str | BaseEstimator" = "linear_svc",
|
|
1480
|
+
estimator_kwargs: "dict | None" = None,
|
|
1481
|
+
cv: "int | BaseCrossValidator | None" = None,
|
|
1482
|
+
groups: "np.ndarray | str | None" = None,
|
|
1483
|
+
scoring: "str | Callable | None" = None,
|
|
1484
|
+
spatial_scale: Literal["whole_brain", "roi", "searchlight"] = "whole_brain",
|
|
1485
|
+
roi_mask: "Nifti1Image | str | Path | None" = None,
|
|
1486
|
+
radius: float = 10.0,
|
|
1487
|
+
plot: bool = False,
|
|
1488
|
+
n_jobs: int = 1,
|
|
1489
|
+
progress_bar: bool = False,
|
|
1490
|
+
) -> "BrainData": ...
|
|
1491
|
+
|
|
1492
|
+
@overload
|
|
1493
|
+
def predict(
|
|
1494
|
+
self,
|
|
1495
|
+
*,
|
|
1496
|
+
X: None = None,
|
|
1497
|
+
y: "np.ndarray | str | None" = None,
|
|
1498
|
+
estimator: "str | BaseEstimator" = "linear_svc",
|
|
1499
|
+
estimator_kwargs: "dict | None" = None,
|
|
1500
|
+
cv: "int | BaseCrossValidator | None" = None,
|
|
1501
|
+
groups: "np.ndarray | str | None" = None,
|
|
1502
|
+
scoring: "str | Callable | None" = None,
|
|
1503
|
+
spatial_scale: Literal["whole_brain", "roi", "searchlight"] = "whole_brain",
|
|
1504
|
+
roi_mask: "Nifti1Image | str | Path | None" = None,
|
|
1505
|
+
radius: float = 10.0,
|
|
1506
|
+
plot: bool = False,
|
|
1507
|
+
n_jobs: int = 1,
|
|
1508
|
+
progress_bar: bool = False,
|
|
1509
|
+
) -> "Predict": ...
|
|
1510
|
+
|
|
1511
|
+
@_coalesced_gc()
|
|
1512
|
+
def predict(
|
|
1513
|
+
self,
|
|
1514
|
+
*,
|
|
1515
|
+
X: "DesignMatrix | np.ndarray | Mapping[str, np.ndarray] | None" = None,
|
|
1516
|
+
y: "np.ndarray | str | None" = None,
|
|
1517
|
+
estimator: "str | BaseEstimator" = "linear_svc",
|
|
1518
|
+
estimator_kwargs: "dict | None" = None,
|
|
1519
|
+
cv: "int | BaseCrossValidator | None" = None,
|
|
1520
|
+
groups: "np.ndarray | str | None" = None,
|
|
1521
|
+
scoring: "str | Callable | None" = None,
|
|
1522
|
+
spatial_scale: Literal["whole_brain", "roi", "searchlight"] = "whole_brain",
|
|
1523
|
+
roi_mask: "Nifti1Image | str | Path | None" = None,
|
|
1524
|
+
radius: float = 10.0,
|
|
1525
|
+
plot: bool = False,
|
|
1526
|
+
n_jobs: int = 1,
|
|
1527
|
+
progress_bar: bool = False,
|
|
1528
|
+
):
|
|
1529
|
+
"""Predict voxel responses from a fitted model, or decode labels with MVPA.
|
|
1530
|
+
|
|
1531
|
+
Exactly one mode is resolved before any work happens:
|
|
1532
|
+
|
|
1533
|
+
- an explicit ``y=`` runs MVPA decoding and returns a `Predict`;
|
|
1534
|
+
- an explicit ``X=`` predicts from the fitted `_Glm` or `_Ridge` and
|
|
1535
|
+
returns a new, independently owned `BrainData`;
|
|
1536
|
+
- with neither argument and a fitted model, an independent copy of the
|
|
1537
|
+
stored training predictions;
|
|
1538
|
+
- with neither argument, no fitted model, and exactly one ``.Y`` column,
|
|
1539
|
+
MVPA on that column.
|
|
1540
|
+
|
|
1541
|
+
Supplying both ``X`` and ``y``, or a decoding argument on a
|
|
1542
|
+
fitted-model call, raises before prediction begins. A fitted model wins
|
|
1543
|
+
over an attached ``.Y`` on the no-argument call — pass ``y=``
|
|
1544
|
+
explicitly to decode instead. `predict` never mutates the source and
|
|
1545
|
+
attaches nothing to it.
|
|
1546
|
+
|
|
1547
|
+
Labels travel with the data: ``y='name'`` picks a column of ``.Y``, and
|
|
1548
|
+
``groups`` accepts a ``.Y`` column name the same way. With an explicit
|
|
1549
|
+
``X=``, the estimator validates and aligns it: a `DesignMatrix` whose
|
|
1550
|
+
column names `_Glm.predict` matches to the fitted order, or, for a
|
|
1551
|
+
banded `_Ridge`, a mapping with exactly the fitted feature-space names
|
|
1552
|
+
in any order.
|
|
1553
|
+
|
|
1554
|
+
Args:
|
|
1555
|
+
X (DesignMatrix | array-like | Mapping, optional): Features for
|
|
1556
|
+
fitted-model prediction, shape ``(n_samples, n_features)``, or a
|
|
1557
|
+
mapping of feature-space names to matrices for a banded `_Ridge`.
|
|
1558
|
+
y (array-like | str, optional): Labels (classification) or
|
|
1559
|
+
continuous targets (regression), shape ``(n_samples,)``, or the
|
|
1560
|
+
name of a ``.Y`` column. Must be one-dimensional with one value
|
|
1561
|
+
per row; multioutput and multilabel targets are not accepted.
|
|
1562
|
+
estimator (str | sklearn estimator): A built-in shortcut —
|
|
1563
|
+
``'linear_svc'``, ``'logistic_regression'``,
|
|
1564
|
+
``'linear_discriminant_analysis'``, ``'ridge_classifier'``,
|
|
1565
|
+
``'ridge'``, ``'lasso'``, ``'linear_svr'`` — or any sklearn
|
|
1566
|
+
estimator or `Pipeline`, which is used exactly as supplied.
|
|
1567
|
+
Default ``'linear_svc'``. Every shortcut standardizes voxels
|
|
1568
|
+
inside each fold and then fits a linear estimator; the two ridge
|
|
1569
|
+
shortcuts select their penalty inside that fold too, by an inner
|
|
1570
|
+
cross-validation over a ten-point log grid from ``1e-3`` to
|
|
1571
|
+
``1e6`` (`RidgeCV` and `RidgeClassifierCV`), because the working
|
|
1572
|
+
penalty at whole-brain scale is nowhere near scikit-learn's
|
|
1573
|
+
default ``alpha=1``. A classification shortcut on a multiclass
|
|
1574
|
+
target is wrapped in
|
|
1575
|
+
`OneVsRestClassifier`, so every class gets its own signed map.
|
|
1576
|
+
A caller-supplied estimator is never wrapped and never has its
|
|
1577
|
+
multiclass strategy overridden — pass a `OneVsRestClassifier`
|
|
1578
|
+
to get one. Every preprocessing step, in every spatial
|
|
1579
|
+
scale, must be one of `StandardScaler`, `PCA`,
|
|
1580
|
+
`VarianceThreshold`,
|
|
1581
|
+
`GenericUnivariateSelect`, `SelectPercentile`, `SelectKBest`,
|
|
1582
|
+
`SelectFpr`, `SelectFdr`, `SelectFwe`, `SelectFromModel`,
|
|
1583
|
+
`RFE`, `RFECV`, `SequentialFeatureSelector`, ``None``, or
|
|
1584
|
+
``'passthrough'``. Whole-brain and ROI pipelines must also end
|
|
1585
|
+
in an estimator exposing ``coef_``, since those two scales
|
|
1586
|
+
extract a weight map; searchlight builds none and does not
|
|
1587
|
+
require it.
|
|
1588
|
+
estimator_kwargs (dict, optional): Constructor options for a
|
|
1589
|
+
shortcut, merged over the shortcut's own defaults so a supplied
|
|
1590
|
+
key wins — ``estimator='ridge', estimator_kwargs={'alphas': [1e4]}``
|
|
1591
|
+
fixes the penalty grid. Passing it alongside a caller-supplied
|
|
1592
|
+
estimator raises `ValueError`: that estimator is used exactly as
|
|
1593
|
+
given, so configure it at construction.
|
|
1594
|
+
cv (int | sklearn splitter, optional): ``None`` (the default, five
|
|
1595
|
+
folds) or an int fold count both mean a deterministic,
|
|
1596
|
+
unshuffled *stratified* K-fold: class-balanced folds for a
|
|
1597
|
+
classifier, and for a regressor folds balanced on quantile bins
|
|
1598
|
+
of ``y`` so every fold matches the outcome distribution — which
|
|
1599
|
+
needs at least two rows per fold, and raises below that. With
|
|
1600
|
+
``groups`` given, the group-aware variant
|
|
1601
|
+
(``StratifiedGroupKFold``), so no group is split across the
|
|
1602
|
+
train/test boundary. An sklearn splitter is used as supplied.
|
|
1603
|
+
Test folds must partition the rows, so shuffle-split and
|
|
1604
|
+
repeated splitters raise.
|
|
1605
|
+
groups (array-like | str, optional): Group labels — one per row
|
|
1606
|
+
(subject, run), or the name of a ``.Y`` column holding them.
|
|
1607
|
+
With an integer or ``None`` ``cv`` they select the group-aware
|
|
1608
|
+
splitter; with an explicit splitter they are forwarded to its
|
|
1609
|
+
``split()`` (e.g. ``LeaveOneGroupOut`` for leave-one-run-out).
|
|
1610
|
+
scoring (str | callable, optional): Follows scikit-learn's
|
|
1611
|
+
single-metric scoring contract. ``None`` (the default) uses the
|
|
1612
|
+
estimator's own ``score`` method; a scoring name or callable
|
|
1613
|
+
overrides it. Multimetric mappings are not accepted.
|
|
1614
|
+
spatial_scale (str): MVPA dispatch — ``'whole_brain'``, ``'roi'``,
|
|
1615
|
+
or ``'searchlight'``.
|
|
1616
|
+
roi_mask (Nifti1Image | path-like, optional): Atlas image; required
|
|
1617
|
+
by, and only valid for, ``spatial_scale='roi'``.
|
|
1618
|
+
radius (float): Searchlight sphere radius in millimeters; only
|
|
1619
|
+
valid for ``spatial_scale='searchlight'``. Default ``10.0``.
|
|
1620
|
+
plot (bool): Draw the cross-validated figures as a side effect.
|
|
1621
|
+
Default ``False``; the returned `Predict` is the same either
|
|
1622
|
+
way. Regression draws predicted values against observed ones,
|
|
1623
|
+
titled with the cross-validated Pearson *r*. Binary
|
|
1624
|
+
classification draws the ROC of the out-of-fold decision values,
|
|
1625
|
+
then the margin figure (`decision_function`) or the probability
|
|
1626
|
+
figure (`predict_proba`), whichever the estimator exposes. Both
|
|
1627
|
+
also draw ``weight_map``. Multiclass decoding, and any spatial
|
|
1628
|
+
scale other than ``'whole_brain'``, raise instead of drawing.
|
|
1629
|
+
n_jobs (int): Parallel workers for the outer independent work of
|
|
1630
|
+
the selected spatial scale — cross-validation folds for
|
|
1631
|
+
whole-brain, parcels for ROI, spheres for searchlight. Default
|
|
1632
|
+
``1``; every worker holds a copy of the data, so a real brain
|
|
1633
|
+
at higher ``n_jobs`` can be memory-heavy.
|
|
1634
|
+
progress_bar (bool): Show a progress bar for searchlight and ROI.
|
|
1635
|
+
|
|
1636
|
+
Returns:
|
|
1637
|
+
Predict | BrainData: A `Predict` record for MVPA; a new `BrainData`
|
|
1638
|
+
holding the predicted timeseries for fitted-model prediction.
|
|
1639
|
+
The record's ``spatial_scale`` says which of its fields carry
|
|
1640
|
+
values: whole-brain fills ``predictions``, ``cv_folds``,
|
|
1641
|
+
``scores``, ``estimator`` and ``weight_map``; ROI fills
|
|
1642
|
+
``scores``, ``roi_labels``, ``score_map`` and ``weight_map``;
|
|
1643
|
+
searchlight fills ``score_map`` alone. ``classes`` accompanies
|
|
1644
|
+
any classifier and ``scoring`` records the scoring
|
|
1645
|
+
specification in every mode. ``mean_score`` and ``std_score``
|
|
1646
|
+
are computed from ``scores`` on demand and do not exist for a
|
|
1647
|
+
searchlight result. ``weight_map`` holds one coefficient map
|
|
1648
|
+
for regression and binary classification (the signed map for
|
|
1649
|
+
``classes[1]`` versus ``classes[0]``) and one map per class, in
|
|
1650
|
+
``classes`` order, for multiclass — never an average across
|
|
1651
|
+
classes. It is projected back to voxel units through the
|
|
1652
|
+
pipeline's fitted preprocessing, but centering is not undone,
|
|
1653
|
+
so ``raw_data @ weight_map`` does not reproduce the decision
|
|
1654
|
+
function; use ``result.estimator`` to predict.
|
|
1655
|
+
|
|
1656
|
+
Raises:
|
|
1657
|
+
ValueError: On both ``X`` and ``y``, a decoding argument on a
|
|
1658
|
+
fitted-model call, an unknown estimator shortcut or spatial
|
|
1659
|
+
scale, a target or group vector that is not one value per row,
|
|
1660
|
+
a continuous target with fewer than two rows per fold under an
|
|
1661
|
+
integer ``cv``, cross-validation folds that do not partition
|
|
1662
|
+
the rows, a preprocessing step outside the supported set, a
|
|
1663
|
+
pipeline whose coefficients cannot be projected back onto the
|
|
1664
|
+
voxel axis (whole-brain and ROI decoding only),
|
|
1665
|
+
``estimator_kwargs`` alongside a caller-supplied estimator, or
|
|
1666
|
+
``plot=True`` on a multiclass target or a non-whole-brain scale.
|
|
1667
|
+
TypeError: On a removed keyword, an `estimator` that is neither a
|
|
1668
|
+
shortcut name nor an object with `fit`/`predict`, or a `cv`
|
|
1669
|
+
that is neither `None`, an int, nor a splitter.
|
|
1670
|
+
|
|
1671
|
+
Note:
|
|
1672
|
+
``weight_map`` is the estimator refit on every observation after
|
|
1673
|
+
cross-validation — the map you publish. Every successful
|
|
1674
|
+
whole-brain or ROI result carries one; a pipeline that cannot
|
|
1675
|
+
produce one raises instead of returning ``None``. Fold-specific
|
|
1676
|
+
coefficient maps are deliberately absent: fits on overlapping
|
|
1677
|
+
training folds are not independent uncertainty samples.
|
|
1678
|
+
|
|
1679
|
+
Note:
|
|
1680
|
+
``cv=None`` and an integer ``cv`` do not shuffle, so the folds are
|
|
1681
|
+
reproducible across calls. They are stratified rather than
|
|
1682
|
+
contiguous, so rows ordered by condition or by outcome no longer
|
|
1683
|
+
make degenerate folds. Pass an explicit
|
|
1684
|
+
``KFold(n_splits=5, shuffle=True, random_state=0)`` if you want a
|
|
1685
|
+
shuffled split anyway.
|
|
1686
|
+
|
|
1687
|
+
Examples:
|
|
1688
|
+
Whole-brain decoding:
|
|
1689
|
+
|
|
1690
|
+
```python
|
|
1691
|
+
result = brain.predict(y=labels, cv=5)
|
|
1692
|
+
result.weight_map.plot() # the all-data refit — the publishable map
|
|
1693
|
+
result.mean_score # the cross-validated score
|
|
1694
|
+
new_pred = result.estimator.predict(new_X)
|
|
1695
|
+
```
|
|
1696
|
+
|
|
1697
|
+
Searchlight and ROI decoding:
|
|
1698
|
+
|
|
1699
|
+
```python
|
|
1700
|
+
result = brain.predict(
|
|
1701
|
+
y=labels, spatial_scale='searchlight', radius=8.0, n_jobs=4
|
|
1702
|
+
)
|
|
1703
|
+
result.score_map.plot() # one score per sphere center
|
|
1704
|
+
|
|
1705
|
+
result = brain.predict(y=labels, spatial_scale='roi', roi_mask=atlas)
|
|
1706
|
+
result.mean_score # one score per parcel
|
|
1707
|
+
result.score_map.plot() # those scores painted into voxel space
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
Prediction from a fitted encoding model:
|
|
1711
|
+
|
|
1712
|
+
```python
|
|
1713
|
+
brain.fit(model='ridge', X=features)
|
|
1714
|
+
predicted = brain.predict(X=new_features)
|
|
1715
|
+
```
|
|
1716
|
+
"""
|
|
1717
|
+
from .prediction import _predict
|
|
1718
|
+
|
|
1719
|
+
return _predict(
|
|
1720
|
+
self,
|
|
1721
|
+
X=X,
|
|
1722
|
+
y=y,
|
|
1723
|
+
estimator=estimator,
|
|
1724
|
+
estimator_kwargs=estimator_kwargs,
|
|
1725
|
+
cv=cv,
|
|
1726
|
+
groups=groups,
|
|
1727
|
+
scoring=scoring,
|
|
1728
|
+
spatial_scale=spatial_scale,
|
|
1729
|
+
roi_mask=roi_mask,
|
|
1730
|
+
radius=radius,
|
|
1731
|
+
plot=plot,
|
|
1732
|
+
n_jobs=n_jobs,
|
|
1733
|
+
progress_bar=progress_bar,
|
|
1734
|
+
)
|
|
1735
|
+
|
|
1736
|
+
def r_to_z(self):
|
|
1737
|
+
"""Apply Fisher's r-to-z transformation to each data element."""
|
|
1738
|
+
from .analysis import _r_to_z
|
|
1739
|
+
|
|
1740
|
+
return _r_to_z(self)
|
|
1741
|
+
|
|
1742
|
+
@_coalesced_gc()
|
|
1743
|
+
def regions(
|
|
1744
|
+
self,
|
|
1745
|
+
*,
|
|
1746
|
+
min_region_size=1350,
|
|
1747
|
+
method="local_regions",
|
|
1748
|
+
smoothing_fwhm=6,
|
|
1749
|
+
is_mask=False,
|
|
1750
|
+
):
|
|
1751
|
+
"""Extract brain connected regions into separate regions.
|
|
1752
|
+
|
|
1753
|
+
Args:
|
|
1754
|
+
min_region_size (int): Minimum volume in mm3 for a region to be kept.
|
|
1755
|
+
method (str): Type of extraction method
|
|
1756
|
+
['connected_components', 'local_regions'].
|
|
1757
|
+
smoothing_fwhm (scalar): Smooth an image to extract more sparser regions.
|
|
1758
|
+
is_mask (bool): Whether to treat as boolean mask.
|
|
1759
|
+
|
|
1760
|
+
Returns:
|
|
1761
|
+
BrainData: BrainData instance with extracted ROIs as data.
|
|
1762
|
+
"""
|
|
1763
|
+
from .analysis import _regions
|
|
1764
|
+
|
|
1765
|
+
return _regions(
|
|
1766
|
+
self,
|
|
1767
|
+
min_region_size=min_region_size,
|
|
1768
|
+
method=method,
|
|
1769
|
+
smoothing_fwhm=smoothing_fwhm,
|
|
1770
|
+
is_mask=is_mask,
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1773
|
+
def resample(self, *, img=None, resolution=None, interpolation=None):
|
|
1774
|
+
"""Resample onto a new voxel grid, carrying the mask along.
|
|
1775
|
+
|
|
1776
|
+
Exactly one of `img` or `resolution` is required. An `img` supplies
|
|
1777
|
+
only the target grid: its intensity values never define the output
|
|
1778
|
+
mask. The current mask is resampled onto the target grid with
|
|
1779
|
+
nearest-neighbor interpolation, so the result's voxel support is the
|
|
1780
|
+
source support expressed on the new grid. Row-aligned `X` and `Y`
|
|
1781
|
+
survive; fitted state does not.
|
|
1782
|
+
|
|
1783
|
+
Args:
|
|
1784
|
+
img (Nifti1Image | str | Path | None): Target image supplying the
|
|
1785
|
+
grid to match.
|
|
1786
|
+
resolution (float | int | None): Target isotropic voxel size in mm.
|
|
1787
|
+
interpolation (str | None): Interpolation method for the data:
|
|
1788
|
+
``'nearest'``, ``'linear'``, ``'continuous'``, or ``None`` to
|
|
1789
|
+
use the instance's setting.
|
|
1790
|
+
|
|
1791
|
+
Returns:
|
|
1792
|
+
BrainData: New BrainData instance with resampled data and mask.
|
|
1793
|
+
|
|
1794
|
+
Raises:
|
|
1795
|
+
ValueError: If both ``img`` and ``resolution`` are None, both are
|
|
1796
|
+
provided, or ``resolution`` is not positive.
|
|
1797
|
+
TypeError: If ``img`` is not a valid image type.
|
|
1798
|
+
|
|
1799
|
+
Examples:
|
|
1800
|
+
```python
|
|
1801
|
+
coarse = brain.resample(resolution=3.0)
|
|
1802
|
+
on_atlas_grid = brain.resample(img=atlas_img)
|
|
1803
|
+
```
|
|
1804
|
+
"""
|
|
1805
|
+
from .io import _resample
|
|
1806
|
+
|
|
1807
|
+
return _resample(
|
|
1808
|
+
self, img=img, resolution=resolution, interpolation=interpolation
|
|
1809
|
+
)
|
|
1810
|
+
|
|
1811
|
+
def scale(self, scale_val=100.0, axis=None):
|
|
1812
|
+
"""Scale data via mean scaling.
|
|
1813
|
+
|
|
1814
|
+
Two scaling modes are available. **Grand-mean scaling** (``axis=None``,
|
|
1815
|
+
default) divides all values by the global mean across all voxels and
|
|
1816
|
+
timepoints. **Voxel-wise scaling** (``axis=0``) divides each voxel's
|
|
1817
|
+
time-series by its own temporal mean.
|
|
1818
|
+
|
|
1819
|
+
Args:
|
|
1820
|
+
scale_val (int | float): Target value for the mean after scaling.
|
|
1821
|
+
Default 100.
|
|
1822
|
+
axis (int | None): ``None`` for grand-mean scaling (default), ``0``
|
|
1823
|
+
for voxel-wise scaling.
|
|
1824
|
+
|
|
1825
|
+
Returns:
|
|
1826
|
+
BrainData: New BrainData instance with scaled data.
|
|
1827
|
+
"""
|
|
1828
|
+
from .analysis import _scale_data
|
|
1829
|
+
|
|
1830
|
+
return _scale_data(self, scale_val, axis)
|
|
1831
|
+
|
|
1832
|
+
@_coalesced_gc()
|
|
1833
|
+
def similarity(self, data, *, metric="correlation"):
|
|
1834
|
+
"""Calculate similarity to a single BrainData or nibabel image.
|
|
1835
|
+
|
|
1836
|
+
Args:
|
|
1837
|
+
data (BrainData | Nifti1Image): Image to evaluate similarity against.
|
|
1838
|
+
metric (str): Type of similarity: ``'correlation'`` (default),
|
|
1839
|
+
``'pearson'``, ``'rank_correlation'``, ``'spearman'``,
|
|
1840
|
+
``'dot_product'``, or ``'cosine'``.
|
|
1841
|
+
|
|
1842
|
+
Returns:
|
|
1843
|
+
float or np.ndarray: Similarity value(s).
|
|
1844
|
+
"""
|
|
1845
|
+
from .analysis import _similarity
|
|
1846
|
+
|
|
1847
|
+
return _similarity(self, data, metric=metric)
|
|
1848
|
+
|
|
1849
|
+
def smooth(self, fwhm):
|
|
1850
|
+
"""Apply spatial smoothing using nilearn smooth_img().
|
|
1851
|
+
|
|
1852
|
+
Args:
|
|
1853
|
+
fwhm (float): Full width at half maximum of the Gaussian spatial
|
|
1854
|
+
filter, in mm.
|
|
1855
|
+
|
|
1856
|
+
Returns:
|
|
1857
|
+
BrainData: Copy with smoothed data.
|
|
1858
|
+
"""
|
|
1859
|
+
from .analysis import _smooth
|
|
1860
|
+
|
|
1861
|
+
return _smooth(self, fwhm)
|
|
1862
|
+
|
|
1863
|
+
def standardize(self, *, method="center", axis=0):
|
|
1864
|
+
"""Standardize data by centering it, optionally scaling to unit variance.
|
|
1865
|
+
|
|
1866
|
+
Constant voxels (or observations) z-score to 0 rather than NaN.
|
|
1867
|
+
|
|
1868
|
+
Args:
|
|
1869
|
+
method (str): ``'center'`` subtracts the mean (default);
|
|
1870
|
+
``'zscore'`` subtracts the mean and divides by the standard
|
|
1871
|
+
deviation.
|
|
1872
|
+
axis (int): 0 standardizes each voxel across observations (default).
|
|
1873
|
+
1 standardizes each observation across voxels.
|
|
1874
|
+
|
|
1875
|
+
Returns:
|
|
1876
|
+
BrainData: Standardized BrainData instance.
|
|
1877
|
+
|
|
1878
|
+
Raises:
|
|
1879
|
+
ValueError: If `method` is neither ``'center'`` nor ``'zscore'``.
|
|
1880
|
+
"""
|
|
1881
|
+
from .analysis import _standardize
|
|
1882
|
+
|
|
1883
|
+
return _standardize(self, method=method, axis=axis)
|
|
1884
|
+
|
|
1885
|
+
def std(self, axis=0):
|
|
1886
|
+
"""Get standard deviation of each voxel or image.
|
|
1887
|
+
|
|
1888
|
+
Args:
|
|
1889
|
+
axis (int): 0 = across images (default, returns BrainData),
|
|
1890
|
+
1 = within images (returns array).
|
|
1891
|
+
|
|
1892
|
+
Returns:
|
|
1893
|
+
float | np.ndarray | BrainData: Standard deviation values.
|
|
1894
|
+
"""
|
|
1895
|
+
from .utils import _apply_func
|
|
1896
|
+
|
|
1897
|
+
return _apply_func(self, np.std, axis)
|
|
1898
|
+
|
|
1899
|
+
def sum(self, axis=0):
|
|
1900
|
+
"""Get sum of each voxel or image.
|
|
1901
|
+
|
|
1902
|
+
Args:
|
|
1903
|
+
axis (int): 0 = across images (default, returns BrainData),
|
|
1904
|
+
1 = within images (returns array).
|
|
1905
|
+
|
|
1906
|
+
Returns:
|
|
1907
|
+
float | np.ndarray | BrainData: Sum values.
|
|
1908
|
+
"""
|
|
1909
|
+
from .utils import _apply_func
|
|
1910
|
+
|
|
1911
|
+
return _apply_func(self, np.sum, axis)
|
|
1912
|
+
|
|
1913
|
+
def temporal_resample(self, *, sampling_freq=None, target=None, target_type="hz"):
|
|
1914
|
+
"""Resample BrainData timeseries to a new target frequency or number of samples.
|
|
1915
|
+
|
|
1916
|
+
Args:
|
|
1917
|
+
sampling_freq (float | None): Sampling frequency of the data in hertz.
|
|
1918
|
+
target (float | None): Resampling target, interpreted per ``target_type``.
|
|
1919
|
+
target_type (str): How to read ``target``: ``'hz'`` (default),
|
|
1920
|
+
``'samples'``, or ``'seconds'``.
|
|
1921
|
+
|
|
1922
|
+
Returns:
|
|
1923
|
+
BrainData: Resampled BrainData instance.
|
|
1924
|
+
"""
|
|
1925
|
+
from .analysis import _temporal_resample
|
|
1926
|
+
|
|
1927
|
+
return _temporal_resample(
|
|
1928
|
+
self, sampling_freq=sampling_freq, target=target, target_type=target_type
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
@_coalesced_gc()
|
|
1932
|
+
def threshold(
|
|
1933
|
+
self,
|
|
1934
|
+
*,
|
|
1935
|
+
upper=None,
|
|
1936
|
+
lower=None,
|
|
1937
|
+
binarize=False,
|
|
1938
|
+
coerce_nan=True,
|
|
1939
|
+
cluster_threshold=0,
|
|
1940
|
+
):
|
|
1941
|
+
"""Threshold BrainData instance with optional cluster filtering.
|
|
1942
|
+
|
|
1943
|
+
Args:
|
|
1944
|
+
upper (float | str | None): Upper cutoff for thresholding; a
|
|
1945
|
+
percentile string like ``'95%'`` is accepted.
|
|
1946
|
+
lower (float | str | None): Lower cutoff for thresholding; a
|
|
1947
|
+
percentile string is accepted.
|
|
1948
|
+
binarize (bool): Return a binarized image. Default False.
|
|
1949
|
+
coerce_nan (bool): Coerce NaN values to 0s. Default True.
|
|
1950
|
+
cluster_threshold (int): Minimum cluster size in voxels. Default 0.
|
|
1951
|
+
|
|
1952
|
+
Returns:
|
|
1953
|
+
BrainData: Thresholded BrainData object.
|
|
1954
|
+
"""
|
|
1955
|
+
from .analysis import _threshold_data
|
|
1956
|
+
|
|
1957
|
+
return _threshold_data(
|
|
1958
|
+
self,
|
|
1959
|
+
upper=upper,
|
|
1960
|
+
lower=lower,
|
|
1961
|
+
binarize=binarize,
|
|
1962
|
+
coerce_nan=coerce_nan,
|
|
1963
|
+
cluster_threshold=cluster_threshold,
|
|
1964
|
+
)
|
|
1965
|
+
|
|
1966
|
+
def to_nifti(self):
|
|
1967
|
+
"""Convert BrainData Instance into Nifti Object.
|
|
1968
|
+
|
|
1969
|
+
Returns:
|
|
1970
|
+
nibabel.Nifti1Image: Brain data as a NIfTI image.
|
|
1971
|
+
"""
|
|
1972
|
+
from .io import _to_nifti
|
|
1973
|
+
|
|
1974
|
+
return _to_nifti(self)
|
|
1975
|
+
|
|
1976
|
+
def cluster_report(
|
|
1977
|
+
self,
|
|
1978
|
+
*,
|
|
1979
|
+
stat_threshold: float | None = 3.0,
|
|
1980
|
+
cluster_threshold: int = 10,
|
|
1981
|
+
two_sided: bool = True,
|
|
1982
|
+
min_distance: float = 8.0,
|
|
1983
|
+
atlas: str | Sequence[str] | None = None,
|
|
1984
|
+
prob_threshold: float = 5.0,
|
|
1985
|
+
) -> "_ClusterReport":
|
|
1986
|
+
"""Generate a cluster report with anatomical labels.
|
|
1987
|
+
|
|
1988
|
+
Identifies surviving clusters in the stat map (after voxel + extent
|
|
1989
|
+
thresholding), reports peak coordinates and sub-peaks, and labels
|
|
1990
|
+
each peak/cluster against one or more atlases.
|
|
1991
|
+
|
|
1992
|
+
Args:
|
|
1993
|
+
stat_threshold: Voxel-level threshold (e.g. z- or t-cutoff).
|
|
1994
|
+
``None`` treats ``self`` as already thresholded.
|
|
1995
|
+
cluster_threshold: Minimum cluster size in voxels.
|
|
1996
|
+
two_sided: Report negative clusters separately.
|
|
1997
|
+
min_distance: Minimum mm between sub-peaks within a cluster.
|
|
1998
|
+
atlas: Atlas name or list of names (see `list_atlases`).
|
|
1999
|
+
Defaults to ``("harvard_oxford", "aal", "schaefer_200")``.
|
|
2000
|
+
prob_threshold: Drop probabilistic-atlas regions below this %.
|
|
2001
|
+
|
|
2002
|
+
Returns:
|
|
2003
|
+
_ClusterReport: Report with `peaks` and `clusters` (polars DataFrames)
|
|
2004
|
+
and `stat_img` (BrainData).
|
|
2005
|
+
"""
|
|
2006
|
+
from nltools.data.atlases import _ClusterReport, _cluster_report_data
|
|
2007
|
+
from nltools.data.atlases.registry import DEFAULT_ATLASES
|
|
2008
|
+
|
|
2009
|
+
peaks, clusters, thr = _cluster_report_data(
|
|
2010
|
+
self,
|
|
2011
|
+
stat_threshold=stat_threshold,
|
|
2012
|
+
cluster_threshold=cluster_threshold,
|
|
2013
|
+
two_sided=two_sided,
|
|
2014
|
+
min_distance=min_distance,
|
|
2015
|
+
atlas=DEFAULT_ATLASES if atlas is None else atlas,
|
|
2016
|
+
prob_threshold=prob_threshold,
|
|
2017
|
+
)
|
|
2018
|
+
return _ClusterReport(peaks=peaks, clusters=clusters, stat_img=thr)
|
|
2019
|
+
|
|
2020
|
+
def transform_pairwise(self):
|
|
2021
|
+
"""Transform data into pairwise comparisons.
|
|
2022
|
+
|
|
2023
|
+
Returns:
|
|
2024
|
+
BrainData: BrainData instance transformed into pairwise comparisons
|
|
2025
|
+
"""
|
|
2026
|
+
from .analysis import _transform_pairwise_data
|
|
2027
|
+
|
|
2028
|
+
return _transform_pairwise_data(self)
|
|
2029
|
+
|
|
2030
|
+
def ttest(
|
|
2031
|
+
self,
|
|
2032
|
+
*,
|
|
2033
|
+
popmean=0.0,
|
|
2034
|
+
permutation=False,
|
|
2035
|
+
n_permute=5000,
|
|
2036
|
+
tail=2,
|
|
2037
|
+
return_null=False,
|
|
2038
|
+
n_jobs=-1,
|
|
2039
|
+
random_state=None,
|
|
2040
|
+
progress_bar: bool = False,
|
|
2041
|
+
):
|
|
2042
|
+
"""Run a one-sample voxelwise t-test across images (axis 0).
|
|
2043
|
+
|
|
2044
|
+
Tests whether the per-voxel mean across a stack of images (e.g.
|
|
2045
|
+
subject-level contrast maps, shape `(n_images, n_voxels)`) differs from
|
|
2046
|
+
`popmean`.
|
|
2047
|
+
|
|
2048
|
+
Args:
|
|
2049
|
+
popmean (float): Population mean to test against. Default 0.0.
|
|
2050
|
+
permutation (bool): If True, take p from a sign-flip permutation
|
|
2051
|
+
test on `images - popmean`. The reported `t` stays the observed
|
|
2052
|
+
parametric statistic. Default False.
|
|
2053
|
+
n_permute (int): Number of permutations, used only when
|
|
2054
|
+
`permutation=True`. Default 5000.
|
|
2055
|
+
tail (int | str): `2` or `'two'` for two-tailed (default); `1` or
|
|
2056
|
+
`'one'` for one-tailed (mean > `popmean`).
|
|
2057
|
+
return_null (bool): If True, also return the permutation null. Has
|
|
2058
|
+
no effect on the parametric path, which computes no null.
|
|
2059
|
+
Default False.
|
|
2060
|
+
n_jobs (int): Number of parallel jobs. Default -1 (all cores).
|
|
2061
|
+
random_state (int | None): Random seed for reproducibility.
|
|
2062
|
+
progress_bar (bool): If True, show a progress bar. Default False.
|
|
2063
|
+
|
|
2064
|
+
Returns:
|
|
2065
|
+
dict: `"mean"`, `"t"`, `"z"` and `"p"` as independent `BrainData`
|
|
2066
|
+
images with observation metadata cleared. `"mean"` is the
|
|
2067
|
+
voxelwise mean minus `popmean` — the effect relative to the
|
|
2068
|
+
tested null, equal to the raw mean only when `popmean=0`.
|
|
2069
|
+
`"t"` is the observed one-sample t-statistic on both paths.
|
|
2070
|
+
`"p"` is parametric, or the empirical sign-flip p-value when
|
|
2071
|
+
`permutation=True`. `"z"` is the tail-aware normal score of `p`
|
|
2072
|
+
(`sign(t) * norm.isf(p/2)` two-tailed), matching nilearn's
|
|
2073
|
+
`output_type='z_score'`. With `permutation=True` and
|
|
2074
|
+
`return_null=True` the dict also holds `"null_dist"`, an owned
|
|
2075
|
+
`(n_permute, n_voxels)` array of centered means in the units of
|
|
2076
|
+
`"mean"`. Maps are unthresholded. Apply a cutoff or a
|
|
2077
|
+
multiple-comparison correction afterwards.
|
|
2078
|
+
|
|
2079
|
+
Raises:
|
|
2080
|
+
ValueError: If this BrainData contains fewer than 2 images.
|
|
2081
|
+
|
|
2082
|
+
Examples:
|
|
2083
|
+
```python
|
|
2084
|
+
# Stack of subject-level contrast maps
|
|
2085
|
+
result = contrast_maps.ttest()
|
|
2086
|
+
effect = result["mean"] # magnitude, for reporting
|
|
2087
|
+
z_map = result["z"] # for nilearn-style thresholding
|
|
2088
|
+
|
|
2089
|
+
# Threshold after testing, never inside it
|
|
2090
|
+
from nltools.algorithms import threshold
|
|
2091
|
+
|
|
2092
|
+
z_thresh = threshold(result["z"], result["p"], thr=0.001)
|
|
2093
|
+
|
|
2094
|
+
# Permutation p-values, keeping the null for a custom correction
|
|
2095
|
+
perm = contrast_maps.ttest(
|
|
2096
|
+
permutation=True, n_permute=5000, return_null=True, random_state=0
|
|
2097
|
+
)
|
|
2098
|
+
perm["null_dist"].shape # → (5000, n_voxels)
|
|
2099
|
+
```
|
|
2100
|
+
"""
|
|
2101
|
+
from .modeling import _ttest
|
|
2102
|
+
|
|
2103
|
+
return _ttest(
|
|
2104
|
+
self,
|
|
2105
|
+
popmean=popmean,
|
|
2106
|
+
permutation=permutation,
|
|
2107
|
+
n_permute=n_permute,
|
|
2108
|
+
tail=tail,
|
|
2109
|
+
return_null=return_null,
|
|
2110
|
+
n_jobs=n_jobs,
|
|
2111
|
+
random_state=random_state,
|
|
2112
|
+
progress_bar=progress_bar,
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
def upload_neurovault( # nosemgrep: kwargs-internal-forwarding # forwards to the NeuroVault API via io.upload_neurovault
|
|
2116
|
+
self,
|
|
2117
|
+
*,
|
|
2118
|
+
access_token=None,
|
|
2119
|
+
collection_name=None,
|
|
2120
|
+
collection_id=None,
|
|
2121
|
+
img_type=None,
|
|
2122
|
+
img_modality=None,
|
|
2123
|
+
**kwargs,
|
|
2124
|
+
):
|
|
2125
|
+
"""Upload BrainData images and metadata to NeuroVault.
|
|
2126
|
+
|
|
2127
|
+
Adds any columns in ``self.X`` to image metadata. The index is used as
|
|
2128
|
+
the image name.
|
|
2129
|
+
|
|
2130
|
+
Args:
|
|
2131
|
+
access_token (str): NeuroVault API access token. Required.
|
|
2132
|
+
collection_name (str | None): Name of a new collection to create.
|
|
2133
|
+
collection_id (int | None): NeuroVault ``collection_id`` when adding
|
|
2134
|
+
images to an existing collection.
|
|
2135
|
+
img_type (str): NeuroVault ``map_type``. Required.
|
|
2136
|
+
img_modality (str): NeuroVault image modality. Required.
|
|
2137
|
+
**kwargs (dict): Additional image metadata forwarded to the NeuroVault
|
|
2138
|
+
API.
|
|
2139
|
+
|
|
2140
|
+
Returns:
|
|
2141
|
+
dict: NeuroVault collection information.
|
|
2142
|
+
"""
|
|
2143
|
+
from .io import _upload_neurovault
|
|
2144
|
+
|
|
2145
|
+
return _upload_neurovault(
|
|
2146
|
+
self,
|
|
2147
|
+
access_token=access_token,
|
|
2148
|
+
collection_name=collection_name,
|
|
2149
|
+
collection_id=collection_id,
|
|
2150
|
+
img_type=img_type,
|
|
2151
|
+
img_modality=img_modality,
|
|
2152
|
+
**kwargs,
|
|
2153
|
+
)
|
|
2154
|
+
|
|
2155
|
+
def write(self, file_name):
|
|
2156
|
+
"""Write out BrainData object to Nifti or HDF5 File.
|
|
2157
|
+
|
|
2158
|
+
Args:
|
|
2159
|
+
file_name (str or Path): Output file path (.nii/.nii.gz for NIfTI,
|
|
2160
|
+
.h5/.hdf5 for HDF5).
|
|
2161
|
+
"""
|
|
2162
|
+
from .io import _write_brain_data
|
|
2163
|
+
|
|
2164
|
+
_write_brain_data(self, file_name)
|
|
2165
|
+
|
|
2166
|
+
def z_to_r(self):
|
|
2167
|
+
"""Convert z score back into r value for each element of data object."""
|
|
2168
|
+
from .analysis import _z_to_r
|
|
2169
|
+
|
|
2170
|
+
return _z_to_r(self)
|