source: mds-and-trees/mds_plot.py @ 609

Last change on this file since 609 was 609, checked in by Maciej Komosinski, 8 years ago

Labels can have colors dependent on label text

File size: 7.4 KB
Line 
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4import sys
5import numpy as np
6#from sklearn import manifold #was needed for manifold MDS http://scikit-learn.org/stable/auto_examples/manifold/plot_compare_methods.html
7
8#to make it work in console, http://stackoverflow.com/questions/2801882/generating-a-png-with-matplotlib-when-display-is-undefined
9#import matplotlib
10#matplotlib.use('Agg')
11
12import matplotlib.pyplot as plt
13from mpl_toolkits.mplot3d import Axes3D
14from matplotlib import cm
15import argparse
16
17
18#http://www.nervouscomputer.com/hfs/cmdscale-in-python/
19def cmdscale(D):
20    """                                                                                       
21    Classical multidimensional scaling (MDS)                                                 
22                                                                                               
23    Parameters                                                                               
24    ----------                                                                               
25    D : (n, n) array                                                                         
26        Symmetric distance matrix.                                                           
27                                                                                               
28    Returns                                                                                   
29    -------                                                                                   
30    Y : (n, p) array                                                                         
31        Configuration matrix. Each column represents a dimension. Only the                   
32        p dimensions corresponding to positive eigenvalues of B are returned.                 
33        Note that each dimension is only determined up to an overall sign,                   
34        corresponding to a reflection.                                                       
35                                                                                               
36    e : (n,) array                                                                           
37        Eigenvalues of B.                                                                     
38                                                                                               
39    """
40    # Number of points                                                                       
41    n = len(D)
42 
43    # Centering matrix                                                                       
44    H = np.eye(n) - np.ones((n, n))/n
45 
46    # YY^T                                                                                   
47    B = -H.dot(D**2).dot(H)/2
48 
49    # Diagonalize                                                                             
50    evals, evecs = np.linalg.eigh(B)
51 
52    # Sort by eigenvalue in descending order                                                 
53    idx   = np.argsort(evals)[::-1]
54    evals = evals[idx]
55    evecs = evecs[:,idx]
56 
57    # Compute the coordinates using positive-eigenvalued components only                     
58    w, = np.where(evals > 0)
59    L  = np.diag(np.sqrt(evals[w]))
60    V  = evecs[:,w]
61    Y  = V.dot(L)
62 
63    return Y, evals
64
65def rand_jitter(arr, jitter):
66        stdev = (arr.max()-arr.min()) / 100. * jitter #dispersion proportional to range
67        return arr + np.random.randn(len(arr)) * stdev
68
69
70def read_file(fname, separator):
71        distances = np.genfromtxt(fname, delimiter=separator)
72        if (distances.shape[0]!=distances.shape[1]):
73                print("Matrix is not square:",distances.shape)
74        if (distances.shape[0]>distances.shape[1]):
75                raise ValueError('More rows than columns?')
76        if (distances.shape[0]<distances.shape[1]):
77                minsize = min(distances.shape[0],distances.shape[1])
78                firstsquarecolumn=distances.shape[1]-minsize
79                distances = np.array([row[firstsquarecolumn:] for row in distances]) #this can only fix matrices with more columns than rows
80                print("Made the matrix square:",distances.shape)
81
82                #if the file has more columns than rows, assume the first extra column on the left of the square matrix has labels
83                labels = np.genfromtxt(fname, delimiter=separator, usecols=firstsquarecolumn-1,dtype=[('label','S10')])
84                labels = [label[0].decode("utf-8") for label in labels]
85        else:
86                labels = None #no labels
87       
88        return distances,labels
89
90
91def compute_mds(distance_matrix, dim):
92        embed, evals = cmdscale(distance_matrix)
93
94        variances = [np.var(embed[:,i]) for i in range(len(embed[0]))]
95        variances_fraction = [sum(variances[:i+1])/sum(variances) for i in range(len(variances))]
96        for i,pv in enumerate(variances_fraction):
97                print("In",i+1,"dimensions:",pv)
98
99        dim = min(dim, len(embed[0]))
100        embed = np.asarray([embed[:,i] for i in range(dim)]).T
101
102        return embed, variances_fraction[dim-1]
103
104
105def plot(coordinates, labels, dimensions, variance_fraction, jitter=0, outname=""):
106        fig = plt.figure()
107
108        if dimensions < 3:
109                ax = fig.add_subplot(111)
110        else:
111                ax = fig.add_subplot(111, projection='3d')
112
113        add_jitter = lambda tab : rand_jitter(tab, jitter) if jitter>0 else tab
114
115        x_dim = len(coordinates[0])
116        y_dim = len(coordinates)
117
118        points = [add_jitter(coordinates[:, i]) for i in range(x_dim)]
119       
120        if labels is not None and dimensions==2: #could be ported to 3D too
121                ax.scatter(*points, alpha=0) #invisible points, because we will show labels instead
122                labelconvert={'velland':'V','velwat':'W','vpp':'P','vpa':'A'} #use this if you want to replace long names with short IDs
123                colors={'velland':'green','velwat':'blue','vpp':'red','vpa':'violet'}
124                #for point in points:
125                #       print(point)
126                for label, x, y in zip(labels, points[0], points[1]):
127                        color='black'
128                        for key in labelconvert:
129                                if label.startswith(key):
130                                        label=labelconvert[key]
131                                        color=colors[key]
132                        plt.annotate(
133                                label,
134                                xy = (x, y), xytext = (0, 0),
135                                textcoords = 'offset points', ha = 'center', va = 'center',
136                                color = color,
137                                alpha = 0.8,
138                                #bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
139                                #arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0')
140                                )
141        else:
142                ax.scatter(*points, alpha=0.5)
143
144
145        plt.title('Projection of phenotype distances, variance preserved = %.1f%%' % (variance_fraction*100))
146        plt.tight_layout()
147        plt.axis('tight')
148
149        if outname == "":
150                plt.show()
151        else:
152                plt.savefig(outname+".pdf")
153                np.savetxt(outname+".csv", coordinates, delimiter=";")
154
155
156def main(filename, dimensions=3, outname="", jitter=0, separator='\t'):
157        distances,labels = read_file(filename, separator)
158        embed,variance_fraction = compute_mds(distances, dimensions)
159
160        if dimensions == 1:
161                embed = np.array([np.insert(e, 0, 0, axis=0) for e in embed])
162       
163        plot(embed, labels, dimensions, variance_fraction, jitter, outname)
164
165
166if __name__ == '__main__':
167        parser = argparse.ArgumentParser()
168        parser.add_argument('--in', dest='input', required=True, help='input file with dissimilarity matrix')
169        parser.add_argument('--out', dest='output', required=False, help='output file name (without extension)')
170        parser.add_argument('--dim', required=False, help='number of dimensions of the new space')
171        parser.add_argument('--sep', required=False, help='separator of the source file')
172        parser.add_argument('--j', required=False, help='for j>0, random jitter is added to points in the plot')
173
174        args = parser.parse_args()
175        set_value = lambda value, default : default if value == None else value
176        main(args.input, int(set_value(args.dim, 3)), set_value(args.output, ""), float(set_value(args.j, 0)), set_value(args.sep, "\t"))
Note: See TracBrowser for help on using the repository browser.