Sort simmilarity matrix according to plot colors

Viewed 448

I have this similarity matrix plot of some documents. I want to sort the values of the matrix, which is a numpynd array, to group colors, while maintaining their relative position (diagonal yellow line), and labels as well.

path = "C:\\Users\\user\\Desktop\\texts\\dataset"
text_files = os.listdir(path)
#print (text_files)

tfidf_vectorizer = TfidfVectorizer()
documents = [open(f, encoding="utf-8").read() for f in text_files if f.endswith('.txt')]
sparse_matrix = tfidf_vectorizer.fit_transform(documents)

labels = []
for f in text_files:
    if f.endswith('.txt'):
        labels.append(f)

pairwise_similarity = sparse_matrix * sparse_matrix.T

pairwise_similarity_array = pairwise_similarity.toarray()
 
fig, ax = plt.subplots(figsize=(20,20))
cax = ax.matshow(pairwise_similarity_array, interpolation='spline16')
ax.grid(True)
plt.title('News articles similarity matrix')
plt.xticks(range(23), labels, rotation=90);
plt.yticks(range(23), labels);
fig.colorbar(cax, ticks=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1])
plt.show() 

enter image description here

2 Answers

Here is one possibility. The idea is to use the information in the similarity matrix and put elements next to each other if they are similar. If two items are similar they should also be similar with respect to other elements ie have similar colors. I start with the element which has the most in common with all other elements (this choice is a bit arbitrary) [a] and as next element I choose from the remaining elements the one which is closest to the current [b].

import numpy as np
import matplotlib.pyplot as plt

def create_dummy_sim_mat(n):
    sm = np.random.random((n, n))
    sm = (sm + sm.T) / 2
    sm[range(n), range(n)] = 1
    return sm

def argsort_sim_mat(sm):
    idx = [np.argmax(np.sum(sm, axis=1))]  # a
    for i in range(1, len(sm)):
        sm_i = sm[idx[-1]].copy()
        sm_i[idx] = -1
        idx.append(np.argmax(sm_i))  # b
    return np.array(idx)


n = 10
sim_mat = create_dummy_sim_mat(n=n)

idx = argsort_sim_mat(sim_mat)
sim_mat2 = sim_mat[idx, :][:, idx]  # apply reordering for rows and columns

# Plot results
fig, ax = plt.subplots(1, 2)
ax[0].imshow(sim_mat)
ax[1].imshow(sim_mat2)

def ticks(_ax, ti, la):
    _ax.set_xticks(ti)
    _ax.set_yticks(ti)
    _ax.set_xticklabels(la)
    _ax.set_yticklabels(la)

ticks(_ax=ax[0], ti=range(n), la=range(n))
ticks(_ax=ax[1], ti=range(n), la=idx)

similarity sorted


After meTchaikovsky's answer I also tested my idea on a clustered similarity matrix (see first image) this method works but is not perfect (see second image).

Because I use the similarity between two elements as approximation to their similarity to all other elements, it is quite clear why this does not work perfectly. So instead of using the initial similarity to sort the elements one could calculate a second order similarity matrix which measures how similar the similarities are (sorry). This measure describes better what you are interested in. If two rows / columns have similar colors they should be close to each other. The algorithm to sort the matrix is the same as before

def add_cluster(sm, c=3):
    idx_cluster = np.array_split(np.random.permutation(np.arange(len(sm))), c)
    for ic in idx_cluster:
        cluster_noise = np.random.uniform(0.9, 1.0, (len(ic),)*2)
        sm[ic[np.newaxis, :], ic[:, np.newaxis]] = cluster_noise

def get_sim_mat2(sm):
    return 1 / (np.linalg.norm(sm[:, np.newaxis] - sm[np.newaxis], axis=-1) + 1/n)

sim_mat = create_dummy_sim_mat(n=100)
add_cluster(sim_mat, c=4)
sim_mat2 = get_sim_mat2(sim_mat)

idx = argsort_sim_mat(sim_mat)
idx2 = argsort_sim_mat(sim_mat2)
sim_mat_sorted = sim_mat[idx, :][:, idx]
sim_mat_sorted2 = sim_mat[idx2, :][:, idx2]

# Plot results
fig, ax = plt.subplots(1, 3)
ax[0].imshow(sim_mat)
ax[1].imshow(sim_mat_sorted)
ax[2].imshow(sim_mat_sorted2)

similarity**2 sorted

