Skip to article content

Probing gray matter microstructure differences in neurodegenerative dementia using b-tensor encoding diffusion MRI

Back to Article
Notebook 1. Whole Brain Analysis
Download Notebook

Notebook 1. Whole Brain Analysis

# Work from this notebook's folder (content/), also when the notebook is executed from the project root
# (myst build --execute, NeuroLibre), so the local modules (helpers, stats_test, ...) and ../data/ resolve.
import os
import sys

if os.path.isdir("content") and not os.path.isfile("helpers.py"):
    os.chdir("content")
if os.getcwd() not in sys.path:
    sys.path.insert(0, os.getcwd())
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

import numpy as np
from scipy.spatial import distance

import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# Setting text svg exports as exports
plt.rcParams['svg.fonttype'] = 'none'
# rpy2 configuration
#import rpy2.robjects as ro
from rpy2.robjects.packages import importr
#from rpy2.robjects import pandas2ri
#from rpy2.robjects.conversion import localconverter

try:
    effectsize = importr('effectsize')
    marginaleffects = importr('marginaleffects')
    car = importr('car')
    print("✅ All R dependencies are loaded and ready!")
except Exception as e:
    print(f"❌ Missing R package: {e}")
✅ All R dependencies are loaded and ready!
# Import auxiliary functions bundled in this notebook
import ancova_assumptions as aa
import stats_test as st
import helpers

The next flag creates static figures that were used as a base for paper static version. Saved in static folder. If you want to create them again, set this variable to True. Otherwise, set it to False.

# Static (matplotlib/seaborn) figure versions for the Word/PDF/LaTeX exports and the journal figures.
# Written to static/figures/ only when STATIC_FIGURE_FORMATS is set (pixi tasks static-figures-png/-pdf/-svg,
# export-docx, export-tex). A plain `pixi run run-notebooks` does not write them.
create_Static_figures = bool(helpers.STATIC_FIGURE_FORMATS)

# This color pallet matches plotly figures with seaborn defaults.
color_palette = ["#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B2", "#937860", "#DA8BC3", "#8C8C8C", "#CCB974", "#64B5CD"]

Load data

df = pd.read_csv(helpers.data_path("hyades_wholeBrain_ROIs.csv"))
# Exclusion list obtained from QC the segemntation and registration of the anatomical scans. Participants with a QC level of 0 were excluded from the analyses.
exclusion_list = pd.read_csv(helpers.data_path("exclusion_list.csv")) 
print(exclusion_list)
exclusion_list = exclusion_list["participant_id"].to_list()
  participant_id                                             reason
0        sub-021  HippUnfold segmentation with a small leak arti...
1        sub-024  HippUnfold segmentation slightly failed: SRLM ...
2        sub-035  HippUnfold segmentation failed with an artifac...
3        sub-045  HippUnfold segmentation slightly failed with s...
4        sub-049  HippUnfold segmentation failed with subiculum ...
5        sub-051  HippUnfold segmentation struggled with multipl...
6        sub-057  HippUnfold segmentation failed with small leak...

Wrangling

# rename column from dataframe (just for stylec onsitency with paper)
df = df.rename(columns={'D_dki': 'MD'})

# normalized_volume is the ROI volume as a fraction of total intracranial volume (TIV); express it as % of TIV
# (same units as the hippocampal subfield volumes; F statistics, effect sizes and p-values are unaffected)
df['normalized_volume'] = df['normalized_volume'] * 100
# Drop subjects with other diagnosis than dementia
nonDemGroups = ['PDD','PSP','VCI','PD','Conc','aMCI','TBD']
df = df.query("group != @nonDemGroups")
# Drop subjects in the exclusion list
df = df.query("participant_id != @exclusion_list")
# Define the desired order of categories
group_order = ['HC', 'LBD', 'FTD', 'AD']

# Convert to ordered categorical type (the data files code healthy controls as 'Control'; the paper uses HC)
df['group'] = df['group'].replace({'Control': 'HC'})
df['group'] = pd.Categorical(df['group'], categories=group_order, ordered=True)

# harmonize color palette
group_colors = dict(zip(group_order, color_palette))
# Drop "unkown" label structre 
df = df.query("structure != 'unknown'")

Extract features to study

