Connectivity analysis - correlation analyses - ROI to ROI

Within this section of the connectivity analyses the ROI to ROI correlation are shown, explained and set up in a way that they can be rerun and reproduced. In short, we employed task-based functional connectivity through NiBetaSeries analysis, to investigate if condition specific information flow between the ROIs of the auditory cortex provides insights concerning the proposed functional principles. The analysis is divided into the following sections:

Correlation analysis - ROI to ROI

  • Condition-specific functional connectivity

  • Comparison of condition-specific functional connectivity

We provide more detailed information and explanations in the respective sections. As mentioned before, we’re working with derivatives here and thus we can share them, meaning that you can rerun the analyses either by downloading this page as a Jupyter Notebook (via the download button at the top) or interactively via a cloud instance through the amazing mybinder project (the little rocket at the top). We recommend the later to spare installation and conda environment related problems. One more thing… Please note, that here derivatives refers to the computed Beta Series (i.e. condition specific trial-wise beta estimates) which we provide via the project’s OSF page because their computation is computationally very expensive and would never run via the resources available through binder. The code that will download the required data is included in the respective sections.

As usual, we will start with importing all necessary modules and functions.

from os.path import join as opj
from os.path import basename as opb
from os import listdir
from copy import deepcopy

import numpy as np
from glob import glob
import pandas as pd
import nibabel as nb
from nilearn.connectome import ConnectivityMeasure
from nilearn.input_data import NiftiLabelsMasker
from netneurotools import stats as nnstats
from pingouin import multicomp

import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
import seaborn as sns
import ptitprince as pt
import plotly.express as px
import plotly.graph_objects as go
from IPython.core.display import display, HTML
from plotly.offline import init_notebook_mode, plot

import pingouin as pg

from utils import plot_auditory_cortex_connectome

%matplotlib inline

Let’s also gather and set some important paths. This includes the path to the general derivatives directory, as well as analyses specific ones. In more detail, we of course aim to keep it as BIDS-y as possible and therefore set a path for the Connectivity analyses here. To keep results and necessary prerequisites separate (keeping it tidy), we also create a dedicated directory which will include the auditory cortex atlas, as well as the input data.

derivatives_path = '/data/mvs/derivatives/'

roi_mask_path ='/data/mvs/derivatives/ROIs_masks/'

prereq_path = '/data/mvs/derivatives/connectivity/prerequisites/'

results_path = '/data/mvs/derivatives/connectivity/'

At first, we need to gather the outputs from the previous step, i.e. the computation of Beta Series. Please note: if you want to run this interactively but did not download the Beta Series in the previous step, you need to download them now via this command:

osf -p <projectid> fetch remote/path.txt local/file.txt

What we have as output are betaseries files per condition and run, each containing n images where n reflects the number of presentations of a given condition per run. Thus, we have six betaseries: for each of two runs, one for music, singing and voice respectively.

listdir(opj(results_path, 'nibetaseries_transform/sub-01'))
['sub-01_task-mvs_run-2_space-individual_desc-Singing_betaseries.nii.gz',
 'sub-01_task-mvs_run-1_space-individual_desc-Music_betaseries.nii.gz',
 'sub-01_task-mvs_run-1_space-individual_desc-Singing_betaseries.nii.gz',
 'sub-01_task-mvs_run-2_space-individual_desc-Music_betaseries.nii.gz',
 'sub-01_task-mvs_run-1_space-individual_desc-Voice_betaseries.nii.gz',
 'sub-01_task-mvs_run-2_space-individual_desc-Voice_betaseries.nii.gz']

And each betaseries consists of 30 images which matches with how often the conditions were presented:

list_beta = glob(opj(results_path, 'nibetaseries_transform/*/*.nii.gz'))
list_beta.sort()

for betaseries in list_beta:
    print(nb.load(betaseries).shape)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)
(97, 115, 97, 30)

We can double check the number of presentations of a given condition using sub-01 and the music condition as an example:

pd.read_csv('/data/mvs/sub-01/func/sub-01_task-mvs_run-01_events.tsv', sep='\t').query('trial_type == "Music"').trial_type.count()
30

As done in the MVPA, we will exclude the data of sub-06 as no reliable activation could be detected in the univariate analyses:

del list_beta[30:36]

We just need one more thing to start our analyses: the Auditory cortex atlas information indicating which index is which region. We created this file in the previous step and can now just load it:

atlas_rois_df = pd.read_csv(opj(prereq_path, 'atlas/AC_atlas_regions.txt'), sep='\t')
atlas_rois_df
index regions
0 1 HG_lh
1 2 HG_rh
2 3 PP_lh
3 4 PP_rh
4 5 PT_lh
5 6 PT_rh
6 7 aSTG_lh
7 8 aSTG_rh
8 9 pSTG_lh
9 10 pSTG_rh

Correlation analyses - ROI to ROI

At first, we will compute condition specific connectivity matrices for each run and participant. That is, we are going to assess the correlation of condition specific beta series between the ROIs of our auditory cortex model across time, here trials or presentations. Subsequently, we will compare the resulting connectivity matrices between conditions to evaluate if the information flow diverges between conditions and if so, where. However, before we do so, we need to talk about our auditory cortex model again. In the way we set it up before, it is fully connected, which means that every ROI is connected to every other ROI. Even though we focus on functional principles, it is still important to incorporate results from prior studies concerning both structure and function to evaluate which of our connections actually make sense/are justifiable. Doing so, for example here, here and here, points to a few connections we should keep and quite a bunch we should not keep.

