Skip to article content

Parkinson’s disease in the spinal cord: an exploratory study to establish T2*w, MTR and diffusion-weighted imaging metric values

import plotly.express as px
from plotly.offline import plot
from IPython.core.display import HTML
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import statsmodels.formula.api as smf
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm
import base64
import plotly.io as pio
pio.renderers.default = "plotly_mimetype"


# Initialize the figure with subplots
fig = make_subplots(
    rows=6, cols=3,  
    vertical_spacing=0.05,  # Adjust vertical spacing (between rows)
    horizontal_spacing=0.16,  # Adjust horizontal spacing (between columns)
)

# Color palettes
palette_boxplots = ['steelblue', '#F0B0B0', 'lightcoral', '#B4464F']
palette_points = ['#00517F', '#B4464F', '#B4464F', '#5E000E']

# Initialize dictionnaries to store dataframes for white matter, gray matter and WM/GM ratio
df_WM = {}
df_GM = {}
df_WMGM = {}

# Dataframes for metrics in the white matter : 
metrics_in_WM = ['FA', 'MD', 'AD', 'RD', 'ODI', 'FISO', 'FICVF', 'MTR']
for metric in metrics_in_WM:
    df = pd.read_csv(f'../../data/parkinsons-spinalcord-mri-metrics/data/{metric}.csv')
    df_WM[metric] = df[(df['Label'] == 'white matter') & (df['SpinalLevel'] == '2:05')]

# Dataframes for metrics in the gray matter : 
metrics_in_GM = ['ODI', 'FISO', 'FICVF']
for metric in metrics_in_GM:
    df = pd.read_csv(f'../../data/parkinsons-spinalcord-mri-metrics/data/{metric}.csv')
    df_GM[metric] = df[(df['Label'] == 'gray matter') & (df['SpinalLevel'] == '2:05')]

# Dataframes for metrics in the WM/GM ratio: 
df = pd.read_csv(f'../../data/parkinsons-spinalcord-mri-metrics/data/T2star.csv')
df_WM['T2star'] = df[(df['Label'] == 'white matter') & (df['SpinalLevel'] == '2:05')]
df_GM['T2star'] = df[(df['Label'] == 'gray matter') & (df['SpinalLevel'] == '2:05')]
df_WMGM['T2star'] = df_WM['T2star'].copy()
df_WMGM['T2star']['WA'] = df_WM['T2star']['WA']/df_GM['T2star']['WA'].values

# Function to add boxplots for each subplot
def add_boxplot_for_subplot(data, row, col):

    #print(f'------------ data used for boxplot in row {row}, col {col} -------------- \n : {data}')
    groups = ['CTRL', 'early', 'mid', 'adv']

    # OLS analysis   
    data['UPDRS_class_bis'] = pd.Categorical(data['UPDRS_class_bis'], ordered=True)

    ols_model = smf.ols(formula='WA ~ C(UPDRS_class_bis) + Age', data=data) # The "C()" here means that UPDRS_class_bis is a categorical variable
    ols_results = ols_model.fit()
    #print(f'OLS results for WM MTR in C2-C5: {ols_results.summary()}')

    # Perform ANOVA test
    anova_results = anova_lm(ols_results, typ=2)  # Type II ANOVA
    #print(f'ANOVA results : {anova_results}')

    # Adjusted R2
    adjR2 = ols_results.rsquared_adj

    # UPDRSIII_class_bis p-value
    pvalue_UPDRS_class_bis = anova_results.loc['C(UPDRS_class_bis)', 'PR(>F)']

    # Age p-value
    pvalue_age = anova_results.loc['Age', 'PR(>F)']
    
    for i, group in enumerate(groups):
        box_color = palette_boxplots[i % len(palette_boxplots)]  # Ensure we don't run out of colors
        points_color = palette_points[i % len(palette_points)]  # Cycle through jitter colors
        
        fig.add_trace(go.Box(
            # Filter data for each group
            x=data['UPDRS_class_bis'][data['UPDRS_class_bis'] == group], 
            y=data['WA'][data['UPDRS_class_bis'] == group],  

            # Show all points
            boxpoints='all',
            jitter=0.7,  # Jitter the points for better visibility
            whiskerwidth=0.8,  # Width of the whiskers
            fillcolor=box_color,  # Color of the box
            marker_size=2.5,  # Marker size for points
            marker_color=points_color, # Color of the points
            marker_opacity=0.8,  # Opacity of the points
            line_width=1,  # Border width of the box
            line_color="black" ,  # Border color of the box
            pointpos=0, # Center the points with the box
        ), row=row, col=col)

        # Determine if the p-value is significant (whether to add an asterisk on the plot or not)
        red_asterisk = '<span style="color:red; font-size:15">*</span>' if pvalue_UPDRS_class_bis < 0.05 else ""
        black_asterisk = '<span style="color:black; font-size:15">*</span>' if pvalue_age < 0.05 else ""

        # Add annotations for p-values
        fig.add_annotation(
        x=0.99,  # Position of the annotation in x
        y=0.98,  # Position of the annotation in y 
        text=f"p-Group: {pvalue_UPDRS_class_bis:.4f}{red_asterisk}<br>p-Age: {pvalue_age:.4f}{black_asterisk}", 
        showarrow=False,
        font=dict(size=11, family="Arial", color="black"),
        bgcolor="white",  # Background color of the annotation
        align="right",
        row=row,
        col=col,
        xref="x domain", 
        yref="y domain"  
    )

