use approx::assert_abs_diff_eq;
use ndarray::{arr2, array};
use rustyml::metrics::{
Average, ConfusionMatrix, MulticlassConfusionMatrix, accuracy, average_precision, cohen_kappa,
log_loss, precision_recall_curve, roc_auc, roc_curve, top_k_accuracy,
};
#[test]
fn cm_new_perfect_predictions() {
let y_true = array![1.0, 0.0, 1.0, 0.0];
let y_pred = array![1.0, 0.0, 1.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_eq!(cm.get_counts(), (2, 0, 2, 0));
}
#[test]
fn cm_new_all_wrong() {
let y_true = array![1.0, 0.0, 1.0, 0.0];
let y_pred = array![0.0, 1.0, 0.0, 1.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_eq!(cm.get_counts(), (0, 2, 0, 2));
}
#[test]
fn cm_new_mixed() {
let y_true = array![1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0];
let y_pred = array![1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_eq!(cm.get_counts(), (3, 1, 2, 2));
}
#[test]
fn cm_new_thresholding_at_0_5() {
let y_true = array![0.9, 0.1, 0.6, 0.4];
let y_pred = array![0.8, 0.2, 0.3, 0.7];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_eq!(cm.get_counts(), (1, 1, 1, 1));
}
fn cm_3_1_2_2() -> ConfusionMatrix {
let y_true = array![1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0];
let y_pred = array![1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
ConfusionMatrix::new(&y_true, &y_pred)
}
#[test]
fn cm_accuracy_partial() {
let cm = cm_3_1_2_2();
assert_abs_diff_eq!(cm.accuracy(), 0.625, epsilon = 1e-12);
}
#[test]
fn cm_accuracy_range() {
let cm = cm_3_1_2_2();
let acc = cm.accuracy();
assert!((0.0..=1.0).contains(&acc));
}
#[test]
fn cm_accuracy_plus_error_rate_equals_one() {
let cm = cm_3_1_2_2();
assert_abs_diff_eq!(cm.accuracy() + cm.error_rate(), 1.0, epsilon = 1e-12);
}
#[test]
fn cm_accuracy_perfect() {
let y_true = array![1.0, 1.0, 0.0, 0.0];
let y_pred = array![1.0, 1.0, 0.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.accuracy(), 1.0, epsilon = 1e-12);
}
#[test]
fn cm_accuracy_all_wrong() {
let y_true = array![1.0, 1.0, 0.0, 0.0];
let y_pred = array![0.0, 0.0, 1.0, 1.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.accuracy(), 0.0, epsilon = 1e-12);
}
#[test]
fn cm_precision_partial() {
let cm = cm_3_1_2_2();
assert_abs_diff_eq!(cm.precision(), 0.75, epsilon = 1e-12);
}
#[test]
fn cm_precision_perfect() {
let y_true = array![1.0, 1.0, 0.0, 0.0];
let y_pred = array![1.0, 1.0, 0.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.precision(), 1.0, epsilon = 1e-12);
}
#[test]
fn cm_precision_no_positive_predictions() {
let y_true = array![1.0, 0.0];
let y_pred = array![0.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_eq!(cm.get_counts(), (0, 0, 1, 1));
assert_abs_diff_eq!(cm.precision(), 0.0, epsilon = 1e-12);
}
#[test]
fn cm_recall_partial() {
let cm = cm_3_1_2_2();
assert_abs_diff_eq!(cm.recall(), 0.6, epsilon = 1e-12);
}
#[test]
fn cm_recall_no_actual_positives() {
let y_true = array![0.0, 0.0];
let y_pred = array![0.0, 1.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_eq!(cm.get_counts(), (0, 1, 1, 0));
assert_abs_diff_eq!(cm.recall(), 0.0, epsilon = 1e-12);
}
#[test]
fn cm_specificity_partial() {
let cm = cm_3_1_2_2();
assert_abs_diff_eq!(cm.specificity(), 2.0 / 3.0, epsilon = 1e-12);
}
#[test]
fn cm_specificity_no_actual_negatives() {
let y_true = array![1.0, 1.0];
let y_pred = array![1.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_eq!(cm.get_counts(), (1, 0, 0, 1));
assert_abs_diff_eq!(cm.specificity(), 1.0, epsilon = 1e-12);
}
#[test]
fn cm_f1_partial() {
let cm = cm_3_1_2_2();
assert_abs_diff_eq!(cm.f1_score(), 2.0 / 3.0, epsilon = 1e-12);
}
#[test]
fn cm_f1_perfect() {
let y_true = array![1.0, 0.0, 1.0, 0.0];
let y_pred = array![1.0, 0.0, 1.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.f1_score(), 1.0, epsilon = 1e-12);
}
#[test]
fn cm_f1_zero_precision_and_recall() {
let y_true = array![1.0, 0.0];
let y_pred = array![0.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.f1_score(), 0.0, epsilon = 1e-12);
}
#[test]
fn cm_mcc_partial() {
let cm = cm_3_1_2_2();
let expected = 4.0 / (240.0_f64).sqrt();
assert_abs_diff_eq!(cm.mcc(), expected, epsilon = 1e-9);
}
#[test]
fn cm_mcc_perfect() {
let y_true = array![1.0, 1.0, 0.0, 0.0];
let y_pred = array![1.0, 1.0, 0.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.mcc(), 1.0, epsilon = 1e-12);
}
#[test]
fn cm_mcc_degenerate_all_predicted_positive() {
let y_true = array![1.0, 1.0, 0.0, 0.0];
let y_pred = array![1.0, 1.0, 1.0, 1.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.mcc(), 0.0, epsilon = 1e-12);
}
#[test]
fn cm_mcc_degenerate_all_predicted_negative() {
let y_true = array![1.0, 1.0, 0.0, 0.0];
let y_pred = array![0.0, 0.0, 0.0, 0.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.mcc(), 0.0, epsilon = 1e-12);
}
#[test]
fn cm_balanced_accuracy_partial() {
let cm = cm_3_1_2_2();
let expected = 19.0 / 30.0;
assert_abs_diff_eq!(cm.balanced_accuracy(), expected, epsilon = 1e-12);
}
#[test]
fn cm_balanced_accuracy_vs_accuracy_imbalanced() {
let y_true = array![1.0, 1.0, 1.0, 1.0, 0.0];
let y_pred = array![1.0, 1.0, 1.0, 1.0, 1.0];
let cm = ConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.accuracy(), 0.8, epsilon = 1e-12);
assert_abs_diff_eq!(cm.balanced_accuracy(), 0.5, epsilon = 1e-12);
}
#[test]
fn cm_summary_contains_metric_labels() {
let cm = cm_3_1_2_2();
let s = cm.summary();
assert!(s.contains("Accuracy:"));
assert!(s.contains("Precision:"));
assert!(s.contains("Recall:"));
assert!(s.contains("F1 Score:"));
assert!(s.contains("MCC:"));
assert!(s.contains("Balanced Accuracy:"));
assert!(s.contains("TP:"));
assert!(s.contains("FP:"));
assert!(s.contains("TN:"));
assert!(s.contains("FN:"));
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn cm_new_length_mismatch_panics() {
let y_true = array![1.0, 0.0, 1.0];
let y_pred = array![1.0, 0.0];
let _ = ConfusionMatrix::new(&y_true, &y_pred);
}
#[test]
#[should_panic(expected = "input is empty")]
fn cm_new_empty_input_panics() {
let y_true: ndarray::Array1<f64> = array![];
let y_pred: ndarray::Array1<f64> = array![];
let _ = ConfusionMatrix::new(&y_true, &y_pred);
}
#[test]
fn accuracy_perfect() {
let y_true = array![0.0, 1.0, 2.0, 3.0];
let y_pred = array![0.0, 1.0, 2.0, 3.0];
assert_abs_diff_eq!(accuracy(&y_true, &y_pred), 1.0, epsilon = 1e-12);
}
#[test]
fn accuracy_all_wrong() {
let y_true = array![0.0, 1.0, 2.0];
let y_pred = array![1.0, 2.0, 0.0];
assert_abs_diff_eq!(accuracy(&y_true, &y_pred), 0.0, epsilon = 1e-12);
}
#[test]
fn accuracy_partial() {
let y_true = array![0.0, 0.0, 1.0];
let y_pred = array![0.0, 1.0, 1.0];
assert_abs_diff_eq!(accuracy(&y_true, &y_pred), 2.0 / 3.0, epsilon = 1e-9);
}
#[test]
fn accuracy_range_invariant() {
let y_true = array![0.0, 1.0, 2.0, 1.0, 0.0];
let y_pred = array![0.0, 2.0, 2.0, 1.0, 1.0];
let acc = accuracy(&y_true, &y_pred);
assert!((0.0..=1.0).contains(&acc));
}
#[test]
fn accuracy_symmetry() {
let y_true = array![0.0, 1.0, 2.0];
let y_pred = array![0.0, 2.0, 2.0];
assert_abs_diff_eq!(
accuracy(&y_true, &y_pred),
accuracy(&y_pred, &y_true),
epsilon = 1e-12
);
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn accuracy_length_mismatch_panics() {
let y_true = array![0.0, 1.0];
let y_pred = array![0.0, 1.0, 2.0];
let _ = accuracy(&y_true, &y_pred);
}
#[test]
#[should_panic(expected = "input is empty")]
fn accuracy_empty_panics() {
let y_true: ndarray::Array1<f64> = array![];
let y_pred: ndarray::Array1<f64> = array![];
let _ = accuracy(&y_true, &y_pred);
}
#[test]
fn roc_auc_perfect() {
let labels = array![true, true, false, false];
let scores = array![0.9, 0.8, 0.2, 0.1];
assert_abs_diff_eq!(roc_auc(&labels, &scores), 1.0, epsilon = 1e-12);
}
#[test]
fn roc_auc_worst() {
let labels = array![true, true, false, false];
let scores = array![0.1, 0.2, 0.8, 0.9];
assert_abs_diff_eq!(roc_auc(&labels, &scores), 0.0, epsilon = 1e-12);
}
#[test]
fn roc_auc_random_half() {
let labels = array![false, true, true, false];
let scores = array![0.1, 0.35, 0.4, 0.8];
assert_abs_diff_eq!(roc_auc(&labels, &scores), 0.5, epsilon = 1e-12);
}
#[test]
fn roc_auc_with_all_tied_scores() {
let labels = array![true, true, false, false];
let scores = array![0.5, 0.5, 0.5, 0.5];
assert_abs_diff_eq!(roc_auc(&labels, &scores), 0.5, epsilon = 1e-12);
}
#[test]
fn roc_auc_partial_ties() {
let labels = array![true, false, true, false];
let scores = array![0.8, 0.8, 0.2, 0.2];
assert_abs_diff_eq!(roc_auc(&labels, &scores), 0.5, epsilon = 1e-12);
}
#[test]
fn roc_auc_range_invariant() {
let labels = array![true, false, true, false, true];
let scores = array![0.9, 0.6, 0.7, 0.3, 0.4];
let auc = roc_auc(&labels, &scores);
assert!((0.0..=1.0).contains(&auc));
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn roc_auc_length_mismatch_panics() {
let labels = array![true, false];
let scores = array![0.9];
let _ = roc_auc(&labels, &scores);
}
#[test]
#[should_panic(expected = "input is empty")]
fn roc_auc_empty_panics() {
let labels: ndarray::Array1<bool> = array![];
let scores: ndarray::Array1<f64> = array![];
let _ = roc_auc(&labels, &scores);
}
#[test]
#[should_panic(expected = "at least one positive and one negative")]
fn roc_auc_no_positive_panics() {
let labels = array![false, false, false];
let scores = array![0.9, 0.5, 0.1];
let _ = roc_auc(&labels, &scores);
}
#[test]
#[should_panic(expected = "at least one positive and one negative")]
fn roc_auc_no_negative_panics() {
let labels = array![true, true, true];
let scores = array![0.9, 0.5, 0.1];
let _ = roc_auc(&labels, &scores);
}
fn mcm_3class() -> MulticlassConfusionMatrix {
let y_true = array![0usize, 1, 2, 2, 1];
let y_pred = array![0usize, 2, 2, 2, 1];
MulticlassConfusionMatrix::new(&y_true, &y_pred)
}
#[test]
fn mcm_n_classes_and_labels() {
let cm = mcm_3class();
assert_eq!(cm.n_classes(), 3);
assert_eq!(cm.labels(), &[0usize, 1, 2]);
}
#[test]
fn mcm_matrix_cell_values() {
let cm = mcm_3class();
let m = cm.matrix();
assert_eq!(m[[0, 0]], 1);
assert_eq!(m[[0, 1]], 0);
assert_eq!(m[[0, 2]], 0);
assert_eq!(m[[1, 0]], 0);
assert_eq!(m[[1, 1]], 1);
assert_eq!(m[[1, 2]], 1);
assert_eq!(m[[2, 0]], 0);
assert_eq!(m[[2, 1]], 0);
assert_eq!(m[[2, 2]], 2);
}
#[test]
fn mcm_support() {
let cm = mcm_3class();
assert_eq!(cm.support(), vec![1, 2, 2]);
}
#[test]
fn mcm_accuracy() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.accuracy(), 0.8, epsilon = 1e-12);
}
#[test]
fn mcm_per_class_precision() {
let cm = mcm_3class();
let prec = cm.per_class_precision();
assert_eq!(prec.len(), 3);
assert_abs_diff_eq!(prec[0], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(prec[1], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(prec[2], 2.0 / 3.0, epsilon = 1e-12);
}
#[test]
fn mcm_per_class_recall() {
let cm = mcm_3class();
let rec = cm.per_class_recall();
assert_eq!(rec.len(), 3);
assert_abs_diff_eq!(rec[0], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(rec[1], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(rec[2], 1.0, epsilon = 1e-12);
}
#[test]
fn mcm_per_class_f1() {
let cm = mcm_3class();
let f1 = cm.per_class_f1();
assert_eq!(f1.len(), 3);
assert_abs_diff_eq!(f1[0], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(f1[1], 2.0 / 3.0, epsilon = 1e-12);
assert_abs_diff_eq!(f1[2], 0.8, epsilon = 1e-12);
}
#[test]
fn mcm_precision_macro() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.precision(Average::Macro), 8.0 / 9.0, epsilon = 1e-12);
}
#[test]
fn mcm_recall_macro() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.recall(Average::Macro), 5.0 / 6.0, epsilon = 1e-12);
}
#[test]
fn mcm_f1_macro() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.f1(Average::Macro), 37.0 / 45.0, epsilon = 1e-12);
}
#[test]
fn mcm_precision_micro() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.precision(Average::Micro), 0.8, epsilon = 1e-12);
}
#[test]
fn mcm_recall_micro() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.recall(Average::Micro), 0.8, epsilon = 1e-12);
}
#[test]
fn mcm_f1_micro() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.f1(Average::Micro), 0.8, epsilon = 1e-12);
}
#[test]
fn mcm_precision_weighted() {
let cm = mcm_3class();
assert_abs_diff_eq!(
cm.precision(Average::Weighted),
13.0 / 15.0,
epsilon = 1e-12
);
}
#[test]
fn mcm_recall_weighted() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.recall(Average::Weighted), 4.0 / 5.0, epsilon = 1e-12);
}
#[test]
fn mcm_f1_weighted() {
let cm = mcm_3class();
assert_abs_diff_eq!(cm.f1(Average::Weighted), 59.0 / 75.0, epsilon = 1e-12);
}
#[test]
fn mcm_per_class_precision_zero_guard() {
let y_true = array![0usize, 1, 2];
let y_pred = array![0usize, 0, 0];
let cm = MulticlassConfusionMatrix::new(&y_true, &y_pred);
let prec = cm.per_class_precision();
assert_abs_diff_eq!(prec[1], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(prec[2], 0.0, epsilon = 1e-12);
}
#[test]
fn mcm_per_class_recall_zero_guard() {
let y_true = array![0usize, 0, 0];
let y_pred = array![0usize, 1, 2];
let cm = MulticlassConfusionMatrix::new(&y_true, &y_pred);
let rec = cm.per_class_recall();
assert_abs_diff_eq!(rec[1], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(rec[2], 0.0, epsilon = 1e-12);
}
#[test]
fn mcm_perfect_accuracy() {
let y_true = array![0usize, 1, 2, 0, 1];
let y_pred = array![0usize, 1, 2, 0, 1];
let cm = MulticlassConfusionMatrix::new(&y_true, &y_pred);
assert_abs_diff_eq!(cm.accuracy(), 1.0, epsilon = 1e-12);
}
#[test]
fn mcm_single_class_only_in_pred() {
let y_true = array![0usize, 0];
let y_pred = array![0usize, 1];
let cm = MulticlassConfusionMatrix::new(&y_true, &y_pred);
assert_eq!(cm.n_classes(), 2);
assert_eq!(cm.labels(), &[0usize, 1]);
}
#[test]
fn mcm_summary_contains_keywords() {
let cm = mcm_3class();
let s = cm.summary();
assert!(s.contains("accuracy"));
assert!(s.contains("precision"));
assert!(s.contains("recall"));
assert!(s.contains("f1-score"));
assert!(s.contains("support"));
assert!(s.contains("macro avg"));
assert!(s.contains("weighted avg"));
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn mcm_length_mismatch_panics() {
let y_true = array![0usize, 1, 2];
let y_pred = array![0usize, 1];
let _ = MulticlassConfusionMatrix::new(&y_true, &y_pred);
}
#[test]
#[should_panic(expected = "input is empty")]
fn mcm_empty_panics() {
let y_true: ndarray::Array1<usize> = ndarray::Array1::from(vec![]);
let y_pred: ndarray::Array1<usize> = ndarray::Array1::from(vec![]);
let _ = MulticlassConfusionMatrix::new(&y_true, &y_pred);
}
#[test]
fn log_loss_two_samples() {
let y_true = array![0usize, 1];
let y_prob = arr2(&[[0.9, 0.1], [0.2, 0.8]]);
let expected = -(0.9_f64.ln() + 0.8_f64.ln()) / 2.0;
assert_abs_diff_eq!(log_loss(&y_true, &y_prob), expected, epsilon = 1e-9);
}
#[test]
fn log_loss_perfect_probs() {
let y_true = array![0usize, 1];
let y_prob = arr2(&[[1.0, 0.0], [0.0, 1.0]]);
let loss = log_loss(&y_true, &y_prob);
assert!(loss.is_finite() && loss >= 0.0);
assert!(loss < 1e-10);
}
#[test]
fn log_loss_zero_prob_clamped() {
let y_true = array![0usize];
let y_prob = arr2(&[[0.0, 1.0]]);
let loss = log_loss(&y_true, &y_prob);
assert!(loss.is_finite());
assert!(loss > 30.0); }
#[test]
fn log_loss_renormalizes_rows() {
let y_true = array![0usize];
let y_prob = arr2(&[[2.0, 2.0]]);
let expected = 2.0_f64.ln();
assert_abs_diff_eq!(log_loss(&y_true, &y_prob), expected, epsilon = 1e-9);
}
#[test]
fn log_loss_multiclass_three_classes() {
let y_true = array![0usize, 1, 2];
let y_prob = arr2(&[[0.8, 0.1, 0.1], [0.1, 0.7, 0.2], [0.2, 0.2, 0.6]]);
let expected = -(0.8_f64.ln() + 0.7_f64.ln() + 0.6_f64.ln()) / 3.0;
assert_abs_diff_eq!(log_loss(&y_true, &y_prob), expected, epsilon = 1e-9);
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn log_loss_row_mismatch_panics() {
let y_true = array![0usize, 1, 2];
let y_prob = arr2(&[[0.9, 0.1], [0.2, 0.8]]);
let _ = log_loss(&y_true, &y_prob);
}
#[test]
#[should_panic(expected = "input is empty")]
fn log_loss_empty_panics() {
let y_true: ndarray::Array1<usize> = array![];
let y_prob: ndarray::Array2<f64> = ndarray::Array2::zeros((0, 2));
let _ = log_loss(&y_true, &y_prob);
}
#[test]
#[should_panic(expected = "out of range")]
fn log_loss_label_out_of_range_panics() {
let y_true = array![0usize, 5];
let y_prob = arr2(&[[0.9, 0.1], [0.2, 0.8]]);
let _ = log_loss(&y_true, &y_prob);
}
#[test]
fn cohen_kappa_perfect() {
let y_true = array![0usize, 1, 0, 1];
let y_pred = array![0usize, 1, 0, 1];
assert_abs_diff_eq!(cohen_kappa(&y_true, &y_pred), 1.0, epsilon = 1e-12);
}
#[test]
fn cohen_kappa_chance_level() {
let y_true = array![0usize, 0, 1, 1];
let y_pred = array![0usize, 1, 0, 1];
assert_abs_diff_eq!(cohen_kappa(&y_true, &y_pred), 0.0, epsilon = 1e-12);
}
#[test]
fn cohen_kappa_negative() {
let y_true = array![0usize, 0, 1, 1];
let y_pred = array![1usize, 1, 0, 0];
assert_abs_diff_eq!(cohen_kappa(&y_true, &y_pred), -1.0, epsilon = 1e-12);
}
#[test]
fn cohen_kappa_degenerate_all_same_label() {
let y_true = array![0usize, 0, 0, 0];
let y_pred = array![0usize, 0, 0, 0];
assert_abs_diff_eq!(cohen_kappa(&y_true, &y_pred), 1.0, epsilon = 1e-12);
}
#[test]
fn cohen_kappa_range_invariant() {
let y_true = array![0usize, 1, 2, 0, 1];
let y_pred = array![0usize, 2, 2, 1, 1];
let k = cohen_kappa(&y_true, &y_pred);
assert!((-1.0..=1.0).contains(&k));
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn cohen_kappa_length_mismatch_panics() {
let y_true = array![0usize, 1, 2];
let y_pred = array![0usize, 1];
let _ = cohen_kappa(&y_true, &y_pred);
}
#[test]
#[should_panic(expected = "input is empty")]
fn cohen_kappa_empty_panics() {
let y_true: ndarray::Array1<usize> = ndarray::Array1::from(vec![]);
let y_pred: ndarray::Array1<usize> = ndarray::Array1::from(vec![]);
let _ = cohen_kappa(&y_true, &y_pred);
}
#[test]
fn top_k_accuracy_top1() {
let y_true = array![0usize, 1, 2];
let y_prob = arr2(&[[0.7, 0.2, 0.1], [0.3, 0.3, 0.4], [0.1, 0.5, 0.4]]);
assert_abs_diff_eq!(
top_k_accuracy(&y_true, &y_prob, 1),
1.0 / 3.0,
epsilon = 1e-9
);
}
#[test]
fn top_k_accuracy_top2() {
let y_true = array![0usize, 1, 2];
let y_prob = arr2(&[[0.7, 0.2, 0.1], [0.3, 0.3, 0.4], [0.1, 0.5, 0.4]]);
assert_abs_diff_eq!(top_k_accuracy(&y_true, &y_prob, 2), 1.0, epsilon = 1e-12);
}
#[test]
fn top_k_accuracy_k_ge_n_classes() {
let y_true = array![0usize, 1, 2];
let y_prob = arr2(&[[0.7, 0.2, 0.1], [0.3, 0.3, 0.4], [0.1, 0.5, 0.4]]);
assert_abs_diff_eq!(top_k_accuracy(&y_true, &y_prob, 3), 1.0, epsilon = 1e-12);
}
#[test]
fn top_k_accuracy_all_wrong_top1() {
let y_true = array![0usize, 1];
let y_prob = arr2(&[[0.1, 0.5, 0.4], [0.5, 0.1, 0.4]]);
assert_abs_diff_eq!(top_k_accuracy(&y_true, &y_prob, 1), 0.0, epsilon = 1e-12);
}
#[test]
fn top_k_accuracy_tie_boundary_counts_in_favor() {
let y_true = array![0usize];
let y_prob = arr2(&[[0.5, 0.5, 0.5]]);
assert_abs_diff_eq!(top_k_accuracy(&y_true, &y_prob, 1), 1.0, epsilon = 1e-12);
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn top_k_accuracy_length_mismatch_panics() {
let y_true = array![0usize, 1, 2];
let y_prob = arr2(&[[0.7, 0.3], [0.4, 0.6]]);
let _ = top_k_accuracy(&y_true, &y_prob, 1);
}
#[test]
#[should_panic(expected = "input is empty")]
fn top_k_accuracy_empty_panics() {
let y_true: ndarray::Array1<usize> = array![];
let y_prob: ndarray::Array2<f64> = ndarray::Array2::zeros((0, 2));
let _ = top_k_accuracy(&y_true, &y_prob, 1);
}
#[test]
#[should_panic(expected = "must be at least 1")]
fn top_k_accuracy_k_zero_panics() {
let y_true = array![0usize, 1];
let y_prob = arr2(&[[0.7, 0.3], [0.4, 0.6]]);
let _ = top_k_accuracy(&y_true, &y_prob, 0);
}
#[test]
#[should_panic(expected = "out of range")]
fn top_k_accuracy_label_out_of_range_panics() {
let y_true = array![0usize, 5];
let y_prob = arr2(&[[0.9, 0.1], [0.2, 0.8]]);
let _ = top_k_accuracy(&y_true, &y_prob, 1);
}
#[test]
#[should_panic(expected = "must not contain NaN")]
fn top_k_accuracy_nan_true_prob_panics() {
let y_true = array![0usize];
let y_prob = arr2(&[[f64::NAN, 0.9]]);
let _ = top_k_accuracy(&y_true, &y_prob, 1);
}
#[test]
fn average_precision_known_value() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let expected = 5.0 / 6.0;
assert_abs_diff_eq!(
average_precision(&labels, &scores),
expected,
epsilon = 1e-9
);
}
#[test]
fn average_precision_perfect() {
let labels = array![true, true, false, false];
let scores = array![0.9, 0.8, 0.3, 0.1];
assert_abs_diff_eq!(average_precision(&labels, &scores), 1.0, epsilon = 1e-12);
}
#[test]
fn average_precision_single_positive() {
let labels = array![true, false, false, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
assert_abs_diff_eq!(average_precision(&labels, &scores), 1.0, epsilon = 1e-12);
}
#[test]
fn average_precision_single_positive_worst_rank() {
let labels = array![false, false, false, true];
let scores = array![0.9, 0.6, 0.4, 0.1];
assert_abs_diff_eq!(average_precision(&labels, &scores), 0.25, epsilon = 1e-12);
}
#[test]
fn average_precision_range_invariant() {
let labels = array![true, false, true, false, true];
let scores = array![0.9, 0.6, 0.7, 0.3, 0.4];
let ap = average_precision(&labels, &scores);
assert!((0.0..=1.0).contains(&ap));
}
#[test]
#[should_panic(expected = "at least one positive label")]
fn average_precision_no_positive_panics() {
let labels = array![false, false, false];
let scores = array![0.9, 0.5, 0.1];
let _ = average_precision(&labels, &scores);
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn average_precision_length_mismatch_panics() {
let labels = array![true, false];
let scores = array![0.9];
let _ = average_precision(&labels, &scores);
}
#[test]
#[should_panic(expected = "input is empty")]
fn average_precision_empty_panics() {
let labels: ndarray::Array1<bool> = array![];
let scores: ndarray::Array1<f64> = array![];
let _ = average_precision(&labels, &scores);
}
#[test]
fn roc_curve_specific_points() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (fpr, tpr, thresholds) = roc_curve(&labels, &scores);
assert_eq!(fpr.len(), 5);
assert_eq!(tpr.len(), 5);
assert_eq!(thresholds.len(), 5);
assert_abs_diff_eq!(fpr[0], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(tpr[0], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(fpr[1], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(tpr[1], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(thresholds[1], 0.9, epsilon = 1e-12);
assert_abs_diff_eq!(fpr[2], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(tpr[2], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(fpr[3], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(tpr[3], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(fpr[4], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(tpr[4], 1.0, epsilon = 1e-12);
}
#[test]
fn roc_curve_first_point_is_origin() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (fpr, tpr, _) = roc_curve(&labels, &scores);
assert_abs_diff_eq!(fpr[0], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(tpr[0], 0.0, epsilon = 1e-12);
}
#[test]
fn roc_curve_last_tpr_is_one() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (_, tpr, _) = roc_curve(&labels, &scores);
let n = tpr.len();
assert_abs_diff_eq!(tpr[n - 1], 1.0, epsilon = 1e-12);
}
#[test]
fn roc_curve_lengths_equal() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (fpr, tpr, thresholds) = roc_curve(&labels, &scores);
assert_eq!(fpr.len(), tpr.len());
assert_eq!(fpr.len(), thresholds.len());
}
#[test]
fn roc_curve_trapezoidal_area_matches_roc_auc() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let auc_direct = roc_auc(&labels, &scores);
let (fpr, tpr, _) = roc_curve(&labels, &scores);
let n = fpr.len();
let trapezoid_area: f64 = (0..n - 1)
.map(|i| (fpr[i + 1] - fpr[i]) * (tpr[i + 1] + tpr[i]) / 2.0)
.sum();
assert_abs_diff_eq!(trapezoid_area, auc_direct, epsilon = 1e-12);
assert_abs_diff_eq!(trapezoid_area, 0.75, epsilon = 1e-12);
}
#[test]
fn roc_curve_fpr_nondecreasing() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (fpr, _, _) = roc_curve(&labels, &scores);
for i in 0..fpr.len() - 1 {
assert!(fpr[i] <= fpr[i + 1], "fpr not non-decreasing at i={}", i);
}
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn roc_curve_length_mismatch_panics() {
let labels = array![true, false, true];
let scores = array![0.9, 0.6];
let _ = roc_curve(&labels, &scores);
}
#[test]
#[should_panic(expected = "input is empty")]
fn roc_curve_empty_panics() {
let labels: ndarray::Array1<bool> = array![];
let scores: ndarray::Array1<f64> = array![];
let _ = roc_curve(&labels, &scores);
}
#[test]
#[should_panic(expected = "at least one positive and one negative")]
fn roc_curve_no_positive_panics() {
let labels = array![false, false, false];
let scores = array![0.9, 0.5, 0.1];
let _ = roc_curve(&labels, &scores);
}
#[test]
#[should_panic(expected = "at least one positive and one negative")]
fn roc_curve_no_negative_panics() {
let labels = array![true, true, true];
let scores = array![0.9, 0.5, 0.1];
let _ = roc_curve(&labels, &scores);
}
#[test]
fn precision_recall_curve_specific_points() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (precision, recall, thresholds) = precision_recall_curve(&labels, &scores);
assert_eq!(precision.len(), recall.len());
assert_eq!(thresholds.len(), precision.len() - 1);
let last = precision.len() - 1;
assert_abs_diff_eq!(precision[last], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(recall[last], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(precision[0], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(recall[0], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(thresholds[0], 0.9, epsilon = 1e-12);
assert_abs_diff_eq!(precision[1], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(recall[1], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(thresholds[1], 0.6, epsilon = 1e-12);
assert_abs_diff_eq!(precision[2], 2.0 / 3.0, epsilon = 1e-9);
assert_abs_diff_eq!(recall[2], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(thresholds[2], 0.4, epsilon = 1e-12);
assert_abs_diff_eq!(precision[3], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(recall[3], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(thresholds[3], 0.1, epsilon = 1e-12);
}
#[test]
fn precision_recall_curve_closing_point() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (precision, recall, _) = precision_recall_curve(&labels, &scores);
let n = precision.len();
assert_abs_diff_eq!(precision[n - 1], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(recall[n - 1], 0.0, epsilon = 1e-12);
}
#[test]
fn precision_recall_curve_length_invariant() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (precision, recall, thresholds) = precision_recall_curve(&labels, &scores);
assert_eq!(precision.len(), recall.len());
assert_eq!(thresholds.len(), precision.len() - 1);
}
#[test]
fn precision_recall_curve_recall_nondecreasing_before_close() {
let labels = array![true, false, true, false];
let scores = array![0.9, 0.6, 0.4, 0.1];
let (_, recall, _) = precision_recall_curve(&labels, &scores);
let n = recall.len();
for i in 0..n - 2 {
assert!(
recall[i] <= recall[i + 1],
"recall not non-decreasing at i={}",
i
);
}
}
#[test]
fn precision_recall_curve_perfect() {
let labels = array![true, true, false, false];
let scores = array![0.9, 0.8, 0.3, 0.1];
let (precision, recall, _) = precision_recall_curve(&labels, &scores);
assert_abs_diff_eq!(precision[0], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(precision[1], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(recall[1], 1.0, epsilon = 1e-12);
}
#[test]
#[should_panic(expected = "dimension mismatch")]
fn precision_recall_curve_length_mismatch_panics() {
let labels = array![true, false, true];
let scores = array![0.9, 0.6];
let _ = precision_recall_curve(&labels, &scores);
}
#[test]
#[should_panic(expected = "input is empty")]
fn precision_recall_curve_empty_panics() {
let labels: ndarray::Array1<bool> = array![];
let scores: ndarray::Array1<f64> = array![];
let _ = precision_recall_curve(&labels, &scores);
}
#[test]
#[should_panic(expected = "at least one positive label")]
fn precision_recall_curve_no_positive_panics() {
let labels = array![false, false, false];
let scores = array![0.9, 0.5, 0.1];
let _ = precision_recall_curve(&labels, &scores);
}
#[test]
#[should_panic(expected = "must not contain NaN")]
fn roc_auc_with_nan_score_panics() {
let labels = array![false, true, false, true];
let scores = array![0.1, 0.4, 0.35, f64::NAN];
let _ = roc_auc(&labels, &scores);
}
#[test]
#[should_panic(expected = "must not contain NaN")]
fn average_precision_with_nan_score_panics() {
let labels = array![true, true, false, false];
let scores = array![f64::NAN, 0.8, 0.3, 0.1];
let _ = average_precision(&labels, &scores);
}
#[test]
#[should_panic(expected = "must not contain NaN")]
fn roc_curve_with_nan_score_panics() {
let labels = array![true, false, true, false];
let scores = array![f64::NAN, 0.6, 0.4, 0.1];
let _ = roc_curve(&labels, &scores);
}
#[test]
#[should_panic(expected = "must not contain NaN")]
fn precision_recall_curve_with_nan_score_panics() {
let labels = array![true, false, true, false];
let scores = array![0.9, f64::NAN, 0.4, 0.1];
let _ = precision_recall_curve(&labels, &scores);
}