The results with this second method are quite good (see third image) but I guess there exist cases where this approach also fails, so I would be happy about feedback.


Edit

I tried to explain it and did also link the ideas to the code with [a] and [b], but obviously I did not do a good job, so here is a second more verbose explanation.

You have n elements and a n x n similarity matrix sm where each cell (i, j) describes how similar element i is to element j. The goal is to order the rows / columns in such a way that one can see existing patterns in the similarity matrix. My idea to achieve this is really simple.

You start with an empty list and add elements one by one. The criterion for the next element is the similarity to the current element. If element i was added in the last step, I chose the element argmax(sm[i, :]) as next, ignoring the elements already added to the list. I ignore the elements by setting the values of those elements to -1.

You can use the function ticks to reorder the labels:

labels = np.array(labels)  # make labels an numpy array, to index it with a list
ticks(_ax=ax[0], ti=range(n), la=labels[idx])

@scleronomic's solution is very elegant, but it also has one shortage, which is we cannot set the number of clusters in the sorted correlation matrix. Assume we are working with a set of variables, in which some of them are weakly correlated

import string 
import numpy as np
import pandas as pd

n_variables = 20
n_clusters = 10
n_samples = 100

np.random.seed(100)
names = list(string.ascii_lowercase)[:n_variables]
belongs_to_cluster = np.random.randint(0,n_clusters,n_variables)

latent = np.random.randn(n_clusters,n_samples)
variables = np.random.rand(n_variables,n_samples)
for ind in range(n_clusters):
    mask = belongs_to_cluster == ind
    # weakening the correlation 
    if ind % 2 == 0:variables[mask] += latent[ind]*0.1
    variables[mask] += latent[ind]

df = pd.DataFrame({key:val for key,val in zip(names,variables)})
corr_mat = np.array(df.corr())

As you can see, there are 10 clusters of variables by construction, however, variables within clusters that has an even index are weakly correlated. If we only want to see roughly 5 clusters in the sorted correlation matrix, maybe we need to find another way.

Based on this post, which is the accepted answer to the question "Clustering a correlation matrix", to sort a correlation matrix into blocks, what we need to find are blocks, where correlations within blocks are high and correlations between blocks are low. However, the solution provided by this accepted answer works best when we know how many blocks are there in the first place, and more importantly, the sizes of the underlying blocks are the same, or at least similar. Therefore, I improved the solution with a new function sort_corr_mat

def sort_corr_mat(corr_mat,clusters_guess): 
    
    def _swap_rows(corr_mat, var1, var2):
        rs = corr_mat.copy()
        rs[var2, :],rs[var1, :]= corr_mat[var1, :],corr_mat[var2, :]
        cs = rs.copy()
        cs[:, var2],cs[:, var1] = rs[:, var1],rs[:, var2]
        return cs

    # analysis
    max_iter = 500
    best_score,current_score,best_count = -1e8,-1e8,0
    num_minimua_to_visit = 20
    
    best_corr = corr_mat
    best_ordering = np.arange(n_variables)
    for i in range(max_iter):
        for row1 in range(n_variables):
            for row2 in range(n_variables):
                if row1 == row2: continue
                option_ordering = best_ordering.copy()
                option_ordering[row1],option_ordering[row2] = best_ordering[row2],best_ordering[row1]
                option_corr = _swap_rows(best_corr,row1,row2)
                option_score = score(option_corr,n_variables,clusters_guess)

                if option_score > best_score:
                    best_corr = option_corr
                    best_ordering = option_ordering
                    best_score = option_score

        if best_score > current_score:
            best_count += 1
            current_corr = best_corr
            current_ordering = best_ordering
            current_score = best_score
            
        if best_count >= num_minimua_to_visit:
            return best_corr#,best_ordering
        
    return best_corr#,best_ordering

With this function and the corr_mat constructed in the first place, I compared the result obtained with my function (on the right) with that obtained with @scleronomic's solution (in the middle)

sim_mat_sorted = corr_mat[argsort_sim_mat(corr_mat), :][:, argsort_sim_mat(corr_mat)]
corr_mat_sorted = sort_corr_mat(corr_mat,clusters_guess=5)

# Plot results
fig, ax = plt.subplots(1,3,figsize=(18,6))
ax[0].imshow(corr_mat)
ax[1].imshow(sim_mat_sorted)
ax[2].imshow(corr_mat_sorted)

results

Clearly, @scleronomic's solution works much better and faster, but my solution offers more control to the pattern of the output.

Related