How to measure similarity between two correlation matrices | by Jin Hyun Cheong, PhD | Towards Data Science

Comparing the similarity between matrices can offer a way to assess the structural relationships among variables and is commonly used across disciplines such as neuroscience, epidemiology, and ecology.
Let’s assume you have data on how people react when watching a short Youtube video. This could be measured by facial expressions, heart rate, or breathing. A subject-by-subject similarity matrix of this data would represent how similar each person’s emotions were to every other subject. High similarity value in the matrix would mean that those individuals’ reactions were more similar than others.
You can compare this reaction similarity matrix with similarities in other factors such as demographics, interests, and/or preferences. This allows you to directly test if similarities in one domain (e.g. emotional reactions) can be explained by similarities in another (e.g. demographic/preference similarities).
This tutorial introduces the different metrics that can be used to measure similarity between similarity matrices and how to calculate the significance of those values.
Let’s simulate some data for analysis. We create a random data m1_u and m2_u that are related by the amount of noise added nr. Next, we create a correlation matrix for each underlying data using the default pandas correlation function. Let’s assume that the correlation matrix m1 represents how similar each subject’s reactions were to every other subject and m2 represents how similar each subject’s preferences were to one another.
We can simulate and visualize these matrices with the following code.
Now let’s see how we can test the similarity of these two matrices.
To measure the similarity between two correlation matrices you first need to extract either the top or the bottom triangle. They are symmetric but I recommend extracting the top triangle as it offers more consistency with other matrix functions when recasting the upper triangle back into a matrix.
The way to extract the upper triangle is simple. You can use the following function upper which leverages numpy functionality triu_indices. We can see from the figure below that the extracted upper triangle matches the original matrix.
Now that you have the matrices in simple vector forms, you can use the metric you need to measure similarity. The precise metric you use will depend on the properties of data you are working with but it is recommended to use a rank correlation such as a Spearman’s ρ or a Kendall’s τ. The benefit is that you don’t need to assume that similarity increases are linear and results will also be more robust to outliers.
In this example, let’s use the Spearman correlation to measure the similarity.
Which yields the following result of a Spearman rho of .15. The Spearman function also offers a p-value of .038, but using this would be inaccurate. Our data is non-independent meaning that each cell value from the upper matrix cannot be taken out independently without affecting other cells that arise from the same subject (learn more here). Alternatively, we can use the permutation testing approach outlined in Step 2.
Each cell value from our upper matrices are non-independent from one another. The independence is at the level of the subject which will be what we would be permuting.
In each iteration of the for loop in the function below, we shuffle the order of rows and columns from one of the correlation matrices, m1. We re-calculate the similarity between two matrices the amount of time we want to permute (e.g. 5000 times). After that, we can see how many values fall above our estimated value to obtain a p-value.
As shown in the figure above, our permuted p is more conservative at p=.042 than the one we obtained earlier at p=.038.
Comparing similarity matrices can help identify shared latent structures in the data across modalities. This tutorial offers a basic approach to assessing the similarity and calculating significance with a non-parametric permutation testing.
This approach can be extended in many ways. One extension would be to compare the average level of similarity between two correlation matrices. You can use the same approach of extracting the upper triangle and permuting the data to get significance for the difference. One thing to add however is that you’ll want to convert the Pearson’s R into a Z distribution using the Fisher transformation to make the distribution approximately normal.
Another extension would be to compare two distance matrices, such as geographical distance, Euclidean distance, or Mahalanobis distance. All you have to do is to create a distance matrix rather than correlation matrix. A real-world example with data comparing how distance between cities relates to average temperature differences is included in the full Colab notebook in the link below.
Author: Jin Hyun Cheong
import seaborn as sns
import numpy as np
import pandas as pd
import scipy.stats as stats
from scipy.spatial.distance import squareform
import matplotlib.pyplot as plt

# n is the number of subjects or items that will determine the size of the correlation matrix. 
n = 20 
data_length = 50
nr = .58

# Random data
np.random.seed(0)
m1_u = np.random.normal(loc=0, scale=1, size=(data_length, n))
m2_u = nr*np.random.normal(loc=0, scale=1, size=(data_length, n)) + (1-nr)*m1_u

m1 = pd.DataFrame(m1_u).corr()
m2 = pd.DataFrame(m2_u).corr()

f,axes = plt.subplots(1,2, figsize=(10,5))
sns.set_style("white")
for ix, m in enumerate([m1,m2]):
  sns.heatmap(m, cmap="RdBu_r", center=0, vmin=-1, vmax=1, ax=axes[ix], square=True, cbar_kws={"shrink": .5}, xticklabels=True)
  axes[ix].set(title=f"m{ix+1}")

def upper(df):
    '''Returns the upper triangle of a correlation matrix.

    You can use scipy.spatial.distance.squareform to recreate matrix from upper triangle.

    Args:
      df: pandas or numpy correlation matrix

    Returns:
      list of values from upper triangle
    '''
    try:
        assert(type(df)==np.ndarray)
    except:
        if type(df)==pd.DataFrame:
            df = df.values
        else:
            raise TypeError('Must be np.ndarray or pd.DataFrame')
    mask = np.triu_indices(df.shape[0], k=1)
    return df[mask]

# Compare that the upper triangle is properly extracted. 
print("Extracted upper triangle from m1")
print(np.round(upper(m1)[:25],3))
print("Top 3 rows from m1")
display(m1.iloc[:3, :].round(3))

Extracted upper triangle from m1
[ 0.002  0.023  0.04  -0.024 -0.063  0.228  0.036 -0.035  0.056  0.034
  0.003  0.181  0.367  0.022  0.079 -0.126 -0.222  0.075  0.011  0.235
 -0.042  0.276 -0.065  0.069 -0.153]
