rmds 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ #
2
+ # RMDS - Ruby Multidimensional Scaling Library
3
+ # Copyright (c) Christoph Heindl, 2010
4
+ # http://github.com/cheind/rmds
5
+ #
6
+
7
+ module MDS
8
+
9
+ #
10
+ # In metric MDS dissimilarities are interpreted as distances und
11
+ # the goal is to find an embedding in an Euclidean space that best preserves
12
+ # the given distances.
13
+ #
14
+ # This implementation gives an analytical solution and avoids iterative
15
+ # optimization. The performance of the algorithm highly depends on the
16
+ # implementation of the eigen-decomposition provided via {MDS::MatrixInterface}.
17
+ #
18
+ # *Examples*
19
+ # - {Examples.minimal_metric}
20
+ # - {Examples.extended_metric}
21
+ #
22
+ class Metric
23
+
24
+ #
25
+ # Find a Cartesian embedding for the given distances.
26
+ #
27
+ # Instead of a fixed dimensionality for the resulting embedding, this
28
+ # method determines the dimensionality based on the variances of distances
29
+ # in its input matrix and the parameter passed. The parameter specifies
30
+ # the percent of variance of distance to preserve in the Cartesian embedding.
31
+ #
32
+ # @param [MDS::Matrix] d squared Eulcidean distance matrix of observations
33
+ # @param [Float] k the percent of variance of distances
34
+ # to preserve in embedding in the range [0..1].
35
+ # @return [MDS::Matrix] the matrix containing the cartesian embedding.
36
+ #
37
+ def Metric.projectk(d, k)
38
+ b = Metric.shift(d)
39
+ eval, evec = b.ed
40
+ dims = Metric.find_dimensionality(eval, k)
41
+ Metric.project(eval, evec, dims)
42
+ end
43
+
44
+ #
45
+ # Find a Cartesian embedding for the given distances.
46
+ #
47
+ # @param [MDS::Matrix] d squared Eulcidean distance matrix of observations
48
+ # @param [Integer] dims the number of dimensions of the embedding.
49
+ # @return [MDS::Matrix] the matrix containing the cartesian embedding.
50
+ #
51
+ def Metric.projectd(d, dims)
52
+ b = Metric.shift(d)
53
+ eval, evec = b.ed
54
+ Metric.project(eval, evec, dims)
55
+ end
56
+
57
+ #
58
+ # Calculates the squared Euclidean distances for all
59
+ # pairwise observations in the given matrix. Each observation
60
+ # corresponds to a matrix row and is provided in Cartesian coordinates.
61
+ #
62
+ # The result is a real symmetric matrix of size +NxN+, where +N+ is the
63
+ # number of observations in the input. Each element +(i,j)+ in this matrix
64
+ # corresponds to the squared distance between the i-th and j-th observation
65
+ # in the input matrix.
66
+ #
67
+ # @param [MDS::Matrix] x the matrix of observations.
68
+ # @return [MDS::Matrix] the squared Euclidean distance matrix
69
+ #
70
+ def Metric.squared_distances(x)
71
+ # Product of x with transpose of x
72
+ xxt = x * x.t
73
+ # 1xN matrix of ones, where N size of xxt
74
+ ones = Matrix.create(1, xxt.nrows, 1.0)
75
+ # Nx1 matrix containing diagonal elements of x
76
+ diagonals = xxt.diagonals
77
+ c = Matrix.create_block(xxt.nrows, 1) do |i, j|
78
+ diagonals[i]
79
+ end
80
+ c * ones + (c * ones).t - xxt * 2
81
+ end
82
+
83
+
84
+ protected
85
+
86
+ #
87
+ # Transform the squared distance matrix into a matrix of Cartesian
88
+ # coordinates by projecting the distances onto the basis formed by
89
+ # the eigen-decomposition of the initial matrix.
90
+ #
91
+ # @param [MDS::Matrix] d squared Euclidean distance matrix
92
+ # @param [Float] k percent [0..1] of variances in distances to keep in embedding.
93
+ # @return [MDS::Matrix] containing the Cartesian embedding.
94
+ #
95
+ def Metric.project(eval, evec, dims)
96
+ lam = eval.minor(0..eval.nrows-1, 0..dims-1)
97
+ for i in 0..lam.ncols-1
98
+ v = lam[i,i]
99
+ if v > 0.0
100
+ lam[i,i] = Math.sqrt(v)
101
+ end
102
+ end
103
+ evec * lam
104
+ end
105
+
106
+ #
107
+ # Transforms the distance matrix into an equivalent scalar product
108
+ # matrix.
109
+ #
110
+ # @param [MDS::Matrix] d the squared Euclidean distance matrix
111
+ # @return [MDS::Matrix] the equivalent scalar product matrix.
112
+ #
113
+ def Metric.shift(d)
114
+ # Nx1 matrix of ones
115
+ ones = Matrix.create(d.nrows, 1, 1.0)
116
+ # 1xN weight vector
117
+ m = Matrix.create(1, d.nrows, 1.0/d.nrows)
118
+ # NxN centering matrix
119
+ xi = Matrix.create_identity(d.nrows) - ones * m
120
+ # NxN shifted distances matrix, B
121
+ (xi * d * xi.t) * -0.5
122
+ end
123
+
124
+
125
+ #
126
+ # Find the dimensionality of the resulting coordinate space so that
127
+ # at least +k+ percent of the variance of the distances is preserved.
128
+ #
129
+ # @param [MDS::Matrix] eval the diagonal matrix of sorted eigen-values.
130
+ # @param [Float] k percent of variance to keep.
131
+ # @return [Integer] minimum number of dimensions to use, to keep k-percent
132
+ # of variances of distances in embedding.
133
+ #
134
+ def Metric.find_dimensionality(eval, k)
135
+ sum_ev = eval.trace
136
+ n = eval.nrows
137
+
138
+ i = 0
139
+ sum = 0
140
+ while ((i < n) &&
141
+ (eval[i,i] > 0.0) &&
142
+ (sum / sum_ev) < k)
143
+ sum += eval[i,i]
144
+ i += 1
145
+ end
146
+ i
147
+ end
148
+ end
149
+
150
+ end
@@ -0,0 +1,199 @@
1
+ #
2
+ # RMDS - Ruby Multidimensional Scaling Library
3
+ # Copyright (c) Christoph Heindl, 2010
4
+ # http://github.com/cheind/rmds
5
+ #
6
+
7
+ require 'mds/test/matrix_assertions.rb'
8
+
9
+ module MDS
10
+ module Test
11
+
12
+ #
13
+ # Module containing standarized tests for matrix interfaces.
14
+ #
15
+ module BundleMatrixInterface
16
+ include MDS::Test::MatrixAssertions
17
+
18
+ #----------------------------
19
+ # Matrix creators
20
+ #----------------------------
21
+
22
+ def test_create
23
+ m = MDS::Matrix.create(2, 3, 0.0)
24
+ assert_equal(2, m.nrows)
25
+ assert_equal(3, m.ncols)
26
+
27
+ for i in 0..m.nrows-1 do
28
+ for j in 0..m.ncols-1 do
29
+ assert_instance_of(Float, m[i,j])
30
+ assert_equal(0.0, m[i,j])
31
+ end
32
+ end
33
+ end
34
+
35
+ def test_create_identity
36
+ m = MDS::Matrix.create_identity(3)
37
+ r = MDS::Matrix.create(3, 3, 0.0)
38
+ r[0,0] = 1.0; r[1,1] = 1.0; r[2,2] = 1.0;
39
+ assert_equal_matrices(m, r)
40
+ end
41
+
42
+ def test_create_diagonal
43
+ m = MDS::Matrix.create_diagonal(1.0, 2.0, 3.0)
44
+ r = MDS::Matrix.create(3, 3, 0.0)
45
+ r[0,0] = 1.0; r[1,1] = 2.0; r[2,2] = 3.0;
46
+ assert_equal_matrices(m, r)
47
+ end
48
+
49
+ def test_create_block
50
+ m = MDS::Matrix.create_block(2,2) do |i,j|
51
+ i == j ? 0.0 : 1.0
52
+ end
53
+ r = MDS::Matrix.create(3, 3, 1.0)
54
+ r[0,0] = 0.0; r[1,1] = 0.0;
55
+ assert_equal_matrices(m, r)
56
+ end
57
+
58
+ def test_create_rows
59
+ m = MDS::Matrix.create_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
60
+ r = MDS::Matrix.create_block(2,3) do |i,j|
61
+ (i*3 + j + 1).to_f
62
+ end
63
+ assert_equal_matrices(m, r)
64
+ end
65
+
66
+ #----------------------------
67
+ # Matrix element accessors
68
+ #----------------------------
69
+
70
+ def test_get_set
71
+ a = MDS::Matrix.create(2, 2, 1.0)
72
+
73
+ a[0,0] = 1.0
74
+ a[0,1] = 2.0
75
+ a[1,0] = 3.0
76
+ a[1,1] = 4.0
77
+
78
+ assert_equal(1.0, a[0,0])
79
+ assert_equal(2.0, a[0,1])
80
+ assert_equal(3.0, a[1,0])
81
+ assert_equal(4.0, a[1,1])
82
+ end
83
+
84
+ #----------------------------
85
+ # Matrix views
86
+ #----------------------------
87
+
88
+ def test_transpose
89
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
90
+ r = MDS::Matrix.create_rows([2.0, 1.0], [3.0, 2.0], [4.0, 3.0])
91
+ m = a.t
92
+ assert_delta_matrices(r, m)
93
+ end
94
+
95
+ def test_diagonals
96
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
97
+ r = MDS::Matrix.create_rows([2.0, 4.0])
98
+ m = MDS::Matrix.create_rows(a.diagonals)
99
+ assert_delta_matrices(r, m)
100
+ end
101
+
102
+ def test_minor
103
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
104
+ r = MDS::Matrix.create_rows([4.0],[3.0])
105
+ m = a.minor(0..1, 2..2)
106
+ assert_delta_matrices(r, m)
107
+ end
108
+
109
+ def test_trace
110
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
111
+ assert_equal(6.0, a.trace)
112
+ end
113
+
114
+ def test_columns
115
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
116
+ assert_equal([[2.0, 1.0], [3.0, 4.0], [4.0, 3.0]], a.columns)
117
+ end
118
+
119
+ def test_rows
120
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
121
+ assert_equal([[2.0, 3.0, 4.0], [1.0, 4.0, 3.0]], a.rows)
122
+ end
123
+
124
+ #----------------------------
125
+ # Matrix operations
126
+ #----------------------------
127
+
128
+ def test_product
129
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
130
+ b = MDS::Matrix.create_rows([3.0, 1.0], [1.0, 2.0], [3.0, -4.0])
131
+ r = MDS::Matrix.create_rows([21.0, -8.0], [14.0, -7.0])
132
+
133
+ m = a * b
134
+ assert_delta_matrices(r, m)
135
+ end
136
+
137
+ def test_product_scalar
138
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
139
+ r = MDS::Matrix.create_rows([4.0, 6.0, 8.0], [2.0, 4.0, 6.0])
140
+ m = a * 2.0
141
+ assert_delta_matrices(r, m)
142
+ end
143
+
144
+ def test_add
145
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
146
+ b = MDS::Matrix.create_rows([1.0, 2.0, 3.0], [0.0, 0.0, 2.0])
147
+ r = MDS::Matrix.create_rows([3.0, 5.0, 7.0], [1.0, 2.0, 5.0])
148
+ m = a + b
149
+ assert_delta_matrices(r, m)
150
+ end
151
+
152
+ def test_sub
153
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
154
+ b = MDS::Matrix.create_rows([1.0, 2.0, 2.0], [0.0, 0.0, 2.0])
155
+ r = MDS::Matrix.create_rows([1.0, 1.0, 2.0], [1.0, 2.0, 1.0])
156
+ m = a - b
157
+ assert_delta_matrices(r, m)
158
+ end
159
+
160
+ def test_ed
161
+ a = MDS::Matrix.create_rows(
162
+ [0.0, 10.0, 2.0],
163
+ [10.0, 0.0, 20.0],
164
+ [2.0, 20.0, 0.0]
165
+ )
166
+
167
+ eval_a, evec_a = a.ed
168
+
169
+ assert_in_delta(23.2051, eval_a[0,0], 1e-3)
170
+ assert_in_delta(-1.5954, eval_a[1,1], 1e-3)
171
+ assert_in_delta(-21.6097, eval_a[2,2], 1e-3)
172
+
173
+ r0 = MDS::Matrix.create_rows([0.3529, 0.6934, 0.6281])
174
+ r1 = MDS::Matrix.create_rows([0.8948, -0.0541, -0.4430])
175
+ r2 = MDS::Matrix.create_rows([-0.2732, 0.7184, -0.6396])
176
+
177
+ v0 = evec_a.minor(0..2, 0..0)
178
+ v1 = evec_a.minor(0..2, 1..1)
179
+ v2 = evec_a.minor(0..2, 2..2)
180
+
181
+ # Norm of vectors
182
+ assert_in_delta(1.0, (v0.t * v0)[0,0], 1e-3)
183
+ assert_in_delta(1.0, (v1.t * v1)[0,0], 1e-3)
184
+ assert_in_delta(1.0, (v2.t * v2)[0,0], 1e-3)
185
+
186
+ # Orthogonality of basis vectors
187
+ assert_in_delta(0.0, (v0.t * v1)[0,0], 1e-3)
188
+ assert_in_delta(0.0, (v0.t * v2)[0,0], 1e-3)
189
+ assert_in_delta(0.0, (v1.t * v2)[0,0], 1e-3)
190
+
191
+ # Orientation of basis vectors (up to 180° ambiguity)
192
+ assert_in_delta(1.0, (r0 * v0)[0,0].abs, 1e-3)
193
+ assert_in_delta(1.0, (r1 * v1)[0,0].abs, 1e-3)
194
+ assert_in_delta(1.0, (r2 * v2)[0,0].abs, 1e-3)
195
+ end
196
+ end
197
+
198
+ end
199
+ end
@@ -0,0 +1,82 @@
1
+ #
2
+ # RMDS - Ruby Multidimensional Scaling Library
3
+ # Copyright (c) Christoph Heindl, 2010
4
+ # http://github.com/cheind/rmds
5
+ #
6
+
7
+ require 'mds/test/matrix_assertions.rb'
8
+
9
+ module MDS
10
+ module Test
11
+
12
+ #
13
+ # Module containing standarized tests for MDS::Metric.
14
+ #
15
+ module BundleMetric
16
+ include MDS::Test::MatrixAssertions
17
+
18
+ def test_squared_distances
19
+ input = MDS::Matrix.create_rows(
20
+ [1.0, 2.0],
21
+ [4.0, 3.0],
22
+ [0.0, 1.0]
23
+ )
24
+
25
+ output = MDS::Matrix.create_rows(
26
+ [0.0, 10.0, 2.0],
27
+ [10.0, 0.0, 20.0],
28
+ [2.0, 20.0, 0.0]
29
+ )
30
+
31
+ d = MDS::Metric.squared_distances(input)
32
+ assert_equal_matrices(output, d)
33
+ end
34
+
35
+ def test_k_2d
36
+ x = MDS::Matrix.create_rows(
37
+ [1.0, 2.0],
38
+ [4.0, 3.0],
39
+ [0.0, 1.0]
40
+ )
41
+ d = MDS::Metric::squared_distances(x)
42
+ proj = MDS::Metric.projectk(d, 0.99)
43
+ dd = MDS::Metric.squared_distances(proj)
44
+ assert_delta_matrices(d, dd, 1e-5)
45
+ end
46
+
47
+ def test_d_2d
48
+ x = MDS::Matrix.create_rows(
49
+ [1.0, 2.0],
50
+ [4.0, 3.0],
51
+ [0.0, 1.0]
52
+ )
53
+ d = MDS::Metric.squared_distances(x)
54
+
55
+ proj = MDS::Metric.projectd(d, 2)
56
+ dd = MDS::Metric.squared_distances(proj)
57
+ assert_delta_matrices(d, dd, 1e-5)
58
+ end
59
+
60
+ def test_k_5d
61
+ x = MDS::Matrix.create_random(10, 5, -10, 10)
62
+ d = MDS::Metric.squared_distances(x)
63
+
64
+ proj = MDS::Metric.projectk(d, 1.0)
65
+ dd = MDS::Metric.squared_distances(proj)
66
+ assert_delta_matrices(d, dd, 1e-5)
67
+
68
+ proj = MDS::Metric.projectk(d, 0.8)
69
+ dd = MDS::Metric.squared_distances(proj)
70
+ assert_delta_matrices(d, dd, 5e2)
71
+ end
72
+
73
+ def test_d_5d
74
+ x = MDS::Matrix.create_random(10, 5, -10, 10)
75
+ d = MDS::Metric.squared_distances(x)
76
+ proj = MDS::Metric.projectd(d, 10)
77
+ dd = MDS::Metric.squared_distances(proj)
78
+ assert_delta_matrices(d, dd, 1e-5)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,54 @@
1
+ #
2
+ # RMDS - Ruby Multidimensional Scaling Library
3
+ # Copyright (c) Christoph Heindl, 2010
4
+ # http://github.com/cheind/rmds
5
+ #
6
+
7
+ module MDS
8
+ module Test
9
+
10
+ #
11
+ # Addition assertions for comparing matrices.
12
+ #
13
+ module MatrixAssertions
14
+
15
+ #
16
+ # Assert that two matrices are equal
17
+ #
18
+ # @param [MDS::Matrix] a first matrix
19
+ # @param [MDS::Matrix] b second matrix
20
+ #
21
+ def assert_equal_matrices(a, b)
22
+ assert_instance_of(a.matrix.class, b.matrix)
23
+ assert(a.nrows, b.nrows)
24
+ assert(a.ncols, b.ncols)
25
+
26
+ for i in 0..a.nrows-1 do
27
+ for j in 0..a.ncols-1 do
28
+ assert_equal(a[i,j], b[i,j])
29
+ end
30
+ end
31
+ end
32
+
33
+ #
34
+ # Assert that two matrices are equal up to delta
35
+ #
36
+ # @param [MDS::Matrix] first matrix
37
+ # @param [MDS::Matrix] second matrix
38
+ # @param [Float] delta delta
39
+ #
40
+ def assert_delta_matrices(a, b, delta = 1e-10)
41
+ assert_instance_of(a.matrix.class, b.matrix)
42
+ assert(a.nrows, b.nrows)
43
+ assert(a.ncols, b.ncols)
44
+
45
+ for i in 0..a.nrows-1 do
46
+ for j in 0..a.ncols-1 do
47
+ assert_in_delta(a[i,j], b[i,j], delta)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end