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
@@ -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
|