Skip to content

Commit

Permalink
Add Corrector and Diagnostics attributes to MetaResult object (#804)
Browse files Browse the repository at this point in the history
* Add Corrector and Diagnostics attributes to MetaResult object

* Update diagnostics.py

* fix documentation

* Update ale.py

* add new bids-like modalities
  • Loading branch information
JulioAPeraza committed May 23, 2023
1 parent 6e7792b commit 476e262
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 28 deletions.
6 changes: 5 additions & 1 deletion docs/outputs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Here is the basic naming convention for statistical maps:

.. code-block:: Text
<value>[_desc-<label>][_level-<cluster|voxel>][_corr-<FWE|FDR>][_method-<label>].nii.gz
<value>[_desc-<label>][_level-<cluster|voxel>][_corr-<FWE|FDR>][_method-<label>][_diag-<Jackknife|FocusCounter>].nii.gz
First, the ``value`` represents type of data in the map (e.g., z-statistic, t-statistic).
Expand All @@ -32,6 +32,7 @@ Some of the values found in NiMARE include:
- ``se``: Standard error of the parameter estimate (IBMA only)
- ``tau2``: Estimated between-study variance (IBMA only)
- ``sigma2``: Estimated within-study variance (IBMA only)
- ``label``: Label map

.. note::
For one-sided tests, p-values > 0.5 will have negative z-statistics. These values should not
Expand All @@ -44,6 +45,9 @@ Next, a series of key/value pairs describe the methods applied to generate the m
- ``level``: Level of multiple comparisons correction. Either ``cluster`` or ``voxel``.
- ``corr``: Type of multiple comparisons correction. Either ``FWE`` (familywise error rate) or ``FDR`` (false discovery rate).
- ``method``: Name of the method used for multiple comparisons correction (e.g., "montecarlo" for a Monte Carlo procedure).
- ``diag``: Type of diagnostic. Either ``Jackknife`` (jackknife analysis) or ``FocusCounter`` (focus-count analysis).
- ``tab``: Type of table. Either ``clust`` (clusters table) or ``counts`` (contribution table).
- ``tail``: Sign of the tail for label maps. Either ``positive`` or ``negative``.

File contents
-------------
Expand Down
13 changes: 11 additions & 2 deletions examples/02_meta-analyses/08_plot_cbma_subtraction_conjunction.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,20 @@
target_image="z_desc-size_level-cluster_corr-FWE_method-montecarlo",
voxel_thresh=None,
)
knowledge_count_table, knowledge_clusters_table, _ = counter.transform(knowledge_corrected_results)
knowledge_corrected_results = counter.transform(knowledge_corrected_results)

###############################################################################
# Clusters table.
knowledge_clusters_table = knowledge_corrected_results.tables[
"z_desc-size_level-cluster_corr-FWE_method-montecarlo_tab-clust"
]
knowledge_clusters_table.head(10)

###############################################################################
# Contribution table. Here ``PostiveTail`` refers to clusters with positive statistics.
knowledge_count_table = knowledge_corrected_results.tables[
"z_desc-size_level-cluster_corr-FWE_method-montecarlo_diag-FocusCounter_tab-counts"
]
knowledge_count_table.head(10)

###############################################################################
Expand All @@ -121,7 +127,10 @@
target_image="z_desc-size_level-cluster_corr-FWE_method-montecarlo",
voxel_thresh=None,
)
related_jackknife_table, _, _ = jackknife.transform(related_corrected_results)
related_corrected_results = jackknife.transform(related_corrected_results)
related_jackknife_table = related_corrected_results.tables[
"z_desc-size_level-cluster_corr-FWE_method-montecarlo_diag-Jackknife_tab-counts"
]
related_jackknife_table.head(10)

###############################################################################
Expand Down
4 changes: 2 additions & 2 deletions examples/02_meta-analyses/10_plot_cbma_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@
###############################################################################
# Clusters table
# ``````````````````````````````````````````````````````````````````````````````
result.tables["z_corr-FDR_method-indep_clust"]
result.tables["z_corr-FDR_method-indep_tab-clust"]

