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

Support clusters table in Diagnostics #765

Merged
merged 20 commits into from
Mar 10, 2023

Conversation

JulioAPeraza
Copy link
Collaborator

Closes None. With this PR we intend to remove reporting.get_clusters_table from the CBMA workflow (#761).

Changes proposed in this pull request:

  • Jackknife and FocusCounter now return the clusters table in addition to the contribution table.

For a future PR it would be nice to refactor both Jackknife and FocusCounter to reduce some of the duplicated code.

@JulioAPeraza JulioAPeraza added the enhancement New feature or request label Feb 1, 2023
@codecov
Copy link

codecov bot commented Feb 1, 2023

Codecov Report

❗ No coverage uploaded for pull request base (main@bd60a9e). Click here to learn what that means.
Patch coverage: 96.77% of modified lines in pull request are covered.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #765   +/-   ##
=======================================
  Coverage        ?   88.85%           
=======================================
  Files           ?       39           
  Lines           ?     4405           
  Branches        ?        0           
=======================================
  Hits            ?     3914           
  Misses          ?      491           
  Partials        ?        0           
Impacted Files Coverage Δ
nimare/diagnostics.py 93.91% <92.85%> (ø)
nimare/utils.py 95.53% <98.95%> (ø)
nimare/workflows/ale.py 93.42% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@tsalo
Copy link
Member

tsalo commented Feb 2, 2023

Since this changes the numbers of outputs for the two methods, I'd label this PR as a breaking change. Alternatively, you could transpose the diagnostic tables and just tack on the clusters table as additional columns.

@JulioAPeraza JulioAPeraza added the breaking-change PRs that change results or interfaces. label Feb 2, 2023
@@ -139,7 +150,6 @@ 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 = estimator.inputs_["id"]
rows = ["Center of Mass"] + list(meta_ids)

# Let's label the clusters in the thresholded map so we can use it as a NiftiLabelsMasker
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Currently get_clusters_table only return the clusters table, so we need to re-run ndimage.label (reproducing the code in get_clusters_table). There is a current PR in nilearn to return the label map, but that won't be available until the next release.

Copy link
Member

Choose a reason for hiding this comment

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

could you link this pull request in another issue so we can revisit this when nilearn makes another release?

Copy link
Member

@jdkent jdkent left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Member

@tsalo tsalo left a comment

Choose a reason for hiding this comment

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

I think this is really close. Can you just double-check that the ndimage.generate_binary_structure and ndimage.label calls are the same as nilearn? I think if they are, then the cluster labels will always match up between the clusters table and the contributions table.

knowledge_count_table, _ = counter.transform(knowledge_corrected_results)
knowledge_count_table, _, _ = counter.transform(knowledge_corrected_results)
Copy link
Member

Choose a reason for hiding this comment

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

Can you show the clusters table in the example?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should we show the clusters table for both the FocusCounter and Jackknife in this example?

@JulioAPeraza
Copy link
Collaborator Author

Thanks, @tsalo!
I think ndimage.generate_binary_structure runs with the same parameters:
nilearn:

# Define array for 6-connectivity, aka NN1 or "faces"
bin_struct = generate_binary_structure(rank=3, connectivity=1)

nimare:

conn = ndimage.generate_binary_structure(rank=3, connectivity=1)

The only difference is in ndimage.label, where nilearn labels positive and negative maps separately and runs on binarized arrays, whereas nimare doesn't make a distinction between positive and negative maps explicitly and runs on the thresholded but non-binarized arrays.
nilearn:

label_map = label(binarized, bin_struct)[0]

nimare:

labeled_cluster_arr, n_clusters = ndimage.label(thresh_arr, conn)

@jdkent
Copy link
Member

jdkent commented Feb 21, 2023

My recommendation is to copy the code from the nilearn pull request and incorporate into nimare until the next nilearn release. Then we can copy how nilearn combines positive and negative clusters into a single table and replicate the order in the contribution tables.

@JulioAPeraza
Copy link
Collaborator Author

Sounds good!
I was looking at reporting.get_clusters_table and noticed that for two_sided=True, it lists the clusters with positive values first and then the clusters with negative values. However, the cluster ID is reset, so we will find the first positive and negative clusters sharing the same "Cluster ID"=1.
In the contribution table, we use the cluster ID as the column identifier. Probably, we will need to use a new name for the columns. What do you think?

@JulioAPeraza
Copy link
Collaborator Author

Another option would be to split the clusters table into two DFs: one for positive clusters and one for negative clusters. Then, the diagnostic will return three tuples:

return contribution_tables, clusters_tables, labeled_cluster_imgs

where:

contribution_tables = (pos_contribution_table, neg_contribution_table)
clusters_tables = (pos_clusters_table, neg_clusters_table)
labeled_cluster_imgs = (pos_labeled_cluster_img, neg_labeled_cluster_img)

We can check if there are negative values in target_img and run reporting.get_clusters_table with two_sided=True, or add two_sided to the list of parameters in Jackknife and FocusCounter.

@JulioAPeraza
Copy link
Collaborator Author

@tsalo @jdkent
I was trying to use get_clusters_tables to get both the clusters table and label maps. We then use the label maps in _transform to get the contribution table.
However, I'm noticing a mismatch in the Cluster ID and the labels in the label_map. This is because clust_ids is sorted based on the peak_vals, but I don't think the label maps are relabeled too: https://github.com/nilearn/nilearn/blob/main/nilearn/reporting/_get_clusters_table.py#L340.

Do you think this is the desired behavior or it is a bug in get_clusters_tables?

@tsalo
Copy link
Member

tsalo commented Mar 2, 2023

I would guess it's a bug. Are you sure that nilearn/nilearn#3477 didn't address the problem?

@JulioAPeraza
Copy link
Collaborator Author

JulioAPeraza commented Mar 2, 2023

I do not think that PR solved the problem. The label_maps wasn't part of the output before, so there was no need to relabel the maps.

If that's the case, perhaps we can solve the issue in our copy of get_clusters_tables by either changing the id (c_id + 1, with c_val) here and here. Or by relabeling the label_map:

re_label_map = np.zeros_like(label_map)
for c_id, c_val in enumerate(clust_ids):
    cluster_mask = label_map == c_val
    re_label_map[cluster_mask] = c_id + 1

What do you think?

@JulioAPeraza
Copy link
Collaborator Author

@tsalo @jdkent, I think this is ready for review. thanks!

cluster_ids = sorted(list(np.unique(label_map.get_fdata())[1:]))

# Create contribution table
col_name = "PosTail" if sign == 1 else "NegTail"
Copy link
Member

Choose a reason for hiding this comment

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

I don't have a great suggestion for what the name should be, maybe it should spell out PositiveTail, because it was not immediately clear in the example notebook.

Regardless, If the fact "PosTail represents positive clusters and NegTail represents negative clusters" is added into the documentation of contribution_table then I would feel good about using whatever names you choose.

@@ -1028,6 +1039,161 @@ def unique_rows(ar, return_counts=False):
return ar[unique_row_indices]


def _local_max(data, affine, min_distance):
Copy link
Member

Choose a reason for hiding this comment

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

I like to include the permalink of the code I copied:
https://github.com/nilearn/nilearn/blob/8b16d36bbf0f1b88b9ccaea2da1f2867024e16d5/nilearn/reporting/_get_clusters_table.py#L26-L116

or in your case it may be difficult since you have an open pull request to nilearn so the permalink from nilearn does not represent the fix you made for the labels map, so disregard this comment when appropriate

Copy link
Member

@jdkent jdkent left a comment

Choose a reason for hiding this comment

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

LGTM, after our meeting, I think we've settled on decent names/structure of the tables with documentation.

@jdkent jdkent merged commit c30add5 into neurostuff:main Mar 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change PRs that change results or interfaces. enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants