Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CBMA workflow #761

Merged
merged 36 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
94a92cc
Add `cbma_workflow` function
JulioAPeraza Jan 23, 2023
f341111
Fix typo
JulioAPeraza Jan 23, 2023
24c8de1
@jdkent Apply suggestions from code review
JulioAPeraza Jan 25, 2023
97779a5
take clusters table from diagnostics
JulioAPeraza Mar 14, 2023
ed33632
Merge branch 'neurostuff:main' into cbma-workflow
JulioAPeraza Mar 14, 2023
9e63156
Merge branch 'neurostuff:main' into cbma-workflow
JulioAPeraza Mar 15, 2023
7af5adc
Add example to gallery
JulioAPeraza Mar 16, 2023
e35143f
Update cbma.py
JulioAPeraza Mar 16, 2023
cd1df83
support string inputs
JulioAPeraza Mar 16, 2023
9649f04
refactor code
JulioAPeraza Mar 16, 2023
1652329
Update cbma.py
JulioAPeraza Mar 16, 2023
1c6a268
[skip CI] support n_cores
JulioAPeraza Mar 16, 2023
5d24099
[skip ci] map string to classes
JulioAPeraza Mar 17, 2023
81ea6b3
[skip ci] test possible combinations
JulioAPeraza Mar 17, 2023
d3a878b
[skip CI] Also support single diagnostic class
JulioAPeraza Mar 17, 2023
4615ebf
Use FDR in the example
JulioAPeraza Mar 17, 2023
7f52193
Merge branch 'neurostuff:main' into cbma-workflow
JulioAPeraza Mar 17, 2023
373fd22
Update test_workflows.py
JulioAPeraza Mar 17, 2023
6317595
Update test_workflows.py
JulioAPeraza Mar 17, 2023
5318cca
fix tests
JulioAPeraza Mar 17, 2023
51bbef1
Update test_workflows.py
JulioAPeraza Mar 17, 2023
5fa9327
Use warning instead of warn
JulioAPeraza Mar 17, 2023
dd52461
Set default voxel_thresh for diagnostics
JulioAPeraza Mar 17, 2023
6e74b0c
Update diagnostics.py
JulioAPeraza Mar 17, 2023
b004ef4
update doc, fix typo
JulioAPeraza Mar 17, 2023
2c2fdda
Update nimare/workflows/cbma.py
JulioAPeraza Mar 18, 2023
244abba
Skips None in save_tables()
JulioAPeraza Mar 22, 2023
dd143fc
Add empty tables and none to dict too
JulioAPeraza Mar 22, 2023
db433bf
Merge branch 'neurostuff:main' into cbma-workflow
JulioAPeraza Mar 27, 2023
58d00e7
Add voxel_thresh and cluster_threshold parameters
JulioAPeraza Mar 28, 2023
6050464
Test non-cbma estimator
JulioAPeraza Mar 28, 2023
0a81725
Update base.py
JulioAPeraza Mar 28, 2023
1761655
Update 10_plot_cbma_workflow.py
JulioAPeraza Mar 28, 2023
050d3a0
Improve coverage
JulioAPeraza Mar 28, 2023
58f2cfc
Update documentation
JulioAPeraza Mar 29, 2023
6af419b
Improve coverage
JulioAPeraza Mar 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ For more information about fetching data from the internet, see :ref:`fetching t

workflows.ale_sleuth_workflow
workflows.macm_workflow
workflows.cbma_workflow


.. _api_base_ref:
Expand Down
89 changes: 89 additions & 0 deletions examples/02_meta-analyses/10_plot_cbma_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""

.. _cbma_workflow:

====================================================
Run a coordinate-based meta-analysis (CBMA) workflow
====================================================

NiMARE provides a plethora of tools for performing meta-analyses on neuroimaging data.
Sometimes it's difficult to know where to start, especially if you're new to meta-analysis.
This tutorial will walk you through using a CBMA workflow function which puts together
the fundamental steps of a CBMA meta-analysis.
"""
import os
from pprint import pprint

import matplotlib.pyplot as plt
from nilearn.plotting import plot_stat_map

from nimare.dataset import Dataset
from nimare.utils import get_resource_path
from nimare.workflows import cbma_workflow

###############################################################################
# Load Dataset
# -----------------------------------------------------------------------------

dset_file = os.path.join(get_resource_path(), "nidm_pain_dset.json")
dset = Dataset(dset_file)

###############################################################################
# Run CBMA Workflow
# -----------------------------------------------------------------------------
# The CBMA workflow function runs the following steps:
#
# 1. Runs a meta-analysis using the specified method (default: ALE)
# 2. Applies a corrector to the meta-analysis results (default: FWECorrector, montecarlo)
# 3. Generates cluster tables and runs diagnostics on the corrected results (default: Jackknife)
#
# All in one function call!
#
# result = cbma_workflow(dset)
#
# For this example, we use an FDR correction because the default corrector (FWE correction with
# Monte Carlo simulation) takes a long time to run due to the high number of iterations that
# are required
result = cbma_workflow(dset, corrector="fdr")

###############################################################################
# Plot Results
# -----------------------------------------------------------------------------
# The CBMA workflow function returns a :class:`~nimare.results.MetaResult` object,
# where you can access the corrected results of the meta-analysis and diagnostics tables.
#
# Corrected map:
img = result.get_map("z_corr-FDR_method-indep")
plot_stat_map(
img,
cut_coords=4,
display_mode="z",
threshold=3.1, # cluster-level p < 0.001, one-tailed
cmap="RdBu_r",
vmax=4,
)
plt.show()

###############################################################################
# Clusters table
# ``````````````````````````````````````````````````````````````````````````````
result.tables["z_corr-FDR_method-indep_clust"]

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

###############################################################################
# Methods
# -----------------------------------------------------------------------------
# The MetaResult object also provides boilerplate text automatically generated by NiMARE,
# which is released under the `CC0 <https://creativecommons.org/publicdomain/zero/1.0/>`_ license.
print("Description:")
pprint(result.description_)

