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

Last change on this file since 602 was 602, checked in by Maciej Komosinski, 8 years ago
• More flexible reading of distance matrices from files
• Can also read labels for individual points and display them
File size: 6.9 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):
66        stdev = arr.max() / 100.
67        return arr + np.random.randn(len(arr)) * stdev * 2
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                minsize = min(distances.shape[0],distances.shape[1])
75                distances = np.array([row[:minsize] for row in distances]) #this can only fix matrices with more columns than rows
76                print("Making it square:",distances.shape)
77
78        try: #maybe the file has more columns than rows, and the extra column has labels?
79                labels = np.genfromtxt(fname, delimiter=separator, usecols=distances.shape[0],dtype=[('label','S10')])
80                labels = [label[0].decode("utf-8") for label in labels]
81        except ValueError:
82                labels = None #no labels
83
84        return distances,labels
85
86
87def compute_mds(distance_matrix, dim):
88        embed, evals = cmdscale(distance_matrix)
89
90        variances = [np.var(embed[:,i]) for i in range(len(embed[0]))]
91        percent_variances = [sum(variances[:i+1])/sum(variances) for i in range(len(variances))]
92        for i,pv in enumerate(percent_variances):
93                print(i+1,"dimension:",pv)
94
95        dim = min(dim, len(embed[0]))
96        embed = np.asarray([embed[:,i] for i in range(dim)]).T
97
98        return embed
99
100
101def plot(coordinates, labels, dimensions, jitter=0, outname=""):
102        fig = plt.figure()
103
104        if dimensions < 3:
105                ax = fig.add_subplot(111)
106        else:
107                ax = fig.add_subplot(111, projection='3d')
108
109        add_jitter = lambda tab : rand_jitter(tab) if jitter==1 else tab
110
111        x_dim = len(coordinates[0])
112        y_dim = len(coordinates)
113
114        points = [add_jitter(coordinates[:, i]) for i in range(x_dim)]
115
116        if labels is not None and dimensions==2:
117                ax.scatter(*points, alpha=0.1) #barely visible points, because we will show labels anyway
118                labelconvert={'vel':'V','vpp':'P','vpa':'A'} #use this if you want to replace long names with short IDs
119                #for point in points:
120                #       print(point)
121                for label, x, y in zip(labels, points[0], points[1]):
122                        #if label not in knownlabels:
123                        #       knownlabels.append(label)
124                        #       colors.append('#ff0000')
125                        for key in labelconvert:
126                                if label.startswith(key):
127                                        label=labelconvert[key]
128                        plt.annotate(
129                                label,
130                                xy = (x, y), xytext = (0, 0),
131                                textcoords = 'offset points', ha = 'center', va = 'center',
132                                #bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
133                                #arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0')
134                                )
135        else:
136                ax.scatter(*points, alpha=0.5)
137
138
139        plt.title('Phenotypes distances')
140        plt.tight_layout()
141        plt.axis('tight')
142
143        if outname == "":
144                plt.show()
145
146        else:
147                plt.savefig(outname+".pdf")
148                np.savetxt(outname+".csv", coordinates, delimiter=";")
149
150
151def main(filename, dimensions=3, outname="", jitter=0, separator='\t'):
152        dimensions = int(dimensions)
153        distances,labels = read_file(filename, separator)
154        embed = compute_mds(distances, dimensions)
155
156        if dimensions == 1:
157                embed = np.array([np.insert(e, 0, 0, axis=0) for e in embed])
158
159        plot(embed, labels, dimensions, jitter, outname)
160
161
162if __name__ == '__main__':
163        parser = argparse.ArgumentParser()
164        parser.add_argument('--in', dest='input', required=True, help='input file with dissimilarity matrix')
165        parser.add_argument('--out', dest='output', required=False, help='output file name without extension')
166        parser.add_argument('--dim', required=False, help='number of dimensions of the new space')
167        parser.add_argument('--sep', required=False, help='separator of the source file')
168        parser.add_argument('--j', required=False, help='for j=1 random jitter is added to the plot')
169
170        args = parser.parse_args()
171        set_value = lambda value, default : default if value == None else value
172        main(args.input, set_value(args.dim, 3), set_value(args.output, ""), set_value(args.j, 0), set_value(args.sep, "\t"))
Note: See TracBrowser for help on using the repository browser.