Skip to content

Commit 4f687a8

Browse files
authored
Add advanced plots to IBMA report (#864)
* Add Summary Stats and N True Voxels advanced plots * Save raw_data to use in Reports * Add dropdown advanced menu for percentage of voxels and summary stats * Update figures.py * Update base.py * Update figures.py * Update ibma.py * Update figures.py * Update figures.py * Update figures.py * Add y-axis ticks label * save inputs_ * Scipy latest only support Python >=3.9. Build the documentation with 3.9 * [skip ci] test documentation build in python 3.9 * Update figures.py
1 parent eb19ff6 commit 4f687a8

6 files changed

Lines changed: 176 additions & 14 deletions

File tree

.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ version: 2
88
build:
99
os: "ubuntu-22.04"
1010
tools:
11-
python: "3.8"
11+
python: "3.9"
1212

1313
# Build documentation in the docs/ directory with Sphinx
1414
sphinx:

nimare/meta/ibma.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,7 @@ def _preprocess_input(self, dataset):
114114
# Mask required input images using either the dataset's mask or the estimator's.
115115
temp_arr = masker.transform(img4d)
116116

117-
data = masker.transform(img4d)
118-
temp_image_inputs[name] = data
117+
temp_image_inputs[name] = temp_arr
119118
if self.aggressive_mask:
120119
# An intermediate step to mask out bad voxels.
121120
# Can be dropped once PyMARE is able to handle masked arrays or missing data.
@@ -132,8 +131,8 @@ def _preprocess_input(self, dataset):
132131
good_voxels_bool,
133132
)
134133
else:
135-
self.inputs_[name] = data # This data is saved only to use in Reports
136-
data_bags = zip(*_apply_liberal_mask(data))
134+
self.inputs_[name] = temp_arr
135+
data_bags = zip(*_apply_liberal_mask(temp_arr))
137136

138137
keys = ["values", "voxel_mask", "study_mask"]
139138
self.inputs_["data_bags"][name] = [dict(zip(keys, bag)) for bag in data_bags]
@@ -153,6 +152,8 @@ def _preprocess_input(self, dataset):
153152
for name, raw_masked_data in temp_image_inputs.items():
154153
self.inputs_[name] = raw_masked_data[:, self.inputs_["aggressive_mask"]]
155154

155+
self.inputs_["raw_data"] = temp_image_inputs # This data is saved only to use in Reports
156+
156157