Top 3 rows from m1

# You can go back to the original matrix using scipy's squareform. 
# But note that the diagonal will be 0s. 
recovered_m1 = pd.DataFrame(squareform(upper(m1))).round(2)

f,axes = plt.subplots(1,2, figsize=(10,5))
sns.set_style("white")
for ix, m in enumerate([m1,recovered_m1]):
  sns.heatmap(m, cmap="RdBu_r", center=0, vmin=-1, vmax=1, ax=axes[ix], square=True, cbar_kws={"shrink": .5})

# Now lets measure the similarity 
print(stats.spearmanr(upper(m1), upper(m2)))

SpearmanrResult(correlation=0.14991317735875465, pvalue=0.038974535666595055)

"""Nonparametric permutation testing Monte Carlo"""
np.random.seed(0)
rhos = []
n_iter = 5000
true_rho, _ = stats.spearmanr(upper(m1), upper(m2))
# matrix permutation, shuffle the groups
m_ids = list(m1.columns)
m2_v = upper(m2)
for iter in range(n_iter):
  np.random.shuffle(m_ids) # shuffle list 
  r, _ = stats.spearmanr(upper(m1.loc[m_ids, m_ids]), m2_v)  
  rhos.append(r)
perm_p = ((np.sum(np.abs(true_rho) <= np.abs(rhos)))+1)/(n_iter+1) # two-tailed test


f,ax = plt.subplots()
plt.hist(rhos,bins=20)
ax.axvline(true_rho,  color = 'r', linestyle='--')
ax.set(title=f"Permuted p: {perm_p:.3f}", ylabel="counts", xlabel="rho")
plt.show()

"""Real data of including latitude, longitude, average temperature, and city name. 

References:
  https://keisan.casio.com/exec/system/1224587128
  https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
"""
seoul = (37.566535, 126.977969, 12.5, "Seoul")
tokyo = (35.676192, 139.650311, 15.4, "Tokyo")
sf = (37.77493, -122.419415, 14.6, "San Francisco")
la = (34.052234, -118.243685, 18.6, "Los Angeles")
chicago = (41.878114, -87.629798, 9.8, "Chicago")
beijing = (39.904211, 116.407395, 12.9, "Beijing")
cairo = (30.04442, 31.235712, 21.4, "Cairo")
moscow = (55.755826, 	37.6173, 5.8, "Moscow")
sydney = (-33.86882, 151.209296, 17.7, "Sydney")
london =(51.507218, -0.127586, 11.3, "London")
paris = (48.856614, 2.352222, 12.3, "Paris")
berlin = (52.520007, 13.404954, 10.3, "Berlin")
rome = (41.902784, 12.496366, 15.2, "Rome")
lima = (-12.046373, -77.042754, 19.2, "Lima")
mexicocity = (19.432608, -99.133208, 17.5, "Mexico City")
capetown = (-33.924868, 18.424055, 16.2, "Cape Town")
newdelhi = (28.613939, 77.209021, 25, "New Delhi")
auckland = (-36.850883, 174.764488, 15.2, "Auckland")
saopaulo = (-23.555771, -46.639557, 19.2, "Sao Paulo")
madrid = (40.416775, -3.70379, 15, "Madrid")
bangkok = (13.756331, 100.501765, 28.6, "Bangkok")
phnompenh = (11.556374,104.92821, 28.3, "Phnom Penh")
mumbai = (19.075984, 72.877656, 27.1, "Mumbai")
singapore = (1.352083, 103.819836, 27, "Singapore")
vladivostok = (43.133248, 131.911297, 4.9, "Vladivostok")
city_list = [seoul, tokyo, sf, la, chicago, beijing, cairo, moscow, sydney, london, paris, 
             berlin, rome, lima, mexicocity, capetown, newdelhi, auckland, saopaulo, madrid,
             bangkok, phnompenh, mumbai, singapore, vladivostok]

# Measure similarity 
n = len(city_list)
city_names = [city[-1] for city in city_list]
m1 = pd.DataFrame(np.zeros((n,n)), index = city_names, columns = city_names)
m2 = pd.DataFrame(np.zeros((n,n)), index = city_names, columns = city_names)
for i, city1 in enumerate(city_list):
  for j, city2 in enumerate(city_list):
    # m1.iloc[i,j] = distance(city1[0], city1[1], city2[0], city2[1])
    m1.iloc[i,j] = np.abs(city1[0]-city2[0]) 
    m2.iloc[i,j] = np.abs(city1[2]-city2[2])

stats.spearmanr(upper(m1), upper(m2))

SpearmanrResult(correlation=0.21213613537496678, pvalue=0.00021460054002915316)
# Permute for significance
rhos = []
n_iter = 5000
true_rho, _ = stats.spearmanr(upper(m1), upper(m2))
# matrix permutation, shuffle the groups
m_ids = list(m1.columns)
m2_v = upper(m2)
for iter in range(n_iter):
  np.random.shuffle(m_ids) # shuffle list 
  r, _ = stats.spearmanr(upper(m1.loc[m_ids, m_ids]), m2_v)  
  rhos.append(r)
perm_p = ((np.sum(np.abs(true_rho) <= np.abs(rhos)))+1)/(n_iter+1) # two-tailed test

f,ax = plt.subplots()
plt.hist(rhos,bins=20)
ax.axvline(true_rho,  color = 'r', linestyle='--')
ax.set(title=f"Permuted p: {perm_p:.3f}", ylabel="counts", xlabel="rho")
plt.show()

Thank you for reading. If you liked this article, please consider subscribing and supporting writers like me by joining Medium through my referral link through which I will earn a small commission at no cost to you. Here are some other articles you might like: