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.
Files changed (119) hide show
  1. benchmarks/__init__.py +0 -0
  2. benchmarks/bench_cbma.py +57 -0
  3. nimare/__init__.py +45 -0
  4. nimare/_version.py +21 -0
  5. nimare/annotate/__init__.py +21 -0
  6. nimare/annotate/cogat.py +213 -0
  7. nimare/annotate/gclda.py +924 -0
  8. nimare/annotate/lda.py +147 -0
  9. nimare/annotate/text.py +75 -0
  10. nimare/annotate/utils.py +87 -0
  11. nimare/base.py +217 -0
  12. nimare/cli.py +124 -0
  13. nimare/correct.py +462 -0
  14. nimare/dataset.py +685 -0
  15. nimare/decode/__init__.py +33 -0
  16. nimare/decode/base.py +115 -0
  17. nimare/decode/continuous.py +462 -0
  18. nimare/decode/discrete.py +753 -0
  19. nimare/decode/encode.py +110 -0
  20. nimare/decode/utils.py +44 -0
  21. nimare/diagnostics.py +510 -0
  22. nimare/estimator.py +139 -0
  23. nimare/extract/__init__.py +19 -0
  24. nimare/extract/extract.py +466 -0
  25. nimare/extract/utils.py +295 -0
  26. nimare/generate.py +331 -0
  27. nimare/io.py +667 -0
  28. nimare/meta/__init__.py +39 -0
  29. nimare/meta/cbma/__init__.py +6 -0
  30. nimare/meta/cbma/ale.py +951 -0
  31. nimare/meta/cbma/base.py +947 -0
  32. nimare/meta/cbma/mkda.py +1361 -0
  33. nimare/meta/cbmr.py +970 -0
  34. nimare/meta/ibma.py +1683 -0
  35. nimare/meta/kernel.py +501 -0
  36. nimare/meta/models.py +1199 -0
  37. nimare/meta/utils.py +494 -0
  38. nimare/nimads.py +492 -0
  39. nimare/reports/__init__.py +24 -0
  40. nimare/reports/base.py +664 -0
  41. nimare/reports/default.yml +123 -0
  42. nimare/reports/figures.py +651 -0
  43. nimare/reports/report.tpl +160 -0
  44. nimare/resources/__init__.py +1 -0
  45. nimare/resources/atlases/Harvard-Oxford-LICENSE +93 -0
  46. nimare/resources/atlases/HarvardOxford-cort-maxprob-thr25-2mm.nii.gz +0 -0
  47. nimare/resources/database_file_manifest.json +142 -0
  48. nimare/resources/english_spellings.csv +1738 -0
  49. nimare/resources/filenames.json +32 -0
  50. nimare/resources/neurosynth_laird_studies.json +58773 -0
  51. nimare/resources/neurosynth_stoplist.txt +396 -0
  52. nimare/resources/nidm_pain_dset.json +1349 -0
  53. nimare/resources/references.bib +541 -0
  54. nimare/resources/semantic_knowledge_children.txt +325 -0
  55. nimare/resources/semantic_relatedness_children.txt +249 -0
  56. nimare/resources/templates/MNI152_2x2x2_brainmask.nii.gz +0 -0
  57. nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz +0 -0
  58. nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_desc-brain_mask.nii.gz +0 -0
  59. nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_T1w.nii.gz +0 -0
  60. nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_desc-brain_mask.nii.gz +0 -0
  61. nimare/results.py +225 -0
  62. nimare/stats.py +276 -0
  63. nimare/tests/__init__.py +1 -0
  64. nimare/tests/conftest.py +229 -0
  65. nimare/tests/data/amygdala_roi.nii.gz +0 -0
  66. nimare/tests/data/data-neurosynth_version-7_coordinates.tsv.gz +0 -0
  67. nimare/tests/data/data-neurosynth_version-7_metadata.tsv.gz +0 -0
  68. nimare/tests/data/data-neurosynth_version-7_vocab-terms_source-abstract_type-tfidf_features.npz +0 -0
  69. nimare/tests/data/data-neurosynth_version-7_vocab-terms_vocabulary.txt +100 -0
  70. nimare/tests/data/neurosynth_dset.json +2868 -0
  71. nimare/tests/data/neurosynth_laird_studies.json +58773 -0
  72. nimare/tests/data/nidm_pain_dset.json +1349 -0
  73. nimare/tests/data/nimads_annotation.json +1 -0
  74. nimare/tests/data/nimads_studyset.json +1 -0
  75. nimare/tests/data/test_baseline.txt +2 -0
  76. nimare/tests/data/test_pain_dataset.json +1278 -0
  77. nimare/tests/data/test_pain_dataset_multiple_contrasts.json +1242 -0
  78. nimare/tests/data/test_sleuth_file.txt +18 -0
  79. nimare/tests/data/test_sleuth_file2.txt +10 -0
  80. nimare/tests/data/test_sleuth_file3.txt +5 -0
  81. nimare/tests/data/test_sleuth_file4.txt +5 -0
  82. nimare/tests/data/test_sleuth_file5.txt +5 -0
  83. nimare/tests/test_annotate_cogat.py +32 -0
  84. nimare/tests/test_annotate_gclda.py +86 -0
  85. nimare/tests/test_annotate_lda.py +27 -0
  86. nimare/tests/test_dataset.py +99 -0
  87. nimare/tests/test_decode_continuous.py +132 -0
  88. nimare/tests/test_decode_discrete.py +92 -0
  89. nimare/tests/test_diagnostics.py +168 -0
  90. nimare/tests/test_estimator_performance.py +385 -0
  91. nimare/tests/test_extract.py +46 -0
  92. nimare/tests/test_generate.py +247 -0
  93. nimare/tests/test_io.py +294 -0
  94. nimare/tests/test_meta_ale.py +298 -0
  95. nimare/tests/test_meta_cbmr.py +295 -0
  96. nimare/tests/test_meta_ibma.py +240 -0
  97. nimare/tests/test_meta_kernel.py +209 -0
  98. nimare/tests/test_meta_mkda.py +234 -0
  99. nimare/tests/test_nimads.py +21 -0
  100. nimare/tests/test_reports.py +110 -0
  101. nimare/tests/test_stats.py +101 -0
  102. nimare/tests/test_transforms.py +272 -0
  103. nimare/tests/test_utils.py +200 -0
  104. nimare/tests/test_workflows.py +221 -0
  105. nimare/tests/utils.py +126 -0
  106. nimare/transforms.py +907 -0
  107. nimare/utils.py +1367 -0
  108. nimare/workflows/__init__.py +14 -0
  109. nimare/workflows/base.py +189 -0
  110. nimare/workflows/cbma.py +165 -0
  111. nimare/workflows/ibma.py +108 -0
  112. nimare/workflows/macm.py +77 -0
  113. nimare/workflows/misc.py +65 -0
  114. nimare-0.4.2.dist-info/LICENSE +21 -0
  115. nimare-0.4.2.dist-info/METADATA +124 -0
  116. nimare-0.4.2.dist-info/RECORD +119 -0
  117. nimare-0.4.2.dist-info/WHEEL +5 -0
  118. nimare-0.4.2.dist-info/entry_points.txt +2 -0
  119. 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