metadata = ["participant_id", "group", "age",'sex',"structure"]
#metrics = ["FA","D_uFA", "uFA", "Ki", "Vol_mm3_norm", "MTsat"]
metrics = ["FA","MD", "uFA", "Ki", "MTsat", "normalized_volume"]
df_brain_grouped = df[metadata + metrics]
# Define the order of the structures for plotting
structure_order = ["Parietal", "Frontal", "Temporal", "Occipital", "Cingulate", "Insula", "Hippocampus", "Amygdala"];

Mahallanobis distance by metric

def mahalanobis_distance_byMetrics(df, group2compare = "HC", metrics=None):
    """
    Calculates the Mahalanobis distance of each subject to the distribution of the 'group2compare' group
    for each unique structure, using the columns in the metrics list.

    Parameters
    ----------
    df : pd.DataFrame
        DataFrame containing participant data.
    group2compare : str
        Column name for the group to compare with.
    metrics : str
        List of metrics to use to calculate the distance.

    Returns
    -------
    pd.DataFrame
        DataFrame with original data and the Mahalanobis distance for each structure to the group2compare group.
    """
    
    group_col='group'
    structure_col='structure'

    results = []
    
    for structure in df[structure_col].unique():
        df_struct = df[df[structure_col] == structure].copy()
        group2compare_df = df_struct[df_struct[group_col] == group2compare][metrics]
        if len(group2compare_df) < 2:
            continue  # Skip if not enough subjects for covariance
        group2compare_mean = group2compare_df.mean().values
        group2compare_cov = np.cov(group2compare_df, rowvar=False)
        group2compare_cov_inv = np.linalg.pinv(group2compare_cov)
        def compute_md(row):
            return distance.mahalanobis(row[metrics], group2compare_mean, group2compare_cov_inv)
        df_struct['mahalanobis_distance'] = df_struct.apply(compute_md, axis=1)
        
        results.append(df_struct)
    return pd.concat(results, ignore_index=True)
df_brain_grouped_mahallanobis = mahalanobis_distance_byMetrics(df_brain_grouped, group2compare = "HC", metrics=metrics)
fig = px.box(
    df_brain_grouped_mahallanobis,
    x='structure',
    y='mahalanobis_distance',
    color='group',
    category_orders={
        'structure': structure_order,
        'group': group_order,
    },
    color_discrete_map=group_colors,
    labels={
        'structure': 'ROI',
        'mahalanobis_distance': 'Mahalanobis distance',
        'group': 'Group'
    },
)

# fig.update_layout(
#     width=2000,
#     height=600,
    
#     legend=dict(
#         orientation='v',
#         yanchor='middle', y=0.5,
#         xanchor='left', x=1.02,
#     ),
#     boxmode='group',
# )

fig
Loading...
if create_Static_figures:
    helpers.static_theme()
    fig, ax = plt.subplots(figsize=(helpers.TEXT_WIDTH_IN, 2.8))
    sns.boxplot(data=df_brain_grouped_mahallanobis, x='structure', y="mahalanobis_distance", hue="group",
                order=structure_order, palette=group_colors, ax=ax)
    sns.move_legend(ax, 'upper left', bbox_to_anchor=(1, 1), title="Group", frameon=False)
    ax.set_xlabel("ROI")
    ax.set_ylabel("Mahalanobis distance")
    sns.despine(fig)
    fig.tight_layout()
    helpers.save_static_figure(fig, "fig_brainRois_MhD_boxplots")
    plt.close(fig)
    sns.set_theme()

Plotting maha vs age

df_brain_grouped_mahallanobis_melted = pd.melt(df_brain_grouped_mahallanobis, id_vars=['participant_id','group','age','sex','structure'], value_vars='mahalanobis_distance',
        var_name='metric', value_name='value')

#df_brain_grouped_mahallanobis_melted
fig = px.scatter(
    df_brain_grouped_mahallanobis_melted,
    x='age',
    y='value',
    color='group',
    facet_col='structure',
    facet_col_wrap=2,
    category_orders={
        'structure': structure_order,
        'group': group_order,
    },
    color_discrete_map=group_colors,
    trendline='ols',
    labels={
        'age': 'Age',
        'value': 'Value',
        'group': 'Group',
    },
)

# sharey=False equivalent: let each facet have its own y-axis range
fig.update_yaxes(matches=None, showticklabels=True)

