1use scirs2_core::ndarray::{Array1, Array2, Axis};
7use scirs2_linalg::svd;
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10use sklears_core::{
11 error::{Result, SklearsError},
12 traits::{Estimator, Fit, Transform, Untrained},
13 types::Float,
14};
15
16#[derive(Debug, Clone)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub struct PcaConfig {
20 pub n_components: Option<usize>,
21 pub whiten: bool,
22 pub svd_solver: String,
23 pub tol: Float,
24 pub iterated_power: usize,
25 pub random_state: Option<u64>,
26}
27
28impl Default for PcaConfig {
29 fn default() -> Self {
30 Self {
31 n_components: None,
32 whiten: false,
33 svd_solver: "auto".to_string(),
34 tol: 0.0,
35 iterated_power: 5,
36 random_state: None,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
44pub struct PCA<State = Untrained> {
45 config: PcaConfig,
46 state: std::marker::PhantomData<State>,
47}
48
49#[derive(Debug, Clone)]
51#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
52pub struct PcaTrained {
53 config: PcaConfig,
54 pub components: Array2<Float>,
56 pub explained_variance: Array1<Float>,
58 pub explained_variance_ratio: Array1<Float>,
60 pub mean: Array1<Float>,
62 pub n_components: usize,
64}
65
66impl PCA<Untrained> {
67 pub fn new(config: PcaConfig) -> Self {
68 Self {
69 config,
70 state: std::marker::PhantomData,
71 }
72 }
73
74 pub fn builder() -> PcaBuilder {
75 PcaBuilder::default()
76 }
77}
78
79impl Estimator for PCA<Untrained> {
80 type Config = PcaConfig;
81 type Error = SklearsError;
82 type Float = Float;
83
84 fn config(&self) -> &Self::Config {
85 &self.config
86 }
87}
88
89impl Fit<Array2<Float>, ()> for PCA<Untrained> {
90 type Fitted = PcaTrained;
91
92 fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
93 let (n_samples, n_features) = x.dim();
94
95 if n_samples == 0 || n_features == 0 {
96 return Err(SklearsError::InvalidInput(
97 "Input array must be non-empty".to_string(),
98 ));
99 }
100
101 let n_components = self
103 .config
104 .n_components
105 .unwrap_or(n_features.min(n_samples));
106
107 if n_components > n_features.min(n_samples) {
108 return Err(SklearsError::InvalidInput(
109 "n_components cannot be larger than min(n_samples, n_features)".to_string(),
110 ));
111 }
112
113 let mean = x
115 .mean_axis(Axis(0))
116 .expect("array should have elements for mean computation");
117
118 let x_centered = x - &mean.clone().insert_axis(Axis(0));
120
121 let (_u, singular_values, vt) = svd(&x_centered.view(), false, None)
125 .map_err(|e| SklearsError::NumericalError(format!("SVD failed in PCA.fit: {}", e)))?;
126
127 let components = vt
129 .slice(scirs2_core::ndarray::s![..n_components, ..])
130 .to_owned();
131
132 let denom = if n_samples > 1 {
134 (n_samples - 1) as Float
135 } else {
136 1.0
137 };
138 let explained_variance: Array1<Float> = singular_values
139 .iter()
140 .take(n_components)
141 .map(|&s| s * s / denom)
142 .collect();
143
144 let total_variance: Float = singular_values.iter().map(|&s| s * s / denom).sum();
146
147 let explained_variance_ratio = if total_variance > Float::EPSILON {
148 explained_variance.mapv(|v| v / total_variance)
149 } else {
150 Array1::zeros(n_components)
151 };
152
153 Ok(PcaTrained {
154 config: self.config,
155 components,
156 explained_variance,
157 explained_variance_ratio,
158 mean,
159 n_components,
160 })
161 }
162}
163
164impl Transform<Array2<Float>, Array2<Float>> for PcaTrained {
165 fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
166 let x_centered = x - &self.mean.clone().insert_axis(Axis(0));
168
169 let x_transformed = x_centered.dot(&self.components.t());
171
172 Ok(x_transformed)
173 }
174}
175
176#[derive(Debug, Clone, Default)]
178pub struct PcaBuilder {
179 config: PcaConfig,
180}
181
182impl PcaBuilder {
183 pub fn n_components(mut self, n_components: Option<usize>) -> Self {
184 self.config.n_components = n_components;
185 self
186 }
187
188 pub fn whiten(mut self, whiten: bool) -> Self {
189 self.config.whiten = whiten;
190 self
191 }
192
193 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
194 self.config.random_state = random_state;
195 self
196 }
197
198 pub fn build(self) -> PCA<Untrained> {
199 PCA::new(self.config)
200 }
201}
202
203pub type PcaProcessor = PCA<Untrained>;
205pub type PcaValidator = PCA<Untrained>;
206pub type PcaEstimator = PCA<Untrained>;
207pub type PcaTransformer = PcaTrained;
208pub type PcaAnalyzer = PcaTrained;
209pub type DimensionalityReducer = PcaTrained;
210pub type DecompositionEngine = PcaTrained;
211pub type StandardPcaAnalyzer = PcaTrained;
212pub type PrincipalComponentAnalysis = PcaTrained;
213pub type VarianceExplained = Array1<Float>;
214pub type ComponentExtractor = PcaTrained;
215pub type DimensionalityReduction = PcaTrained;
216pub type PcaProjection = Array2<Float>;
217pub type PcaInverseTransform = Array2<Float>;
218
219#[derive(Debug, Clone)]
221#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
222pub struct RobustPCA;
223
224#[derive(Debug, Clone)]
225#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
226pub struct SparsePCA;
227
228#[derive(Debug, Clone)]
229#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
230pub struct ProbabilisticPCA;
231
232#[derive(Debug, Clone)]
234#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
235pub struct SvdSolver;
236
237#[derive(Debug, Clone)]
238#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
239pub struct SvdSolverConfig;
240
241pub type RobustPcaConfig = PcaConfig;
243pub type SparsePcaConfig = PcaConfig;
244pub type ProbabilisticPcaConfig = PcaConfig;
245pub type IncrementalPcaConfig = PcaConfig;
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use scirs2_core::ndarray::{array, Array2};
251
252 fn two_feature_data() -> Array2<Float> {
254 array![
255 [1.0, 0.5],
256 [2.0, 1.0],
257 [3.0, 1.5],
258 [4.0, 2.0],
259 [5.0, 2.5],
260 [6.0, 3.0],
261 ]
262 }
263
264 #[test]
265 fn test_pca_output_shape() {
266 let x = two_feature_data();
267 let config = PcaConfig {
268 n_components: Some(1),
269 ..Default::default()
270 };
271 let trained = PCA::new(config).fit(&x, &()).expect("fit should succeed");
272 let transformed = trained.transform(&x).expect("transform should succeed");
273 assert_eq!(transformed.shape(), &[6, 1]);
274 }
275
276 #[test]
277 fn test_pca_components_shape() {
278 let x = two_feature_data();
279 let config = PcaConfig {
280 n_components: Some(2),
281 ..Default::default()
282 };
283 let trained = PCA::new(config).fit(&x, &()).expect("fit should succeed");
284 assert_eq!(trained.components.shape(), &[2, 2]);
285 }
286
287 #[test]
288 fn test_pca_explained_variance_ratio_sums_to_one() {
289 let x = two_feature_data();
290 let config = PcaConfig {
291 n_components: Some(2),
292 ..Default::default()
293 };
294 let trained = PCA::new(config).fit(&x, &()).expect("fit should succeed");
295 let ratio_sum: Float = trained.explained_variance_ratio.sum();
296 assert!((ratio_sum - 1.0).abs() < 1e-10, "ratio sum = {}", ratio_sum);
297 }
298
299 #[test]
300 fn test_pca_explained_variance_ratio_non_negative() {
301 let x = two_feature_data();
302 let config = PcaConfig {
303 n_components: Some(2),
304 ..Default::default()
305 };
306 let trained = PCA::new(config).fit(&x, &()).expect("fit should succeed");
307 for &r in trained.explained_variance_ratio.iter() {
308 assert!(r >= 0.0, "negative ratio {}", r);
309 }
310 }
311
312 #[test]
313 fn test_pca_explained_variance_descending() {
314 let x = two_feature_data();
316 let config = PcaConfig {
317 n_components: Some(2),
318 ..Default::default()
319 };
320 let trained = PCA::new(config).fit(&x, &()).expect("fit should succeed");
321 assert!(
322 trained.explained_variance[0] >= trained.explained_variance[1],
323 "PC1 variance {} < PC2 variance {}",
324 trained.explained_variance[0],
325 trained.explained_variance[1]
326 );
327 }
328
329 #[test]
330 fn test_pca_transform_finiteness() {
331 let x = two_feature_data();
332 let config = PcaConfig {
333 n_components: Some(1),
334 ..Default::default()
335 };
336 let trained = PCA::new(config).fit(&x, &()).expect("fit should succeed");
337 let t = trained.transform(&x).expect("transform should succeed");
338 for &v in t.iter() {
339 assert!(v.is_finite(), "non-finite value {}", v);
340 }
341 }
342
343 #[test]
344 fn test_pca_n_components_validation() {
345 let x = two_feature_data();
346 let config = PcaConfig {
348 n_components: Some(100),
349 ..Default::default()
350 };
351 assert!(PCA::new(config).fit(&x, &()).is_err());
352 }
353
354 #[test]
355 fn test_pca_empty_input() {
356 let x: Array2<Float> = Array2::zeros((0, 3));
357 let config = PcaConfig::default();
358 assert!(PCA::new(config).fit(&x, &()).is_err());
359 }
360
361 #[test]
362 fn test_pca_first_component_captures_most_variance() {
363 let x = two_feature_data();
365 let config = PcaConfig {
366 n_components: Some(1),
367 ..Default::default()
368 };
369 let trained = PCA::new(config).fit(&x, &()).expect("fit should succeed");
370 let ratio = trained.explained_variance_ratio[0];
372 assert!(ratio > 0.99, "1-PC ratio = {}", ratio);
373 }
374}