###############################################################################
# Contribution table
# ``````````````````````````````````````````````````````````````````````````````
result.tables["z_corr-FDR_method-indep_Jackknife"]
result.tables["z_corr-FDR_method-indep_diag-Jackknife_tab-counts"]

###############################################################################
# Methods
Expand Down
3 changes: 3 additions & 0 deletions nimare/correct.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ def transform(self, result):
# Update the estimator as well, in order to retain updated null distributions
result.estimator = est

# Save the corrected maps
result.corrector = self

return result

def _transform(self, result, method):
Expand Down
48 changes: 43 additions & 5 deletions nimare/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
class Diagnostics(NiMAREBase):
"""Base class for diagnostic methods.
.. versionchanged:: 0.1.0
- Transform now returns a MetaResult object.
.. versionadded:: 0.0.14
Parameters
Expand Down Expand Up @@ -96,6 +100,9 @@ def transform(self, result):
correspond to positive and negative tails.
If no clusters are found, this list will be empty.
"""
masker = result.estimator.masker
diag_name = self.__class__.__name__

none_contribution_table = False
if not hasattr(result.estimator, "dataset"):
LGR.warning(
Expand Down Expand Up @@ -142,22 +149,38 @@ def transform(self, result):
for _, row in clusters_table.iterrows()
]

# Define bids-like names for tables and maps
image_name = "_".join(self.target_image.split("_")[1:])
image_name = "_" + image_name if image_name else image_name
clusters_table_name = f"{self.target_image}_tab-clust"
contribution_table_name = f"{self.target_image}_diag-{diag_name}_tab-counts"
label_map_names = (
[f"label{image_name}_tail-positive", f"label{image_name}_tail-negative"]
if len(label_maps) == 2
else [f"label{image_name}_tail-positive"]
)

# Check number of clusters
if (n_clusters == 0) or none_contribution_table:
return None, clusters_table, label_maps
result.tables[clusters_table_name] = clusters_table
result.tables[contribution_table_name] = None
result.maps[label_map_names[0]] = None

result.diagnostics.append(self)
return result

# Use study IDs in inputs_ instead of dataset, because we don't want to try fitting the
# estimator to a study that might have been filtered out by the estimator's criteria.
meta_ids = result.estimator.inputs_["id"]
rows = list(meta_ids)

contribution_tables = []
signs = [1, -1] if len(label_maps) == 2 else [1]
signs = ["PositiveTail", "NegativeTail"] if len(label_maps) == 2 else ["PositiveTail"]
for sign, label_map in zip(signs, label_maps):
cluster_ids = sorted(list(np.unique(label_map.get_fdata())[1:]))

# Create contribution table
col_name = "PositiveTail" if sign == 1 else "NegativeTail"
cols = [f"{col_name} {int(c_id)}" for c_id in cluster_ids]
cols = [f"{sign} {int(c_id)}" for c_id in cluster_ids]
contribution_table = pd.DataFrame(index=rows, columns=cols)
contribution_table.index.name = "id"

Expand All @@ -175,7 +198,22 @@ def transform(self, result):
# Concat PositiveTail and NegativeTail tables
contribution_table = pd.concat(contribution_tables, ignore_index=True, sort=False)

return contribution_table, clusters_table, label_maps
# Save tables and maps to result
diag_tables_dict = {
clusters_table_name: clusters_table,
contribution_table_name: contribution_table,
}
diag_maps_dict = {
label_map_name: np.squeeze(masker.transform(label_map))
for label_map_name, label_map in zip(label_map_names, label_maps)
}

result.tables.update(diag_tables_dict)
result.maps.update(diag_maps_dict)

# Add diagnostics class to result, since more than one can be run
result.diagnostics.append(self)
return result