###############################################################################
# References
# ``````````````````````````````````````````````````````````````````````````````
print("References:")
pprint(result.bibtex_)
2 changes: 1 addition & 1 deletion nimare/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def transform(self, result):
"""
none_contribution_table = False
if not hasattr(result.estimator, "dataset"):
LGR.warn(
LGR.warning(
"MetaResult was not generated by an Estimator with a `dataset` attribute. "
"This may be because the Estimator was a pairwise Estimator. The "
"Jackknife/FocusCounter method does not currently work with pairwise Estimators. "
Expand Down
7 changes: 5 additions & 2 deletions nimare/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,12 @@ def save_tables(self, output_dir=".", prefix="", prefix_sep="_", names=None):
tables = {k: self.tables[k] for k in names}

for tabletype, table in tables.items():
filename = prefix + tables + ".tsv"
filename = prefix + tabletype + ".tsv"
outpath = os.path.join(output_dir, filename)
table.to_csv(outpath, sep="\t", index=False)
if table is not None:
table.to_csv(outpath, sep="\t", index=False)
else:
LGR.warning(f"Table {tabletype} is None. Not saving.")

def copy(self):
"""Return copy of result object."""
Expand Down
53 changes: 53 additions & 0 deletions nimare/tests/test_workflows.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
"""Test nimare.workflows."""
import os.path as op

import pytest

import nimare
from nimare import cli, workflows
from nimare.correct import FWECorrector
from nimare.diagnostics import FocusCounter, Jackknife
from nimare.meta.cbma import ALE, MKDAChi2
from nimare.tests.utils import get_test_data_path


Expand Down Expand Up @@ -81,3 +87,50 @@ def test_ale_workflow_cli_smoke_2(tmp_path_factory):
]
)
assert op.isfile(op.join(tmpdir, f"{prefix}_group2_input_coordinates.txt"))


@pytest.mark.parametrize(
"estimator,corrector,diagnostics",
[
(ALE, FWECorrector(method="montecarlo", n_iters=10), [Jackknife]),
("ale", "bonferroni", [Jackknife, FocusCounter]),
("kda", "fdr", Jackknife),
(MKDAChi2, "montecarlo", "focuscounter"),
],
)
def test_cbma_workflow_function_smoke(
tmp_path_factory, testdata_cbma_full, estimator, corrector, diagnostics
):
"""Run smoke test for CBMA workflow."""
tmpdir = tmp_path_factory.mktemp("test_cbma_workflow_function_smoke")

if estimator == MKDAChi2:
with pytest.raises(AttributeError):
workflows.cbma_workflow(
testdata_cbma_full,
estimator=estimator,
corrector=corrector,
diagnostics=diagnostics,
)
else:
cres = workflows.cbma_workflow(
testdata_cbma_full,
estimator=estimator,
corrector=corrector,
diagnostics=diagnostics,
output_dir=tmpdir,
)

assert isinstance(cres, nimare.results.MetaResult)
assert op.isfile(op.join(tmpdir, "boilerplate.txt"))
assert op.isfile(op.join(tmpdir, "references.bib"))

for imgtype in cres.maps.keys():
filename = imgtype + ".nii.gz"
outpath = op.join(tmpdir, filename)
assert op.isfile(outpath)

for tabletype in cres.tables.keys():
filename = tabletype + ".tsv"
outpath = op.join(tmpdir, filename)
assert op.isfile(outpath)
3 changes: 2 additions & 1 deletion nimare/workflows/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Common meta-analytic workflows."""

from .ale import ale_sleuth_workflow
from .cbma import cbma_workflow
from .macm import macm_workflow

