Skip to main content

sklears_decomposition/
component_selection.rs

1//! Component Selection Methods for Decomposition
2//!
3//! This module provides methods for selecting the optimal number of components
4//! in decomposition algorithms including:
5//! - Cross-validation for component number selection
6//! - Bootstrap methods for component stability assessment
7//! - Information criteria (AIC, BIC) for model selection
8//! - Parallel analysis for factor number determination
9//! - Stability-based selection methods
10
11use 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/// Cross-validation methods for component selection
22#[derive(Debug, Clone, Copy)]
23pub enum CrossValidationMethod {
24    /// K-fold cross-validation
25    KFold,
26    /// Leave-one-out cross-validation
27    LeaveOneOut,
28    /// Stratified cross-validation
29    Stratified,
30    /// Time series cross-validation (forward chaining)
31    TimeSeries,
32}
33
34/// Bootstrap methods for stability assessment
35#[derive(Debug, Clone, Copy)]
36pub enum BootstrapMethod {
37    /// Standard bootstrap resampling
38    Standard,
39    /// Balanced bootstrap
40    Balanced,
41    /// Block bootstrap for time series
42    Block,
43    /// Parametric bootstrap
44    Parametric,
45}
46
47/// Information criteria for model selection
48#[derive(Debug, Clone, Copy)]
49pub enum InformationCriterion {
50    /// Akaike Information Criterion
51    AIC,
52    /// Bayesian Information Criterion
53    BIC,
54    /// Hannan-Quinn Information Criterion
55    HQC,
56    /// Consistent Akaike Information Criterion
57    CAIC,
58}
59
60/// Component selection configuration
61#[derive(Debug, Clone)]
62pub struct ComponentSelectionConfig {
63    /// Maximum number of components to test
64    pub max_components: usize,
65    /// Minimum number of components to test
66    pub min_components: usize,
67    /// Number of cross-validation folds
68    pub n_folds: usize,
69    /// Number of bootstrap samples
70    pub n_bootstrap: usize,
71    /// Random state for reproducibility
72    pub random_state: Option<u64>,
73    /// Tolerance for convergence criteria
74    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
90/// Cross-validation based component selection
91pub struct CrossValidationSelector {
92    config: ComponentSelectionConfig,
93    method: CrossValidationMethod,
94}
95
96impl CrossValidationSelector {
97    /// Create a new cross-validation selector
98    pub fn new(method: CrossValidationMethod) -> Self {
99        Self {
100            config: ComponentSelectionConfig::default(),
101            method,
102        }
103    }
104
105    /// Set configuration
106    pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
107        self.config = config;
108        self
109    }
110
111    /// Set maximum components to test
112    pub fn max_components(mut self, max_components: usize) -> Self {
113        self.config.max_components = max_components;
114        self
115    }
116
117    /// Set number of folds
118    pub fn n_folds(mut self, n_folds: usize) -> Self {
119        self.config.n_folds = n_folds;
120        self
121    }
122
123    /// Set random state
124    pub fn random_state(mut self, random_state: u64) -> Self {
125        self.config.random_state = Some(random_state);
126        self
127    }
128
129    /// Select optimal number of components for PCA using cross-validation
130    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                // Extract training and test data
143                let train_data = self.extract_rows(data, &train_idx);
144                let test_data = self.extract_rows(data, &test_idx);
145
146                // Fit PCA on training data
147                let pca_result = self.fit_pca(&train_data, n_comp)?;
148
149                // Compute reconstruction error on test data
150                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        // Find optimal number of components (minimum reconstruction error)
163        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    /// Create cross-validation folds
182    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                // For now, fall back to k-fold (stratified would need labels)
194                self.k_fold_split(n_samples, &mut rng)
195            }
196            CrossValidationMethod::TimeSeries => self.time_series_split(n_samples),
197        }
198    }
199
200    /// K-fold cross-validation split
201    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    /// Leave-one-out cross-validation split
234    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    /// Time series cross-validation split (forward chaining)
247    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    /// Extract rows from matrix
262    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    /// Simplified PCA fitting (returns components)
276    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        // Center the data
286        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        // Compute covariance matrix
292        let cov_matrix = centered_data.t().dot(&centered_data) / (n_samples - 1) as Float;
293
294        // Simplified eigendecomposition (in practice would use proper SVD/eigendecomposition)
295        let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
296
297        // Take top n_components
298        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    /// Simplified eigendecomposition (placeholder implementation)
313    fn simple_eigendecomposition(
314        &self,
315        matrix: &Array2<Float>,
316    ) -> Result<(Array1<Float>, Array2<Float>)> {
317        let n = matrix.nrows();
318
319        // This is a simplified implementation for demonstration
320        // In practice, would use proper linear algebra libraries
321        let mut eigenvalues = Array1::zeros(n);
322        let mut eigenvectors = Array2::eye(n);
323
324        // Power iteration for largest eigenvalue
325        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        // Fill remaining eigenvalues (simplified)
342        for i in 1..n {
343            eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
344        }
345
346        // Sort in descending order
347        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    /// Compute PCA reconstruction error
363    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        // Center the data
371        let centered_data = data - &pca.mean.clone().insert_axis(Axis(0));
372
373        // Project to lower dimensional space
374        let projected = centered_data.dot(&pca.components);
375
376        // Reconstruct
377        let reconstructed =
378            projected.dot(&pca.components.t()) + &pca.mean.clone().insert_axis(Axis(0));
379
380        // Compute reconstruction error
381        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    /// Compute standard deviation
388    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/// Simplified PCA result for cross-validation
404#[derive(Debug, Clone)]
405struct SimplePCAResult {
406    components: Array2<Float>,
407    mean: Array1<Float>,
408    explained_variance: Array1<Float>,
409}
410
411/// Bootstrap-based component stability assessment
412pub struct BootstrapSelector {
413    config: ComponentSelectionConfig,
414    method: BootstrapMethod,
415}
416
417impl BootstrapSelector {
418    /// Create a new bootstrap selector
419    pub fn new(method: BootstrapMethod) -> Self {
420        Self {
421            config: ComponentSelectionConfig::default(),
422            method,
423        }
424    }
425
426    /// Set configuration
427    pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
428        self.config = config;
429        self
430    }
431
432    /// Set number of bootstrap samples
433    pub fn n_bootstrap(mut self, n_bootstrap: usize) -> Self {
434        self.config.n_bootstrap = n_bootstrap;
435        self
436    }
437
438    /// Assess component stability using bootstrap resampling
439    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        // Fit PCA on original data
455        let original_pca = self.fit_pca(data, n_components)?;
456
457        for _ in 0..self.config.n_bootstrap {
458            // Create bootstrap sample
459            let bootstrap_data = self.create_bootstrap_sample(data, &mut rng)?;
460
461            // Fit PCA on bootstrap sample
462            let bootstrap_pca = self.fit_pca(&bootstrap_data, n_components)?;
463
464            // Compute component similarity
465            let similarity = self.compute_component_similarity(
466                &original_pca.components,
467                &bootstrap_pca.components,
468            )?;
469            component_similarities.push(similarity);
470
471            // Compare eigenvalues
472            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    /// Create bootstrap sample
496    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                // Ensure each original sample appears at least once
515                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                // Block bootstrap for time series (simplified)
532                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                // Parametric bootstrap (generate from fitted distribution)
552                // For simplicity, use normal distribution
553                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    /// Fit PCA (simplified version)
572    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        // Center the data
582        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        // Compute covariance matrix
588        let cov_matrix = centered_data.t().dot(&centered_data) / (n_samples - 1) as Float;
589
590        // Simplified eigendecomposition
591        let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
592
593        // Take top n_components
594        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    /// Simplified eigendecomposition
609    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        // Power iteration for largest eigenvalue
618        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        // Fill remaining eigenvalues (simplified)
635        for i in 1..n {
636            eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
637        }
638
639        Ok((eigenvalues, eigenvectors))
640    }
641
642    /// Compute component similarity using correlation
643    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    /// Compute eigenvalue similarity
668    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    /// Compute Pearson correlation
691    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    /// Compute standard deviation
720    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
735/// Information criteria selector
736pub struct InformationCriteriaSelector {
737    config: ComponentSelectionConfig,
738    criterion: InformationCriterion,
739}
740
741impl InformationCriteriaSelector {
742    /// Create a new information criteria selector
743    pub fn new(criterion: InformationCriterion) -> Self {
744        Self {
745            config: ComponentSelectionConfig::default(),
746            criterion,
747        }
748    }
749
750    /// Set configuration
751    pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
752        self.config = config;
753        self
754    }
755
756    /// Select optimal number of components using information criteria
757    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        // Find optimal (minimum for AIC/BIC)
770        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    /// Fit PCA (simplified)
789    fn fit_pca(&self, data: &Array2<Float>, n_components: usize) -> Result<SimplePCAResult> {
790        let (n_samples, _n_features) = data.dim();
791
792        // Center the data
793        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        // Compute covariance matrix
799        let cov_matrix = centered_data.t().dot(&centered_data) / (n_samples - 1) as Float;
800
801        // Simplified eigendecomposition
802        let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
803
804        // Take top n_components
805        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    /// Simplified eigendecomposition
820    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        // Power iteration for largest eigenvalue
829        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        // Fill remaining eigenvalues (simplified)
846        for i in 1..n {
847            eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
848        }
849
850        Ok((eigenvalues, eigenvectors))
851    }
852
853    /// Compute information criterion
854    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        // Compute reconstruction error (negative log-likelihood proxy)
863        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        // Negative log-likelihood (assuming Gaussian errors)
871        let log_likelihood =
872            -0.5 * n_samples as Float * (2.0 * std::f64::consts::PI * mse).ln() as Float;
873
874        // Number of parameters
875        let n_params = n_components * (data.ncols() + 1); // Components + explained variance
876
877        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/// Selection method used
893#[derive(Debug, Clone, Copy)]
894pub enum SelectionMethod {
895    CrossValidation,
896    Bootstrap,
897    InformationCriteria,
898    ParallelAnalysis,
899}
900
901/// Component selection result
902#[derive(Debug, Clone)]
903pub struct ComponentSelectionResult {
904    /// Optimal number of components
905    pub optimal_components: usize,
906    /// Scores for each number of components tested
907    pub cv_scores: Array1<Float>,
908    /// Range of components tested
909    pub component_range: Vec<usize>,
910    /// Selection method used
911    pub method: SelectionMethod,
912    /// Additional metadata
913    pub metadata: HashMap<String, Vec<Float>>,
914}
915
916impl ComponentSelectionResult {
917    /// Get the score for the optimal number of components
918    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    /// Check if the optimal selection is stable (low variance)
924    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; // Threshold for stability
929            }
930        }
931        true
932    }
933}
934
935/// Stability assessment result
936#[derive(Debug, Clone)]
937pub struct StabilityResult {
938    /// Mean component similarity across bootstrap samples
939    pub mean_component_similarity: Float,
940    /// Standard deviation of component similarities
941    pub std_component_similarity: Float,
942    /// Component similarities for each bootstrap sample
943    pub component_similarities: Array1<Float>,
944    /// Eigenvalue stability
945    pub eigenvalue_stability: Float,
946    /// Whether the configuration is considered stable
947    pub is_stable: bool,
948}
949
950impl StabilityResult {
951    /// Get stability confidence (higher is more stable)
952    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
961/// Parallel analysis for factor number determination
962pub struct ParallelAnalysis {
963    config: ComponentSelectionConfig,
964    n_simulations: usize,
965}
966
967impl ParallelAnalysis {
968    /// Create a new parallel analysis instance
969    pub fn new() -> Self {
970        Self {
971            config: ComponentSelectionConfig::default(),
972            n_simulations: 100,
973        }
974    }
975
976    /// Set number of simulations
977    pub fn n_simulations(mut self, n_simulations: usize) -> Self {
978        self.n_simulations = n_simulations;
979        self
980    }
981
982    /// Set random state
983    pub fn random_state(mut self, random_state: u64) -> Self {
984        self.config.random_state = Some(random_state);
985        self
986    }
987
988    /// Perform parallel analysis to determine number of factors
989    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        // Compute eigenvalues for original data
998        let original_eigenvalues = self.compute_eigenvalues(data)?;
999
1000        // Generate random data eigenvalues
1001        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        // Compute 95th percentile of random eigenvalues
1013        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        // Count components where original eigenvalues exceed random eigenvalues
1022        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    /// Compute eigenvalues of covariance matrix
1055    fn compute_eigenvalues(&self, data: &Array2<Float>) -> Result<Array1<Float>> {
1056        let (n_samples, _) = data.dim();
1057
1058        // Center the data
1059        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        // Compute covariance matrix
1065        let cov_matrix = centered_data.t().dot(&centered_data) / (n_samples - 1) as Float;
1066
1067        // Simplified eigendecomposition
1068        let (eigenvalues, _) = self.simple_eigendecomposition(&cov_matrix)?;
1069
1070        Ok(eigenvalues)
1071    }
1072
1073    /// Generate random data with same dimensions
1074    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; // Centered random values
1085            }
1086        }
1087
1088        random_data
1089    }
1090
1091    /// Simplified eigendecomposition
1092    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        // Power iteration for largest eigenvalue
1101        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        // Fill remaining eigenvalues (simplified)
1118        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        // optimal_components is always >= 0 by type (usize)
1210        assert!(result.optimal_components <= 3); // Can't exceed number of features
1211        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        // Check that all indices are covered
1225        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}