class Jackknife(Diagnostics):
Expand Down
33 changes: 30 additions & 3 deletions nimare/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@
class MetaResult(NiMAREBase):
"""Base class for meta-analytic results.
.. versionchanged:: 0.1.0
- Added corrector and diagnostics attributes.
.. versionchanged:: 0.0.12
- Added the description attribute.
Parameters
----------
estimator : :class:`~nimare.base.Estimator`
The Estimator used to generate the maps in the MetaResult.
corrector : :class:`~nimare.correct.Corrector`
The Corrector used to correct the maps in the MetaResult.
diagnostics : :obj:`list` of :class:`~nimare.diagnostics.Diagnostics`
List of diagnostic classes.
mask : Niimg-like or `nilearn.input_data.base_masker.BaseMasker`
Mask for converting maps between arrays and images.
maps : None or :obj:`dict` of :obj:`numpy.ndarray`, optional
Expand All @@ -36,6 +45,10 @@ class MetaResult(NiMAREBase):
----------
estimator : :class:`~nimare.base.Estimator`
The Estimator used to generate the maps in the MetaResult.
corrector : :class:`~nimare.correct.Corrector`
The Corrector used to correct the maps in the MetaResult.
diagnostics : :obj:`list` of :class:`~nimare.diagnostics.Diagnostics`
List of diagnostic classes.
masker : :class:`~nilearn.input_data.NiftiMasker` or similar
Masker object.
maps : :obj:`dict`
Expand All @@ -55,8 +68,20 @@ class MetaResult(NiMAREBase):
BibTeX file without issue.
"""

def __init__(self, estimator, mask, maps=None, tables=None, description=""):
def __init__(
self,
estimator,
corrector=None,
diagnostics=None,
mask=None,
maps=None,
tables=None,
description="",
):
self.estimator = copy.deepcopy(estimator)
self.corrector = copy.deepcopy(corrector)
diagnostics = diagnostics or []
self.diagnostics = [copy.deepcopy(diagnostic) for diagnostic in diagnostics]
self.masker = get_masker(mask)

maps = maps or {}
Expand Down Expand Up @@ -140,7 +165,7 @@ def save_maps(self, output_dir=".", prefix="", prefix_sep="_", names=None):
os.makedirs(output_dir)

names = names or list(self.maps.keys())
maps = {k: self.get_map(k) for k in names}
maps = {k: self.get_map(k) for k in names if self.maps[k] is not None}

