Skip to main content

rill_ml/models/
baseline.rs

1//! Baseline regressors.
2//!
3//! These simple models serve as comparison baselines. A complex online model
4//! should be compared against at least [`MeanRegressor`] and
5//! [`ExponentiallyWeightedMeanRegressor`] before being considered useful.
6
7use crate::error::{RillError, checked_increment, ensure_finite, ensure_finite_target};
8use crate::stats::{ExponentiallyWeightedMean, Mean};
9use crate::traits::{OnlineRegressor, OnlineStatistic};
10
11/// Configuration shared by baseline regressors.
12#[derive(Debug, Clone)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct BaselineConfig {
15    /// Prediction returned before any target has been observed.
16    pub initial_prediction: f64,
17}
18
19impl Default for BaselineConfig {
20    fn default() -> Self {
21        Self {
22            initial_prediction: 0.0,
23        }
24    }
25}
26
27fn validate_baseline_config(config: &BaselineConfig) -> Result<(), RillError> {
28    ensure_finite("initial_prediction", config.initial_prediction)
29}
30
31/// A regressor that always predicts the running mean of observed targets.
32///
33/// This is the simplest meaningful online regression baseline.
34#[derive(Debug, Clone)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct MeanRegressor {
37    config: BaselineConfig,
38    mean: Mean,
39}
40
41impl MeanRegressor {
42    /// Create a new mean regressor with the given configuration.
43    pub fn new(config: BaselineConfig) -> Result<Self, RillError> {
44        validate_baseline_config(&config)?;
45        Ok(Self {
46            config,
47            mean: Mean::new(),
48        })
49    }
50
51    /// The current running mean of targets.
52    pub const fn mean(&self) -> f64 {
53        self.mean.value()
54    }
55}
56
57impl OnlineRegressor for MeanRegressor {
58    fn feature_count(&self) -> usize {
59        0
60    }
61
62    fn samples_seen(&self) -> u64 {
63        self.mean.samples_seen()
64    }
65
66    fn predict(&self, _features: &[f64]) -> Result<f64, RillError> {
67        if self.mean.count() == 0 {
68            Ok(self.config.initial_prediction)
69        } else {
70            Ok(self.mean.value())
71        }
72    }
73
74    fn learn(&mut self, _features: &[f64], target: f64) -> Result<(), RillError> {
75        ensure_finite_target(target)?;
76        self.mean.update(target)
77    }
78
79    fn reset(&mut self) {
80        self.mean.reset();
81    }
82}
83
84impl Default for MeanRegressor {
85    fn default() -> Self {
86        Self::new(BaselineConfig::default()).expect("default config is valid")
87    }
88}
89
90/// A regressor that always predicts the last observed target.
91#[derive(Debug, Clone)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93pub struct LastValueRegressor {
94    config: BaselineConfig,
95    last_value: Option<f64>,
96    count: u64,
97}
98
99impl LastValueRegressor {
100    /// Create a new last-value regressor.
101    pub fn new(config: BaselineConfig) -> Result<Self, RillError> {
102        validate_baseline_config(&config)?;
103        Ok(Self {
104            config,
105            last_value: None,
106            count: 0,
107        })
108    }
109
110    /// The last observed target, if any.
111    pub const fn last_value(&self) -> Option<f64> {
112        self.last_value
113    }
114}
115
116impl OnlineRegressor for LastValueRegressor {
117    fn feature_count(&self) -> usize {
118        0
119    }
120
121    fn samples_seen(&self) -> u64 {
122        self.count
123    }
124
125    fn predict(&self, _features: &[f64]) -> Result<f64, RillError> {
126        Ok(self.last_value.unwrap_or(self.config.initial_prediction))
127    }
128
129    fn learn(&mut self, _features: &[f64], target: f64) -> Result<(), RillError> {
130        ensure_finite_target(target)?;
131        let next_count = checked_increment(self.count, "last-value sample")?;
132        self.last_value = Some(target);
133        self.count = next_count;
134        Ok(())
135    }
136
137    fn reset(&mut self) {
138        self.last_value = None;
139        self.count = 0;
140    }
141}
142
143impl Default for LastValueRegressor {
144    fn default() -> Self {
145        Self::new(BaselineConfig::default()).expect("default config is valid")
146    }
147}
148
149/// A regressor that predicts an exponentially weighted mean of targets.
150///
151/// Suitable when recent observations are more relevant than older ones.
152#[derive(Debug, Clone)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub struct ExponentiallyWeightedMeanRegressor {
155    config: BaselineConfig,
156    ew: ExponentiallyWeightedMean,
157}
158
159impl ExponentiallyWeightedMeanRegressor {
160    /// Create a new EW mean regressor.
161    ///
162    /// `alpha` must be in `(0, 1]`.
163    pub fn new(alpha: f64, config: BaselineConfig) -> Result<Self, RillError> {
164        validate_baseline_config(&config)?;
165        Ok(Self {
166            config,
167            ew: ExponentiallyWeightedMean::new(alpha)?,
168        })
169    }
170
171    /// The configured alpha.
172    pub const fn alpha(&self) -> f64 {
173        self.ew.alpha()
174    }
175
176    /// The current weighted mean.
177    pub const fn value(&self) -> f64 {
178        self.ew.value()
179    }
180}
181
182impl OnlineRegressor for ExponentiallyWeightedMeanRegressor {
183    fn feature_count(&self) -> usize {
184        0
185    }
186
187    fn samples_seen(&self) -> u64 {
188        self.ew.samples_seen()
189    }
190
191    fn predict(&self, _features: &[f64]) -> Result<f64, RillError> {
192        if self.ew.count() == 0 {
193            Ok(self.config.initial_prediction)
194        } else {
195            Ok(self.ew.value())
196        }
197    }
198
199    fn learn(&mut self, _features: &[f64], target: f64) -> Result<(), RillError> {
200        ensure_finite_target(target)?;
201        self.ew.update(target)
202    }
203
204    fn reset(&mut self) {
205        self.ew.reset();
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn mean_regressor_cold_start() {
215        let r = MeanRegressor::default();
216        assert_eq!(r.predict(&[]).unwrap(), 0.0);
217    }
218
219    #[test]
220    fn mean_regressor_predicts_running_mean() {
221        let mut r = MeanRegressor::default();
222        r.learn(&[], 10.0).unwrap();
223        r.learn(&[], 20.0).unwrap();
224        assert_eq!(r.predict(&[]).unwrap(), 15.0);
225    }
226
227    #[test]
228    fn last_value_regressor_cold_start() {
229        let r = LastValueRegressor::default();
230        assert_eq!(r.predict(&[]).unwrap(), 0.0);
231    }
232
233    #[test]
234    fn last_value_regressor_tracks_last() {
235        let mut r = LastValueRegressor::default();
236        r.learn(&[], 10.0).unwrap();
237        r.learn(&[], 20.0).unwrap();
238        assert_eq!(r.predict(&[]).unwrap(), 20.0);
239    }
240
241    #[test]
242    fn ew_mean_regressor_cold_start() {
243        let r = ExponentiallyWeightedMeanRegressor::new(0.5, BaselineConfig::default()).unwrap();
244        assert_eq!(r.predict(&[]).unwrap(), 0.0);
245    }
246
247    #[test]
248    fn ew_mean_regressor_weights_recent() {
249        let mut r =
250            ExponentiallyWeightedMeanRegressor::new(0.5, BaselineConfig::default()).unwrap();
251        r.learn(&[], 10.0).unwrap();
252        r.learn(&[], 20.0).unwrap();
253        assert!((r.predict(&[]).unwrap() - 15.0).abs() < 1e-12);
254    }
255
256    #[test]
257    fn initial_prediction_custom() {
258        let r = MeanRegressor::new(BaselineConfig {
259            initial_prediction: 42.0,
260        })
261        .unwrap();
262        assert_eq!(r.predict(&[]).unwrap(), 42.0);
263    }
264
265    #[test]
266    fn non_finite_target_rejected() {
267        let mut r = MeanRegressor::default();
268        assert!(r.learn(&[], f64::NAN).is_err());
269    }
270}