1use scirs2_core::ndarray::{Array1, Array2, Axis};
12use scirs2_core::rand_prelude::SliceRandom;
13use scirs2_core::random::rngs::StdRng;
14use scirs2_core::random::{rng as make_rng, RngExt, SeedableRng};
15use sklears_core::{
16 error::{Result, SklearsError},
17 types::Float,
18};
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Copy)]
23pub enum CrossValidationMethod {
24 KFold,
26 LeaveOneOut,
28 Stratified,
30 TimeSeries,
32}
33
34#[derive(Debug, Clone, Copy)]
36pub enum BootstrapMethod {
37 Standard,
39 Balanced,
41 Block,
43 Parametric,
45}
46
47#[derive(Debug, Clone, Copy)]
49pub enum InformationCriterion {
50 AIC,
52 BIC,
54 HQC,
56 CAIC,
58}
59
60#[derive(Debug, Clone)]
62pub struct ComponentSelectionConfig {
63 pub max_components: usize,
65 pub min_components: usize,
67 pub n_folds: usize,
69 pub n_bootstrap: usize,
71 pub random_state: Option<u64>,
73 pub tolerance: Float,
75}
76
77impl Default for ComponentSelectionConfig {
78 fn default() -> Self {
79 Self {
80 max_components: 20,
81 min_components: 1,
82 n_folds: 5,
83 n_bootstrap: 100,
84 random_state: None,
85 tolerance: 1e-6,
86 }
87 }
88}
89
90pub struct CrossValidationSelector {
92 config: ComponentSelectionConfig,
93 method: CrossValidationMethod,
94}
95
96impl CrossValidationSelector {
97 pub fn new(method: CrossValidationMethod) -> Self {
99 Self {
100 config: ComponentSelectionConfig::default(),
101 method,
102 }
103 }
104
105 pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
107 self.config = config;
108 self
109 }
110
111 pub fn max_components(mut self, max_components: usize) -> Self {
113 self.config.max_components = max_components;
114 self
115 }
116
117 pub fn n_folds(mut self, n_folds: usize) -> Self {
119 self.config.n_folds = n_folds;
120 self
121 }
122
123 pub fn random_state(mut self, random_state: u64) -> Self {
125 self.config.random_state = Some(random_state);
126 self
127 }
128
129 pub fn select_pca_components(&self, data: &Array2<Float>) -> Result<ComponentSelectionResult> {
131 let (n_samples, _n_features) = data.dim();
132 let max_components = self.config.max_components.min(n_samples);
133
134 let mut cv_scores = Vec::new();
135 let mut reconstruction_errors = Vec::new();
136
137 for n_comp in self.config.min_components..=max_components {
138 let folds = self.create_folds(n_samples)?;
139 let mut fold_scores = Vec::new();
140
141 for (train_idx, test_idx) in folds {
142 let train_data = self.extract_rows(data, &train_idx);
144 let test_data = self.extract_rows(data, &test_idx);
145
146 let pca_result = self.fit_pca(&train_data, n_comp)?;
148
149 let reconstruction_error =
151 self.compute_pca_reconstruction_error(&test_data, &pca_result)?;
152 fold_scores.push(reconstruction_error);
153 }
154
155 let mean_score = fold_scores.iter().sum::<Float>() / fold_scores.len() as Float;
156 let std_score = self.compute_std(&fold_scores, mean_score);
157
158 cv_scores.push(mean_score);
159 reconstruction_errors.push(std_score);
160 }
161
162 let optimal_idx = cv_scores
164 .iter()
165 .enumerate()
166 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
167 .map(|(idx, _)| idx)
168 .unwrap_or(0);
169
170 let optimal_components = self.config.min_components + optimal_idx;
171
172 Ok(ComponentSelectionResult {
173 optimal_components,
174 cv_scores: Array1::from_vec(cv_scores),
175 component_range: (self.config.min_components..=max_components).collect(),
176 method: SelectionMethod::CrossValidation,
177 metadata: HashMap::from([("reconstruction_errors".to_string(), reconstruction_errors)]),
178 })
179 }
180
181 fn create_folds(&self, n_samples: usize) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
183 let mut rng = if let Some(seed) = self.config.random_state {
184 StdRng::seed_from_u64(seed)
185 } else {
186 StdRng::from_rng(&mut make_rng())
187 };
188
189 match self.method {
190 CrossValidationMethod::KFold => self.k_fold_split(n_samples, &mut rng),
191 CrossValidationMethod::LeaveOneOut => self.leave_one_out_split(n_samples),
192 CrossValidationMethod::Stratified => {
193 self.k_fold_split(n_samples, &mut rng)
195 }
196 CrossValidationMethod::TimeSeries => self.time_series_split(n_samples),
197 }
198 }
199
200 fn k_fold_split(
202 &self,
203 n_samples: usize,
204 rng: &mut impl RngExt,
205 ) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
206 let mut indices: Vec<usize> = (0..n_samples).collect();
207 indices.shuffle(rng);
208
209 let fold_size = n_samples / self.config.n_folds;
210 let mut folds = Vec::new();
211
212 for fold in 0..self.config.n_folds {
213 let start = fold * fold_size;
214 let end = if fold == self.config.n_folds - 1 {
215 n_samples
216 } else {
217 (fold + 1) * fold_size
218 };
219
220 let test_idx: Vec<usize> = indices[start..end].to_vec();
221 let train_idx: Vec<usize> = indices[..start]
222 .iter()
223 .chain(indices[end..].iter())
224 .cloned()
225 .collect();
226
227 folds.push((train_idx, test_idx));
228 }
229
230 Ok(folds)
231 }
232
233 fn leave_one_out_split(&self, n_samples: usize) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
235 let mut folds = Vec::new();
236
237 for i in 0..n_samples {
238 let test_idx = vec![i];
239 let train_idx: Vec<usize> = (0..n_samples).filter(|&x| x != i).collect();
240 folds.push((train_idx, test_idx));
241 }
242
243 Ok(folds)
244 }
245
246 fn time_series_split(&self, n_samples: usize) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
248 let min_train_size = n_samples / 3;
249 let test_size = n_samples / 10;
250 let mut folds = Vec::new();
251
252 for start in min_train_size..n_samples - test_size {
253 let train_idx: Vec<usize> = (0..start).collect();
254 let test_idx: Vec<usize> = (start..start + test_size).collect();
255 folds.push((train_idx, test_idx));
256 }
257
258 Ok(folds)
259 }
260
261 fn extract_rows(&self, data: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
263 let n_cols = data.ncols();
264 let mut result = Array2::zeros((indices.len(), n_cols));
265
266 for (i, &row_idx) in indices.iter().enumerate() {
267 for j in 0..n_cols {
268 result[[i, j]] = data[[row_idx, j]];
269 }
270 }
271
272 result
273 }
274
275 fn fit_pca(&self, data: &Array2<Float>, n_components: usize) -> Result<SimplePCAResult> {
277 let (n_samples, n_features) = data.dim();
278
279 if n_components > n_features.min(n_samples) {
280 return Err(SklearsError::InvalidInput(
281 "n_components too large".to_string(),
282 ));
283 }
284
285 let mean = data
287 .mean_axis(Axis(0))
288 .expect("array should have elements for mean computation");
289 let centered_data = data - &mean.clone().insert_axis(Axis(0));
290
291 let cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
293
294 let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
296
297 let components = eigenvectors
299 .slice(scirs2_core::ndarray::s![.., 0..n_components])
300 .to_owned();
301 let explained_variance = eigenvalues
302 .slice(scirs2_core::ndarray::s![0..n_components])
303 .to_owned();
304
305 Ok(SimplePCAResult {
306 components,
307 mean,
308 explained_variance,
309 })
310 }
311
312 fn simple_eigendecomposition(
314 &self,
315 matrix: &Array2<Float>,
316 ) -> Result<(Array1<Float>, Array2<Float>)> {
317 let n = matrix.nrows();
318
319 let mut eigenvalues = Array1::zeros(n);
322 let mut eigenvectors = Array2::eye(n);
323
324 let mut v = Array1::ones(n);
326 for _ in 0..100 {
327 let v_new = matrix.dot(&v);
328 let norm = (v_new.dot(&v_new)).sqrt();
329 if norm > 1e-12 {
330 v = v_new / norm;
331 }
332
333 let eigenvalue = v.dot(&matrix.dot(&v));
334 eigenvalues[0] = eigenvalue;
335
336 for i in 0..n {
337 eigenvectors[[i, 0]] = v[i];
338 }
339 }
340
341 for i in 1..n {
343 eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
344 }
345
346 let mut indices: Vec<usize> = (0..n).collect();
348 indices.sort_by(|&i, &j| {
349 eigenvalues[j]
350 .partial_cmp(&eigenvalues[i])
351 .unwrap_or(std::cmp::Ordering::Equal)
352 });
353
354 let sorted_eigenvalues =
355 Array1::from_vec(indices.iter().map(|&i| eigenvalues[i]).collect());
356 let sorted_eigenvectors =
357 Array2::from_shape_fn((n, n), |(i, j)| eigenvectors[[i, indices[j]]]);
358
359 Ok((sorted_eigenvalues, sorted_eigenvectors))
360 }
361
362 fn compute_pca_reconstruction_error(
364 &self,
365 data: &Array2<Float>,
366 pca: &SimplePCAResult,
367 ) -> Result<Float> {
368 let (n_samples, _) = data.dim();
369
370 let centered_data = data - &pca.mean.clone().insert_axis(Axis(0));
372
373 let projected = centered_data.dot(&pca.components);
375
376 let reconstructed =
378 projected.dot(&pca.components.t()) + &pca.mean.clone().insert_axis(Axis(0));
379
380 let diff = data - &reconstructed;
382 let error = diff.iter().map(|&x| x * x).sum::<Float>() / (n_samples as Float);
383
384 Ok(error)
385 }
386
387 fn compute_std(&self, values: &[Float], mean: Float) -> Float {
389 if values.len() <= 1 {
390 return 0.0;
391 }
392
393 let variance = values
394 .iter()
395 .map(|&x| (x - mean) * (x - mean))
396 .sum::<Float>()
397 / (values.len() - 1) as Float;
398
399 variance.sqrt()
400 }
401}
402
403#[derive(Debug, Clone)]
405struct SimplePCAResult {
406 components: Array2<Float>,
407 mean: Array1<Float>,
408 explained_variance: Array1<Float>,
409}
410
411pub struct BootstrapSelector {
413 config: ComponentSelectionConfig,
414 method: BootstrapMethod,
415}
416
417impl BootstrapSelector {
418 pub fn new(method: BootstrapMethod) -> Self {
420 Self {
421 config: ComponentSelectionConfig::default(),
422 method,
423 }
424 }
425
426 pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
428 self.config = config;
429 self
430 }
431
432 pub fn n_bootstrap(mut self, n_bootstrap: usize) -> Self {
434 self.config.n_bootstrap = n_bootstrap;
435 self
436 }
437
438 pub fn assess_stability(
440 &self,
441 data: &Array2<Float>,
442 n_components: usize,
443 ) -> Result<StabilityResult> {
444 let (_n_samples, _) = data.dim();
445 let mut rng = if let Some(seed) = self.config.random_state {
446 StdRng::seed_from_u64(seed)
447 } else {
448 StdRng::from_rng(&mut make_rng())
449 };
450
451 let mut component_similarities = Vec::new();
452 let mut eigenvalue_stability = Vec::new();
453
454 let original_pca = self.fit_pca(data, n_components)?;
456
457 for _ in 0..self.config.n_bootstrap {
458 let bootstrap_data = self.create_bootstrap_sample(data, &mut rng)?;
460
461 let bootstrap_pca = self.fit_pca(&bootstrap_data, n_components)?;
463
464 let similarity = self.compute_component_similarity(
466 &original_pca.components,
467 &bootstrap_pca.components,
468 )?;
469 component_similarities.push(similarity);
470
471 let eigenvalue_sim = self.compute_eigenvalue_similarity(
473 &original_pca.explained_variance,
474 &bootstrap_pca.explained_variance,
475 );
476 eigenvalue_stability.push(eigenvalue_sim);
477 }
478
479 let mean_similarity =
480 component_similarities.iter().sum::<Float>() / component_similarities.len() as Float;
481 let std_similarity = self.compute_std(&component_similarities, mean_similarity);
482
483 let mean_eigenvalue_stability =
484 eigenvalue_stability.iter().sum::<Float>() / eigenvalue_stability.len() as Float;
485
486 Ok(StabilityResult {
487 mean_component_similarity: mean_similarity,
488 std_component_similarity: std_similarity,
489 component_similarities: Array1::from_vec(component_similarities),
490 eigenvalue_stability: mean_eigenvalue_stability,
491 is_stable: mean_similarity > 0.8 && std_similarity < 0.2,
492 })
493 }
494
495 fn create_bootstrap_sample(
497 &self,
498 data: &Array2<Float>,
499 rng: &mut impl RngExt,
500 ) -> Result<Array2<Float>> {
501 let (n_samples, n_features) = data.dim();
502 let mut bootstrap_data = Array2::zeros((n_samples, n_features));
503
504 match self.method {
505 BootstrapMethod::Standard => {
506 for i in 0..n_samples {
507 let sample_idx = rng.random_range(0..n_samples);
508 for j in 0..n_features {
509 bootstrap_data[[i, j]] = data[[sample_idx, j]];
510 }
511 }
512 }
513 BootstrapMethod::Balanced => {
514 let mut indices: Vec<usize> = (0..n_samples).collect();
516 indices.shuffle(rng);
517
518 for i in 0..n_samples {
519 let sample_idx = if i < indices.len() {
520 indices[i]
521 } else {
522 rng.random_range(0..n_samples)
523 };
524
525 for j in 0..n_features {
526 bootstrap_data[[i, j]] = data[[sample_idx, j]];
527 }
528 }
529 }
530 BootstrapMethod::Block => {
531 let block_size = (n_samples as f64).sqrt() as usize;
533 let n_blocks = n_samples / block_size;
534
535 for block in 0..n_blocks {
536 let start_idx = rng.random_range(0..n_samples - block_size + 1);
537
538 for i in 0..block_size {
539 let src_idx = start_idx + i;
540 let dst_idx = block * block_size + i;
541
542 if dst_idx < n_samples {
543 for j in 0..n_features {
544 bootstrap_data[[dst_idx, j]] = data[[src_idx, j]];
545 }
546 }
547 }
548 }
549 }
550 BootstrapMethod::Parametric => {
551 for j in 0..n_features {
554 let column = data.column(j);
555 let mean = column.mean().unwrap_or(0.0);
556 let std = self.compute_std(
557 column.as_slice().expect("matrix indexing should be valid"),
558 mean,
559 );
560
561 for i in 0..n_samples {
562 bootstrap_data[[i, j]] = rng.random::<Float>() * std + mean;
563 }
564 }
565 }
566 }
567
568 Ok(bootstrap_data)
569 }
570
571 fn fit_pca(&self, data: &Array2<Float>, n_components: usize) -> Result<SimplePCAResult> {
573 let (n_samples, n_features) = data.dim();
574
575 if n_components > n_features.min(n_samples) {
576 return Err(SklearsError::InvalidInput(
577 "n_components too large".to_string(),
578 ));
579 }
580
581 let mean = data
583 .mean_axis(Axis(0))
584 .expect("array should have elements for mean computation");
585 let centered_data = data - &mean.clone().insert_axis(Axis(0));
586
587 let cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
589
590 let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
592
593 let components = eigenvectors
595 .slice(scirs2_core::ndarray::s![.., 0..n_components])
596 .to_owned();
597 let explained_variance = eigenvalues
598 .slice(scirs2_core::ndarray::s![0..n_components])
599 .to_owned();
600
601 Ok(SimplePCAResult {
602 components,
603 mean,
604 explained_variance,
605 })
606 }
607
608 fn simple_eigendecomposition(
610 &self,
611 matrix: &Array2<Float>,
612 ) -> Result<(Array1<Float>, Array2<Float>)> {
613 let n = matrix.nrows();
614 let mut eigenvalues = Array1::zeros(n);
615 let mut eigenvectors = Array2::eye(n);
616
617 let mut v = Array1::ones(n);
619 for _ in 0..100 {
620 let v_new = matrix.dot(&v);
621 let norm = (v_new.dot(&v_new)).sqrt();
622 if norm > 1e-12 {
623 v = v_new / norm;
624 }
625
626 let eigenvalue = v.dot(&matrix.dot(&v));
627 eigenvalues[0] = eigenvalue;
628
629 for i in 0..n {
630 eigenvectors[[i, 0]] = v[i];
631 }
632 }
633
634 for i in 1..n {
636 eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
637 }
638
639 Ok((eigenvalues, eigenvectors))
640 }
641
642 fn compute_component_similarity(
644 &self,
645 components1: &Array2<Float>,
646 components2: &Array2<Float>,
647 ) -> Result<Float> {
648 let (_, n_comp1) = components1.dim();
649 let (_, n_comp2) = components2.dim();
650 let n_comp = n_comp1.min(n_comp2);
651
652 let mut total_similarity = 0.0;
653
654 for i in 0..n_comp {
655 let comp1 = components1.column(i);
656 let comp2 = components2.column(i);
657
658 let correlation = self
659 .compute_correlation(&comp1.to_owned(), &comp2.to_owned())
660 .abs();
661 total_similarity += correlation;
662 }
663
664 Ok(total_similarity / n_comp as Float)
665 }
666
667 fn compute_eigenvalue_similarity(
669 &self,
670 eigenvals1: &Array1<Float>,
671 eigenvals2: &Array1<Float>,
672 ) -> Float {
673 let n = eigenvals1.len().min(eigenvals2.len());
674 if n == 0 {
675 return 0.0;
676 }
677
678 let mut similarity = 0.0;
679 for i in 0..n {
680 let diff = (eigenvals1[i] - eigenvals2[i]).abs();
681 let avg = (eigenvals1[i] + eigenvals2[i]) / 2.0;
682 if avg > 1e-12 {
683 similarity += 1.0 - diff / avg;
684 }
685 }
686
687 similarity / n as Float
688 }
689
690 fn compute_correlation(&self, x: &Array1<Float>, y: &Array1<Float>) -> Float {
692 let n = x.len();
693 if n != y.len() || n == 0 {
694 return 0.0;
695 }
696
697 let mean_x = x.mean().unwrap_or(0.0);
698 let mean_y = y.mean().unwrap_or(0.0);
699
700 let mut num = 0.0;
701 let mut den_x = 0.0;
702 let mut den_y = 0.0;
703
704 for i in 0..n {
705 let dx = x[i] - mean_x;
706 let dy = y[i] - mean_y;
707 num += dx * dy;
708 den_x += dx * dx;
709 den_y += dy * dy;
710 }
711
712 if den_x == 0.0 || den_y == 0.0 {
713 return 0.0;
714 }
715
716 num / (den_x * den_y).sqrt()
717 }
718
719 fn compute_std(&self, values: &[Float], mean: Float) -> Float {
721 if values.len() <= 1 {
722 return 0.0;
723 }
724
725 let variance = values
726 .iter()
727 .map(|&x| (x - mean) * (x - mean))
728 .sum::<Float>()
729 / (values.len() - 1) as Float;
730
731 variance.sqrt()
732 }
733}
734
735pub struct InformationCriteriaSelector {
737 config: ComponentSelectionConfig,
738 criterion: InformationCriterion,
739}
740
741impl InformationCriteriaSelector {
742 pub fn new(criterion: InformationCriterion) -> Self {
744 Self {
745 config: ComponentSelectionConfig::default(),
746 criterion,
747 }
748 }
749
750 pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
752 self.config = config;
753 self
754 }
755
756 pub fn select_components(&self, data: &Array2<Float>) -> Result<ComponentSelectionResult> {
758 let (n_samples, n_features) = data.dim();
759 let max_components = self.config.max_components.min(n_features.min(n_samples));
760
761 let mut ic_scores = Vec::new();
762
763 for n_comp in self.config.min_components..=max_components {
764 let pca_result = self.fit_pca(data, n_comp)?;
765 let ic_score = self.compute_information_criterion(data, &pca_result, n_comp)?;
766 ic_scores.push(ic_score);
767 }
768
769 let optimal_idx = ic_scores
771 .iter()
772 .enumerate()
773 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
774 .map(|(idx, _)| idx)
775 .unwrap_or(0);
776
777 let optimal_components = self.config.min_components + optimal_idx;
778
779 Ok(ComponentSelectionResult {
780 optimal_components,
781 cv_scores: Array1::from_vec(ic_scores),
782 component_range: (self.config.min_components..=max_components).collect(),
783 method: SelectionMethod::InformationCriteria,
784 metadata: HashMap::new(),
785 })
786 }
787
788 fn fit_pca(&self, data: &Array2<Float>, n_components: usize) -> Result<SimplePCAResult> {
790 let (n_samples, _n_features) = data.dim();
791
792 let mean = data
794 .mean_axis(Axis(0))
795 .expect("array should have elements for mean computation");
796 let centered_data = data - &mean.clone().insert_axis(Axis(0));
797
798 let cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
800
801 let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
803
804 let components = eigenvectors
806 .slice(scirs2_core::ndarray::s![.., 0..n_components])
807 .to_owned();
808 let explained_variance = eigenvalues
809 .slice(scirs2_core::ndarray::s![0..n_components])
810 .to_owned();
811
812 Ok(SimplePCAResult {
813 components,
814 mean,
815 explained_variance,
816 })
817 }
818
819 fn simple_eigendecomposition(
821 &self,
822 matrix: &Array2<Float>,
823 ) -> Result<(Array1<Float>, Array2<Float>)> {
824 let n = matrix.nrows();
825 let mut eigenvalues = Array1::zeros(n);
826 let mut eigenvectors = Array2::eye(n);
827
828 let mut v = Array1::ones(n);
830 for _ in 0..100 {
831 let v_new = matrix.dot(&v);
832 let norm = (v_new.dot(&v_new)).sqrt();
833 if norm > 1e-12 {
834 v = v_new / norm;
835 }
836
837 let eigenvalue = v.dot(&matrix.dot(&v));
838 eigenvalues[0] = eigenvalue;
839
840 for i in 0..n {
841 eigenvectors[[i, 0]] = v[i];
842 }
843 }
844
845 for i in 1..n {
847 eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
848 }
849
850 Ok((eigenvalues, eigenvectors))
851 }
852
853 fn compute_information_criterion(
855 &self,
856 data: &Array2<Float>,
857 pca: &SimplePCAResult,
858 n_components: usize,
859 ) -> Result<Float> {
860 let (n_samples, _) = data.dim();
861
862 let centered_data = data - &pca.mean.clone().insert_axis(Axis(0));
864 let projected = centered_data.dot(&pca.components);
865 let reconstructed =
866 projected.dot(&pca.components.t()) + &pca.mean.clone().insert_axis(Axis(0));
867 let diff = data - &reconstructed;
868 let mse = diff.iter().map(|&x| x * x).sum::<Float>() / (n_samples as Float);
869
870 let log_likelihood =
872 -0.5 * n_samples as Float * (2.0 * std::f64::consts::PI * mse).ln() as Float;
873
874 let n_params = n_components * (data.ncols() + 1); Ok(match self.criterion {
878 InformationCriterion::AIC => -2.0 * log_likelihood + 2.0 * n_params as Float,
879 InformationCriterion::BIC => {
880 -2.0 * log_likelihood + (n_params as Float) * (n_samples as Float).ln()
881 }
882 InformationCriterion::HQC => {
883 -2.0 * log_likelihood + 2.0 * (n_params as Float) * (n_samples as Float).ln().ln()
884 }
885 InformationCriterion::CAIC => {
886 -2.0 * log_likelihood + (n_params as Float) * ((n_samples as Float).ln() + 1.0)
887 }
888 })
889 }
890}
891
892#[derive(Debug, Clone, Copy)]
894pub enum SelectionMethod {
895 CrossValidation,
896 Bootstrap,
897 InformationCriteria,
898 ParallelAnalysis,
899}
900
901#[derive(Debug, Clone)]
903pub struct ComponentSelectionResult {
904 pub optimal_components: usize,
906 pub cv_scores: Array1<Float>,
908 pub component_range: Vec<usize>,
910 pub method: SelectionMethod,
912 pub metadata: HashMap<String, Vec<Float>>,
914}
915
916impl ComponentSelectionResult {
917 pub fn optimal_score(&self) -> Float {
919 let optimal_idx = self.optimal_components - self.component_range[0];
920 self.cv_scores[optimal_idx]
921 }
922
923 pub fn is_stable(&self) -> bool {
925 if let Some(errors) = self.metadata.get("reconstruction_errors") {
926 let optimal_idx = self.optimal_components - self.component_range[0];
927 if optimal_idx < errors.len() {
928 return errors[optimal_idx] < 0.1; }
930 }
931 true
932 }
933}
934
935#[derive(Debug, Clone)]
937pub struct StabilityResult {
938 pub mean_component_similarity: Float,
940 pub std_component_similarity: Float,
942 pub component_similarities: Array1<Float>,
944 pub eigenvalue_stability: Float,
946 pub is_stable: bool,
948}
949
950impl StabilityResult {
951 pub fn stability_confidence(&self) -> Float {
953 if self.std_component_similarity > 0.0 {
954 self.mean_component_similarity / self.std_component_similarity
955 } else {
956 self.mean_component_similarity
957 }
958 }
959}
960
961pub struct ParallelAnalysis {
963 config: ComponentSelectionConfig,
964 n_simulations: usize,
965}
966
967impl ParallelAnalysis {
968 pub fn new() -> Self {
970 Self {
971 config: ComponentSelectionConfig::default(),
972 n_simulations: 100,
973 }
974 }
975
976 pub fn n_simulations(mut self, n_simulations: usize) -> Self {
978 self.n_simulations = n_simulations;
979 self
980 }
981
982 pub fn random_state(mut self, random_state: u64) -> Self {
984 self.config.random_state = Some(random_state);
985 self
986 }
987
988 pub fn analyze(&self, data: &Array2<Float>) -> Result<ComponentSelectionResult> {
990 let (n_samples, n_features) = data.dim();
991 let mut rng = if let Some(seed) = self.config.random_state {
992 StdRng::seed_from_u64(seed)
993 } else {
994 StdRng::from_rng(&mut make_rng())
995 };
996
997 let original_eigenvalues = self.compute_eigenvalues(data)?;
999
1000 let mut random_eigenvalues = Array2::zeros((self.n_simulations, n_features));
1002
1003 for sim in 0..self.n_simulations {
1004 let random_data = self.generate_random_data(n_samples, n_features, &mut rng);
1005 let eigenvals = self.compute_eigenvalues(&random_data)?;
1006
1007 for (i, &val) in eigenvals.iter().enumerate() {
1008 random_eigenvalues[[sim, i]] = val;
1009 }
1010 }
1011
1012 let mut percentile_eigenvalues = Array1::zeros(n_features);
1014 for i in 0..n_features {
1015 let mut column: Vec<Float> = random_eigenvalues.column(i).to_vec();
1016 column.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1017 let percentile_idx = (0.95 * self.n_simulations as Float) as usize;
1018 percentile_eigenvalues[i] = column[percentile_idx.min(column.len() - 1)];
1019 }
1020
1021 let mut optimal_components = 0;
1023 let mut scores = Vec::new();
1024
1025 for i in 0..n_features {
1026 let difference = original_eigenvalues[i] - percentile_eigenvalues[i];
1027 scores.push(difference);
1028
1029 if original_eigenvalues[i] > percentile_eigenvalues[i] {
1030 optimal_components = i + 1;
1031 } else {
1032 break;
1033 }
1034 }
1035
1036 Ok(ComponentSelectionResult {
1037 optimal_components,
1038 cv_scores: Array1::from_vec(scores),
1039 component_range: (1..=n_features).collect(),
1040 method: SelectionMethod::ParallelAnalysis,
1041 metadata: HashMap::from([
1042 (
1043 "original_eigenvalues".to_string(),
1044 original_eigenvalues.to_vec(),
1045 ),
1046 (
1047 "random_eigenvalues_95th".to_string(),
1048 percentile_eigenvalues.to_vec(),
1049 ),
1050 ]),
1051 })
1052 }
1053
1054 fn compute_eigenvalues(&self, data: &Array2<Float>) -> Result<Array1<Float>> {
1056 let (n_samples, _) = data.dim();
1057
1058 let mean = data
1060 .mean_axis(Axis(0))
1061 .expect("array should have elements for mean computation");
1062 let centered_data = data - &mean.clone().insert_axis(Axis(0));
1063
1064 let cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
1066
1067 let (eigenvalues, _) = self.simple_eigendecomposition(&cov_matrix)?;
1069
1070 Ok(eigenvalues)
1071 }
1072
1073 fn generate_random_data(
1075 &self,
1076 n_samples: usize,
1077 n_features: usize,
1078 rng: &mut impl RngExt,
1079 ) -> Array2<Float> {
1080 let mut random_data = Array2::zeros((n_samples, n_features));
1081
1082 for i in 0..n_samples {
1083 for j in 0..n_features {
1084 random_data[[i, j]] = rng.random::<Float>() - 0.5; }
1086 }
1087
1088 random_data
1089 }
1090
1091 fn simple_eigendecomposition(
1093 &self,
1094 matrix: &Array2<Float>,
1095 ) -> Result<(Array1<Float>, Array2<Float>)> {
1096 let n = matrix.nrows();
1097 let mut eigenvalues = Array1::zeros(n);
1098 let mut eigenvectors = Array2::eye(n);
1099
1100 let mut v = Array1::ones(n);
1102 for _ in 0..100 {
1103 let v_new = matrix.dot(&v);
1104 let norm = (v_new.dot(&v_new)).sqrt();
1105 if norm > 1e-12 {
1106 v = v_new / norm;
1107 }
1108
1109 let eigenvalue = v.dot(&matrix.dot(&v));
1110 eigenvalues[0] = eigenvalue;
1111
1112 for i in 0..n {
1113 eigenvectors[[i, 0]] = v[i];
1114 }
1115 }
1116
1117 for i in 1..n {
1119 eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
1120 }
1121
1122 Ok((eigenvalues, eigenvectors))
1123 }
1124}
1125
1126impl Default for ParallelAnalysis {
1127 fn default() -> Self {
1128 Self::new()
1129 }
1130}
1131
1132#[allow(non_snake_case)]
1133#[cfg(test)]
1134mod tests {
1135 use super::*;
1136 use scirs2_core::ndarray::array;
1137
1138 #[test]
1139 fn test_cross_validation_selector() {
1140 let data = array![
1141 [1.0, 2.0, 3.0],
1142 [2.0, 3.0, 4.0],
1143 [3.0, 4.0, 5.0],
1144 [4.0, 5.0, 6.0],
1145 [5.0, 6.0, 7.0],
1146 ];
1147
1148 let selector = CrossValidationSelector::new(CrossValidationMethod::KFold)
1149 .max_components(2)
1150 .n_folds(3)
1151 .random_state(42);
1152
1153 let result = selector
1154 .select_pca_components(&data)
1155 .expect("operation should succeed");
1156
1157 assert!(result.optimal_components >= 1);
1158 assert!(result.optimal_components <= 2);
1159 assert_eq!(result.cv_scores.len(), 2);
1160 }
1161
1162 #[test]
1163 fn test_bootstrap_selector() {
1164 let data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0], [5.0, 6.0],];
1165
1166 let selector = BootstrapSelector::new(BootstrapMethod::Standard).n_bootstrap(10);
1167
1168 let result = selector
1169 .assess_stability(&data, 1)
1170 .expect("operation should succeed");
1171
1172 assert!(result.mean_component_similarity >= 0.0);
1173 assert!(result.mean_component_similarity <= 1.0);
1174 assert_eq!(result.component_similarities.len(), 10);
1175 }
1176
1177 #[test]
1178 fn test_information_criteria_selector() {
1179 let data = array![
1180 [1.0, 2.0, 3.0],
1181 [2.0, 3.0, 4.0],
1182 [3.0, 4.0, 5.0],
1183 [4.0, 5.0, 6.0],
1184 ];
1185
1186 let selector = InformationCriteriaSelector::new(InformationCriterion::AIC);
1187 let result = selector
1188 .select_components(&data)
1189 .expect("operation should succeed");
1190
1191 assert!(result.optimal_components >= 1);
1192 assert!(!result.cv_scores.is_empty());
1193 }
1194
1195 #[test]
1196 fn test_parallel_analysis() {
1197 let data = array![
1198 [1.0, 2.0, 3.0],
1199 [2.0, 3.0, 4.0],
1200 [3.0, 4.0, 5.0],
1201 [4.0, 5.0, 6.0],
1202 [5.0, 6.0, 7.0],
1203 ];
1204
1205 let pa = ParallelAnalysis::new().n_simulations(10).random_state(42);
1206
1207 let result = pa.analyze(&data).expect("operation should succeed");
1208
1209 assert!(result.optimal_components <= 3); assert_eq!(result.cv_scores.len(), 3);
1212 }
1213
1214 #[test]
1215 fn test_k_fold_split() {
1216 let selector = CrossValidationSelector::new(CrossValidationMethod::KFold)
1217 .n_folds(3)
1218 .random_state(42);
1219
1220 let folds = selector.create_folds(10).expect("operation should succeed");
1221
1222 assert_eq!(folds.len(), 3);
1223
1224 let mut all_test_indices: Vec<usize> = Vec::new();
1226 for (_, test_idx) in &folds {
1227 all_test_indices.extend(test_idx);
1228 }
1229 all_test_indices.sort();
1230
1231 let expected_indices: Vec<usize> = (0..10).collect();
1232 assert_eq!(all_test_indices, expected_indices);
1233 }
1234
1235 #[test]
1236 fn test_leave_one_out_split() {
1237 let selector = CrossValidationSelector::new(CrossValidationMethod::LeaveOneOut);
1238 let folds = selector.create_folds(5).expect("operation should succeed");
1239
1240 assert_eq!(folds.len(), 5);
1241
1242 for (i, (train_idx, test_idx)) in folds.iter().enumerate() {
1243 assert_eq!(test_idx.len(), 1);
1244 assert_eq!(test_idx[0], i);
1245 assert_eq!(train_idx.len(), 4);
1246 assert!(!train_idx.contains(&i));
1247 }
1248 }
1249}