solow_decomposition/
incremental_pca.rs1use ndarray::{Array1, Array2, ArrayView2};
6use solow_core::{Error, Result};
7
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[derive(Clone, Debug, PartialEq)]
11pub struct IncrementalPCA {
12 pub mean: Array1<f64>,
14 pub components: Array2<f64>,
16 pub explained_variance: Array1<f64>,
18 pub n_samples_seen: usize,
20 pub n_components: usize,
22}
23
24impl IncrementalPCA {
25 pub fn fit(x: ArrayView2<'_, f64>, n_components: usize) -> Result<Self> {
27 let n = x.nrows();
28 let d = x.ncols();
29 if n_components == 0 || n_components > n.min(d) {
30 return Err(Error::Value(format!(
31 "IncrementalPCA: n_components must be in [1, {}] (got {n_components})",
32 n.min(d)
33 )));
34 }
35 let mut mean = Array1::<f64>::zeros(d);
37 for j in 0..d {
38 let mut s = 0.0_f64;
39 for i in 0..n {
40 s += x[[i, j]];
41 }
42 mean[j] = s / n as f64;
43 }
44 let mut centred = Array2::<f64>::zeros((n, d));
45 for i in 0..n {
46 for j in 0..d {
47 centred[[i, j]] = x[[i, j]] - mean[j];
48 }
49 }
50 let mut cov = Array2::<f64>::zeros((d, d));
52 for i in 0..n {
53 for j in 0..d {
54 for k in 0..d {
55 cov[[j, k]] += centred[[i, j]] * centred[[i, k]];
56 }
57 }
58 }
59 let denom = (n as f64 - 1.0).max(1.0);
60 for j in 0..d {
61 for k in 0..d {
62 cov[[j, k]] /= denom;
63 }
64 }
65 let (eig, vecs) = jacobi_symmetric(&cov, 400, 1e-12);
66 let mut idx: Vec<usize> = (0..d).collect();
67 idx.sort_by(|&a, &b| eig[b].partial_cmp(&eig[a]).unwrap());
68 let mut comps = Array2::<f64>::zeros((n_components, d));
69 let mut expl = Array1::<f64>::zeros(n_components);
70 for (k, &orig) in idx.iter().take(n_components).enumerate() {
71 for j in 0..d {
72 comps[[k, j]] = vecs[[j, orig]];
73 }
74 expl[k] = eig[orig];
75 }
76 Ok(Self {
77 mean,
78 components: comps,
79 explained_variance: expl,
80 n_samples_seen: n,
81 n_components,
82 })
83 }
84
85 pub fn transform(&self, x: ArrayView2<'_, f64>) -> Result<Array2<f64>> {
87 let n = x.nrows();
88 let d = self.mean.len();
89 let k = self.n_components;
90 if x.ncols() != d {
91 return Err(Error::Shape("IncrementalPCA::transform: shape mismatch".into()));
92 }
93 let mut out = Array2::<f64>::zeros((n, k));
94 for i in 0..n {
95 for j in 0..k {
96 let mut s = 0.0_f64;
97 for r in 0..d {
98 s += (x[[i, r]] - self.mean[r]) * self.components[[j, r]];
99 }
100 out[[i, j]] = s;
101 }
102 }
103 Ok(out)
104 }
105}
106
107fn jacobi_symmetric(a: &Array2<f64>, max_sweeps: usize, tol: f64) -> (Vec<f64>, Array2<f64>) {
108 let n = a.nrows();
109 let mut m = a.clone();
110 let mut v = Array2::<f64>::eye(n);
111 for _ in 0..max_sweeps {
112 let mut off = 0.0_f64;
113 for p in 0..(n - 1) {
114 for q in (p + 1)..n {
115 off += m[[p, q]] * m[[p, q]];
116 }
117 }
118 if off.sqrt() < tol {
119 break;
120 }
121 for p in 0..(n - 1) {
122 for q in (p + 1)..n {
123 let apq = m[[p, q]];
124 if apq.abs() < 1e-30 {
125 continue;
126 }
127 let theta = (m[[q, q]] - m[[p, p]]) / (2.0 * apq);
128 let t = theta.signum() / (theta.abs() + (1.0 + theta * theta).sqrt());
129 let c = 1.0 / (1.0 + t * t).sqrt();
130 let s = t * c;
131 for i in 0..n {
132 let mip = m[[i, p]];
133 let miq = m[[i, q]];
134 m[[i, p]] = c * mip - s * miq;
135 m[[i, q]] = s * mip + c * miq;
136 }
137 for j in 0..n {
138 let mpj = m[[p, j]];
139 let mqj = m[[q, j]];
140 m[[p, j]] = c * mpj - s * mqj;
141 m[[q, j]] = s * mpj + c * mqj;
142 }
143 for i in 0..n {
144 let vip = v[[i, p]];
145 let viq = v[[i, q]];
146 v[[i, p]] = c * vip - s * viq;
147 v[[i, q]] = s * vip + c * viq;
148 }
149 }
150 }
151 }
152 let mut eig = vec![0.0_f64; n];
153 for i in 0..n {
154 eig[i] = m[[i, i]];
155 }
156 (eig, v)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use ndarray::array;
163
164 #[test]
165 fn ipca_returns_components_of_the_right_shape() {
166 let x = array![
167 [1.0_f64, 2.0, 3.0], [3.0, 5.0, 8.0], [5.0, 7.0, 11.0],
168 [7.0, 9.0, 15.0], [9.0, 12.0, 20.0]
169 ];
170 let m = IncrementalPCA::fit(x.view(), 2).unwrap();
171 assert_eq!(m.components.shape(), &[2, 3]);
172 assert!(m.explained_variance[0] >= m.explained_variance[1]);
173 }
174}