Basic Data Operations¶
Run this tutorial
This page is rendered from the marimo notebook docs/tutorials/data-operations/01_download.py. Click the badge to run it in the cloud (free, no install), or locally: download 01_download.py and run uvx marimo edit --sandbox 01_download.py. The outputs below were produced when this page was built.
Most of nltools is built around BrainData. It holds imaging data as an
images-by-voxels matrix: one row per image, one column per in-mask voxel. That
shape is what makes the class feel like a dataframe — you index it, slice it, do
arithmetic on it, and iterate over it with plain Python.
Download a dataset¶
fetch_pain() retrieves the pain dataset from
Chang et al., 2015:
28 subjects with three beta images each, at low, medium and high thermal pain.
The files are cached locally on first use and loaded straight into a BrainData.
nltools.data.braindata.BrainData(data=(84, 238955), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)
The image metadata comes with it, on .X, as a polars DataFrame with one row
per image:
| filename | SubjectID | PainLevel | PainIntensity | Age | Sex | neurovault_id | name |
|---|---|---|---|---|---|---|---|
| str | i64 | i64 | str | i64 | str | i64 | str |
| "sub-01_pain-low.nii.gz" | 1 | 1 | "low" | 29 | "Female" | 7540 | "Pain Subject 1 Low" |
| "sub-01_pain-medium.nii.gz" | 1 | 2 | "medium" | 29 | "Female" | 7541 | "Pain Subject 1 Medium" |
| "sub-01_pain-high.nii.gz" | 1 | 3 | "high" | 29 | "Female" | 7539 | "Pain Subject 1 High" |
| "sub-02_pain-low.nii.gz" | 2 | 1 | "low" | 25 | "Male" | 7570 | "Pain Subject 2 Low" |
| "sub-02_pain-medium.nii.gz" | 2 | 2 | "medium" | 25 | "Male" | 7571 | "Pain Subject 2 Medium" |
Load your own files¶
A NIfTI file loads by path, and many files load together as a list. The grid
comes from the file's own affine: nltools matches it to the closest bundled
MNI template, and resamples only when the match is not exact. The active
brain space is the default for data that has no grid of its own, not an
override for data that does. Pass mask= to pin a specific grid — see
Brain Space and Resolution.
from nltools.data import BrainData
one = BrainData("sub-01_pain-high.nii.gz")
many = BrainData(["sub-01_pain-high.nii.gz", "sub-02_pain-high.nii.gz"])
remote = BrainData("https://neurovault.org/media/images/2099/some_map.nii.gz")
Basic operations¶
len() is the number of images:
84
.shape is images by voxels:
(84, 238955)
Index with integers, lists of integers, slices, or boolean arrays:
nltools.data.braindata.BrainData(data=(3, 238955), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)
Reduce across images to get one value per voxel:
nltools.data.braindata.BrainData(data=(238955,), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)
nltools.data.braindata.BrainData(data=(238955,), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)
Methods chain, so the mean of 84 images is a single image:
(238955,)
Two BrainData objects add and subtract voxelwise:
nltools.data.braindata.BrainData(data=(238955,), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)
Scalars broadcast over every voxel — here, add 10 and scale by 2:
nltools.data.braindata.BrainData(data=(84, 238955), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)
.copy() gives you an independent object:
nltools.data.braindata.BrainData(data=(84, 238955), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)
.to_nifti() converts back to a nibabel image — a 3D volume for one image, 4D
for a stack — which is how you hand data to
nilearn or any other toolbox:
(91, 109, 91, 84)
.append() concatenates along the image axis:
nltools.data.braindata.BrainData(data=(3, 238955), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)
.write() saves to NIfTI or, with an .h5 extension, to HDF5 — which is
smaller and keeps .X, .Y and the mask:
import os
import tempfile
with tempfile.TemporaryDirectory() as _tmpdir:
_path = os.path.join(_tmpdir, "pain_subset.nii.gz")
data[:3].write(_path)
print(
f"{os.path.getsize(_path) / 1e6:.1f} MB written to {os.path.basename(_path)}"
)
2.7 MB written to pain_subset.nii.gz
Images are iterable, so a comprehension gives you one value per image:
[np.float32(-0.32340634), np.float32(-0.21444643), np.float32(-0.111590356), np.float32(-0.099880464), np.float32(-0.06570267)]
Plotting¶
Convert to nibabel and any nilearn plot works:
.plot() is the built-in shortcut, and draws a glass brain by default:
Calling it on a stack draws one figure per image. limit caps how many, and
defaults to 3 so that an 84-image object does not silently produce 84 figures;
raise it, or index first, when you want more:
[<Figure size 950x350 with 6 Axes>, <Figure size 950x350 with 6 Axes>, <Figure size 950x350 with 6 Axes>, <Figure size 950x350 with 6 Axes>]