use core::fmt::{Display, Formatter, Result};
use num_traits::Float;
use crate::math::scaling::ScalingMethod;
#[derive(Debug, Clone, PartialEq)]
pub struct Diagnostics<T> {
pub rmse: T,
pub mae: T,
pub r_squared: T,
pub aic: Option<T>,
pub aicc: Option<T>,
pub effective_df: Option<T>,
pub residual_sd: T,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DiagnosticsState<T> {
pub n: usize,
pub sum_y: T,
pub sum_y_sq: T,
pub sum_r: T,
pub sum_r_sq: T,
pub sum_abs_r: T,
}
impl<T: Float> Default for DiagnosticsState<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Float> DiagnosticsState<T> {
pub fn new() -> Self {
Self {
n: 0,
sum_y: T::zero(),
sum_y_sq: T::zero(),
sum_r: T::zero(),
sum_r_sq: T::zero(),
sum_abs_r: T::zero(),
}
}
pub fn update(&mut self, y: &[T], y_smooth: &[T]) {
for (&yi, &ys) in y.iter().zip(y_smooth.iter()) {
let r = yi - ys;
self.n += 1;
self.sum_y = self.sum_y + yi;
self.sum_y_sq = self.sum_y_sq + yi * yi;
self.sum_r = self.sum_r + r;
self.sum_r_sq = self.sum_r_sq + r * r;
self.sum_abs_r = self.sum_abs_r + r.abs();
}
}
pub fn finalize(&self) -> Diagnostics<T> {
let n_t = T::from(self.n).unwrap_or(T::one());
if self.n == 0 {
return Diagnostics {
rmse: T::zero(),
mae: T::zero(),
r_squared: T::zero(),
aic: None,
aicc: None,
effective_df: None,
residual_sd: T::zero(),
};
}
let rmse = (self.sum_r_sq / n_t).sqrt();
let mae = self.sum_abs_r / n_t;
let ss_tot = self.sum_y_sq - (self.sum_y * self.sum_y) / n_t;
let r_squared = if ss_tot > T::from(1e-12).unwrap() * self.sum_y_sq.abs() {
T::one() - self.sum_r_sq / ss_tot
} else if self.sum_r_sq < T::from(1e-12).unwrap() * self.sum_y_sq.abs()
|| self.sum_r_sq == T::zero()
{
T::one()
} else {
T::zero()
};
let residual_sd = if self.n > 1 {
let var_r = (self.sum_r_sq - (self.sum_r * self.sum_r) / n_t) / (n_t - T::one());
var_r.max(T::zero()).sqrt()
} else {
rmse
};
Diagnostics {
rmse,
mae,
r_squared,
aic: None,
aicc: None,
effective_df: None,
residual_sd,
}
}
}
impl<T: Float> Diagnostics<T> {
const LINEAR_PARAMS: f64 = 2.0;
const MIN_TUNED_SCALE: f64 = 1e-12;
const MAD_TO_STD_FACTOR: f64 = 1.4826;
pub fn compute(y: &[T], y_smooth: &[T], residuals: &[T], std_errors: Option<&[T]>) -> Self {
let rmse = Self::calculate_rmse(y, y_smooth);
let mae = Self::calculate_mae(y, y_smooth);
let r_squared = Self::calculate_r_squared(y, y_smooth);
let residual_sd = Self::calculate_residual_sd(residuals);
let effective_df = std_errors.and_then(|se| Self::calculate_effective_df(se, residual_sd));
let (aic, aicc) = if let Some(df) = effective_df {
(
Some(Self::calculate_aic(residuals, df)),
Some(Self::calculate_aicc(residuals, df)),
)
} else {
(None, None)
};
Diagnostics {
rmse,
mae,
r_squared,
aic,
aicc,
effective_df,
residual_sd,
}
}
fn calculate_rss(residuals: &[T]) -> T {
residuals.iter().fold(T::zero(), |acc, &r| acc + r * r)
}
pub fn calculate_rmse(y: &[T], y_smooth: &[T]) -> T {
let n_t = T::from(y.len()).unwrap_or(T::one());
let rss = y
.iter()
.zip(y_smooth.iter())
.fold(T::zero(), |acc, (&yi, &ys)| {
let r = yi - ys;
acc + r * r
});
(rss / n_t).sqrt()
}
pub fn calculate_mae(y: &[T], y_smooth: &[T]) -> T {
let n_t = T::from(y.len()).unwrap_or(T::one());
let sum = y
.iter()
.zip(y_smooth.iter())
.fold(T::zero(), |acc, (&yi, &ys)| acc + (yi - ys).abs());
sum / n_t
}
pub fn calculate_r_squared(y: &[T], y_smooth: &[T]) -> T {
let n = y.len();
if n == 1 {
return T::one();
}
let n_t = T::from(n).unwrap_or(T::one());
let sum = y.iter().copied().fold(T::zero(), |acc, v| acc + v);
let mean = sum / n_t;
let (ss_tot, ss_res) =
y.iter()
.zip(y_smooth.iter())
.fold((T::zero(), T::zero()), |(tot, res), (&yi, &ys)| {
let deviation = yi - mean;
let residual = yi - ys;
(tot + deviation * deviation, res + residual * residual)
});
if ss_tot == T::zero() {
if ss_res == T::zero() {
T::one() } else {
T::zero() }
} else {
T::one() - ss_res / ss_tot
}
}
pub fn calculate_residual_sd(residuals: &[T]) -> T {
let n = residuals.len();
let scale_const = T::from(Self::MAD_TO_STD_FACTOR).unwrap();
if n == 1 {
return residuals[0].abs() * scale_const;
}
let mut vals = residuals.to_vec();
let mad = ScalingMethod::MAD.compute(&mut vals);
if mad > T::zero() {
mad * scale_const
} else {
let min_eps = T::from(Self::MIN_TUNED_SCALE).unwrap();
min_eps * scale_const
}
}
fn calculate_effective_df(std_errors: &[T], residual_sd: T) -> Option<T> {
if residual_sd <= T::zero() {
return None;
}
let leverage_sum = std_errors.iter().fold(T::zero(), |acc, &se| {
let ratio = se / residual_sd;
let mut leverage = ratio * ratio;
if leverage < T::zero() {
leverage = T::zero();
} else if leverage > T::one() {
leverage = T::one();
}
acc + leverage
});
if leverage_sum > T::zero() {
Some(leverage_sum)
} else {
None
}
}
pub fn calculate_effective_df_from_leverages(leverage_values: &[T]) -> T {
leverage_values
.iter()
.copied()
.fold(T::zero(), |acc, v| acc + v)
}
pub fn calculate_aic(residuals: &[T], effective_df: T) -> T {
let n = T::from(residuals.len()).unwrap_or(T::one());
let rss = Self::calculate_rss(residuals);
if rss <= T::zero() || n <= T::zero() {
return T::infinity();
}
n * (rss / n).ln() + T::from(Self::LINEAR_PARAMS).unwrap() * effective_df
}
pub fn calculate_aicc(residuals: &[T], effective_df: T) -> T {
let n = T::from(residuals.len()).unwrap_or(T::one());
let aic = Self::calculate_aic(residuals, effective_df);
let denom = n - effective_df - T::one();
if denom <= T::zero() {
return T::infinity();
}
let correction =
(T::from(Self::LINEAR_PARAMS).unwrap() * effective_df * (effective_df + T::one()))
/ denom;
aic + correction
}
}
impl<T: Float + Display> Display for Diagnostics<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
writeln!(f, "LOESS Diagnostics:")?;
writeln!(f, " RMSE: {:.6}", self.rmse)?;
writeln!(f, " MAE: {:.6}", self.mae)?;
writeln!(f, " R²: {:.6}", self.r_squared)?;
writeln!(f, " Residual SD: {:.6}", self.residual_sd)?;
if let Some(df) = self.effective_df {
writeln!(f, " Effective DF: {:.2}", df)?;
}
if let Some(aic) = self.aic {
writeln!(f, " AIC: {:.2}", aic)?;
}
if let Some(aicc) = self.aicc {
writeln!(f, " AICc: {:.2}", aicc)?;
}
Ok(())
}
}