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
@@ -0,0 +1,33 @@
1
+ """Functional decoding tools."""
2
+
3
+ from . import continuous, discrete, encode
4
+ from .continuous import (
5
+ CorrelationDecoder,
6
+ CorrelationDistributionDecoder,
7
+ gclda_decode_map,
8
+ )
9
+ from .discrete import (
10
+ BrainMapDecoder,
11
+ NeurosynthDecoder,
12
+ ROIAssociationDecoder,
13
+ brainmap_decode,
14
+ gclda_decode_roi,
15
+ neurosynth_decode,
16
+ )
17
+ from .encode import gclda_encode
18
+
19
+ __all__ = [
20
+ "CorrelationDecoder",
21
+ "CorrelationDistributionDecoder",
22
+ "gclda_decode_map",
23
+ "BrainMapDecoder",
24
+ "NeurosynthDecoder",
25
+ "ROIAssociationDecoder",
26
+ "brainmap_decode",
27
+ "gclda_decode_roi",
28
+ "neurosynth_decode",
29
+ "gclda_encode",
30
+ "continuous",
31
+ "discrete",
32
+ "encode",
33
+ ]
nimare/decode/base.py ADDED
@@ -0,0 +1,115 @@
1
+ """Base classes for the decode module."""
2
+
3
+ import logging
4
+ from abc import abstractmethod
5
+
6
+ from nimare.base import NiMAREBase
7
+
8
+ LGR = logging.getLogger(__name__)
9
+
10
+
11
+ class Decoder(NiMAREBase):
12
+ """Base class for decoders in :mod:`~nimare.decode`.
13
+
14
+ .. versionchanged:: 0.0.12
15
+
16
+ Moved from ``nimare.base`` to ``nimare.decode.base``.
17
+
18
+ .. versionadded:: 0.0.3
19
+
20
+ """
21
+
22
+ __id_cols = ["id", "study_id", "contrast_id"]
23
+
24
+ def _collect_inputs(self, dataset, drop_invalid=True):
25
+ """Search for, and validate, required inputs as necessary."""
26
+ if not hasattr(dataset, "slice"):
27
+ raise ValueError(
28
+ f"Argument 'dataset' must be a valid Dataset object, not a {type(dataset)}."
29
+ )
30
+
31
+ if self._required_inputs:
32
+ data = dataset.get(self._required_inputs, drop_invalid=drop_invalid)
33
+ # Do not overwrite existing inputs_ attribute.
34
+ # This is necessary for PairwiseCBMAEstimator, which validates two sets of coordinates
35
+ # in the same object.
36
+ # It makes the *strong* assumption that required inputs will not changes within an
37
+ # Estimator across fit calls, so all fields of inputs_ will be overwritten instead of
38
+ # retaining outdated fields from previous fit calls.
39
+ if not hasattr(self, "inputs_"):
40
+ self.inputs_ = {}
41
+
42
+ for k, v in data.items():
43
+ if v is None:
44
+ raise ValueError(
45
+ f"Estimator {self.__class__.__name__} requires input dataset to contain "
46
+ f"{k}, but no matching data were found."
47
+ )
48
+ self.inputs_[k] = v
49
+
50
+ def _preprocess_input(self, dataset):
51
+ """Select features for model based on requested features and feature_group.
52
+
53
+ This also takes into account which features have at least one study in the
54
+ Dataset with the feature.
55
+ """
56
+ # Reduce feature list as desired
57
+ if self.feature_group is not None:
58
+ if not self.feature_group.endswith("__"):
59
+ self.feature_group += "__"
60
+ feature_names = self.inputs_["annotations"].columns.values
61
+ feature_names = [f for f in feature_names if f.startswith(self.feature_group)]
62
+ if self.features is not None:
63
+ features = [f.split("__")[-1] for f in feature_names if f in self.features]
64
+ else:
65
+ features = feature_names
66
+ else:
67
+ if self.features is None:
68
+ features = self.inputs_["annotations"].columns.values
69
+ else:
70
+ features = self.features
71
+
72
+ features = [f for f in features if f not in self.__id_cols]
73
+ n_features_orig = len(features)
74
+
75
+ # At least one study in the dataset much have each label
76
+ counts = (self.inputs_["annotations"][features] > self.frequency_threshold).sum(0)
77
+ features = counts[counts > 0].index.tolist()
78
+ if not len(features):
79
+ raise Exception("No features identified in Dataset!")
80
+ elif len(features) < n_features_orig:
81
+ LGR.info(f"Retaining {len(features)}/{n_features_orig} features.")
82
+
83
+ self.features_ = features
84
+
85
+ def fit(self, dataset, drop_invalid=True):
86
+ """Fit Decoder to Dataset.
87
+
88
+ Parameters
89
+ ----------
90
+ dataset : :obj:`~nimare.dataset.Dataset`
91
+ Dataset object to analyze.
92
+ drop_invalid : :obj:`bool`, default=True
93
+ Whether to automatically ignore any studies without the required data or not.
94
+ Default is True.
95
+
96
+ Notes
97
+ -----
98
+ The `fit` method is a light wrapper that runs input validation and
99
+ preprocessing before fitting the actual model. Decoders' individual
100
+ "fitting" methods are implemented as `_fit`, although users should
101
+ call `fit`.
102
+
103
+ Selection of features based on requested features and feature group is performed in
104
+ `Decoder._preprocess_input`.
105
+ """
106
+ self._collect_inputs(dataset, drop_invalid=drop_invalid)
107
+ self._preprocess_input(dataset)
108
+ self._fit(dataset)
109
+
110
+ @abstractmethod
111
+ def _fit(self, dataset):
112
+ """Apply decoding to dataset and output results.
113
+
114
+ Must return a DataFrame, with one row for each feature.
115
+ """
@@ -0,0 +1,462 @@
1
+ """Methods for decoding unthresholded brain maps into text."""
2
+
3
+ import inspect
4
+ import logging
5
+ import os
6
+ from glob import glob
7
+
8
+ import nibabel as nib
9
+ import numpy as np
10
+ import pandas as pd
11
+ from joblib import Parallel, delayed
12
+ from nilearn._utils import load_niimg
13
+ from nilearn.masking import apply_mask
14
+ from tqdm.auto import tqdm
15
+
16
+ from nimare.decode.base import Decoder
17
+ from nimare.decode.utils import weight_priors
18
+ from nimare.meta.cbma.base import CBMAEstimator
19
+ from nimare.meta.cbma.mkda import MKDAChi2
20
+ from nimare.results import MetaResult
21
+ from nimare.stats import pearson
22
+ from nimare.utils import _check_ncores, _check_type, _safe_transform, get_masker
23
+
24
+ LGR = logging.getLogger(__name__)
25
+
26
+
27
+ def gclda_decode_map(model, image, topic_priors=None, prior_weight=1):
28
+ r"""Perform image-to-text decoding for continuous inputs using method from Rubin et al. (2017).
29
+
30
+ The method used in this function was originally described in :footcite:t:`rubin2017decoding`.
31
+
32
+ Parameters
33
+ ----------
34
+ model : :obj:`~nimare.annotate.gclda.GCLDAModel`
35
+ Model object needed for decoding.
36
+ image : :obj:`nibabel.nifti1.Nifti1Image` or :obj:`str`
37
+ Whole-brain image to decode into text. Must be in same space as
38
+ model and dataset. Model's template available in
39
+ `model.dataset.mask_img`.
40
+ topic_priors : :obj:`numpy.ndarray` of :obj:`float`, optional
41
+ A 1d array of size (n_topics) with values for topic weighting.
42
+ If None, no weighting is done. Default is None.
43
+ prior_weight : :obj:`float`, optional
44
+ The weight by which the prior will affect the decoding.
45
+ Default is 1.
46
+
47
+ Returns
48
+ -------
49
+ decoded_df : :obj:`pandas.DataFrame`
50
+ A DataFrame with the word-tokens and their associated weights.
51
+ topic_weights : :obj:`numpy.ndarray` of :obj:`float`
52
+ The weights of the topics used in decoding.
53
+
54
+ Notes
55
+ -----
56
+ ====================== ==============================================================
57
+ Notation Meaning
58
+ ====================== ==============================================================
59
+ :math:`v` Voxel
60
+ :math:`t` Topic
61
+ :math:`w` Word type
62
+ :math:`i` Input image
63
+ :math:`p(v|t)` Probability of topic given voxel (``p_topic_g_voxel``)
64
+ :math:`\\tau_{t}` Topic weight vector (``topic_weights``)
65
+ :math:`p(w|t)` Probability of word type given topic (``p_word_g_topic``)
66
+ :math:`\omega` 1d array from input image (``input_values``)
67
+ ====================== ==============================================================
68
+
69
+ 1. Compute :math:`p(t|v)` (``p_topic_g_voxel``).
70
+
71
+ - From :func:`gclda.model.Model.get_spatial_probs()`
72
+
73
+ 2. Squeeze input image to 1d array :math:`\omega` (``input_values``).
74
+ 3. Compute topic weight vector (:math:`\\tau_{t}`) by multiplying :math:`p(t|v)` by input
75
+ image.
76
+
77
+ - :math:`\\tau_{t} = p(t|v) \cdot \omega`
78
+
79
+ 4. Multiply :math:`\\tau_{t}` by :math:`p(w|t)`.
80
+
81
+ - :math:`p(w|i) \propto \\tau_{t} \cdot p(w|t)`
82
+
83
+ 5. The resulting vector (``word_weights``) reflects arbitrarily scaled term weights for the
84
+ input image.
85
+
86
+ See Also
87
+ --------
88
+ :class:`~nimare.annotate.gclda.GCLDAModel`
89
+ :func:`~nimare.decode.discrete.gclda_decode_roi`
90
+ :func:`~nimare.decode.encode.gclda_encode`
91
+
92
+ References
93
+ ----------
94
+ .. footbibliography::
95
+ """
96
+ image = load_niimg(image)
97
+
98
+ # Load image file and get voxel values
99
+ input_values = apply_mask(image, model.mask)
100
+ topic_weights = np.squeeze(np.dot(model.p_topic_g_voxel_.T, input_values[:, None]))
101
+ if topic_priors is not None:
102
+ weighted_priors = weight_priors(topic_priors, prior_weight)
103
+ topic_weights *= weighted_priors
104
+
105
+ # Multiply topic_weights by topic-by-word matrix (p_word_g_topic).
106
+ # n_word_tokens_per_topic = np.sum(model.n_word_tokens_word_by_topic, axis=0)
107
+ # p_word_g_topic = model.n_word_tokens_word_by_topic / n_word_tokens_per_topic[None, :]
108
+ # p_word_g_topic = np.nan_to_num(p_word_g_topic, 0)
109
+ word_weights = np.dot(model.p_word_g_topic_, topic_weights)
110
+
111
+ decoded_df = pd.DataFrame(index=model.vocabulary, columns=["Weight"], data=word_weights)
112
+ decoded_df.index.name = "Term"
113
+ return decoded_df, topic_weights
114
+
115
+
116
+ class CorrelationDecoder(Decoder):
117
+ """Decode an unthresholded image by correlating the image with meta-analytic maps.
118
+
119
+ .. versionchanged:: 0.1.0
120
+
121
+ * New method: `load_imgs`. Load pre-generated meta-analytic maps for decoding.
122
+
123
+ * New attribute: `results_`. MetaResult object containing masker, meta-analytic maps,
124
+ and tables. This attribute replaces `masker`, `features_`, and `images_`.
125
+
126
+ .. versionchanged:: 0.0.13
127
+
128
+ * New parameter: `n_cores`. Number of cores to use for parallelization.
129
+
130
+ .. versionchanged:: 0.0.12
131
+
132
+ * Remove low-memory option in favor of sparse arrays.
133
+
134
+ Parameters
135
+ ----------
136
+ feature_group : :obj:`str`, optional
137
+ Feature group
138
+ features : :obj:`list`, optional
139
+ Features
140
+ frequency_threshold : :obj:`float`, optional
141
+ Frequency threshold
142
+ meta_estimator : :class:`~nimare.base.CBMAEstimator`, optional
143
+ Meta-analysis estimator. Default is :class:`~nimare.meta.mkda.MKDAChi2`.
144
+ target_image : :obj:`str`, optional
145
+ Name of meta-analysis results image to use for decoding.
146
+ n_cores : :obj:`int`, optional
147
+ Number of cores to use for parallelization.
148
+ If <=0, defaults to using all available cores.
149
+ Default is 1.
150
+
151
+ Warnings
152
+ --------
153
+ Coefficients from correlating two maps have very large degrees of freedom,
154
+ so almost all results will be statistically significant. Do not attempt to
155
+ evaluate results based on significance.
156
+ """
157
+
158
+ _required_inputs = {
159
+ "coordinates": ("coordinates", None),
160
+ "annotations": ("annotations", None),
161
+ }
162
+
163
+ def __init__(
164
+ self,
165
+ feature_group=None,
166
+ features=None,
167
+ frequency_threshold=0.001,
168
+ meta_estimator=None,
169
+ target_image="z_desc-association",
170
+ n_cores=1,
171
+ ):
172
+ meta_estimator = (
173
+ MKDAChi2() if meta_estimator is None else _check_type(meta_estimator, CBMAEstimator)
174
+ )
175
+
176
+ self.feature_group = feature_group
177
+ self.features = features
178
+ self.frequency_threshold = frequency_threshold
179
+ self.meta_estimator = meta_estimator
180
+ self.target_image = target_image
181
+ self.n_cores = _check_ncores(n_cores)
182
+
183
+ def _fit(self, dataset):
184
+ """Generate feature-specific meta-analytic maps for dataset.
185
+
186
+ Parameters
187
+ ----------
188
+ dataset : :obj:`~nimare.dataset.Dataset`
189
+ Dataset for which to run meta-analyses to generate maps.
190
+
191
+ Attributes
192
+ ----------
193
+ results_ : :obj:`~nimare.results.MetaResult`
194
+ MetaResult with meta-analytic maps and masker added.
195
+ """
196
+ n_features = len(self.features_)
197
+ maps = {
198
+ r: v
199
+ for r, v in tqdm(
200
+ Parallel(return_as="generator", n_jobs=self.n_cores)(
201
+ delayed(self._run_fit)(feature, dataset) for feature in self.features_
202
+ ),
203
+ total=n_features,
204
+ )
205
+ }
206
+
207
+ self.results_ = MetaResult(self, mask=dataset.masker, maps=maps)
208
+
209
+ def _run_fit(self, feature, dataset):
210
+ feature_ids = dataset.get_studies_by_label(
211
+ labels=[feature],
212
+ label_threshold=self.frequency_threshold,
213
+ )
214
+ # Limit selected studies to studies with valid data
215
+ feature_ids = sorted(list(set(feature_ids).intersection(self.inputs_["id"])))
216
+
217
+ # Create the reduced Dataset
218
+ feature_dset = dataset.slice(feature_ids)
219
+
220
+ # Check if the meta method is a pairwise estimator
221
+ # This seems like a somewhat inelegant solution
222
+ if "dataset2" in inspect.getfullargspec(self.meta_estimator.fit).args:
223
+ nonfeature_ids = sorted(list(set(self.inputs_["id"]) - set(feature_ids)))
224
+ nonfeature_dset = dataset.slice(nonfeature_ids)
225
+ meta_results = self.meta_estimator.fit(feature_dset, nonfeature_dset)
226
+ else:
227
+ meta_results = self.meta_estimator.fit(feature_dset)
228
+
229
+ feature_data = meta_results.get_map(
230
+ self.target_image,
231
+ return_type="array",
232
+ )
233
+
234
+ return feature, feature_data
235
+
236
+ def load_imgs(self, features_imgs, mask=None):
237
+ """Load pregenerated maps from disk.
238
+
239
+ .. versionadded:: 0.1.0
240
+
241
+ Parameters
242
+ ----------
243
+ features_imgs : :obj:`dict`, or str
244
+ Dictionary with feature names as keys and paths to images as values.
245
+ If a string is provided, it is assumed to be a path to a folder with NIfTI images,
246
+ where the file's name (without the extension .nii.gz) will be considered as the
247
+ feature name by the decoder.
248
+ mask : str, :class:`nibabel.nifti1.Nifti1Image`, or any nilearn Masker
249
+ Mask to apply to pre-generated maps.
250
+
251
+ Attributes
252
+ ----------
253
+ results_ : :obj:`~nimare.results.MetaResult`
254
+ MetaResult with meta-analytic maps and masker added.
255
+ """
256
+ if isinstance(features_imgs, dict):
257
+ feature_imgs_dict = features_imgs
258
+ elif isinstance(features_imgs, str):
259
+ img_paths = sorted(glob(os.path.join(features_imgs, "*.nii*")))
260
+ img_names = [os.path.basename(img).split(os.extsep)[0] for img in img_paths]
261
+ feature_imgs_dict = dict(zip(img_names, img_paths))
262
+ else:
263
+ raise ValueError(
264
+ f'"feature_imgs" must be a dictionary or a string, not a {type(features_imgs)}.'
265
+ )
266
+
267
+ # Replace attributes of initialized class self with Nones, so that default values are not
268
+ # confused with the parameters used before to generate the maps that are read from disk.
269
+ for attr in self.__dict__:
270
+ setattr(self, attr, None)
271
+
272
+ if mask is not None:
273
+ mask = get_masker(mask)
274
+ else:
275
+ raise ValueError("A mask must be provided.")
276
+ self.masker = mask
277
+
278
+ # Load pre-generated maps
279
+ features, images = ([], [])
280
+ for feature, img_path in feature_imgs_dict.items():
281
+ img = nib.load(img_path)
282
+ features.append(feature)
283
+ images.append(np.squeeze(self.masker.transform(img)))
284
+
285
+ maps = {feature: image for feature, image in zip(features, images)}
286
+ self.results_ = MetaResult(self, mask=self.masker, maps=maps)
287
+
288
+ def transform(self, img):
289
+ """Correlate target image with each feature-specific meta-analytic map.
290
+
291
+ Parameters
292
+ ----------
293
+ img : :obj:`~nibabel.nifti1.Nifti1Image`
294
+ Image to decode. Must be in same space as ``dataset``.
295
+
296
+ Returns
297
+ -------
298
+ out_df : :obj:`pandas.DataFrame`
299
+ DataFrame with one row for each feature, an index named "feature", and one column: "r".
300
+ """
301
+ if not hasattr(self, "results_"):
302
+ raise AttributeError(
303
+ f"This {self.__class__.__name__} instance is not fitted yet. "
304
+ "Call 'fit' or 'load_imgs' before using 'transform'."
305
+ )
306
+
307
+ # Make sure we return a copy of the MetaResult
308
+ results = self.results_.copy()
309
+ features = list(results.maps.keys())
310
+ images = np.array(list(results.maps.values()))
311
+
312
+ img_vec = results.masker.transform(img)
313
+ corrs = pearson(img_vec, images)
314
+ out_df = pd.DataFrame(index=features, columns=["r"], data=corrs)
315
+ out_df.index.name = "feature"
316
+
317
+ # Update self.results_ to include the new table
318
+ results.tables["correlation"] = out_df
319
+ self.results_ = results
320
+
321
+ return out_df
322
+
323
+
324
+ class CorrelationDistributionDecoder(Decoder):
325
+ """Decode an unthresholded image by correlating the image with study-wise images.
326
+
327
+ .. versionchanged:: 0.1.0
328
+
329
+ * New attribute: `results_`. MetaResult object containing masker, meta-analytic maps,
330
+ and tables. This attribute replaces `masker`, `features_`, and `images_`.
331
+
332
+ .. versionchanged:: 0.0.13
333
+
334
+ * New parameter: `n_cores`. Number of cores to use for parallelization.
335
+
336
+ Parameters
337
+ ----------
338
+ feature_group : :obj:`str`, optional
339
+ Feature group. Default is None, which uses all available features.
340
+ features : :obj:`list`, optional
341
+ Features. Default is None, which uses all available features.
342
+ frequency_threshold : :obj:`float`, optional
343
+ Frequency threshold. Default is 0.001.
344
+ target_image : {'z', 'con'}, optional
345
+ Name of meta-analysis results image to use for decoding. Default is 'z'.
346
+ n_cores : :obj:`int`, optional
347
+ Number of cores to use for parallelization.
348
+ If <=0, defaults to using all available cores.
349
+ Default is 1.
350
+
351
+ Warnings
352
+ --------
353
+ Coefficients from correlating two maps have very large degrees of freedom,
354
+ so almost all results will be statistically significant. Do not attempt to
355
+ evaluate results based on significance.
356
+ """
357
+
358
+ _required_inputs = {
359
+ "annotations": ("annotations", None),
360
+ }
361
+
362
+ def __init__(
363
+ self,
364
+ feature_group=None,
365
+ features=None,
366
+ frequency_threshold=0.001,
367
+ target_image="z",
368
+ n_cores=1,
369
+ ):
370
+ self.feature_group = feature_group
371
+ self.features = features
372
+ self.frequency_threshold = frequency_threshold
373
+ self._required_inputs["images"] = ("image", target_image)
374
+ self.n_cores = _check_ncores(n_cores)
375
+
376
+ def _fit(self, dataset):
377
+ """Collect sets of maps from the Dataset corresponding to each requested feature.
378
+
379
+ Parameters
380
+ ----------
381
+ dataset : :obj:`~nimare.dataset.Dataset`
382
+ Dataset for which to run meta-analyses to generate maps.
383
+
384
+ Attributes
385
+ ----------
386
+ results : :obj:`~nimare.results.MetaResult`
387
+ MetaResult with meta-analytic maps and masker added.
388
+ """
389
+ n_features = len(self.features_)
390
+ maps = {
391
+ r: v
392
+ for r, v in tqdm(
393
+ Parallel(return_as="generator", n_jobs=self.n_cores)(
394
+ delayed(self._run_fit)(feature, dataset) for feature in self.features_
395
+ ),
396
+ total=n_features,
397
+ )
398
+ }
399
+
400
+ self.results_ = MetaResult(self, mask=dataset.masker, maps=maps)
401
+
402
+ def _run_fit(self, feature, dataset):
403
+ feature_ids = dataset.get_studies_by_label(
404
+ labels=[feature], label_threshold=self.frequency_threshold
405
+ )
406
+ selected_ids = sorted(list(set(feature_ids).intersection(self.inputs_["id"])))
407
+ selected_id_idx = [
408
+ i_id for i_id, id_ in enumerate(self.inputs_["id"]) if id_ in selected_ids
409
+ ]
410
+ test_imgs = [
411
+ img for i_img, img in enumerate(self.inputs_["images"]) if i_img in selected_id_idx
412
+ ]
413
+ if len(test_imgs):
414
+ feature_arr = _safe_transform(
415
+ test_imgs,
416
+ dataset.masker,
417
+ memfile=None,
418
+ )
419
+ return feature, feature_arr
420
+ else:
421
+ LGR.info(f"Skipping feature '{feature}'. No images found.")
422
+
423
+ def transform(self, img):
424
+ """Correlate target image with each map associated with each feature.
425
+
426
+ Parameters
427
+ ----------
428
+ img : :obj:`~nibabel.nifti1.Nifti1Image`
429
+ Image to decode. Must be in same space as ``dataset``.
430
+
431
+ Returns
432
+ -------
433
+ out_df : :obj:`pandas.DataFrame`
434
+ DataFrame with one row for each feature, an index named "feature", and two columns:
435
+ "mean" and "std".
436
+ """
437
+ if not hasattr(self, "results_"):
438
+ raise AttributeError(
439
+ f"This {self.__class__.__name__} instance is not fitted yet. "
440
+ "Call 'fit' before using 'transform'."
441
+ )
442
+
443
+ # Make sure we return a copy of the MetaResult
444
+ results = self.results_.copy()
445
+ features = list(results.maps.keys())
446
+
447
+ img_vec = results.masker.transform(img)
448
+ out_df = pd.DataFrame(
449
+ index=features, columns=["mean", "std"], data=np.zeros((len(features), 2))
450
+ )
451
+ out_df.index.name = "feature"
452
+ for feature, feature_arr in results.maps.items():
453
+ corrs = pearson(img_vec, feature_arr)
454
+ corrs_z = np.arctanh(corrs)
455
+ out_df.loc[feature, "mean"] = np.mean(corrs_z)
456
+ out_df.loc[feature, "std"] = np.std(corrs_z)
457
+
458
+ # Update self.results_ to include the new table
459
+ results.tables["correlation"] = out_df
460
+ self.results_ = results
461
+
462
+ return out_df