use scirs2_core::ndarray::{Array1, Array2, Axis};
use scirs2_core::rand_prelude::SliceRandom;
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{rng as make_rng, RngExt, SeedableRng};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub enum CrossValidationMethod {
KFold,
LeaveOneOut,
Stratified,
TimeSeries,
}
#[derive(Debug, Clone, Copy)]
pub enum BootstrapMethod {
Standard,
Balanced,
Block,
Parametric,
}
#[derive(Debug, Clone, Copy)]
pub enum InformationCriterion {
AIC,
BIC,
HQC,
CAIC,
}
#[derive(Debug, Clone)]
pub struct ComponentSelectionConfig {
pub max_components: usize,
pub min_components: usize,
pub n_folds: usize,
pub n_bootstrap: usize,
pub random_state: Option<u64>,
pub tolerance: Float,
}
impl Default for ComponentSelectionConfig {
fn default() -> Self {
Self {
max_components: 20,
min_components: 1,
n_folds: 5,
n_bootstrap: 100,
random_state: None,
tolerance: 1e-6,
}
}
}
pub struct CrossValidationSelector {
config: ComponentSelectionConfig,
method: CrossValidationMethod,
}
impl CrossValidationSelector {
pub fn new(method: CrossValidationMethod) -> Self {
Self {
config: ComponentSelectionConfig::default(),
method,
}
}
pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
self.config = config;
self
}
pub fn max_components(mut self, max_components: usize) -> Self {
self.config.max_components = max_components;
self
}
pub fn n_folds(mut self, n_folds: usize) -> Self {
self.config.n_folds = n_folds;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.config.random_state = Some(random_state);
self
}
pub fn select_pca_components(&self, data: &Array2<Float>) -> Result<ComponentSelectionResult> {
let (n_samples, _n_features) = data.dim();
let max_components = self.config.max_components.min(n_samples);
let mut cv_scores = Vec::new();
let mut reconstruction_errors = Vec::new();
for n_comp in self.config.min_components..=max_components {
let folds = self.create_folds(n_samples)?;
let mut fold_scores = Vec::new();
for (train_idx, test_idx) in folds {
let train_data = self.extract_rows(data, &train_idx);
let test_data = self.extract_rows(data, &test_idx);
let pca_result = self.fit_pca(&train_data, n_comp)?;
let reconstruction_error =
self.compute_pca_reconstruction_error(&test_data, &pca_result)?;
fold_scores.push(reconstruction_error);
}
let mean_score = fold_scores.iter().sum::<Float>() / fold_scores.len() as Float;
let std_score = self.compute_std(&fold_scores, mean_score);
cv_scores.push(mean_score);
reconstruction_errors.push(std_score);
}
let optimal_idx = cv_scores
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0);
let optimal_components = self.config.min_components + optimal_idx;
Ok(ComponentSelectionResult {
optimal_components,
cv_scores: Array1::from_vec(cv_scores),
component_range: (self.config.min_components..=max_components).collect(),
method: SelectionMethod::CrossValidation,
metadata: HashMap::from([("reconstruction_errors".to_string(), reconstruction_errors)]),
})
}
fn create_folds(&self, n_samples: usize) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
let mut rng = if let Some(seed) = self.config.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut make_rng())
};
match self.method {
CrossValidationMethod::KFold => self.k_fold_split(n_samples, &mut rng),
CrossValidationMethod::LeaveOneOut => self.leave_one_out_split(n_samples),
CrossValidationMethod::Stratified => {
self.k_fold_split(n_samples, &mut rng)
}
CrossValidationMethod::TimeSeries => self.time_series_split(n_samples),
}
}
fn k_fold_split(
&self,
n_samples: usize,
rng: &mut impl RngExt,
) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
let mut indices: Vec<usize> = (0..n_samples).collect();
indices.shuffle(rng);
let fold_size = n_samples / self.config.n_folds;
let mut folds = Vec::new();
for fold in 0..self.config.n_folds {
let start = fold * fold_size;
let end = if fold == self.config.n_folds - 1 {
n_samples
} else {
(fold + 1) * fold_size
};
let test_idx: Vec<usize> = indices[start..end].to_vec();
let train_idx: Vec<usize> = indices[..start]
.iter()
.chain(indices[end..].iter())
.cloned()
.collect();
folds.push((train_idx, test_idx));
}
Ok(folds)
}
fn leave_one_out_split(&self, n_samples: usize) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
let mut folds = Vec::new();
for i in 0..n_samples {
let test_idx = vec![i];
let train_idx: Vec<usize> = (0..n_samples).filter(|&x| x != i).collect();
folds.push((train_idx, test_idx));
}
Ok(folds)
}
fn time_series_split(&self, n_samples: usize) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
let min_train_size = n_samples / 3;
let test_size = n_samples / 10;
let mut folds = Vec::new();
for start in min_train_size..n_samples - test_size {
let train_idx: Vec<usize> = (0..start).collect();
let test_idx: Vec<usize> = (start..start + test_size).collect();
folds.push((train_idx, test_idx));
}
Ok(folds)
}
fn extract_rows(&self, data: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
let n_cols = data.ncols();
let mut result = Array2::zeros((indices.len(), n_cols));
for (i, &row_idx) in indices.iter().enumerate() {
for j in 0..n_cols {
result[[i, j]] = data[[row_idx, j]];
}
}
result
}
fn fit_pca(&self, data: &Array2<Float>, n_components: usize) -> Result<SimplePCAResult> {
let (n_samples, n_features) = data.dim();
if n_components > n_features.min(n_samples) {
return Err(SklearsError::InvalidInput(
"n_components too large".to_string(),
));
}
let mean = data
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let centered_data = data - &mean.clone().insert_axis(Axis(0));
let cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
let components = eigenvectors
.slice(scirs2_core::ndarray::s![.., 0..n_components])
.to_owned();
let explained_variance = eigenvalues
.slice(scirs2_core::ndarray::s![0..n_components])
.to_owned();
Ok(SimplePCAResult {
components,
mean,
explained_variance,
})
}
fn simple_eigendecomposition(
&self,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
let mut eigenvalues = Array1::zeros(n);
let mut eigenvectors = Array2::eye(n);
let mut v = Array1::ones(n);
for _ in 0..100 {
let v_new = matrix.dot(&v);
let norm = (v_new.dot(&v_new)).sqrt();
if norm > 1e-12 {
v = v_new / norm;
}
let eigenvalue = v.dot(&matrix.dot(&v));
eigenvalues[0] = eigenvalue;
for i in 0..n {
eigenvectors[[i, 0]] = v[i];
}
}
for i in 1..n {
eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
}
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&i, &j| {
eigenvalues[j]
.partial_cmp(&eigenvalues[i])
.unwrap_or(std::cmp::Ordering::Equal)
});
let sorted_eigenvalues =
Array1::from_vec(indices.iter().map(|&i| eigenvalues[i]).collect());
let sorted_eigenvectors =
Array2::from_shape_fn((n, n), |(i, j)| eigenvectors[[i, indices[j]]]);
Ok((sorted_eigenvalues, sorted_eigenvectors))
}
fn compute_pca_reconstruction_error(
&self,
data: &Array2<Float>,
pca: &SimplePCAResult,
) -> Result<Float> {
let (n_samples, _) = data.dim();
let centered_data = data - &pca.mean.clone().insert_axis(Axis(0));
let projected = centered_data.dot(&pca.components);
let reconstructed =
projected.dot(&pca.components.t()) + &pca.mean.clone().insert_axis(Axis(0));
let diff = data - &reconstructed;
let error = diff.iter().map(|&x| x * x).sum::<Float>() / (n_samples as Float);
Ok(error)
}
fn compute_std(&self, values: &[Float], mean: Float) -> Float {
if values.len() <= 1 {
return 0.0;
}
let variance = values
.iter()
.map(|&x| (x - mean) * (x - mean))
.sum::<Float>()
/ (values.len() - 1) as Float;
variance.sqrt()
}
}
#[derive(Debug, Clone)]
struct SimplePCAResult {
components: Array2<Float>,
mean: Array1<Float>,
explained_variance: Array1<Float>,
}
pub struct BootstrapSelector {
config: ComponentSelectionConfig,
method: BootstrapMethod,
}
impl BootstrapSelector {
pub fn new(method: BootstrapMethod) -> Self {
Self {
config: ComponentSelectionConfig::default(),
method,
}
}
pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
self.config = config;
self
}
pub fn n_bootstrap(mut self, n_bootstrap: usize) -> Self {
self.config.n_bootstrap = n_bootstrap;
self
}
pub fn assess_stability(
&self,
data: &Array2<Float>,
n_components: usize,
) -> Result<StabilityResult> {
let (_n_samples, _) = data.dim();
let mut rng = if let Some(seed) = self.config.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut make_rng())
};
let mut component_similarities = Vec::new();
let mut eigenvalue_stability = Vec::new();
let original_pca = self.fit_pca(data, n_components)?;
for _ in 0..self.config.n_bootstrap {
let bootstrap_data = self.create_bootstrap_sample(data, &mut rng)?;
let bootstrap_pca = self.fit_pca(&bootstrap_data, n_components)?;
let similarity = self.compute_component_similarity(
&original_pca.components,
&bootstrap_pca.components,
)?;
component_similarities.push(similarity);
let eigenvalue_sim = self.compute_eigenvalue_similarity(
&original_pca.explained_variance,
&bootstrap_pca.explained_variance,
);
eigenvalue_stability.push(eigenvalue_sim);
}
let mean_similarity =
component_similarities.iter().sum::<Float>() / component_similarities.len() as Float;
let std_similarity = self.compute_std(&component_similarities, mean_similarity);
let mean_eigenvalue_stability =
eigenvalue_stability.iter().sum::<Float>() / eigenvalue_stability.len() as Float;
Ok(StabilityResult {
mean_component_similarity: mean_similarity,
std_component_similarity: std_similarity,
component_similarities: Array1::from_vec(component_similarities),
eigenvalue_stability: mean_eigenvalue_stability,
is_stable: mean_similarity > 0.8 && std_similarity < 0.2,
})
}
fn create_bootstrap_sample(
&self,
data: &Array2<Float>,
rng: &mut impl RngExt,
) -> Result<Array2<Float>> {
let (n_samples, n_features) = data.dim();
let mut bootstrap_data = Array2::zeros((n_samples, n_features));
match self.method {
BootstrapMethod::Standard => {
for i in 0..n_samples {
let sample_idx = rng.random_range(0..n_samples);
for j in 0..n_features {
bootstrap_data[[i, j]] = data[[sample_idx, j]];
}
}
}
BootstrapMethod::Balanced => {
let mut indices: Vec<usize> = (0..n_samples).collect();
indices.shuffle(rng);
for i in 0..n_samples {
let sample_idx = if i < indices.len() {
indices[i]
} else {
rng.random_range(0..n_samples)
};
for j in 0..n_features {
bootstrap_data[[i, j]] = data[[sample_idx, j]];
}
}
}
BootstrapMethod::Block => {
let block_size = (n_samples as f64).sqrt() as usize;
let n_blocks = n_samples / block_size;
for block in 0..n_blocks {
let start_idx = rng.random_range(0..n_samples - block_size + 1);
for i in 0..block_size {
let src_idx = start_idx + i;
let dst_idx = block * block_size + i;
if dst_idx < n_samples {
for j in 0..n_features {
bootstrap_data[[dst_idx, j]] = data[[src_idx, j]];
}
}
}
}
}
BootstrapMethod::Parametric => {
for j in 0..n_features {
let column = data.column(j);
let mean = column.mean().unwrap_or(0.0);
let std = self.compute_std(
column.as_slice().expect("matrix indexing should be valid"),
mean,
);
for i in 0..n_samples {
bootstrap_data[[i, j]] = rng.random::<Float>() * std + mean;
}
}
}
}
Ok(bootstrap_data)
}
fn fit_pca(&self, data: &Array2<Float>, n_components: usize) -> Result<SimplePCAResult> {
let (n_samples, n_features) = data.dim();
if n_components > n_features.min(n_samples) {
return Err(SklearsError::InvalidInput(
"n_components too large".to_string(),
));
}
let mean = data
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let centered_data = data - &mean.clone().insert_axis(Axis(0));
let cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
let components = eigenvectors
.slice(scirs2_core::ndarray::s![.., 0..n_components])
.to_owned();
let explained_variance = eigenvalues
.slice(scirs2_core::ndarray::s![0..n_components])
.to_owned();
Ok(SimplePCAResult {
components,
mean,
explained_variance,
})
}
fn simple_eigendecomposition(
&self,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
let mut eigenvalues = Array1::zeros(n);
let mut eigenvectors = Array2::eye(n);
let mut v = Array1::ones(n);
for _ in 0..100 {
let v_new = matrix.dot(&v);
let norm = (v_new.dot(&v_new)).sqrt();
if norm > 1e-12 {
v = v_new / norm;
}
let eigenvalue = v.dot(&matrix.dot(&v));
eigenvalues[0] = eigenvalue;
for i in 0..n {
eigenvectors[[i, 0]] = v[i];
}
}
for i in 1..n {
eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
}
Ok((eigenvalues, eigenvectors))
}
fn compute_component_similarity(
&self,
components1: &Array2<Float>,
components2: &Array2<Float>,
) -> Result<Float> {
let (_, n_comp1) = components1.dim();
let (_, n_comp2) = components2.dim();
let n_comp = n_comp1.min(n_comp2);
let mut total_similarity = 0.0;
for i in 0..n_comp {
let comp1 = components1.column(i);
let comp2 = components2.column(i);
let correlation = self
.compute_correlation(&comp1.to_owned(), &comp2.to_owned())
.abs();
total_similarity += correlation;
}
Ok(total_similarity / n_comp as Float)
}
fn compute_eigenvalue_similarity(
&self,
eigenvals1: &Array1<Float>,
eigenvals2: &Array1<Float>,
) -> Float {
let n = eigenvals1.len().min(eigenvals2.len());
if n == 0 {
return 0.0;
}
let mut similarity = 0.0;
for i in 0..n {
let diff = (eigenvals1[i] - eigenvals2[i]).abs();
let avg = (eigenvals1[i] + eigenvals2[i]) / 2.0;
if avg > 1e-12 {
similarity += 1.0 - diff / avg;
}
}
similarity / n as Float
}
fn compute_correlation(&self, x: &Array1<Float>, y: &Array1<Float>) -> Float {
let n = x.len();
if n != y.len() || n == 0 {
return 0.0;
}
let mean_x = x.mean().unwrap_or(0.0);
let mean_y = y.mean().unwrap_or(0.0);
let mut num = 0.0;
let mut den_x = 0.0;
let mut den_y = 0.0;
for i in 0..n {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
num += dx * dy;
den_x += dx * dx;
den_y += dy * dy;
}
if den_x == 0.0 || den_y == 0.0 {
return 0.0;
}
num / (den_x * den_y).sqrt()
}
fn compute_std(&self, values: &[Float], mean: Float) -> Float {
if values.len() <= 1 {
return 0.0;
}
let variance = values
.iter()
.map(|&x| (x - mean) * (x - mean))
.sum::<Float>()
/ (values.len() - 1) as Float;
variance.sqrt()
}
}
pub struct InformationCriteriaSelector {
config: ComponentSelectionConfig,
criterion: InformationCriterion,
}
impl InformationCriteriaSelector {
pub fn new(criterion: InformationCriterion) -> Self {
Self {
config: ComponentSelectionConfig::default(),
criterion,
}
}
pub fn config(mut self, config: ComponentSelectionConfig) -> Self {
self.config = config;
self
}
pub fn select_components(&self, data: &Array2<Float>) -> Result<ComponentSelectionResult> {
let (n_samples, n_features) = data.dim();
let max_components = self.config.max_components.min(n_features.min(n_samples));
let mut ic_scores = Vec::new();
for n_comp in self.config.min_components..=max_components {
let pca_result = self.fit_pca(data, n_comp)?;
let ic_score = self.compute_information_criterion(data, &pca_result, n_comp)?;
ic_scores.push(ic_score);
}
let optimal_idx = ic_scores
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0);
let optimal_components = self.config.min_components + optimal_idx;
Ok(ComponentSelectionResult {
optimal_components,
cv_scores: Array1::from_vec(ic_scores),
component_range: (self.config.min_components..=max_components).collect(),
method: SelectionMethod::InformationCriteria,
metadata: HashMap::new(),
})
}
fn fit_pca(&self, data: &Array2<Float>, n_components: usize) -> Result<SimplePCAResult> {
let (n_samples, _n_features) = data.dim();
let mean = data
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let centered_data = data - &mean.clone().insert_axis(Axis(0));
let cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
let (eigenvalues, eigenvectors) = self.simple_eigendecomposition(&cov_matrix)?;
let components = eigenvectors
.slice(scirs2_core::ndarray::s![.., 0..n_components])
.to_owned();
let explained_variance = eigenvalues
.slice(scirs2_core::ndarray::s![0..n_components])
.to_owned();
Ok(SimplePCAResult {
components,
mean,
explained_variance,
})
}
fn simple_eigendecomposition(
&self,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
let mut eigenvalues = Array1::zeros(n);
let mut eigenvectors = Array2::eye(n);
let mut v = Array1::ones(n);
for _ in 0..100 {
let v_new = matrix.dot(&v);
let norm = (v_new.dot(&v_new)).sqrt();
if norm > 1e-12 {
v = v_new / norm;
}
let eigenvalue = v.dot(&matrix.dot(&v));
eigenvalues[0] = eigenvalue;
for i in 0..n {
eigenvectors[[i, 0]] = v[i];
}
}
for i in 1..n {
eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
}
Ok((eigenvalues, eigenvectors))
}
fn compute_information_criterion(
&self,
data: &Array2<Float>,
pca: &SimplePCAResult,
n_components: usize,
) -> Result<Float> {
let (n_samples, _) = data.dim();
let centered_data = data - &pca.mean.clone().insert_axis(Axis(0));
let projected = centered_data.dot(&pca.components);
let reconstructed =
projected.dot(&pca.components.t()) + &pca.mean.clone().insert_axis(Axis(0));
let diff = data - &reconstructed;
let mse = diff.iter().map(|&x| x * x).sum::<Float>() / (n_samples as Float);
let log_likelihood =
-0.5 * n_samples as Float * (2.0 * std::f64::consts::PI * mse).ln() as Float;
let n_params = n_components * (data.ncols() + 1);
Ok(match self.criterion {
InformationCriterion::AIC => -2.0 * log_likelihood + 2.0 * n_params as Float,
InformationCriterion::BIC => {
-2.0 * log_likelihood + (n_params as Float) * (n_samples as Float).ln()
}
InformationCriterion::HQC => {
-2.0 * log_likelihood + 2.0 * (n_params as Float) * (n_samples as Float).ln().ln()
}
InformationCriterion::CAIC => {
-2.0 * log_likelihood + (n_params as Float) * ((n_samples as Float).ln() + 1.0)
}
})
}
}
#[derive(Debug, Clone, Copy)]
pub enum SelectionMethod {
CrossValidation,
Bootstrap,
InformationCriteria,
ParallelAnalysis,
}
#[derive(Debug, Clone)]
pub struct ComponentSelectionResult {
pub optimal_components: usize,
pub cv_scores: Array1<Float>,
pub component_range: Vec<usize>,
pub method: SelectionMethod,
pub metadata: HashMap<String, Vec<Float>>,
}
impl ComponentSelectionResult {
pub fn optimal_score(&self) -> Float {
let optimal_idx = self.optimal_components - self.component_range[0];
self.cv_scores[optimal_idx]
}
pub fn is_stable(&self) -> bool {
if let Some(errors) = self.metadata.get("reconstruction_errors") {
let optimal_idx = self.optimal_components - self.component_range[0];
if optimal_idx < errors.len() {
return errors[optimal_idx] < 0.1; }
}
true
}
}
#[derive(Debug, Clone)]
pub struct StabilityResult {
pub mean_component_similarity: Float,
pub std_component_similarity: Float,
pub component_similarities: Array1<Float>,
pub eigenvalue_stability: Float,
pub is_stable: bool,
}
impl StabilityResult {
pub fn stability_confidence(&self) -> Float {
if self.std_component_similarity > 0.0 {
self.mean_component_similarity / self.std_component_similarity
} else {
self.mean_component_similarity
}
}
}
pub struct ParallelAnalysis {
config: ComponentSelectionConfig,
n_simulations: usize,
}
impl ParallelAnalysis {
pub fn new() -> Self {
Self {
config: ComponentSelectionConfig::default(),
n_simulations: 100,
}
}
pub fn n_simulations(mut self, n_simulations: usize) -> Self {
self.n_simulations = n_simulations;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.config.random_state = Some(random_state);
self
}
pub fn analyze(&self, data: &Array2<Float>) -> Result<ComponentSelectionResult> {
let (n_samples, n_features) = data.dim();
let mut rng = if let Some(seed) = self.config.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut make_rng())
};
let original_eigenvalues = self.compute_eigenvalues(data)?;
let mut random_eigenvalues = Array2::zeros((self.n_simulations, n_features));
for sim in 0..self.n_simulations {
let random_data = self.generate_random_data(n_samples, n_features, &mut rng);
let eigenvals = self.compute_eigenvalues(&random_data)?;
for (i, &val) in eigenvals.iter().enumerate() {
random_eigenvalues[[sim, i]] = val;
}
}
let mut percentile_eigenvalues = Array1::zeros(n_features);
for i in 0..n_features {
let mut column: Vec<Float> = random_eigenvalues.column(i).to_vec();
column.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let percentile_idx = (0.95 * self.n_simulations as Float) as usize;
percentile_eigenvalues[i] = column[percentile_idx.min(column.len() - 1)];
}
let mut optimal_components = 0;
let mut scores = Vec::new();
for i in 0..n_features {
let difference = original_eigenvalues[i] - percentile_eigenvalues[i];
scores.push(difference);
if original_eigenvalues[i] > percentile_eigenvalues[i] {
optimal_components = i + 1;
} else {
break;
}
}
Ok(ComponentSelectionResult {
optimal_components,
cv_scores: Array1::from_vec(scores),
component_range: (1..=n_features).collect(),
method: SelectionMethod::ParallelAnalysis,
metadata: HashMap::from([
(
"original_eigenvalues".to_string(),
original_eigenvalues.to_vec(),
),
(
"random_eigenvalues_95th".to_string(),
percentile_eigenvalues.to_vec(),
),
]),
})
}
fn compute_eigenvalues(&self, data: &Array2<Float>) -> Result<Array1<Float>> {
let (n_samples, _) = data.dim();
let mean = data
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let centered_data = data - &mean.clone().insert_axis(Axis(0));
let cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
let (eigenvalues, _) = self.simple_eigendecomposition(&cov_matrix)?;
Ok(eigenvalues)
}
fn generate_random_data(
&self,
n_samples: usize,
n_features: usize,
rng: &mut impl RngExt,
) -> Array2<Float> {
let mut random_data = Array2::zeros((n_samples, n_features));
for i in 0..n_samples {
for j in 0..n_features {
random_data[[i, j]] = rng.random::<Float>() - 0.5; }
}
random_data
}
fn simple_eigendecomposition(
&self,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
let mut eigenvalues = Array1::zeros(n);
let mut eigenvectors = Array2::eye(n);
let mut v = Array1::ones(n);
for _ in 0..100 {
let v_new = matrix.dot(&v);
let norm = (v_new.dot(&v_new)).sqrt();
if norm > 1e-12 {
v = v_new / norm;
}
let eigenvalue = v.dot(&matrix.dot(&v));
eigenvalues[0] = eigenvalue;
for i in 0..n {
eigenvectors[[i, 0]] = v[i];
}
}
for i in 1..n {
eigenvalues[i] = eigenvalues[0] / (i + 1) as Float;
}
Ok((eigenvalues, eigenvectors))
}
}
impl Default for ParallelAnalysis {
fn default() -> Self {
Self::new()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_cross_validation_selector() {
let data = array![
[1.0, 2.0, 3.0],
[2.0, 3.0, 4.0],
[3.0, 4.0, 5.0],
[4.0, 5.0, 6.0],
[5.0, 6.0, 7.0],
];
let selector = CrossValidationSelector::new(CrossValidationMethod::KFold)
.max_components(2)
.n_folds(3)
.random_state(42);
let result = selector
.select_pca_components(&data)
.expect("operation should succeed");
assert!(result.optimal_components >= 1);
assert!(result.optimal_components <= 2);
assert_eq!(result.cv_scores.len(), 2);
}
#[test]
fn test_bootstrap_selector() {
let data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0], [5.0, 6.0],];
let selector = BootstrapSelector::new(BootstrapMethod::Standard).n_bootstrap(10);
let result = selector
.assess_stability(&data, 1)
.expect("operation should succeed");
assert!(result.mean_component_similarity >= 0.0);
assert!(result.mean_component_similarity <= 1.0);
assert_eq!(result.component_similarities.len(), 10);
}
#[test]
fn test_information_criteria_selector() {
let data = array![
[1.0, 2.0, 3.0],
[2.0, 3.0, 4.0],
[3.0, 4.0, 5.0],
[4.0, 5.0, 6.0],
];
let selector = InformationCriteriaSelector::new(InformationCriterion::AIC);
let result = selector
.select_components(&data)
.expect("operation should succeed");
assert!(result.optimal_components >= 1);
assert!(!result.cv_scores.is_empty());
}
#[test]
fn test_parallel_analysis() {
let data = array![
[1.0, 2.0, 3.0],
[2.0, 3.0, 4.0],
[3.0, 4.0, 5.0],
[4.0, 5.0, 6.0],
[5.0, 6.0, 7.0],
];
let pa = ParallelAnalysis::new().n_simulations(10).random_state(42);
let result = pa.analyze(&data).expect("operation should succeed");
assert!(result.optimal_components <= 3); assert_eq!(result.cv_scores.len(), 3);
}
#[test]
fn test_k_fold_split() {
let selector = CrossValidationSelector::new(CrossValidationMethod::KFold)
.n_folds(3)
.random_state(42);
let folds = selector.create_folds(10).expect("operation should succeed");
assert_eq!(folds.len(), 3);
let mut all_test_indices: Vec<usize> = Vec::new();
for (_, test_idx) in &folds {
all_test_indices.extend(test_idx);
}
all_test_indices.sort();
let expected_indices: Vec<usize> = (0..10).collect();
assert_eq!(all_test_indices, expected_indices);
}
#[test]
fn test_leave_one_out_split() {
let selector = CrossValidationSelector::new(CrossValidationMethod::LeaveOneOut);
let folds = selector.create_folds(5).expect("operation should succeed");
assert_eq!(folds.len(), 5);
for (i, (train_idx, test_idx)) in folds.iter().enumerate() {
assert_eq!(test_idx.len(), 1);
assert_eq!(test_idx[0], i);
assert_eq!(train_idx.len(), 4);
assert!(!train_idx.contains(&i));
}
}
}