Skip to main content

rill_ml/evaluate/
progressive.rs

1//! Progressive evaluation implementation.
2
3use crate::error::{RillError, ensure_finite, ensure_finite_target};
4use crate::traits::{Metric, OnlineBinaryClassifier, OnlineRegressor};
5
6/// A single regression sample.
7#[derive(Debug, Clone)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct RegressionSample {
10    /// Feature vector.
11    pub features: Vec<f64>,
12    /// Target value.
13    pub target: f64,
14}
15
16/// A single binary classification sample.
17#[derive(Debug, Clone)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct BinaryClassificationSample {
20    /// Feature vector.
21    pub features: Vec<f64>,
22    /// Boolean label.
23    pub target: bool,
24}
25
26/// A single step of progressive evaluation, recording the prediction made
27/// *before* learning from this sample.
28#[derive(Debug, Clone)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct ProgressiveStep {
31    /// Zero-based index of the sample in the stream.
32    pub index: usize,
33    /// The metric value after incorporating this prediction.
34    pub metric_value: Option<f64>,
35}
36
37/// Run progressive evaluation on a regression stream.
38///
39/// The model is updated in place. Returns the final metric value.
40///
41/// The evaluation order is strictly `predict → metric.update → learn`.
42pub fn evaluate_regression<M, Met>(
43    model: &mut M,
44    metric: &mut Met,
45    samples: impl IntoIterator<Item = RegressionSample>,
46) -> Result<Option<f64>, RillError>
47where
48    M: OnlineRegressor,
49    Met: Metric<Truth = f64, Prediction = f64>,
50{
51    evaluate_regression_with_steps(model, metric, samples).map(|(v, _)| v)
52}
53
54/// Like [`evaluate_regression`] but also collects per-step records.
55pub fn evaluate_regression_with_steps<M, Met>(
56    model: &mut M,
57    metric: &mut Met,
58    samples: impl IntoIterator<Item = RegressionSample>,
59) -> Result<(Option<f64>, Vec<ProgressiveStep>), RillError>
60where
61    M: OnlineRegressor,
62    Met: Metric<Truth = f64, Prediction = f64>,
63{
64    let mut steps = Vec::new();
65    for (i, sample) in samples.into_iter().enumerate() {
66        // 0. validate inputs (defense-in-depth before predict)
67        ensure_finite_target(sample.target)?;
68        // 1. predict (no state change)
69        let prediction = model.predict(&sample.features)?;
70        // 1a. validate prediction before passing to metric
71        ensure_finite("prediction", prediction)?;
72        // 2. update metric with truth and prediction
73        metric.update(sample.target, prediction)?;
74        // 3. learn from this sample
75        model.learn(&sample.features, sample.target)?;
76        steps.push(ProgressiveStep {
77            index: i,
78            metric_value: metric.value(),
79        });
80    }
81    Ok((metric.value(), steps))
82}
83
84/// Run progressive evaluation on a binary classification stream.
85pub fn evaluate_binary_classification<M, Met>(
86    model: &mut M,
87    metric: &mut Met,
88    samples: impl IntoIterator<Item = BinaryClassificationSample>,
89) -> Result<Option<f64>, RillError>
90where
91    M: OnlineBinaryClassifier,
92    Met: Metric<Truth = bool, Prediction = bool>,
93{
94    for sample in samples.into_iter() {
95        // prediction is bool for classifiers — no ensure_finite needed.
96        let prediction = model.predict(&sample.features)?;
97        metric.update(sample.target, prediction)?;
98        model.learn(&sample.features, sample.target)?;
99    }
100    Ok(metric.value())
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::metrics::Mae;
107    use crate::models::MeanRegressor;
108
109    #[test]
110    fn progressive_evaluates_before_learning() {
111        // MeanRegressor: first prediction is initial_prediction (0.0),
112        // then mean of targets seen so far.
113        let mut model = MeanRegressor::default();
114        let mut mae = Mae::default();
115        let samples = vec![
116            RegressionSample {
117                features: vec![],
118                target: 10.0,
119            },
120            RegressionSample {
121                features: vec![],
122                target: 20.0,
123            },
124            RegressionSample {
125                features: vec![],
126                target: 30.0,
127            },
128        ];
129        let (final_mae, steps) =
130            evaluate_regression_with_steps(&mut model, &mut mae, samples).unwrap();
131
132        // Step 0: predict 0.0, truth 10.0 -> err 10
133        // Step 1: predict 10.0 (mean of [10]), truth 20.0 -> err 10
134        // Step 2: predict 15.0 (mean of [10,20]), truth 30.0 -> err 15
135        // MAE = (10+10+15)/3 = 11.666...
136        assert!((final_mae.unwrap() - 35.0 / 3.0).abs() < 1e-9);
137        assert_eq!(steps.len(), 3);
138    }
139}