use scirs2_core::ndarray::{s, Array1, Array2, Axis, ScalarOperand};
use scirs2_core::numeric::{Float, FromPrimitive, NumAssign};
use std::fmt::Debug;
use std::iter::Sum;
use crate::error::{Result, TimeSeriesError};
pub(super) type PCAResultData<F> = (Array2<F>, Array1<F>, Option<Array1<F>>);
#[derive(Debug, Clone)]
pub struct PCAConfig {
pub n_components: Option<usize>,
pub center_data: bool,
pub scale_data: bool,
pub min_variance_ratio: f64,
pub max_cumulative_variance: f64,
pub use_svd: bool,
pub eigenvalue_tolerance: f64,
pub sort_components: bool,
}
impl Default for PCAConfig {
fn default() -> Self {
Self {
n_components: None,
center_data: true,
scale_data: false,
min_variance_ratio: 0.01,
max_cumulative_variance: 0.95,
use_svd: true,
eigenvalue_tolerance: 1e-10,
sort_components: true,
}
}
}
#[derive(Debug, Clone)]
pub struct PCAResult<F> {
pub transformed_data: Array2<F>,
pub components: Array2<F>,
pub explained_variance: Array1<F>,
pub explained_variance_ratio: Array1<F>,
pub cumulative_variance_ratio: Array1<F>,
pub mean: Array1<F>,
pub std: Array1<F>,
pub singular_values: Option<Array1<F>>,
pub n_components_selected: usize,
}
#[allow(dead_code)]
pub fn apply_pca<F>(data: &Array2<F>, config: &PCAConfig) -> Result<PCAResult<F>>
where
F: Float
+ FromPrimitive
+ Debug
+ Clone
+ NumAssign
+ Sum
+ Send
+ Sync
+ ScalarOperand
+ 'static,
{
use scirs2_core::ndarray::ArrayStatCompat;
let (n_samples, n_features) = data.dim();
if n_samples == 0 || n_features == 0 {
return Err(TimeSeriesError::InvalidInput(
"Data matrix cannot be empty".to_string(),
));
}
let mut processed_data = data.clone();
let mean = if config.center_data {
let mean = data.mean_axis(Axis(0)).expect("Operation failed");
for mut row in processed_data.axis_iter_mut(Axis(0)) {
for (j, &mean_val) in mean.iter().enumerate() {
row[j] = row[j] - mean_val;
}
}
mean
} else {
Array1::zeros(n_features)
};
let std = if config.scale_data {
let std = data.std_axis(Axis(0), F::zero());
for mut row in processed_data.axis_iter_mut(Axis(0)) {
for (i, val) in row.iter_mut().enumerate() {
if std[i] > F::from(1e-10).expect("Failed to convert constant to float") {
*val = *val / std[i];
}
}
}
std
} else {
Array1::ones(n_features)
};
let (components, explained_variance, singular_values) =
if config.use_svd || n_features > n_samples {
compute_pca_svd(&processed_data, config)?
} else {
compute_pca_eigendecomposition(&processed_data, config)?
};
let n_components = determine_n_components(&explained_variance, config);
let selected_components = components.slice(s![.., ..n_components]).to_owned();
let selected_explained_variance = explained_variance.slice(s![..n_components]).to_owned();
let total_variance = explained_variance.sum();
let explained_variance_ratio = selected_explained_variance.mapv(|x| x / total_variance);
let mut cumulative_variance_ratio = Array1::zeros(n_components);
let mut cumsum = F::zero();
for i in 0..n_components {
cumsum = cumsum + explained_variance_ratio[i];
cumulative_variance_ratio[i] = cumsum;
}
let transformed_data = processed_data.dot(&selected_components);
let selected_singular_values = singular_values.map(|sv| {
let k = n_components.min(sv.len());
sv.slice(s![..k]).to_owned()
});
Ok(PCAResult {
transformed_data,
components: selected_components,
explained_variance: selected_explained_variance,
explained_variance_ratio,
cumulative_variance_ratio,
mean,
std,
singular_values: selected_singular_values,
n_components_selected: n_components,
})
}
#[allow(dead_code)]
pub(super) fn compute_pca_svd<F>(data: &Array2<F>, config: &PCAConfig) -> Result<PCAResultData<F>>
where
F: Float
+ FromPrimitive
+ Debug
+ Clone
+ NumAssign
+ Sum
+ Send
+ Sync
+ ScalarOperand
+ 'static,
{
let (n_samples, _n_features) = data.dim();
let n_samples_f = F::from(n_samples)
.ok_or_else(|| TimeSeriesError::ComputationError("Invalid sample count".to_string()))?;
let (_u, singular_values, vt) = scirs2_linalg::svd(&data.view(), false, None)
.map_err(|e| TimeSeriesError::ComputationError(format!("SVD computation failed: {e}")))?;
let eigenvalues = singular_values.mapv(|sv| (sv * sv) / n_samples_f);
let eigenvectors = vt.t().to_owned();
let (final_eigenvalues, final_eigenvectors) =
select_significant_components(eigenvalues, eigenvectors, config)?;
let final_singular_values =
final_eigenvalues.mapv(|ev| (ev * n_samples_f).max(F::zero()).sqrt());
Ok((
final_eigenvectors,
final_eigenvalues,
Some(final_singular_values),
))
}
#[allow(dead_code)]
pub(super) fn compute_pca_eigendecomposition<F>(
data: &Array2<F>,
config: &PCAConfig,
) -> Result<PCAResultData<F>>
where
F: Float
+ FromPrimitive
+ Debug
+ Clone
+ NumAssign
+ Sum
+ Send
+ Sync
+ ScalarOperand
+ 'static,
{
let covariance = compute_covariance_matrix(data)?;
let (eigenvalues, eigenvectors) = compute_eigendecomposition(&covariance)?;
let (final_eigenvalues, final_eigenvectors) =
select_significant_components(eigenvalues, eigenvectors, config)?;
Ok((final_eigenvectors, final_eigenvalues, None))
}
fn select_significant_components<F>(
eigenvalues: Array1<F>,
eigenvectors: Array2<F>,
config: &PCAConfig,
) -> Result<(Array1<F>, Array2<F>)>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let (sorted_eigenvalues, sorted_eigenvectors) = if config.sort_components {
sort_eigen_pairs(eigenvalues, eigenvectors)?
} else {
(eigenvalues, eigenvectors)
};
let tolerance = F::from(config.eigenvalue_tolerance).expect("Failed to convert to float");
let mut valid_components = 0;
for &eigenval in sorted_eigenvalues.iter() {
if eigenval > tolerance {
valid_components += 1;
} else {
break;
}
}
let final_eigenvalues = sorted_eigenvalues.slice(s![..valid_components]).to_owned();
let final_eigenvectors = sorted_eigenvectors
.slice(s![.., ..valid_components])
.to_owned();
Ok((final_eigenvalues, final_eigenvectors))
}
#[allow(dead_code)]
pub(super) fn compute_covariance_matrix<F>(data: &Array2<F>) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + ScalarOperand + 'static,
{
let (n_samples, _n_features) = data.dim();
let n_samples_f = F::from(n_samples).expect("Failed to convert to float");
let covariance = data.t().dot(data) / n_samples_f;
Ok(covariance)
}
#[allow(dead_code)]
pub(super) fn compute_eigendecomposition<F>(matrix: &Array2<F>) -> Result<(Array1<F>, Array2<F>)>
where
F: Float
+ FromPrimitive
+ Debug
+ Clone
+ NumAssign
+ Sum
+ Send
+ Sync
+ ScalarOperand
+ 'static,
{
let n = matrix.nrows();
if n == 0 || matrix.ncols() != n {
return Err(TimeSeriesError::InvalidInput(format!(
"compute_eigendecomposition requires a square matrix, got shape {:?}",
matrix.dim()
)));
}
let two = F::one() + F::one();
let mut symmetric = matrix.clone();
for i in 0..n {
for j in (i + 1)..n {
let avg = (symmetric[[i, j]] + symmetric[[j, i]]) / two;
symmetric[[i, j]] = avg;
symmetric[[j, i]] = avg;
}
}
let (eigenvalues, eigenvectors) =
scirs2_linalg::eigh(&symmetric.view(), None).map_err(|e| {
TimeSeriesError::ComputationError(format!("Eigendecomposition failed: {e}"))
})?;
sort_eigen_pairs(eigenvalues, eigenvectors)
}
#[allow(dead_code)]
fn sort_eigen_pairs<F>(
eigenvalues: Array1<F>,
eigenvectors: Array2<F>,
) -> Result<(Array1<F>, Array2<F>)>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let n = eigenvalues.len();
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_shape_fn(n, |i| eigenvalues[indices[i]]);
let sorted_eigenvectors = Array2::from_shape_fn((eigenvectors.nrows(), n), |(i, j)| {
eigenvectors[(i, indices[j])]
});
Ok((sorted_eigenvalues, sorted_eigenvectors))
}
#[allow(dead_code)]
pub(super) fn determine_n_components<F>(_explainedvariance: &Array1<F>, config: &PCAConfig) -> usize
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let total_variance = _explainedvariance.sum();
let min_variance_ratio =
F::from(config.min_variance_ratio).expect("Failed to convert to float");
let max_cumulative_variance =
F::from(config.max_cumulative_variance).expect("Failed to convert to float");
if let Some(n) = config.n_components {
return std::cmp::min(n, _explainedvariance.len());
}
let mut cumulative_variance = F::zero();
for (i, &_variance) in _explainedvariance.iter().enumerate() {
let variance_ratio = _variance / total_variance;
if variance_ratio < min_variance_ratio {
return i;
}
cumulative_variance = cumulative_variance + variance_ratio;
if cumulative_variance >= max_cumulative_variance {
return i + 1;
}
}
_explainedvariance.len()
}