# Add boxplots

# White matter plots
add_boxplot_for_subplot(df_WM['FA'], 2, 2)
add_boxplot_for_subplot(df_WM['MD'], 3, 2)
add_boxplot_for_subplot(df_WM['AD'], 4, 2)
add_boxplot_for_subplot(df_WM['RD'], 5, 2)
add_boxplot_for_subplot(df_WM['ODI'], 2, 3)
add_boxplot_for_subplot(df_WM['FISO'], 3, 3)
add_boxplot_for_subplot(df_WM['FICVF'], 4, 3)
add_boxplot_for_subplot(df_WM['MTR'], 5, 3)

# Gray matter plots
add_boxplot_for_subplot(df_GM['ODI'], 4, 1)
add_boxplot_for_subplot(df_GM['FISO'], 5, 1)
add_boxplot_for_subplot(df_GM['FICVF'], 6, 1)

# WM/GM plots
add_boxplot_for_subplot(df_WMGM['T2star'], 6, 3)

# Update layout
fig.update_layout(
    margin=dict(
        l=111,  
        r=95, 
        t=40,  
        b=100, 
    ),
    width=800, 
    height=990,
    showlegend=False, 
    
    yaxis5_title="FA",
    yaxis5_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis5_title_standoff=1,
    
    yaxis8_title="MD",
    yaxis8_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis8_title_standoff=1,
    
    yaxis11_title="AD",
    yaxis11_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis11_title_standoff=1,
    
    yaxis14_title="RD", 
    yaxis14_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis14_title_standoff=1,
    
    yaxis6_title="ODI",
    yaxis6_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis6_title_standoff=1,
    
    yaxis9_title="FISO",
    yaxis9_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis9_title_standoff=1,
    
    yaxis12_title="FICVF",
    yaxis12_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis12_title_standoff=1,
    
    yaxis15_title='MTR',
    yaxis15_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis15_title_standoff=1,
    
    yaxis10_title="ODI",
    yaxis10_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis10_title_standoff=1,
    
    yaxis13_title="FISO",
    yaxis13_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis13_title_standoff=1,
    
    yaxis16_title='FICVF',
    yaxis16_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis16_title_standoff=1,
    
    yaxis18_title="T2*w ratio",
    yaxis18_title_font=dict(size=20, family="Arial", color="black", weight='bold'),
    yaxis18_title_standoff=1,
)

# Load static background image (from the "templates_for_figures" folder) and encode as base64
with open("../templates_for_figures/figure4_template.png", "rb") as image_file:
    encoded_image = base64.b64encode(image_file.read()).decode()

fig.update_layout(
    images=[dict(
        source="data:image/png;base64," + encoded_image, # Add static background image
        x=-0.24,
        y=0.995,
        xanchor="left",
        yanchor="top",
        sizex=1.4,
        sizey=2.5, 
        layer="below",  
    )],
)

# Set the y-axis range for each subplot
fig.update_yaxes(range=[0.3, 0.9],  row=2, col=2) # FA (WM)
fig.update_yaxes(range=[0.0002, 0.0016], row=3, col=2) # MD (WM) 
fig.update_yaxes(range=[0.0002, 0.0026], row=4, col=2) # AD (WM) 
fig.update_yaxes(range=[0.00025, 0.001], row=5, col=2) # RD (WM) 
fig.update_yaxes(range=[0, 0.5], row=2, col=3) # ODI (WM) 
fig.update_yaxes(range=[0.1, 0.7], row=3, col=3) # FISO (WM)
fig.update_yaxes(range=[0.4, 1.3], row=4, col=3) # FICVF (WM)
fig.update_yaxes(range=[35, 55], row=5, col=3) # MTR (WM)
fig.update_yaxes(range=[0, 0.5], row=4, col=1) # ODI (GM)
fig.update_yaxes(range=[0.1, 0.7], row=5, col=1) # FISO (GM)
fig.update_yaxes(range=[0.4, 1.3], row=6, col=1) # FICVF (GM)
fig.update_yaxes(range=[0.8, 1.1], row=6, col=3) # T2* (WM/GM ratio) 

# List of subplot positions for x-axis updates
xaxis_subplots = [(2, 2), (3, 2), (4, 2), (5, 2), (2, 3), (3, 3), (4, 3), (5, 3), (4, 1), (5, 1), (6, 1), (6, 3)]

# Update x-axis labels and font
for row, col in xaxis_subplots:
    fig.update_xaxes(
        ticktext=['HC', 'Early', 'Mid', 'Adv'],
        tickvals=['CTRL', 'early', 'mid', 'adv'],
        tickfont=dict(size=13, weight='bold', family="Arial"),
        row=row,
        col=col
    )

fig.show()
Loading...
Parkinson’s disease in the spinal cord: an exploratory study to establish T2*w, MTR and diffusion-weighted imaging metric values
Figure 4
Parkinson’s disease in the spinal cord: an exploratory study to establish T2*w, MTR and diffusion-weighted imaging metric values
Suppl Figure 1