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.
Files changed (95) hide show
  1. nltools/__init__.py +55 -0
  2. nltools/algorithms/__init__.py +90 -0
  3. nltools/algorithms/alignment/__init__.py +21 -0
  4. nltools/algorithms/alignment/procrustes.py +565 -0
  5. nltools/algorithms/alignment/srm.py +758 -0
  6. nltools/algorithms/backends.py +1059 -0
  7. nltools/algorithms/corrections.py +177 -0
  8. nltools/algorithms/decoding.py +327 -0
  9. nltools/algorithms/inference/__init__.py +50 -0
  10. nltools/algorithms/inference/bootstrap.py +1386 -0
  11. nltools/algorithms/inference/correlation.py +373 -0
  12. nltools/algorithms/inference/intersubject.py +422 -0
  13. nltools/algorithms/inference/isc.py +1554 -0
  14. nltools/algorithms/inference/matrix.py +602 -0
  15. nltools/algorithms/inference/one_sample.py +288 -0
  16. nltools/algorithms/inference/random.py +122 -0
  17. nltools/algorithms/inference/timeseries.py +347 -0
  18. nltools/algorithms/inference/two_sample.py +212 -0
  19. nltools/algorithms/inference/utils.py +58 -0
  20. nltools/algorithms/inference/validation.py +282 -0
  21. nltools/algorithms/neighborhoods.py +207 -0
  22. nltools/algorithms/outliers.py +308 -0
  23. nltools/algorithms/regression.py +83 -0
  24. nltools/algorithms/signal.py +303 -0
  25. nltools/algorithms/similarity.py +234 -0
  26. nltools/algorithms/validation.py +151 -0
  27. nltools/cross_validation.py +72 -0
  28. nltools/data/__init__.py +30 -0
  29. nltools/data/adjacency/__init__.py +875 -0
  30. nltools/data/adjacency/io.py +111 -0
  31. nltools/data/adjacency/modeling.py +569 -0
  32. nltools/data/adjacency/plotting.py +174 -0
  33. nltools/data/adjacency/state.py +349 -0
  34. nltools/data/adjacency/stats.py +596 -0
  35. nltools/data/adjacency/utils.py +79 -0
  36. nltools/data/atlases/__init__.py +23 -0
  37. nltools/data/atlases/labeling.py +158 -0
  38. nltools/data/atlases/loading.py +76 -0
  39. nltools/data/atlases/registry.py +96 -0
  40. nltools/data/atlases/reporting.py +456 -0
  41. nltools/data/braindata/__init__.py +2170 -0
  42. nltools/data/braindata/analysis.py +1381 -0
  43. nltools/data/braindata/bootstrap.py +398 -0
  44. nltools/data/braindata/io.py +896 -0
  45. nltools/data/braindata/modeling.py +594 -0
  46. nltools/data/braindata/plotting.py +501 -0
  47. nltools/data/braindata/prediction.py +1250 -0
  48. nltools/data/braindata/utils.py +348 -0
  49. nltools/data/braindata/validation.py +197 -0
  50. nltools/data/braindata/viewer.js +266 -0
  51. nltools/data/braindata/viewer.py +770 -0
  52. nltools/data/combine.py +27 -0
  53. nltools/data/designmatrix/__init__.py +1032 -0
  54. nltools/data/designmatrix/append.py +518 -0
  55. nltools/data/designmatrix/diagnostics.py +248 -0
  56. nltools/data/designmatrix/io.py +356 -0
  57. nltools/data/designmatrix/plotting.py +291 -0
  58. nltools/data/designmatrix/regressors.py +463 -0
  59. nltools/data/designmatrix/transforms.py +200 -0
  60. nltools/data/designmatrix/utils.py +350 -0
  61. nltools/data/ownership.py +129 -0
  62. nltools/data/results.py +291 -0
  63. nltools/data/roc/__init__.py +398 -0
  64. nltools/data/simulator/__init__.py +927 -0
  65. nltools/data/simulator/haxby.py +124 -0
  66. nltools/data/validation.py +83 -0
  67. nltools/datasets.py +218 -0
  68. nltools/io/__init__.py +10 -0
  69. nltools/io/events.py +67 -0
  70. nltools/io/h5.py +246 -0
  71. nltools/mask.py +403 -0
  72. nltools/models/__init__.py +11 -0
  73. nltools/models/glm.py +543 -0
  74. nltools/models/results.py +49 -0
  75. nltools/models/ridge.py +1303 -0
  76. nltools/models/validation.py +26 -0
  77. nltools/plotting/__init__.py +32 -0
  78. nltools/plotting/adjacency.py +421 -0
  79. nltools/plotting/brain.py +669 -0
  80. nltools/plotting/decomposition.py +111 -0
  81. nltools/plotting/prediction.py +110 -0
  82. nltools/resources/covariates_example.csv +161 -0
  83. nltools/resources/onsets_example.csv +40 -0
  84. nltools/templates/__init__.py +51 -0
  85. nltools/templates/config.py +144 -0
  86. nltools/templates/fetch.py +260 -0
  87. nltools/templates/matching.py +183 -0
  88. nltools/templates/paths.py +106 -0
  89. nltools/templates/registry.py +25 -0
  90. nltools/utils.py +230 -0
  91. nltools/version.py +13 -0
  92. nltools-0.6.0.dev0.dist-info/METADATA +95 -0
  93. nltools-0.6.0.dev0.dist-info/RECORD +95 -0
  94. nltools-0.6.0.dev0.dist-info/WHEEL +4 -0
  95. 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)