# px.scatter + trendline creates a duplicate legend entry per group
# (one for points, one for the "...,trendline" line) — collapse to one
seen = set()
for trace in fig.data:
    base_name = trace.name.split(',')[0]
    trace.legendgroup = base_name
    if 'trendline' in trace.name:
        trace.showlegend = False
    elif base_name in seen:
        trace.showlegend = False
    else:
        trace.name = base_name
        seen.add(base_name)

# clean up facet titles ("structure=Hippocampus" -> "Hippocampus")
fig.for_each_annotation(lambda a: a.update(text=a.text.split('=')[-1]))

fig.update_layout(
    width=1100,
    height=1600,
    legend=dict(
        orientation='v',
        yanchor='middle', y=0.5,
        xanchor='left', x=1.02,
    ),
)

fig
Loading...
if create_Static_figures:
    helpers.static_theme()
    # FacetGrid + regplot (same as lmplot) so the bootstrapped confidence bands can use a fixed seed
    p = sns.FacetGrid(df_brain_grouped_mahallanobis_melted, hue='group', palette=group_colors, col="structure",
                      col_wrap=4, col_order=structure_order, sharey=False, height=1.55, aspect=1.0)
    p.map_dataframe(sns.regplot, x='age', y='value', seed=0, scatter_kws={"s": 6})
    p.add_legend(title="Group")
    p.set_titles("{col_name}")
    p.set_axis_labels("Age (years)", "Mahalanobis distance")
    helpers.save_static_figure(p.figure, "fig_wholeBrain_ageMhD")
    plt.close(p.figure)
    sns.set_theme()

Visualization of metrics by region

qMRI_metrics = ["FA", "MD", "uFA", "Ki", "MTsat", "normalized_volume"]
default_metric = "uFA"
default_idx = qMRI_metrics.index(default_metric)

fig = go.Figure()

traces_per_metric = len(group_order)

for i, metric in enumerate(qMRI_metrics):
    if metric == 'MTsat':
        data_plot = df_brain_grouped[df_brain_grouped["MTsat"].notna()]
    else:
        data_plot = df_brain_grouped

    for group in group_order:
        data_group = data_plot[data_plot["group"] == group]
        fig.add_trace(
            go.Box(
                x=data_group["structure"],
                y=data_group[metric],
                name=group,
                legendgroup=group,
                offsetgroup=group,
                marker_color=group_colors[group],
                visible=(i == default_idx),
                showlegend=(i == default_idx),
            )
        )

fig.update_xaxes(
    categoryorder='array',
    categoryarray=structure_order,
    #tickangle=-90,
)

buttons = []
n_total = len(qMRI_metrics) * traces_per_metric
for i, metric in enumerate(qMRI_metrics):
    visible_mask = [False] * n_total
    start = i * traces_per_metric
    for j in range(traces_per_metric):
        visible_mask[start + j] = True

    buttons.append(dict(
        label=st.metric_name(metric),
        method='update',
        args=[
            {'visible': visible_mask, 'showlegend': visible_mask},
            {'yaxis': {'title': st.metric_axis_label(metric)}, 'title': f"Boxplot — {st.metric_name(metric)}"},
        ],
    ))

fig.update_layout(
    width=750,
    height=500,
    boxmode='group',
    boxgap=0.15,
    boxgroupgap=0.0,
    title=f"Boxplot — {st.metric_name(default_metric)}",
    yaxis_title=st.metric_axis_label(default_metric),
    yaxis_automargin=True,
    margin=dict(l=20, r=100, t=80, b=60),
    legend=dict(
        orientation='v',
        yanchor='middle', y=0.5,
        xanchor='left', x=1.02,
    ),
    updatemenus=[dict(
        type='dropdown',
        buttons=buttons,
        direction='down',
        x=.7, xanchor='left',
        y=1.15, yanchor='top',
        showactive=True,
        active=default_idx,
    )],
)