157158
class Fishers(IBMAEstimator):
158159
"""An image-based meta-analytic test using t- or z-statistic images.

nimare/reports/base.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
_plot_dof_map,
3636
_plot_relcov_map,
3737
_plot_ridgeplot,
38+
_plot_sumstats,
39+
_plot_true_voxels,
3840
gen_table,
3941
plot_clusters,
4042
plot_coordinates,
@@ -378,11 +380,17 @@ def __init__(self, out_dir, config=None):
378380
ext = "".join(src.suffixes)
379381
desc_text = config.get("caption")
380382
iframe = config.get("iframe", False)
383+
dropdown = config.get("dropdown", False)
381384

382385
contents = None
383386
html_anchor = src.relative_to(out_dir)
384387
if ext == ".html":
385388
contents = IFRAME_SNIPPET.format(html_anchor) if iframe else src.read_text()
389+
if dropdown:
390+
contents = (
391+
f"<details><summary>Advanced ({self.title})</summary>{contents}</details>"
392+
)
393+
self.title = ""
386394
elif ext == ".png":
387395
contents = PNG_SNIPPET.format(html_anchor)
388396

@@ -475,16 +483,19 @@ def __init__(
475483
)
476484
elif meta_type == "IBMA":
477485
# Use "z_maps", for Fishers, and Stouffers; otherwise use "beta_maps".
478-
key_maps = "z_maps" if "z_maps" in self.results.estimator.inputs_ else "beta_maps"
479-
maps_arr = self.results.estimator.inputs_[key_maps]
486+
key_maps = (
487+
"z_maps"
488+
if "z_maps" in self.results.estimator.inputs_["raw_data"]
489+
else "beta_maps"
490+
)
491+
maps_arr = self.results.estimator.inputs_["raw_data"][key_maps]
480492
ids_ = self.results.estimator.inputs_["id"]
481493
x_label = "Z" if key_maps == "z_maps" else "Beta"
482494

483495
if self.results.estimator.aggressive_mask:
484496
_plot_relcov_map(
485497
maps_arr,
486498
self.results.estimator.masker,
487-
self.results.estimator.inputs_["aggressive_mask"],
488499
self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-relcov.png",
489500
)
490501
else:
@@ -494,13 +505,25 @@ def __init__(
494505
self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-dof.png",
495506
)
496507

508+
_plot_true_voxels(
509+
maps_arr,
510+
ids_,
511+
self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-truevoxels.html",
512+
)
513+
497514
_plot_ridgeplot(
498515
maps_arr,
499516
ids_,
500517
x_label,
501518
self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-ridgeplot.html",
502519
)
503520

521+
_plot_sumstats(
522+
maps_arr,
523+
ids_,
524+
self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-summarystats.html",
525+
)
526+
504527
similarity_table = _compute_similarities(maps_arr, ids_)
505528

506529
plot_heatmap(

nimare/reports/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,14 @@ sections:
5555
title: Relative Coverage Map
5656
- bids: {value: preliminary, dset: 1, suffix: figure-dof}
5757
title: DoF Map
58+
- bids: {value: preliminary, dset: 1, suffix: figure-truevoxels}
59+
title: Percentage of Valid Voxels
60+
dropdown: True
5861
- bids: {value: preliminary, dset: 1, suffix: figure-ridgeplot}
5962
title: Ridge Plot
63+
- bids: {value: preliminary, dset: 1, suffix: figure-summarystats}
64+
title: Summary Statistics
65+
dropdown: True
6066
- bids: {value: preliminary, dset: 1, suffix: figure-similarity}
6167
title: Similarity of Input Maps
6268
- bids: {value: preliminary, dset: 2, suffix: summary}

nimare/reports/figures.py

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
view_img,
1616
)
1717
from ridgeplot import ridgeplot
18+
from scipy import stats
1819
from scipy.cluster.hierarchy import leaves_list, linkage, optimal_leaf_ordering
1920

20-
from nimare.utils import _boolean_unmask
21-
2221
TABLE_STYLE = [
2322
dict(
2423
selector="th, td",
@@ -402,6 +401,60 @@ def plot_clusters(img, out_filename):
402401
fig.close()
403402

404403

404+
def _plot_true_voxels(maps_arr, ids_, out_filename):
405+
"""Plot percentage of valid voxels.
406+
407+
.. versionadded:: 0.2.2
408+
409+
"""
410+
n_studies, n_voxels = maps_arr.shape
411+
mask = ~np.isnan(maps_arr) & (maps_arr != 0)
412+
413+
x_label, y_label = "Voxels Included", "ID"
414+
perc_voxs = mask.sum(axis=1) / n_voxels
415+
valid_df = pd.DataFrame({y_label: ids_, x_label: perc_voxs})
416+
valid_sorted_df = valid_df.sort_values(x_label, ascending=True)
417+
418+
fig = px.bar(
419+
valid_sorted_df,
420+
x=x_label,
421+
y=y_label,
422+
orientation="h",
423+
color=x_label,
424+
color_continuous_scale="blues",
425+
range_color=(0, 1),
426+
)
427+
428+
fig.update_xaxes(
429+
showline=True,
430+
linewidth=2,
431+
linecolor="black",
432+
visible=True,
433+
showticklabels=False,
434+
title=None,
435+
)
436+
fig.update_yaxes(
437+
showline=True,
438+
linewidth=2,
439+
linecolor="black",
440+
visible=True,
441+
ticktext=valid_sorted_df[y_label].str.slice(0, MAX_CHARS).tolist(),
442+
)
443+
444+
height = n_studies * PXS_PER_STD
445+
fig.update_layout(
446+
height=height,
447+
autosize=True,
448+
font_size=14,
449+
plot_bgcolor="white",
450+
xaxis_gridcolor="white",
451+
yaxis_gridcolor="white",
452+
xaxis_gridwidth=2,
453+
showlegend=False,
454+
)
455+
fig.write_html(out_filename, full_html=True, include_plotlyjs=True)
456+
457+
405458
def _plot_ridgeplot(maps_arr, ids_, x_label, out_filename):
406459
"""Plot histograms of the images.
407460
@@ -446,7 +499,88 @@ def _plot_ridgeplot(maps_arr, ids_, x_label, out_filename):
446499
fig.write_html(out_filename, full_html=True, include_plotlyjs=True)
447500

