use crate::kernels::Kernel;
use crate::utils;
use scirs2_core::ndarray::{Array1, Array2, Axis};
use sklears_core::error::{Result as SklResult, SklearsError};
use sklears_core::traits::{Estimator, Fit, Predict};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct Untrained;
#[derive(Debug, Clone)]
pub struct Trained {
pub kernel: Box<dyn Kernel>,
pub robust_likelihood: RobustLikelihood,
pub training_data: (Array2<f64>, Array1<f64>),
pub alpha: Array1<f64>,
pub cholesky: Array2<f64>,
pub log_likelihood: f64,
pub outlier_weights: Array1<f64>,
pub outlier_indices: Vec<usize>,
pub robustness_metrics: RobustnessMetrics,
}
#[derive(Debug, Clone)]
pub enum RobustLikelihood {
Gaussian,
StudentT { degrees_of_freedom: f64 },
Laplace { scale: f64 },
Huber { threshold: f64 },
Cauchy { scale: f64 },
ContaminationMixture {
clean_variance: f64,
contamination_variance: f64,
contamination_probability: f64,
},
Adaptive {
base_likelihood: Box<RobustLikelihood>,
adaptation_rate: f64,
},
}
impl RobustLikelihood {
pub fn student_t(degrees_of_freedom: f64) -> Self {
Self::StudentT { degrees_of_freedom }
}
pub fn laplace(scale: f64) -> Self {
Self::Laplace { scale }
}
pub fn huber(threshold: f64) -> Self {
Self::Huber { threshold }
}
pub fn contamination_mixture(clean_var: f64, contam_var: f64, contam_prob: f64) -> Self {
Self::ContaminationMixture {
clean_variance: clean_var,
contamination_variance: contam_var,
contamination_probability: contam_prob,
}
}
pub fn log_likelihood(&self, residual: f64) -> f64 {
match self {
Self::Gaussian => -0.5 * (residual.powi(2) + (2.0 * PI).ln()),
Self::StudentT { degrees_of_freedom } => {
let nu = *degrees_of_freedom;
let gamma_ratio = Self::log_gamma((nu + 1.0) / 2.0) - Self::log_gamma(nu / 2.0);
gamma_ratio
- 0.5 * (nu * PI).ln()
- 0.5 * (nu + 1.0) * (1.0 + residual.powi(2) / nu).ln()
}
Self::Laplace { scale } => -(residual.abs() / scale + scale.ln() + 2.0_f64.ln()),
Self::Huber { threshold } => {
let abs_res = residual.abs();
if abs_res <= *threshold {
-0.5 * residual.powi(2) } else {
-threshold * abs_res + 0.5 * threshold.powi(2) }
}
Self::Cauchy { scale } => -(PI * scale * (1.0 + (residual / scale).powi(2))).ln(),
Self::ContaminationMixture {
clean_variance,
contamination_variance,
contamination_probability,
} => {
let clean_ll = -0.5
* (residual.powi(2) / clean_variance + clean_variance.ln() + (2.0 * PI).ln());
let contam_ll = -0.5
* (residual.powi(2) / contamination_variance
+ contamination_variance.ln()
+ (2.0 * PI).ln());
let clean_prob = 1.0 - contamination_probability;
let clean_weight = clean_prob * clean_ll.exp();
let contam_weight = contamination_probability * contam_ll.exp();
(clean_weight + contam_weight).ln()
}
Self::Adaptive {
base_likelihood, ..
} => base_likelihood.log_likelihood(residual),
}
}
pub fn log_likelihood_derivative(&self, residual: f64) -> f64 {
match self {
Self::Gaussian => -residual,
Self::StudentT { degrees_of_freedom } => {
let nu = *degrees_of_freedom;
-(nu + 1.0) * residual / (nu + residual.powi(2))
}
Self::Laplace { scale } => -residual.signum() / scale,
Self::Huber { threshold } => {
let abs_res = residual.abs();
if abs_res <= *threshold {
-residual
} else {
-threshold * residual.signum()
}
}
Self::Cauchy { scale } => -2.0 * residual / (scale.powi(2) + residual.powi(2)),
Self::ContaminationMixture { .. } => {
-residual }
Self::Adaptive {
base_likelihood, ..
} => base_likelihood.log_likelihood_derivative(residual),
}
}
pub fn compute_weights(&self, residuals: &Array1<f64>) -> Array1<f64> {
match self {
Self::Gaussian => Array1::ones(residuals.len()),
Self::StudentT { degrees_of_freedom } => {
let nu = *degrees_of_freedom;
residuals.map(|&r| (nu + 1.0) / (nu + r.powi(2)))
}
Self::Laplace { .. } => {
residuals.map(|&r| if r.abs() > 1e-12 { 1.0 / r.abs() } else { 1e12 })
}
Self::Huber { threshold } => residuals.map(|&r| {
let abs_r = r.abs();
if abs_r <= *threshold {
1.0
} else {
threshold / abs_r
}
}),
Self::Cauchy { scale } => residuals.map(|&r| 2.0 / (1.0 + (r / scale).powi(2))),
Self::ContaminationMixture {
clean_variance,
contamination_variance,
contamination_probability,
} => {
residuals.map(|&r| {
let clean_ll = (-0.5 * r.powi(2) / clean_variance).exp();
let contam_ll = (-0.5 * r.powi(2) / contamination_variance).exp();
let clean_prob = 1.0 - contamination_probability;
let clean_weight = clean_prob * clean_ll;
let contam_weight = contamination_probability * contam_ll;
clean_weight / (clean_weight + contam_weight)
})
}
Self::Adaptive {
base_likelihood, ..
} => base_likelihood.compute_weights(residuals),
}
}
fn log_gamma(x: f64) -> f64 {
if x < 1.0 {
return Self::log_gamma(x + 1.0) - x.ln();
}
(x - 0.5) * x.ln() - x + 0.5 * (2.0 * PI).ln()
}
pub fn breakdown_point(&self) -> f64 {
match self {
Self::Gaussian => 0.0, Self::StudentT { degrees_of_freedom } => {
1.0 / (1.0 + degrees_of_freedom)
}
Self::Laplace { .. } => 0.5, Self::Huber { .. } => 0.5, Self::Cauchy { .. } => 0.5, Self::ContaminationMixture {
contamination_probability,
..
} => contamination_probability.min(0.5),
Self::Adaptive {
base_likelihood, ..
} => base_likelihood.breakdown_point(),
}
}
}
#[derive(Debug, Clone)]
pub struct RobustnessMetrics {
pub breakdown_point: f64,
pub influence_function_bound: f64,
pub gross_error_sensitivity: f64,
pub local_shift_sensitivity: f64,
pub contamination_estimate: f64,
}
impl RobustnessMetrics {
pub fn compute(
residuals: &Array1<f64>,
weights: &Array1<f64>,
likelihood: &RobustLikelihood,
) -> Self {
let n = residuals.len() as f64;
let breakdown_point = likelihood.breakdown_point();
let weight_range = weights
.iter()
.fold((f64::INFINITY, 0.0_f64), |(min, max), &w| {
(min.min(w), max.max(w))
});
let influence_function_bound = weight_range.1 - weight_range.0;
let gross_error_sensitivity = weight_range.1;
let mut local_shift_sum = 0.0;
for i in 1..residuals.len() {
let weight_diff = (weights[i] - weights[i - 1]).abs();
local_shift_sum += weight_diff;
}
let local_shift_sensitivity = local_shift_sum / (n - 1.0);
let low_weight_threshold = 0.5; let contaminated_count = weights
.iter()
.filter(|&&w| w < low_weight_threshold)
.count();
let contamination_estimate = contaminated_count as f64 / n;
Self {
breakdown_point,
influence_function_bound,
gross_error_sensitivity,
local_shift_sensitivity,
contamination_estimate,
}
}
}
#[derive(Debug, Clone)]
pub enum OutlierDetectionMethod {
StandardizedResiduals { threshold: f64 },
MahalanobisDistance { threshold: f64 },
InfluenceFunction { threshold: f64 },
CooksDistance { threshold: f64 },
Leverage { threshold: f64 },
RobustMahalanobis { threshold: f64 },
}
impl OutlierDetectionMethod {
pub fn detect_outliers(
&self,
residuals: &Array1<f64>,
predictions: &Array1<f64>,
_training_data: &(Array2<f64>, Array1<f64>),
) -> Vec<usize> {
match self {
Self::StandardizedResiduals { threshold } => {
let std_dev = residuals.std(0.0).max(1e-8); let mean_residual = residuals.mean().unwrap_or(0.0);
residuals
.iter()
.enumerate()
.filter(|(_, &r)| {
let standardized = (r - mean_residual).abs() / std_dev;
standardized > *threshold
})
.map(|(i, _)| i)
.collect()
}
Self::MahalanobisDistance { threshold } => {
let mean_residual = residuals.mean().unwrap_or(0.0);
let variance = residuals.var(0.0);
residuals
.iter()
.enumerate()
.filter(|(_, &r)| {
let maha_dist = (r - mean_residual).powi(2) / variance;
maha_dist > threshold.powi(2)
})
.map(|(i, _)| i)
.collect()
}
Self::InfluenceFunction { threshold } => {
residuals
.iter()
.enumerate()
.filter(|(_, &r)| r.abs() > *threshold)
.map(|(i, _)| i)
.collect()
}
Self::CooksDistance { threshold } => {
let mean_pred = predictions.mean().unwrap_or(0.0);
let pred_var = predictions.var(0.0);
residuals
.iter()
.zip(predictions.iter())
.enumerate()
.filter(|(_, (&r, &p))| {
let leverage = (p - mean_pred).powi(2) / pred_var;
let cooks_d = r.powi(2) * leverage / (1.0 - leverage + 1e-12);
cooks_d > *threshold
})
.map(|(i, _)| i)
.collect()
}
Self::Leverage { threshold } => {
let mean_pred = predictions.mean().unwrap_or(0.0);
let pred_var = predictions.var(0.0);
predictions
.iter()
.enumerate()
.filter(|(_, &p)| {
let leverage = (p - mean_pred).powi(2) / (pred_var + 1e-12);
leverage > *threshold
})
.map(|(i, _)| i)
.collect()
}
Self::RobustMahalanobis { threshold } => {
let mut sorted_residuals = residuals.to_vec();
sorted_residuals
.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = sorted_residuals.len();
let median = if n.is_multiple_of(2) {
(sorted_residuals[n / 2 - 1] + sorted_residuals[n / 2]) / 2.0
} else {
sorted_residuals[n / 2]
};
let mut deviations: Vec<f64> =
residuals.iter().map(|&r| (r - median).abs()).collect();
deviations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mad = if n.is_multiple_of(2) {
(deviations[n / 2 - 1] + deviations[n / 2]) / 2.0
} else {
deviations[n / 2]
};
let robust_scale = 1.4826 * mad;
residuals
.iter()
.enumerate()
.filter(|(_, &r)| {
let robust_maha = (r - median).abs() / (robust_scale + 1e-12);
robust_maha > *threshold
})
.map(|(i, _)| i)
.collect()
}
}
}
}
#[derive(Debug, Clone)]
pub struct RobustGaussianProcessRegressor<S = Untrained> {
kernel: Option<Box<dyn Kernel>>,
robust_likelihood: RobustLikelihood,
outlier_detection_method: OutlierDetectionMethod,
outlier_detection_threshold: f64,
max_iterations: usize,
convergence_threshold: f64,
alpha: f64,
_state: S,
}
#[derive(Debug, Clone)]
pub struct RobustGPConfig {
pub likelihood: RobustLikelihood,
pub outlier_threshold: f64,
pub max_iterations: usize,
pub convergence_threshold: f64,
pub regularization: f64,
}
impl Default for RobustGPConfig {
fn default() -> Self {
Self {
likelihood: RobustLikelihood::StudentT {
degrees_of_freedom: 3.0,
},
outlier_threshold: 2.5,
max_iterations: 100,
convergence_threshold: 1e-6,
regularization: 1e-6,
}
}
}
impl Default for RobustGaussianProcessRegressor<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl RobustGaussianProcessRegressor<Untrained> {
pub fn new() -> Self {
Self {
kernel: None,
robust_likelihood: RobustLikelihood::StudentT {
degrees_of_freedom: 3.0,
},
outlier_detection_method: OutlierDetectionMethod::StandardizedResiduals {
threshold: 2.5,
},
outlier_detection_threshold: 2.5,
max_iterations: 100,
convergence_threshold: 1e-6,
alpha: 1e-6,
_state: Untrained,
}
}
pub fn builder() -> RobustGPBuilder {
RobustGPBuilder::new()
}
pub fn kernel(mut self, kernel: Box<dyn Kernel>) -> Self {
self.kernel = Some(kernel);
self
}
pub fn robust_likelihood(mut self, likelihood: RobustLikelihood) -> Self {
self.robust_likelihood = likelihood;
self
}
pub fn outlier_detection_threshold(mut self, threshold: f64) -> Self {
self.outlier_detection_threshold = threshold;
self
}
pub fn max_iterations(mut self, max_iter: usize) -> Self {
self.max_iterations = max_iter;
self
}
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha;
self
}
}
#[derive(Debug, Clone)]
pub struct RobustGPBuilder {
kernel: Option<Box<dyn Kernel>>,
likelihood: RobustLikelihood,
outlier_method: OutlierDetectionMethod,
outlier_threshold: f64,
max_iterations: usize,
convergence_threshold: f64,
alpha: f64,
}
impl Default for RobustGPBuilder {
fn default() -> Self {
Self::new()
}
}
impl RobustGPBuilder {
pub fn new() -> Self {
Self {
kernel: None,
likelihood: RobustLikelihood::StudentT {
degrees_of_freedom: 3.0,
},
outlier_method: OutlierDetectionMethod::StandardizedResiduals { threshold: 2.5 },
outlier_threshold: 2.5,
max_iterations: 100,
convergence_threshold: 1e-6,
alpha: 1e-6,
}
}
pub fn kernel(mut self, kernel: Box<dyn Kernel>) -> Self {
self.kernel = Some(kernel);
self
}
pub fn robust_likelihood(mut self, likelihood: RobustLikelihood) -> Self {
self.likelihood = likelihood;
self
}
pub fn outlier_detection_method(mut self, method: OutlierDetectionMethod) -> Self {
self.outlier_method = method;
self
}
pub fn outlier_detection_threshold(mut self, threshold: f64) -> Self {
self.outlier_threshold = threshold;
self
}
pub fn max_iterations(mut self, max_iter: usize) -> Self {
self.max_iterations = max_iter;
self
}
pub fn convergence_threshold(mut self, threshold: f64) -> Self {
self.convergence_threshold = threshold;
self
}
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha;
self
}
pub fn build(self) -> RobustGaussianProcessRegressor<Untrained> {
RobustGaussianProcessRegressor {
kernel: self.kernel,
robust_likelihood: self.likelihood,
outlier_detection_method: self.outlier_method,
outlier_detection_threshold: self.outlier_threshold,
max_iterations: self.max_iterations,
convergence_threshold: self.convergence_threshold,
alpha: self.alpha,
_state: Untrained,
}
}
}
impl Estimator for RobustGaussianProcessRegressor<Untrained> {
type Config = RobustGPConfig;
type Error = SklearsError;
type Float = f64;
fn config(&self) -> &Self::Config {
static DEFAULT_CONFIG: RobustGPConfig = RobustGPConfig {
likelihood: RobustLikelihood::StudentT {
degrees_of_freedom: 3.0,
},
outlier_threshold: 2.5,
max_iterations: 100,
convergence_threshold: 1e-6,
regularization: 1e-6,
};
&DEFAULT_CONFIG
}
}
impl Estimator for RobustGaussianProcessRegressor<Trained> {
type Config = RobustGPConfig;
type Error = SklearsError;
type Float = f64;
fn config(&self) -> &Self::Config {
static DEFAULT_CONFIG: RobustGPConfig = RobustGPConfig {
likelihood: RobustLikelihood::StudentT {
degrees_of_freedom: 3.0,
},
outlier_threshold: 2.5,
max_iterations: 100,
convergence_threshold: 1e-6,
regularization: 1e-6,
};
&DEFAULT_CONFIG
}
}
#[allow(non_snake_case)]
impl Fit<Array2<f64>, Array1<f64>> for RobustGaussianProcessRegressor<Untrained> {
type Fitted = RobustGaussianProcessRegressor<Trained>;
fn fit(self, X: &Array2<f64>, y: &Array1<f64>) -> SklResult<Self::Fitted> {
if X.nrows() != y.len() {
return Err(SklearsError::DimensionMismatch {
expected: X.nrows(),
actual: y.len(),
});
}
let kernel = self
.kernel
.clone()
.ok_or_else(|| SklearsError::InvalidInput("Kernel must be specified".to_string()))?;
let X_owned = X.to_owned();
let y_owned = y.to_owned();
let K = kernel.compute_kernel_matrix(&X_owned, None)?;
let mut K_reg = K.clone();
for i in 0..K_reg.nrows() {
K_reg[[i, i]] += self.alpha;
}
let chol_decomp = utils::robust_cholesky(&K_reg)?;
let mut alpha = utils::triangular_solve(&chol_decomp, &y_owned)?;
let mut weights = Array1::ones(y_owned.len());
let mut prev_log_likelihood = f64::NEG_INFINITY;
for _iteration in 0..self.max_iterations {
let predictions = K.dot(&alpha);
let residuals = &y_owned - &predictions;
weights = self.robust_likelihood.compute_weights(&residuals);
let mut K_weighted = K.clone();
for i in 0..K_weighted.nrows() {
for j in 0..K_weighted.ncols() {
K_weighted[[i, j]] *= (weights[i] * weights[j]).sqrt();
}
K_weighted[[i, i]] += self.alpha;
}
let y_weighted = &y_owned * &weights;
if let Ok(chol_weighted) = utils::robust_cholesky(&K_weighted) {
if let Ok(alpha_new) = utils::triangular_solve(&chol_weighted, &y_weighted) {
alpha = alpha_new;
}
}
let log_likelihood = residuals
.iter()
.map(|&r| self.robust_likelihood.log_likelihood(r))
.sum::<f64>();
if (log_likelihood - prev_log_likelihood).abs() < self.convergence_threshold {
break;
}
prev_log_likelihood = log_likelihood;
}
let final_predictions = K.dot(&alpha);
let final_residuals = &y_owned - &final_predictions;
let outlier_indices = self.outlier_detection_method.detect_outliers(
&final_residuals,
&final_predictions,
&(X_owned.clone(), y_owned.clone()),
);
let robustness_metrics =
RobustnessMetrics::compute(&final_residuals, &weights, &self.robust_likelihood);
Ok(RobustGaussianProcessRegressor {
kernel: self.kernel,
robust_likelihood: self.robust_likelihood.clone(),
outlier_detection_method: self.outlier_detection_method,
outlier_detection_threshold: self.outlier_detection_threshold,
max_iterations: self.max_iterations,
convergence_threshold: self.convergence_threshold,
alpha: self.alpha,
_state: Trained {
kernel,
robust_likelihood: self.robust_likelihood,
training_data: (X_owned, y_owned),
alpha,
cholesky: chol_decomp,
log_likelihood: prev_log_likelihood,
outlier_weights: weights,
outlier_indices,
robustness_metrics,
},
})
}
}
#[allow(non_snake_case)]
impl RobustGaussianProcessRegressor<Trained> {
pub fn trained_state(&self) -> &Trained {
&self._state
}
pub fn outlier_indices(&self) -> &[usize] {
&self._state.outlier_indices
}
pub fn outlier_weights(&self) -> &Array1<f64> {
&self._state.outlier_weights
}
pub fn robustness_metrics(&self) -> &RobustnessMetrics {
&self._state.robustness_metrics
}
pub fn predict_with_robust_uncertainty(
&self,
X: &Array2<f64>,
) -> SklResult<(Array1<f64>, Array1<f64>)> {
let K_star = self
._state
.kernel
.compute_kernel_matrix(&self._state.training_data.0, Some(X))?;
let predictions = K_star.t().dot(&self._state.alpha);
let K_star_star = self._state.kernel.compute_kernel_matrix(X, None)?;
let n_test = X.nrows();
let mut v_squared_sum = Array1::<f64>::zeros(n_test);
for i in 0..n_test {
let k_star_i = K_star.column(i).to_owned();
let v_i = utils::triangular_solve(&self._state.cholesky, &k_star_i)?;
v_squared_sum[i] = v_i.iter().map(|x| x.powi(2)).sum();
}
let base_variance = K_star_star.diag().to_owned() - &v_squared_sum;
let uncertainty_factor = match self._state.robust_likelihood {
RobustLikelihood::StudentT { degrees_of_freedom } => {
degrees_of_freedom / (degrees_of_freedom - 2.0).max(1.0)
}
RobustLikelihood::Laplace { .. } => 2.0, RobustLikelihood::Cauchy { .. } => 10.0, _ => 1.0,
};
let robust_uncertainties = base_variance.map(|x| (x * uncertainty_factor).max(0.0).sqrt());
Ok((predictions, robust_uncertainties))
}
pub fn assess_contamination(&self) -> f64 {
self._state.robustness_metrics.contamination_estimate
}
pub fn compute_influence_function(&self) -> Array1<f64> {
let residuals = &self._state.training_data.1
- &self
._state
.kernel
.compute_kernel_matrix(&self._state.training_data.0, None)
.expect("operation should succeed")
.dot(&self._state.alpha);
residuals
.iter()
.zip(self._state.outlier_weights.iter())
.map(|(&r, &w)| {
let clamped_w = w.clamp(0.0, 1.0);
r.abs() * (1.0 - clamped_w)
})
.collect()
}
pub fn robust_cross_validation(&self, folds: usize) -> SklResult<f64> {
let n = self._state.training_data.0.nrows();
let fold_size = n / folds;
let mut cv_scores = Vec::new();
for fold in 0..folds {
let start_idx = fold * fold_size;
let end_idx = if fold == folds - 1 {
n
} else {
(fold + 1) * fold_size
};
let mut train_indices = Vec::new();
let mut test_indices = Vec::new();
for i in 0..n {
if i >= start_idx && i < end_idx {
test_indices.push(i);
} else {
train_indices.push(i);
}
}
let X_train = self._state.training_data.0.select(Axis(0), &train_indices);
let y_train = self._state.training_data.1.select(Axis(0), &train_indices);
let X_test = self._state.training_data.0.select(Axis(0), &test_indices);
let y_test = self._state.training_data.1.select(Axis(0), &test_indices);
let fold_gp = RobustGaussianProcessRegressor::builder()
.kernel(self._state.kernel.clone_box())
.robust_likelihood(self._state.robust_likelihood.clone())
.outlier_detection_threshold(self.outlier_detection_threshold)
.max_iterations(self.max_iterations)
.alpha(self.alpha)
.build();
if let Ok(fitted) = fold_gp.fit(&X_train, &y_train) {
if let Ok(pred) = fitted.predict(&X_test) {
let mut errors: Vec<f64> = pred
.iter()
.zip(y_test.iter())
.map(|(&p, &y)| (p - y).abs())
.collect();
errors.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median_error = if errors.len().is_multiple_of(2) {
(errors[errors.len() / 2 - 1] + errors[errors.len() / 2]) / 2.0
} else {
errors[errors.len() / 2]
};
cv_scores.push(median_error);
}
}
}
if cv_scores.is_empty() {
return Err(SklearsError::InvalidInput(
"Cross-validation failed".to_string(),
));
}
cv_scores.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median_cv_score = if cv_scores.len() % 2 == 0 {
(cv_scores[cv_scores.len() / 2 - 1] + cv_scores[cv_scores.len() / 2]) / 2.0
} else {
cv_scores[cv_scores.len() / 2]
};
Ok(median_cv_score)
}
}
#[allow(non_snake_case)]
impl Predict<Array2<f64>, Array1<f64>> for RobustGaussianProcessRegressor<Trained> {
fn predict(&self, X: &Array2<f64>) -> SklResult<Array1<f64>> {
let (predictions, _) = self.predict_with_robust_uncertainty(X)?;
Ok(predictions)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use crate::kernels::RBF;
use scirs2_core::ndarray::array;
#[test]
fn test_robust_likelihood_student_t() {
let likelihood = RobustLikelihood::student_t(3.0);
let ll_0 = likelihood.log_likelihood(0.0);
let ll_1 = likelihood.log_likelihood(1.0);
let ll_2 = likelihood.log_likelihood(2.0);
assert!(ll_0 > ll_1);
assert!(ll_1 > ll_2);
assert!(ll_0.is_finite());
}
#[test]
fn test_robust_likelihood_weights() {
let likelihood = RobustLikelihood::student_t(3.0);
let residuals = array![0.0, 1.0, 2.0, 5.0, 10.0];
let weights = likelihood.compute_weights(&residuals);
assert!(weights[0] > weights[1]);
assert!(weights[1] > weights[2]);
assert!(weights[2] > weights[3]);
assert!(weights[3] > weights[4]);
assert!(weights.iter().all(|&w| w > 0.0));
}
#[test]
fn test_robust_gp_fit_predict() {
let X = array![[1.0], [2.0], [3.0], [4.0], [5.0]];
let y = array![1.0, 2.0, 10.0, 4.0, 5.0];
let robust_gp = RobustGaussianProcessRegressor::builder()
.kernel(Box::new(RBF::new(1.0)))
.robust_likelihood(RobustLikelihood::student_t(3.0))
.outlier_detection_threshold(2.5)
.max_iterations(10)
.build();
let trained = robust_gp.fit(&X, &y).expect("model fitting should succeed");
let predictions = trained.predict(&X).expect("prediction should succeed");
assert_eq!(predictions.len(), X.nrows());
}
#[test]
fn test_outlier_detection() {
let X = array![[1.0], [2.0], [3.0], [4.0], [5.0]];
let y = array![1.0, 2.0, 10.0, 4.0, 5.0];
let robust_gp = RobustGaussianProcessRegressor::builder()
.kernel(Box::new(RBF::new(1.0)))
.robust_likelihood(RobustLikelihood::student_t(3.0))
.outlier_detection_threshold(1.5) .build();
let trained = robust_gp.fit(&X, &y).expect("model fitting should succeed");
let outliers = trained.outlier_indices();
assert!(outliers.len() <= y.len());
}
#[test]
fn test_laplace_likelihood() {
let likelihood = RobustLikelihood::laplace(1.0);
let residuals = array![0.0, 1.0, 2.0];
let weights = likelihood.compute_weights(&residuals);
assert!(weights.iter().all(|&w| w.is_finite() && w > 0.0));
}
#[test]
fn test_huber_likelihood() {
let likelihood = RobustLikelihood::huber(1.5);
let small_residual = 1.0; let large_residual = 3.0;
let small_weight = likelihood.compute_weights(&array![small_residual])[0];
let large_weight = likelihood.compute_weights(&array![large_residual])[0];
assert!(small_weight >= large_weight);
}
#[test]
fn test_contamination_mixture_likelihood() {
let likelihood = RobustLikelihood::contamination_mixture(1.0, 10.0, 0.1);
let residuals = array![0.5, 5.0, 0.2];
let weights = likelihood.compute_weights(&residuals);
assert!(weights.iter().all(|&w| (0.0..=1.0).contains(&w)));
assert!(weights[2] > weights[1]); }
#[test]
fn test_robustness_metrics() {
let residuals = array![0.1, 0.2, 5.0, 0.15, 0.3]; let weights = array![1.0, 1.0, 0.1, 1.0, 1.0]; let likelihood = RobustLikelihood::student_t(3.0);
let metrics = RobustnessMetrics::compute(&residuals, &weights, &likelihood);
assert!(metrics.breakdown_point > 0.0);
assert!(metrics.contamination_estimate > 0.0);
assert!(metrics.gross_error_sensitivity > 0.0);
}
#[test]
fn test_robust_uncertainty() {
let X = array![[1.0], [2.0], [3.0], [4.0]];
let y = array![1.0, 2.0, 3.0, 4.0];
let robust_gp = RobustGaussianProcessRegressor::builder()
.kernel(Box::new(RBF::new(1.0)))
.robust_likelihood(RobustLikelihood::student_t(3.0))
.build();
let trained = robust_gp.fit(&X, &y).expect("model fitting should succeed");
let (predictions, uncertainties) = trained
.predict_with_robust_uncertainty(&X)
.expect("operation should succeed");
assert_eq!(predictions.len(), X.nrows());
assert_eq!(uncertainties.len(), X.nrows());
assert!(uncertainties.iter().all(|&u| u >= 0.0));
}
#[test]
fn test_influence_function() {
let X = array![[1.0], [2.0], [3.0], [4.0]];
let y = array![1.0, 2.0, 10.0, 4.0];
let robust_gp = RobustGaussianProcessRegressor::builder()
.kernel(Box::new(RBF::new(1.0)))
.robust_likelihood(RobustLikelihood::student_t(3.0))
.build();
let trained = robust_gp.fit(&X, &y).expect("model fitting should succeed");
let influence = trained.compute_influence_function();
assert_eq!(influence.len(), X.nrows());
for (i, &inf) in influence.iter().enumerate() {
assert!(inf >= 0.0, "Influence at index {} is negative: {}", i, inf);
}
let total_influence: f64 = influence.iter().sum();
assert!(
total_influence >= 0.0,
"Total influence should be non-negative"
);
assert!(influence.iter().any(|&inf| inf.is_finite()));
}
#[test]
fn test_breakdown_points() {
let gaussian = RobustLikelihood::Gaussian;
let student_t = RobustLikelihood::student_t(3.0);
let laplace = RobustLikelihood::laplace(1.0);
assert_eq!(gaussian.breakdown_point(), 0.0);
assert!(student_t.breakdown_point() > 0.0);
assert_eq!(laplace.breakdown_point(), 0.5);
}
#[test]
fn test_outlier_detection_methods() {
let residuals = array![0.1, 0.2, 5.0, 0.15];
let predictions = array![1.0, 2.0, 3.0, 4.0];
let training_data = (
array![[1.0], [2.0], [3.0], [4.0]],
array![1.1, 2.2, 8.0, 4.15], );
let methods = [
OutlierDetectionMethod::StandardizedResiduals { threshold: 2.0 },
OutlierDetectionMethod::MahalanobisDistance { threshold: 2.0 },
OutlierDetectionMethod::Leverage { threshold: 2.0 },
];
for method in &methods {
let outliers = method.detect_outliers(&residuals, &predictions, &training_data);
let _ = outliers; }
}
#[test]
fn test_robust_cross_validation() {
let X = array![[1.0], [2.0], [3.0], [4.0], [5.0], [6.0]];
let y = array![1.0, 2.0, 10.0, 4.0, 5.0, 6.0];
let robust_gp = RobustGaussianProcessRegressor::builder()
.kernel(Box::new(RBF::new(1.0)))
.robust_likelihood(RobustLikelihood::student_t(3.0))
.max_iterations(5)
.build();
let trained = robust_gp.fit(&X, &y).expect("model fitting should succeed");
let cv_score = trained
.robust_cross_validation(3)
.expect("operation should succeed");
assert!(cv_score >= 0.0);
}
}