fig
Loading...
if create_Static_figures:
    from matplotlib.patches import Patch

    helpers.static_theme()
    # A: quantitative MRI metrics; B: normalized volume, split into larger and smaller ROIs (own y scales)
    fig = plt.figure(figsize=(helpers.TEXT_WIDTH_IN, 7.0))
    subfig_a, subfig_b = fig.subfigures(2, 1, height_ratios=[3, 1.25])
    axes = subfig_a.subplots(nrows=3, ncols=2).flatten()

    qMRI_metrics = ["FA", "MD", "uFA", "Ki", "MTsat"]
    for i, metric in enumerate(qMRI_metrics):
        data_plot = df_brain_grouped[df_brain_grouped["MTsat"].notna()] if metric == 'MTsat' else df_brain_grouped
        sns.boxplot(data=data_plot, x="structure", y=metric, hue="group", palette=group_colors, ax=axes[i],
                    order=structure_order)
        axes[i].set_title(st.metric_axis_label(metric))
        axes[i].set_ylabel("")
        axes[i].set_xlabel("")
        # ROI names only on the lowest panel of each column (MTsat, Ki)
        if metric in ("MTsat", "Ki"):
            axes[i].tick_params(axis='x', rotation=45)
        else:
            axes[i].tick_params(axis='x', labelbottom=False)
        axes[i].get_legend().remove()
    # sixth slot: legend
    handles = [Patch(facecolor=group_colors[g], edgecolor="0.25", label=g) for g in group_order]
    axes[5].axis("off")
    axes[5].legend(handles=handles, title="Group", loc="center", frameon=False)

    metric = "normalized_volume"
    structures_plot = [['Parietal', 'Frontal', 'Temporal', 'Occipital'], ['Insula', 'Cingulate', 'Hippocampus', 'Amygdala']]
    axes_b = subfig_b.subplots(nrows=1, ncols=2)
    for ax, structures in zip(axes_b, structures_plot):
        data_plot = df_brain_grouped.query("structure in @structures")
        sns.boxplot(data=data_plot, x="structure", y=metric, hue="group", palette=group_colors, ax=ax,
                    order=structures, legend=False)
        ax.set_xlabel("")
        ax.set_ylabel("")
    axes_b[0].set_ylabel(st.metric_axis_label(metric))

    for subfig, letter in ((subfig_a, "A"), (subfig_b, "B")):
        subfig.text(0, 1, letter, fontsize=10, fontweight="bold", va="top", ha="left")
    sns.despine(fig)
    subfig_a.subplots_adjust(left=0.08, right=0.97, top=0.95, bottom=0.12, hspace=0.35, wspace=0.18)
    subfig_b.subplots_adjust(left=0.08, right=0.97, top=0.88, bottom=0.15, wspace=0.18)
    helpers.save_static_figure(fig, "fig_brainRois_metrics_boxplots")
    plt.close(fig)
    sns.set_theme()

Stadistics

# Define general variables
structures =  df_brain_grouped["structure"].unique()
mc_alpha = 0.05

Maha

# MhD transformation to stabilize variance in data for simple stats
df_brain_grouped_mahallanobis['mahalanobis_distance_WilHil'] = df_brain_grouped_mahallanobis['mahalanobis_distance'].pow(1/3)
df_brain_grouped_mahallanobis['mahalanobis_distance_log'] = np.log(df_brain_grouped_mahallanobis['mahalanobis_distance'])
#summary_ancovaAssumptions, raw = aa.ancova_assumption_tests(df_brain_grouped_mahallanobis, ['mahalanobis_distance'], structures, covar='age + sex')
#summary_ancovaAssumptions, raw = aa.ancova_assumption_tests(df_brain_grouped_mahallanobis, ['mahalanobis_distance_WilHil'], structures, covar='age + sex')
summary_ancovaAssumptions, raw = aa.ancova_assumption_tests(df_brain_grouped_mahallanobis, ['mahalanobis_distance_log'], structures, covar='age + sex')
display(summary_ancovaAssumptions[summary_ancovaAssumptions["flag_violated"] == True])
Total results collected: 48 rows across 8 structure/metric combinations
Total results collected: 8
Total results collected: 16
Total assumption checks run: 40 | Violations flagged: 1
Loading...
sns.reset_defaults()
plt.figure(figsize=(18, 8)) 
sns.set_theme(style="white", font_scale=2)
plt.rcParams['svg.fonttype'] = 'none'
ax = sns.boxplot(data=df_brain_grouped_mahallanobis, x='structure', y="mahalanobis_distance_log", hue="group", order=structure_order)
sns.move_legend(ax, 'center left' , bbox_to_anchor=(1, 0.5))
# Code to adjust layout and show the plot
ax.set_xticks(ax.get_xticks())
ax.set_xticklabels(ax.get_xticklabels(), rotation=90) 

