nimare 0.4.2__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.
- benchmarks/__init__.py +0 -0
- benchmarks/bench_cbma.py +57 -0
- nimare/__init__.py +45 -0
- nimare/_version.py +21 -0
- nimare/annotate/__init__.py +21 -0
- nimare/annotate/cogat.py +213 -0
- nimare/annotate/gclda.py +924 -0
- nimare/annotate/lda.py +147 -0
- nimare/annotate/text.py +75 -0
- nimare/annotate/utils.py +87 -0
- nimare/base.py +217 -0
- nimare/cli.py +124 -0
- nimare/correct.py +462 -0
- nimare/dataset.py +685 -0
- nimare/decode/__init__.py +33 -0
- nimare/decode/base.py +115 -0
- nimare/decode/continuous.py +462 -0
- nimare/decode/discrete.py +753 -0
- nimare/decode/encode.py +110 -0
- nimare/decode/utils.py +44 -0
- nimare/diagnostics.py +510 -0
- nimare/estimator.py +139 -0
- nimare/extract/__init__.py +19 -0
- nimare/extract/extract.py +466 -0
- nimare/extract/utils.py +295 -0
- nimare/generate.py +331 -0
- nimare/io.py +667 -0
- nimare/meta/__init__.py +39 -0
- nimare/meta/cbma/__init__.py +6 -0
- nimare/meta/cbma/ale.py +951 -0
- nimare/meta/cbma/base.py +947 -0
- nimare/meta/cbma/mkda.py +1361 -0
- nimare/meta/cbmr.py +970 -0
- nimare/meta/ibma.py +1683 -0
- nimare/meta/kernel.py +501 -0
- nimare/meta/models.py +1199 -0
- nimare/meta/utils.py +494 -0
- nimare/nimads.py +492 -0
- nimare/reports/__init__.py +24 -0
- nimare/reports/base.py +664 -0
- nimare/reports/default.yml +123 -0
- nimare/reports/figures.py +651 -0
- nimare/reports/report.tpl +160 -0
- nimare/resources/__init__.py +1 -0
- nimare/resources/atlases/Harvard-Oxford-LICENSE +93 -0
- nimare/resources/atlases/HarvardOxford-cort-maxprob-thr25-2mm.nii.gz +0 -0
- nimare/resources/database_file_manifest.json +142 -0
- nimare/resources/english_spellings.csv +1738 -0
- nimare/resources/filenames.json +32 -0
- nimare/resources/neurosynth_laird_studies.json +58773 -0
- nimare/resources/neurosynth_stoplist.txt +396 -0
- nimare/resources/nidm_pain_dset.json +1349 -0
- nimare/resources/references.bib +541 -0
- nimare/resources/semantic_knowledge_children.txt +325 -0
- nimare/resources/semantic_relatedness_children.txt +249 -0
- nimare/resources/templates/MNI152_2x2x2_brainmask.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_desc-brain_mask.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_T1w.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_desc-brain_mask.nii.gz +0 -0
- nimare/results.py +225 -0
- nimare/stats.py +276 -0
- nimare/tests/__init__.py +1 -0
- nimare/tests/conftest.py +229 -0
- nimare/tests/data/amygdala_roi.nii.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_coordinates.tsv.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_metadata.tsv.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_vocab-terms_source-abstract_type-tfidf_features.npz +0 -0
- nimare/tests/data/data-neurosynth_version-7_vocab-terms_vocabulary.txt +100 -0
- nimare/tests/data/neurosynth_dset.json +2868 -0
- nimare/tests/data/neurosynth_laird_studies.json +58773 -0
- nimare/tests/data/nidm_pain_dset.json +1349 -0
- nimare/tests/data/nimads_annotation.json +1 -0
- nimare/tests/data/nimads_studyset.json +1 -0
- nimare/tests/data/test_baseline.txt +2 -0
- nimare/tests/data/test_pain_dataset.json +1278 -0
- nimare/tests/data/test_pain_dataset_multiple_contrasts.json +1242 -0
- nimare/tests/data/test_sleuth_file.txt +18 -0
- nimare/tests/data/test_sleuth_file2.txt +10 -0
- nimare/tests/data/test_sleuth_file3.txt +5 -0
- nimare/tests/data/test_sleuth_file4.txt +5 -0
- nimare/tests/data/test_sleuth_file5.txt +5 -0
- nimare/tests/test_annotate_cogat.py +32 -0
- nimare/tests/test_annotate_gclda.py +86 -0
- nimare/tests/test_annotate_lda.py +27 -0
- nimare/tests/test_dataset.py +99 -0
- nimare/tests/test_decode_continuous.py +132 -0
- nimare/tests/test_decode_discrete.py +92 -0
- nimare/tests/test_diagnostics.py +168 -0
- nimare/tests/test_estimator_performance.py +385 -0
- nimare/tests/test_extract.py +46 -0
- nimare/tests/test_generate.py +247 -0
- nimare/tests/test_io.py +294 -0
- nimare/tests/test_meta_ale.py +298 -0
- nimare/tests/test_meta_cbmr.py +295 -0
- nimare/tests/test_meta_ibma.py +240 -0
- nimare/tests/test_meta_kernel.py +209 -0
- nimare/tests/test_meta_mkda.py +234 -0
- nimare/tests/test_nimads.py +21 -0
- nimare/tests/test_reports.py +110 -0
- nimare/tests/test_stats.py +101 -0
- nimare/tests/test_transforms.py +272 -0
- nimare/tests/test_utils.py +200 -0
- nimare/tests/test_workflows.py +221 -0
- nimare/tests/utils.py +126 -0
- nimare/transforms.py +907 -0
- nimare/utils.py +1367 -0
- nimare/workflows/__init__.py +14 -0
- nimare/workflows/base.py +189 -0
- nimare/workflows/cbma.py +165 -0
- nimare/workflows/ibma.py +108 -0
- nimare/workflows/macm.py +77 -0
- nimare/workflows/misc.py +65 -0
- nimare-0.4.2.dist-info/LICENSE +21 -0
- nimare-0.4.2.dist-info/METADATA +124 -0
- nimare-0.4.2.dist-info/RECORD +119 -0
- nimare-0.4.2.dist-info/WHEEL +5 -0
- nimare-0.4.2.dist-info/entry_points.txt +2 -0
- nimare-0.4.2.dist-info/top_level.txt +2 -0
nimare/meta/ibma.py
ADDED
@@ -0,0 +1,1683 @@
|
|
1
|
+
"""Image-based meta-analysis estimators."""
|
2
|
+
|
3
|
+
from __future__ import division
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from collections import Counter
|
7
|
+
|
8
|
+
import nibabel as nib
|
9
|
+
import numpy as np
|
10
|
+
import pandas as pd
|
11
|
+
import pymare
|
12
|
+
from joblib import Memory
|
13
|
+
|
14
|
+
try:
|
15
|
+
# nilearn>0.10.3
|
16
|
+
from nilearn._utils.niimg_conversions import check_same_fov
|
17
|
+
except ImportError:
|
18
|
+
# nilearn < 0.10.3
|
19
|
+
from nilearn._utils.niimg_conversions import _check_same_fov as check_same_fov
|
20
|
+
|
21
|
+
from nilearn.image import concat_imgs, resample_to_img
|
22
|
+
from nilearn.input_data import NiftiMasker
|
23
|
+
from nilearn.mass_univariate import permuted_ols
|
24
|
+
|
25
|
+
from nimare import _version
|
26
|
+
from nimare.estimator import Estimator
|
27
|
+
from nimare.meta.utils import _apply_liberal_mask
|
28
|
+
from nimare.transforms import d_to_g, p_to_z, t_to_d, t_to_z
|
29
|
+
from nimare.utils import _boolean_unmask, _check_ncores, get_masker
|
30
|
+
|
31
|
+
LGR = logging.getLogger(__name__)
|
32
|
+
__version__ = _version.get_versions()["version"]
|
33
|
+
|
34
|
+
|
35
|
+
class IBMAEstimator(Estimator):
|
36
|
+
"""Base class for meta-analysis methods in :mod:`~nimare.meta`.
|
37
|
+
|
38
|
+
.. versionchanged:: 0.2.1
|
39
|
+
|
40
|
+
- New parameters: ``memory`` and ``memory_level`` for memory caching.
|
41
|
+
|
42
|
+
.. versionchanged:: 0.2.0
|
43
|
+
|
44
|
+
* Remove `resample` and `memory_limit` arguments. Resampling is now
|
45
|
+
performed only if shape/affines are different.
|
46
|
+
|
47
|
+
.. versionadded:: 0.0.12
|
48
|
+
|
49
|
+
* IBMA-specific elements of ``Estimator`` excised and used to create ``IBMAEstimator``.
|
50
|
+
* Generic kwargs and args converted to named kwargs.
|
51
|
+
All remaining kwargs are for resampling.
|
52
|
+
|
53
|
+
"""
|
54
|
+
|
55
|
+
def __init__(
|
56
|
+
self,
|
57
|
+
aggressive_mask=True,
|
58
|
+
memory=Memory(location=None, verbose=0),
|
59
|
+
memory_level=0,
|
60
|
+
*,
|
61
|
+
mask=None,
|
62
|
+
**kwargs,
|
63
|
+
):
|
64
|
+
self.aggressive_mask = aggressive_mask
|
65
|
+
|
66
|
+
if mask is not None:
|
67
|
+
mask = get_masker(mask, memory=memory, memory_level=memory_level)
|
68
|
+
self.masker = mask
|
69
|
+
|
70
|
+
super().__init__(memory=memory, memory_level=memory_level)
|
71
|
+
|
72
|
+
# defaults for resampling images (nilearn's defaults do not work well)
|
73
|
+
self._resample_kwargs = {"clip": True, "interpolation": "linear"}
|
74
|
+
|
75
|
+
# Identify any kwargs
|
76
|
+
resample_kwargs = {k: v for k, v in kwargs.items() if k.startswith("resample__")}
|
77
|
+
|
78
|
+
# Flag any extraneous kwargs
|
79
|
+
other_kwargs = dict(set(kwargs.items()) - set(resample_kwargs.items()))
|
80
|
+
if other_kwargs:
|
81
|
+
LGR.warn(f"Unused keyword arguments found: {tuple(other_kwargs.items())}")
|
82
|
+
|
83
|
+
# Update the default resampling parameters
|
84
|
+
resample_kwargs = {k.split("resample__")[1]: v for k, v in resample_kwargs.items()}
|
85
|
+
self._resample_kwargs.update(resample_kwargs)
|
86
|
+
|
87
|
+
def _preprocess_input(self, dataset):
|
88
|
+
"""Preprocess inputs to the Estimator from the Dataset as needed."""
|
89
|
+
masker = self.masker or dataset.masker
|
90
|
+
|
91
|
+
mask_img = masker.mask_img or masker.labels_img
|
92
|
+
if isinstance(mask_img, str):
|
93
|
+
mask_img = nib.load(mask_img)
|
94
|
+
|
95
|
+
# Reserve the key for the correlation matrix
|
96
|
+
self.inputs_["corr_matrix"] = None
|
97
|
+
|
98
|
+
if self.aggressive_mask:
|
99
|
+
# Ensure that protected values are not included among _required_inputs
|
100
|
+
assert (
|
101
|
+
"aggressive_mask" not in self._required_inputs.keys()
|
102
|
+
), "This is a protected name."
|
103
|
+
|
104
|
+
if "aggressive_mask" in self.inputs_.keys():
|
105
|
+
LGR.warning("Removing existing 'aggressive_mask' from Estimator.")
|
106
|
+
self.inputs_.pop("aggressive_mask")
|
107
|
+
else:
|
108
|
+
# A dictionary to collect data, to be further reduced by the liberal mask.
|
109
|
+
self.inputs_["data_bags"] = {}
|
110
|
+
|
111
|
+
for name, (type_, _) in self._required_inputs.items():
|
112
|
+
if type_ == "image":
|
113
|
+
# Resampling will only occur if shape/affines are different
|
114
|
+
imgs = [
|
115
|
+
(
|
116
|
+
nib.load(img)
|
117
|
+
if check_same_fov(nib.load(img), reference_masker=mask_img)
|
118
|
+
else resample_to_img(nib.load(img), mask_img, **self._resample_kwargs)
|
119
|
+
)
|
120
|
+
for img in self.inputs_[name]
|
121
|
+
]
|
122
|
+
|
123
|
+
# input to NiFtiLabelsMasker must be 4d
|
124
|
+
img4d = concat_imgs(imgs, ensure_ndim=4)
|
125
|
+
|
126
|
+
# Mask required input images using either the dataset's mask or the estimator's.
|
127
|
+
temp_arr = masker.transform(img4d)
|
128
|
+
|
129
|
+
# To save memory, we only save the original image array and perform masking later
|
130
|
+
# in the estimator if self.aggressive_mask is True.
|
131
|
+
self.inputs_[name] = temp_arr
|
132
|
+
|
133
|
+
if self.aggressive_mask:
|
134
|
+
# Determine the good voxels here
|
135
|
+
nonzero_voxels_bool = np.all(temp_arr != 0, axis=0)
|
136
|
+
nonnan_voxels_bool = np.all(~np.isnan(temp_arr), axis=0)
|
137
|
+
good_voxels_bool = np.logical_and(nonzero_voxels_bool, nonnan_voxels_bool)
|
138
|
+
|
139
|
+
if "aggressive_mask" not in self.inputs_.keys():
|
140
|
+
self.inputs_["aggressive_mask"] = good_voxels_bool
|
141
|
+
else:
|
142
|
+
# Remove any voxels that are bad in any image-based inputs
|
143
|
+
self.inputs_["aggressive_mask"] = np.logical_or(
|
144
|
+
self.inputs_["aggressive_mask"],
|
145
|
+
good_voxels_bool,
|
146
|
+
)
|
147
|
+
else:
|
148
|
+
data_bags = zip(*_apply_liberal_mask(temp_arr))
|
149
|
+
|
150
|
+
keys = ["values", "voxel_mask", "study_mask"]
|
151
|
+
self.inputs_["data_bags"][name] = [dict(zip(keys, bag)) for bag in data_bags]
|
152
|
+
|
153
|
+
# Further reduce image-based inputs to remove "bad" voxels
|
154
|
+
# (voxels with zeros or NaNs in any studies)
|
155
|
+
if self.aggressive_mask:
|
156
|
+
if n_bad_voxels := (
|
157
|
+
self.inputs_["aggressive_mask"].size - self.inputs_["aggressive_mask"].sum()
|
158
|
+
):
|
159
|
+
LGR.warning(f"Masking out {n_bad_voxels} additional voxels.")
|
160
|
+
|
161
|
+
|
162
|
+
class Fishers(IBMAEstimator):
|
163
|
+
"""An image-based meta-analytic test using t- or z-statistic images.
|
164
|
+
|
165
|
+
Requires z-statistic images, but will be extended to work with t-statistic images as well.
|
166
|
+
|
167
|
+
This method is described in :footcite:t:`fisher1946statistical`.
|
168
|
+
|
169
|
+
.. versionchanged:: 0.3.0
|
170
|
+
|
171
|
+
* New parameter: ``two_sided``, controls the type of test to be performed. In addition,
|
172
|
+
the default is now set to True (two-sided), which differs from previous versions
|
173
|
+
where only one-sided tests were performed.
|
174
|
+
|
175
|
+
.. versionchanged:: 0.2.1
|
176
|
+
|
177
|
+
* New parameter: ``aggressive_mask``, to control whether to use an aggressive mask.
|
178
|
+
|
179
|
+
Parameters
|
180
|
+
----------
|
181
|
+
aggressive_mask : :obj:`bool`, optional
|
182
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
183
|
+
from the analysis.
|
184
|
+
If False, all voxels are included by running a separate analysis on bags
|
185
|
+
of voxels that belong that have a valid value across the same studies.
|
186
|
+
Default is True.
|
187
|
+
two_sided : :obj:`bool`, optional
|
188
|
+
If True, performs an unsigned t-test. Both positive and negative effects are considered;
|
189
|
+
the null hypothesis is that the effect is zero. If False, only positive effects are
|
190
|
+
considered as relevant. The null hypothesis is that the effect is zero or negative.
|
191
|
+
Default is True.
|
192
|
+
|
193
|
+
Notes
|
194
|
+
-----
|
195
|
+
Requires ``z`` images.
|
196
|
+
|
197
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
198
|
+
|
199
|
+
============== ===============================================================================
|
200
|
+
"z" Z-statistic map from one-sample test.
|
201
|
+
"p" P-value map from one-sample test.
|
202
|
+
"dof" Degrees of freedom map from one-sample test.
|
203
|
+
============== ===============================================================================
|
204
|
+
|
205
|
+
Warnings
|
206
|
+
--------
|
207
|
+
Masking approaches which average across voxels (e.g., NiftiLabelsMaskers)
|
208
|
+
will result in invalid results. It cannot be used with these types of maskers.
|
209
|
+
|
210
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
211
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
212
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
213
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
214
|
+
the same studies.
|
215
|
+
|
216
|
+
References
|
217
|
+
----------
|
218
|
+
.. footbibliography::
|
219
|
+
|
220
|
+
See Also
|
221
|
+
--------
|
222
|
+
:class:`pymare.estimators.FisherCombinationTest`:
|
223
|
+
The PyMARE estimator called by this class.
|
224
|
+
"""
|
225
|
+
|
226
|
+
_required_inputs = {"z_maps": ("image", "z")}
|
227
|
+
|
228
|
+
def __init__(self, two_sided=True, **kwargs):
|
229
|
+
super().__init__(**kwargs)
|
230
|
+
self.two_sided = two_sided
|
231
|
+
self._mode = "concordant" if self.two_sided else "directed"
|
232
|
+
|
233
|
+
def _generate_description(self):
|
234
|
+
description = (
|
235
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
236
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}) on "
|
237
|
+
f"{len(self.inputs_['id'])} z-statistic images using the Fisher "
|
238
|
+
"combined probability method \\citep{fisher1946statistical}."
|
239
|
+
)
|
240
|
+
return description
|
241
|
+
|
242
|
+
def _fit_model(self, stat_maps):
|
243
|
+
"""Fit the model to the data."""
|
244
|
+
n_studies, n_voxels = stat_maps.shape
|
245
|
+
|
246
|
+
pymare_dset = pymare.Dataset(y=stat_maps)
|
247
|
+
est = pymare.estimators.FisherCombinationTest(mode=self._mode)
|
248
|
+
est.fit_dataset(pymare_dset)
|
249
|
+
est_summary = est.summary()
|
250
|
+
|
251
|
+
z_map = est_summary.z.squeeze()
|
252
|
+
p_map = est_summary.p.squeeze()
|
253
|
+
dof_map = np.tile(n_studies - 1, n_voxels).astype(np.int32)
|
254
|
+
|
255
|
+
return z_map, p_map, dof_map
|
256
|
+
|
257
|
+
def _fit(self, dataset):
|
258
|
+
self.dataset = dataset
|
259
|
+
self.masker = self.masker or dataset.masker
|
260
|
+
if not isinstance(self.masker, NiftiMasker):
|
261
|
+
raise ValueError(
|
262
|
+
f"A {type(self.masker)} mask has been detected. "
|
263
|
+
"Only NiftiMaskers are allowed for this Estimator. "
|
264
|
+
"This is because aggregation, such as averaging values across ROIs, "
|
265
|
+
"will produce invalid results."
|
266
|
+
)
|
267
|
+
|
268
|
+
if self.aggressive_mask:
|
269
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
270
|
+
result_maps = self._fit_model(self.inputs_["z_maps"][:, voxel_mask])
|
271
|
+
|
272
|
+
z_map, p_map, dof_map = tuple(
|
273
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps)
|
274
|
+
)
|
275
|
+
else:
|
276
|
+
n_voxels = self.inputs_["z_maps"].shape[1]
|
277
|
+
z_map = np.zeros(n_voxels, dtype=float)
|
278
|
+
p_map = np.zeros(n_voxels, dtype=float)
|
279
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
280
|
+
for bag in self.inputs_["data_bags"]["z_maps"]:
|
281
|
+
(
|
282
|
+
z_map[bag["voxel_mask"]],
|
283
|
+
p_map[bag["voxel_mask"]],
|
284
|
+
dof_map[bag["voxel_mask"]],
|
285
|
+
) = self._fit_model(bag["values"])
|
286
|
+
|
287
|
+
maps = {"z": z_map, "p": p_map, "dof": dof_map}
|
288
|
+
description = self._generate_description()
|
289
|
+
|
290
|
+
return maps, {}, description
|
291
|
+
|
292
|
+
|
293
|
+
class Stouffers(IBMAEstimator):
|
294
|
+
"""A t-test on z-statistic images.
|
295
|
+
|
296
|
+
Requires z-statistic images.
|
297
|
+
|
298
|
+
This method is described in :footcite:t:`stouffer1949american`.
|
299
|
+
|
300
|
+
.. versionchanged:: 0.3.0
|
301
|
+
|
302
|
+
* New parameter: ``two_sided``, controls the type of test to be performed. In addition,
|
303
|
+
the default is now set to True (two-sided), which differs from previous versions
|
304
|
+
where only one-sided tests were performed.
|
305
|
+
* Add correction for multiple contrasts within a study.
|
306
|
+
* New parameter: ``normalize_contrast_weights`` to normalized the weights by the
|
307
|
+
number of contrasts in each study.
|
308
|
+
|
309
|
+
.. versionchanged:: 0.2.1
|
310
|
+
|
311
|
+
* New parameter: ``aggressive_mask``, to control whether to use an aggressive mask.
|
312
|
+
|
313
|
+
Parameters
|
314
|
+
----------
|
315
|
+
aggressive_mask : :obj:`bool`, optional
|
316
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
317
|
+
from the analysis.
|
318
|
+
If False, all voxels are included by running a separate analysis on bags
|
319
|
+
of voxels that belong that have a valid value across the same studies.
|
320
|
+
Default is True.
|
321
|
+
use_sample_size : :obj:`bool`, optional
|
322
|
+
Whether to use sample sizes for weights (i.e., "weighted Stouffer's") or not,
|
323
|
+
as described in :footcite:t:`zaykin2011optimally`.
|
324
|
+
Default is False.
|
325
|
+
normalize_contrast_weights : :obj:`bool`, optional
|
326
|
+
Whether to use number of contrast per study to normalized the weights or not.
|
327
|
+
Default is False.
|
328
|
+
two_sided : :obj:`bool`, optional
|
329
|
+
If True, performs an unsigned t-test. Both positive and negative effects are considered;
|
330
|
+
the null hypothesis is that the effect is zero. If False, only positive effects are
|
331
|
+
considered as relevant. The null hypothesis is that the effect is zero or negative.
|
332
|
+
Default is True.
|
333
|
+
|
334
|
+
Notes
|
335
|
+
-----
|
336
|
+
Requires ``z`` images and optionally the sample size metadata field.
|
337
|
+
|
338
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
339
|
+
|
340
|
+
============== ===============================================================================
|
341
|
+
"z" Z-statistic map from one-sample test.
|
342
|
+
"p" P-value map from one-sample test.
|
343
|
+
"dof" Degrees of freedom map from one-sample test.
|
344
|
+
============== ===============================================================================
|
345
|
+
|
346
|
+
Warnings
|
347
|
+
--------
|
348
|
+
Masking approaches which average across voxels (e.g., NiftiLabelsMaskers)
|
349
|
+
will result in invalid results. It cannot be used with these types of maskers.
|
350
|
+
|
351
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
352
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
353
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
354
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
355
|
+
the same studies.
|
356
|
+
|
357
|
+
References
|
358
|
+
----------
|
359
|
+
.. footbibliography::
|
360
|
+
|
361
|
+
See Also
|
362
|
+
--------
|
363
|
+
:class:`pymare.estimators.StoufferCombinationTest`:
|
364
|
+
The PyMARE estimator called by this class.
|
365
|
+
"""
|
366
|
+
|
367
|
+
_required_inputs = {"z_maps": ("image", "z")}
|
368
|
+
|
369
|
+
def __init__(
|
370
|
+
self,
|
371
|
+
use_sample_size=False,
|
372
|
+
normalize_contrast_weights=False,
|
373
|
+
two_sided=True,
|
374
|
+
**kwargs,
|
375
|
+
):
|
376
|
+
super().__init__(**kwargs)
|
377
|
+
self.use_sample_size = use_sample_size
|
378
|
+
if self.use_sample_size:
|
379
|
+
self._required_inputs["sample_sizes"] = ("metadata", "sample_sizes")
|
380
|
+
|
381
|
+
self.normalize_contrast_weights = normalize_contrast_weights
|
382
|
+
|
383
|
+
self.two_sided = two_sided
|
384
|
+
self._mode = "concordant" if self.two_sided else "directed"
|
385
|
+
|
386
|
+
def _preprocess_input(self, dataset):
|
387
|
+
"""Preprocess additional inputs to the Estimator from the Dataset as needed."""
|
388
|
+
super()._preprocess_input(dataset)
|
389
|
+
|
390
|
+
study_mask = dataset.images["id"].isin(self.inputs_["id"])
|
391
|
+
|
392
|
+
# Convert each contrast name to a unique integer value.
|
393
|
+
labels = dataset.images["study_id"][study_mask].to_list()
|
394
|
+
label_to_int = {label: i for i, label in enumerate(set(labels))}
|
395
|
+
label_counts = Counter(labels)
|
396
|
+
|
397
|
+
self.inputs_["contrast_names"] = np.array([label_to_int[label] for label in labels])
|
398
|
+
self.inputs_["num_contrasts"] = np.array([label_counts[label] for label in labels])
|
399
|
+
|
400
|
+
n_studies = len(self.inputs_["id"])
|
401
|
+
if n_studies != np.unique(self.inputs_["contrast_names"]).size:
|
402
|
+
# If all studies are not unique, we will need to correct for multiple contrasts
|
403
|
+
# Calculate correlation matrix on valid voxels
|
404
|
+
if self.aggressive_mask:
|
405
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
406
|
+
self.inputs_["corr_matrix"] = np.corrcoef(
|
407
|
+
self.inputs_["z_maps"][:, voxel_mask],
|
408
|
+
rowvar=True,
|
409
|
+
)
|
410
|
+
else:
|
411
|
+
self.inputs_["corr_matrix"] = np.corrcoef(
|
412
|
+
self.inputs_["z_maps"],
|
413
|
+
rowvar=True,
|
414
|
+
)
|
415
|
+
|
416
|
+
def _generate_description(self):
|
417
|
+
description = (
|
418
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
419
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}) on "
|
420
|
+
f"{len(self.inputs_['id'])} z-statistic images using the Stouffer "
|
421
|
+
"method \\citep{stouffer1949american}"
|
422
|
+
)
|
423
|
+
|
424
|
+
if self.use_sample_size:
|
425
|
+
description += (
|
426
|
+
", with studies weighted by the square root of the study sample sizes, per "
|
427
|
+
"\\cite{zaykin2011optimally}."
|
428
|
+
)
|
429
|
+
else:
|
430
|
+
description += "."
|
431
|
+
|
432
|
+
return description
|
433
|
+
|
434
|
+
def _fit_model(self, stat_maps, study_mask=None, corr=None):
|
435
|
+
"""Fit the model to the data."""
|
436
|
+
n_studies, n_voxels = stat_maps.shape
|
437
|
+
|
438
|
+
if study_mask is None:
|
439
|
+
# If no mask is provided, assume all studies are included. This is always the case
|
440
|
+
# when using the aggressive mask.
|
441
|
+
study_mask = np.arange(n_studies)
|
442
|
+
|
443
|
+
est = pymare.estimators.StoufferCombinationTest(mode=self._mode)
|
444
|
+
|
445
|
+
contrast_maps, sub_corr = None, None
|
446
|
+
if corr is not None:
|
447
|
+
contrast_maps = np.tile(self.inputs_["contrast_names"][study_mask], (n_voxels, 1)).T
|
448
|
+
sub_corr = corr[np.ix_(study_mask, study_mask)]
|
449
|
+
|
450
|
+
weights = np.ones(n_studies)
|
451
|
+
|
452
|
+
if self.normalize_contrast_weights:
|
453
|
+
weights *= 1 / self.inputs_["num_contrasts"][study_mask]
|
454
|
+
|
455
|
+
if self.use_sample_size:
|
456
|
+
sample_sizes = np.array(
|
457
|
+
[np.mean(self.inputs_["sample_sizes"][idx]) for idx in study_mask]
|
458
|
+
)
|
459
|
+
weights *= np.sqrt(sample_sizes)
|
460
|
+
|
461
|
+
weight_maps = np.tile(weights, (n_voxels, 1)).T
|
462
|
+
|
463
|
+
pymare_dset = pymare.Dataset(y=stat_maps, n=weight_maps, v=contrast_maps)
|
464
|
+
est.fit_dataset(pymare_dset, corr=sub_corr)
|
465
|
+
est_summary = est.summary()
|
466
|
+
|
467
|
+
z_map = est_summary.z.squeeze()
|
468
|
+
p_map = est_summary.p.squeeze()
|
469
|
+
dof_map = np.tile(n_studies - 1, n_voxels).astype(np.int32)
|
470
|
+
|
471
|
+
return z_map, p_map, dof_map
|
472
|
+
|
473
|
+
def _fit(self, dataset):
|
474
|
+
self.dataset = dataset
|
475
|
+
self.masker = self.masker or dataset.masker
|
476
|
+
if not isinstance(self.masker, NiftiMasker):
|
477
|
+
raise ValueError(
|
478
|
+
f"A {type(self.masker)} mask has been detected. "
|
479
|
+
"Only NiftiMaskers are allowed for this Estimator. "
|
480
|
+
"This is because aggregation, such as averaging values across ROIs, "
|
481
|
+
"will produce invalid results."
|
482
|
+
)
|
483
|
+
|
484
|
+
if self.aggressive_mask:
|
485
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
486
|
+
|
487
|
+
result_maps = self._fit_model(
|
488
|
+
self.inputs_["z_maps"][:, voxel_mask],
|
489
|
+
corr=self.inputs_["corr_matrix"],
|
490
|
+
)
|
491
|
+
|
492
|
+
z_map, p_map, dof_map = tuple(
|
493
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps)
|
494
|
+
)
|
495
|
+
else:
|
496
|
+
n_voxels = self.inputs_["z_maps"].shape[1]
|
497
|
+
z_map = np.zeros(n_voxels, dtype=float)
|
498
|
+
p_map = np.zeros(n_voxels, dtype=float)
|
499
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
500
|
+
for bag in self.inputs_["data_bags"]["z_maps"]:
|
501
|
+
(
|
502
|
+
z_map[bag["voxel_mask"]],
|
503
|
+
p_map[bag["voxel_mask"]],
|
504
|
+
dof_map[bag["voxel_mask"]],
|
505
|
+
) = self._fit_model(
|
506
|
+
bag["values"], bag["study_mask"], corr=self.inputs_["corr_matrix"]
|
507
|
+
)
|
508
|
+
|
509
|
+
maps = {"z": z_map, "p": p_map, "dof": dof_map}
|
510
|
+
description = self._generate_description()
|
511
|
+
|
512
|
+
return maps, {}, description
|
513
|
+
|
514
|
+
|
515
|
+
class WeightedLeastSquares(IBMAEstimator):
|
516
|
+
"""Weighted least-squares meta-regression.
|
517
|
+
|
518
|
+
.. versionchanged:: 0.2.1
|
519
|
+
|
520
|
+
* New parameter: ``aggressive_mask``, to control whether to use an aggressive mask.
|
521
|
+
|
522
|
+
.. versionchanged:: 0.0.12
|
523
|
+
|
524
|
+
* Add "se" to outputs.
|
525
|
+
|
526
|
+
.. versionchanged:: 0.0.8
|
527
|
+
|
528
|
+
* [FIX] Remove single-dimensional entries of each array of returns (:obj:`dict`).
|
529
|
+
|
530
|
+
.. versionadded:: 0.0.4
|
531
|
+
|
532
|
+
Provides the weighted least-squares estimate of the fixed effects given
|
533
|
+
known/assumed between-study variance tau^2.
|
534
|
+
When tau^2 = 0 (default), the model is the standard inverse-weighted
|
535
|
+
fixed-effects meta-regression.
|
536
|
+
|
537
|
+
This method was described in :footcite:t:`brockwell2001comparison`.
|
538
|
+
|
539
|
+
Parameters
|
540
|
+
----------
|
541
|
+
aggressive_mask : :obj:`bool`, optional
|
542
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
543
|
+
from the analysis.
|
544
|
+
If False, all voxels are included by running a separate analysis on bags
|
545
|
+
of voxels that belong that have a valid value across the same studies.
|
546
|
+
Default is True.
|
547
|
+
tau2 : :obj:`float` or 1D :class:`numpy.ndarray`, optional
|
548
|
+
Assumed/known value of tau^2. Must be >= 0. Default is 0.
|
549
|
+
|
550
|
+
Notes
|
551
|
+
-----
|
552
|
+
Requires :term:`beta` and :term:`varcope` images.
|
553
|
+
|
554
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
555
|
+
|
556
|
+
============== ===============================================================================
|
557
|
+
"z" Z-statistic map from one-sample test.
|
558
|
+
"p" P-value map from one-sample test.
|
559
|
+
"est" Fixed effects estimate for intercept test.
|
560
|
+
"se" Standard error of fixed effects estimate.
|
561
|
+
"dof" Degrees of freedom map from one-sample test.
|
562
|
+
============== ===============================================================================
|
563
|
+
|
564
|
+
Warnings
|
565
|
+
--------
|
566
|
+
Masking approaches which average across voxels (e.g., NiftiLabelsMaskers)
|
567
|
+
will likely result in biased results. The extent of this bias is currently
|
568
|
+
unknown.
|
569
|
+
|
570
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
571
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
572
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
573
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
574
|
+
the same studies.
|
575
|
+
|
576
|
+
References
|
577
|
+
----------
|
578
|
+
.. footbibliography::
|
579
|
+
|
580
|
+
See Also
|
581
|
+
--------
|
582
|
+
:class:`pymare.estimators.WeightedLeastSquares`:
|
583
|
+
The PyMARE estimator called by this class.
|
584
|
+
"""
|
585
|
+
|
586
|
+
_required_inputs = {"beta_maps": ("image", "beta"), "varcope_maps": ("image", "varcope")}
|
587
|
+
|
588
|
+
def __init__(self, tau2=0, **kwargs):
|
589
|
+
super().__init__(**kwargs)
|
590
|
+
self.tau2 = tau2
|
591
|
+
|
592
|
+
def _generate_description(self):
|
593
|
+
description = (
|
594
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
595
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}), on "
|
596
|
+
f"{len(self.inputs_['id'])} beta images using the Weighted Least Squares approach "
|
597
|
+
"\\citep{brockwell2001comparison}, "
|
598
|
+
f"with an a priori tau-squared value of {self.tau2} defined across all voxels."
|
599
|
+
)
|
600
|
+
return description
|
601
|
+
|
602
|
+
def _fit_model(self, beta_maps, varcope_maps):
|
603
|
+
"""Fit the model to the data."""
|
604
|
+
n_studies, n_voxels = beta_maps.shape
|
605
|
+
|
606
|
+
pymare_dset = pymare.Dataset(y=beta_maps, v=varcope_maps)
|
607
|
+
est = pymare.estimators.WeightedLeastSquares(tau2=self.tau2)
|
608
|
+
est.fit_dataset(pymare_dset)
|
609
|
+
est_summary = est.summary()
|
610
|
+
|
611
|
+
fe_stats = est_summary.get_fe_stats()
|
612
|
+
z_map = fe_stats["z"].squeeze()
|
613
|
+
p_map = fe_stats["p"].squeeze()
|
614
|
+
est_map = fe_stats["est"].squeeze()
|
615
|
+
se_map = fe_stats["se"].squeeze()
|
616
|
+
dof_map = np.tile(n_studies - 1, n_voxels).astype(np.int32)
|
617
|
+
|
618
|
+
return z_map, p_map, est_map, se_map, dof_map
|
619
|
+
|
620
|
+
def _fit(self, dataset):
|
621
|
+
self.dataset = dataset
|
622
|
+
self.masker = self.masker or dataset.masker
|
623
|
+
if not isinstance(self.masker, NiftiMasker):
|
624
|
+
LGR.warning(
|
625
|
+
f"A {type(self.masker)} mask has been detected. "
|
626
|
+
"Masks which average across voxels will likely produce biased results when used "
|
627
|
+
"with this Estimator."
|
628
|
+
)
|
629
|
+
|
630
|
+
if self.aggressive_mask:
|
631
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
632
|
+
result_maps = self._fit_model(
|
633
|
+
self.inputs_["beta_maps"][:, voxel_mask],
|
634
|
+
self.inputs_["varcope_maps"][:, voxel_mask],
|
635
|
+
)
|
636
|
+
|
637
|
+
z_map, p_map, est_map, se_map, dof_map = tuple(
|
638
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps)
|
639
|
+
)
|
640
|
+
else:
|
641
|
+
n_voxels = self.inputs_["beta_maps"].shape[1]
|
642
|
+
|
643
|
+
z_map, p_map, est_map, se_map = [np.zeros(n_voxels, dtype=float) for _ in range(4)]
|
644
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
645
|
+
|
646
|
+
beta_bags = self.inputs_["data_bags"]["beta_maps"]
|
647
|
+
varcope_bags = self.inputs_["data_bags"]["varcope_maps"]
|
648
|
+
for beta_bag, varcope_bag in zip(beta_bags, varcope_bags):
|
649
|
+
(
|
650
|
+
z_map[beta_bag["voxel_mask"]],
|
651
|
+
p_map[beta_bag["voxel_mask"]],
|
652
|
+
est_map[beta_bag["voxel_mask"]],
|
653
|
+
se_map[beta_bag["voxel_mask"]],
|
654
|
+
dof_map[beta_bag["voxel_mask"]],
|
655
|
+
) = self._fit_model(beta_bag["values"], varcope_bag["values"])
|
656
|
+
|
657
|
+
# tau2 is a float, not a map, so it can't go into the results dictionary
|
658
|
+
tables = {"level-estimator": pd.DataFrame(columns=["tau2"], data=[self.tau2])}
|
659
|
+
maps = {"z": z_map, "p": p_map, "est": est_map, "se": se_map, "dof": dof_map}
|
660
|
+
description = self._generate_description()
|
661
|
+
|
662
|
+
return maps, tables, description
|
663
|
+
|
664
|
+
|
665
|
+
class DerSimonianLaird(IBMAEstimator):
|
666
|
+
"""DerSimonian-Laird meta-regression estimator.
|
667
|
+
|
668
|
+
.. versionchanged:: 0.2.1
|
669
|
+
|
670
|
+
* New parameter: ``aggressive_mask``, to control whether to use an aggressive mask.
|
671
|
+
|
672
|
+
.. versionchanged:: 0.0.12
|
673
|
+
|
674
|
+
* Add "se" to outputs.
|
675
|
+
|
676
|
+
.. versionchanged:: 0.0.8
|
677
|
+
|
678
|
+
* [FIX] Remove single-dimensional entries of each array of returns (:obj:`dict`).
|
679
|
+
|
680
|
+
.. versionadded:: 0.0.4
|
681
|
+
|
682
|
+
Estimates the between-subject variance tau^2 using the :footcite:t:`dersimonian1986meta`
|
683
|
+
method-of-moments approach :footcite:p:`dersimonian1986meta,kosmidis2017improving`.
|
684
|
+
|
685
|
+
Parameters
|
686
|
+
----------
|
687
|
+
aggressive_mask : :obj:`bool`, optional
|
688
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
689
|
+
from the analysis.
|
690
|
+
If False, all voxels are included by running a separate analysis on bags
|
691
|
+
of voxels that belong that have a valid value across the same studies.
|
692
|
+
Default is True.
|
693
|
+
|
694
|
+
Notes
|
695
|
+
-----
|
696
|
+
Requires :term:`beta` and :term:`varcope` images.
|
697
|
+
|
698
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
699
|
+
|
700
|
+
============== ===============================================================================
|
701
|
+
"z" Z-statistic map from one-sample test.
|
702
|
+
"p" P-value map from one-sample test.
|
703
|
+
"est" Fixed effects estimate for intercept test.
|
704
|
+
"se" Standard error of fixed effects estimate.
|
705
|
+
"tau2" Estimated between-study variance.
|
706
|
+
"dof" Degrees of freedom map from one-sample test.
|
707
|
+
============== ===============================================================================
|
708
|
+
|
709
|
+
Warnings
|
710
|
+
--------
|
711
|
+
Masking approaches which average across voxels (e.g., NiftiLabelsMaskers)
|
712
|
+
will likely result in biased results. The extent of this bias is currently
|
713
|
+
unknown.
|
714
|
+
|
715
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
716
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
717
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
718
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
719
|
+
the same studies.
|
720
|
+
|
721
|
+
References
|
722
|
+
----------
|
723
|
+
.. footbibliography::
|
724
|
+
|
725
|
+
See Also
|
726
|
+
--------
|
727
|
+
:class:`pymare.estimators.DerSimonianLaird`:
|
728
|
+
The PyMARE estimator called by this class.
|
729
|
+
"""
|
730
|
+
|
731
|
+
_required_inputs = {"beta_maps": ("image", "beta"), "varcope_maps": ("image", "varcope")}
|
732
|
+
|
733
|
+
def _generate_description(self):
|
734
|
+
description = (
|
735
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
736
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}), on "
|
737
|
+
f"{len(self.inputs_['id'])} beta and variance images using the "
|
738
|
+
"DerSimonian-Laird method \\citep{dersimonian1986meta}, in which tau-squared is "
|
739
|
+
"estimated on a voxel-wise basis using the method-of-moments approach "
|
740
|
+
"\\citep{dersimonian1986meta,kosmidis2017improving}."
|
741
|
+
)
|
742
|
+
return description
|
743
|
+
|
744
|
+
def _fit_model(self, beta_maps, varcope_maps):
|
745
|
+
"""Fit the model to the data."""
|
746
|
+
n_studies, n_voxels = beta_maps.shape
|
747
|
+
|
748
|
+
pymare_dset = pymare.Dataset(y=beta_maps, v=varcope_maps)
|
749
|
+
est = pymare.estimators.DerSimonianLaird()
|
750
|
+
est.fit_dataset(pymare_dset)
|
751
|
+
est_summary = est.summary()
|
752
|
+
|
753
|
+
fe_stats = est_summary.get_fe_stats()
|
754
|
+
z_map = fe_stats["z"].squeeze()
|
755
|
+
p_map = fe_stats["p"].squeeze()
|
756
|
+
est_map = fe_stats["est"].squeeze()
|
757
|
+
se_map = fe_stats["se"].squeeze()
|
758
|
+
tau2_map = est_summary.tau2.squeeze()
|
759
|
+
dof_map = np.tile(n_studies - 1, n_voxels).astype(np.int32)
|
760
|
+
|
761
|
+
return z_map, p_map, est_map, se_map, tau2_map, dof_map
|
762
|
+
|
763
|
+
def _fit(self, dataset):
|
764
|
+
self.dataset = dataset
|
765
|
+
self.masker = self.masker or dataset.masker
|
766
|
+
if not isinstance(self.masker, NiftiMasker):
|
767
|
+
LGR.warning(
|
768
|
+
f"A {type(self.masker)} mask has been detected. "
|
769
|
+
"Masks which average across voxels will likely produce biased results when used "
|
770
|
+
"with this Estimator."
|
771
|
+
)
|
772
|
+
|
773
|
+
if self.aggressive_mask:
|
774
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
775
|
+
result_maps = self._fit_model(
|
776
|
+
self.inputs_["beta_maps"][:, voxel_mask],
|
777
|
+
self.inputs_["varcope_maps"][:, voxel_mask],
|
778
|
+
)
|
779
|
+
|
780
|
+
z_map, p_map, est_map, se_map, tau2_map, dof_map = tuple(
|
781
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps)
|
782
|
+
)
|
783
|
+
else:
|
784
|
+
n_voxels = self.inputs_["beta_maps"].shape[1]
|
785
|
+
|
786
|
+
z_map, p_map, est_map, se_map, tau2_map = [
|
787
|
+
np.zeros(n_voxels, dtype=float) for _ in range(5)
|
788
|
+
]
|
789
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
790
|
+
|
791
|
+
beta_bags = self.inputs_["data_bags"]["beta_maps"]
|
792
|
+
varcope_bags = self.inputs_["data_bags"]["varcope_maps"]
|
793
|
+
for beta_bag, varcope_bag in zip(beta_bags, varcope_bags):
|
794
|
+
(
|
795
|
+
z_map[beta_bag["voxel_mask"]],
|
796
|
+
p_map[beta_bag["voxel_mask"]],
|
797
|
+
est_map[beta_bag["voxel_mask"]],
|
798
|
+
se_map[beta_bag["voxel_mask"]],
|
799
|
+
tau2_map[beta_bag["voxel_mask"]],
|
800
|
+
dof_map[beta_bag["voxel_mask"]],
|
801
|
+
) = self._fit_model(beta_bag["values"], varcope_bag["values"])
|
802
|
+
|
803
|
+
maps = {
|
804
|
+
"z": z_map,
|
805
|
+
"p": p_map,
|
806
|
+
"est": est_map,
|
807
|
+
"se": se_map,
|
808
|
+
"tau2": tau2_map,
|
809
|
+
"dof": dof_map,
|
810
|
+
}
|
811
|
+
description = self._generate_description()
|
812
|
+
|
813
|
+
return maps, {}, description
|
814
|
+
|
815
|
+
|
816
|
+
class Hedges(IBMAEstimator):
|
817
|
+
"""Hedges meta-regression estimator.
|
818
|
+
|
819
|
+
.. versionchanged:: 0.2.1
|
820
|
+
|
821
|
+
* New parameter: ``aggressive_mask``, to control whether to use an aggressive mask.
|
822
|
+
|
823
|
+
.. versionchanged:: 0.0.12
|
824
|
+
|
825
|
+
* Add "se" to outputs.
|
826
|
+
|
827
|
+
.. versionchanged:: 0.0.8
|
828
|
+
|
829
|
+
* [FIX] Remove single-dimensional entries of each array of returns (:obj:`dict`).
|
830
|
+
|
831
|
+
.. versionadded:: 0.0.4
|
832
|
+
|
833
|
+
Estimates the between-subject variance tau^2 using the :footcite:t:`hedges2014statistical`
|
834
|
+
approach.
|
835
|
+
|
836
|
+
Parameters
|
837
|
+
----------
|
838
|
+
aggressive_mask : :obj:`bool`, optional
|
839
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
840
|
+
from the analysis.
|
841
|
+
If False, all voxels are included by running a separate analysis on bags
|
842
|
+
of voxels that belong that have a valid value across the same studies.
|
843
|
+
Default is True.
|
844
|
+
|
845
|
+
Notes
|
846
|
+
-----
|
847
|
+
Requires :term:`beta` and :term:`varcope` images.
|
848
|
+
|
849
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
850
|
+
|
851
|
+
============== ===============================================================================
|
852
|
+
"z" Z-statistic map from one-sample test.
|
853
|
+
"p" P-value map from one-sample test.
|
854
|
+
"est" Fixed effects estimate for intercept test.
|
855
|
+
"se" Standard error of fixed effects estimate.
|
856
|
+
"tau2" Estimated between-study variance.
|
857
|
+
"dof" Degrees of freedom map from one-sample test.
|
858
|
+
============== ===============================================================================
|
859
|
+
|
860
|
+
Warnings
|
861
|
+
--------
|
862
|
+
Masking approaches which average across voxels (e.g., NiftiLabelsMaskers)
|
863
|
+
will likely result in biased results. The extent of this bias is currently
|
864
|
+
unknown.
|
865
|
+
|
866
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
867
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
868
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
869
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
870
|
+
the same studies.
|
871
|
+
|
872
|
+
References
|
873
|
+
----------
|
874
|
+
.. footbibliography::
|
875
|
+
|
876
|
+
See Also
|
877
|
+
--------
|
878
|
+
:class:`pymare.estimators.Hedges`:
|
879
|
+
The PyMARE estimator called by this class.
|
880
|
+
"""
|
881
|
+
|
882
|
+
_required_inputs = {"beta_maps": ("image", "beta"), "varcope_maps": ("image", "varcope")}
|
883
|
+
|
884
|
+
def _generate_description(self):
|
885
|
+
description = (
|
886
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
887
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}), on "
|
888
|
+
f"{len(self.inputs_['id'])} beta and variance images using the Hedges "
|
889
|
+
"method \\citep{hedges2014statistical}, in which tau-squared is estimated on a "
|
890
|
+
"voxel-wise basis."
|
891
|
+
)
|
892
|
+
return description
|
893
|
+
|
894
|
+
def _fit_model(self, beta_maps, varcope_maps):
|
895
|
+
"""Fit the model to the data."""
|
896
|
+
n_studies, n_voxels = beta_maps.shape
|
897
|
+
|
898
|
+
pymare_dset = pymare.Dataset(y=beta_maps, v=varcope_maps)
|
899
|
+
est = pymare.estimators.Hedges()
|
900
|
+
est.fit_dataset(pymare_dset)
|
901
|
+
est_summary = est.summary()
|
902
|
+
|
903
|
+
fe_stats = est_summary.get_fe_stats()
|
904
|
+
z_map = fe_stats["z"].squeeze()
|
905
|
+
p_map = fe_stats["p"].squeeze()
|
906
|
+
est_map = fe_stats["est"].squeeze()
|
907
|
+
se_map = fe_stats["se"].squeeze()
|
908
|
+
tau2_map = est_summary.tau2.squeeze()
|
909
|
+
dof_map = np.tile(n_studies - 1, n_voxels).astype(np.int32)
|
910
|
+
|
911
|
+
return z_map, p_map, est_map, se_map, tau2_map, dof_map
|
912
|
+
|
913
|
+
def _fit(self, dataset):
|
914
|
+
self.dataset = dataset
|
915
|
+
self.masker = self.masker or dataset.masker
|
916
|
+
if not isinstance(self.masker, NiftiMasker):
|
917
|
+
LGR.warning(
|
918
|
+
f"A {type(self.masker)} mask has been detected. "
|
919
|
+
"Masks which average across voxels will likely produce biased results when used "
|
920
|
+
"with this Estimator."
|
921
|
+
)
|
922
|
+
|
923
|
+
if self.aggressive_mask:
|
924
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
925
|
+
result_maps = self._fit_model(
|
926
|
+
self.inputs_["beta_maps"][:, voxel_mask],
|
927
|
+
self.inputs_["varcope_maps"][:, voxel_mask],
|
928
|
+
)
|
929
|
+
|
930
|
+
z_map, p_map, est_map, se_map, tau2_map, dof_map = tuple(
|
931
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps)
|
932
|
+
)
|
933
|
+
else:
|
934
|
+
n_voxels = self.inputs_["beta_maps"].shape[1]
|
935
|
+
|
936
|
+
z_map, p_map, est_map, se_map, tau2_map = [
|
937
|
+
np.zeros(n_voxels, dtype=float) for _ in range(5)
|
938
|
+
]
|
939
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
940
|
+
|
941
|
+
beta_bags = self.inputs_["data_bags"]["beta_maps"]
|
942
|
+
varcope_bags = self.inputs_["data_bags"]["varcope_maps"]
|
943
|
+
for beta_bag, varcope_bag in zip(beta_bags, varcope_bags):
|
944
|
+
(
|
945
|
+
z_map[beta_bag["voxel_mask"]],
|
946
|
+
p_map[beta_bag["voxel_mask"]],
|
947
|
+
est_map[beta_bag["voxel_mask"]],
|
948
|
+
se_map[beta_bag["voxel_mask"]],
|
949
|
+
tau2_map[beta_bag["voxel_mask"]],
|
950
|
+
dof_map[beta_bag["voxel_mask"]],
|
951
|
+
) = self._fit_model(beta_bag["values"], varcope_bag["values"])
|
952
|
+
|
953
|
+
maps = {
|
954
|
+
"z": z_map,
|
955
|
+
"p": p_map,
|
956
|
+
"est": est_map,
|
957
|
+
"se": se_map,
|
958
|
+
"tau2": tau2_map,
|
959
|
+
"dof": dof_map,
|
960
|
+
}
|
961
|
+
description = self._generate_description()
|
962
|
+
|
963
|
+
return maps, {}, description
|
964
|
+
|
965
|
+
|
966
|
+
class SampleSizeBasedLikelihood(IBMAEstimator):
|
967
|
+
"""Method estimates with known sample sizes but unknown sampling variances.
|
968
|
+
|
969
|
+
.. versionchanged:: 0.2.1
|
970
|
+
|
971
|
+
* New parameter: ``aggressive_mask``, to control whether to use an aggressive mask.
|
972
|
+
|
973
|
+
.. versionchanged:: 0.0.12
|
974
|
+
|
975
|
+
* Add "se" and "sigma2" to outputs.
|
976
|
+
|
977
|
+
.. versionchanged:: 0.0.8
|
978
|
+
|
979
|
+
* [FIX] Remove single-dimensional entries of each array of returns (:obj:`dict`).
|
980
|
+
|
981
|
+
.. versionadded:: 0.0.4
|
982
|
+
|
983
|
+
Iteratively estimates the between-subject variance tau^2 and fixed effect
|
984
|
+
betas using the specified likelihood-based estimator (ML or REML).
|
985
|
+
|
986
|
+
Parameters
|
987
|
+
----------
|
988
|
+
aggressive_mask : :obj:`bool`, optional
|
989
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
990
|
+
from the analysis.
|
991
|
+
If False, all voxels are included by running a separate analysis on bags
|
992
|
+
of voxels that belong that have a valid value across the same studies.
|
993
|
+
Default is True.
|
994
|
+
method : {'ml', 'reml'}, optional
|
995
|
+
The estimation method to use. The available options are
|
996
|
+
|
997
|
+
============== =============================
|
998
|
+
"ml" (default) Maximum likelihood
|
999
|
+
"reml" Restricted maximum likelihood
|
1000
|
+
============== =============================
|
1001
|
+
|
1002
|
+
Notes
|
1003
|
+
-----
|
1004
|
+
Requires :term:`beta` images and sample size from metadata.
|
1005
|
+
|
1006
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
1007
|
+
|
1008
|
+
============== ===============================================================================
|
1009
|
+
"z" Z-statistic map from one-sample test.
|
1010
|
+
"p" P-value map from one-sample test.
|
1011
|
+
"est" Fixed effects estimate for intercept test.
|
1012
|
+
"se" Standard error of fixed effects estimate.
|
1013
|
+
"tau2" Estimated between-study variance.
|
1014
|
+
"sigma2" Estimated within-study variance. Assumed to be the same for all studies.
|
1015
|
+
"dof" Degrees of freedom map from one-sample test.
|
1016
|
+
============== ===============================================================================
|
1017
|
+
|
1018
|
+
Homogeneity of sigma^2 across studies is assumed.
|
1019
|
+
The ML and REML solutions are obtained via SciPy's scalar function
|
1020
|
+
minimizer (:func:`scipy.optimize.minimize`).
|
1021
|
+
Parameters to ``minimize()`` can be passed in as keyword arguments.
|
1022
|
+
|
1023
|
+
Warnings
|
1024
|
+
--------
|
1025
|
+
Likelihood-based estimators are not parallelized across voxels, so this
|
1026
|
+
method should not be used on full brains, unless you can submit your code
|
1027
|
+
to a job scheduler.
|
1028
|
+
|
1029
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
1030
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
1031
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
1032
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
1033
|
+
the same studies.
|
1034
|
+
|
1035
|
+
See Also
|
1036
|
+
--------
|
1037
|
+
:class:`pymare.estimators.SampleSizeBasedLikelihoodEstimator`:
|
1038
|
+
The PyMARE estimator called by this class.
|
1039
|
+
"""
|
1040
|
+
|
1041
|
+
_required_inputs = {
|
1042
|
+
"beta_maps": ("image", "beta"),
|
1043
|
+
"sample_sizes": ("metadata", "sample_sizes"),
|
1044
|
+
}
|
1045
|
+
|
1046
|
+
def __init__(self, method="ml", **kwargs):
|
1047
|
+
super().__init__(**kwargs)
|
1048
|
+
self.method = method
|
1049
|
+
|
1050
|
+
def _generate_description(self):
|
1051
|
+
description = (
|
1052
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
1053
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}), on "
|
1054
|
+
f"{len(self.inputs_['id'])} beta images using sample size-based "
|
1055
|
+
"maximum likelihood estimation, in which tau-squared and sigma-squared are estimated "
|
1056
|
+
"on a voxel-wise basis."
|
1057
|
+
)
|
1058
|
+
return description
|
1059
|
+
|
1060
|
+
def _fit_model(self, beta_maps, study_mask=None):
|
1061
|
+
"""Fit the model to the data."""
|
1062
|
+
n_studies, n_voxels = beta_maps.shape
|
1063
|
+
|
1064
|
+
if study_mask is None:
|
1065
|
+
# If no mask is provided, assume all studies are included. This is always the case
|
1066
|
+
# when using the aggressive mask.
|
1067
|
+
study_mask = np.arange(n_studies)
|
1068
|
+
|
1069
|
+
sample_sizes = np.array([np.mean(self.inputs_["sample_sizes"][idx]) for idx in study_mask])
|
1070
|
+
n_maps = np.tile(sample_sizes, (n_voxels, 1)).T
|
1071
|
+
|
1072
|
+
pymare_dset = pymare.Dataset(y=beta_maps, n=n_maps)
|
1073
|
+
est = pymare.estimators.SampleSizeBasedLikelihoodEstimator(method=self.method)
|
1074
|
+
est.fit_dataset(pymare_dset)
|
1075
|
+
est_summary = est.summary()
|
1076
|
+
fe_stats = est_summary.get_fe_stats()
|
1077
|
+
|
1078
|
+
z_map = fe_stats["z"].squeeze()
|
1079
|
+
p_map = fe_stats["p"].squeeze()
|
1080
|
+
est_map = fe_stats["est"].squeeze()
|
1081
|
+
se_map = fe_stats["se"].squeeze()
|
1082
|
+
tau2_map = est_summary.tau2.squeeze()
|
1083
|
+
sigma2_map = est.params_["sigma2"].squeeze()
|
1084
|
+
dof_map = np.tile(n_studies - 1, n_voxels).astype(np.int32)
|
1085
|
+
|
1086
|
+
return z_map, p_map, est_map, se_map, tau2_map, sigma2_map, dof_map
|
1087
|
+
|
1088
|
+
def _fit(self, dataset):
|
1089
|
+
self.dataset = dataset
|
1090
|
+
self.masker = self.masker or dataset.masker
|
1091
|
+
|
1092
|
+
if self.aggressive_mask:
|
1093
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
1094
|
+
result_maps = self._fit_model(
|
1095
|
+
self.inputs_["beta_maps"][:, voxel_mask],
|
1096
|
+
)
|
1097
|
+
|
1098
|
+
z_map, p_map, est_map, se_map, tau2_map, sigma2_map, dof_map = tuple(
|
1099
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps)
|
1100
|
+
)
|
1101
|
+
else:
|
1102
|
+
n_voxels = self.inputs_["beta_maps"].shape[1]
|
1103
|
+
|
1104
|
+
z_map, p_map, est_map, se_map, tau2_map, sigma2_map = [
|
1105
|
+
np.zeros(n_voxels, dtype=float) for _ in range(6)
|
1106
|
+
]
|
1107
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
1108
|
+
|
1109
|
+
for bag in self.inputs_["data_bags"]["beta_maps"]:
|
1110
|
+
(
|
1111
|
+
z_map[bag["voxel_mask"]],
|
1112
|
+
p_map[bag["voxel_mask"]],
|
1113
|
+
est_map[bag["voxel_mask"]],
|
1114
|
+
se_map[bag["voxel_mask"]],
|
1115
|
+
tau2_map[bag["voxel_mask"]],
|
1116
|
+
sigma2_map[bag["voxel_mask"]],
|
1117
|
+
dof_map[bag["voxel_mask"]],
|
1118
|
+
) = self._fit_model(bag["values"], bag["study_mask"])
|
1119
|
+
|
1120
|
+
maps = {
|
1121
|
+
"z": z_map,
|
1122
|
+
"p": p_map,
|
1123
|
+
"est": est_map,
|
1124
|
+
"se": se_map,
|
1125
|
+
"tau2": tau2_map,
|
1126
|
+
"sigma2": sigma2_map,
|
1127
|
+
"dof": dof_map,
|
1128
|
+
}
|
1129
|
+
description = self._generate_description()
|
1130
|
+
|
1131
|
+
return maps, {}, description
|
1132
|
+
|
1133
|
+
|
1134
|
+
class VarianceBasedLikelihood(IBMAEstimator):
|
1135
|
+
"""A likelihood-based meta-analysis method for estimates with known variances.
|
1136
|
+
|
1137
|
+
.. versionchanged:: 0.2.1
|
1138
|
+
|
1139
|
+
* New parameter: ``aggressive_mask``, to control whether to use an aggressive mask.
|
1140
|
+
|
1141
|
+
.. versionchanged:: 0.0.12
|
1142
|
+
|
1143
|
+
Add "se" output.
|
1144
|
+
|
1145
|
+
.. versionchanged:: 0.0.8
|
1146
|
+
|
1147
|
+
* [FIX] Remove single-dimensional entries of each array of returns (:obj:`dict`).
|
1148
|
+
|
1149
|
+
.. versionadded:: 0.0.4
|
1150
|
+
|
1151
|
+
Iteratively estimates the between-subject variance tau^2 and fixed effect
|
1152
|
+
coefficients using the specified likelihood-based estimator (ML or REML)
|
1153
|
+
:footcite:p:`dersimonian1986meta,kosmidis2017improving`.
|
1154
|
+
|
1155
|
+
Parameters
|
1156
|
+
----------
|
1157
|
+
aggressive_mask : :obj:`bool`, optional
|
1158
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
1159
|
+
from the analysis.
|
1160
|
+
If False, all voxels are included by running a separate analysis on bags
|
1161
|
+
of voxels that belong that have a valid value across the same studies.
|
1162
|
+
Default is True.
|
1163
|
+
method : {'ml', 'reml'}, optional
|
1164
|
+
The estimation method to use. The available options are
|
1165
|
+
|
1166
|
+
============== =============================
|
1167
|
+
"ml" (default) Maximum likelihood
|
1168
|
+
"reml" Restricted maximum likelihood
|
1169
|
+
============== =============================
|
1170
|
+
|
1171
|
+
Notes
|
1172
|
+
-----
|
1173
|
+
Requires :term:`beta` and :term:`varcope` images.
|
1174
|
+
|
1175
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
1176
|
+
|
1177
|
+
============== ===============================================================================
|
1178
|
+
"z" Z-statistic map from one-sample test.
|
1179
|
+
"p" P-value map from one-sample test.
|
1180
|
+
"est" Fixed effects estimate for intercept test.
|
1181
|
+
"se" Standard error of fixed effects estimate.
|
1182
|
+
"tau2" Estimated between-study variance.
|
1183
|
+
"dof" Degrees of freedom map from one-sample test.
|
1184
|
+
============== ===============================================================================
|
1185
|
+
|
1186
|
+
The ML and REML solutions are obtained via SciPy's scalar function
|
1187
|
+
minimizer (:func:`scipy.optimize.minimize`).
|
1188
|
+
Parameters to ``minimize()`` can be passed in as keyword arguments.
|
1189
|
+
|
1190
|
+
Warnings
|
1191
|
+
--------
|
1192
|
+
Likelihood-based estimators are not parallelized across voxels, so this
|
1193
|
+
method should not be used on full brains, unless you can submit your code
|
1194
|
+
to a job scheduler.
|
1195
|
+
|
1196
|
+
Masking approaches which average across voxels (e.g., NiftiLabelsMaskers)
|
1197
|
+
will likely result in biased results. The extent of this bias is currently
|
1198
|
+
unknown.
|
1199
|
+
|
1200
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
1201
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
1202
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
1203
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
1204
|
+
the same studies.
|
1205
|
+
|
1206
|
+
References
|
1207
|
+
----------
|
1208
|
+
.. footbibliography::
|
1209
|
+
|
1210
|
+
See Also
|
1211
|
+
--------
|
1212
|
+
:class:`pymare.estimators.VarianceBasedLikelihoodEstimator`:
|
1213
|
+
The PyMARE estimator called by this class.
|
1214
|
+
"""
|
1215
|
+
|
1216
|
+
_required_inputs = {"beta_maps": ("image", "beta"), "varcope_maps": ("image", "varcope")}
|
1217
|
+
|
1218
|
+
def __init__(self, method="ml", **kwargs):
|
1219
|
+
super().__init__(**kwargs)
|
1220
|
+
self.method = method
|
1221
|
+
|
1222
|
+
def _generate_description(self):
|
1223
|
+
description = (
|
1224
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
1225
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}), on "
|
1226
|
+
f"{len(self.inputs_['id'])} beta and variance images using "
|
1227
|
+
"variance-based maximum likelihood estimation, in which tau-squared is estimated on a "
|
1228
|
+
"voxel-wise basis."
|
1229
|
+
)
|
1230
|
+
return description
|
1231
|
+
|
1232
|
+
def _fit_model(self, beta_maps, varcope_maps):
|
1233
|
+
"""Fit the model to the data."""
|
1234
|
+
n_studies, n_voxels = beta_maps.shape
|
1235
|
+
|
1236
|
+
pymare_dset = pymare.Dataset(y=beta_maps, v=varcope_maps)
|
1237
|
+
est = pymare.estimators.VarianceBasedLikelihoodEstimator(method=self.method)
|
1238
|
+
est.fit_dataset(pymare_dset)
|
1239
|
+
est_summary = est.summary()
|
1240
|
+
fe_stats = est_summary.get_fe_stats()
|
1241
|
+
|
1242
|
+
z_map = fe_stats["z"].squeeze()
|
1243
|
+
p_map = fe_stats["p"].squeeze()
|
1244
|
+
est_map = fe_stats["est"].squeeze()
|
1245
|
+
se_map = fe_stats["se"].squeeze()
|
1246
|
+
tau2_map = est_summary.tau2.squeeze()
|
1247
|
+
dof_map = np.tile(n_studies - 1, n_voxels).astype(np.int32)
|
1248
|
+
|
1249
|
+
return z_map, p_map, est_map, se_map, tau2_map, dof_map
|
1250
|
+
|
1251
|
+
def _fit(self, dataset):
|
1252
|
+
self.dataset = dataset
|
1253
|
+
self.masker = self.masker or dataset.masker
|
1254
|
+
|
1255
|
+
if not isinstance(self.masker, NiftiMasker):
|
1256
|
+
LGR.warning(
|
1257
|
+
f"A {type(self.masker)} mask has been detected. "
|
1258
|
+
"Masks which average across voxels will likely produce biased results when used "
|
1259
|
+
"with this Estimator."
|
1260
|
+
)
|
1261
|
+
|
1262
|
+
if self.aggressive_mask:
|
1263
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
1264
|
+
result_maps = self._fit_model(
|
1265
|
+
self.inputs_["beta_maps"][:, voxel_mask],
|
1266
|
+
self.inputs_["varcope_maps"][:, voxel_mask],
|
1267
|
+
)
|
1268
|
+
|
1269
|
+
z_map, p_map, est_map, se_map, tau2_map, dof_map = tuple(
|
1270
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps)
|
1271
|
+
)
|
1272
|
+
else:
|
1273
|
+
n_voxels = self.inputs_["beta_maps"].shape[1]
|
1274
|
+
|
1275
|
+
z_map, p_map, est_map, se_map, tau2_map = [
|
1276
|
+
np.zeros(n_voxels, dtype=float) for _ in range(5)
|
1277
|
+
]
|
1278
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
1279
|
+
|
1280
|
+
beta_bags = self.inputs_["data_bags"]["beta_maps"]
|
1281
|
+
varcope_bags = self.inputs_["data_bags"]["varcope_maps"]
|
1282
|
+
for beta_bag, varcope_bag in zip(beta_bags, varcope_bags):
|
1283
|
+
(
|
1284
|
+
z_map[beta_bag["voxel_mask"]],
|
1285
|
+
p_map[beta_bag["voxel_mask"]],
|
1286
|
+
est_map[beta_bag["voxel_mask"]],
|
1287
|
+
se_map[beta_bag["voxel_mask"]],
|
1288
|
+
tau2_map[beta_bag["voxel_mask"]],
|
1289
|
+
dof_map[beta_bag["voxel_mask"]],
|
1290
|
+
) = self._fit_model(beta_bag["values"], varcope_bag["values"])
|
1291
|
+
|
1292
|
+
maps = {
|
1293
|
+
"z": z_map,
|
1294
|
+
"p": p_map,
|
1295
|
+
"est": est_map,
|
1296
|
+
"se": se_map,
|
1297
|
+
"tau2": tau2_map,
|
1298
|
+
"dof": dof_map,
|
1299
|
+
}
|
1300
|
+
description = self._generate_description()
|
1301
|
+
|
1302
|
+
return maps, {}, description
|
1303
|
+
|
1304
|
+
|
1305
|
+
class PermutedOLS(IBMAEstimator):
|
1306
|
+
r"""An analysis with permuted ordinary least squares (OLS), using nilearn.
|
1307
|
+
|
1308
|
+
.. versionchanged:: 0.2.1
|
1309
|
+
|
1310
|
+
* New parameter: ``aggressive_mask``, to control whether to use an aggressive mask.
|
1311
|
+
|
1312
|
+
.. versionchanged:: 0.0.12
|
1313
|
+
|
1314
|
+
* Use beta maps instead of z maps.
|
1315
|
+
|
1316
|
+
.. versionchanged:: 0.0.8
|
1317
|
+
|
1318
|
+
* [FIX] Remove single-dimensional entries of each array of returns (:obj:`dict`).
|
1319
|
+
|
1320
|
+
.. versionadded:: 0.0.4
|
1321
|
+
|
1322
|
+
This approach is described in :footcite:t:`freedman1983nonstochastic`.
|
1323
|
+
|
1324
|
+
Parameters
|
1325
|
+
----------
|
1326
|
+
aggressive_mask : :obj:`bool`, optional
|
1327
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
1328
|
+
from the analysis.
|
1329
|
+
If False, all voxels are included by running a separate analysis on bags
|
1330
|
+
of voxels that belong that have a valid value across the same studies.
|
1331
|
+
Default is True.
|
1332
|
+
two_sided : :obj:`bool`, optional
|
1333
|
+
If True, performs an unsigned t-test. Both positive and negative effects are considered;
|
1334
|
+
the null hypothesis is that the effect is zero. If False, only positive effects are
|
1335
|
+
considered as relevant. The null hypothesis is that the effect is zero or negative.
|
1336
|
+
Default is True.
|
1337
|
+
|
1338
|
+
Notes
|
1339
|
+
-----
|
1340
|
+
Requires ``beta`` images.
|
1341
|
+
|
1342
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
1343
|
+
|
1344
|
+
============== ===============================================================================
|
1345
|
+
"t" T-statistic map from one-sample test.
|
1346
|
+
"z" Z-statistic map from one-sample test.
|
1347
|
+
"dof" Degrees of freedom map from one-sample test.
|
1348
|
+
============== ===============================================================================
|
1349
|
+
|
1350
|
+
Available correction methods: :func:`PermutedOLS.correct_fwe_montecarlo`
|
1351
|
+
|
1352
|
+
Warnings
|
1353
|
+
--------
|
1354
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
1355
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
1356
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
1357
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
1358
|
+
the same studies.
|
1359
|
+
|
1360
|
+
References
|
1361
|
+
----------
|
1362
|
+
.. footbibliography::
|
1363
|
+
|
1364
|
+
See Also
|
1365
|
+
--------
|
1366
|
+
nilearn.mass_univariate.permuted_ols : The function used for this IBMA.
|
1367
|
+
"""
|
1368
|
+
|
1369
|
+
_required_inputs = {"beta_maps": ("image", "beta")}
|
1370
|
+
|
1371
|
+
def __init__(self, two_sided=True, **kwargs):
|
1372
|
+
super().__init__(**kwargs)
|
1373
|
+
self.two_sided = two_sided
|
1374
|
+
self.parameters_ = {}
|
1375
|
+
|
1376
|
+
def _generate_description(self):
|
1377
|
+
description = (
|
1378
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
1379
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}), on "
|
1380
|
+
f"{len(self.inputs_['id'])} beta images using Nilearn's "
|
1381
|
+
"\\citep{10.3389/fninf.2014.00014} permuted ordinary least squares method."
|
1382
|
+
)
|
1383
|
+
return description
|
1384
|
+
|
1385
|
+
def _fit_model(self, beta_maps, n_perm=0):
|
1386
|
+
"""Fit the model to the data."""
|
1387
|
+
n_studies, n_voxels = beta_maps.shape
|
1388
|
+
|
1389
|
+
# Use intercept as explanatory variable
|
1390
|
+
tested_vars = np.ones((n_studies, 1))
|
1391
|
+
confounding_vars = None
|
1392
|
+
|
1393
|
+
log_p_map, t_map, _ = permuted_ols(
|
1394
|
+
tested_vars,
|
1395
|
+
beta_maps,
|
1396
|
+
confounding_vars=confounding_vars,
|
1397
|
+
model_intercept=False, # modeled by tested_vars
|
1398
|
+
n_perm=n_perm,
|
1399
|
+
two_sided_test=self.two_sided,
|
1400
|
+
random_state=42,
|
1401
|
+
n_jobs=1,
|
1402
|
+
verbose=0,
|
1403
|
+
)
|
1404
|
+
|
1405
|
+
# Convert t to z, preserving signs
|
1406
|
+
dof = n_studies - 1
|
1407
|
+
|
1408
|
+
z_map = t_to_z(t_map, dof)
|
1409
|
+
dof_map = np.tile(dof, n_voxels).astype(np.int32)
|
1410
|
+
|
1411
|
+
return log_p_map.squeeze(), t_map.squeeze(), z_map.squeeze(), dof_map
|
1412
|
+
|
1413
|
+
def _fit(self, dataset):
|
1414
|
+
self.dataset = dataset
|
1415
|
+
|
1416
|
+
if self.aggressive_mask:
|
1417
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
1418
|
+
result_maps = self._fit_model(self.inputs_["beta_maps"][:, voxel_mask])
|
1419
|
+
|
1420
|
+
# Skip log_p_map
|
1421
|
+
t_map, z_map, dof_map = tuple(
|
1422
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps[1:])
|
1423
|
+
)
|
1424
|
+
else:
|
1425
|
+
n_voxels = self.inputs_["beta_maps"].shape[1]
|
1426
|
+
t_map = np.zeros(n_voxels, dtype=float)
|
1427
|
+
z_map = np.zeros(n_voxels, dtype=float)
|
1428
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
1429
|
+
|
1430
|
+
for bag in self.inputs_["data_bags"]["beta_maps"]:
|
1431
|
+
(
|
1432
|
+
_, # Skip log_p_map
|
1433
|
+
t_map[bag["voxel_mask"]],
|
1434
|
+
z_map[bag["voxel_mask"]],
|
1435
|
+
dof_map[bag["voxel_mask"]],
|
1436
|
+
) = self._fit_model(bag["values"])
|
1437
|
+
|
1438
|
+
maps = {"t": t_map, "z": z_map, "dof": dof_map}
|
1439
|
+
description = self._generate_description()
|
1440
|
+
|
1441
|
+
return maps, {}, description
|
1442
|
+
|
1443
|
+
def correct_fwe_montecarlo(self, result, n_iters=5000, n_cores=1):
|
1444
|
+
"""Perform FWE correction using the max-value permutation method.
|
1445
|
+
|
1446
|
+
.. versionchanged:: 0.0.8
|
1447
|
+
|
1448
|
+
* [FIX] Remove single-dimensional entries of each array of returns (:obj:`dict`).
|
1449
|
+
|
1450
|
+
.. versionadded:: 0.0.4
|
1451
|
+
|
1452
|
+
Only call this method from within a Corrector.
|
1453
|
+
|
1454
|
+
Parameters
|
1455
|
+
----------
|
1456
|
+
result : :obj:`~nimare.results.MetaResult`
|
1457
|
+
Result object from an ALE meta-analysis.
|
1458
|
+
n_iters : :obj:`int`, default=5000
|
1459
|
+
The number of iterations to run in estimating the null distribution.
|
1460
|
+
Default is 5000.
|
1461
|
+
n_cores : :obj:`int`, default=1
|
1462
|
+
Number of cores to use for parallelization.
|
1463
|
+
If <=0, defaults to using all available cores. Default is 1.
|
1464
|
+
|
1465
|
+
Returns
|
1466
|
+
-------
|
1467
|
+
images : :obj:`dict`
|
1468
|
+
Dictionary of 1D arrays corresponding to masked images generated by
|
1469
|
+
the correction procedure. The following arrays are generated by
|
1470
|
+
this method: 'p_level-voxel', 'z_level-voxel', 'logp_level-voxel'.
|
1471
|
+
|
1472
|
+
See Also
|
1473
|
+
--------
|
1474
|
+
nimare.correct.FWECorrector : The Corrector from which to call this method.
|
1475
|
+
nilearn.mass_univariate.permuted_ols : The function used for this IBMA.
|
1476
|
+
|
1477
|
+
Examples
|
1478
|
+
--------
|
1479
|
+
>>> meta = PermutedOLS()
|
1480
|
+
>>> result = meta.fit(dset)
|
1481
|
+
>>> corrector = FWECorrector(method='montecarlo',
|
1482
|
+
n_iters=5, n_cores=1)
|
1483
|
+
>>> cresult = corrector.transform(result)
|
1484
|
+
"""
|
1485
|
+
n_cores = _check_ncores(n_cores)
|
1486
|
+
|
1487
|
+
if self.aggressive_mask:
|
1488
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
1489
|
+
log_p_map, t_map, _, _ = self._fit_model(
|
1490
|
+
self.inputs_["beta_maps"][:, voxel_mask], n_perm=n_iters
|
1491
|
+
)
|
1492
|
+
|
1493
|
+
# Fill complete maps
|
1494
|
+
p_map = np.power(10.0, -log_p_map)
|
1495
|
+
|
1496
|
+
# Convert p to z, preserving signs
|
1497
|
+
sign = np.sign(t_map)
|
1498
|
+
sign[sign == 0] = 1
|
1499
|
+
z_map = p_to_z(p_map, tail="two") * sign
|
1500
|
+
|
1501
|
+
log_p_map = _boolean_unmask(log_p_map, voxel_mask)
|
1502
|
+
z_map = _boolean_unmask(z_map, voxel_mask)
|
1503
|
+
|
1504
|
+
else:
|
1505
|
+
n_voxels = self.inputs_["beta_maps"].shape[1]
|
1506
|
+
log_p_map = np.zeros(n_voxels, dtype=float)
|
1507
|
+
z_map = np.zeros(n_voxels, dtype=float)
|
1508
|
+
|
1509
|
+
for bag in self.inputs_["data_bags"]["beta_maps"]:
|
1510
|
+
log_p_map_tmp, t_map_tmp, _, _ = self._fit_model(
|
1511
|
+
self.inputs_["beta_maps"][:, bag["voxel_mask"]], n_perm=n_iters
|
1512
|
+
)
|
1513
|
+
|
1514
|
+
# Fill complete maps
|
1515
|
+
p_map_tmp = np.power(10.0, -log_p_map_tmp)
|
1516
|
+
|
1517
|
+
# Convert p to z, preserving signs
|
1518
|
+
sign = np.sign(t_map_tmp)
|
1519
|
+
sign[sign == 0] = 1
|
1520
|
+
z_map_tmp = p_to_z(p_map_tmp, tail="two") * sign
|
1521
|
+
|
1522
|
+
log_p_map[bag["voxel_mask"]] = log_p_map_tmp.squeeze()
|
1523
|
+
z_map[bag["voxel_mask"]] = z_map_tmp.squeeze()
|
1524
|
+
|
1525
|
+
maps = {"logp_level-voxel": log_p_map, "z_level-voxel": z_map}
|
1526
|
+
description = (
|
1527
|
+
"Family-wise error rate correction was performed using Nilearn's "
|
1528
|
+
"\\citep{10.3389/fninf.2014.00014} permuted OLS method, in which null distributions "
|
1529
|
+
"of test statistics were estimated using the "
|
1530
|
+
"max-value permutation method detailed in \\cite{freedman1983nonstochastic}. "
|
1531
|
+
f"{n_iters} iterations were performed to generate the null distribution."
|
1532
|
+
)
|
1533
|
+
|
1534
|
+
return maps, {}, description
|
1535
|
+
|
1536
|
+
|
1537
|
+
class FixedEffectsHedges(IBMAEstimator):
|
1538
|
+
"""Fixed Effects Hedges meta-regression estimator.
|
1539
|
+
|
1540
|
+
.. versionadded:: 0.4.0
|
1541
|
+
|
1542
|
+
Provides the weighted least-squares estimate of the fixed effects using Hedge's g
|
1543
|
+
as the point estimate and the variance of bias-corrected Cohen's d as the variance
|
1544
|
+
estimate, and given known/assumed between-study variance tau^2.
|
1545
|
+
When tau^2 = 0 (default), the model is the standard inverse-weighted
|
1546
|
+
fixed-effects meta-regression.
|
1547
|
+
|
1548
|
+
This method was described in :footcite:t:`bossier2019`.
|
1549
|
+
|
1550
|
+
Parameters
|
1551
|
+
----------
|
1552
|
+
aggressive_mask : :obj:`bool`, optional
|
1553
|
+
Voxels with a value of zero of NaN in any of the input maps will be removed
|
1554
|
+
from the analysis.
|
1555
|
+
If False, all voxels are included by running a separate analysis on bags
|
1556
|
+
of voxels that belong that have a valid value across the same studies.
|
1557
|
+
Default is True.
|
1558
|
+
tau2 : :obj:`float` or 1D :class:`numpy.ndarray`, optional
|
1559
|
+
Assumed/known value of tau^2. Must be >= 0. Default is 0.
|
1560
|
+
|
1561
|
+
Notes
|
1562
|
+
-----
|
1563
|
+
Requires `t` images and sample size from metadata.
|
1564
|
+
|
1565
|
+
:meth:`fit` produces a :class:`~nimare.results.MetaResult` object with the following maps:
|
1566
|
+
|
1567
|
+
============== ===============================================================================
|
1568
|
+
"z" Z-statistic map from one-sample test.
|
1569
|
+
"p" P-value map from one-sample test.
|
1570
|
+
"est" Fixed effects estimate for intercept test.
|
1571
|
+
"se" Standard error of fixed effects estimate.
|
1572
|
+
"dof" Degrees of freedom map from one-sample test.
|
1573
|
+
============== ===============================================================================
|
1574
|
+
|
1575
|
+
Warnings
|
1576
|
+
--------
|
1577
|
+
Masking approaches which average across voxels (e.g., NiftiLabelsMaskers)
|
1578
|
+
will likely result in biased results. The extent of this bias is currently
|
1579
|
+
unknown.
|
1580
|
+
|
1581
|
+
By default, all image-based meta-analysis estimators adopt an aggressive masking
|
1582
|
+
strategy, in which any voxels with a value of zero in any of the input maps
|
1583
|
+
will be removed from the analysis. Setting ``aggressive_mask=False`` will
|
1584
|
+
instead run tha analysis in bags of voxels that have a valid value across
|
1585
|
+
the same studies.
|
1586
|
+
|
1587
|
+
References
|
1588
|
+
----------
|
1589
|
+
.. footbibliography::
|
1590
|
+
|
1591
|
+
See Also
|
1592
|
+
--------
|
1593
|
+
:class:`pymare.estimators.WeightedLeastSquares`:
|
1594
|
+
The PyMARE estimator called by this class.
|
1595
|
+
"""
|
1596
|
+
|
1597
|
+
_required_inputs = {"t_maps": ("image", "t"), "sample_sizes": ("metadata", "sample_sizes")}
|
1598
|
+
|
1599
|
+
def __init__(self, tau2=0, **kwargs):
|
1600
|
+
super().__init__(**kwargs)
|
1601
|
+
self.tau2 = tau2
|
1602
|
+
|
1603
|
+
def _generate_description(self):
|
1604
|
+
description = (
|
1605
|
+
f"An image-based meta-analysis was performed with NiMARE {__version__} "
|
1606
|
+
"(RRID:SCR_017398; \\citealt{Salo2023}), on "
|
1607
|
+
f"{len(self.inputs_['id'])} t-statistic images using Heges' g as point estimates "
|
1608
|
+
"and the variance of bias-corrected Cohen's in a Weighted Least Squares approach "
|
1609
|
+
"\\citep{brockwell2001comparison,bossier2019}, "
|
1610
|
+
f"with an a priori tau-squared value of {self.tau2} defined across all voxels."
|
1611
|
+
)
|
1612
|
+
return description
|
1613
|
+
|
1614
|
+
def _fit_model(self, t_maps, study_mask=None):
|
1615
|
+
"""Fit the model to the data."""
|
1616
|
+
n_studies, n_voxels = t_maps.shape
|
1617
|
+
|
1618
|
+
if study_mask is None:
|
1619
|
+
# If no mask is provided, assume all studies are included. This is always the case
|
1620
|
+
# when using the aggressive mask.
|
1621
|
+
study_mask = np.arange(n_studies)
|
1622
|
+
|
1623
|
+
sample_sizes = np.array([np.mean(self.inputs_["sample_sizes"][idx]) for idx in study_mask])
|
1624
|
+
n_maps = np.tile(sample_sizes, (n_voxels, 1)).T
|
1625
|
+
|
1626
|
+
# Calculate Hedge's g maps: Standardized mean
|
1627
|
+
cohens_maps = t_to_d(t_maps, n_maps)
|
1628
|
+
hedges_maps, var_hedges_maps = d_to_g(cohens_maps, n_maps, return_variance=True)
|
1629
|
+
|
1630
|
+
del n_maps, sample_sizes, cohens_maps
|
1631
|
+
|
1632
|
+
pymare_dset = pymare.Dataset(y=hedges_maps, v=var_hedges_maps)
|
1633
|
+
est = pymare.estimators.WeightedLeastSquares(tau2=self.tau2)
|
1634
|
+
est.fit_dataset(pymare_dset)
|
1635
|
+
est_summary = est.summary()
|
1636
|
+
|
1637
|
+
fe_stats = est_summary.get_fe_stats()
|
1638
|
+
z_map = fe_stats["z"].squeeze()
|
1639
|
+
p_map = fe_stats["p"].squeeze()
|
1640
|
+
est_map = fe_stats["est"].squeeze()
|
1641
|
+
se_map = fe_stats["se"].squeeze()
|
1642
|
+
dof_map = np.tile(n_studies - 1, n_voxels).astype(np.int32)
|
1643
|
+
|
1644
|
+
return z_map, p_map, est_map, se_map, dof_map
|
1645
|
+
|
1646
|
+
def _fit(self, dataset):
|
1647
|
+
self.dataset = dataset
|
1648
|
+
self.masker = self.masker or dataset.masker
|
1649
|
+
if not isinstance(self.masker, NiftiMasker):
|
1650
|
+
LGR.warning(
|
1651
|
+
f"A {type(self.masker)} mask has been detected. "
|
1652
|
+
"Masks which average across voxels will likely produce biased results when used "
|
1653
|
+
"with this Estimator."
|
1654
|
+
)
|
1655
|
+
|
1656
|
+
if self.aggressive_mask:
|
1657
|
+
voxel_mask = self.inputs_["aggressive_mask"]
|
1658
|
+
result_maps = self._fit_model(self.inputs_["t_maps"][:, voxel_mask])
|
1659
|
+
|
1660
|
+
z_map, p_map, est_map, se_map, dof_map = tuple(
|
1661
|
+
map(lambda x: _boolean_unmask(x, voxel_mask), result_maps)
|
1662
|
+
)
|
1663
|
+
else:
|
1664
|
+
n_voxels = self.inputs_["t_maps"].shape[1]
|
1665
|
+
|
1666
|
+
z_map, p_map, est_map, se_map = [np.zeros(n_voxels, dtype=float) for _ in range(4)]
|
1667
|
+
dof_map = np.zeros(n_voxels, dtype=np.int32)
|
1668
|
+
|
1669
|
+
for bag in self.inputs_["data_bags"]["t_maps"]:
|
1670
|
+
(
|
1671
|
+
z_map[bag["voxel_mask"]],
|
1672
|
+
p_map[bag["voxel_mask"]],
|
1673
|
+
est_map[bag["voxel_mask"]],
|
1674
|
+
se_map[bag["voxel_mask"]],
|
1675
|
+
dof_map[bag["voxel_mask"]],
|
1676
|
+
) = self._fit_model(bag["values"], bag["study_mask"])
|
1677
|
+
|
1678
|
+
# tau2 is a float, not a map, so it can't go into the results dictionary
|
1679
|
+
tables = {"level-estimator": pd.DataFrame(columns=["tau2"], data=[self.tau2])}
|
1680
|
+
maps = {"z": z_map, "p": p_map, "est": est_map, "se": se_map, "dof": dof_map}
|
1681
|
+
description = self._generate_description()
|
1682
|
+
|
1683
|
+
return maps, tables, description
|