1use crate::error::{RillError, checked_finite_add, checked_increment, ensure_finite};
8#[cfg(feature = "serde")]
9use crate::persistence::ValidateState;
10use crate::traits::{OnlineBinaryClassifier, OnlineRegressor};
11
12pub trait WeightedStatistic {
14 fn update_weighted(&mut self, value: f64, weight: f64) -> Result<(), RillError>;
16 fn total_weight(&self) -> f64;
18 fn samples_seen(&self) -> u64;
20 fn reset(&mut self);
22}
23
24pub trait WeightedOnlineRegressor: OnlineRegressor {
26 fn learn_weighted(
28 &mut self,
29 features: &[f64],
30 target: f64,
31 weight: f64,
32 ) -> Result<(), RillError>;
33}
34
35pub trait WeightedOnlineBinaryClassifier: OnlineBinaryClassifier {
37 fn learn_weighted(
39 &mut self,
40 features: &[f64],
41 target: bool,
42 weight: f64,
43 ) -> Result<(), RillError>;
44}
45
46pub fn validate_weight(weight: f64) -> Result<(), RillError> {
48 ensure_finite("weight", weight)?;
49 if weight < 0.0 {
50 return Err(RillError::InvalidParameter {
51 name: "weight",
52 value: weight,
53 });
54 }
55 Ok(())
56}
57
58#[derive(Debug, Clone, Default, PartialEq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
62pub struct WeightedMean {
63 total_weight: f64,
64 mean: f64,
65 samples_seen: u64,
66}
67
68impl WeightedMean {
69 pub const fn new() -> Self {
71 Self {
72 total_weight: 0.0,
73 mean: 0.0,
74 samples_seen: 0,
75 }
76 }
77
78 pub fn value(&self) -> Option<f64> {
80 (self.total_weight > 0.0).then_some(self.mean)
81 }
82}
83
84impl WeightedStatistic for WeightedMean {
85 fn update_weighted(&mut self, value: f64, weight: f64) -> Result<(), RillError> {
86 ensure_finite("value", value)?;
87 validate_weight(weight)?;
88 if weight == 0.0 {
89 return Ok(());
90 }
91 let next_weight = checked_finite_add(self.total_weight, weight, "total weight")?;
92 let delta = value - self.mean;
93 ensure_finite("weighted mean delta", delta)?;
94 let next_mean = self.mean + delta * (weight / next_weight);
95 ensure_finite("weighted mean", next_mean)?;
96 let next_samples = checked_increment(self.samples_seen, "weighted mean sample")?;
97 self.total_weight = next_weight;
98 self.mean = next_mean;
99 self.samples_seen = next_samples;
100 Ok(())
101 }
102
103 fn total_weight(&self) -> f64 {
104 self.total_weight
105 }
106
107 fn samples_seen(&self) -> u64 {
108 self.samples_seen
109 }
110
111 fn reset(&mut self) {
112 *self = Self::new();
113 }
114}
115
116#[derive(Debug, Clone, Default, PartialEq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
120pub struct WeightedVariance {
121 total_weight: f64,
122 mean: f64,
123 m2: f64,
124 samples_seen: u64,
125}
126
127impl WeightedVariance {
128 pub const fn new() -> Self {
130 Self {
131 total_weight: 0.0,
132 mean: 0.0,
133 m2: 0.0,
134 samples_seen: 0,
135 }
136 }
137
138 pub fn mean(&self) -> Option<f64> {
140 (self.total_weight > 0.0).then_some(self.mean)
141 }
142
143 pub fn value(&self) -> Option<f64> {
145 (self.total_weight > 0.0).then_some(self.m2 / self.total_weight)
146 }
147}
148
149impl WeightedStatistic for WeightedVariance {
150 fn update_weighted(&mut self, value: f64, weight: f64) -> Result<(), RillError> {
151 ensure_finite("value", value)?;
152 validate_weight(weight)?;
153 if weight == 0.0 {
154 return Ok(());
155 }
156 let next_weight = checked_finite_add(self.total_weight, weight, "total weight")?;
157 let delta = value - self.mean;
158 ensure_finite("weighted variance delta", delta)?;
159 let ratio = weight / next_weight;
160 let next_mean = self.mean + ratio * delta;
161 ensure_finite("weighted variance mean", next_mean)?;
162 let m2_delta = weight * delta * (value - next_mean);
163 ensure_finite("weighted variance m2 delta", m2_delta)?;
164 let next_m2 = checked_finite_add(self.m2, m2_delta, "weighted variance m2")?;
165 if next_m2 < -f64::EPSILON * next_weight.max(1.0) {
166 return Err(RillError::InvalidState(
167 "weighted variance became negative".to_owned(),
168 ));
169 }
170 let next_samples = checked_increment(self.samples_seen, "weighted variance sample")?;
171 self.total_weight = next_weight;
172 self.mean = next_mean;
173 self.m2 = next_m2.max(0.0);
174 self.samples_seen = next_samples;
175 Ok(())
176 }
177
178 fn total_weight(&self) -> f64 {
179 self.total_weight
180 }
181
182 fn samples_seen(&self) -> u64 {
183 self.samples_seen
184 }
185
186 fn reset(&mut self) {
187 *self = Self::new();
188 }
189}
190
191#[derive(Debug, Clone, PartialEq)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
199pub struct WeightedExponentiallyWeightedMean {
200 alpha: f64,
201 total_weight: f64,
202 mean: f64,
203 samples_seen: u64,
204}
205
206impl WeightedExponentiallyWeightedMean {
207 pub fn new(alpha: f64) -> Result<Self, RillError> {
209 ensure_finite("alpha", alpha)?;
210 if !(0.0 < alpha && alpha <= 1.0) {
211 return Err(RillError::InvalidParameter {
212 name: "alpha",
213 value: alpha,
214 });
215 }
216 Ok(Self {
217 alpha,
218 total_weight: 0.0,
219 mean: 0.0,
220 samples_seen: 0,
221 })
222 }
223
224 pub fn value(&self) -> Option<f64> {
226 (self.total_weight > 0.0).then_some(self.mean)
227 }
228
229 pub fn alpha(&self) -> f64 {
231 self.alpha
232 }
233}
234
235impl WeightedStatistic for WeightedExponentiallyWeightedMean {
236 fn update_weighted(&mut self, value: f64, weight: f64) -> Result<(), RillError> {
237 ensure_finite("value", value)?;
238 validate_weight(weight)?;
239 if weight == 0.0 {
240 return Ok(());
241 }
242 let next_weight = checked_finite_add(self.total_weight, weight, "total weight")?;
243 let next_samples = checked_increment(self.samples_seen, "weighted EW mean sample")?;
244 let next_mean = if self.total_weight == 0.0 {
245 value
246 } else {
247 let effective_alpha = 1.0 - (1.0 - self.alpha).powf(weight);
248 ensure_finite("effective alpha", effective_alpha)?;
249 self.mean + effective_alpha * (value - self.mean)
250 };
251 ensure_finite("weighted EW mean", next_mean)?;
252 self.total_weight = next_weight;
253 self.samples_seen = next_samples;
254 self.mean = next_mean;
255 Ok(())
256 }
257
258 fn total_weight(&self) -> f64 {
259 self.total_weight
260 }
261
262 fn samples_seen(&self) -> u64 {
263 self.samples_seen
264 }
265
266 fn reset(&mut self) {
267 self.total_weight = 0.0;
268 self.mean = 0.0;
269 self.samples_seen = 0;
270 }
271}
272
273#[derive(Debug, Clone, Default, PartialEq)]
275#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
276#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
277pub struct WeightedMae {
278 errors: WeightedMean,
279}
280
281impl WeightedMae {
282 pub fn update(&mut self, truth: f64, prediction: f64, weight: f64) -> Result<(), RillError> {
284 ensure_finite("truth", truth)?;
285 ensure_finite("prediction", prediction)?;
286 let error = (truth - prediction).abs();
287 ensure_finite("absolute error", error)?;
288 self.errors.update_weighted(error, weight)
289 }
290
291 pub fn value(&self) -> Option<f64> {
293 self.errors.value()
294 }
295
296 pub fn total_weight(&self) -> f64 {
298 self.errors.total_weight()
299 }
300
301 pub fn samples_seen(&self) -> u64 {
303 self.errors.samples_seen()
304 }
305
306 pub fn reset(&mut self) {
308 self.errors.reset();
309 }
310}
311
312#[derive(Debug, Clone, Default, PartialEq)]
314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
315#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
316pub struct WeightedMse {
317 errors: WeightedMean,
318}
319
320impl WeightedMse {
321 pub fn update(&mut self, truth: f64, prediction: f64, weight: f64) -> Result<(), RillError> {
323 ensure_finite("truth", truth)?;
324 ensure_finite("prediction", prediction)?;
325 let error = truth - prediction;
326 let squared = error * error;
327 ensure_finite("squared error", squared)?;
328 self.errors.update_weighted(squared, weight)
329 }
330
331 pub fn value(&self) -> Option<f64> {
333 self.errors.value()
334 }
335
336 pub fn total_weight(&self) -> f64 {
338 self.errors.total_weight()
339 }
340
341 pub fn samples_seen(&self) -> u64 {
343 self.errors.samples_seen()
344 }
345
346 pub fn reset(&mut self) {
348 self.errors.reset();
349 }
350}
351
352#[cfg(feature = "serde")]
353fn validate_weighted_moments(
354 total_weight: f64,
355 samples_seen: u64,
356 values: &[(&'static str, f64)],
357) -> Result<(), RillError> {
358 ensure_finite("total weight", total_weight)?;
359 if total_weight < 0.0 || (samples_seen == 0) != (total_weight == 0.0) {
360 return Err(RillError::InvalidState(
361 "weighted state has inconsistent count and total weight".to_owned(),
362 ));
363 }
364 for &(name, value) in values {
365 ensure_finite(name, value)?;
366 }
367 Ok(())
368}
369
370#[cfg(feature = "serde")]
371impl ValidateState for WeightedMean {
372 fn validate_state(&self) -> Result<(), RillError> {
373 validate_weighted_moments(self.total_weight, self.samples_seen, &[("mean", self.mean)])
374 }
375}
376
377#[cfg(feature = "serde")]
378impl ValidateState for WeightedVariance {
379 fn validate_state(&self) -> Result<(), RillError> {
380 validate_weighted_moments(
381 self.total_weight,
382 self.samples_seen,
383 &[("mean", self.mean), ("m2", self.m2)],
384 )?;
385 if self.m2 < 0.0 {
386 return Err(RillError::InvalidState(
387 "weighted variance m2 must be non-negative".to_owned(),
388 ));
389 }
390 Ok(())
391 }
392}
393
394#[cfg(feature = "serde")]
395impl ValidateState for WeightedExponentiallyWeightedMean {
396 fn validate_state(&self) -> Result<(), RillError> {
397 if !(self.alpha.is_finite() && 0.0 < self.alpha && self.alpha <= 1.0) {
398 return Err(RillError::InvalidState(
399 "weighted EW mean alpha must be in (0, 1]".to_owned(),
400 ));
401 }
402 validate_weighted_moments(self.total_weight, self.samples_seen, &[("mean", self.mean)])
403 }
404}
405
406#[cfg(feature = "serde")]
407impl ValidateState for WeightedMae {
408 fn validate_state(&self) -> Result<(), RillError> {
409 self.errors.validate_state()
410 }
411}
412
413#[cfg(feature = "serde")]
414impl ValidateState for WeightedMse {
415 fn validate_state(&self) -> Result<(), RillError> {
416 self.errors.validate_state()
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use approx::assert_abs_diff_eq;
424
425 #[test]
426 fn weighted_mean_and_variance_match_offline() {
427 let samples = [(1.0, 0.5), (3.0, 2.0), (10.0, 1.5)];
428 let total = samples.iter().map(|(_, weight)| weight).sum::<f64>();
429 let offline_mean = samples.iter().map(|(x, w)| x * w).sum::<f64>() / total;
430 let offline_variance = samples
431 .iter()
432 .map(|(x, w)| w * (x - offline_mean).powi(2))
433 .sum::<f64>()
434 / total;
435 let mut mean = WeightedMean::new();
436 let mut variance = WeightedVariance::new();
437 for (x, weight) in samples {
438 mean.update_weighted(x, weight).unwrap();
439 variance.update_weighted(x, weight).unwrap();
440 }
441 assert_abs_diff_eq!(mean.value().unwrap(), offline_mean, epsilon = 1e-12);
442 assert_abs_diff_eq!(variance.value().unwrap(), offline_variance, epsilon = 1e-12);
443 }
444
445 #[test]
446 fn integer_weight_ew_mean_matches_repeated_samples() {
447 let mut weighted = WeightedExponentiallyWeightedMean::new(0.2).unwrap();
448 let mut repeated = WeightedExponentiallyWeightedMean::new(0.2).unwrap();
449 weighted.update_weighted(1.0, 1.0).unwrap();
450 repeated.update_weighted(1.0, 1.0).unwrap();
451 weighted.update_weighted(5.0, 3.0).unwrap();
452 for _ in 0..3 {
453 repeated.update_weighted(5.0, 1.0).unwrap();
454 }
455 assert_abs_diff_eq!(
456 weighted.value().unwrap(),
457 repeated.value().unwrap(),
458 epsilon = 1e-12
459 );
460 }
461
462 #[test]
463 fn zero_weight_is_validated_noop_and_bad_weights_are_atomic() {
464 let mut mean = WeightedMean::new();
465 mean.update_weighted(3.0, 0.0).unwrap();
466 assert_eq!(mean, WeightedMean::new());
467 let before = mean.clone();
468 assert!(mean.update_weighted(1.0, -1.0).is_err());
469 assert!(mean.update_weighted(1.0, f64::NAN).is_err());
470 assert_eq!(mean, before);
471 }
472
473 #[test]
474 fn weighted_errors_match_offline_calculation_and_reset() {
475 let samples: [(f64, f64, f64); 3] = [(2.0, 1.0, 0.5), (5.0, 2.0, 2.0), (-1.0, 1.0, 1.5)];
476 let total_weight = samples.iter().map(|sample| sample.2).sum::<f64>();
477 let expected_mae = samples
478 .iter()
479 .map(|(truth, prediction, weight)| weight * (truth - prediction).abs())
480 .sum::<f64>()
481 / total_weight;
482 let expected_mse = samples
483 .iter()
484 .map(|(truth, prediction, weight)| weight * (truth - prediction).powi(2))
485 .sum::<f64>()
486 / total_weight;
487 let mut mae = WeightedMae::default();
488 let mut mse = WeightedMse::default();
489 for (truth, prediction, weight) in samples {
490 mae.update(truth, prediction, weight).unwrap();
491 mse.update(truth, prediction, weight).unwrap();
492 }
493 assert_abs_diff_eq!(mae.value().unwrap(), expected_mae, epsilon = 1e-12);
494 assert_abs_diff_eq!(mse.value().unwrap(), expected_mse, epsilon = 1e-12);
495 assert_eq!(mae.samples_seen(), 3);
496 assert_eq!(mse.total_weight(), total_weight);
497 mae.reset();
498 mse.reset();
499 assert_eq!(mae.value(), None);
500 assert_eq!(mse.samples_seen(), 0);
501 }
502
503 #[cfg(feature = "serde")]
504 #[test]
505 fn weighted_state_roundtrip_and_corruption_rejected() {
506 let mut variance = WeightedVariance::new();
507 variance.update_weighted(2.0, 0.5).unwrap();
508 let json = serde_json::to_string(&variance).unwrap();
509 let restored: WeightedVariance = serde_json::from_str(&json).unwrap();
510 restored.validate_state().unwrap();
511 let corrupt = json.replace("\"total_weight\":0.5", "\"total_weight\":-1.0");
512 let restored: WeightedVariance = serde_json::from_str(&corrupt).unwrap();
513 assert!(restored.validate_state().is_err());
514 }
515}