Skip to main content

model_selection_rs/scoring/
smartcore_adapter.rs

1//! Feature-gated adapters wrapping `smartcore::metrics` as [`Scorer`]s.
2//!
3//! Enabled by the `smartcore-metrics` feature. These wrap metrics that already
4//! exist in mature form in `smartcore` (F1, ROC-AUC) rather than reimplementing
5//! them here — following this project's convention of preferring an existing,
6//! well-tested crate over duplication. If you do not already depend on
7//! `smartcore`, leave the feature off and use the built-in scorers.
8
9use ndarray::Array1;
10use smartcore::metrics::{ClassificationMetrics, Metrics};
11
12use super::Scorer;
13
14/// F1 score (harmonic mean of precision and recall) via `smartcore`.
15///
16/// Assumes binary targets encoded as `0.0` / `1.0`.
17#[derive(Debug, Clone, Copy)]
18pub struct SmartcoreF1 {
19    /// The `beta` weighting (1.0 for the standard F1).
20    pub beta: f64,
21}
22
23impl Default for SmartcoreF1 {
24    fn default() -> Self {
25        Self { beta: 1.0 }
26    }
27}
28
29impl Scorer for SmartcoreF1 {
30    fn score(&self, y_true: &Array1<f64>, y_pred: &Array1<f64>) -> f64 {
31        // smartcore metrics operate on its own array traits; `Vec<f64>`
32        // implements `ArrayView1<f64>`, so a plain vec is the simplest bridge.
33        let t = y_true.to_vec();
34        let p = y_pred.to_vec();
35        ClassificationMetrics::<f64>::f1(self.beta).get_score(&t, &p)
36    }
37    fn name(&self) -> &str {
38        "f1"
39    }
40}
41
42/// ROC-AUC via `smartcore`.
43///
44/// `y_pred` should carry the positive-class scores/probabilities, not hard
45/// labels; `y_true` the `0.0` / `1.0` ground truth.
46#[derive(Debug, Clone, Copy, Default)]
47pub struct SmartcoreRocAuc;
48
49impl Scorer for SmartcoreRocAuc {
50    fn score(&self, y_true: &Array1<f64>, y_pred: &Array1<f64>) -> f64 {
51        let t = y_true.to_vec();
52        let p = y_pred.to_vec();
53        ClassificationMetrics::<f64>::roc_auc_score().get_score(&t, &p)
54    }
55    fn name(&self) -> &str {
56        "roc_auc"
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use ndarray::array;
64
65    #[test]
66    fn f1_perfect_prediction_is_one() {
67        let t = array![0.0, 1.0, 1.0, 0.0, 1.0];
68        let f1 = SmartcoreF1::default().score(&t, &t);
69        assert!((f1 - 1.0).abs() < 1e-9, "f1 = {f1}");
70    }
71
72    #[test]
73    fn roc_auc_perfect_ranking_is_one() {
74        let t = array![0.0, 0.0, 1.0, 1.0];
75        let scores = array![0.1, 0.2, 0.8, 0.9];
76        let auc = SmartcoreRocAuc.score(&t, &scores);
77        assert!((auc - 1.0).abs() < 1e-9, "auc = {auc}");
78    }
79
80    #[test]
81    fn matches_calling_smartcore_directly() {
82        let t = array![0.0, 1.0, 1.0, 0.0, 1.0, 0.0];
83        let p = array![0.0, 1.0, 0.0, 0.0, 1.0, 1.0];
84        let via_adapter = SmartcoreF1::default().score(&t, &p);
85        let direct = ClassificationMetrics::<f64>::f1(1.0).get_score(&t.to_vec(), &p.to_vec());
86        assert_eq!(via_adapter, direct);
87    }
88}