448501

449-
def _plot_relcov_map(maps_arr, masker, aggressive_mask, out_filename):
502+
def _plot_sumstats(maps_arr, ids_, out_filename):
503+
"""Plot summary statistics of the images.
504+
505+
.. versionadded:: 0.2.2
506+
507+
"""
508+
n_studies = len(ids_)
509+
mask = ~np.isnan(maps_arr) & (maps_arr != 0)
510+
maps_lst = [maps_arr[i][mask[i]] for i in range(n_studies)]
511+
512+
stats_lbls = [
513+
"Mean",
514+
"STD",
515+
"Var",
516+
"Median",
517+
"Mode",
518+
"Min",
519+
"Max",
520+
"Skew",
521+
"Kurtosis",
522+
"Range",
523+
"Moment",
524+
"IQR",
525+
]
526+
scores, id_lst = [], []
527+
for id_, map_ in zip(ids_, maps_lst):
528+
scores.extend(
529+
[
530+
np.mean(map_),
531+
np.std(map_),
532+
np.var(map_),
533+
np.median(map_),
534+
stats.mode(map_)[0],
535+
np.min(map_),
536+
np.max(map_),
537+
stats.skew(map_),
538+
stats.kurtosis(map_),
539+
np.max(map_) - np.min(map_),
540+
stats.moment(map_, moment=4),
541+
stats.iqr(map_),
542+
]
543+
)
544+
id_lst.extend([id_] * len(stats_lbls))
545+
546+
stats_labels = stats_lbls * n_studies
547+
data_df = pd.DataFrame({"ID": id_lst, "Score": scores, "Stat": stats_labels})
548+
549+
fig = px.strip(
550+
data_df,
551+
y="Score",
552+
color="ID",
553+
facet_col="Stat",
554+
stripmode="group",
555+
facet_col_wrap=4,
556+
facet_col_spacing=0.08,
557+
)
558+
559+
fig.update_xaxes(showline=True, linewidth=2, linecolor="black", mirror=True)
560+
fig.update_yaxes(
561+
constrain="domain",
562+
matches=None,
563+
showline=True,
564+
linewidth=2,
565+
linecolor="black",
566+
mirror=True,
567+
title=None,
568+
)
569+
fig.update_layout(
570+
height=900,
571+
autosize=True,
572+
font_size=14,
573+
plot_bgcolor="white",
574+
xaxis_gridcolor="white",
575+
yaxis_gridcolor="white",
576+
xaxis_gridwidth=2,
577+
showlegend=False,
578+
)
579+
fig.for_each_yaxis(lambda yaxis: yaxis.update(showticklabels=True))
580+
fig.write_html(out_filename, full_html=True, include_plotlyjs=True)
581+
582+
583+
def _plot_relcov_map(maps_arr, masker, out_filename):
450584
"""Plot relative coverage map.
451585
452586
.. versionadded:: 0.2.0
@@ -460,8 +594,6 @@ def _plot_relcov_map(maps_arr, masker, aggressive_mask, out_filename):
460594
binary_maps_arr = np.where((-epsilon > maps_arr) | (maps_arr > epsilon), 1, 0)
461595
coverage_arr = np.sum(binary_maps_arr, axis=0) / binary_maps_arr.shape[0]
462596

463-
# Add bad voxels back to the arr to transform it back to an image
464-
coverage_arr = _boolean_unmask(coverage_arr, aggressive_mask)
465597
coverage_img = masker.inverse_transform(coverage_arr)
466598

467599
# Plot coverage map

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ doc =
7777
sphinx>=3.5
7878
sphinx-argparse
7979
sphinx-copybutton
80-
sphinx_gallery==0.10.1
80+
sphinx-gallery
8181
sphinx_rtd_theme>=1.3.0
8282
sphinxcontrib-bibtex
8383
tests =

0 commit comments

Comments
 (0)