network_structure_reduced = np.array([[0., 2., 1., 0., 1., 0., 0., 0., 0., 0.],
                                      [2., 0., 0., 1., 0., 1., 0., 0., 0., 0.],
                                      [1., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
                                      [0., 1., 0., 0., 0., 0., 0., 1., 0., 0.],
                                      [1., 0., 0., 0., 0., 2., 0., 0., 1., 0.],
                                      [0., 1., 0., 0., 2., 0., 0., 0., 0., 1.],
                                      [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
                                      [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
                                      [0., 0., 0., 0., 1., 0., 0., 0., 0., 2.],
                                      [0., 0., 0., 0., 0., 1., 0., 0., 2., 0.]])

And we plot it using the function we defined in the previous step:

plot_auditory_cortex_connectome(network_structure_reduced, atlas_info=atlas_rois_df, name='reduced', n_lines=10)
_images/Connectivity_analyses-ROI_20_0.png

Looks quite different from our fully connected model and while it might seem drastic, cutting the connections down to those that are plausible makes sense.

Condition specific functional connectivity

In order to compute the condition specific functional connectivity between ROIs, we need to do two things: extract the respective beta series for each condition and ROI and subsequently assess the correlation between the beta series of all pairs of ROIs. For the first, we can use nilearn’s NiftiMapsMasker:

masker = NiftiLabelsMasker(opj(results_path, 
                               'prerequisites/atlas/tpl-MNI152NLin2009cAsym_res-02_desc-ACatlas.nii.gz'),
                           memory='nilearn_cache', standardize=True)

We then create list for the extracted time series and their respective information (participant, run and condition) as we need this for the statistical analysis.

beta_series = []
participant = []
run = []
condition = []

Let’s briefly check if we have all the beta series images we need:

print('We have a total of %s beta series images.' %str(int(len(list_beta))))
print('Given that we have beta series images for 3 conditions and 2 runs each, this means we have data from %s participants.' %str(int(len(list_beta)/(3*2))))
We have a total of 138 beta series images.
Given that we have beta series images for 3 conditions and 2 runs each, this means we have data from 23 participants.

Nice, that checks out. Having everything in place, we can put it together in a loop and extract the beta series, participant id, run and condition:

for betaseries in list_beta:
        
    beta_series.append(masker.fit_transform(betaseries)) 

    participant.append(betaseries[betaseries.rfind('sub'):betaseries.rfind('_task')])
    run.append(betaseries[betaseries.rfind('run'):betaseries.rfind('_s')])
    condition.append(betaseries[betaseries.rfind('-')+1:betaseries.rfind('_')])

For the ease of use, we put the extracted participant. run and condition information in a DataFrame:

beta_series_info = pd.DataFrame({'participant': participant, 'run': run, 'condition': condition})
beta_series_info.head()
participant run condition
0 sub-01 run-1 Music
1 sub-01 run-1 Singing
2 sub-01 run-1 Voice
3 sub-01 run-2 Music
4 sub-01 run-2 Singing

With that, we can now easily check if we everything worked as expected, including the number and ids of participants:

print('We have a  total of %s participants, comprising the following ids: ' %str(int(len(beta_series_info['participant'].unique()))))
list(beta_series_info['participant'].unique())
We have a  total of 23 participants, comprising the following ids: 
['sub-01',
 'sub-02',
 'sub-03',
 'sub-04',
 'sub-05',
 'sub-07',
 'sub-08',
 'sub-09',
 'sub-10',
 'sub-11',
 'sub-12',
 'sub-13',
 'sub-14',
 'sub-15',
 'sub-16',
 'sub-17',
 'sub-18',
 'sub-19',
 'sub-20',
 'sub-21',
 'sub-22',
 'sub-23',
 'sub-24']

and the runs:

print('We have the following runs: ')
print(list(beta_series_info['run'].unique()))
print('(%s in total, 2 for each of 23 participants)' %str(int(len(beta_series_info['participant'].unique()) * len(beta_series_info['run'].unique()))))
We have the following runs: 
['run-1', 'run-2']
(46 in total, 2 for each of 23 participants)

and the conditions:

print('We have the following conditions: ')
print(list(beta_series_info['condition'].unique()))
print('(%s in total, 3 for each of 23 participants and 2 runs)' %str(int(len(beta_series_info['participant'].unique()) * 
                                                                         len(beta_series_info['condition'].unique()) * 
                                                                         len(beta_series_info['run'].unique()))))
We have the following conditions: 
['Music', 'Singing', 'Voice']
(138 in total, 3 for each of 23 participants and 2 runs)

Great, that looks about right. Next, we are going to add an additional variable to our information DataFrame to enable certain statistical models later on. This refers to a combination of the participant, run and conditions:

list_mod_ind = []

for i, row in beta_series_info.iterrows():
    list_mod_ind.append(row['participant']+'_'+row['run']+'_'+row['condition'])
    
beta_series_info['mod_ind'] = list_mod_ind

Did that work?

beta_series_info.head()
participant run condition mod_ind
0 sub-01 run-1 Music sub-01_run-1_Music
1 sub-01 run-1 Singing sub-01_run-1_Singing
2 sub-01 run-1 Voice sub-01_run-1_Voice
3 sub-01 run-2 Music sub-01_run-2_Music
4 sub-01 run-2 Singing sub-01_run-2_Singing

It did! Let’s save these outputs so that we can load them later without having the necessity to rerun the extraction again:

np.save(opj(results_path, 'results/nibetaseries/nibetaseries_transform/beta_series/mss_beta_series.npy'), beta_series, allow_pickle=True)
beta_series_info.to_csv(opj(results_path, 'results/nibetaseries/nibetaseries_transform/beta_series/mvs_beta_series_info.csv'), index=False)

Depending on what and how folks might run the analyses, we will also include code to download this data from the project’s OSF page and load it:

osf -p <projectid> fetch remote/path.txt local/file.txt
beta_series = np.load(opj(results_path, 'results/nibetaseries/nibetaseries_transform/beta_series/mss_beta_series.npy'), allow_pickle=True)
beta_series_info = pd.read_csv(opj(results_path, 'results/nibetaseries/nibetaseries_transform/beta_series/mvs_time_series_info.csv'))

Now we stack the beta series of the two runs for a given condition and participant, also changing the order of conditions to Voice, Singing and Music for consistency reasons. This was motivated by the analyses we wanted to conduct and are outlined further below. We set a few new lists:

part_list = []
cond_list = []
bs_list = []

and iterate over participants and conditions, concatenating beta series across runs:

for part in beta_series_info['participant'].unique():

    for cond in ['Voice', 'Singing', 'Music']:
    
        part_list.append(part)
    
        cond_list.append(cond)
    
        bs_list.append(np.vstack(np.array(beta_series)[beta_series_info.condition.isin([cond]) & beta_series_info.participant.isin([part])]))
    
part_cond = [(part+'_'+cond) for part,cond in zip(part_list, cond_list)]    

Let’s create a new DataFrame that reflects those changes:

beta_series_info_stacked = pd.DataFrame({'participant': part_list, 'condition': cond_list, 'part_cond': part_cond})
beta_series_info_stacked.head()
participant condition part_cond
0 sub-01 Voice sub-01_Voice
1 sub-01 Singing sub-01_Singing
2 sub-01 Music sub-01_Music
3 sub-02 Voice sub-02_Voice
4 sub-02 Singing sub-02_Singing

We will also save the stacked versions of the beta series and DataFrame. Please note, that going forward, we will work with the stacked versions only.

np.save(opj(results_path, 'results/nibetaseries/nibetaseries_transform/beta_series/mss_beta_series_desc-stacked.npy'), bs_list, allow_pickle=True)
beta_series_info_stacked.to_csv(opj(results_path, 'results/nibetaseries/nibetaseries_transform/beta_series/mvs_beta_series_info_desc-stacked.csv'), index=False)

We finally arrived at the second step of the ROI to ROI correlation analyses: computing the correlation of beta series between ROIs. Once more, nilearn has our back and we can its ConnectivityMeasure class which makes the computation of connectivity matrices straightforward. First, we need to define our correlation measure. We decided to go with the full correlation following the NiBetaSeries workflow and prior research.

cor_measure = ConnectivityMeasure(kind='correlation')

Second, we apply or fit it to the extracted beta series:

cor_matrices = cor_measure.fit_transform(bs_list, )

This results in n connectivity matrices, one for each condition and participant, thus 69 in total:

cor_matrices.shape
(69, 10, 10)

As you can see each of our 69 connectivity matrices is 10 x 10, this is because our auditory cortex model has 10 regions and thus will always results in the fully connected model. The reduced model we introduced above, will be used to masked those connections that were cut. We will achieve this via a for-loop within which we will set the respective connections of the computed correlation matrices to 0:

for matrix in cor_matrices:
    matrix[network_structure_reduced == 0] = int(0)

Let’s check if this worked by comparing our reduced model to one of the masked correlation matrices we just obtained:

network_structure_reduced_viz_test = deepcopy(network_structure_reduced)
network_structure_reduced_viz_test[network_structure_reduced==0] = np.nan

At first, the reduced network model:

fig = px.imshow(network_structure_reduced_viz_test, x=atlas_rois_df['regions'], y=atlas_rois_df['regions'],
               color_continuous_scale=['gray', 'red'])
fig.update_layout(coloraxis_showscale=False)
fig['layout'].update(plot_bgcolor='white')
init_notebook_mode(connected=True)

plot(fig, filename = 'ac_network_model_reduced.html')
display(HTML('ac_network_model_reduced.html'))

And as an example, the correlation matrix for the condition voice of sub-01:

cor_matrix_viz_test = cor_matrices[0]
cor_matrix_viz_test[cor_matrices[0]==0] = np.nan

fig = px.imshow(cor_matrix_viz_test, x=atlas_rois_df['regions'], y=atlas_rois_df['regions'],
               color_continuous_scale='darkmint_r')
fig.update_layout(coloraxis_showscale=True)
fig['layout'].update(plot_bgcolor='white')

plot(fig, filename = 'ac_network_cor-voice_sub-01.html')
display(HTML('ac_network_cor-voice_sub-01.html'))

A bit hard to see, but using NumPy’s nonzero function we can easily get and compare the indices of values that are not zero:

np.nonzero(network_structure_reduced)
(array([0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 6, 7, 8, 8, 9, 9]),
 array([1, 2, 4, 0, 3, 5, 0, 6, 1, 7, 0, 5, 8, 1, 4, 9, 2, 3, 4, 9, 5, 8]))
np.nonzero(cor_matrices[0])
(array([0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 6, 7, 8, 8, 9, 9]),
 array([1, 2, 4, 0, 3, 5, 0, 6, 1, 7, 0, 5, 8, 1, 4, 9, 2, 3, 4, 9, 5, 8]))

Ok, things work as expected, cool. Now we will bring the correlation values into a form which makes handling, saving and statistical analyses a bit easier: a Pandas DataFrame. To make this straightforward and because we deal with task-based functional connectivity (thus having a network structure/connectivity matrix that is mirrored across the diagonal), we define a mask that entails the indices of non-zero values of the lower triangle of the network structure/connectivity matrix. Otherwise, we would have the value for each connection twice and this would obscure our planned analyses.

connection_analyses_indices = np.where(np.tril(cor_matrices[0]) != 0)

And to be sure, we check the mask using the example from above:

cor_matrices[0][connection_analyses_indices]
array([0.72974441, 0.73035626, 0.63890314, 0.83369683, 0.75232852,
       0.74362356, 0.67876427, 0.42367251, 0.67637752, 0.67960324,
       0.77169163])

Fantastic! Now we create a list that specifies which index is which connection to include it into the DataFrame and thus ease that part up a bit:

list_connections = ['HG_lh_HG_rh', 'HG_lh_PP_lh', 'HG_rh_PP_rh', 'HG_lh_PT_lh', 'HG_rh_PT_rh',
                    'PT_lh_PT_rh', 'PP_lh_aSTG_lh', 'PP_rh_aSTG_rh', 'PT_lh_pSTG_lh', 'PT_rh_pSTG_rh',
                    'pSTG_lh_pSTG_rh']

Following this order, we are now going to extract the corresponding for a given connection. To do so we will use a combination of list comprehension and indexing which which we will extract a certain connection:

list_HG_lh_HG_rh = [m[connection_analyses_indices][0] for m in cor_matrices]
list_HG_lh_PP_lh = [m[connection_analyses_indices][1] for m in cor_matrices]
list_HG_rh_PP_rh = [m[connection_analyses_indices][2] for m in cor_matrices]
list_HG_lh_PT_lh = [m[connection_analyses_indices][3] for m in cor_matrices]
list_HG_rh_PT_rh = [m[connection_analyses_indices][4] for m in cor_matrices]
list_PT_lh_PT_rh = [m[connection_analyses_indices][5] for m in cor_matrices]
list_PP_lh_aSTG_lh = [m[connection_analyses_indices][6] for m in cor_matrices]
list_PP_rh_aSTG_rh = [m[connection_analyses_indices][7] for m in cor_matrices]
list_PT_lh_pSTG_lh = [m[connection_analyses_indices][8] for m in cor_matrices]
list_PT_rh_pSTG_rh = [m[connection_analyses_indices][9] for m in cor_matrices]
list_pSTG_lh_pSTG_rh = [m[connection_analyses_indices][10] for m in cor_matrices]

With that, we can create our DataFrame. As we already have a lot of useful information in our beta_series_information DataFrame, including participant and run ID, as well as conditions, we will use it as a base:

con_info = deepcopy(beta_series_info_stacked)

and add simply add the extracted correlation values for all connections:

con_info['HG_lh_HG_rh'] = list_HG_lh_HG_rh
con_info['HG_lh_PP_lh'] = list_HG_lh_PP_lh
con_info['HG_rh_PP_rh'] = list_HG_rh_PP_rh
con_info['HG_lh_PT_lh'] = list_HG_lh_PT_lh
con_info['HG_rh_PT_rh'] = list_HG_rh_PT_rh
con_info['PT_lh_PT_rh'] = list_PT_lh_PT_rh
con_info['PP_lh_aSTG_lh'] = list_PP_lh_aSTG_lh
con_info['PP_rh_aSTG_rh'] = list_PP_rh_aSTG_rh
con_info['PT_lh_pSTG_lh'] = list_PT_lh_pSTG_lh
con_info['PT_rh_pSTG_rh'] = list_PT_rh_pSTG_rh
con_info['pSTG_lh_pSTG_rh'] = list_pSTG_lh_pSTG_rh

We now have one big DataFrame with everything we need to run the planned statistical analyses:

con_info
participant condition part_cond HG_lh_HG_rh HG_lh_PP_lh HG_rh_PP_rh HG_lh_PT_lh HG_rh_PT_rh PT_lh_PT_rh PP_lh_aSTG_lh PP_rh_aSTG_rh PT_lh_pSTG_lh PT_rh_pSTG_rh pSTG_lh_pSTG_rh
0 sub-01 Voice sub-01_Voice 0.729744 0.730356 0.638903 0.833697 0.752329 0.743624 0.678764 0.423673 0.676378 0.679603 0.771692
1 sub-01 Singing sub-01_Singing 0.675542 0.771575 0.663902 0.819895 0.722174 0.753315 0.720142 0.531696 0.653344 0.651568 0.687224
2 sub-01 Music sub-01_Music 0.664941 0.777863 0.755443 0.864224 0.749391 0.811263 0.671052 0.443218 0.708958 0.620904 0.821649
3 sub-02 Voice sub-02_Voice 0.830138 0.877590 0.820580 0.855173 0.820108 0.883193 0.689591 0.701428 0.743975 0.789122 0.814807
4 sub-02 Singing sub-02_Singing 0.781083 0.761344 0.765417 0.737433 0.782925 0.851739 0.582042 0.740368 0.730446 0.756289 0.859267
5 sub-02 Music sub-02_Music 0.823370 0.846985 0.785204 0.847632 0.825368 0.869806 0.575630 0.694261 0.696670 0.702350 0.835517
6 sub-03 Voice sub-03_Voice 0.875702 0.739229 0.746251 0.870731 0.788325 0.868515 0.650099 0.674626 0.810724 0.787583 0.827930
7 sub-03 Singing sub-03_Singing 0.838800 0.783068 0.798430 0.895312 0.790128 0.860714 0.675208 0.595846 0.847480 0.803247 0.766505
8 sub-03 Music sub-03_Music 0.870248 0.827551 0.826401 0.888271 0.835686 0.886036 0.795027 0.792858 0.881290 0.831516 0.866635
9 sub-04 Voice sub-04_Voice 0.661841 0.669932 0.732870 0.813031 0.653174 0.724992 0.698228 0.730935 0.720296 0.672631 0.735072
10 sub-04 Singing sub-04_Singing 0.730157 0.702212 0.706484 0.787749 0.768039 0.802235 0.625223 0.634347 0.714656 0.702342 0.780434
11 sub-04 Music sub-04_Music 0.724174 0.691277 0.694784 0.806611 0.749147 0.814798 0.709694 0.788474 0.744303 0.781017 0.770780
12 sub-05 Voice sub-05_Voice 0.897685 0.807468 0.810559 0.909669 0.882354 0.910752 0.797057 0.560030 0.785998 0.836483 0.889023
13 sub-05 Singing sub-05_Singing 0.799600 0.752605 0.692251 0.876613 0.798767 0.868799 0.699611 0.650478 0.801587 0.848003 0.888526
14 sub-05 Music sub-05_Music 0.865606 0.813042 0.783262 0.883580 0.819614 0.856640 0.760175 0.585665 0.742173 0.808724 0.869472
15 sub-07 Voice sub-07_Voice 0.805987 0.574494 0.702516 0.768399 0.800056 0.745696 0.690009 0.708555 0.667727 0.662427 0.744635
16 sub-07 Singing sub-07_Singing 0.887511 0.700370 0.823750 0.823850 0.869131 0.868068 0.788123 0.782616 0.841680 0.749210 0.869769
17 sub-07 Music sub-07_Music 0.877708 0.761849 0.761749 0.869963 0.812417 0.863646 0.819363 0.814318 0.783096 0.761850 0.853976
18 sub-08 Voice sub-08_Voice 0.791797 0.766930 0.497295 0.827435 0.709005 0.770647 0.632541 0.714519 0.804723 0.815431 0.826184
19 sub-08 Singing sub-08_Singing 0.828154 0.724248 0.739817 0.839104 0.758017 0.862691 0.767996 0.750330 0.854543 0.867934 0.882947
20 sub-08 Music sub-08_Music 0.861437 0.573426 0.708550 0.839057 0.765003 0.823242 0.730894 0.750383 0.850546 0.856770 0.867493
21 sub-09 Voice sub-09_Voice 0.873669 0.755972 0.699044 0.870561 0.841119 0.899899 0.799017 0.729051 0.772754 0.770347 0.792606
22 sub-09 Singing sub-09_Singing 0.878697 0.802687 0.808839 0.869726 0.882443 0.895286 0.770071 0.664606 0.705208 0.805390 0.773021
23 sub-09 Music sub-09_Music 0.893716 0.810361 0.852695 0.882243 0.902263 0.927310 0.882516 0.797353 0.842763 0.869202 0.888899
24 sub-10 Voice sub-10_Voice 0.819320 0.777916 0.708211 0.855883 0.751549 0.883982 0.751733 0.763174 0.601840 0.847766 0.584261
25 sub-10 Singing sub-10_Singing 0.857621 0.744601 0.761159 0.893520 0.826999 0.880832 0.826932 0.821098 0.766651 0.864738 0.762504
26 sub-10 Music sub-10_Music 0.874622 0.837903 0.823044 0.896296 0.875524 0.896465 0.813362 0.847050 0.726633 0.872768 0.764338
27 sub-11 Voice sub-11_Voice 0.889455 0.894281 0.844728 0.888034 0.857996 0.904549 0.818911 0.863114 0.900270 0.907826 0.909496
28 sub-11 Singing sub-11_Singing 0.836644 0.880357 0.871613 0.889654 0.846883 0.828805 0.861511 0.856606 0.905394 0.859689 0.892326
29 sub-11 Music sub-11_Music 0.860635 0.891791 0.835302 0.894762 0.840117 0.838626 0.832634 0.832511 0.892754 0.892157 0.873614
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
39 sub-15 Voice sub-15_Voice 0.725923 0.741241 0.697167 0.803694 0.716742 0.755990 0.690591 0.415193 0.778888 0.754890 0.816032
40 sub-15 Singing sub-15_Singing 0.785832 0.774702 0.710268 0.806606 0.772127 0.759329 0.759825 0.627574 0.849923 0.734162 0.852930
41 sub-15 Music sub-15_Music 0.805539 0.758701 0.771028 0.826969 0.813685 0.828913 0.679663 0.561055 0.853834 0.758606 0.857450
42 sub-16 Voice sub-16_Voice 0.804853 0.804400 0.712141 0.848407 0.881750 0.842596 0.722955 0.597685 0.742244 0.792127 0.873360
43 sub-16 Singing sub-16_Singing 0.767650 0.619864 0.705160 0.683718 0.759166 0.746174 0.505979 0.444140 0.624753 0.675188 0.706097
44 sub-16 Music sub-16_Music 0.781665 0.649025 0.674398 0.717090 0.800665 0.784200 0.499719 0.363384 0.633187 0.681355 0.770259
45 sub-17 Voice sub-17_Voice 0.852302 0.876621 0.815713 0.872777 0.803822 0.827776 0.805299 0.818800 0.833794 0.849316 0.850614
46 sub-17 Singing sub-17_Singing 0.900347 0.924346 0.890996 0.914996 0.838629 0.822124 0.857267 0.887393 0.925807 0.908975 0.896646
47 sub-17 Music sub-17_Music 0.904244 0.921129 0.892791 0.908271 0.858751 0.872729 0.850724 0.877083 0.898561 0.901858 0.908555
48 sub-18 Voice sub-18_Voice 0.864487 0.751691 0.762957 0.838216 0.792504 0.821922 0.845034 0.821798 0.705442 0.841723 0.792311
49 sub-18 Singing sub-18_Singing 0.822911 0.724895 0.709841 0.829893 0.744758 0.884711 0.802367 0.759428 0.771592 0.869423 0.809301
50 sub-18 Music sub-18_Music 0.859919 0.818191 0.735433 0.845690 0.842977 0.866264 0.831154 0.829731 0.797859 0.871945 0.836192
51 sub-19 Voice sub-19_Voice 0.762531 0.818732 0.833521 0.826748 0.882631 0.896734 0.741946 0.810613 0.803592 0.843286 0.877768
52 sub-19 Singing sub-19_Singing 0.736630 0.803147 0.744531 0.816359 0.884139 0.861244 0.689953 0.752081 0.737424 0.748864 0.781274
53 sub-19 Music sub-19_Music 0.576005 0.792424 0.642705 0.767322 0.819035 0.813145 0.650924 0.660559 0.651293 0.687516 0.833543
54 sub-20 Voice sub-20_Voice 0.809400 0.637385 0.688888 0.836099 0.704910 0.804044 0.543473 0.493560 0.749583 0.718560 0.827523
55 sub-20 Singing sub-20_Singing 0.880853 0.718936 0.791845 0.887984 0.792575 0.860018 0.676912 0.631330 0.817802 0.775915 0.834294
56 sub-20 Music sub-20_Music 0.835547 0.709680 0.833981 0.881845 0.839787 0.859247 0.614188 0.675014 0.810536 0.752211 0.837192
57 sub-21 Voice sub-21_Voice 0.461008 0.571672 0.499984 0.647146 0.676794 0.716477 0.639954 0.696143 0.640307 0.697720 0.723124
58 sub-21 Singing sub-21_Singing 0.471160 0.626583 0.523658 0.660902 0.650604 0.670875 0.591440 0.588983 0.505290 0.662194 0.624693
59 sub-21 Music sub-21_Music 0.350784 0.350739 0.350673 0.650759 0.661649 0.617042 0.656578 0.558696 0.568893 0.782805 0.650059
60 sub-22 Voice sub-22_Voice 0.858569 0.674932 0.700881 0.662112 0.718076 0.701368 0.691428 0.551090 0.800601 0.784101 0.786322
61 sub-22 Singing sub-22_Singing 0.790798 0.636231 0.756569 0.730166 0.667834 0.809873 0.707035 0.703480 0.818719 0.797161 0.792888
62 sub-22 Music sub-22_Music 0.829753 0.786600 0.822241 0.794923 0.784545 0.767923 0.717012 0.735915 0.832958 0.783768 0.829268
63 sub-23 Voice sub-23_Voice 0.667993 0.634921 0.675987 0.717864 0.604840 0.729161 0.523030 0.582644 0.776759 0.687612 0.808971
64 sub-23 Singing sub-23_Singing 0.641399 0.752562 0.729688 0.772601 0.621414 0.809544 0.669865 0.599939 0.789958 0.820986 0.851517
65 sub-23 Music sub-23_Music 0.652196 0.761275 0.734215 0.809774 0.606825 0.870133 0.689058 0.638142 0.828419 0.827823 0.796080
66 sub-24 Voice sub-24_Voice 0.569008 0.423242 0.343981 0.352008 0.533012 0.557505 0.092850 0.344452 0.604644 0.537573 0.684387
67 sub-24 Singing sub-24_Singing 0.536243 0.395264 0.541994 0.450791 0.601991 0.513685 0.143058 0.350383 0.533949 0.583680 0.567898
68 sub-24 Music sub-24_Music 0.625497 0.444980 0.524848 0.475135 0.700660 0.563156 0.052575 0.336557 0.491712 0.508260 0.621611

69 rows × 14 columns

In order to get a first idea how things looks, we will plot condition specific correlations across connections via raincloud plots (as done in the MVPA). This requires reshaping our DataFrame from wide to long:

con_info_long = pd.melt(con_info[con_info.columns[2:,]],id_vars=['part_cond'],var_name='connection', value_name='cor_values')
con_info_long[['participant','condition']] = con_info_long['part_cond'].str.split('_', expand=True)

con_info_long
part_cond connection cor_values participant condition
0 sub-01_Voice HG_lh_HG_rh 0.729744 sub-01 Voice
1 sub-01_Singing HG_lh_HG_rh 0.675542 sub-01 Singing
2 sub-01_Music HG_lh_HG_rh 0.664941 sub-01 Music
3 sub-02_Voice HG_lh_HG_rh 0.830138 sub-02 Voice
4 sub-02_Singing HG_lh_HG_rh 0.781083 sub-02 Singing
5 sub-02_Music HG_lh_HG_rh 0.823370 sub-02 Music
6 sub-03_Voice HG_lh_HG_rh 0.875702 sub-03 Voice
7 sub-03_Singing HG_lh_HG_rh 0.838800 sub-03 Singing
8 sub-03_Music HG_lh_HG_rh 0.870248 sub-03 Music
9 sub-04_Voice HG_lh_HG_rh 0.661841 sub-04 Voice
10 sub-04_Singing HG_lh_HG_rh 0.730157 sub-04 Singing
11 sub-04_Music HG_lh_HG_rh 0.724174 sub-04 Music
12 sub-05_Voice HG_lh_HG_rh 0.897685 sub-05 Voice
13 sub-05_Singing HG_lh_HG_rh 0.799600 sub-05 Singing
14 sub-05_Music HG_lh_HG_rh 0.865606 sub-05 Music
15 sub-07_Voice HG_lh_HG_rh 0.805987 sub-07 Voice
16 sub-07_Singing HG_lh_HG_rh 0.887511 sub-07 Singing
17 sub-07_Music HG_lh_HG_rh 0.877708 sub-07 Music
18 sub-08_Voice HG_lh_HG_rh 0.791797 sub-08 Voice
19 sub-08_Singing HG_lh_HG_rh 0.828154 sub-08 Singing
20 sub-08_Music HG_lh_HG_rh 0.861437 sub-08 Music
21 sub-09_Voice HG_lh_HG_rh 0.873669 sub-09 Voice
22 sub-09_Singing HG_lh_HG_rh 0.878697 sub-09 Singing
23 sub-09_Music HG_lh_HG_rh 0.893716 sub-09 Music
24 sub-10_Voice HG_lh_HG_rh 0.819320 sub-10 Voice
25 sub-10_Singing HG_lh_HG_rh 0.857621 sub-10 Singing
26 sub-10_Music HG_lh_HG_rh 0.874622 sub-10 Music
27 sub-11_Voice HG_lh_HG_rh 0.889455 sub-11 Voice
28 sub-11_Singing HG_lh_HG_rh 0.836644 sub-11 Singing
29 sub-11_Music HG_lh_HG_rh 0.860635 sub-11 Music
... ... ... ... ... ...
729 sub-15_Voice pSTG_lh_pSTG_rh 0.816032 sub-15 Voice
730 sub-15_Singing pSTG_lh_pSTG_rh 0.852930 sub-15 Singing
731 sub-15_Music pSTG_lh_pSTG_rh 0.857450 sub-15 Music
732 sub-16_Voice pSTG_lh_pSTG_rh 0.873360 sub-16 Voice
733 sub-16_Singing pSTG_lh_pSTG_rh 0.706097 sub-16 Singing
734 sub-16_Music pSTG_lh_pSTG_rh 0.770259 sub-16 Music
735 sub-17_Voice pSTG_lh_pSTG_rh 0.850614 sub-17 Voice
736 sub-17_Singing pSTG_lh_pSTG_rh 0.896646 sub-17 Singing
737 sub-17_Music pSTG_lh_pSTG_rh 0.908555 sub-17 Music
738 sub-18_Voice pSTG_lh_pSTG_rh 0.792311 sub-18 Voice
739 sub-18_Singing pSTG_lh_pSTG_rh 0.809301 sub-18 Singing
740 sub-18_Music pSTG_lh_pSTG_rh 0.836192 sub-18 Music
741 sub-19_Voice pSTG_lh_pSTG_rh 0.877768 sub-19 Voice
742 sub-19_Singing pSTG_lh_pSTG_rh 0.781274 sub-19 Singing
743 sub-19_Music pSTG_lh_pSTG_rh 0.833543 sub-19 Music
744 sub-20_Voice pSTG_lh_pSTG_rh 0.827523 sub-20 Voice
745 sub-20_Singing pSTG_lh_pSTG_rh 0.834294 sub-20 Singing
746 sub-20_Music pSTG_lh_pSTG_rh 0.837192 sub-20 Music
747 sub-21_Voice pSTG_lh_pSTG_rh 0.723124 sub-21 Voice
748 sub-21_Singing pSTG_lh_pSTG_rh 0.624693 sub-21 Singing
749 sub-21_Music pSTG_lh_pSTG_rh 0.650059 sub-21 Music
750 sub-22_Voice pSTG_lh_pSTG_rh 0.786322 sub-22 Voice
751 sub-22_Singing pSTG_lh_pSTG_rh 0.792888 sub-22 Singing
752 sub-22_Music pSTG_lh_pSTG_rh 0.829268 sub-22 Music
753 sub-23_Voice pSTG_lh_pSTG_rh 0.808971 sub-23 Voice
754 sub-23_Singing pSTG_lh_pSTG_rh 0.851517 sub-23 Singing
755 sub-23_Music pSTG_lh_pSTG_rh 0.796080 sub-23 Music
756 sub-24_Voice pSTG_lh_pSTG_rh 0.684387 sub-24 Voice
757 sub-24_Singing pSTG_lh_pSTG_rh 0.567898 sub-24 Singing
758 sub-24_Music pSTG_lh_pSTG_rh 0.621611 sub-24 Music

759 rows × 5 columns

Let’s also save that one:

con_info_long.to_csv(opj(results_path, 'results/nibetaseries/nibetaseries_transform/beta_series/mvs_con_info_desc-long.csv'), index=False)

And it is raincloud plot time. We will plot correlation values against connections, indicating conditions via our defined colormap:

def plot_roi2roi(interactive=True):
    
    
    if interactive is False:
        
        cmap = plt.cm.get_cmap('Set2')

        music_color = cmap(0.3)
        voice_color = cmap(0.2)
        singing_color = cmap(0.0)

        sns.set_context('paper')
        sns.set_style('whitegrid')

        plt.rcParams["font.family"] = "Arial"

        f, ax = plt.subplots(figsize=(12, 22))
        dx="connection"; dy="cor_values"; dhue = "condition"; ort="h"; pal = sns.color_palette([voice_color, singing_color, music_color]); sigma = .2

        ax=pt.RainCloud(x = dx, y = dy, hue = dhue, data = con_info_long[['cor_values', 'connection', 'condition']], palette = pal, bw = sigma,
        width_viol = .7, ax = ax, orient = ort , alpha = .55, dodge = True)

        plt.xlabel('Correlation values', fontsize=20)
        plt.ylabel('Connection', fontsize=20)
        ax.set_yticklabels(list(pd.Series(con_info_long['connection'].unique()).str.replace("h_", "h to ").str.replace("_", "-")))
        ax.tick_params(axis='x', labelsize=14) 
        ax.tick_params(axis='y', labelsize=12) 

        plt.title('ROI to ROI correlation per condition', fontsize=22)
        sns.despine(offset=5)

        roi_legend = [Line2D([0], [0], color=colors.to_hex(voice_color), lw=4, label='voice'),
                      Line2D([0], [0], color=colors.to_hex(singing_color), lw=4, label='singing'), 
                      Line2D([0], [0], color=colors.to_hex(music_color), lw=4, label='music')
                           ]

        plt.legend(handles=roi_legend, bbox_to_anchor=(0.5, -0.08), fontsize=14, ncol=3, loc="lower center", frameon=False)
        
    else:
        
        
        fig = px.violin(con_info_long,y='connection',x='cor_values', color='condition', box=True, 
          orientation='h', template='plotly_white', 
                color_discrete_map={
                "Voice": 'rgb%s' %str(sns.color_palette('Set2')[1]),
                "Singing": 'rgb%s' %str(sns.color_palette('Set2')[0]),
                "Music": 'rgb%s' %str(sns.color_palette('Set2')[2])},
                labels={
                     "connection": "Connection",
                     "cor_values": "Correlation values",
                     "condition": "Condition"}).update_traces(side='positive',width=1)
        fig.update_layout(autosize=False, width=800, height=1000, 
                          font_family="Arial")
        fig.update_layout(
            title={
                'text': "ROI to ROI correlation per condition", 
                'y':0.98,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'})
        fig.update_traces(points='all', jitter=0)
        fig.show()
plot_roi2roi(interactive=False)
_images/Connectivity_analyses-ROI_93_0.png

Now that we have computed the ROI to ROI correlation in our reduced auditory cortex network model for each participant and condition, as well as investigated the outputs on a descriptive level, it is time to compute some statistics. More precisely, we will assess two things: (1) which connections are significantly different from 0 for each condition and (2) which connections differ significantly between conditions.

Comparison of condition-specific functional connectivity

In order to assess which connections are significantly different from 0 for a given condition, we will compute a one-sample permutation test per connection and condition. This approach entails generating a null distribution via flipping the sign of a random number of values and recomputing the difference between the resulting mean and assumend 0 within each permutation fold. The original mean difference is then compared against this distribution and obtained p-values corrected for multiple comparisons through the Benjamin-Hochberg implementation of the FDR. To provide a detailed overview, we will do that step by step on an example, the voice condition. At first, we restrict our ROI to ROI DataFrame to include only data from the voice condition:

con_info_long_voice = con_info_long[con_info_long['condition']=='Voice']
con_info_long_voice
part_cond connection cor_values participant condition
0 sub-01_Voice HG_lh_HG_rh 0.729744 sub-01 Voice
3 sub-02_Voice HG_lh_HG_rh 0.830138 sub-02 Voice
6 sub-03_Voice HG_lh_HG_rh 0.875702 sub-03 Voice
9 sub-04_Voice HG_lh_HG_rh 0.661841 sub-04 Voice
12 sub-05_Voice HG_lh_HG_rh 0.897685 sub-05 Voice
15 sub-07_Voice HG_lh_HG_rh 0.805987 sub-07 Voice
18 sub-08_Voice HG_lh_HG_rh 0.791797 sub-08 Voice
21 sub-09_Voice HG_lh_HG_rh 0.873669 sub-09 Voice
24 sub-10_Voice HG_lh_HG_rh 0.819320 sub-10 Voice
27 sub-11_Voice HG_lh_HG_rh 0.889455 sub-11 Voice
30 sub-12_Voice HG_lh_HG_rh 0.793997 sub-12 Voice
33 sub-13_Voice HG_lh_HG_rh 0.750438 sub-13 Voice
36 sub-14_Voice HG_lh_HG_rh 0.774893 sub-14 Voice
39 sub-15_Voice HG_lh_HG_rh 0.725923 sub-15 Voice
42 sub-16_Voice HG_lh_HG_rh 0.804853 sub-16 Voice
45 sub-17_Voice HG_lh_HG_rh 0.852302 sub-17 Voice
48 sub-18_Voice HG_lh_HG_rh 0.864487 sub-18 Voice
51 sub-19_Voice HG_lh_HG_rh 0.762531 sub-19 Voice
54 sub-20_Voice HG_lh_HG_rh 0.809400 sub-20 Voice
57 sub-21_Voice HG_lh_HG_rh 0.461008 sub-21 Voice
60 sub-22_Voice HG_lh_HG_rh 0.858569 sub-22 Voice
63 sub-23_Voice HG_lh_HG_rh 0.667993 sub-23 Voice
66 sub-24_Voice HG_lh_HG_rh 0.569008 sub-24 Voice
69 sub-01_Voice HG_lh_PP_lh 0.730356 sub-01 Voice
72 sub-02_Voice HG_lh_PP_lh 0.877590 sub-02 Voice
75 sub-03_Voice HG_lh_PP_lh 0.739229 sub-03 Voice
78 sub-04_Voice HG_lh_PP_lh 0.669932 sub-04 Voice
81 sub-05_Voice HG_lh_PP_lh 0.807468 sub-05 Voice
84 sub-07_Voice HG_lh_PP_lh 0.574494 sub-07 Voice
87 sub-08_Voice HG_lh_PP_lh 0.766930 sub-08 Voice
... ... ... ... ... ...
669 sub-18_Voice PT_rh_pSTG_rh 0.841723 sub-18 Voice
672 sub-19_Voice PT_rh_pSTG_rh 0.843286 sub-19 Voice
675 sub-20_Voice PT_rh_pSTG_rh 0.718560 sub-20 Voice
678 sub-21_Voice PT_rh_pSTG_rh 0.697720 sub-21 Voice
681 sub-22_Voice PT_rh_pSTG_rh 0.784101 sub-22 Voice
684 sub-23_Voice PT_rh_pSTG_rh 0.687612 sub-23 Voice
687 sub-24_Voice PT_rh_pSTG_rh 0.537573 sub-24 Voice
690 sub-01_Voice pSTG_lh_pSTG_rh 0.771692 sub-01 Voice
693 sub-02_Voice pSTG_lh_pSTG_rh 0.814807 sub-02 Voice
696 sub-03_Voice pSTG_lh_pSTG_rh 0.827930 sub-03 Voice
699 sub-04_Voice pSTG_lh_pSTG_rh 0.735072 sub-04 Voice
702 sub-05_Voice pSTG_lh_pSTG_rh 0.889023 sub-05 Voice
705 sub-07_Voice pSTG_lh_pSTG_rh 0.744635 sub-07 Voice
708 sub-08_Voice pSTG_lh_pSTG_rh 0.826184 sub-08 Voice
711 sub-09_Voice pSTG_lh_pSTG_rh 0.792606 sub-09 Voice
714 sub-10_Voice pSTG_lh_pSTG_rh 0.584261 sub-10 Voice
717 sub-11_Voice pSTG_lh_pSTG_rh 0.909496 sub-11 Voice
720 sub-12_Voice pSTG_lh_pSTG_rh 0.846500 sub-12 Voice
723 sub-13_Voice pSTG_lh_pSTG_rh 0.758354 sub-13 Voice
726 sub-14_Voice pSTG_lh_pSTG_rh 0.874212 sub-14 Voice
729 sub-15_Voice pSTG_lh_pSTG_rh 0.816032 sub-15 Voice
732 sub-16_Voice pSTG_lh_pSTG_rh 0.873360 sub-16 Voice
735 sub-17_Voice pSTG_lh_pSTG_rh 0.850614 sub-17 Voice
738 sub-18_Voice pSTG_lh_pSTG_rh 0.792311 sub-18 Voice
741 sub-19_Voice pSTG_lh_pSTG_rh 0.877768 sub-19 Voice
744 sub-20_Voice pSTG_lh_pSTG_rh 0.827523 sub-20 Voice
747 sub-21_Voice pSTG_lh_pSTG_rh 0.723124 sub-21 Voice
750 sub-22_Voice pSTG_lh_pSTG_rh 0.786322 sub-22 Voice
753 sub-23_Voice pSTG_lh_pSTG_rh 0.808971 sub-23 Voice
756 sub-24_Voice pSTG_lh_pSTG_rh 0.684387 sub-24 Voice

253 rows × 5 columns

Next, we will set up a small for-loop that computes the mean, mean difference and p value for each connection:

list_con = []
list_mean = []
list_mean_diff = [] 
list_p_values = []

for connection in con_info_long_voice['connection'].unique():
    
    print('working on connection: %s' %connection)
    
    list_con.append(connection)
    
    df_tmp = con_info_long_voice[con_info_long_voice['connection']==connection]
    
    list_mean.append(df_tmp['cor_values'].mean())
    
    list_mean_diff.append(nnstats.permtest_1samp(df_tmp['cor_values'], 0.0)[0])
    
    list_p_values.append(nnstats.permtest_1samp(df_tmp['cor_values'], 0.0)[1])
working on connection: HG_lh_HG_rh
working on connection: HG_lh_PP_lh
working on connection: HG_rh_PP_rh
working on connection: HG_lh_PT_lh
working on connection: HG_rh_PT_rh
working on connection: PT_lh_PT_rh
working on connection: PP_lh_aSTG_lh
working on connection: PP_rh_aSTG_rh
working on connection: PT_lh_pSTG_lh
working on connection: PT_rh_pSTG_rh
working on connection: pSTG_lh_pSTG_rh

As usual, we will pack everything into a DataFrame to make things easier:

con_info_long_voice_stats = pd.DataFrame({'connection': list_con, 'mean': list_mean, 'mean_diff': list_mean_diff, 'p_value': list_p_values})
con_info_long_voice_stats
connection mean mean_diff p_value
0 HG_lh_HG_rh 0.776989 0.776989 0.000999
1 HG_lh_PP_lh 0.723761 0.723761 0.000999
2 HG_rh_PP_rh 0.706425 0.706425 0.000999
3 HG_lh_PT_lh 0.791526 0.791526 0.000999
4 HG_rh_PT_rh 0.761162 0.761162 0.000999
5 PT_lh_PT_rh 0.805668 0.805668 0.000999
6 PP_lh_aSTG_lh 0.674060 0.674060 0.000999
7 PP_rh_aSTG_rh 0.631744 0.631744 0.000999
8 PT_lh_pSTG_lh 0.737993 0.737993 0.000999
9 PT_rh_pSTG_rh 0.757304 0.757304 0.000999
10 pSTG_lh_pSTG_rh 0.800660 0.800660 0.000999

So far, our p-values are not corrected for multiple comparisons and thus we use the great pingouin package to do that. All we need to do is provide the list of our uncorrected p-values and create a new column in our DataFrame to store the obtained corrected p-values.

con_info_long_voice_stats['p_values_FDR'] = multicomp(con_info_long_voice_stats['p_value'].to_list(), alpha=0.05, method='fdr_bh')[1]
con_info_long_voice_stats
connection mean mean_diff p_value p_values_FDR
0 HG_lh_HG_rh 0.776989 0.776989 0.000999 0.000999
1 HG_lh_PP_lh 0.723761 0.723761 0.000999 0.000999
2 HG_rh_PP_rh 0.706425 0.706425 0.000999 0.000999
3 HG_lh_PT_lh 0.791526 0.791526 0.000999 0.000999
4 HG_rh_PT_rh 0.761162 0.761162 0.000999 0.000999
5 PT_lh_PT_rh 0.805668 0.805668 0.000999 0.000999
6 PP_lh_aSTG_lh 0.674060 0.674060 0.000999 0.000999
7 PP_rh_aSTG_rh 0.631744 0.631744 0.000999 0.000999
8 PT_lh_pSTG_lh 0.737993 0.737993 0.000999 0.000999
9 PT_rh_pSTG_rh 0.757304 0.757304 0.000999 0.000999
10 pSTG_lh_pSTG_rh 0.800660 0.800660 0.000999 0.000999

Coolio, now to visualization. While the DataFrame is nicely structured and informative, a corresponding graphic would be useful as well. What we are going to do is reusing steps from above and bring our DataFrame values back into the correlation matrix/network structure. Importantly, we will do that for mean values but only those which p-value are surviving thethreshold (p<=0.05).

voice_stats = np.zeros_like(network_structure_reduced)
voice_stats[connection_analyses_indices] = [row['mean']  if row['p_values_FDR'] <= 0.05 else np.nan for i, row in con_info_long_voice_stats.iterrows()]
voice_stats[voice_stats == 0] = np.nan

With that, we can plot the results as we did before:

fig = px.imshow(voice_stats, x=atlas_rois_df['regions'], y=atlas_rois_df['regions'],
               color_continuous_scale='darkmint_r')
fig.update_layout(coloraxis_showscale=True)
fig['layout'].update(plot_bgcolor='white')

plot(fig, filename = 'ac_network_stats-voice.html')
display(HTML('ac_network_stats-voice.html'))

And the connectome version:

plot_auditory_cortex_connectome(voice_stats, atlas_info=atlas_rois_df, name='Voice', model=False)
_images/Connectivity_analyses-ROI_108_0.png

Nice, some informative and easy to grasp figures to visualize the results of the statistical analyses. As we don’t want to run all this every time for each condition, we pack everything into a function. While doing so, we will also extend it a bit to support not only one-sample permutation tests for a single condition but also two sample related permutation tests for comparisons between conditions. The latter will be explained in more detail later:

def connection_stats(condition):
    
    if len(condition) == 1:
        print('Computing stats for condition: %s.' %condition[0])
        plot_name = condition[0]
        plot_name_graphic = condition[0]
    else:
        print('Computing stats for difference between %s and %s.' %(condition[0], condition[1]))
        plot_name = '%s vs %s' %(condition[0], condition[1])
        plot_name_graphic = '%svs%s' %(condition[0], condition[1])

    con_info_long_tmp = con_info_long[con_info_long['condition'].isin(condition)]

    list_con = []
    list_mean = []
    list_mean_2 = []
    list_mean_diff = [] 
    list_p_values = []

    for connection in con_info_long_tmp['connection'].unique():

        print('working on connection: %s' %connection)

        list_con.append(connection)

        df_tmp = con_info_long_tmp[con_info_long_tmp['connection']==connection]

        if len(condition) == 1:
            
            list_mean.append(df_tmp['cor_values'].mean())

            list_mean_diff.append(nnstats.permtest_1samp(df_tmp['cor_values'], 0.0)[0])

            list_p_values.append(nnstats.permtest_1samp(df_tmp['cor_values'], 0.0)[1])

            con_info_long_tmp_stats = pd.DataFrame({'connection': list_con, 'mean': list_mean, 'mean_diff': list_mean_diff, 'p_value': list_p_values})

            con_info_long_tmp_stats['p_values_FDR'] = multicomp(con_info_long_tmp_stats['p_value'].to_list(), alpha=0.05, method='fdr_bh')[1]
            
        else:

            list_mean.append(df_tmp[df_tmp['condition'] == condition[0]]['cor_values'].mean())

            list_mean_2.append(df_tmp[df_tmp['condition'] == condition[1]]['cor_values'].mean())
            
            list_mean_diff.append(nnstats.permtest_rel(df_tmp[df_tmp['condition'] == condition[0]]['cor_values'],
                                                       df_tmp[df_tmp['condition'] == condition[1]]['cor_values'])[0])

            list_p_values.append(nnstats.permtest_rel(df_tmp[df_tmp['condition'] == condition[0]]['cor_values'],
                                                       df_tmp[df_tmp['condition'] == condition[1]]['cor_values'])[1])

            con_info_long_tmp_stats = pd.DataFrame({'connection': list_con, 'mean_%s' %condition[0]: list_mean, 'mean_%s' %condition[1]: list_mean_2, 
                                                    'mean_diff': list_mean_diff, 'p_value': list_p_values})

            con_info_long_tmp_stats['p_values_FDR'] = multicomp(con_info_long_tmp_stats['p_value'].to_list(), alpha=0.05, method='fdr_bh')[1]


    tmp_stats = np.zeros_like(network_structure_reduced)
    tmp_stats[connection_analyses_indices] = [row['mean']  if row['p_values_FDR'] <= 0.05 else np.nan for i, row in con_info_long_tmp_stats.iterrows()]
    tmp_stats[tmp_stats == 0] = np.nan
        
    if np.isnan(tmp_stats).all():

        print('No connection significant.')
        
    else:
        
        fig = px.imshow(tmp_stats, x=atlas_rois_df['regions'], y=atlas_rois_df['regions'],
                       color_continuous_scale='darkmint_r')
        fig.update_layout(coloraxis_showscale=True, font_family="Arial")
        fig.update_layout(title={'text': 'Connection statistics (mean, FDR<=0.05) - %s' %plot_name, 'y':0.9, 'x':0.5, 'xanchor': 'center', 'yanchor': 'top'})
        fig['layout'].update(plot_bgcolor='white')
        plot(fig, filename = 'ac_network_stats-%s.html' %plot_name_graphic)
        display(HTML('ac_network_stats-%s.html' %plot_name_graphic))

        plot_auditory_cortex_connectome(tmp_stats, atlas_info=atlas_rois_df, name=plot_name, model=False)
        
        
              
    return con_info_long_tmp_stats, tmp_stats

To be sure everything works as expected, let’s recompute the outputs for the voice condition:

con_voice, stats_voice = connection_stats(['Voice'])
Computing stats for condition: Voice.
working on connection: HG_lh_HG_rh
working on connection: HG_lh_PP_lh
working on connection: HG_rh_PP_rh
working on connection: HG_lh_PT_lh
working on connection: HG_rh_PT_rh
working on connection: PT_lh_PT_rh
working on connection: PP_lh_aSTG_lh
working on connection: PP_rh_aSTG_rh
working on connection: PT_lh_pSTG_lh
working on connection: PT_rh_pSTG_rh
working on connection: pSTG_lh_pSTG_rh
_images/Connectivity_analyses-ROI_112_2.png

That looks about right. Now, we can easily apply the function to compute the statistics for the other conditions. At first, singing:

con_singing, stats_singing = connection_stats(['Singing'])
Computing stats for condition: Singing.
working on connection: HG_lh_HG_rh
working on connection: HG_lh_PP_lh
working on connection: HG_rh_PP_rh
working on connection: HG_lh_PT_lh
working on connection: HG_rh_PT_rh
working on connection: PT_lh_PT_rh
working on connection: PP_lh_aSTG_lh
working on connection: PP_rh_aSTG_rh
working on connection: PT_lh_pSTG_lh
working on connection: PT_rh_pSTG_rh
working on connection: pSTG_lh_pSTG_rh
_images/Connectivity_analyses-ROI_114_2.png

And second, music:

con_music, stats_music = connection_stats(['Music'])
Computing stats for condition: Music.
working on connection: HG_lh_HG_rh
working on connection: HG_lh_PP_lh
working on connection: HG_rh_PP_rh
working on connection: HG_lh_PT_lh
working on connection: HG_rh_PT_rh
working on connection: PT_lh_PT_rh
working on connection: PP_lh_aSTG_lh
working on connection: PP_rh_aSTG_rh
working on connection: PT_lh_pSTG_lh
working on connection: PT_rh_pSTG_rh
working on connection: pSTG_lh_pSTG_rh
_images/Connectivity_analyses-ROI_116_2.png

After computing the statistics for a single condition via one sample permutation tests, it is time to compare connections between conditions via two sample permutation tests. Here, null distributions are generated by exchanging a random number of values between conditions and recomputing the difference in means within a given permutation fold. The original difference in means is then compared to the obtained distribution. Using our function, all we need to do to achieve this is providing a list of the two conditions we want to compare. Everything else works as before. Let’s start with the comparison between the voice and singing condition.

con_voice_singing, stats_voice_singing = connection_stats(['Voice', 'Singing'])
Computing stats for difference between Voice and Singing.
working on connection: HG_lh_HG_rh
working on connection: HG_lh_PP_lh
working on connection: HG_rh_PP_rh
working on connection: HG_lh_PT_lh
working on connection: HG_rh_PT_rh
working on connection: PT_lh_PT_rh
working on connection: PP_lh_aSTG_lh
working on connection: PP_rh_aSTG_rh
working on connection: PT_lh_pSTG_lh
working on connection: PT_rh_pSTG_rh
working on connection: pSTG_lh_pSTG_rh
No connection significant.

We do not see any graphics, because no connection is significantly different between the conditions as indicated by the output. How do things look for the comparison between voice and music?

con_voice_music, stats_voice_music = connection_stats(['Voice', 'Music'])
Computing stats for difference between Voice and Music.
working on connection: HG_lh_HG_rh
working on connection: HG_lh_PP_lh
working on connection: HG_rh_PP_rh
working on connection: HG_lh_PT_lh
working on connection: HG_rh_PT_rh
working on connection: PT_lh_PT_rh
working on connection: PP_lh_aSTG_lh
working on connection: PP_rh_aSTG_rh
working on connection: PT_lh_pSTG_lh
working on connection: PT_rh_pSTG_rh
working on connection: pSTG_lh_pSTG_rh
No connection significant.

The same, no connection is significantly different. Last but not least: the comparison between music and singing:

con_music_singing, stats_music_singing = connection_stats(['Music', 'Singing'])
Computing stats for difference between Music and Singing.
working on connection: HG_lh_HG_rh
working on connection: HG_lh_PP_lh
working on connection: HG_rh_PP_rh
working on connection: HG_lh_PT_lh
working on connection: HG_rh_PT_rh
working on connection: PT_lh_PT_rh
working on connection: PP_lh_aSTG_lh
working on connection: PP_rh_aSTG_rh
working on connection: PT_lh_pSTG_lh
working on connection: PT_rh_pSTG_rh
working on connection: pSTG_lh_pSTG_rh
No connection significant.

Same same. This comparison also concludes the ROI to ROI correlation analyses of the the Connectivity analyses.

Summary and outlook