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/cbma/base.py
ADDED
@@ -0,0 +1,947 @@
|
|
1
|
+
"""CBMA methods from the ALE and MKDA families."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from abc import abstractmethod
|
5
|
+
|
6
|
+
import nibabel as nib
|
7
|
+
import numpy as np
|
8
|
+
import pandas as pd
|
9
|
+
import sparse
|
10
|
+
from joblib import Memory, Parallel, delayed
|
11
|
+
from nilearn.input_data import NiftiMasker
|
12
|
+
from scipy import ndimage
|
13
|
+
from tqdm.auto import tqdm
|
14
|
+
|
15
|
+
from nimare.estimator import Estimator
|
16
|
+
from nimare.meta.kernel import KernelTransformer
|
17
|
+
from nimare.meta.utils import _calculate_cluster_measures, _get_last_bin
|
18
|
+
from nimare.results import MetaResult
|
19
|
+
from nimare.stats import null_to_p, nullhist_to_p
|
20
|
+
from nimare.transforms import p_to_z
|
21
|
+
from nimare.utils import (
|
22
|
+
_add_metadata_to_dataframe,
|
23
|
+
_check_ncores,
|
24
|
+
_check_type,
|
25
|
+
get_masker,
|
26
|
+
mm2vox,
|
27
|
+
vox2mm,
|
28
|
+
)
|
29
|
+
|
30
|
+
LGR = logging.getLogger(__name__)
|
31
|
+
|
32
|
+
|
33
|
+
class CBMAEstimator(Estimator):
|
34
|
+
"""Base class for coordinate-based meta-analysis methods.
|
35
|
+
|
36
|
+
.. versionchanged:: 0.0.12
|
37
|
+
|
38
|
+
* Remove *low_memory* option
|
39
|
+
* CBMA-specific elements of ``Estimator`` excised and moved into ``CBMAEstimator``.
|
40
|
+
* Generic kwargs and args converted to named kwargs. All remaining kwargs are for kernels.
|
41
|
+
* Use a 4D sparse array for modeled activation maps.
|
42
|
+
|
43
|
+
.. versionchanged:: 0.0.8
|
44
|
+
|
45
|
+
* [REF] Use saved MA maps, when available.
|
46
|
+
* [REF] Add *low_memory* option.
|
47
|
+
|
48
|
+
.. versionadded:: 0.0.3
|
49
|
+
|
50
|
+
Parameters
|
51
|
+
----------
|
52
|
+
kernel_transformer : :obj:`~nimare.meta.kernel.KernelTransformer`, optional
|
53
|
+
Kernel with which to convolve coordinates from dataset. Default is
|
54
|
+
ALEKernel.
|
55
|
+
memory : instance of :class:`joblib.Memory`, :obj:`str`, or :class:`pathlib.Path`
|
56
|
+
Used to cache the output of a function. By default, no caching is done.
|
57
|
+
If a :obj:`str` is given, it is the path to the caching directory.
|
58
|
+
memory_level : :obj:`int`, default=0
|
59
|
+
Rough estimator of the amount of memory used by caching.
|
60
|
+
Higher value means more memory for caching. Zero means no caching.
|
61
|
+
*args
|
62
|
+
Optional arguments to the :obj:`~nimare.base.Estimator` __init__
|
63
|
+
(called automatically).
|
64
|
+
**kwargs
|
65
|
+
Optional keyword arguments to the :obj:`~nimare.base.Estimator`
|
66
|
+
__init__ (called automatically).
|
67
|
+
"""
|
68
|
+
|
69
|
+
# The standard required inputs are just coordinates.
|
70
|
+
# An individual CBMAEstimator may override this.
|
71
|
+
_required_inputs = {"coordinates": ("coordinates", None)}
|
72
|
+
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
kernel_transformer,
|
76
|
+
memory=Memory(location=None, verbose=0),
|
77
|
+
memory_level=0,
|
78
|
+
*,
|
79
|
+
mask=None,
|
80
|
+
**kwargs,
|
81
|
+
):
|
82
|
+
if mask is not None:
|
83
|
+
mask = get_masker(mask, memory=memory, memory_level=memory_level)
|
84
|
+
self.masker = mask
|
85
|
+
|
86
|
+
# Identify any kwargs
|
87
|
+
kernel_args = {k: v for k, v in kwargs.items() if k.startswith("kernel__")}
|
88
|
+
|
89
|
+
# Flag any extraneous kwargs
|
90
|
+
other_kwargs = dict(set(kwargs.items()) - set(kernel_args.items()))
|
91
|
+
if other_kwargs:
|
92
|
+
LGR.warning(f"Unused keyword arguments found: {tuple(other_kwargs.items())}")
|
93
|
+
|
94
|
+
# Get kernel transformer
|
95
|
+
kernel_args = {k.split("kernel__")[1]: v for k, v in kernel_args.items()}
|
96
|
+
if "memory" not in kernel_args.keys() and "memory_level" not in kernel_args.keys():
|
97
|
+
kernel_args.update(memory=memory, memory_level=memory_level)
|
98
|
+
kernel_transformer = _check_type(kernel_transformer, KernelTransformer, **kernel_args)
|
99
|
+
self.kernel_transformer = kernel_transformer
|
100
|
+
|
101
|
+
super().__init__(memory=memory, memory_level=memory_level)
|
102
|
+
|
103
|
+
def _preprocess_input(self, dataset):
|
104
|
+
"""Mask required input images using either the Dataset's mask or the Estimator's.
|
105
|
+
|
106
|
+
Also, insert required metadata into coordinates DataFrame.
|
107
|
+
|
108
|
+
Parameters
|
109
|
+
----------
|
110
|
+
dataset : :obj:`~nimare.dataset.Dataset`
|
111
|
+
In this method, the Dataset is used to (1) select the appropriate mask image,
|
112
|
+
and (2) extract sample size metadata and place it into the coordinates input.
|
113
|
+
|
114
|
+
Attributes
|
115
|
+
----------
|
116
|
+
inputs_ : :obj:`dict`
|
117
|
+
This attribute (created by ``_collect_inputs()``) is updated in this method.
|
118
|
+
Specifically, (1) an "ma_maps" key may be added if pre-generated MA maps are available,
|
119
|
+
(2) IJK coordinates will be added based on the mask image's affine,
|
120
|
+
and (3) sample sizes may be added to the "coordinates" key, as needed.
|
121
|
+
"""
|
122
|
+
masker = self.masker or dataset.masker
|
123
|
+
|
124
|
+
mask_img = masker.mask_img or masker.labels_img
|
125
|
+
if isinstance(mask_img, str):
|
126
|
+
mask_img = nib.load(mask_img)
|
127
|
+
|
128
|
+
for name, (type_, _) in self._required_inputs.items():
|
129
|
+
if type_ == "coordinates":
|
130
|
+
# Calculate IJK matrix indices for target mask
|
131
|
+
# Mask space is assumed to be the same as the Dataset's space
|
132
|
+
# These indices are used directly by any KernelTransformer
|
133
|
+
xyz = self.inputs_["coordinates"][["x", "y", "z"]].values
|
134
|
+
ijk = mm2vox(xyz, mask_img.affine)
|
135
|
+
self.inputs_["coordinates"][["i", "j", "k"]] = ijk
|
136
|
+
|
137
|
+
# All extra (non-ijk) parameters for a kernel should be overrideable as
|
138
|
+
# parameters to __init__, so we can access them with get_params()
|
139
|
+
kt_args = list(self.kernel_transformer.get_params().keys())
|
140
|
+
|
141
|
+
# Integrate "sample_size" from metadata into DataFrame so that
|
142
|
+
# kernel_transformer can access it.
|
143
|
+
if "sample_size" in kt_args:
|
144
|
+
self.inputs_["coordinates"] = _add_metadata_to_dataframe(
|
145
|
+
dataset,
|
146
|
+
self.inputs_["coordinates"],
|
147
|
+
metadata_field="sample_sizes",
|
148
|
+
target_column="sample_size",
|
149
|
+
filter_func=np.mean,
|
150
|
+
)
|
151
|
+
|
152
|
+
def _fit(self, dataset):
|
153
|
+
"""Perform coordinate-based meta-analysis on dataset.
|
154
|
+
|
155
|
+
Parameters
|
156
|
+
----------
|
157
|
+
dataset : :obj:`~nimare.dataset.Dataset`
|
158
|
+
Dataset to analyze.
|
159
|
+
"""
|
160
|
+
self.dataset = dataset
|
161
|
+
self.masker = self.masker or dataset.masker
|
162
|
+
|
163
|
+
if not isinstance(self.masker, NiftiMasker):
|
164
|
+
raise ValueError(
|
165
|
+
f"A {type(self.masker)} mask has been detected. "
|
166
|
+
"Only NiftiMaskers are allowed for this Estimator."
|
167
|
+
)
|
168
|
+
|
169
|
+
self.null_distributions_ = {}
|
170
|
+
|
171
|
+
ma_values = self._collect_ma_maps(
|
172
|
+
coords_key="coordinates",
|
173
|
+
maps_key="ma_maps",
|
174
|
+
)
|
175
|
+
|
176
|
+
# Infer a weight vector, when applicable. Primarily used only for MKDADensity.
|
177
|
+
self.weight_vec_ = self._compute_weights(ma_values)
|
178
|
+
|
179
|
+
stat_values = self._compute_summarystat(ma_values)
|
180
|
+
|
181
|
+
# Determine null distributions for summary stat (OF) to p conversion
|
182
|
+
self._determine_histogram_bins(ma_values)
|
183
|
+
if self.null_method.startswith("approximate"):
|
184
|
+
self._compute_null_approximate(ma_values)
|
185
|
+
|
186
|
+
elif self.null_method == "montecarlo":
|
187
|
+
self._compute_null_montecarlo(n_iters=self.n_iters, n_cores=self.n_cores)
|
188
|
+
|
189
|
+
else:
|
190
|
+
# A hidden option only used for internal validation/testing
|
191
|
+
self._compute_null_reduced_montecarlo(ma_values, n_iters=self.n_iters)
|
192
|
+
|
193
|
+
p_values, z_values = self._summarystat_to_p(stat_values, null_method=self.null_method)
|
194
|
+
|
195
|
+
maps = {"stat": stat_values, "p": p_values, "z": z_values}
|
196
|
+
description = self._generate_description()
|
197
|
+
return maps, {}, description
|
198
|
+
|
199
|
+
def _compute_weights(self, ma_values):
|
200
|
+
"""Perform optional weight computation routine.
|
201
|
+
|
202
|
+
Takes an array of meta-analysis values as input and returns an array
|
203
|
+
of the same shape, weighted as desired.
|
204
|
+
Can be ignored by algorithms that don't support weighting.
|
205
|
+
"""
|
206
|
+
return None
|
207
|
+
|
208
|
+
def _collect_ma_maps(self, coords_key="coordinates", maps_key="ma_maps", return_type="sparse"):
|
209
|
+
"""Collect modeled activation maps from Estimator inputs.
|
210
|
+
|
211
|
+
Parameters
|
212
|
+
----------
|
213
|
+
coords_key : :obj:`str`, optional
|
214
|
+
Key to ``Estimator.inputs_`` dictionary containing coordinates DataFrame.
|
215
|
+
This key should **always** be present.
|
216
|
+
Default is "coordinates".
|
217
|
+
maps_key : :obj:`str`, optional
|
218
|
+
Key to ``Estimator.inputs_`` dictionary containing list of MA map files.
|
219
|
+
This key should only be present if the kernel transformer was already fitted to the
|
220
|
+
input Dataset.
|
221
|
+
Default is "ma_maps".
|
222
|
+
|
223
|
+
Returns
|
224
|
+
-------
|
225
|
+
ma_maps : :obj:`sparse._coo.core.COO`
|
226
|
+
Return a 4D sparse array of shape
|
227
|
+
(n_studies, mask.shape) with MA maps.
|
228
|
+
"""
|
229
|
+
LGR.debug(f"Generating MA maps from coordinates ({coords_key}).")
|
230
|
+
|
231
|
+
ma_maps = self.kernel_transformer.transform(
|
232
|
+
self.inputs_[coords_key],
|
233
|
+
masker=self.masker,
|
234
|
+
return_type=return_type,
|
235
|
+
)
|
236
|
+
|
237
|
+
return ma_maps
|
238
|
+
|
239
|
+
def _compute_summarystat(self, data):
|
240
|
+
"""Compute summary statistics from data.
|
241
|
+
|
242
|
+
The actual summary statistic varies across Estimators, and is implemented in
|
243
|
+
``_compute_summarystat_est``.
|
244
|
+
For ALE and SCALE, the values are known as ALE values.
|
245
|
+
For (M)KDA, they are "OF" scores.
|
246
|
+
|
247
|
+
Parameters
|
248
|
+
----------
|
249
|
+
data : array, sparse._coo.core.COO, pandas.DataFrame, or list of img_like
|
250
|
+
Data from which to estimate summary statistics.
|
251
|
+
The data can be:
|
252
|
+
(1) a 1d contrast-len or 2d contrast-by-voxel array of MA values,
|
253
|
+
(2) a 4d sparse array of MA maps,
|
254
|
+
(3) a DataFrame containing coordinates to produce MA values,
|
255
|
+
or (4) a list of imgs containing MA values.
|
256
|
+
|
257
|
+
Returns
|
258
|
+
-------
|
259
|
+
stat_values : 1d array
|
260
|
+
Summary statistic values. One value per voxel.
|
261
|
+
"""
|
262
|
+
if isinstance(data, pd.DataFrame):
|
263
|
+
ma_values = self.kernel_transformer.transform(
|
264
|
+
data, masker=self.masker, return_type="sparse"
|
265
|
+
)
|
266
|
+
elif isinstance(data, list):
|
267
|
+
ma_values = self.masker.transform(data)
|
268
|
+
elif isinstance(data, (np.ndarray, sparse._coo.core.COO)):
|
269
|
+
ma_values = data
|
270
|
+
else:
|
271
|
+
raise ValueError(f"Unsupported data type '{type(data)}'")
|
272
|
+
|
273
|
+
# Apply weights before returning
|
274
|
+
return self._compute_summarystat_est(ma_values)
|
275
|
+
|
276
|
+
@abstractmethod
|
277
|
+
def _compute_summarystat_est(self, ma_values):
|
278
|
+
"""Compute summary statistic according to estimator-specific method.
|
279
|
+
|
280
|
+
Must be overriden by subclasses.
|
281
|
+
Input and output are both numpy arrays; the output must
|
282
|
+
aggregate over the 0th dimension of the input.
|
283
|
+
(i.e., if the input has K dimensions, the output has K - 1 dimensions.)
|
284
|
+
"""
|
285
|
+
pass
|
286
|
+
|
287
|
+
def _summarystat_to_p(self, stat_values, null_method="approximate"):
|
288
|
+
"""Compute p- and z-values from summary statistics (e.g., ALE scores).
|
289
|
+
|
290
|
+
Uses either histograms from "approximate" null or null distribution from "montecarlo" null.
|
291
|
+
|
292
|
+
Parameters
|
293
|
+
----------
|
294
|
+
stat_values : 1D array_like
|
295
|
+
Array of summary statistic values from estimator.
|
296
|
+
null_method : {"approximate", "montecarlo"}, optional
|
297
|
+
Whether to use approximate null or montecarlo null.
|
298
|
+
Default is "approximate".
|
299
|
+
|
300
|
+
Returns
|
301
|
+
-------
|
302
|
+
p_values, z_values : 1D array
|
303
|
+
P- and Z-values for statistic values.
|
304
|
+
Same shape as stat_values.
|
305
|
+
"""
|
306
|
+
if null_method.startswith("approximate"):
|
307
|
+
assert "histogram_bins" in self.null_distributions_.keys()
|
308
|
+
assert "histweights_corr-none_method-approximate" in self.null_distributions_.keys()
|
309
|
+
|
310
|
+
p_values = nullhist_to_p(
|
311
|
+
stat_values,
|
312
|
+
self.null_distributions_["histweights_corr-none_method-approximate"],
|
313
|
+
self.null_distributions_["histogram_bins"],
|
314
|
+
)
|
315
|
+
|
316
|
+
elif null_method == "montecarlo":
|
317
|
+
assert "histogram_bins" in self.null_distributions_.keys()
|
318
|
+
assert "histweights_corr-none_method-montecarlo" in self.null_distributions_.keys()
|
319
|
+
|
320
|
+
p_values = nullhist_to_p(
|
321
|
+
stat_values,
|
322
|
+
self.null_distributions_["histweights_corr-none_method-montecarlo"],
|
323
|
+
self.null_distributions_["histogram_bins"],
|
324
|
+
)
|
325
|
+
|
326
|
+
elif null_method == "reduced_montecarlo":
|
327
|
+
assert "values_corr-none_method-reducedMontecarlo" in self.null_distributions_.keys()
|
328
|
+
|
329
|
+
p_values = null_to_p(
|
330
|
+
stat_values,
|
331
|
+
self.null_distributions_["values_corr-none_method-reducedMontecarlo"],
|
332
|
+
tail="upper",
|
333
|
+
)
|
334
|
+
|
335
|
+
else:
|
336
|
+
raise ValueError("Argument 'null_method' must be one of: 'approximate', 'montecarlo'.")
|
337
|
+
|
338
|
+
z_values = p_to_z(p_values, tail="one")
|
339
|
+
return p_values, z_values
|
340
|
+
|
341
|
+
def _p_to_summarystat(self, p, null_method=None):
|
342
|
+
"""Compute a summary statistic threshold that corresponds to the provided p-value.
|
343
|
+
|
344
|
+
Uses either histograms from approximate null or null distribution from montecarlo null.
|
345
|
+
|
346
|
+
Parameters
|
347
|
+
----------
|
348
|
+
p : :obj:`float`
|
349
|
+
The p-value that corresponds to the summary statistic threshold.
|
350
|
+
null_method : {None, "approximate", "montecarlo"}, optional
|
351
|
+
Whether to use approximate null or montecarlo null. If None, defaults to using
|
352
|
+
whichever method was set at initialization.
|
353
|
+
|
354
|
+
Returns
|
355
|
+
-------
|
356
|
+
ss : float
|
357
|
+
A float giving the summary statistic value corresponding to the passed p.
|
358
|
+
"""
|
359
|
+
if null_method is None:
|
360
|
+
null_method = self.null_method
|
361
|
+
|
362
|
+
if null_method.startswith("approximate"):
|
363
|
+
assert "histogram_bins" in self.null_distributions_.keys()
|
364
|
+
assert "histweights_corr-none_method-approximate" in self.null_distributions_.keys()
|
365
|
+
|
366
|
+
# Convert unnormalized histogram weights to null distribution
|
367
|
+
histogram_weights = self.null_distributions_[
|
368
|
+
"histweights_corr-none_method-approximate"
|
369
|
+
]
|
370
|
+
null_distribution = histogram_weights / np.sum(histogram_weights)
|
371
|
+
null_distribution = np.cumsum(null_distribution[::-1])[::-1]
|
372
|
+
null_distribution /= np.max(null_distribution)
|
373
|
+
null_distribution = np.squeeze(null_distribution)
|
374
|
+
|
375
|
+
# Desired bin is the first one _before_ the target p-value (for uniformity
|
376
|
+
# with the montecarlo null).
|
377
|
+
ss_idx = np.maximum(0, np.where(null_distribution <= p)[0][0] - 1)
|
378
|
+
ss = self.null_distributions_["histogram_bins"][ss_idx]
|
379
|
+
|
380
|
+
elif null_method == "montecarlo":
|
381
|
+
assert "histogram_bins" in self.null_distributions_.keys()
|
382
|
+
assert "histweights_corr-none_method-montecarlo" in self.null_distributions_.keys()
|
383
|
+
|
384
|
+
hist_weights = self.null_distributions_["histweights_corr-none_method-montecarlo"]
|
385
|
+
# Desired bin is the first one _before_ the target p-value (for uniformity
|
386
|
+
# with the montecarlo null).
|
387
|
+
ss_idx = np.maximum(0, np.where(hist_weights <= p)[0][0] - 1)
|
388
|
+
ss = self.null_distributions_["histogram_bins"][ss_idx]
|
389
|
+
|
390
|
+
elif null_method == "reduced_montecarlo":
|
391
|
+
assert "values_corr-none_method-reducedMontecarlo" in self.null_distributions_.keys()
|
392
|
+
|
393
|
+
null_dist = np.sort(
|
394
|
+
self.null_distributions_["values_corr-none_method-reducedMontecarlo"]
|
395
|
+
)
|
396
|
+
n_vals = len(null_dist)
|
397
|
+
ss_idx = np.floor(p * n_vals).astype(int)
|
398
|
+
ss = null_dist[-ss_idx]
|
399
|
+
|
400
|
+
else:
|
401
|
+
raise ValueError("Argument 'null_method' must be one of: 'approximate', 'montecarlo'.")
|
402
|
+
|
403
|
+
return ss
|
404
|
+
|
405
|
+
def _compute_null_reduced_montecarlo(self, ma_maps, n_iters=5000):
|
406
|
+
"""Compute uncorrected null distribution using the reduced montecarlo method.
|
407
|
+
|
408
|
+
This method is much faster than the full montecarlo approach, but is still slower than the
|
409
|
+
approximate method. Given that its resolution is roughly the same as the approximate
|
410
|
+
method, we recommend against using this method.
|
411
|
+
|
412
|
+
Parameters
|
413
|
+
----------
|
414
|
+
ma_maps : (C x V) array
|
415
|
+
Contrast by voxel array of MA values, after weighting with weight_vec.
|
416
|
+
|
417
|
+
Notes
|
418
|
+
-----
|
419
|
+
This method adds one entry to the null_distributions_ dict attribute:
|
420
|
+
"values_corr-none_method-reducedMontecarlo".
|
421
|
+
|
422
|
+
Warnings
|
423
|
+
--------
|
424
|
+
This method is only retained for testing and algorithm development.
|
425
|
+
"""
|
426
|
+
if isinstance(ma_maps, sparse._coo.core.COO):
|
427
|
+
masker = self.dataset.masker if not self.masker else self.masker
|
428
|
+
mask = masker.mask_img
|
429
|
+
mask_data = mask.get_fdata().astype(bool)
|
430
|
+
|
431
|
+
ma_maps = ma_maps.todense()
|
432
|
+
ma_maps = ma_maps[:, mask_data]
|
433
|
+
|
434
|
+
n_studies, n_voxels = ma_maps.shape
|
435
|
+
null_ijk = np.random.choice(np.arange(n_voxels), (n_iters, n_studies))
|
436
|
+
iter_ma_values = ma_maps[np.arange(n_studies), tuple(null_ijk)].T
|
437
|
+
null_dist = self._compute_summarystat(iter_ma_values)
|
438
|
+
self.null_distributions_["values_corr-none_method-reducedMontecarlo"] = null_dist
|
439
|
+
|
440
|
+
def _compute_null_montecarlo_permutation(self, iter_xyz, iter_df):
|
441
|
+
"""Run a single Monte Carlo permutation of a dataset.
|
442
|
+
|
443
|
+
Does the shared work between uncorrected stat-to-p conversion and vFWE.
|
444
|
+
|
445
|
+
Parameters
|
446
|
+
----------
|
447
|
+
params : tuple
|
448
|
+
A tuple containing 2 elements, respectively providing (1) the permuted
|
449
|
+
coordinates and (2) the original coordinate DataFrame.
|
450
|
+
|
451
|
+
Returns
|
452
|
+
-------
|
453
|
+
counts : 1D array_like
|
454
|
+
Weights associated with the attribute `null_distributions_["histogram_bins"]`.
|
455
|
+
"""
|
456
|
+
# Not sure if joblib will automatically use a copy of the object, but I'll make a copy to
|
457
|
+
# be safe.
|
458
|
+
iter_df = iter_df.copy()
|
459
|
+
|
460
|
+
iter_xyz = np.squeeze(iter_xyz)
|
461
|
+
iter_df[["x", "y", "z"]] = iter_xyz
|
462
|
+
|
463
|
+
iter_ma_maps = self.kernel_transformer.transform(
|
464
|
+
iter_df, masker=self.masker, return_type="sparse"
|
465
|
+
)
|
466
|
+
iter_ss_map = self._compute_summarystat(iter_ma_maps)
|
467
|
+
|
468
|
+
del iter_ma_maps
|
469
|
+
|
470
|
+
# Get bin edges for histogram
|
471
|
+
bin_centers = self.null_distributions_["histogram_bins"]
|
472
|
+
step_size = bin_centers[1] - bin_centers[0]
|
473
|
+
bin_edges = bin_centers - (step_size / 2)
|
474
|
+
bin_edges = np.append(bin_centers, bin_centers[-1] + step_size)
|
475
|
+
|
476
|
+
counts, _ = np.histogram(iter_ss_map, bins=bin_edges, density=False)
|
477
|
+
return counts
|
478
|
+
|
479
|
+
def _compute_null_montecarlo(self, n_iters, n_cores):
|
480
|
+
"""Compute uncorrected null distribution using Monte Carlo method.
|
481
|
+
|
482
|
+
Parameters
|
483
|
+
----------
|
484
|
+
n_iters : int
|
485
|
+
Number of permutations.
|
486
|
+
n_cores : int
|
487
|
+
Number of cores to use.
|
488
|
+
|
489
|
+
Notes
|
490
|
+
-----
|
491
|
+
This method adds two entries to the null_distributions_ dict attribute:
|
492
|
+
"histweights_corr-none_method-montecarlo" and
|
493
|
+
"histweights_level-voxel_corr-fwe_method-montecarlo".
|
494
|
+
"""
|
495
|
+
null_ijk = np.vstack(np.where(self.masker.mask_img.get_fdata())).T
|
496
|
+
|
497
|
+
n_cores = _check_ncores(n_cores)
|
498
|
+
|
499
|
+
rand_idx = np.random.choice(
|
500
|
+
null_ijk.shape[0],
|
501
|
+
size=(self.inputs_["coordinates"].shape[0], n_iters),
|
502
|
+
)
|
503
|
+
rand_ijk = null_ijk[rand_idx, :]
|
504
|
+
rand_xyz = vox2mm(rand_ijk, self.masker.mask_img.affine)
|
505
|
+
iter_xyzs = np.split(rand_xyz, rand_xyz.shape[1], axis=1)
|
506
|
+
iter_df = self.inputs_["coordinates"].copy()
|
507
|
+
|
508
|
+
perm_histograms = [
|
509
|
+
r
|
510
|
+
for r in tqdm(
|
511
|
+
Parallel(return_as="generator", n_jobs=n_cores)(
|
512
|
+
delayed(self._compute_null_montecarlo_permutation)(
|
513
|
+
iter_xyzs[i_iter], iter_df=iter_df
|
514
|
+
)
|
515
|
+
for i_iter in range(n_iters)
|
516
|
+
),
|
517
|
+
total=n_iters,
|
518
|
+
)
|
519
|
+
]
|
520
|
+
|
521
|
+
perm_histograms = np.vstack(perm_histograms)
|
522
|
+
self.null_distributions_["histweights_corr-none_method-montecarlo"] = np.sum(
|
523
|
+
perm_histograms, axis=0
|
524
|
+
)
|
525
|
+
|
526
|
+
fwe_voxel_max = np.apply_along_axis(_get_last_bin, 1, perm_histograms)
|
527
|
+
histweights = np.zeros(perm_histograms.shape[1], dtype=perm_histograms.dtype)
|
528
|
+
for perm in fwe_voxel_max:
|
529
|
+
histweights[perm] += 1
|
530
|
+
|
531
|
+
self.null_distributions_["histweights_level-voxel_corr-fwe_method-montecarlo"] = (
|
532
|
+
histweights
|
533
|
+
)
|
534
|
+
|
535
|
+
def _correct_fwe_montecarlo_permutation(
|
536
|
+
self,
|
537
|
+
iter_xyz,
|
538
|
+
iter_df,
|
539
|
+
conn,
|
540
|
+
voxel_thresh,
|
541
|
+
vfwe_only,
|
542
|
+
):
|
543
|
+
"""Run a single Monte Carlo permutation of a dataset.
|
544
|
+
|
545
|
+
Does the shared work between vFWE and cFWE.
|
546
|
+
|
547
|
+
Parameters
|
548
|
+
----------
|
549
|
+
iter_xyz : :obj:`numpy.ndarray` of shape (C, 3)
|
550
|
+
The permuted coordinates. One row for each peak.
|
551
|
+
Columns correspond to x, y, and z coordinates.
|
552
|
+
iter_df : :obj:`pandas.DataFrame`
|
553
|
+
The coordinates DataFrame, to be filled with the permuted coordinates in ``iter_xyz``
|
554
|
+
before permutation MA maps are generated.
|
555
|
+
conn : :obj:`numpy.ndarray` of shape (3, 3, 3)
|
556
|
+
The 3D structuring array for labeling clusters.
|
557
|
+
voxel_thresh : :obj:`float`
|
558
|
+
Uncorrected summary statistic threshold for defining clusters.
|
559
|
+
vfwe_only : :obj:`bool`
|
560
|
+
If True, only calculate the voxel-level FWE-corrected maps.
|
561
|
+
|
562
|
+
Returns
|
563
|
+
-------
|
564
|
+
(iter_max value, iter_max_cluster, iter_max_mass)
|
565
|
+
A 3-tuple of floats giving the maximum voxel-wise value, maximum cluster size,
|
566
|
+
and maximum cluster mass for the permuted dataset.
|
567
|
+
If ``vfwe_only`` is True, the latter two values will be None.
|
568
|
+
"""
|
569
|
+
iter_df = iter_df.copy()
|
570
|
+
|
571
|
+
iter_xyz = np.squeeze(iter_xyz)
|
572
|
+
iter_df[["x", "y", "z"]] = iter_xyz
|
573
|
+
|
574
|
+
iter_ma_maps = self.kernel_transformer.transform(
|
575
|
+
iter_df, masker=self.masker, return_type="sparse"
|
576
|
+
)
|
577
|
+
iter_ss_map = self._compute_summarystat(iter_ma_maps)
|
578
|
+
|
579
|
+
del iter_ma_maps
|
580
|
+
|
581
|
+
# Voxel-level inference
|
582
|
+
iter_max_value = np.max(iter_ss_map)
|
583
|
+
|
584
|
+
if vfwe_only:
|
585
|
+
iter_max_size, iter_max_mass = None, None
|
586
|
+
else:
|
587
|
+
# Cluster-level inference
|
588
|
+
iter_ss_map = self.masker.inverse_transform(iter_ss_map).get_fdata()
|
589
|
+
iter_max_size, iter_max_mass = _calculate_cluster_measures(
|
590
|
+
iter_ss_map, voxel_thresh, conn, tail="upper"
|
591
|
+
)
|
592
|
+
return iter_max_value, iter_max_size, iter_max_mass
|
593
|
+
|
594
|
+
def correct_fwe_montecarlo(
|
595
|
+
self,
|
596
|
+
result,
|
597
|
+
voxel_thresh=0.001,
|
598
|
+
n_iters=5000,
|
599
|
+
n_cores=1,
|
600
|
+
vfwe_only=False,
|
601
|
+
):
|
602
|
+
"""Perform FWE correction using the max-value permutation method.
|
603
|
+
|
604
|
+
Only call this method from within a Corrector.
|
605
|
+
|
606
|
+
.. versionchanged:: 0.0.13
|
607
|
+
|
608
|
+
Change cluster neighborhood from faces+edges to faces, to match Nilearn.
|
609
|
+
|
610
|
+
.. versionchanged:: 0.0.12
|
611
|
+
|
612
|
+
* Fix the ``vfwe_only`` option.
|
613
|
+
|
614
|
+
.. versionchanged:: 0.0.11
|
615
|
+
|
616
|
+
* Rename ``*_level-cluster`` maps to ``*_desc-size_level-cluster``.
|
617
|
+
* Add new ``*_desc-mass_level-cluster`` maps that use cluster mass-based inference.
|
618
|
+
|
619
|
+
Parameters
|
620
|
+
----------
|
621
|
+
result : :obj:`~nimare.results.MetaResult`
|
622
|
+
Result object from a CBMA meta-analysis.
|
623
|
+
voxel_thresh : :obj:`float`, default=0.001
|
624
|
+
Cluster-defining p-value threshold. Default is 0.001.
|
625
|
+
n_iters : :obj:`int`, default=5000
|
626
|
+
Number of iterations to build the voxel-level, cluster-size, and cluster-mass FWE
|
627
|
+
null distributions. Default is 5000.
|
628
|
+
n_cores : :obj:`int`, default=1
|
629
|
+
Number of cores to use for parallelization.
|
630
|
+
If <=0, defaults to using all available cores. Default is 1.
|
631
|
+
vfwe_only : :obj:`bool`, default=False
|
632
|
+
If True, only calculate the voxel-level FWE-corrected maps. Voxel-level correction
|
633
|
+
can be performed very quickly if the Estimator's ``null_method`` was "montecarlo".
|
634
|
+
Default is False.
|
635
|
+
|
636
|
+
Returns
|
637
|
+
-------
|
638
|
+
images : :obj:`dict`
|
639
|
+
Dictionary of 1D arrays corresponding to masked images generated by
|
640
|
+
the correction procedure. The following arrays are generated by
|
641
|
+
this method:
|
642
|
+
|
643
|
+
- ``logp_desc-size_level-cluster``: Cluster-level FWE-corrected ``-log10(p)`` map
|
644
|
+
based on cluster size. This was previously simply called "logp_level-cluster".
|
645
|
+
This array is **not** generated if ``vfwe_only`` is ``True``.
|
646
|
+
- ``logp_desc-mass_level-cluster``: Cluster-level FWE-corrected ``-log10(p)`` map
|
647
|
+
based on cluster mass. According to :footcite:t:`bullmore1999global` and
|
648
|
+
:footcite:t:`zhang2009cluster`, cluster mass-based inference is more powerful than
|
649
|
+
cluster size.
|
650
|
+
This array is **not** generated if ``vfwe_only`` is ``True``.
|
651
|
+
- ``logp_level-voxel``: Voxel-level FWE-corrected ``-log10(p)`` map.
|
652
|
+
Voxel-level correction is generally more conservative than cluster-level
|
653
|
+
correction, so it is only recommended for very large meta-analyses
|
654
|
+
(i.e., hundreds of studies), per :footcite:t:`eickhoff2016behavior`.
|
655
|
+
description_ : :obj:`str`
|
656
|
+
A text description of the correction procedure.
|
657
|
+
|
658
|
+
Notes
|
659
|
+
-----
|
660
|
+
If ``vfwe_only`` is ``False``, this method adds three new keys to the
|
661
|
+
``null_distributions_`` attribute:
|
662
|
+
|
663
|
+
- ``values_level-voxel_corr-fwe_method-montecarlo``: The maximum summary statistic
|
664
|
+
value from each Monte Carlo iteration. An array of shape (n_iters,).
|
665
|
+
- ``values_desc-size_level-cluster_corr-fwe_method-montecarlo``: The maximum cluster
|
666
|
+
size from each Monte Carlo iteration. An array of shape (n_iters,).
|
667
|
+
- ``values_desc-mass_level-cluster_corr-fwe_method-montecarlo``: The maximum cluster
|
668
|
+
mass from each Monte Carlo iteration. An array of shape (n_iters,).
|
669
|
+
|
670
|
+
See Also
|
671
|
+
--------
|
672
|
+
nimare.correct.FWECorrector : The Corrector from which to call this method.
|
673
|
+
|
674
|
+
References
|
675
|
+
----------
|
676
|
+
.. footbibliography::
|
677
|
+
|
678
|
+
Examples
|
679
|
+
--------
|
680
|
+
>>> meta = MKDADensity()
|
681
|
+
>>> result = meta.fit(dset)
|
682
|
+
>>> corrector = FWECorrector(method='montecarlo', voxel_thresh=0.01,
|
683
|
+
n_iters=5, n_cores=1)
|
684
|
+
>>> cresult = corrector.transform(result)
|
685
|
+
"""
|
686
|
+
stat_values = result.get_map("stat", return_type="array")
|
687
|
+
|
688
|
+
if vfwe_only and (self.null_method == "montecarlo"):
|
689
|
+
LGR.info("Using precalculated histogram for voxel-level FWE correction.")
|
690
|
+
|
691
|
+
# Determine p- and z-values from stat values and null distribution.
|
692
|
+
p_vfwe_values = nullhist_to_p(
|
693
|
+
stat_values,
|
694
|
+
self.null_distributions_["histweights_level-voxel_corr-fwe_method-montecarlo"],
|
695
|
+
self.null_distributions_["histogram_bins"],
|
696
|
+
)
|
697
|
+
|
698
|
+
else:
|
699
|
+
if vfwe_only:
|
700
|
+
LGR.warn(
|
701
|
+
"In order to run this method with the 'vfwe_only' option, "
|
702
|
+
"the Estimator must use the 'montecarlo' null_method. "
|
703
|
+
"Running permutations from scratch."
|
704
|
+
)
|
705
|
+
|
706
|
+
null_xyz = vox2mm(
|
707
|
+
np.vstack(np.where(self.masker.mask_img.get_fdata())).T,
|
708
|
+
self.masker.mask_img.affine,
|
709
|
+
)
|
710
|
+
|
711
|
+
n_cores = _check_ncores(n_cores)
|
712
|
+
|
713
|
+
# Identify summary statistic corresponding to intensity threshold
|
714
|
+
ss_thresh = self._p_to_summarystat(voxel_thresh)
|
715
|
+
|
716
|
+
rand_idx = np.random.choice(
|
717
|
+
null_xyz.shape[0],
|
718
|
+
size=(self.inputs_["coordinates"].shape[0], n_iters),
|
719
|
+
)
|
720
|
+
rand_xyz = null_xyz[rand_idx, :]
|
721
|
+
iter_xyzs = np.split(rand_xyz, rand_xyz.shape[1], axis=1)
|
722
|
+
iter_df = self.inputs_["coordinates"].copy()
|
723
|
+
|
724
|
+
# Define connectivity matrix for cluster labeling
|
725
|
+
conn = ndimage.generate_binary_structure(rank=3, connectivity=1)
|
726
|
+
|
727
|
+
perm_results = [
|
728
|
+
r
|
729
|
+
for r in tqdm(
|
730
|
+
Parallel(return_as="generator", n_jobs=n_cores)(
|
731
|
+
delayed(self._correct_fwe_montecarlo_permutation)(
|
732
|
+
iter_xyzs[i_iter],
|
733
|
+
iter_df=iter_df,
|
734
|
+
conn=conn,
|
735
|
+
voxel_thresh=ss_thresh,
|
736
|
+
vfwe_only=vfwe_only,
|
737
|
+
)
|
738
|
+
for i_iter in range(n_iters)
|
739
|
+
),
|
740
|
+
total=n_iters,
|
741
|
+
)
|
742
|
+
]
|
743
|
+
|
744
|
+
fwe_voxel_max, fwe_cluster_size_max, fwe_cluster_mass_max = zip(*perm_results)
|
745
|
+
|
746
|
+
if not vfwe_only:
|
747
|
+
# Cluster-level FWE
|
748
|
+
# Extract the summary statistics in voxel-wise (3D) form, threshold, and
|
749
|
+
# cluster-label
|
750
|
+
thresh_stat_values = self.masker.inverse_transform(stat_values).get_fdata()
|
751
|
+
thresh_stat_values[thresh_stat_values <= ss_thresh] = 0
|
752
|
+
labeled_matrix, _ = ndimage.label(thresh_stat_values, conn)
|
753
|
+
|
754
|
+
cluster_labels, idx, cluster_sizes = np.unique(
|
755
|
+
labeled_matrix,
|
756
|
+
return_inverse=True,
|
757
|
+
return_counts=True,
|
758
|
+
)
|
759
|
+
assert cluster_labels[0] == 0
|
760
|
+
|
761
|
+
# Cluster mass-based inference
|
762
|
+
cluster_masses = np.zeros(cluster_labels.shape)
|
763
|
+
for i_val in cluster_labels:
|
764
|
+
if i_val == 0:
|
765
|
+
cluster_masses[i_val] = 0
|
766
|
+
|
767
|
+
cluster_mass = np.sum(thresh_stat_values[labeled_matrix == i_val] - ss_thresh)
|
768
|
+
cluster_masses[i_val] = cluster_mass
|
769
|
+
|
770
|
+
p_cmfwe_vals = null_to_p(cluster_masses, fwe_cluster_mass_max, "upper")
|
771
|
+
p_cmfwe_map = p_cmfwe_vals[np.reshape(idx, labeled_matrix.shape)]
|
772
|
+
|
773
|
+
p_cmfwe_values = np.squeeze(
|
774
|
+
self.masker.transform(
|
775
|
+
nib.Nifti1Image(p_cmfwe_map, self.masker.mask_img.affine)
|
776
|
+
)
|
777
|
+
)
|
778
|
+
logp_cmfwe_values = -np.log10(p_cmfwe_values)
|
779
|
+
logp_cmfwe_values[np.isinf(logp_cmfwe_values)] = -np.log10(np.finfo(float).eps)
|
780
|
+
z_cmfwe_values = p_to_z(p_cmfwe_values, tail="one")
|
781
|
+
|
782
|
+
# Cluster size-based inference
|
783
|
+
cluster_sizes[0] = 0 # replace background's "cluster size" with zeros
|
784
|
+
p_csfwe_vals = null_to_p(cluster_sizes, fwe_cluster_size_max, "upper")
|
785
|
+
p_csfwe_map = p_csfwe_vals[np.reshape(idx, labeled_matrix.shape)]
|
786
|
+
|
787
|
+
p_csfwe_values = np.squeeze(
|
788
|
+
self.masker.transform(
|
789
|
+
nib.Nifti1Image(p_csfwe_map, self.masker.mask_img.affine)
|
790
|
+
)
|
791
|
+
)
|
792
|
+
logp_csfwe_values = -np.log10(p_csfwe_values)
|
793
|
+
logp_csfwe_values[np.isinf(logp_csfwe_values)] = -np.log10(np.finfo(float).eps)
|
794
|
+
z_csfwe_values = p_to_z(p_csfwe_values, tail="one")
|
795
|
+
|
796
|
+
self.null_distributions_[
|
797
|
+
"values_desc-size_level-cluster_corr-fwe_method-montecarlo"
|
798
|
+
] = fwe_cluster_size_max
|
799
|
+
self.null_distributions_[
|
800
|
+
"values_desc-mass_level-cluster_corr-fwe_method-montecarlo"
|
801
|
+
] = fwe_cluster_mass_max
|
802
|
+
|
803
|
+
# Voxel-level FWE
|
804
|
+
LGR.info("Using null distribution for voxel-level FWE correction.")
|
805
|
+
p_vfwe_values = null_to_p(stat_values, fwe_voxel_max, tail="upper")
|
806
|
+
self.null_distributions_["values_level-voxel_corr-fwe_method-montecarlo"] = (
|
807
|
+
fwe_voxel_max
|
808
|
+
)
|
809
|
+
|
810
|
+
z_vfwe_values = p_to_z(p_vfwe_values, tail="one")
|
811
|
+
logp_vfwe_values = -np.log10(p_vfwe_values)
|
812
|
+
logp_vfwe_values[np.isinf(logp_vfwe_values)] = -np.log10(np.finfo(float).eps)
|
813
|
+
|
814
|
+
if vfwe_only:
|
815
|
+
# Return unthresholded value images
|
816
|
+
maps = {
|
817
|
+
"logp_level-voxel": logp_vfwe_values,
|
818
|
+
"z_level-voxel": z_vfwe_values,
|
819
|
+
}
|
820
|
+
|
821
|
+
else:
|
822
|
+
# Return unthresholded value images
|
823
|
+
maps = {
|
824
|
+
"logp_level-voxel": logp_vfwe_values,
|
825
|
+
"z_level-voxel": z_vfwe_values,
|
826
|
+
"logp_desc-size_level-cluster": logp_csfwe_values,
|
827
|
+
"z_desc-size_level-cluster": z_csfwe_values,
|
828
|
+
"logp_desc-mass_level-cluster": logp_cmfwe_values,
|
829
|
+
"z_desc-mass_level-cluster": z_cmfwe_values,
|
830
|
+
}
|
831
|
+
|
832
|
+
if vfwe_only:
|
833
|
+
description = (
|
834
|
+
"Family-wise error correction was performed using a voxel-level Monte Carlo "
|
835
|
+
"procedure. "
|
836
|
+
"In this procedure, null datasets are generated in which dataset coordinates are "
|
837
|
+
"substituted with coordinates randomly drawn from the meta-analysis mask, and "
|
838
|
+
"the maximum summary statistic is retained. "
|
839
|
+
f"This procedure was repeated {n_iters} times to build a null distribution of "
|
840
|
+
"summary statistics."
|
841
|
+
)
|
842
|
+
else:
|
843
|
+
description = (
|
844
|
+
"Family-wise error rate correction was performed using a Monte Carlo procedure. "
|
845
|
+
"In this procedure, null datasets are generated in which dataset coordinates are "
|
846
|
+
"substituted with coordinates randomly drawn from the meta-analysis mask, and "
|
847
|
+
"maximum values are retained. "
|
848
|
+
f"This procedure was repeated {n_iters} times to build null distributions of "
|
849
|
+
"summary statistics, cluster sizes, and cluster masses. "
|
850
|
+
"Clusters for cluster-level correction were defined using edge-wise connectivity "
|
851
|
+
f"and a voxel-level threshold of p < {voxel_thresh} from the uncorrected null "
|
852
|
+
"distribution."
|
853
|
+
)
|
854
|
+
|
855
|
+
return maps, {}, description
|
856
|
+
|
857
|
+
|
858
|
+
class PairwiseCBMAEstimator(CBMAEstimator):
|
859
|
+
"""Base class for pairwise coordinate-based meta-analysis methods.
|
860
|
+
|
861
|
+
.. versionchanged:: 0.0.12
|
862
|
+
|
863
|
+
- Use a 4D sparse array for modeled activation maps.
|
864
|
+
|
865
|
+
.. versionchanged:: 0.0.8
|
866
|
+
|
867
|
+
* [REF] Use saved MA maps, when available.
|
868
|
+
|
869
|
+
.. versionadded:: 0.0.3
|
870
|
+
|
871
|
+
Parameters
|
872
|
+
----------
|
873
|
+
kernel_transformer : :obj:`~nimare.meta.kernel.KernelTransformer`, optional
|
874
|
+
Kernel with which to convolve coordinates from dataset. Default is
|
875
|
+
ALEKernel.
|
876
|
+
memory : instance of :class:`joblib.Memory`, :obj:`str`, or :class:`pathlib.Path`
|
877
|
+
Used to cache the output of a function. By default, no caching is done.
|
878
|
+
If a :obj:`str` is given, it is the path to the caching directory.
|
879
|
+
memory_level : :obj:`int`, default=0
|
880
|
+
Rough estimator of the amount of memory used by caching.
|
881
|
+
Higher value means more memory for caching. Zero means no caching.
|
882
|
+
*args
|
883
|
+
Optional arguments to the :obj:`~nimare.base.Estimator` __init__
|
884
|
+
(called automatically).
|
885
|
+
**kwargs
|
886
|
+
Optional keyword arguments to the :obj:`~nimare.base.Estimator`
|
887
|
+
__init__ (called automatically).
|
888
|
+
"""
|
889
|
+
|
890
|
+
def _compute_summarystat_est(self, ma_values):
|
891
|
+
"""Calculate the Estimator's summary statistic.
|
892
|
+
|
893
|
+
This method is only included because CBMAEstimator has it as an abstract method.
|
894
|
+
PairwiseCBMAEstimators are not constructed uniformly enough for this structure to work
|
895
|
+
consistently.
|
896
|
+
"""
|
897
|
+
raise NotImplementedError
|
898
|
+
|
899
|
+
def fit(self, dataset1, dataset2, drop_invalid=True):
|
900
|
+
"""Fit Estimator to two Datasets.
|
901
|
+
|
902
|
+
Parameters
|
903
|
+
----------
|
904
|
+
dataset1/dataset2 : :obj:`~nimare.dataset.Dataset`
|
905
|
+
Dataset objects to analyze.
|
906
|
+
|
907
|
+
Returns
|
908
|
+
-------
|
909
|
+
:obj:`~nimare.results.MetaResult`
|
910
|
+
Results of Estimator fitting.
|
911
|
+
|
912
|
+
Notes
|
913
|
+
-----
|
914
|
+
The `fit` method is a light wrapper that runs input validation and
|
915
|
+
preprocessing before fitting the actual model. Estimators' individual
|
916
|
+
"fitting" methods are implemented as `_fit`, although users should
|
917
|
+
call `fit`.
|
918
|
+
"""
|
919
|
+
# Reproduce fit() for dataset1 to collect and process inputs.
|
920
|
+
self._collect_inputs(dataset1, drop_invalid=drop_invalid)
|
921
|
+
self._preprocess_input(dataset1)
|
922
|
+
if "ma_maps" in self.inputs_.keys():
|
923
|
+
# Grab pre-generated MA maps
|
924
|
+
self.inputs_["ma_maps1"] = self.inputs_.pop("ma_maps")
|
925
|
+
|
926
|
+
self.inputs_["id1"] = self.inputs_.pop("id")
|
927
|
+
self.inputs_["coordinates1"] = self.inputs_.pop("coordinates")
|
928
|
+
|
929
|
+
# Reproduce fit() for dataset2 to collect and process inputs.
|
930
|
+
self._collect_inputs(dataset2, drop_invalid=drop_invalid)
|
931
|
+
self._preprocess_input(dataset2)
|
932
|
+
if "ma_maps" in self.inputs_.keys():
|
933
|
+
# Grab pre-generated MA maps
|
934
|
+
self.inputs_["ma_maps2"] = self.inputs_.pop("ma_maps")
|
935
|
+
|
936
|
+
self.inputs_["id2"] = self.inputs_.pop("id")
|
937
|
+
self.inputs_["coordinates2"] = self.inputs_.pop("coordinates")
|
938
|
+
|
939
|
+
# Now run the Estimator-specific _fit() method.
|
940
|
+
maps, tables, description = self._cache(self._fit, func_memory_level=1)(dataset1, dataset2)
|
941
|
+
|
942
|
+
if hasattr(self, "masker") and self.masker is not None:
|
943
|
+
masker = self.masker
|
944
|
+
else:
|
945
|
+
masker = dataset1.masker
|
946
|
+
|
947
|
+
return MetaResult(self, mask=masker, maps=maps, tables=tables, description=description)
|