use scirs2_core::ndarray::{Array1, Array2, Axis};
use scirs2_core::random::{thread_rng, RandNormal};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
#[derive(Debug, Clone)]
pub struct RobustConfig {
pub max_iterations: usize,
pub tolerance: Float,
pub outlier_threshold: Float,
pub tuning_constant: Float,
pub loss_function: LossFunction,
}
#[derive(Debug, Clone, Copy)]
pub enum LossFunction {
Huber,
Tukey,
L1,
Cauchy,
Welsch,
}
impl Default for RobustConfig {
fn default() -> Self {
Self {
max_iterations: 100,
tolerance: 1e-6,
outlier_threshold: 2.5,
tuning_constant: 1.345, loss_function: LossFunction::Huber,
}
}
}
pub struct RobustPCA {
config: RobustConfig,
n_components: Option<usize>,
components_: Option<Array2<Float>>,
explained_variance_: Option<Array1<Float>>,
mean_: Option<Array1<Float>>,
outlier_weights_: Option<Array2<Float>>,
}
impl RobustPCA {
pub fn new() -> Self {
Self {
config: RobustConfig::default(),
n_components: None,
components_: None,
explained_variance_: None,
mean_: None,
outlier_weights_: None,
}
}
pub fn n_components(mut self, n_components: usize) -> Self {
self.n_components = Some(n_components);
self
}
pub fn loss_function(mut self, loss_function: LossFunction) -> Self {
self.config.loss_function = loss_function;
self
}
pub fn tuning_constant(mut self, tuning_constant: Float) -> Self {
self.config.tuning_constant = tuning_constant;
self
}
pub fn outlier_threshold(mut self, threshold: Float) -> Self {
self.config.outlier_threshold = threshold;
self
}
pub fn fit_transform(&mut self, data: &Array2<Float>) -> Result<RobustPCAResult> {
self.fit(data)?;
self.transform(data)
}
pub fn fit(&mut self, data: &Array2<Float>) -> Result<()> {
let (n_samples, n_features) = data.dim();
let n_components = self.n_components.unwrap_or(n_features.min(n_samples));
if n_components > n_features.min(n_samples) {
return Err(SklearsError::InvalidInput(
"Number of components cannot exceed min(n_samples, n_features)".to_string(),
));
}
let robust_mean = self.compute_robust_mean(data)?;
let centered_data = data - &robust_mean.view().insert_axis(Axis(0));
let mut rng = thread_rng();
let mut components = Array2::zeros((n_components, n_features));
for elem in components.iter_mut() {
*elem = rng.sample(RandNormal::new(0.0, 1.0).expect("sampling should succeed"));
}
self.orthogonalize_components(&mut components);
let mut prev_components = components.clone();
let mut outlier_weights = Array2::ones((n_samples, n_components));
for _iter in 0..self.config.max_iterations {
for comp_idx in 0..n_components {
let weights = outlier_weights.column(comp_idx);
let updated_component =
self.update_component(¢ered_data, &weights, comp_idx)?;
components.row_mut(comp_idx).assign(&updated_component);
}
self.orthogonalize_components(&mut components);
outlier_weights = self.compute_outlier_weights(¢ered_data, &components)?;
let component_change = self.compute_component_change(&components, &prev_components);
if component_change < self.config.tolerance {
break;
}
prev_components = components.clone();
}
let explained_variance = self.compute_explained_variance(¢ered_data, &components)?;
self.components_ = Some(components);
self.explained_variance_ = Some(explained_variance);
self.mean_ = Some(robust_mean);
self.outlier_weights_ = Some(outlier_weights);
Ok(())
}
pub fn transform(&self, data: &Array2<Float>) -> Result<RobustPCAResult> {
let components = self
.components_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let mean = self
.mean_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let explained_variance = self
.explained_variance_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let outlier_weights = self
.outlier_weights_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let centered_data = data - &mean.view().insert_axis(Axis(0));
let transformed = centered_data.dot(&components.t());
Ok(RobustPCAResult {
transformed_data: transformed,
components: components.clone(),
explained_variance: explained_variance.clone(),
mean: mean.clone(),
outlier_weights: outlier_weights.clone(),
})
}
fn compute_robust_mean(&self, data: &Array2<Float>) -> Result<Array1<Float>> {
let (n_samples, n_features) = data.dim();
let mut mean = data
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
for _ in 0..self.config.max_iterations {
let mut new_mean = Array1::zeros(n_features);
let mut weight_sum = 0.0;
for i in 0..n_samples {
let residual = &data.row(i) - &mean;
let residual_norm = residual.dot(&residual).sqrt();
let weight = self.robust_weight(residual_norm);
new_mean += &(weight * &data.row(i));
weight_sum += weight;
}
if weight_sum > 0.0 {
new_mean /= weight_sum;
}
let change = (&new_mean - &mean).dot(&(&new_mean - &mean)).sqrt();
mean = new_mean;
if change < self.config.tolerance {
break;
}
}
Ok(mean)
}
fn update_component(
&self,
data: &Array2<Float>,
weights: &scirs2_core::ndarray::ArrayView1<Float>,
_comp_idx: usize,
) -> Result<Array1<Float>> {
let (n_samples, n_features) = data.dim();
let mut component = Array1::zeros(n_features);
let mut weight_sum = 0.0;
for i in 0..n_samples {
let weight = weights[i];
let sample = data.row(i);
component += &(weight * &sample);
weight_sum += weight;
}
if weight_sum > 0.0 {
component /= weight_sum;
}
let norm = component.dot(&component).sqrt();
if norm > 1e-12 {
component /= norm;
}
Ok(component)
}
fn orthogonalize_components(&self, components: &mut Array2<Float>) {
let n_components = components.nrows();
for i in 0..n_components {
let prev_components: Vec<Array1<Float>> =
(0..i).map(|j| components.row(j).to_owned()).collect();
let mut current = components.row_mut(i);
let norm = current.dot(¤t).sqrt();
if norm > 1e-12 {
current /= norm;
}
for prev in prev_components {
let projection = current.dot(&prev);
current -= &(projection * &prev);
let norm = current.dot(¤t).sqrt();
if norm > 1e-12 {
current /= norm;
}
}
}
}
fn compute_outlier_weights(
&self,
data: &Array2<Float>,
components: &Array2<Float>,
) -> Result<Array2<Float>> {
let (n_samples, _) = data.dim();
let n_components = components.nrows();
let mut weights = Array2::zeros((n_samples, n_components));
for i in 0..n_samples {
let sample = data.row(i);
for j in 0..n_components {
let component = components.row(j);
let projection = sample.dot(&component);
let reconstructed = projection * &component;
let residual = &sample - &reconstructed;
let residual_norm = residual.dot(&residual).sqrt();
weights[[i, j]] = self.robust_weight(residual_norm);
}
}
Ok(weights)
}
fn robust_weight(&self, residual: Float) -> Float {
let standardized_residual = residual / self.config.tuning_constant;
match self.config.loss_function {
LossFunction::Huber => {
if standardized_residual.abs() <= 1.0 {
1.0
} else {
1.0 / standardized_residual.abs()
}
}
LossFunction::Tukey => {
if standardized_residual.abs() <= 1.0 {
let x2 = standardized_residual * standardized_residual;
(1.0 - x2) * (1.0 - x2)
} else {
0.0
}
}
LossFunction::L1 => {
if standardized_residual.abs() > 1e-12 {
1.0 / standardized_residual.abs()
} else {
1.0
}
}
LossFunction::Cauchy => 1.0 / (1.0 + standardized_residual * standardized_residual),
LossFunction::Welsch => {
let x2 = standardized_residual * standardized_residual;
(-x2).exp()
}
}
}
fn compute_explained_variance(
&self,
data: &Array2<Float>,
components: &Array2<Float>,
) -> Result<Array1<Float>> {
let n_components = components.nrows();
let mut explained_variance = Array1::zeros(n_components);
let total_variance = self.compute_robust_variance(data);
for i in 0..n_components {
let component = components.row(i);
let projections = data.dot(&component);
let component_variance = self.compute_robust_variance_1d(&projections);
explained_variance[i] = component_variance / total_variance;
}
Ok(explained_variance)
}
fn compute_robust_variance(&self, data: &Array2<Float>) -> Float {
let (_n_samples, n_features) = data.dim();
let mut total_variance = 0.0;
for j in 0..n_features {
let column = data.column(j);
total_variance += self.compute_robust_variance_1d(&column.to_owned());
}
total_variance / n_features as Float
}
fn compute_robust_variance_1d(&self, data: &Array1<Float>) -> Float {
let n = data.len();
if n < 2 {
return 0.0;
}
let median = self.compute_median(data);
let mut abs_deviations: Vec<Float> = data.iter().map(|&x| (x - median).abs()).collect();
abs_deviations.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let mad = if abs_deviations.len().is_multiple_of(2) {
let mid = abs_deviations.len() / 2;
(abs_deviations[mid - 1] + abs_deviations[mid]) / 2.0
} else {
abs_deviations[abs_deviations.len() / 2]
};
1.4826 * mad * 1.4826 * mad }
fn compute_median(&self, data: &Array1<Float>) -> Float {
let mut sorted_data: Vec<Float> = data.iter().cloned().collect();
sorted_data.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let n = sorted_data.len();
if n.is_multiple_of(2) {
(sorted_data[n / 2 - 1] + sorted_data[n / 2]) / 2.0
} else {
sorted_data[n / 2]
}
}
fn compute_component_change(&self, current: &Array2<Float>, previous: &Array2<Float>) -> Float {
let diff = current - previous;
(diff.iter().map(|&x| x * x).sum::<Float>()).sqrt()
}
pub fn components(&self) -> Option<&Array2<Float>> {
self.components_.as_ref()
}
pub fn explained_variance(&self) -> Option<&Array1<Float>> {
self.explained_variance_.as_ref()
}
pub fn mean(&self) -> Option<&Array1<Float>> {
self.mean_.as_ref()
}
pub fn outlier_weights(&self) -> Option<&Array2<Float>> {
self.outlier_weights_.as_ref()
}
}
impl Default for RobustPCA {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RobustPCAResult {
pub transformed_data: Array2<Float>,
pub components: Array2<Float>,
pub explained_variance: Array1<Float>,
pub mean: Array1<Float>,
pub outlier_weights: Array2<Float>,
}
impl RobustPCAResult {
pub fn inverse_transform(&self, transformed_data: &Array2<Float>) -> Array2<Float> {
let reconstructed = transformed_data.dot(&self.components);
reconstructed + self.mean.view().insert_axis(Axis(0))
}
pub fn identify_outliers(&self, threshold: Float) -> Array1<bool> {
let (n_samples, _) = self.outlier_weights.dim();
let mut is_outlier = Array1::from_elem(n_samples, false);
for i in 0..n_samples {
let min_weight = self
.outlier_weights
.row(i)
.iter()
.cloned()
.fold(Float::INFINITY, Float::min);
if min_weight < threshold {
is_outlier[i] = true;
}
}
is_outlier
}
pub fn reconstruction_error(&self, original_data: &Array2<Float>) -> Result<Array1<Float>> {
let reconstructed = self.inverse_transform(&self.transformed_data);
let (n_samples, _) = original_data.dim();
let mut errors = Array1::zeros(n_samples);
for i in 0..n_samples {
let original_row = original_data.row(i);
let reconstructed_row = reconstructed.row(i);
let diff = &original_row - &reconstructed_row;
errors[i] = diff.dot(&diff).sqrt();
}
Ok(errors)
}
}
pub struct MEstimatorDecomposition {
config: RobustConfig,
rank: Option<usize>,
}
impl MEstimatorDecomposition {
pub fn new() -> Self {
Self {
config: RobustConfig::default(),
rank: None,
}
}
pub fn rank(mut self, rank: usize) -> Self {
self.rank = Some(rank);
self
}
pub fn loss_function(mut self, loss_function: LossFunction) -> Self {
self.config.loss_function = loss_function;
self
}
pub fn decompose(&self, matrix: &Array2<Float>) -> Result<MEstimatorResult> {
let (m, n) = matrix.dim();
let rank = self.rank.unwrap_or((m.min(n) / 2).max(1));
let mut rng = thread_rng();
let mut u = Array2::zeros((m, rank));
let mut v = Array2::zeros((n, rank));
for elem in u.iter_mut() {
*elem = rng.sample(RandNormal::new(0.0, 1.0).expect("sampling should succeed"));
}
for elem in v.iter_mut() {
*elem = rng.sample(RandNormal::new(0.0, 1.0).expect("sampling should succeed"));
}
for _ in 0..self.config.max_iterations {
let prev_u = u.clone();
let prev_v = v.clone();
u = self.update_factor_u(matrix, &u, &v)?;
v = self.update_factor_v(matrix, &u, &v)?;
let u_change = (&u - &prev_u).iter().map(|&x| x * x).sum::<Float>().sqrt();
let v_change = (&v - &prev_v).iter().map(|&x| x * x).sum::<Float>().sqrt();
if u_change + v_change < self.config.tolerance {
break;
}
}
let reconstruction = u.dot(&v.t());
let residuals = matrix - &reconstruction;
let weights = self.compute_residual_weights(&residuals);
Ok(MEstimatorResult {
u_factor: u,
v_factor: v,
reconstruction,
residuals,
weights,
})
}
fn update_factor_u(
&self,
matrix: &Array2<Float>,
u: &Array2<Float>,
v: &Array2<Float>,
) -> Result<Array2<Float>> {
let (m, rank) = u.dim();
let mut new_u = Array2::zeros((m, rank));
for i in 0..m {
for r in 0..rank {
let mut numerator = 0.0;
let mut denominator = 0.0;
for j in 0..matrix.ncols() {
let residual = matrix[[i, j]] - u.row(i).dot(&v.row(j));
let weight = self.robust_weight(residual.abs());
numerator += weight * matrix[[i, j]] * v[[j, r]];
denominator += weight * v[[j, r]] * v[[j, r]];
}
if denominator > 1e-12 {
new_u[[i, r]] = numerator / denominator;
}
}
}
Ok(new_u)
}
fn update_factor_v(
&self,
matrix: &Array2<Float>,
u: &Array2<Float>,
v: &Array2<Float>,
) -> Result<Array2<Float>> {
let (n, rank) = v.dim();
let mut new_v = Array2::zeros((n, rank));
for j in 0..n {
for r in 0..rank {
let mut numerator = 0.0;
let mut denominator = 0.0;
for i in 0..matrix.nrows() {
let residual = matrix[[i, j]] - u.row(i).dot(&v.row(j));
let weight = self.robust_weight(residual.abs());
numerator += weight * matrix[[i, j]] * u[[i, r]];
denominator += weight * u[[i, r]] * u[[i, r]];
}
if denominator > 1e-12 {
new_v[[j, r]] = numerator / denominator;
}
}
}
Ok(new_v)
}
fn compute_residual_weights(&self, residuals: &Array2<Float>) -> Array2<Float> {
residuals.mapv(|r| self.robust_weight(r.abs()))
}
fn robust_weight(&self, residual: Float) -> Float {
let standardized_residual = residual / self.config.tuning_constant;
match self.config.loss_function {
LossFunction::Huber => {
if standardized_residual.abs() <= 1.0 {
1.0
} else {
1.0 / standardized_residual.abs()
}
}
LossFunction::Tukey => {
if standardized_residual.abs() <= 1.0 {
let x2 = standardized_residual * standardized_residual;
(1.0 - x2) * (1.0 - x2)
} else {
0.0
}
}
LossFunction::L1 => {
if standardized_residual.abs() > 1e-12 {
1.0 / standardized_residual.abs()
} else {
1.0
}
}
LossFunction::Cauchy => 1.0 / (1.0 + standardized_residual * standardized_residual),
LossFunction::Welsch => {
let x2 = standardized_residual * standardized_residual;
(-x2).exp()
}
}
}
}
impl Default for MEstimatorDecomposition {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MEstimatorResult {
pub u_factor: Array2<Float>,
pub v_factor: Array2<Float>,
pub reconstruction: Array2<Float>,
pub residuals: Array2<Float>,
pub weights: Array2<Float>,
}
impl MEstimatorResult {
pub fn residual_norm(&self) -> Float {
self.residuals.iter().map(|&x| x * x).sum::<Float>().sqrt()
}
pub fn weighted_residual_norm(&self) -> Float {
let weighted_residuals = &self.residuals * &self.weights;
weighted_residuals
.iter()
.map(|&x| x * x)
.sum::<Float>()
.sqrt()
}
pub fn identify_outlier_elements(&self, threshold: Float) -> Array2<bool> {
self.weights.mapv(|w| w < threshold)
}
}
pub struct BreakdownPointAnalysis {
#[allow(dead_code)]
config: RobustConfig,
}
impl BreakdownPointAnalysis {
pub fn new() -> Self {
Self {
config: RobustConfig::default(),
}
}
pub fn empirical_breakdown_point(
&self,
data: &Array2<Float>,
contamination_levels: &[Float],
) -> Result<BreakdownResult> {
let (n_samples, _) = data.dim();
let mut breakdown_errors = Vec::new();
let mut breakdown_levels = Vec::new();
for &contamination_level in contamination_levels {
let n_outliers = (contamination_level * n_samples as Float) as usize;
if n_outliers >= n_samples {
continue;
}
let contaminated_data = self.add_outliers(data, n_outliers)?;
let mut robust_pca = RobustPCA::new().n_components(2);
let robust_result = robust_pca.fit_transform(&contaminated_data)?;
let standard_result = self.standard_pca(&contaminated_data, 2)?;
let direction_error =
self.compute_direction_error(&robust_result.components, &standard_result);
breakdown_errors.push(direction_error);
breakdown_levels.push(contamination_level);
}
Ok(BreakdownResult {
contamination_levels: Array1::from_vec(breakdown_levels),
direction_errors: Array1::from_vec(breakdown_errors),
})
}
fn add_outliers(&self, data: &Array2<Float>, n_outliers: usize) -> Result<Array2<Float>> {
let (n_samples, n_features) = data.dim();
let mut contaminated_data = data.clone();
let data_center = data
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let data_scale = self.estimate_scale(data);
let mut rng = thread_rng();
for i in 0..n_outliers.min(n_samples) {
for j in 0..n_features {
contaminated_data[[i, j]] =
data_center[j] + 10.0 * data_scale * (2.0 * (rng.random::<Float>()) - 1.0);
}
}
Ok(contaminated_data)
}
fn estimate_scale(&self, data: &Array2<Float>) -> Float {
let center = data
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let distances: Vec<Float> = data
.axis_iter(Axis(0))
.map(|row| {
let diff = &row - ¢er;
diff.dot(&diff).sqrt()
})
.collect();
let mut sorted_distances = distances;
sorted_distances.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let n = sorted_distances.len();
if n.is_multiple_of(2) {
(sorted_distances[n / 2 - 1] + sorted_distances[n / 2]) / 2.0
} else {
sorted_distances[n / 2]
}
}
fn standard_pca(&self, data: &Array2<Float>, n_components: usize) -> Result<Array2<Float>> {
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.view().insert_axis(Axis(0));
let _cov_matrix = centered_data.t().dot(¢ered_data) / (n_samples - 1) as Float;
let mut components = Array2::eye(n_features);
components = components
.slice(scirs2_core::ndarray::s![0..n_components, ..])
.to_owned();
Ok(components)
}
fn compute_direction_error(
&self,
robust_components: &Array2<Float>,
standard_components: &Array2<Float>,
) -> Float {
let n_components = robust_components.nrows().min(standard_components.nrows());
let mut total_error = 0.0;
for i in 0..n_components {
let robust_dir = robust_components.row(i);
let standard_dir = standard_components.row(i);
let dot_product = robust_dir.dot(&standard_dir);
let robust_norm = robust_dir.dot(&robust_dir).sqrt();
let standard_norm = standard_dir.dot(&standard_dir).sqrt();
if robust_norm > 1e-12 && standard_norm > 1e-12 {
let cos_angle = dot_product / (robust_norm * standard_norm);
let angle = cos_angle.abs().acos();
total_error += angle;
}
}
total_error / n_components as Float
}
}
impl Default for BreakdownPointAnalysis {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BreakdownResult {
pub contamination_levels: Array1<Float>,
pub direction_errors: Array1<Float>,
}
impl BreakdownResult {
pub fn breakdown_point(&self, error_threshold: Float) -> Option<Float> {
for (level, error) in self
.contamination_levels
.iter()
.zip(self.direction_errors.iter())
{
if *error > error_threshold {
return Some(*level);
}
}
None
}
pub fn robustness_score(&self) -> Float {
let n = self.contamination_levels.len();
if n < 2 {
return 0.0;
}
let mut area = 0.0;
for i in 1..n {
let dx = self.contamination_levels[i] - self.contamination_levels[i - 1];
let avg_error = (self.direction_errors[i] + self.direction_errors[i - 1]) / 2.0;
area += dx * avg_error;
}
1.0 / (area + 1e-12)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_robust_pca_basic() {
let data = Array2::from_shape_vec(
(4, 3),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 2.0, 3.0, 4.0],
)
.expect("operation should succeed");
let mut rpca = RobustPCA::new().n_components(2);
let result = rpca.fit_transform(&data).expect("operation should succeed");
assert_eq!(result.transformed_data.dim(), (4, 2));
assert_eq!(result.components.dim(), (2, 3));
assert_eq!(result.explained_variance.len(), 2);
assert_eq!(result.outlier_weights.dim(), (4, 2));
}
#[test]
fn test_robust_pca_with_outliers() {
let data = Array2::from_shape_vec(
(5, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 2.0, 3.0, 4.0, 100.0, 100.0,
100.0, ],
)
.expect("operation should succeed");
let mut rpca = RobustPCA::new()
.n_components(2)
.loss_function(LossFunction::Tukey);
let result = rpca.fit_transform(&data).expect("operation should succeed");
let outlier_weights = result.outlier_weights.row(4);
let min_weight = outlier_weights
.iter()
.cloned()
.fold(Float::INFINITY, Float::min);
assert!(min_weight < 0.5);
let outliers = result.identify_outliers(0.5);
assert!(outliers[4]); }
#[test]
fn test_m_estimator_decomposition() {
let matrix =
Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
.expect("operation should succeed");
let m_est = MEstimatorDecomposition::new()
.rank(2)
.loss_function(LossFunction::Huber);
let result = m_est.decompose(&matrix).expect("operation should succeed");
assert_eq!(result.u_factor.dim(), (3, 2));
assert_eq!(result.v_factor.dim(), (3, 2));
assert_eq!(result.reconstruction.dim(), (3, 3));
assert_eq!(result.residuals.dim(), (3, 3));
assert_eq!(result.weights.dim(), (3, 3));
}
#[test]
fn test_loss_functions() {
let _config = RobustConfig::default();
let rpca = RobustPCA::new();
let residuals = vec![0.5, 1.0, 1.5, 2.0, 5.0];
for residual in residuals {
let huber_weight = rpca.robust_weight(residual);
assert!(huber_weight > 0.0);
assert!(huber_weight <= 1.0);
}
}
#[test]
fn test_breakdown_analysis() {
let data = Array2::from_shape_vec(
(10, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0,
30.0,
],
)
.expect("operation should succeed");
let breakdown_analysis = BreakdownPointAnalysis::new();
let contamination_levels = vec![0.1, 0.2, 0.3];
let result = breakdown_analysis
.empirical_breakdown_point(&data, &contamination_levels)
.expect("operation should succeed");
assert_eq!(
result.contamination_levels.len(),
result.direction_errors.len()
);
assert!(result.contamination_levels.len() <= contamination_levels.len());
let score = result.robustness_score();
assert!(score > 0.0);
}
#[test]
fn test_robust_mean_computation() {
let data = Array2::from_shape_vec(
(5, 2),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 2.0, 3.0, 100.0, 100.0, ],
)
.expect("operation should succeed");
let rpca = RobustPCA::new();
let robust_mean = rpca
.compute_robust_mean(&data)
.expect("operation should succeed");
assert!(robust_mean[0] < 50.0); assert!(robust_mean[1] < 50.0);
}
#[test]
fn test_reconstruction_error() {
let data = Array2::from_shape_vec(
(4, 3),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 2.0, 3.0, 4.0],
)
.expect("operation should succeed");
let mut rpca = RobustPCA::new().n_components(2);
let result = rpca.fit_transform(&data).expect("operation should succeed");
let errors = result
.reconstruction_error(&data)
.expect("operation should succeed");
assert_eq!(errors.len(), 4);
for error in errors.iter() {
assert!(*error >= 0.0);
}
}
}