1use scirs2_core::ndarray::{Array1, Array2, Axis};
12use sklears_core::{
13 error::{Result, SklearsError},
14 types::Float,
15};
16
17#[derive(Debug, Clone)]
19pub struct ComponentVisualization {
20 pub loadings: Array2<Float>,
22 pub scores: Array2<Float>,
24 pub explained_variance: Array1<Float>,
26 pub explained_variance_ratio: Array1<Float>,
28 pub feature_names: Option<Vec<String>>,
30 pub component_names: Option<Vec<String>>,
32}
33
34impl ComponentVisualization {
35 pub fn new(
37 loadings: Array2<Float>,
38 scores: Array2<Float>,
39 explained_variance: Array1<Float>,
40 ) -> Self {
41 let total_variance = explained_variance.sum();
42 let explained_variance_ratio = if total_variance > 0.0 {
43 explained_variance.mapv(|x| x / total_variance)
44 } else {
45 Array1::zeros(explained_variance.len())
46 };
47
48 Self {
49 loadings,
50 scores,
51 explained_variance,
52 explained_variance_ratio,
53 feature_names: None,
54 component_names: None,
55 }
56 }
57
58 pub fn with_feature_names(mut self, names: Vec<String>) -> Self {
60 self.feature_names = Some(names);
61 self
62 }
63
64 pub fn with_component_names(mut self, names: Vec<String>) -> Self {
66 self.component_names = Some(names);
67 self
68 }
69
70 pub fn loading_plot_data(&self, comp1: usize, comp2: usize) -> Result<LoadingPlotData> {
72 if comp1 >= self.loadings.ncols() || comp2 >= self.loadings.ncols() {
73 return Err(SklearsError::InvalidInput(
74 "Component indices out of bounds".to_string(),
75 ));
76 }
77
78 let loadings1 = self.loadings.column(comp1).to_owned();
79 let loadings2 = self.loadings.column(comp2).to_owned();
80
81 Ok(LoadingPlotData {
82 x_loadings: loadings1,
83 y_loadings: loadings2,
84 component1: comp1,
85 component2: comp2,
86 explained_variance1: self.explained_variance_ratio[comp1],
87 explained_variance2: self.explained_variance_ratio[comp2],
88 feature_names: self.feature_names.clone(),
89 })
90 }
91
92 pub fn biplot_data(&self, comp1: usize, comp2: usize) -> Result<BiplotData> {
94 if comp1 >= self.scores.ncols() || comp2 >= self.scores.ncols() {
95 return Err(SklearsError::InvalidInput(
96 "Component indices out of bounds".to_string(),
97 ));
98 }
99
100 let scores1 = self.scores.column(comp1).to_owned();
101 let scores2 = self.scores.column(comp2).to_owned();
102
103 let loading_plot = self.loading_plot_data(comp1, comp2)?;
104
105 Ok(BiplotData {
106 x_scores: scores1,
107 y_scores: scores2,
108 loading_plot,
109 })
110 }
111
112 pub fn feature_importance(&self) -> Array2<Float> {
114 let mut importance = Array2::zeros(self.loadings.dim());
116
117 for i in 0..self.loadings.ncols() {
118 let variance_weight = self.explained_variance_ratio[i];
119 for j in 0..self.loadings.nrows() {
120 importance[[j, i]] = self.loadings[[j, i]].powi(2) * variance_weight;
121 }
122 }
123
124 importance
125 }
126
127 pub fn top_features_per_component(&self, n_features: usize) -> Vec<Vec<FeatureImportance>> {
129 let importance = self.feature_importance();
130 let mut results = Vec::new();
131
132 for comp in 0..importance.ncols() {
133 let mut feature_scores: Vec<(usize, Float)> = importance
134 .column(comp)
135 .iter()
136 .enumerate()
137 .map(|(idx, &score)| (idx, score))
138 .collect();
139
140 feature_scores
142 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
143
144 let top_features: Vec<FeatureImportance> = feature_scores
145 .into_iter()
146 .take(n_features)
147 .map(|(idx, score)| FeatureImportance {
148 feature_index: idx,
149 feature_name: self
150 .feature_names
151 .as_ref()
152 .and_then(|names| names.get(idx))
153 .cloned()
154 .unwrap_or_else(|| format!("feature_{idx}")),
155 importance_score: score,
156 loading_value: self.loadings[[idx, comp]],
157 })
158 .collect();
159
160 results.push(top_features);
161 }
162
163 results
164 }
165
166 pub fn component_contributions(&self) -> ComponentContribution {
168 let cumulative_variance: Array1<Float> = {
169 let mut cum = Array1::zeros(self.explained_variance_ratio.len());
170 let mut sum = 0.0;
171 for i in 0..self.explained_variance_ratio.len() {
172 sum += self.explained_variance_ratio[i];
173 cum[i] = sum;
174 }
175 cum
176 };
177
178 let total_variance_explained = cumulative_variance[cumulative_variance.len() - 1];
179
180 ComponentContribution {
181 individual_variance: self.explained_variance_ratio.clone(),
182 cumulative_variance,
183 total_variance_explained,
184 }
185 }
186
187 pub fn scree_plot_data(&self) -> ScreePlotData {
189 let component_numbers: Array1<Float> =
190 Array1::from_shape_fn(self.explained_variance.len(), |i| (i + 1) as Float);
191
192 ScreePlotData {
193 component_numbers,
194 eigenvalues: self.explained_variance.clone(),
195 explained_variance_ratios: self.explained_variance_ratio.clone(),
196 }
197 }
198
199 pub fn optimal_components(&self) -> OptimalComponentsAnalysis {
201 let n_components = self.explained_variance.len();
202
203 let mean_eigenvalue = self.explained_variance.mean().unwrap_or(0.0);
205 let kaiser_components = self
206 .explained_variance
207 .iter()
208 .position(|&x| x <= mean_eigenvalue)
209 .unwrap_or(n_components);
210
211 let elbow_components = self.find_elbow_point();
213
214 let variance_95_components = self
216 .explained_variance_ratio
217 .iter()
218 .scan(0.0, |acc, &x| {
219 *acc += x;
220 Some(*acc)
221 })
222 .position(|x| x >= 0.95)
223 .map(|x| x + 1)
224 .unwrap_or(n_components);
225
226 OptimalComponentsAnalysis {
227 kaiser_criterion: kaiser_components,
228 elbow_method: elbow_components,
229 variance_95_percent: variance_95_components,
230 total_components: n_components,
231 }
232 }
233
234 fn find_elbow_point(&self) -> usize {
236 let n = self.explained_variance.len();
237 if n < 3 {
238 return n;
239 }
240
241 let mut second_derivatives = Vec::new();
242
243 for i in 1..n - 1 {
245 let second_deriv = self.explained_variance[i - 1] - 2.0 * self.explained_variance[i]
246 + self.explained_variance[i + 1];
247 second_derivatives.push(second_deriv.abs());
248 }
249
250 let elbow_idx = second_derivatives
252 .iter()
253 .enumerate()
254 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
255 .map(|(idx, _)| idx + 2) .unwrap_or(n);
257
258 elbow_idx.min(n)
259 }
260}
261
262#[derive(Debug, Clone)]
264pub struct LoadingPlotData {
265 pub x_loadings: Array1<Float>,
267 pub y_loadings: Array1<Float>,
269 pub component1: usize,
271 pub component2: usize,
273 pub explained_variance1: Float,
275 pub explained_variance2: Float,
277 pub feature_names: Option<Vec<String>>,
279}
280
281impl LoadingPlotData {
282 pub fn axis_labels(&self) -> (String, String) {
284 let x_label = format!(
285 "PC{} ({:.1}%)",
286 self.component1 + 1,
287 self.explained_variance1 * 100.0
288 );
289 let y_label = format!(
290 "PC{} ({:.1}%)",
291 self.component2 + 1,
292 self.explained_variance2 * 100.0
293 );
294 (x_label, y_label)
295 }
296
297 pub fn feature_coordinates(&self) -> Vec<(Float, Float, String)> {
299 let n_features = self.x_loadings.len();
300 let mut coordinates = Vec::with_capacity(n_features);
301
302 for i in 0..n_features {
303 let name = self
304 .feature_names
305 .as_ref()
306 .and_then(|names| names.get(i))
307 .cloned()
308 .unwrap_or_else(|| format!("feature_{i}"));
309
310 coordinates.push((self.x_loadings[i], self.y_loadings[i], name));
311 }
312
313 coordinates
314 }
315}
316
317#[derive(Debug, Clone)]
319pub struct BiplotData {
320 pub x_scores: Array1<Float>,
322 pub y_scores: Array1<Float>,
324 pub loading_plot: LoadingPlotData,
326}
327
328impl BiplotData {
329 pub fn sample_coordinates(&self) -> Vec<(Float, Float)> {
331 self.x_scores
332 .iter()
333 .zip(self.y_scores.iter())
334 .map(|(&x, &y)| (x, y))
335 .collect()
336 }
337
338 pub fn scaled_loadings(&self, scale_factor: Float) -> Vec<(Float, Float, String)> {
340 self.loading_plot
341 .feature_coordinates()
342 .into_iter()
343 .map(|(x, y, name)| (x * scale_factor, y * scale_factor, name))
344 .collect()
345 }
346}
347
348#[derive(Debug, Clone)]
350pub struct FeatureImportance {
351 pub feature_index: usize,
353 pub feature_name: String,
355 pub importance_score: Float,
357 pub loading_value: Float,
359}
360
361#[derive(Debug, Clone)]
363pub struct ComponentContribution {
364 pub individual_variance: Array1<Float>,
366 pub cumulative_variance: Array1<Float>,
368 pub total_variance_explained: Float,
370}
371
372impl ComponentContribution {
373 pub fn components_for_variance(&self, threshold: Float) -> usize {
375 self.cumulative_variance
376 .iter()
377 .position(|&x| x >= threshold)
378 .map(|x| x + 1)
379 .unwrap_or(self.cumulative_variance.len())
380 }
381
382 pub fn variance_explained_by_n_components(&self, n: usize) -> Float {
384 if n == 0 {
385 0.0
386 } else if n >= self.cumulative_variance.len() {
387 self.total_variance_explained
388 } else {
389 self.cumulative_variance[n - 1]
390 }
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct ScreePlotData {
397 pub component_numbers: Array1<Float>,
399 pub eigenvalues: Array1<Float>,
401 pub explained_variance_ratios: Array1<Float>,
403}
404
405#[derive(Debug, Clone)]
407pub struct OptimalComponentsAnalysis {
408 pub kaiser_criterion: usize,
410 pub elbow_method: usize,
412 pub variance_95_percent: usize,
414 pub total_components: usize,
416}
417
418impl OptimalComponentsAnalysis {
419 pub fn recommendation(&self) -> ComponentRecommendation {
421 let criteria = vec![
422 self.kaiser_criterion,
423 self.elbow_method,
424 self.variance_95_percent,
425 ];
426
427 let mut sorted_criteria = criteria.clone();
429 sorted_criteria.sort();
430 let median = sorted_criteria[sorted_criteria.len() / 2];
431
432 ComponentRecommendation {
433 recommended_components: median,
434 confidence: self.calculate_confidence(&criteria),
435 reasoning: self.generate_reasoning(&criteria),
436 }
437 }
438
439 fn calculate_confidence(&self, criteria: &[usize]) -> Float {
440 let max_val = *criteria.iter().max().unwrap_or(&1);
441 let min_val = *criteria.iter().min().unwrap_or(&1);
442
443 if max_val == min_val {
444 1.0 } else {
446 1.0 - (max_val - min_val) as Float / self.total_components as Float
447 }
448 }
449
450 fn generate_reasoning(&self, _criteria: &[usize]) -> String {
451 format!(
452 "Kaiser criterion suggests {} components, elbow method suggests {}, \
453 and 95% variance threshold suggests {} components.",
454 self.kaiser_criterion, self.elbow_method, self.variance_95_percent
455 )
456 }
457}
458
459#[derive(Debug, Clone)]
461pub struct ComponentRecommendation {
462 pub recommended_components: usize,
464 pub confidence: Float,
466 pub reasoning: String,
468}
469
470pub struct DecompositionVisualizer;
472
473impl DecompositionVisualizer {
474 pub fn pca_visualization(
476 components: &Array2<Float>,
477 transformed_data: &Array2<Float>,
478 explained_variance: &Array1<Float>,
479 ) -> ComponentVisualization {
480 ComponentVisualization::new(
481 components.t().to_owned(), transformed_data.clone(),
483 explained_variance.clone(),
484 )
485 }
486
487 pub fn ica_visualization(
489 mixing_matrix: &Array2<Float>,
490 sources: &Array2<Float>,
491 ) -> ComponentVisualization {
492 let n_components = mixing_matrix.ncols();
494 let explained_variance = Array1::ones(n_components);
495
496 ComponentVisualization::new(mixing_matrix.clone(), sources.clone(), explained_variance)
497 }
498
499 pub fn nmf_visualization(
501 components: &Array2<Float>,
502 transformed_data: &Array2<Float>,
503 ) -> ComponentVisualization {
504 let explained_variance = components
506 .axis_iter(Axis(0))
507 .map(|comp| comp.dot(&comp))
508 .collect::<Array1<Float>>();
509
510 ComponentVisualization::new(
511 components.t().to_owned(),
512 transformed_data.clone(),
513 explained_variance,
514 )
515 }
516
517 pub fn factor_analysis_visualization(
519 loadings: &Array2<Float>,
520 factors: &Array2<Float>,
521 explained_variance: &Array1<Float>,
522 ) -> ComponentVisualization {
523 ComponentVisualization::new(
524 loadings.clone(),
525 factors.clone(),
526 explained_variance.clone(),
527 )
528 }
529}
530
531#[allow(non_snake_case)]
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use approx::assert_abs_diff_eq;
536
537 #[test]
538 fn test_component_visualization_creation() {
539 let loadings = Array2::from_shape_vec((3, 2), vec![0.8, 0.2, 0.6, 0.5, 0.1, 0.9])
540 .expect("shape and data length should match");
541 let scores = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5])
542 .expect("shape and data length should match");
543 let explained_variance = Array1::from_vec(vec![2.0, 1.0]);
544
545 let viz = ComponentVisualization::new(loadings, scores, explained_variance);
546
547 assert_eq!(viz.explained_variance_ratio.len(), 2);
548 assert_abs_diff_eq!(viz.explained_variance_ratio[0], 2.0 / 3.0, epsilon = 1e-10);
549 assert_abs_diff_eq!(viz.explained_variance_ratio[1], 1.0 / 3.0, epsilon = 1e-10);
550 }
551
552 #[test]
553 fn test_loading_plot_data() {
554 let loadings = Array2::from_shape_vec((3, 2), vec![0.8, 0.2, 0.6, 0.5, 0.1, 0.9])
555 .expect("shape and data length should match");
556 let scores = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5])
557 .expect("shape and data length should match");
558 let explained_variance = Array1::from_vec(vec![2.0, 1.0]);
559
560 let viz = ComponentVisualization::new(loadings, scores, explained_variance);
561 let loading_plot = viz
562 .loading_plot_data(0, 1)
563 .expect("operation should succeed");
564
565 assert_eq!(loading_plot.x_loadings.len(), 3);
566 assert_eq!(loading_plot.y_loadings.len(), 3);
567 assert_eq!(loading_plot.component1, 0);
568 assert_eq!(loading_plot.component2, 1);
569
570 let (x_label, y_label) = loading_plot.axis_labels();
571 assert!(x_label.contains("PC1"));
572 assert!(y_label.contains("PC2"));
573 }
574
575 #[test]
576 fn test_feature_importance() {
577 let loadings = Array2::from_shape_vec((3, 2), vec![0.8, 0.2, 0.6, 0.5, 0.1, 0.9])
578 .expect("shape and data length should match");
579 let scores = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5])
580 .expect("shape and data length should match");
581 let explained_variance = Array1::from_vec(vec![2.0, 1.0]);
582
583 let viz = ComponentVisualization::new(loadings, scores, explained_variance);
584 let importance = viz.feature_importance();
585
586 assert_eq!(importance.dim(), (3, 2));
587 for &val in importance.iter() {
589 assert!(val >= 0.0);
590 }
591 }
592
593 #[test]
594 fn test_top_features_per_component() {
595 let loadings = Array2::from_shape_vec((3, 2), vec![0.8, 0.2, 0.6, 0.5, 0.1, 0.9])
596 .expect("shape and data length should match");
597 let scores = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5])
598 .expect("shape and data length should match");
599 let explained_variance = Array1::from_vec(vec![2.0, 1.0]);
600
601 let viz = ComponentVisualization::new(loadings, scores, explained_variance);
602 let top_features = viz.top_features_per_component(2);
603
604 assert_eq!(top_features.len(), 2); assert_eq!(top_features[0].len(), 2); assert_eq!(top_features[1].len(), 2); }
608
609 #[test]
610 fn test_component_contributions() {
611 let loadings = Array2::from_shape_vec((3, 2), vec![0.8, 0.2, 0.6, 0.5, 0.1, 0.9])
612 .expect("shape and data length should match");
613 let scores = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5])
614 .expect("shape and data length should match");
615 let explained_variance = Array1::from_vec(vec![2.0, 1.0]);
616
617 let viz = ComponentVisualization::new(loadings, scores, explained_variance);
618 let contributions = viz.component_contributions();
619
620 assert_eq!(contributions.cumulative_variance.len(), 2);
621 assert!(contributions.cumulative_variance[0] <= contributions.cumulative_variance[1]);
622 assert_abs_diff_eq!(contributions.total_variance_explained, 1.0, epsilon = 1e-10);
623 }
624
625 #[test]
626 fn test_optimal_components_analysis() {
627 let loadings = Array2::from_shape_vec(
628 (5, 5),
629 vec![
630 0.8, 0.2, 0.1, 0.05, 0.02, 0.6, 0.5, 0.3, 0.1, 0.01, 0.1, 0.9, 0.2, 0.05, 0.01,
631 0.05, 0.1, 0.8, 0.4, 0.1, 0.02, 0.01, 0.1, 0.7, 0.6,
632 ],
633 )
634 .expect("operation should succeed");
635 let scores = Array2::zeros((10, 5));
636 let explained_variance = Array1::from_vec(vec![3.0, 2.0, 1.5, 0.8, 0.2]);
637
638 let viz = ComponentVisualization::new(loadings, scores, explained_variance);
639 let analysis = viz.optimal_components();
640
641 assert!(analysis.kaiser_criterion <= 5);
642 assert!(analysis.elbow_method <= 5);
643 assert!(analysis.variance_95_percent <= 5);
644 assert_eq!(analysis.total_components, 5);
645
646 let recommendation = analysis.recommendation();
647 assert!(recommendation.recommended_components <= 5);
648 assert!(recommendation.confidence >= 0.0 && recommendation.confidence <= 1.0);
649 assert!(!recommendation.reasoning.is_empty());
650 }
651
652 #[test]
653 fn test_decomposition_visualizer() {
654 let components = Array2::from_shape_vec((2, 3), vec![0.8, 0.6, 0.1, 0.2, 0.5, 0.9])
655 .expect("shape and data length should match");
656 let transformed_data =
657 Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5])
658 .expect("shape and data length should match");
659 let explained_variance = Array1::from_vec(vec![2.0, 1.0]);
660
661 let viz = DecompositionVisualizer::pca_visualization(
662 &components,
663 &transformed_data,
664 &explained_variance,
665 );
666
667 assert_eq!(viz.loadings.dim(), (3, 2)); assert_eq!(viz.scores.dim(), (4, 2));
669 assert_eq!(viz.explained_variance.len(), 2);
670 }
671
672 #[test]
673 fn test_biplot_data() {
674 let loadings = Array2::from_shape_vec((3, 2), vec![0.8, 0.2, 0.6, 0.5, 0.1, 0.9])
675 .expect("shape and data length should match");
676 let scores = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5])
677 .expect("shape and data length should match");
678 let explained_variance = Array1::from_vec(vec![2.0, 1.0]);
679
680 let viz = ComponentVisualization::new(loadings, scores, explained_variance);
681 let biplot = viz.biplot_data(0, 1).expect("operation should succeed");
682
683 assert_eq!(biplot.x_scores.len(), 4);
684 assert_eq!(biplot.y_scores.len(), 4);
685
686 let sample_coords = biplot.sample_coordinates();
687 assert_eq!(sample_coords.len(), 4);
688
689 let scaled_loadings = biplot.scaled_loadings(1.5);
690 assert_eq!(scaled_loadings.len(), 3);
691 }
692}