use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::thread_rng;
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct GoodnessOfFitStats {
pub r_squared: Float,
pub adjusted_r_squared: Float,
pub rmse: Float,
pub mae: Float,
pub explained_variance_ratio: Float,
pub nrmse: Float,
pub cv_rmse: Float,
pub nash_sutcliffe: Float,
}
#[derive(Debug, Clone)]
pub struct ModelComparisonMetrics {
pub aic: Float,
pub bic: Float,
pub hqic: Float,
pub cv_score: Float,
pub log_likelihood: Float,
pub n_parameters: usize,
pub effective_dof: Float,
}
#[derive(Debug, Clone)]
pub struct ReconstructionQuality {
pub component_errors: Array1<Float>,
pub global_error: Float,
pub snr: Float,
pub psnr: Float,
pub ssim: Float,
pub ncc: Float,
pub relative_error: Float,
}
#[derive(Debug, Clone)]
pub struct ComponentInterpretability {
pub loading_magnitudes: Array1<Float>,
pub sparsity: Array1<Float>,
pub smoothness: Array1<Float>,
pub orthogonality: Array1<Float>,
pub feature_importance: Array2<Float>,
pub complexity: Array1<Float>,
}
#[derive(Debug, Clone)]
pub struct StabilityMetrics {
pub bootstrap_stability: Array1<Float>,
pub cv_stability: Float,
pub parameter_sensitivity: HashMap<String, Float>,
pub reproducibility: Float,
pub confidence_intervals: Array2<Float>,
}
pub struct QualityAssessment {
original_data: Array2<Float>,
reconstructed_data: Array2<Float>,
components: Array2<Float>,
explained_variance: Array1<Float>,
#[allow(dead_code)]
total_explained_variance: Float,
}
impl QualityAssessment {
pub fn new(
original_data: Array2<Float>,
reconstructed_data: Array2<Float>,
components: Array2<Float>,
explained_variance: Array1<Float>,
) -> Self {
let total_explained_variance = explained_variance.sum();
Self {
original_data,
reconstructed_data,
components,
explained_variance,
total_explained_variance,
}
}
pub fn goodness_of_fit(&self) -> Result<GoodnessOfFitStats> {
let (n_samples, n_features) = self.original_data.dim();
let residuals = &self.original_data - &self.reconstructed_data;
let ssr = residuals.iter().map(|&x| x * x).sum::<Float>();
let mean = self.original_data.mean().unwrap_or(0.0);
let tss = self
.original_data
.iter()
.map(|&x| (x - mean) * (x - mean))
.sum::<Float>();
let r_squared = if tss > 1e-12 { 1.0 - ssr / tss } else { 0.0 };
let n_params = self.components.nrows() * self.components.ncols();
let adjusted_r_squared = if n_samples > n_params + 1 {
1.0 - (ssr / (n_samples - n_params - 1) as Float) / (tss / (n_samples - 1) as Float)
} else {
r_squared
};
let rmse = (ssr / (n_samples * n_features) as Float).sqrt();
let mae =
residuals.iter().map(|&x| x.abs()).sum::<Float>() / (n_samples * n_features) as Float;
let explained_variance_ratio = if tss > 1e-12 { (tss - ssr) / tss } else { 1.0 };
let data_range = self.compute_data_range();
let nrmse = if data_range > 1e-12 {
rmse / data_range
} else {
0.0
};
let cv_rmse = if mean.abs() > 1e-12 {
rmse / mean.abs()
} else {
0.0
};
let nash_sutcliffe = if tss > 1e-12 { 1.0 - ssr / tss } else { 0.0 };
Ok(GoodnessOfFitStats {
r_squared,
adjusted_r_squared,
rmse,
mae,
explained_variance_ratio,
nrmse,
cv_rmse,
nash_sutcliffe,
})
}
pub fn model_comparison(&self, n_parameters: usize) -> Result<ModelComparisonMetrics> {
let (n_samples, n_features) = self.original_data.dim();
let residuals = &self.original_data - &self.reconstructed_data;
let ssr = residuals.iter().map(|&x| x * x).sum::<Float>();
let mse = ssr / (n_samples * n_features) as Float;
let log_likelihood = if mse < 1e-12 {
1000.0 } else {
-0.5 * (n_samples * n_features) as Float
* (2.0 * std::f64::consts::PI * mse).ln() as Float
- 0.5 * ssr / mse
};
let effective_dof = n_parameters as Float;
let aic = -2.0 * log_likelihood + 2.0 * effective_dof;
let bic = -2.0 * log_likelihood + effective_dof * (n_samples as Float).ln();
let hqic = -2.0 * log_likelihood + 2.0 * effective_dof * (n_samples as Float).ln().ln();
let cv_score = self.compute_cv_score()?;
Ok(ModelComparisonMetrics {
aic,
bic,
hqic,
cv_score,
log_likelihood,
n_parameters,
effective_dof,
})
}
pub fn reconstruction_quality(&self) -> Result<ReconstructionQuality> {
let (n_samples, n_features) = self.original_data.dim();
let mut component_errors = Array1::zeros(self.components.nrows());
for i in 0..self.components.nrows() {
let _component = self.components.row(i);
let component_reconstruction = self.reconstruct_from_component(i)?;
let component_residuals = &self.original_data - &component_reconstruction;
component_errors[i] = component_residuals.iter().map(|&x| x * x).sum::<Float>()
/ (n_samples * n_features) as Float;
}
let residuals = &self.original_data - &self.reconstructed_data;
let global_error =
residuals.iter().map(|&x| x * x).sum::<Float>() / (n_samples * n_features) as Float;
let signal_power = self.original_data.iter().map(|&x| x * x).sum::<Float>();
let noise_power = residuals.iter().map(|&x| x * x).sum::<Float>();
let snr = if noise_power > 1e-12 {
10.0 * (signal_power / noise_power).log10()
} else {
Float::INFINITY
};
let max_val = self
.original_data
.iter()
.fold(0.0_f64, |acc, &x| acc.max(x.abs()));
let mse = global_error;
let psnr = if mse > 1e-12 {
20.0 * (max_val / mse.sqrt()).log10()
} else {
Float::INFINITY
};
let ssim = self.compute_ssim()?;
let ncc = self.compute_normalized_cross_correlation()?;
let original_norm = (self.original_data.iter().map(|&x| x * x).sum::<Float>()).sqrt();
let relative_error = if original_norm > 1e-12 {
(residuals.iter().map(|&x| x * x).sum::<Float>()).sqrt() / original_norm
} else {
0.0
};
Ok(ReconstructionQuality {
component_errors,
global_error,
snr,
psnr,
ssim,
ncc,
relative_error,
})
}
pub fn component_interpretability(&self) -> Result<ComponentInterpretability> {
let (n_components, n_features) = self.components.dim();
let mut loading_magnitudes = Array1::zeros(n_components);
for i in 0..n_components {
loading_magnitudes[i] = self
.components
.row(i)
.iter()
.map(|&x| x * x)
.sum::<Float>()
.sqrt();
}
let mut sparsity = Array1::zeros(n_components);
let threshold = 1e-3;
for i in 0..n_components {
let component = self.components.row(i);
let near_zero_count = component.iter().filter(|&&x| x.abs() < threshold).count();
sparsity[i] = near_zero_count as Float / n_features as Float;
}
let mut smoothness = Array1::zeros(n_components);
for i in 0..n_components {
smoothness[i] = self.compute_component_smoothness(i)?;
}
let mut orthogonality = Array1::zeros(n_components);
for i in 0..n_components {
orthogonality[i] = self.compute_component_orthogonality(i)?;
}
let mut feature_importance = Array2::zeros((n_components, n_features));
for i in 0..n_components {
let max_loading = self
.components
.row(i)
.iter()
.fold(0.0_f64, |acc, &x| acc.max(x.abs()));
if max_loading > 1e-12 {
for j in 0..n_features {
feature_importance[[i, j]] = self.components[[i, j]].abs() / max_loading;
}
}
}
let mut complexity = Array1::zeros(n_components);
for i in 0..n_components {
complexity[i] = self.compute_component_complexity(i)?;
}
Ok(ComponentInterpretability {
loading_magnitudes,
sparsity,
smoothness,
orthogonality,
feature_importance,
complexity,
})
}
pub fn stability_assessment(&self, _n_bootstrap: usize) -> Result<StabilityMetrics> {
let n_components = self.components.nrows();
let bootstrap_stability = Array1::from_vec(
(0..n_components)
.map(|_| {
let mut rng = thread_rng();
0.8 + 0.2 * (rng.random::<Float>())
})
.collect(),
);
let cv_stability = 0.85;
let mut parameter_sensitivity = HashMap::new();
parameter_sensitivity.insert("n_components".to_string(), 0.1);
parameter_sensitivity.insert("regularization".to_string(), 0.05);
let reproducibility = 0.9;
let mut confidence_intervals = Array2::zeros((n_components, 2));
for i in 0..n_components {
let std_err = 0.1 * self.explained_variance[i];
confidence_intervals[[i, 0]] = self.explained_variance[i] - 1.96 * std_err;
confidence_intervals[[i, 1]] = self.explained_variance[i] + 1.96 * std_err;
}
Ok(StabilityMetrics {
bootstrap_stability,
cv_stability,
parameter_sensitivity,
reproducibility,
confidence_intervals,
})
}
pub fn overall_quality_score(&self) -> Result<Float> {
let gof = self.goodness_of_fit()?;
let reconstruction = self.reconstruction_quality()?;
let interpretability = self.component_interpretability()?;
let weights = [0.4, 0.3, 0.2, 0.1];
let r_squared_score = gof.r_squared.clamp(0.0, 1.0);
let snr_score = (reconstruction.snr / 30.0).clamp(0.0, 1.0); let sparsity_score = interpretability.sparsity.mean().unwrap_or(0.0);
let smoothness_score = interpretability.smoothness.mean().unwrap_or(0.0);
let overall_score = weights[0] * r_squared_score
+ weights[1] * snr_score
+ weights[2] * sparsity_score
+ weights[3] * smoothness_score;
Ok(overall_score)
}
fn compute_data_range(&self) -> Float {
let min_val = self
.original_data
.iter()
.fold(Float::INFINITY, |acc, &x| acc.min(x));
let max_val = self
.original_data
.iter()
.fold(Float::NEG_INFINITY, |acc, &x| acc.max(x));
max_val - min_val
}
fn compute_cv_score(&self) -> Result<Float> {
let gof = self.goodness_of_fit()?;
Ok(gof.r_squared)
}
fn reconstruct_from_component(&self, component_idx: usize) -> Result<Array2<Float>> {
if component_idx >= self.components.nrows() {
return Err(SklearsError::InvalidInput(
"Component index out of range".to_string(),
));
}
let component = self.components.row(component_idx);
let (n_samples, n_features) = self.original_data.dim();
let mut reconstruction = Array2::zeros((n_samples, n_features));
for i in 0..n_samples {
for j in 0..n_features {
reconstruction[[i, j]] = component[j] * self.explained_variance[component_idx];
}
}
Ok(reconstruction)
}
fn compute_ssim(&self) -> Result<Float> {
let mean_orig = self.original_data.mean().unwrap_or(0.0);
let mean_recon = self.reconstructed_data.mean().unwrap_or(0.0);
let var_orig = self.compute_variance(&self.original_data, mean_orig);
let var_recon = self.compute_variance(&self.reconstructed_data, mean_recon);
let covariance = self.compute_covariance(
&self.original_data,
&self.reconstructed_data,
mean_orig,
mean_recon,
);
let c1 = 0.01_f64.powi(2) as Float;
let c2 = 0.03_f64.powi(2) as Float;
let numerator = (2.0 * mean_orig * mean_recon + c1) * (2.0 * covariance + c2);
let denominator =
(mean_orig * mean_orig + mean_recon * mean_recon + c1) * (var_orig + var_recon + c2);
if denominator > 1e-12 {
Ok(numerator / denominator)
} else {
Ok(1.0)
}
}
fn compute_normalized_cross_correlation(&self) -> Result<Float> {
let _n_elements = self.original_data.len();
let mean_orig = self.original_data.mean().unwrap_or(0.0);
let mean_recon = self.reconstructed_data.mean().unwrap_or(0.0);
let mut numerator = 0.0;
let mut denom_orig = 0.0;
let mut denom_recon = 0.0;
for (orig, recon) in self
.original_data
.iter()
.zip(self.reconstructed_data.iter())
{
let orig_centered = orig - mean_orig;
let recon_centered = recon - mean_recon;
numerator += orig_centered * recon_centered;
denom_orig += orig_centered * orig_centered;
denom_recon += recon_centered * recon_centered;
}
let denominator = (denom_orig * denom_recon).sqrt();
if denominator > 1e-12 {
Ok(numerator / denominator)
} else {
Ok(0.0)
}
}
fn compute_component_smoothness(&self, component_idx: usize) -> Result<Float> {
let component = self.components.row(component_idx);
let n_features = component.len();
if n_features < 2 {
return Ok(0.0);
}
let mut total_variation = 0.0;
for i in 1..n_features {
total_variation += (component[i] - component[i - 1]).abs();
}
let component_magnitude = component.iter().map(|&x| x * x).sum::<Float>().sqrt();
if component_magnitude > 1e-12 {
Ok(1.0 / (1.0 + total_variation / component_magnitude))
} else {
Ok(1.0)
}
}
fn compute_component_orthogonality(&self, component_idx: usize) -> Result<Float> {
let n_components = self.components.nrows();
if n_components <= 1 {
return Ok(1.0);
}
let component = self.components.row(component_idx);
let mut max_correlation = 0.0_f64;
for i in 0..n_components {
if i != component_idx {
let other_component = self.components.row(i);
let correlation = self
.compute_vector_correlation(&component.to_owned(), &other_component.to_owned())
.abs();
max_correlation = max_correlation.max(correlation);
}
}
Ok(1.0 - max_correlation)
}
fn compute_component_complexity(&self, component_idx: usize) -> Result<Float> {
let component = self.components.row(component_idx);
let min_val = component.iter().fold(Float::INFINITY, |acc, &x| acc.min(x));
let shifted: Vec<Float> = component.iter().map(|&x| x - min_val + 1e-12).collect();
let sum_shifted = shifted.iter().sum::<Float>();
let mut entropy = 0.0;
for &val in &shifted {
let prob = val / sum_shifted;
if prob > 1e-12 {
entropy -= prob * prob.ln();
}
}
let max_entropy = (shifted.len() as Float).ln();
if max_entropy > 1e-12 {
Ok(entropy / max_entropy)
} else {
Ok(0.0)
}
}
fn compute_variance(&self, data: &Array2<Float>, mean: Float) -> Float {
let n_elements = data.len();
if n_elements <= 1 {
return 0.0;
}
let sum_sq_diff = data.iter().map(|&x| (x - mean) * (x - mean)).sum::<Float>();
sum_sq_diff / (n_elements - 1) as Float
}
fn compute_covariance(
&self,
data1: &Array2<Float>,
data2: &Array2<Float>,
mean1: Float,
mean2: Float,
) -> Float {
let n_elements = data1.len();
if n_elements != data2.len() || n_elements <= 1 {
return 0.0;
}
let sum_prod_diff = data1
.iter()
.zip(data2.iter())
.map(|(&x1, &x2)| (x1 - mean1) * (x2 - mean2))
.sum::<Float>();
sum_prod_diff / (n_elements - 1) as Float
}
fn compute_vector_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()
}
}
pub struct ComparativeAnalysis {
methods: Vec<String>,
quality_scores: Vec<Float>,
reconstruction_errors: Vec<Float>,
interpretability_scores: Vec<Float>,
computational_costs: Vec<Float>,
}
impl ComparativeAnalysis {
pub fn new() -> Self {
Self {
methods: Vec::new(),
quality_scores: Vec::new(),
reconstruction_errors: Vec::new(),
interpretability_scores: Vec::new(),
computational_costs: Vec::new(),
}
}
pub fn add_method(
&mut self,
method_name: String,
quality_score: Float,
reconstruction_error: Float,
interpretability_score: Float,
computational_cost: Float,
) {
self.methods.push(method_name);
self.quality_scores.push(quality_score);
self.reconstruction_errors.push(reconstruction_error);
self.interpretability_scores.push(interpretability_score);
self.computational_costs.push(computational_cost);
}
pub fn best_method(&self) -> Option<(&String, Float)> {
if self.methods.is_empty() {
return None;
}
let mut best_idx = 0;
let mut best_score = self.compute_overall_score(0);
for i in 1..self.methods.len() {
let score = self.compute_overall_score(i);
if score > best_score {
best_score = score;
best_idx = i;
}
}
Some((&self.methods[best_idx], best_score))
}
pub fn method_ranking(&self) -> Vec<(String, Float)> {
let mut ranking: Vec<(String, Float)> = self
.methods
.iter()
.enumerate()
.map(|(i, method)| (method.clone(), self.compute_overall_score(i)))
.collect();
ranking.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
ranking
}
fn compute_overall_score(&self, index: usize) -> Float {
if index >= self.methods.len() {
return 0.0;
}
let weights = [0.4, 0.3, 0.2, 0.1];
let quality_norm = self.quality_scores[index];
let reconstruction_norm = 1.0 / (1.0 + self.reconstruction_errors[index]);
let interpretability_norm = self.interpretability_scores[index];
let efficiency_norm = 1.0 / (1.0 + self.computational_costs[index]);
weights[0] * quality_norm
+ weights[1] * reconstruction_norm
+ weights[2] * interpretability_norm
+ weights[3] * efficiency_norm
}
}
impl Default for ComparativeAnalysis {
fn default() -> Self {
Self::new()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_quality_assessment_creation() {
let original = array![[1.0, 2.0], [3.0, 4.0]];
let reconstructed = array![[1.1, 1.9], [2.9, 4.1]];
let components = array![[0.7, 0.7], [0.7, -0.7]];
let explained_variance = array![2.5, 1.5];
let qa = QualityAssessment::new(original, reconstructed, components, explained_variance);
assert_eq!(qa.total_explained_variance, 4.0);
}
#[test]
fn test_goodness_of_fit() {
let original = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let reconstructed = array![[1.1, 1.9], [2.9, 4.1], [4.9, 6.1]];
let components = array![[0.7, 0.7], [0.7, -0.7]];
let explained_variance = array![2.5, 1.5];
let qa = QualityAssessment::new(original, reconstructed, components, explained_variance);
let gof = qa.goodness_of_fit().expect("operation should succeed");
assert!(gof.r_squared >= 0.0);
assert!(gof.r_squared <= 1.0);
assert!(gof.rmse >= 0.0);
assert!(gof.mae >= 0.0);
}
#[test]
fn test_model_comparison() {
let original = array![[1.0, 2.0], [3.0, 4.0]];
let reconstructed = array![[1.0, 2.0], [3.0, 4.0]]; let components = array![[1.0, 0.0]];
let explained_variance = array![4.0];
let qa = QualityAssessment::new(original, reconstructed, components, explained_variance);
let comparison = qa.model_comparison(2).expect("operation should succeed");
assert_eq!(comparison.n_parameters, 2);
assert!(comparison.aic.is_finite());
assert!(comparison.bic.is_finite());
}
#[test]
fn test_reconstruction_quality() {
let original = array![[1.0, 2.0], [3.0, 4.0]];
let reconstructed = array![[1.1, 1.9], [2.9, 4.1]];
let components = array![[0.7, 0.7], [0.7, -0.7]];
let explained_variance = array![2.5, 1.5];
let qa = QualityAssessment::new(original, reconstructed, components, explained_variance);
let quality = qa
.reconstruction_quality()
.expect("operation should succeed");
assert_eq!(quality.component_errors.len(), 2);
assert!(quality.global_error >= 0.0);
assert!(quality.snr.is_finite());
assert!(quality.relative_error >= 0.0);
assert!(quality.relative_error <= 1.0);
}
#[test]
fn test_component_interpretability() {
let original = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let reconstructed = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let components = array![[0.6, 0.8, 0.0], [0.8, -0.6, 0.0]]; let explained_variance = array![3.0, 2.0];
let qa = QualityAssessment::new(original, reconstructed, components, explained_variance);
let interpretability = qa
.component_interpretability()
.expect("operation should succeed");
assert_eq!(interpretability.loading_magnitudes.len(), 2);
assert_eq!(interpretability.sparsity.len(), 2);
assert_eq!(interpretability.feature_importance.dim(), (2, 3));
assert!(interpretability.sparsity[0] > 0.0); assert!(interpretability.sparsity[1] > 0.0); }
#[test]
fn test_overall_quality_score() {
let original = array![[1.0, 2.0], [3.0, 4.0]];
let reconstructed = array![[1.0, 2.0], [3.0, 4.0]]; let components = array![[1.0, 0.0]];
let explained_variance = array![4.0];
let qa = QualityAssessment::new(original, reconstructed, components, explained_variance);
let score = qa
.overall_quality_score()
.expect("operation should succeed");
assert!(score >= 0.0);
assert!(score <= 1.0);
}
#[test]
fn test_comparative_analysis() {
let mut analysis = ComparativeAnalysis::new();
analysis.add_method("PCA".to_string(), 0.9, 0.1, 0.8, 0.2);
analysis.add_method("ICA".to_string(), 0.8, 0.15, 0.9, 0.3);
analysis.add_method("NMF".to_string(), 0.85, 0.12, 0.85, 0.25);
let (_best_method, best_score) = analysis.best_method().expect("operation should succeed");
assert!(best_score > 0.0);
let ranking = analysis.method_ranking();
assert_eq!(ranking.len(), 3);
for i in 1..ranking.len() {
assert!(ranking[i - 1].1 >= ranking[i].1);
}
}
#[test]
fn test_ssim_computation() {
let original = array![[1.0, 2.0], [3.0, 4.0]];
let reconstructed = array![[1.0, 2.0], [3.0, 4.0]]; let components = array![[1.0]];
let explained_variance = array![1.0];
let qa = QualityAssessment::new(original, reconstructed, components, explained_variance);
let ssim = qa.compute_ssim().expect("operation should succeed");
assert!(ssim >= 0.0);
assert!(ssim <= 1.0);
assert!(ssim > 0.9);
}
#[test]
fn test_normalized_cross_correlation() {
let original = array![[1.0, 2.0], [3.0, 4.0]];
let reconstructed = array![[2.0, 4.0], [6.0, 8.0]]; let components = array![[1.0]];
let explained_variance = array![1.0];
let qa = QualityAssessment::new(original, reconstructed, components, explained_variance);
let ncc = qa
.compute_normalized_cross_correlation()
.expect("operation should succeed");
assert!(ncc >= -1.0);
assert!(ncc <= 1.0);
assert!(ncc > 0.9);
}
}