rill_ml/models/
baseline.rs1use crate::error::{RillError, checked_increment, ensure_finite, ensure_finite_target};
8use crate::stats::{ExponentiallyWeightedMean, Mean};
9use crate::traits::{OnlineRegressor, OnlineStatistic};
10
11#[derive(Debug, Clone)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct BaselineConfig {
15 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#[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 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 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#[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 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 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#[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 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 pub const fn alpha(&self) -> f64 {
173 self.ew.alpha()
174 }
175
176 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}