model-selection-rs 0.1.0

Cross-validation and model-selection utilities for Rust: stratified / group-aware / time-series splitting, nested CV, and learning & validation curves. Dependency-light, composes with any modeling crate.
Documentation
//! Feature-gated adapters wrapping `smartcore::metrics` as [`Scorer`]s.
//!
//! Enabled by the `smartcore-metrics` feature. These wrap metrics that already
//! exist in mature form in `smartcore` (F1, ROC-AUC) rather than reimplementing
//! them here — following this project's convention of preferring an existing,
//! well-tested crate over duplication. If you do not already depend on
//! `smartcore`, leave the feature off and use the built-in scorers.

use ndarray::Array1;
use smartcore::metrics::{ClassificationMetrics, Metrics};

use super::Scorer;

/// F1 score (harmonic mean of precision and recall) via `smartcore`.
///
/// Assumes binary targets encoded as `0.0` / `1.0`.
#[derive(Debug, Clone, Copy)]
pub struct SmartcoreF1 {
    /// The `beta` weighting (1.0 for the standard F1).
    pub beta: f64,
}

impl Default for SmartcoreF1 {
    fn default() -> Self {
        Self { beta: 1.0 }
    }
}

impl Scorer for SmartcoreF1 {
    fn score(&self, y_true: &Array1<f64>, y_pred: &Array1<f64>) -> f64 {
        // smartcore metrics operate on its own array traits; `Vec<f64>`
        // implements `ArrayView1<f64>`, so a plain vec is the simplest bridge.
        let t = y_true.to_vec();
        let p = y_pred.to_vec();
        ClassificationMetrics::<f64>::f1(self.beta).get_score(&t, &p)
    }
    fn name(&self) -> &str {
        "f1"
    }
}

/// ROC-AUC via `smartcore`.
///
/// `y_pred` should carry the positive-class scores/probabilities, not hard
/// labels; `y_true` the `0.0` / `1.0` ground truth.
#[derive(Debug, Clone, Copy, Default)]
pub struct SmartcoreRocAuc;

impl Scorer for SmartcoreRocAuc {
    fn score(&self, y_true: &Array1<f64>, y_pred: &Array1<f64>) -> f64 {
        let t = y_true.to_vec();
        let p = y_pred.to_vec();
        ClassificationMetrics::<f64>::roc_auc_score().get_score(&t, &p)
    }
    fn name(&self) -> &str {
        "roc_auc"
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ndarray::array;

    #[test]
    fn f1_perfect_prediction_is_one() {
        let t = array![0.0, 1.0, 1.0, 0.0, 1.0];
        let f1 = SmartcoreF1::default().score(&t, &t);
        assert!((f1 - 1.0).abs() < 1e-9, "f1 = {f1}");
    }

    #[test]
    fn roc_auc_perfect_ranking_is_one() {
        let t = array![0.0, 0.0, 1.0, 1.0];
        let scores = array![0.1, 0.2, 0.8, 0.9];
        let auc = SmartcoreRocAuc.score(&t, &scores);
        assert!((auc - 1.0).abs() < 1e-9, "auc = {auc}");
    }

    #[test]
    fn matches_calling_smartcore_directly() {
        let t = array![0.0, 1.0, 1.0, 0.0, 1.0, 0.0];
        let p = array![0.0, 1.0, 0.0, 0.0, 1.0, 1.0];
        let via_adapter = SmartcoreF1::default().score(&t, &p);
        let direct = ClassificationMetrics::<f64>::f1(1.0).get_score(&t.to_vec(), &p.to_vec());
        assert_eq!(via_adapter, direct);
    }
}