plt.tight_layout()
#plt.savefig('../static/fig_boxplotMahaMetrics.svg')

plt.show()
<Figure size 1800x800 with 1 Axes>
# Run ANCOVA test for each structure and MhD, controlling for age + sex
df_brain_grouped_ancova = st.ancova_tests(df_brain_grouped_mahallanobis, ['mahalanobis_distance_log'], structures, summary_ancovaAssumptions, covar='age + sex')
# Perform multiple-comparison correction on the group variable results of the ANCOVA tests
df_brain_grouped_ancova_Pcorrected = st.post_ancova_correctMultipleComparison(df_brain_grouped_ancova, mc_method='fdr_by', mc_alpha=mc_alpha)
Total results collected: 32
cols = ["structure", "metric", "F value", "Df_residual",
        "Omega2_partial", "CI_low.y", "pvals_adj", "level",
        "violated_terms"
        ]

df_sig_maha = (
    df_brain_grouped_ancova_Pcorrected
    #.query("pvals_adj < @mc_alpha")
    [cols]
    .rename(columns={
        "F value":        "F(3, df_err)",
        "Df_residual":    "df_err",
        "Omega2_partial": "ω²ₚ",
        "CI_low.y":       "95% CI lower",
        "pvals_adj":      "p_adj",
        "violated_terms": "Interaction included"
    })
    .sort_values(["structure", "metric"])
    #.round(4)
    .copy()
).reset_index(drop=True)

df_sig_maha["Interaction included"] = df_sig_maha["Interaction included"].replace("", "—")

st.display_table(st.format_table(df_sig_maha.sort_values(by='ω²ₚ', ascending=False), two_decimals=["F(3, df_err)", "ω²ₚ", "95% CI lower"], p_values=["p_adj"]))
Loading...

ROIs

# Run ANCOVA test for each structure and metric, controlling for age + sex
df_brain_grouped_ancova = st.ancova_tests(df_brain_grouped, metrics, structures, summary_ancovaAssumptions, covar='age + sex')
# Perform multiple-comparison correction on the group variable results of the ANCOVA tests
df_brain_grouped_ancova_Pcorrected = st.post_ancova_correctMultipleComparison(df_brain_grouped_ancova, mc_method='fdr_by', mc_alpha=mc_alpha)
Total results collected: 192

Check homogeneity of regression slopes to asses Ancova assumpsions

summary_ancovaAssumptions, raw = aa.ancova_assumption_tests(df_brain_grouped, metrics, structures, covar='age + sex')
Total results collected: 288 rows across 48 structure/metric combinations
Total results collected: 48
Total results collected: 96
Total assumption checks run: 240 | Violations flagged: 20
assumption_names = {
    "homogeneity_of_slopes": "Homogeneity of regression slopes",
    "normality_of_residuals": "Normality of residuals",
    "homogeneity_of_variance": "Homogeneity of variance",
}
test_names = {
    "group:age": "group × age interaction",
    "group:sex": "group × sex interaction",
    "shapiro_wilk": "Shapiro–Wilk",
    "group": "Levene",
}

df_violations = (
    summary_ancovaAssumptions
    .query("flag_violated == True")
    .drop(columns="flag_violated")
    .assign(test_name=lambda d: d["test_name"].replace(assumption_names),
            term=lambda d: d["term"].replace(test_names))
    .reset_index(drop=True)
)

st.display_table(st.format_table(df_violations, p_values=["p_value"]))
Loading...
# Uncomment to visually inspect the normality assumption for violated cases
#fig = aa.plot_normality_diagnostics(df, raw['normality'])
#fig
# Run ANCOVA test for each structure and metric, controlling for age + sex
df_brain_grouped_ancova = st.ancova_tests(df_brain_grouped_mahallanobis, metrics, structures, summary_ancovaAssumptions, covar='age + sex')
# Perform multiple-comparison correction on the group variable results of the ANCOVA tests
df_brain_grouped_ancova_Pcorrected = st.post_ancova_correctMultipleComparison(df_brain_grouped_ancova, mc_method='fdr_by', mc_alpha=mc_alpha)
Total results collected: 206
cols = ["structure", "metric", "F value", "Df_residual",
        "Omega2_partial", "CI_low.y", "pvals_adj", "level",
        "violated_terms"
        ]