for imgtype, img in maps.items():
filename = prefix + imgtype + ".nii.gz"
Expand Down Expand Up @@ -188,7 +213,9 @@ def save_tables(self, output_dir=".", prefix="", prefix_sep="_", names=None):
def copy(self):
"""Return copy of result object."""
new = MetaResult(
self.estimator,
estimator=self.estimator,
corrector=self.corrector,
diagnostics=self.diagnostics,
mask=self.masker,
maps=copy.deepcopy(self.maps),
tables=copy.deepcopy(self.tables),
Expand Down
28 changes: 22 additions & 6 deletions nimare/tests/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,21 @@ def test_jackknife_smoke(
res = meta.fit(testdata)

jackknife = diagnostics.Jackknife(target_image=target_image, voxel_thresh=1.65)
contribution_table, clusters_table, label_maps = jackknife.transform(res)
results = jackknife.transform(res)

image_name = "_".join(target_image.split("_")[1:])
image_name = "_" + image_name if image_name else image_name
contribution_table = results.tables[f"{target_image}_diag-Jackknife_tab-counts"]
clusters_table = results.tables[f"{target_image}_tab-clust"]
label_maps = results.maps[f"label{image_name}_tail-positive"]
if n_samples == "twosample":
assert contribution_table is None
assert not clusters_table.empty
assert len(label_maps) > 0
assert label_maps is None
else:
assert contribution_table.shape[0] == len(meta.inputs_["id"])
assert clusters_table.shape[0] >= contribution_table.shape[1] - 1
assert len(label_maps) > 0


def test_jackknife_with_zero_clusters(testdata_cbma_full):
Expand All @@ -60,8 +66,11 @@ def test_jackknife_with_zero_clusters(testdata_cbma_full):
res = meta.fit(testdata_cbma_full)

jackknife = diagnostics.Jackknife(target_image="z", voxel_thresh=10)
contribution_table, clusters_table, label_maps = jackknife.transform(res)
results = jackknife.transform(res)

contribution_table = results.tables["z_diag-Jackknife_tab-counts"]
clusters_table = results.tables["z_tab-clust"]
label_maps = results.maps["label_tail-positive"]
assert contribution_table is None
assert clusters_table.empty
assert not label_maps
Expand All @@ -80,7 +89,8 @@ def test_jackknife_with_custom_masker_smoke(testdata_ibma):
res = meta.fit(testdata_ibma)

jackknife = diagnostics.Jackknife(target_image="z", voxel_thresh=0.5)
contribution_table, _, _ = jackknife.transform(res)
results = jackknife.transform(res)
contribution_table = results.tables["z_diag-Jackknife_tab-counts"]
assert contribution_table.shape[0] == len(meta.inputs_["id"])

# A Jackknife with a target_image that isn't present in the MetaResult raises a ValueError.
Expand Down Expand Up @@ -115,15 +125,21 @@ def test_focuscounter_smoke(
res = meta.fit(testdata)

counter = diagnostics.FocusCounter(target_image=target_image, voxel_thresh=1.65)
contribution_table, clusters_table, label_maps = counter.transform(res)
results = counter.transform(res)

image_name = "_".join(target_image.split("_")[1:])
image_name = "_" + image_name if image_name else image_name
contribution_table = results.tables[f"{target_image}_diag-FocusCounter_tab-counts"]
clusters_table = results.tables[f"{target_image}_tab-clust"]
label_maps = results.maps[f"label{image_name}_tail-positive"]
if n_samples == "twosample":
assert contribution_table is None
assert not clusters_table.empty
assert len(label_maps) > 0
assert label_maps is None
else:
assert contribution_table.shape[0] == len(meta.inputs_["id"])
assert clusters_table.shape[0] >= contribution_table.shape[1] - 1
assert len(label_maps) > 0


def test_focusfilter(testdata_laird):
Expand Down
4 changes: 3 additions & 1 deletion nimare/tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ def test_cbma_workflow_function_smoke(
for imgtype in cres.maps.keys():
filename = imgtype + ".nii.gz"
outpath = op.join(tmpdir, filename)
assert op.isfile(outpath)
# For estimator == ALE, maps are None
if estimator != ALE:
assert op.isfile(outpath)

for tabletype in cres.tables.keys():
filename = tabletype + ".tsv"
Expand Down
15 changes: 12 additions & 3 deletions nimare/workflows/ale.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ def ale_sleuth_workflow(
target_image="z_desc-size_level-cluster_corr-FWE_method-montecarlo",
voxel_thresh=None,
)
count_df, _, _ = fcounter.transform(cres)
cres = fcounter.transform(cres)
count_df = cres.tables[
"z_desc-size_level-cluster_corr-FWE_method-montecarlo_diag-FocusCounter_tab-counts"
]
boilerplate = cres.description_
bibtex = cres.bibtex_

Expand Down Expand Up @@ -80,12 +83,18 @@ def ale_sleuth_workflow(
target_image="z_desc-size_level-cluster_corr-FWE_method-montecarlo",
voxel_thresh=None,
)
count_df1, _, _ = fcounter.transform(cres1)
cres1 = fcounter.transform(cres1)
count_df1 = cres1.tables[
"z_desc-size_level-cluster_corr-FWE_method-montecarlo_diag-FocusCounter_tab-counts"
]

cres2 = corr.transform(res2)
boilerplate += "\n" + cres2.description_

count_df2, _, _ = fcounter.transform(cres2)
cres2 = fcounter.transform(cres2)
count_df2 = cres2.tables[
"z_desc-size_level-cluster_corr-FWE_method-montecarlo_diag-FocusCounter_tab-counts"
]

sub = ALESubtraction(n_iters=n_iters, kernel__fwhm=fwhm)
sres = sub.fit(dset1, dset2)
Expand Down
6 changes: 1 addition & 5 deletions nimare/workflows/cbma.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,7 @@ def cbma_workflow(
]
for img_key, diagnostic in itertools.product(img_keys, diagnostics):
diagnostic.target_image = img_key
contribution_table, clusters_table, _ = diagnostic.transform(corr_results)

diag_name = diagnostic.__class__.__name__
corr_results.tables[f"{img_key}_clust"] = clusters_table
corr_results.tables[f"{img_key}_{diag_name}"] = contribution_table
corr_results = diagnostic.transform(corr_results)

if output_dir is not None:
LGR.info(f"Saving meta-analytic maps, tables and boilerplate to {output_dir}...")
Expand Down

0 comments on commit 476e262

Please sign in to comment.