use crate::error::{DatarustError, Result};
fn check_lengths(y_true: &[f64], y_pred: &[f64]) -> Result<()> {
if y_true.is_empty() {
return Err(DatarustError::EmptyInput("y_true is empty".into()));
}
if y_true.len() != y_pred.len() {
return Err(DatarustError::ShapeMismatch {
expected: format!("{} predictions", y_true.len()),
actual: format!("{} predictions", y_pred.len()),
});
}
Ok(())
}
pub fn confusion_matrix(y_true: &[f64], y_pred: &[f64]) -> Result<[[usize; 2]; 2]> {
check_lengths(y_true, y_pred)?;
let mut cm = [[0_usize; 2]; 2];
for (&t, &p) in y_true.iter().zip(y_pred.iter()) {
let ti = if t >= 0.5 { 1 } else { 0 };
let pi = if p >= 0.5 { 1 } else { 0 };
cm[ti][pi] += 1;
}
Ok(cm)
}
pub fn accuracy_score(y_true: &[f64], y_pred: &[f64]) -> Result<f64> {
check_lengths(y_true, y_pred)?;
let n = y_true.len();
let correct = y_true
.iter()
.zip(y_pred.iter())
.filter(|(t, p)| (**t >= 0.5) == (**p >= 0.5))
.count();
Ok(correct as f64 / n as f64)
}
pub fn precision_score(y_true: &[f64], y_pred: &[f64]) -> Result<f64> {
let cm = confusion_matrix(y_true, y_pred)?;
let tp = cm[1][1];
let fp = cm[0][1];
if tp + fp == 0 {
return Ok(0.0);
}
Ok(tp as f64 / (tp + fp) as f64)
}
pub fn recall_score(y_true: &[f64], y_pred: &[f64]) -> Result<f64> {
let cm = confusion_matrix(y_true, y_pred)?;
let tp = cm[1][1];
let fn_ = cm[1][0];
if tp + fn_ == 0 {
return Ok(0.0);
}
Ok(tp as f64 / (tp + fn_) as f64)
}
pub fn f1_score(y_true: &[f64], y_pred: &[f64]) -> Result<f64> {
let p = precision_score(y_true, y_pred)?;
let r = recall_score(y_true, y_pred)?;
if p + r == 0.0 {
return Ok(0.0);
}
Ok(2.0 * p * r / (p + r))
}
pub fn log_loss(y_true: &[f64], y_proba: &[f64], eps: f64) -> Result<f64> {
check_lengths(y_true, y_proba)?;
let n = y_true.len() as f64;
let eps = eps.max(f64::MIN_POSITIVE);
let mut sum = 0.0;
for (&t, &p) in y_true.iter().zip(y_proba.iter()) {
let pc = p.clamp(eps, 1.0 - eps);
let ti = if t >= 0.5 { 1.0 } else { 0.0 };
sum += ti * pc.ln() + (1.0 - ti) * (1.0 - pc).ln();
}
Ok(-sum / n)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> (Vec<f64>, Vec<f64>) {
(
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
vec![0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0],
)
}
#[test]
fn confusion_matrix_known() {
let (y_true, y_pred) = sample();
let cm = confusion_matrix(&y_true, &y_pred).unwrap();
assert_eq!(cm, [[2, 1], [1, 3]]);
}
#[test]
fn accuracy_matches_hand_computation() {
let (y_true, y_pred) = sample();
let acc = accuracy_score(&y_true, &y_pred).unwrap();
assert!((acc - 5.0 / 7.0).abs() < 1e-12);
}
#[test]
fn precision_matches_hand_computation() {
let (y_true, y_pred) = sample();
let p = precision_score(&y_true, &y_pred).unwrap();
assert!((p - 0.75).abs() < 1e-12);
}
#[test]
fn recall_matches_hand_computation() {
let (y_true, y_pred) = sample();
let r = recall_score(&y_true, &y_pred).unwrap();
assert!((r - 0.75).abs() < 1e-12);
}
#[test]
fn f1_matches_hand_computation() {
let (y_true, y_pred) = sample();
let f1 = f1_score(&y_true, &y_pred).unwrap();
assert!((f1 - 0.75).abs() < 1e-12);
}
#[test]
fn perfect_classifier() {
let y = vec![0.0, 1.0, 0.0, 1.0];
assert!((accuracy_score(&y, &y).unwrap() - 1.0).abs() < 1e-12);
assert!((precision_score(&y, &y).unwrap() - 1.0).abs() < 1e-12);
assert!((recall_score(&y, &y).unwrap() - 1.0).abs() < 1e-12);
assert!((f1_score(&y, &y).unwrap() - 1.0).abs() < 1e-12);
}
#[test]
fn all_wrong_classifier() {
let y_true = vec![0.0, 1.0, 0.0, 1.0];
let y_pred = vec![1.0, 0.0, 1.0, 0.0];
assert!((accuracy_score(&y_true, &y_pred).unwrap() - 0.0).abs() < 1e-12);
assert!((precision_score(&y_true, &y_pred).unwrap() - 0.0).abs() < 1e-12);
assert!((recall_score(&y_true, &y_pred).unwrap() - 0.0).abs() < 1e-12);
}
#[test]
fn log_loss_perfect_confident() {
let y = vec![0.0, 1.0];
let p = vec![1e-15, 1.0 - 1e-15];
let ll = log_loss(&y, &p, 1e-15).unwrap();
assert!(ll < 1e-10, "log loss should be ~0, got {ll}");
}
#[test]
fn log_loss_known_value() {
let y_true = vec![0.0, 0.0, 1.0, 1.0];
let p = vec![0.1, 0.2, 0.8, 0.9];
let ll = log_loss(&y_true, &p, 1e-15).unwrap();
let expected = -(0.9_f64.ln() + 0.8_f64.ln() + 0.8_f64.ln() + 0.9_f64.ln()) / 4.0;
assert!((ll - expected).abs() < 1e-12);
}
#[test]
fn no_positive_predictions_precision_zero() {
let y_true = vec![1.0, 1.0];
let y_pred = vec![0.0, 0.0];
assert!((precision_score(&y_true, &y_pred).unwrap() - 0.0).abs() < 1e-12);
}
#[test]
fn length_mismatch_errors() {
let err = accuracy_score(&[1.0, 0.0], &[1.0]).unwrap_err();
assert!(matches!(err, DatarustError::ShapeMismatch { .. }));
}
#[test]
fn empty_errors() {
let err = accuracy_score(&[], &[]).unwrap_err();
assert!(matches!(err, DatarustError::EmptyInput(_)));
}
}