__all__ = ["ale_sleuth_workflow", "macm_workflow"]
__all__ = ["ale_sleuth_workflow", "cbma_workflow", "macm_workflow"]
166 changes: 166 additions & 0 deletions nimare/workflows/cbma.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Workflow for running an coordinates-based meta-analysis from a NiMARE database."""
import itertools
import logging
import os.path as op

from nimare.correct import Corrector, FDRCorrector, FWECorrector
from nimare.dataset import Dataset
from nimare.diagnostics import Diagnostics, FocusCounter, Jackknife
from nimare.meta import ALE, KDA, SCALE, MKDADensity
from nimare.meta.cbma.base import CBMAEstimator, PairwiseCBMAEstimator
from nimare.utils import _check_ncores, _check_type

LGR = logging.getLogger(__name__)


def _str_to_class(str_name):
"""Match a string to a class name without initializing the class."""
classes = {
"ale": ALE,
"scale": SCALE,
"mkdadensity": MKDADensity,
"kda": KDA,
"montecarlo": FWECorrector,
"fdr": FDRCorrector,
"bonferroni": FWECorrector,
"jackknife": Jackknife,
"focuscounter": FocusCounter,
}
return classes[str_name]


def _check_input(obj, clss, options, **kwargs):
"""Check input for workflow functions."""
if isinstance(obj, str):
if obj not in options:
raise ValueError(f'"estimator" of kind string must be {", ".join(options)}')

# Get the class from the string
obj_str = obj
obj = _str_to_class(obj_str)

# Add the method to the kwargs if it's a FWECorrector
if obj == FWECorrector:
kwargs["method"] = obj_str

return _check_type(obj, clss, **kwargs)


def cbma_workflow(
JulioAPeraza marked this conversation as resolved.
Show resolved Hide resolved
dataset,
estimator=None,
corrector=None,
diagnostics=None,
output_dir=None,
n_cores=1,
):
"""Compose a coordinate-based meta-analysis workflow.

.. versionadded:: 0.0.14

This workflow performs a coordinate-based meta-analysis, multiple comparison correction,
and diagnostics analyses on corrected meta-analytic maps.

Parameters
----------
dataset : :obj:`~nimare.dataset.Dataset`
Dataset for which to run meta-analyses to generate maps.
estimator : :class:`~nimare.base.CBMAEstimator`, :obj:`str` {'ale', 'scale', 'mkdadensity', \
'kda'}, or optional
Meta-analysis estimator. Default is :class:`~nimare.meta.cbma.ale.ALE`.
corrector : :class:`~nimare.correct.Corrector`, :obj:`str` {'montecarlo', 'fdr', \
'bonferroni'} or optional
Meta-analysis corrector. Default is :class:`~nimare.correct.FWECorrector`.
diagnostics : :obj:`list` of :class:`~nimare.diagnostics.Diagnostics`, \
:class:`~nimare.diagnostics.Diagnostics`, :obj:`str` {'jackknife', 'focuscounter'}, \
or optional
List of meta-analysis diagnostic classes. A single diagnostic class can also be passed.
Default is :class:`~nimare.diagnostics.FocusCounter`.
output_dir : :obj:`str`, optional
Output directory in which to save results. If the directory doesn't
exist, it will be created. Default is None (the results are not saved).
n_cores : :obj:`int`, optional
Number of cores to use for parallelization.
If <=0, defaults to using all available cores.
If estimator, corrector, or diagnostics are passed as initialized objects, this parameter
will be ignored.
Default is 1.

Returns
-------
:obj:`~nimare.results.MetaResult`
Results of Estimator and Corrector fitting with cluster and diagnostic tables.
"""
n_cores = _check_ncores(n_cores)

if not isinstance(diagnostics, list) and diagnostics is not None:
diagnostics = [diagnostics]

# Check dataset type
dataset = _check_type(dataset, Dataset)

# Options allows for string input
estm_options = ("ale", "scale", "mkdadensity", "kda")
corr_options = ("montecarlo", "fdr", "bonferroni")
diag_options = ("jackknife", "focuscounter")

# Check inputs and set defaults if input is None
estimator = (
ALE(n_cores=n_cores)
if estimator is None
else _check_input(estimator, CBMAEstimator, estm_options, n_cores=n_cores)
)
corrector = (
FWECorrector(method="montecarlo", n_cores=n_cores)
if corrector is None
else _check_input(corrector, Corrector, corr_options, n_cores=n_cores)
)

if diagnostics is None:
diagnostics = [Jackknife(n_cores=n_cores)]
else:
diagnostics = [
_check_input(diagnostic, Diagnostics, diag_options, n_cores=n_cores)
for diagnostic in diagnostics
]

if isinstance(estimator, PairwiseCBMAEstimator):
raise AttributeError(
'The "cbma_workflow" function does not currently work with pairwise Estimators.'
)

LGR.info("Performing meta-analysis...")
results = estimator.fit(dataset)

LGR.info("Performing correction on meta-analysis...")
corr_results = corrector.transform(results)

LGR.info("Generating clusters tables and performing diagnostics on corrected meta-analyses...")
img_keys = [
img_key
for img_key in corr_results.maps.keys()
if img_key.startswith("z_") and ("_corr-" in img_key)
]
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

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

boilerplate = corr_results.description_
JulioAPeraza marked this conversation as resolved.
Show resolved Hide resolved
with open(op.join(output_dir, "boilerplate.txt"), "w") as fo:
fo.write(boilerplate)

bibtex = corr_results.bibtex_
with open(op.join(output_dir, "references.bib"), "w") as fo:
fo.write(bibtex)

LGR.info("Workflow completed.")
return corr_results