Skip to content

Commit e8ce2cb

Browse files
committed
Add PCA to statistics
Signed-off-by: Kyle Corry <kylecorry31@gmail.com>
1 parent 3586bc9 commit e8ce2cb

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.kylecorry.sol.math.statistics
2+
3+
import com.kylecorry.sol.math.algebra.Matrix
4+
import com.kylecorry.sol.math.algebra.LinearAlgebra
5+
import com.kylecorry.sol.math.algebra.dot
6+
import com.kylecorry.sol.math.algebra.subtract
7+
import com.kylecorry.sol.math.algebra.transpose
8+
9+
internal object PrincipalComponentAnalysis {
10+
11+
fun getComponents(x: Matrix, k: Int): Matrix {
12+
require(k >= 0) { "k must be non-negative" }
13+
require(k <= x.columns()) { "k must be less than or equal to the number of features" }
14+
15+
if (k == 0 || x.rows() == 0) {
16+
return Matrix.zeros(x.rows(), k)
17+
}
18+
19+
val featureMean = Matrix.create(1, x.columns()) { _, column ->
20+
x.getColumn(column).average().toFloat()
21+
}
22+
val xTilde = x.subtract(featureMean)
23+
val x2 = xTilde.transpose().dot(xTilde)
24+
val eigen = LinearAlgebra.eigen(x2, TOLERANCE, MAX_ITERATIONS)
25+
val sortedIndices = (0..<eigen.values.size).sortedByDescending { eigen.values[it] }
26+
val components = Matrix.create(eigen.vectors.rows(), k) { row, column ->
27+
eigen.vectors[row, sortedIndices[column]]
28+
}
29+
return xTilde.dot(components)
30+
}
31+
32+
private const val TOLERANCE = 1e-6f
33+
private const val MAX_ITERATIONS = 100
34+
35+
}

src/main/kotlin/com/kylecorry/sol/math/statistics/Statistics.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,14 @@ object Statistics {
433433

434434
return quantile(values, 0.75f, interpolate) - quantile(values, 0.25f, interpolate)
435435
}
436+
437+
/**
438+
* Computes the PCA of the input matrix.
439+
* @param x the input matrix where rows are samples and columns are features
440+
* @param k the number of components to extract
441+
* @return a matrix where rows are samples and columns are principal component projections (centered)
442+
*/
443+
fun principalComponentAnalysis(x: Matrix, k: Int): Matrix {
444+
return PrincipalComponentAnalysis.getComponents(x, k)
445+
}
436446
}

src/test/kotlin/com/kylecorry/sol/math/statistics/StatisticsTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,50 @@ internal class StatisticsTest {
137137
assertEquals(expected, actual, 0.00001f)
138138
}
139139

140+
@Test
141+
fun principalComponentAnalysis() {
142+
val data = Matrix.create(
143+
arrayOf(
144+
floatArrayOf(2f, 1f, 5f),
145+
floatArrayOf(4f, 3f, 4f),
146+
floatArrayOf(6f, 2f, 3f),
147+
floatArrayOf(8f, 5f, 2f),
148+
floatArrayOf(10f, 7f, 1f)
149+
)
150+
)
151+
152+
val actual = Statistics.principalComponentAnalysis(data, 2)
153+
154+
assertEquals(5, actual.rows())
155+
assertEquals(2, actual.columns())
156+
assertProjectedEquals(
157+
Matrix.create(
158+
arrayOf(
159+
floatArrayOf(-5.164735f, 0.292427f),
160+
floatArrayOf(-2.196894f, 0.730517f),
161+
floatArrayOf(-0.881081f, -1.335551f),
162+
floatArrayOf(2.637435f, -0.062742f),
163+
floatArrayOf(5.605276f, 0.375348f)
164+
)
165+
),
166+
actual
167+
)
168+
}
169+
170+
private fun assertProjectedEquals(
171+
expected: Matrix,
172+
actual: Matrix,
173+
tolerance: Float = 0.0001f
174+
) {
175+
assertEquals(expected.rows(), actual.rows())
176+
assertEquals(expected.columns(), actual.columns())
177+
for (row in 0..<expected.rows()) {
178+
for (col in 0..<expected.columns()) {
179+
assertEquals(expected[row, col], actual[row, col], tolerance)
180+
}
181+
}
182+
}
183+
140184
companion object {
141185

142186
@JvmStatic

0 commit comments

Comments
 (0)