df_sig = (
    df_brain_grouped_ancova_Pcorrected
    .query("pvals_adj < @mc_alpha")
    [cols]
    .rename(columns={
        "F value":        "F(3, df_err)",
        "Df_residual":    "df_err",
        "Omega2_partial": "ω²ₚ",
        "CI_low.y":       "95% CI lower",
        "pvals_adj":      "p_adj",
        "violated_terms": "Interaction included"
    })
    .sort_values(["structure", "metric"])
).reset_index(drop=True)

df_sig["Interaction included"] = df_sig["Interaction included"].replace("", "—")

st.display_table(st.format_table(df_sig, two_decimals=["F(3, df_err)", "ω²ₚ", "95% CI lower"], p_values=["p_adj"]))
Loading...
df_brain_grouped_ancova_posthoc = st.posthoc_eem_tests(df_brain_grouped, df_brain_grouped_ancova_Pcorrected, summary_ancovaAssumptions, mc_alpha=0.05, covar = 'age + sex')
cols = [
    "structure", "metric", "contrast",
    "df", "t_value",
    "hedges_g", "hedges_g_low", "hedges_g_high",
    "p_holm", "level"
]
df_brain_grouped_ancova_posthoc_sig = (
    df_brain_grouped_ancova_posthoc
    .query("p_holm < @mc_alpha")
    [cols]
    .rename(columns={
        "df":            "df_err",
        "t_value":       "t",
        "hedges_g":      "g",
        "hedges_g_low":  "g CI low",
        "hedges_g_high": "g CI high",
        "p_holm":        "p (Holm)"
    })
    .sort_values(["structure", "metric", "contrast"])
    .reset_index(drop=True)
)

st.display_table(st.format_table(df_brain_grouped_ancova_posthoc_sig, two_decimals=["t", "g", "g CI low", "g CI high"], p_values=["p (Holm)"]))
Loading...
pivot_order = ['AD - HC', 'FTD - HC', 'LBD - HC', 'AD - LBD', 'FTD - LBD', 'AD - FTD'] 

pivot = (
    df_brain_grouped_ancova_posthoc_sig
    .pivot_table(index=['structure', 'metric'],
                 columns='contrast',
                 values="g",
                 aggfunc='first')
    .reindex(columns=pivot_order)
)
# contrasts that did not survive Holm correction stay NaN: drawn as blank cells, not as g = 0

# flatten the multiindex row labels into a single string per row for display
row_labels = [f"{s} — {st.metric_name(m)}" for s, m in pivot.index]

fig = px.imshow(
    pivot.values,
    x=pivot.columns.tolist(),
    y=row_labels,
    color_continuous_scale='RdBu_r',
    color_continuous_midpoint=0,
    aspect='auto',
    labels=dict(color="Hedges' g"),
)

fig.update_traces(
    text=[["" if np.isnan(v) else f"{v:.2f}" for v in row] for row in pivot.values],
    texttemplate="%{text}",
    hovertemplate="%{y}<br>%{x}<br>Hedges' g = %{z:.2f}<extra></extra>",
)

fig.update_layout(
    plot_bgcolor="white",
    title="Pairwise group differences",
    width=700,
    height=900,
    margin=dict(l=200, r=60, t=60, b=60),  # extra left margin for long row labels
)

fig
Loading...
if create_Static_figures:
    helpers.static_theme(style="white")
    pivot = (
        df_brain_grouped_ancova_posthoc_sig
        .pivot_table(index=['structure', 'metric'], columns='contrast', values="g", aggfunc='first')
        .reindex(columns=pivot_order)
    )
    pivot.index = [f"{s} — {st.metric_name(m)}" for s, m in pivot.index]

    fig, ax = plt.subplots(figsize=(helpers.TEXT_WIDTH_IN, 5.2))
    # contrasts that did not survive Holm correction (NaN) are left blank, as in the interactive figure
    sns.heatmap(pivot, mask=pivot.isna(), annot=True, fmt='.2f', annot_kws={"size": 7}, center=0, cmap='RdBu_r',
                linewidths=0.5, linecolor="white", ax=ax, cbar_kws={'label': "Hedges' g"})
    ax.set_xlabel("Contrast")
    ax.set_ylabel("ROI — metric")
    ax.tick_params(length=0)
    fig.tight_layout()
    helpers.save_static_figure(fig, "fig_posthocResults_significant")
    plt.close(fig)
    sns.set_theme()