brainstat 0.5.2__tar.gz → 0.6.0__tar.gz
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.
- {brainstat-0.5.2/brainstat.egg-info → brainstat-0.6.0}/PKG-INFO +3 -3
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/__init__.py +1 -1
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/histology.py +23 -6
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/datasets/base.py +49 -2
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/SLM.py +62 -11
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/_multiple_comparisons.py +0 -1
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/_t_test.py +4 -3
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_SLM.py +50 -0
- {brainstat-0.5.2 → brainstat-0.6.0/brainstat.egg-info}/PKG-INFO +3 -3
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat.egg-info/requires.txt +2 -2
- {brainstat-0.5.2 → brainstat-0.6.0}/setup.py +5 -2
- {brainstat-0.5.2 → brainstat-0.6.0}/LICENSE +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/MANIFEST.in +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/README.rst +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/_typing.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/_utils.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/genetics.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/meta_analysis.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/resting.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/data_urls.json +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/logging.conf +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_civet164k.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_civet41k.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fsaverage.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fsaverage5.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fslr32k.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/datasets/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/mesh/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/mesh/data.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/mesh/interpolate.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/mesh/utils.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/_linear_model.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/terms.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/utils.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/conftest.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_f.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_fdr.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_linear_model.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_mesh_edges.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_mesh_smooth.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_peak_clus.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_random_field_theory.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_resels.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_slm.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_stat_threshold.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_t_test.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_datasets.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_f_test.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_fdr.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_genetics.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_histology.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_linear_model.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_mesh_edges.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_mesh_smooth.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_peak_clus.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_random_field_theory.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_resels.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_stat_threshold.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_t_test.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_terms.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/testutil.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tutorial/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tutorial/utils.py +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat.egg-info/SOURCES.txt +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat.egg-info/dependency_links.txt +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/brainstat.egg-info/top_level.txt +0 -0
- {brainstat-0.5.2 → brainstat-0.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brainstat
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A toolbox for statistical analysis of neuroimaging data
|
|
5
5
|
Home-page: https://github.com/MICA-LAB/BrainStat
|
|
6
6
|
Author: MNI-MICA Lab and MPI-CNG Lab
|
|
@@ -26,8 +26,8 @@ Requires-Dist: neurosynth
|
|
|
26
26
|
Requires-Dist: nibabel
|
|
27
27
|
Requires-Dist: nilearn>=0.7.0
|
|
28
28
|
Requires-Dist: nimare
|
|
29
|
-
Requires-Dist: numpy
|
|
30
|
-
Requires-Dist: pandas
|
|
29
|
+
Requires-Dist: numpy<2,>=1.21
|
|
30
|
+
Requires-Dist: pandas<2.1,>=1.3
|
|
31
31
|
Requires-Dist: scikit_learn>=1.0
|
|
32
32
|
Requires-Dist: scipy>=1.7
|
|
33
33
|
Requires-Dist: templateflow
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Neuroimaging statistics toolbox."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.6.0"
|
|
@@ -114,6 +114,7 @@ def read_histology_profile(
|
|
|
114
114
|
data_dir: Optional[Union[str, Path]] = None,
|
|
115
115
|
template: str = "fsaverage",
|
|
116
116
|
overwrite: bool = False,
|
|
117
|
+
invert: bool = True,
|
|
117
118
|
) -> np.ndarray:
|
|
118
119
|
"""Reads BigBrain histology profiles.
|
|
119
120
|
|
|
@@ -127,6 +128,15 @@ def read_histology_profile(
|
|
|
127
128
|
default 'fsaverage'.
|
|
128
129
|
overwrite : bool, optional
|
|
129
130
|
If true, existing data will be overwrriten, by default False.
|
|
131
|
+
invert : bool, optional
|
|
132
|
+
If True (default), invert the 8-bit intensity values so that higher
|
|
133
|
+
numbers correspond to darker, more cell-dense regions, matching the
|
|
134
|
+
convention used in Paquola et al. 2021 (eLife) and most downstream
|
|
135
|
+
BigBrain analyses. The raw BigBrainWarp data uses the opposite
|
|
136
|
+
convention (0 = black). Set to False to return the raw values. MPC
|
|
137
|
+
and gradient computations (correlation-based) are invariant to this
|
|
138
|
+
flip; only direct inspection of profile intensities is affected.
|
|
139
|
+
See https://github.com/MICA-MNI/BrainStat/issues/274 for context.
|
|
130
140
|
|
|
131
141
|
Returns
|
|
132
142
|
-------
|
|
@@ -169,12 +179,19 @@ def read_histology_profile(
|
|
|
169
179
|
profiles = h5_file.get("fs_LR_64k")[...]
|
|
170
180
|
else:
|
|
171
181
|
profiles = h5_file.get(template)[...]
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
|
|
183
|
+
if invert:
|
|
184
|
+
# Map from BigBrainWarp convention (low = dark/dense) to the
|
|
185
|
+
# literature convention (high = dark/dense). The data is unsigned
|
|
186
|
+
# 8-bit, so the inversion is `255 - x`.
|
|
187
|
+
max_value = np.iinfo(profiles.dtype).max if np.issubdtype(profiles.dtype, np.integer) else 255
|
|
188
|
+
profiles = max_value - profiles
|
|
189
|
+
|
|
190
|
+
if civet_template:
|
|
191
|
+
fsaverage_surface = fetch_template_surface("fsaverage")
|
|
192
|
+
civet_surface = fetch_template_surface(civet_template)
|
|
193
|
+
return _surf2surf(fsaverage_surface, civet_surface, profiles.T).T
|
|
194
|
+
return profiles
|
|
178
195
|
|
|
179
196
|
|
|
180
197
|
def download_histology_profiles(
|
|
@@ -111,12 +111,15 @@ def fetch_parcellation(
|
|
|
111
111
|
The surface template. Valid values are "fsaverage", "fsaverage5",
|
|
112
112
|
"fsaverage6", "fslr32k", "civet41k", "civet164k", by default "fsaverage5".
|
|
113
113
|
atlas : str
|
|
114
|
-
Name of the atlas. Valid names are "cammoun", "
|
|
114
|
+
Name of the atlas. Valid names are "cammoun", "destrieux", "glasser",
|
|
115
|
+
"schaefer", "yeo".
|
|
115
116
|
n_regions : int
|
|
116
117
|
Number of regions of the requested atlas. Valid values for the cammoun
|
|
117
118
|
atlas are 33, 60, 125, 250, 500. Valid values for the glasser atlas are
|
|
118
119
|
360. Valid values for the "schaefer" atlas are 100, 200, 300, 400, 500,
|
|
119
|
-
600, 800, 1000. Valid values for "yeo" are 7 and 17.
|
|
120
|
+
600, 800, 1000. Valid values for "yeo" are 7 and 17. The
|
|
121
|
+
"destrieux" atlas (a2009s, FreeSurfer's 75 cortical labels per
|
|
122
|
+
hemisphere) ignores ``n_regions``.
|
|
120
123
|
join : bool, optional
|
|
121
124
|
If true, returns parcellation as a single array, if false, returns an
|
|
122
125
|
array per hemisphere, by default True.
|
|
@@ -154,6 +157,8 @@ def fetch_parcellation(
|
|
|
154
157
|
parcellations = _fetch_glasser_parcellation(template, data_dir)
|
|
155
158
|
elif atlas == "yeo":
|
|
156
159
|
parcellations = _fetch_yeo_parcellation(template, n_regions, data_dir)
|
|
160
|
+
elif atlas == "destrieux":
|
|
161
|
+
parcellations = _fetch_destrieux_parcellation(template, data_dir)
|
|
157
162
|
else:
|
|
158
163
|
raise ValueError(f"Invalid atlas: {atlas}")
|
|
159
164
|
|
|
@@ -537,6 +542,48 @@ def _fetch_glasser_parcellation(template: str, data_dir: Path) -> List[np.ndarra
|
|
|
537
542
|
return parcellations
|
|
538
543
|
|
|
539
544
|
|
|
545
|
+
def _fetch_destrieux_parcellation(
|
|
546
|
+
template: str, data_dir: Path
|
|
547
|
+
) -> List[np.ndarray]:
|
|
548
|
+
"""Fetches the Destrieux 2009 (FreeSurfer ``aparc.a2009s``) parcellation.
|
|
549
|
+
|
|
550
|
+
Uses :func:`nilearn.datasets.fetch_atlas_surf_destrieux`, which provides
|
|
551
|
+
the parcellation on the ``fsaverage5`` surface. For other ``fsaverage*``
|
|
552
|
+
templates we resample with nearest-neighbour interpolation; ``fslr32k``
|
|
553
|
+
is not supported because the destrieux atlas is FreeSurfer-specific.
|
|
554
|
+
"""
|
|
555
|
+
from nilearn.datasets import fetch_atlas_surf_destrieux
|
|
556
|
+
|
|
557
|
+
if template == "fslr32k":
|
|
558
|
+
raise ValueError(
|
|
559
|
+
"The destrieux atlas is FreeSurfer-specific; fslr32k is not "
|
|
560
|
+
"supported. Use a fsaverage* template instead."
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
bunch = fetch_atlas_surf_destrieux(data_dir=str(data_dir))
|
|
564
|
+
# nilearn returns int32 arrays for fsaverage5 (10242 vertices/hemi).
|
|
565
|
+
parcellations = [np.asarray(bunch["map_left"]), np.asarray(bunch["map_right"])]
|
|
566
|
+
|
|
567
|
+
if template != "fsaverage5":
|
|
568
|
+
# Nearest-neighbour resample fsaverage5 -> requested fsaverage*.
|
|
569
|
+
from brainstat.mesh.interpolate import _surf2surf
|
|
570
|
+
|
|
571
|
+
src_left, src_right = fetch_template_surface(
|
|
572
|
+
"fsaverage5", layer="white", join=False
|
|
573
|
+
)
|
|
574
|
+
dst_left, dst_right = fetch_template_surface(
|
|
575
|
+
template, layer="white", join=False
|
|
576
|
+
)
|
|
577
|
+
parcellations[0] = _surf2surf(
|
|
578
|
+
src_left, dst_left, parcellations[0], interpolation="nearest"
|
|
579
|
+
).astype(parcellations[0].dtype)
|
|
580
|
+
parcellations[1] = _surf2surf(
|
|
581
|
+
src_right, dst_right, parcellations[1], interpolation="nearest"
|
|
582
|
+
).astype(parcellations[1].dtype)
|
|
583
|
+
|
|
584
|
+
return parcellations
|
|
585
|
+
|
|
586
|
+
|
|
540
587
|
def _fetch_yeo_parcellation(
|
|
541
588
|
template: str, n_regions: int, data_dir: Path
|
|
542
589
|
) -> List[np.ndarray]:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
# type: ignore
|
|
2
1
|
""" Standard Linear regression models. """
|
|
3
2
|
import warnings
|
|
4
|
-
from
|
|
3
|
+
from math import sqrt
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from pprint import pformat
|
|
7
6
|
from typing import Optional, Sequence, Tuple, Union
|
|
@@ -17,8 +16,11 @@ from nibabel.nifti1 import Nifti1Image
|
|
|
17
16
|
|
|
18
17
|
from brainstat._typing import ArrayLike
|
|
19
18
|
from brainstat.datasets import fetch_parcellation, fetch_template_surface
|
|
20
|
-
from brainstat.datasets.base import
|
|
19
|
+
from brainstat.datasets.base import fetch_yeo_networks_metadata
|
|
21
20
|
from brainstat.mesh.utils import _mask_edges, mesh_edges
|
|
21
|
+
from brainstat.stats._linear_model import _linear_model
|
|
22
|
+
from brainstat.stats._multiple_comparisons import _fdr, _random_field_theory
|
|
23
|
+
from brainstat.stats._t_test import _t_test
|
|
22
24
|
from brainstat.stats.terms import FixedEffect, MixedEffect
|
|
23
25
|
from brainstat.stats.utils import apply_mask, undo_mask
|
|
24
26
|
|
|
@@ -26,10 +28,13 @@ from brainstat.stats.utils import apply_mask, undo_mask
|
|
|
26
28
|
class SLM:
|
|
27
29
|
"""Core Class for running BrainStat linear models"""
|
|
28
30
|
|
|
29
|
-
#
|
|
30
|
-
from
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
# Class methods are imported at module level (above) and assigned here
|
|
32
|
+
# rather than via in-class `from ... import ...`, which trips
|
|
33
|
+
# python/mypy#10521 and previously forced a file-level `# type: ignore`.
|
|
34
|
+
_linear_model = _linear_model
|
|
35
|
+
_fdr = _fdr
|
|
36
|
+
_random_field_theory = _random_field_theory
|
|
37
|
+
_t_test = _t_test
|
|
33
38
|
|
|
34
39
|
def __init__(
|
|
35
40
|
self,
|
|
@@ -89,7 +94,7 @@ class SLM:
|
|
|
89
94
|
"""
|
|
90
95
|
# Input arguments.
|
|
91
96
|
self.model = model
|
|
92
|
-
self.contrast =
|
|
97
|
+
self.contrast = self._coerce_contrast(contrast)
|
|
93
98
|
|
|
94
99
|
if isinstance(surf, str):
|
|
95
100
|
self.surf_name = surf
|
|
@@ -98,7 +103,7 @@ class SLM:
|
|
|
98
103
|
self.surf_name = None
|
|
99
104
|
self.surf = surf
|
|
100
105
|
|
|
101
|
-
self.mask = mask
|
|
106
|
+
self.mask = self._coerce_mask(mask)
|
|
102
107
|
self.correction = [correction] if isinstance(correction, str) else correction
|
|
103
108
|
self.niter = 1
|
|
104
109
|
self.thetalim = thetalim
|
|
@@ -110,12 +115,54 @@ class SLM:
|
|
|
110
115
|
# Error check
|
|
111
116
|
if self.surf is None:
|
|
112
117
|
if self.correction is not None and "rft" in self.correction:
|
|
113
|
-
raise ValueError(
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"Random Field Theory corrections require a surface. For "
|
|
120
|
+
"volumetric data, drop 'rft' from `correction` and use "
|
|
121
|
+
"`correction='fdr'` (cluster-level RFT/TFCE on volumes is "
|
|
122
|
+
"not supported by BrainStat — see issue #342)."
|
|
123
|
+
)
|
|
114
124
|
|
|
115
125
|
# We have to initialize fit parameters for our unit tests here.
|
|
116
126
|
# TODO: remove this requirement.
|
|
117
127
|
self._reset_fit_parameters()
|
|
118
128
|
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _coerce_contrast(contrast: ArrayLike) -> np.ndarray:
|
|
131
|
+
"""Convert ``contrast`` to a numeric ndarray.
|
|
132
|
+
|
|
133
|
+
Pandas Series and categorical/object inputs are common foot-guns —
|
|
134
|
+
np.dot then raises a cryptic "can't multiply sequence by non-int"
|
|
135
|
+
error inside _t_test. Cast to float here and surface a clear
|
|
136
|
+
TypeError if that fails (see issue #342).
|
|
137
|
+
"""
|
|
138
|
+
if isinstance(contrast, FixedEffect):
|
|
139
|
+
return contrast # type: ignore[return-value]
|
|
140
|
+
arr = np.asarray(contrast)
|
|
141
|
+
if arr.dtype.kind in "biufc":
|
|
142
|
+
return arr
|
|
143
|
+
try:
|
|
144
|
+
return arr.astype(float)
|
|
145
|
+
except (ValueError, TypeError) as exc:
|
|
146
|
+
raise TypeError(
|
|
147
|
+
f"`contrast` must be numeric; got dtype {arr.dtype}. If you "
|
|
148
|
+
"passed a pandas Series with categorical/object dtype, cast "
|
|
149
|
+
"it to float first (e.g. df['col'].astype(float))."
|
|
150
|
+
) from exc
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _coerce_mask(mask: Optional[ArrayLike]) -> Optional[np.ndarray]:
|
|
154
|
+
"""Convert ``mask`` to a 1-D boolean ndarray.
|
|
155
|
+
|
|
156
|
+
Volumetric workflows commonly produce 0/1 *integer* masks (e.g.
|
|
157
|
+
from `nib.load(...).get_fdata().astype(int)`). The downstream
|
|
158
|
+
boolean-indexing path then degenerates into fancy indexing, which
|
|
159
|
+
triggers shape-mismatch errors in `_fdr` (see issue #342). Cast to
|
|
160
|
+
bool here so callers don't have to remember.
|
|
161
|
+
"""
|
|
162
|
+
if mask is None:
|
|
163
|
+
return None
|
|
164
|
+
return np.asarray(mask).astype(bool)
|
|
165
|
+
|
|
119
166
|
def fit(self, Y: np.ndarray) -> None:
|
|
120
167
|
"""Fits the SLM model
|
|
121
168
|
|
|
@@ -322,7 +369,11 @@ class SLM:
|
|
|
322
369
|
)
|
|
323
370
|
yeo_names, _ = fetch_yeo_networks_metadata(7)
|
|
324
371
|
yeo_names.insert(0, "Undefined")
|
|
325
|
-
|
|
372
|
+
# vertid is stored as 1-based (inherited from SurfStat /
|
|
373
|
+
# MATLAB conventions in peak_clus); subtract 1 before
|
|
374
|
+
# indexing the 0-based yeo7 array (see #347).
|
|
375
|
+
vertid = self.P["peak"]["vertid"][i]
|
|
376
|
+
yeo7_index = yeo7[vertid - 1] if vertid is not None else None
|
|
326
377
|
if "yeo7" not in self.P["peak"]:
|
|
327
378
|
self.P["peak"]["yeo7"] = []
|
|
328
379
|
self.P["peak"]["yeo7"].append(np.take(yeo_names, yeo7_index))
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import math
|
|
2
|
-
import sys
|
|
3
2
|
import warnings
|
|
4
3
|
|
|
5
4
|
import numpy as np
|
|
@@ -39,7 +38,7 @@ def _t_test(self) -> None:
|
|
|
39
38
|
if np.square(np.dot(null_space(self.X).T, c)).sum() / np.square(
|
|
40
39
|
c
|
|
41
40
|
).sum() > np.spacing(1):
|
|
42
|
-
|
|
41
|
+
raise ValueError("Contrast is not estimable.")
|
|
43
42
|
|
|
44
43
|
else:
|
|
45
44
|
c = np.dot(pinvX, self.contrast)
|
|
@@ -173,6 +172,8 @@ def _t_test(self) -> None:
|
|
|
173
172
|
M[M_ef] = ef_duplicate[:, i]
|
|
174
173
|
M[M_sse] = sse_matrix.flatten()
|
|
175
174
|
|
|
176
|
-
|
|
175
|
+
# Use .item() rather than float(): numpy >=2.0 raises
|
|
176
|
+
# TypeError when float() is applied to size-1 ndarrays.
|
|
177
|
+
self.t[i] = np.sqrt(-np.linalg.det(M) / det_sse / vf).item()
|
|
177
178
|
|
|
178
179
|
self.t = np.atleast_2d(self.t)
|
|
@@ -180,3 +180,53 @@ def test_volumetric_input():
|
|
|
180
180
|
|
|
181
181
|
slm = SLM(model, contrast, surf=mask_image)
|
|
182
182
|
slm.fit(data)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_volumetric_contrast_coercion():
|
|
186
|
+
"""Pandas Series / categorical contrasts must be coerced rather than
|
|
187
|
+
crashing inside _t_test (issue #342)."""
|
|
188
|
+
model = FixedEffect(np.ones(3))
|
|
189
|
+
slm = SLM(model, pd.Series([1.0, 0.5, -0.5]))
|
|
190
|
+
assert slm.contrast.dtype == np.float64
|
|
191
|
+
|
|
192
|
+
with pytest.raises(TypeError, match="must be numeric"):
|
|
193
|
+
SLM(model, pd.Series(["a", "b", "c"]))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_volumetric_int_mask_is_coerced_to_bool():
|
|
197
|
+
"""Volumetric workflows commonly hand SLM a 0/1 *int* mask
|
|
198
|
+
(issue #342). It should be coerced rather than degenerating into
|
|
199
|
+
fancy indexing inside _fdr."""
|
|
200
|
+
model = FixedEffect(np.ones(3))
|
|
201
|
+
int_mask = np.array([1, 0, 1, 1, 0, 1], dtype=int)
|
|
202
|
+
slm = SLM(model, np.ones(3), mask=int_mask)
|
|
203
|
+
assert slm.mask.dtype == np.bool_
|
|
204
|
+
assert slm.mask.sum() == 4
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_volumetric_rft_without_surface_explains_alternatives():
|
|
208
|
+
"""RFT on a volume is unsupported; the error should point users at FDR
|
|
209
|
+
rather than just refusing (issue #342)."""
|
|
210
|
+
model = FixedEffect(np.ones(3))
|
|
211
|
+
with pytest.raises(ValueError, match="fdr"):
|
|
212
|
+
SLM(model, np.ones(3), correction="rft")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_volumetric_fdr_with_int_mask_runs_end_to_end():
|
|
216
|
+
"""The full int-mask + FDR + non-trivial contrast path that the issue
|
|
217
|
+
reporters originally hit should now run without error (issue #342)."""
|
|
218
|
+
n_subjects, n_voxels = 8, 64
|
|
219
|
+
rng = np.random.default_rng(0)
|
|
220
|
+
data = rng.standard_normal((n_subjects, n_voxels))
|
|
221
|
+
# Half the voxels in the mask, expressed as ints (the foot-gun shape).
|
|
222
|
+
int_mask = np.zeros(n_voxels, dtype=int)
|
|
223
|
+
int_mask[: n_voxels // 2] = 1
|
|
224
|
+
model = FixedEffect(rng.standard_normal(n_subjects))
|
|
225
|
+
contrast = pd.Series(rng.standard_normal(n_subjects)) # pandas, on purpose
|
|
226
|
+
slm = SLM(model, contrast, mask=int_mask, correction="fdr")
|
|
227
|
+
slm.fit(data)
|
|
228
|
+
assert slm.t.shape[-1] == n_voxels
|
|
229
|
+
# Inside the mask the t-values must be finite; outside they're masked
|
|
230
|
+
# out and undo_mask fills them with NaN.
|
|
231
|
+
assert np.all(np.isfinite(slm.t[..., int_mask.astype(bool)]))
|
|
232
|
+
assert slm.Q.shape[-1] == n_voxels
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brainstat
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A toolbox for statistical analysis of neuroimaging data
|
|
5
5
|
Home-page: https://github.com/MICA-LAB/BrainStat
|
|
6
6
|
Author: MNI-MICA Lab and MPI-CNG Lab
|
|
@@ -26,8 +26,8 @@ Requires-Dist: neurosynth
|
|
|
26
26
|
Requires-Dist: nibabel
|
|
27
27
|
Requires-Dist: nilearn>=0.7.0
|
|
28
28
|
Requires-Dist: nimare
|
|
29
|
-
Requires-Dist: numpy
|
|
30
|
-
Requires-Dist: pandas
|
|
29
|
+
Requires-Dist: numpy<2,>=1.21
|
|
30
|
+
Requires-Dist: pandas<2.1,>=1.3
|
|
31
31
|
Requires-Dist: scikit_learn>=1.0
|
|
32
32
|
Requires-Dist: scipy>=1.7
|
|
33
33
|
Requires-Dist: templateflow
|
|
@@ -44,8 +44,11 @@ setuptools.setup(
|
|
|
44
44
|
"nibabel",
|
|
45
45
|
"nilearn>=0.7.0",
|
|
46
46
|
"nimare",
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
# numpy<2 is required so the pinned pandas<2.1 (whose 2.0.x C
|
|
48
|
+
# extensions were built against the numpy 1.x ABI) loads cleanly.
|
|
49
|
+
# See requirements.txt for the abagen-driven rationale.
|
|
50
|
+
"numpy>=1.21,<2",
|
|
51
|
+
"pandas>=1.3,<2.1",
|
|
49
52
|
"scikit_learn>=1.0",
|
|
50
53
|
"scipy>=1.7",
|
|
51
54
|
"templateflow",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|