use scirs2_core::ndarray::{s, Array1, Array2, Axis, ScalarOperand};
use scirs2_core::numeric::{Float, FromPrimitive};
use std::fmt::Debug;
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 + 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);
Ok(PCAResult {
transformed_data,
components: selected_components,
explained_variance: selected_explained_variance,
explained_variance_ratio,
cumulative_variance_ratio,
mean,
std,
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 + ScalarOperand + 'static,
{
let _n_samples_n_features = data.dim();
compute_pca_eigendecomposition(data, config)
}
#[allow(dead_code)]
pub(super) fn compute_pca_eigendecomposition<F>(
data: &Array2<F>,
config: &PCAConfig,
) -> Result<PCAResultData<F>>
where
F: Float + FromPrimitive + Debug + Clone + ScalarOperand + 'static,
{
let _n_samples_n_features = data.dim();
let covariance = compute_covariance_matrix(data)?;
let (eigenvalues, eigenvectors) = compute_eigendecomposition(&covariance)?;
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_eigenvectors, final_eigenvalues, None))
}
#[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 + 'static,
{
let n = matrix.nrows();
let eigenvalues = Array1::from_shape_fn(n, |i| {
F::from(n - i).expect("Failed to convert to float") });
let eigenvectors = Array2::eye(n);
Ok((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()
}