Spatial nulls for significance testing
In the last section we showed how to resample and
statistically compare two brain annotations. This process gave us a correlation
estimate (or other image similarity metric) but no means by which to test the
significance of the association between the tested maps. Enter: the
neuromaps.nulls
module.
This module provides access to a variety of null models that can be used to generate “null” brain maps that retain aspects of the spatial autocorrelation of the original brain maps.
For a review of these models, please refer to Markello & Misic, 2021, NeuroImage. We also recommend watching this recorded session from the OHBM 2024 Educational Course if you are new to this topic.
There are four available null models that can be used with voxel- and
vertex-wise data and eight null models that can be used with parcellated data.
Refer to the neuromaps.nulls
API for the complete list of null models.
Nulls with surface-based data
Nulls with non-parcellated data
All of the null model functions in neuromaps
have (more-or-less) identical
interfaces. They accept (1) a data array or tuple-of-images, (2) the coordinate
system + density of the provided data, (3) the number of desired nulls
(i.e., permutations), and (4) a random seed for reproducibility. The functions
will yield a two-dimensional array of shape (vertices, nulls):
>>> from neuromaps import datasets, images, nulls, resampling
>>> neurosynth = datasets.fetch_annotation(source='neurosynth')
>>> abagen = datasets.fetch_annotation(source='abagen')
>>> neurosynth, abagen = resampling.resample_images(neurosynth, abagen, 'MNI152', 'fsaverage')
>>> rotated = nulls.alexander_bloch(neurosynth, atlas='fsaverage', density='10k',
... n_perm=100, seed=1234)
>>> print(rotated.shape)
(20484, 100)
Once we’ve generated the null maps we can pass them directly to the optional
nulls
argument of neuromaps.stats.compare_images()
>>> from neuromaps import stats
>>> corr, pval = stats.compare_images(neurosynth, abagen, nulls=rotated)
>>> print(f'r = {corr:.3f}, p = {pval:.3f}')
r = 0.339, p = 0.178
Important
The null array provided to the nulls
argument must be for the data
passed as the first positional argument of the function! In the above
example, the rotated
array corresponds to null maps for neurosynth
.
If we called the function like stats.compare_images(abagen, neurosynth)
then we would have had to generate our rotated
array for the abagen
data instead. The function has no way of checking this so you must be very
careful when providing null arrays!
Now, our call to compare_images()
returns both a correlation and a p-value.
Note that the p-values are bounded based on the requested number of
permutations. That is, if you provide an array to nulls
the smallest
p-value that can be returned is (1 / (1 + nulls.shape[1]))
.
Nulls with parcellated data
The null model functions in neuromaps
can also handle parcellated data, and
do so by accepting an additional optional keyword parameter: parcellation
.
If provided, the null functions assume this is a tuple-of-images (left, right
hemisphere, as usual) that is in the same space as the provided data
.
Generally you will already have pre-parcellated data, but for the purposes of
demonstration we we will fetch a surface parcellation (for the 10k fsaverage
system) using nilearn
:
>>> from nilearn.datasets import fetch_atlas_surf_destrieux
>>> destrieux = fetch_atlas_surf_destrieux()
>>> print(sorted(destrieux))
['description', 'labels', 'map_left', 'map_right']
>>> print(len(destrieux['map_left']), len(destrieux['map_right']))
10242 10242
>>> print(len(destrieux['labels']))
76
Unfortunately the Destrieux atlas provided here is designed such that left
and right hemispheres have identical label values. The functions in
neuromaps
that handle parcellated data always assume that the labels IDs
are ascending across hemispheres. Moreover, the map_left
and map_right
values are simple numpy arrays and neuromaps
prefers to work with GIFTI
images. We can convert the arrays to GIFTI label images and then relabel them
so that the labels are consecutive across hemispheres:
>>> labels = [label.decode() for label in destrieux['labels']]
>>> parc_left = images.construct_shape_gii(destrieux['map_left'], labels=labels,
... intent='NIFTI_INTENT_LABEL')
>>> parc_right = images.construct_shape_gii(destrieux['map_right'], labels=labels,
... intent='NIFTI_INTENT_LABEL')
>>> parcellation = images.relabel_gifti((parc_left, parc_right), background=['Medial_wall'])
>>> print(parcellation)
(<nibabel.gifti.gifti.GiftiImage object at ...>, <nibabel.gifti.gifti.GiftiImage object at ...>)
Note that we set background=['Medial_wall']
in our call to
relabel_gifti()
. This is because, by default, the medial wall
has a label of 42 and we want it to be set to 0. (The neuromaps
functions
assume that the 0 label is the background, and it is omitted from most
calculations.)
We can use these images to parcellate our data with an instance of the
neuromaps.parcellate.Parcellater
class:
>>> from neuromaps import parcellate
>>> destrieux = parcellate.Parcellater(parcellation, 'fsaverage').fit()
>>> neurosynth_parc = destrieux.transform(neurosynth, 'fsaverage')
>>> abagen_parc = destrieux.transform(abagen, 'fsaverage')
>>> print(neurosynth_parc.shape, abagen_parc.shape)
(148,) (148,)
Now that we’ve got our parcellated arrays we can generate our null maps. We
use the same call as above but provide the
additional parcellation
parameter:
>>> rotated = nulls.alexander_bloch(neurosynth_parc, atlas='fsaverage', density='10k',
... n_perm=100, seed=1234, parcellation=parcellation)
>>> print(rotated.shape)
(148, 100)
We can pass the generated array to the nulls
argument of
compare_images()
as before:
>>> corr, pval = stats.compare_images(neurosynth_parc, abagen_parc, nulls=rotated)
>>> print(f'r = {corr:.3f}, p = {pval:.3f}')
r = 0.416, p = 0.376
The correlation has changed (because we parcellated the data!), but remains non-significant.
Nulls for volumetric data
Warning
Nulls for high-resolution volumetric data (especially at 1mm or 2mm resolution) can be extremely demanding (days & hundreds of GBs). This is an inherent limitation of the original model that currently has no immediate workaround!
The majority of spatial nulls work best with data represented in one of the surface-based coordinate systems. If you are working with data that are represented in the MNI152 system you must use one of the following three null models:
Whereas the other available null models assume that the provided data are represented on a cortical surface, these models are more flexible. However, they all depend on calculating and storing a distance matrix of the provided images in memory, and as such will be very computationally intensive for volumetric images.
You would call the functions in the same manner as above:
>>> neurosynth_mni152 = datasets.fetch_annotation(source='neurosynth')
>>> nulls = nulls.burt2020(neurosynth_mni152, atlas='MNI152', density='2mm',
... n_perm=100, seed=1234)
>>> print(nulls.shape)
(224705, 100)
When working with volumetric data, please note some important computational considerations. While the function supports both voxelwise and parcellated analyses, processing high-resolution volumetric data (especially at 1mm or 2mm resolution) can be extremely demanding. The calculations for voxelwise data can take several days to complete even on high-performance computing nodes, and may require hundreds of GBs of temporary storage space. This is an inherent limitation of the original model that currently has no immediate workaround (see BrainSMASH). We welcome any suggestions for improving this method’s computational efficiency and performance.
To make your analysis more tractable, we recommend you consider using parcellated data instead of voxelwise analysis. Parcellation dramatically reduces both computation time and storage requirements.
For voxelwise input, if possible it is recommended that you mask your data
(i.e., with a gray matter mask) before generating nulls using this procedure. To use
parcellation images for volumetric data, simply pass the volumetric parcellation image
to the parcellation
keyword argument and the function will take care of the rest.