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 Workflow and CBMAWorkflow classes. Support pairwise CBMA workflows #809

Merged
merged 40 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cb1fa66
Convert cbma_workflow into a class. Support pairwise estimators
JulioAPeraza May 31, 2023
72e1ef4
Add support for reports and diagnostics with pairwise estimator
JulioAPeraza Jun 1, 2023
adfb9f1
fix dicstring
JulioAPeraza Jun 2, 2023
48b5aed
add deprecation warning
JulioAPeraza Jun 2, 2023
5ffa968
fix test for tables == None
JulioAPeraza Jun 2, 2023
7a40379
add version changes to docstring
JulioAPeraza Jun 2, 2023
73fc15e
fix documentation
JulioAPeraza Jun 2, 2023
8839cf6
improve coverage
JulioAPeraza Jun 2, 2023
9b77e9b
Use specificity maps instead
JulioAPeraza Jun 7, 2023
9fb98cc
Merge _preprocess_input
JulioAPeraza Jun 7, 2023
5455231
remove Workflow from __init__
JulioAPeraza Jun 8, 2023
ebd4b13
Add Pairwise estimator report
JulioAPeraza Jun 8, 2023
56ec484
update diagnostics
JulioAPeraza Jun 8, 2023
78bcb0e
reduce the number of iterations. we are running out of time/memory in…
JulioAPeraza Jun 8, 2023
1d25b02
Merge branch 'neurostuff:main' into enh-workflow
JulioAPeraza Jun 8, 2023
2268f17
see if using focuscounter reduces the time for building the documenta…
JulioAPeraza Jun 8, 2023
c7a7bf8
New parameter display_second_group
JulioAPeraza Jun 12, 2023
b08e712
Update 08_plot_cbma_subtraction_conjunction.py
JulioAPeraza Jun 12, 2023
a3573af
Add dataset 2 to summary
JulioAPeraza Jun 12, 2023
6c50469
Update diagnostics.py
JulioAPeraza Jun 12, 2023
3447502
Update versionchanged
JulioAPeraza Jun 12, 2023
5d33e92
Merge branch 'neurostuff:main' into enh-workflow
JulioAPeraza Jun 12, 2023
5630c9f
Reorder matrix only if more than 1 cluster/experiment
JulioAPeraza Jun 12, 2023
4f18d84
display_second_group in the example
JulioAPeraza Jun 13, 2023
98bc738
fix #814
JulioAPeraza Jun 13, 2023
9b950e3
Use iframe only for connectome
JulioAPeraza Jun 13, 2023
363effd
Set the size of the heatmap proportional to rows and columns
JulioAPeraza Jun 13, 2023
fa5d876
Separate positive from negative tail contribution table for pairwise …
JulioAPeraza Jun 13, 2023
385091d
Add subsubtitle
JulioAPeraza Jun 13, 2023
761ee12
Merge branch 'main' into enh-workflow
JulioAPeraza Jun 14, 2023
b923cd9
Test a realistic scenario with different dset1 and desert 2
JulioAPeraza Jun 16, 2023
73347ad
Apply @jdkent code review
JulioAPeraza Jun 16, 2023
e7360ed
consider the length of the study label in the figure size
JulioAPeraza Jun 21, 2023
afdf4a4
fix issues with figure sizes
JulioAPeraza Jun 21, 2023
fbd7ae4
Define "PositiveTail" and "NegativeTail" as variables
JulioAPeraza Jun 21, 2023
a4e8acd
Update diagnostics.py
JulioAPeraza Jun 21, 2023
883fa6e
Make a distinction between studies and experiments in report
JulioAPeraza Jun 21, 2023
cc1a3e0
Restore the diagnostics summary
JulioAPeraza Jun 21, 2023
8ac8e50
Limit the colormap to the total number of clusters
JulioAPeraza Jun 21, 2023
d46e3ca
Update 08_plot_cbma_subtraction_conjunction.py
JulioAPeraza Jun 22, 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
4 changes: 3 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,9 @@ For more information about fetching data from the internet, see :ref:`fetching t

workflows.ale_sleuth_workflow
workflows.macm_workflow
workflows.cbma_workflow
workflows.base.Workflow
workflows.cbma.CBMAWorkflow
workflows.cbma.PairwiseCBMAWorkflow

:mod:`nimare.reports`: NiMARE report
--------------------------------------------------
Expand Down
13 changes: 7 additions & 6 deletions examples/02_meta-analyses/10_plot_cbma_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from nimare.dataset import Dataset
from nimare.reports.base import run_reports
from nimare.utils import get_resource_path
from nimare.workflows import cbma_workflow
from nimare.workflows.cbma import CBMAWorkflow

###############################################################################
# Load Dataset
Expand All @@ -32,25 +32,26 @@
###############################################################################
# Run CBMA Workflow
# -----------------------------------------------------------------------------
# The CBMA workflow function runs the following steps:
# The fit method of a CBMA workflow class 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!
# All in one call!
#
# result = cbma_workflow(dset)
# result = CBMAWorkflow().fit(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")
workflow = CBMAWorkflow(corrector="fdr")
result = workflow.fit(dset)

###############################################################################
# Plot Results
# -----------------------------------------------------------------------------
# The CBMA workflow function returns a :class:`~nimare.results.MetaResult` object,
# The fit method of the CBMA workflow class returns a :class:`~nimare.results.MetaResult` object,
# where you can access the corrected results of the meta-analysis and diagnostics tables.
#
# Corrected map:
Expand Down
75 changes: 46 additions & 29 deletions nimare/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from tqdm.auto import tqdm

from nimare.base import NiMAREBase
from nimare.meta.cbma.base import PairwiseCBMAEstimator
from nimare.meta.ibma import IBMAEstimator
from nimare.utils import _check_ncores, get_masker, mm2vox, tqdm_joblib

LGR = logging.getLogger(__name__)
Expand Down Expand Up @@ -109,19 +111,10 @@ def transform(self, result):
correspond to positive and negative tails.
If no clusters are found, this list will be empty.
"""
pairwaise_estimators = issubclass(type(result.estimator), PairwiseCBMAEstimator)
masker = result.estimator.masker
diag_name = self.__class__.__name__

none_contribution_table = False
if not hasattr(result.estimator, "dataset"):
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. "
"The ``contribution_table`` will be returned as ``None``."
)
none_contribution_table = True

# Collect the thresholded cluster map
if self.target_image in result.maps:
target_img = result.get_map(self.target_image, return_type="image")
Expand Down Expand Up @@ -160,7 +153,7 @@ def transform(self, result):

# 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
image_name = f"_{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 = (
Expand All @@ -170,7 +163,7 @@ def transform(self, result):
)

# Check number of clusters
if (n_clusters == 0) or none_contribution_table:
if n_clusters == 0:
result.tables[clusters_table_name] = clusters_table
result.tables[contribution_table_name] = None
result.maps[label_map_names[0]] = None
Expand All @@ -180,7 +173,12 @@ def transform(self, 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"]
# Use only id1 for pairwise estimators.
meta_ids = (
result.estimator.inputs_["id1"]
if pairwaise_estimators
else result.estimator.inputs_["id"]
)
rows = list(meta_ids)

contribution_tables = []
Expand All @@ -204,8 +202,14 @@ def transform(self, result):

contribution_tables.append(contribution_table.reset_index())

# Concat PositiveTail and NegativeTail tables
contribution_table = pd.concat(contribution_tables, ignore_index=True, sort=False)
if pairwaise_estimators or len(label_maps) == 1:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For MKDAChi2, the z-values can be positive or negative (you can check the z-map results from running the test in this pull request: #811).

I think we would want to display both Positive and Negative Tail cluster with Pairwise estimators.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would want to display both Positive and Negative Tail cluster with Pairwise estimators.

We are displaying Positive and Negative tail cluster tables and maps. Here, I'm only excluding the Negative tail contribution table, which will be zero for studies in id1. I think if we want to display that table we would need to plot it in a separate figure and use the id2 for estimating the values. WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also in the report, we only show the summary for id1 and coordinates1 from dataset1. Should we include another section for the second group?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little more questionable, overall, I would say yes, we do want to display id2 for the coordinates. for the contribution table, this is a bit debatable. With ALESubtraction, yes, I think it is useful to know how both groups contributed to the existing clusters.

For the MKDAChi2 I think it is less useful to see a contribution table (and with jacknife it would be computationally prohibitive if one was using the entire neurosynth dataset).

But the notion that ALESubtraction is used for more similarly sized groups and MKDAChi2 is used for comparing a relatively smaller dataset to a huge dataset is only reflective of how we are currently the algorithms, there's nothing stopping you from using ALESubtraction with neurosynth.

So I think the answer would be creating an option/flag for the user to decide whether they want to show the second group or not. could be called display_second_group or something, with a default value of False.

# Only export PositiveTail table for pairwise estimators
contribution_table = contribution_tables[0]
else:
# Merge PositiveTail and NegativeTail tables
contribution_table = pd.merge(
contribution_tables[0], contribution_tables[1], on="id", sort=False
)

# Save tables and maps to result
diag_tables_dict = {
Expand All @@ -228,14 +232,18 @@ def transform(self, result):
class Jackknife(Diagnostics):
"""Run a jackknife analysis on a meta-analysis result.

.. versionchanged:: 0.1.1

* Support for pairwise meta-analyses.

.. versionchanged:: 0.0.14

* New parameter: `cluster_threshold`.
* Return clusters table.

.. versionchanged:: 0.0.13

Change cluster neighborhood from faces+edges to faces, to match Nilearn.
* Change cluster neighborhood from faces+edges to faces, to match Nilearn.

.. versionadded:: 0.0.11

Expand All @@ -246,11 +254,6 @@ class Jackknife(Diagnostics):
statistic for all experiments *except* the target experiment, dividing the resulting test
summary statistics by the summary statistics from the original meta-analysis, and finally
averaging the resulting proportion values across all voxels in each cluster.

Warnings
--------
Pairwise meta-analyses, like ALESubtraction and MKDAChi2, are not yet supported in this
method.
"""

def _transform(self, expid, label_map, result):
Expand All @@ -272,11 +275,11 @@ def _transform(self, expid, label_map, result):
"""
# We need to copy the estimator because it will otherwise overwrite the original version
# with one missing a study in its inputs.
pairwaise_estimators = issubclass(type(result.estimator), PairwiseCBMAEstimator)
estimator = copy.deepcopy(result.estimator)

dset = estimator.dataset
all_ids = estimator.inputs_["id1"] if pairwaise_estimators else estimator.inputs_["id"]
original_masker = estimator.masker
all_ids = estimator.inputs_["id"]

# Mask using a labels masker, so that we can easily get the mean value for each cluster
cluster_masker = input_data.NiftiLabelsMasker(label_map)
Expand All @@ -288,15 +291,21 @@ def _transform(self, expid, label_map, result):
target_value_map = "est"
elif "stat" in result.maps:
target_value_map = "stat"
elif "z_desc-consistency" in result.maps:
JulioAPeraza marked this conversation as resolved.
Show resolved Hide resolved
target_value_map = "z_desc-consistency"
else:
target_value_map = "z"

stat_values = result.get_map(target_value_map, return_type="array")

# Fit Estimator to all studies except the target study
other_ids = [id_ for id_ in all_ids if id_ != expid]
temp_dset = dset.slice(other_ids)
temp_result = estimator.fit(temp_dset)
if pairwaise_estimators:
temp_dset = estimator.dataset1.slice(other_ids)
temp_result = estimator.fit(temp_dset, estimator.dataset2)
else:
temp_dset = estimator.dataset.slice(other_ids)
temp_result = estimator.fit(temp_dset)

# Collect the target values (e.g., ALE values) from the N-1 meta-analysis
temp_stat_img = temp_result.get_map(target_value_map, return_type="image")
Expand All @@ -317,6 +326,10 @@ def _transform(self, expid, label_map, result):
class FocusCounter(Diagnostics):
"""Run a focus-count analysis on a coordinate-based meta-analysis result.

.. versionchanged:: 0.1.1

* Support for pairwise meta-analyses.

.. versionchanged:: 0.0.14

* New parameter: `cluster_threshold`.
Expand All @@ -337,9 +350,6 @@ class FocusCounter(Diagnostics):
Warnings
--------
This method only works for coordinate-based meta-analyses.

Pairwise meta-analyses, like ALESubtraction and MKDAChi2, are not yet supported in this
method.
"""

def _transform(self, expid, label_map, result):
Expand All @@ -359,11 +369,18 @@ def _transform(self, expid, label_map, result):
stat_prop_values : 1D :obj:`numpy.ndarray`
1D array with the contribution of `expid` in each cluster of `label_map`.
"""
if issubclass(type(result.estimator), IBMAEstimator):
raise ValueError("This method only works for coordinate-based meta-analyses.")

affine = label_map.affine
label_arr = label_map.get_fdata()
clust_ids = sorted(list(np.unique(label_arr)[1:]))

coordinates_df = result.estimator.inputs_["coordinates"]
coordinates_df = (
result.estimator.inputs_["coordinates1"]
if issubclass(type(result.estimator), PairwiseCBMAEstimator)
else result.estimator.inputs_["coordinates"]
)
coords = coordinates_df.loc[coordinates_df["id"] == expid]
ijk = mm2vox(coords[["x", "y", "z"]], affine)

Expand Down
19 changes: 15 additions & 4 deletions nimare/reports/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import jinja2
from pkg_resources import resource_filename as pkgrf

from nimare.meta.cbma.base import CBMAEstimator
from nimare.meta.cbma.base import CBMAEstimator, PairwiseCBMAEstimator
from nimare.reports.figures import (
gen_table,
plot_clusters,
Expand Down Expand Up @@ -76,6 +76,7 @@
"fdr": "False discovery rate (FDR) correction",
"method": "Method",
"alpha": "Alpha",
"prior": "Prior",
}

SUMMARY_TEMPLATE = """\
Expand All @@ -93,6 +94,7 @@

ESTIMATOR_TEMPLATE = """\
<ul class="elem-desc">
<li>Estimator: {est_name}</li>
<li>Kernel Transformer: {kernel_transformer}{ker_params_text}</li>
{est_params_text}
</ul>
Expand Down Expand Up @@ -133,7 +135,10 @@ def _gen_est_summary(obj, out_filename):
est_params_text.append(f"<li>{PARAMETERS_DICT[k]}: {v}</li>")
est_params_text = "".join(est_params_text)

est_name = obj.__class__.__name__

summary_text = ESTIMATOR_TEMPLATE.format(
est_name=est_name,
kernel_transformer=str(params_dict["kernel_transformer"]),
ker_params_text=ker_params_text,
est_params_text=est_params_text,
Expand Down Expand Up @@ -359,19 +364,25 @@ def __init__(
self.fig_dir = self.out_dir / "figures"
self.fig_dir.mkdir(parents=True, exist_ok=True)

dataset = (
self.results.estimator.dataset1
if issubclass(type(self.results.estimator), PairwiseCBMAEstimator)
else self.results.estimator.dataset
)

# Generate summary text
_gen_summary(self.results.estimator.dataset, self.fig_dir / "preliminary_summary.html")
_gen_summary(dataset, self.fig_dir / "preliminary_summary.html")

# Plot mask
plot_mask(
self.results.estimator.dataset.masker.mask_img,
dataset.masker.mask_img,
self.fig_dir / "preliminary_figure-mask.png",
)

if issubclass(type(self.results.estimator), CBMAEstimator):
# Plot coordinates for CBMA estimators
plot_coordinates(
self.results.estimator.dataset.coordinates,
dataset.coordinates,
self.fig_dir / "preliminary_figure-static.png",
self.fig_dir / "preliminary_figure-interactive.html",
self.fig_dir / "preliminary_figure-legend.png",
Expand Down
5 changes: 2 additions & 3 deletions nimare/reports/figures.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _reorder_matrix(mat, row_labels, col_labels, reorder):

# Order columns
col_linkage_matrix = linkage(mat.T, method=reorder)
col_ordered_linkage = optimal_leaf_ordering(col_linkage_matrix, mat)
col_ordered_linkage = optimal_leaf_ordering(col_linkage_matrix, mat.T)
col_index = leaves_list(col_ordered_linkage)

# Make sure labels is an ndarray and copy it
Expand Down Expand Up @@ -124,7 +124,6 @@ def plot_static_brain(img, out_filename, threshold=1e-06):
black_bg=False,
draw_cross=False,
threshold=threshold,
vmax=4,
display_mode="mosaic",
)
fig.savefig(out_filename, dpi=300)
Expand Down Expand Up @@ -251,7 +250,7 @@ def plot_interactive_brain(img, out_filename, threshold=1e-06):
_check_extention(out_filename, [".html"])

template = datasets.load_mni152_template(resolution=1)
html_view = view_img(img, bg_img=template, black_bg=False, threshold=threshold, vmax=4)
html_view = view_img(img, bg_img=template, black_bg=False, threshold=threshold)
html_view.save_as_html(out_filename)


Expand Down
Loading