1use crate::error::{RillError, checked_finite_add, checked_increment, ensure_finite};
4use crate::loss::log_loss::BinaryLogLoss;
5use crate::traits::Metric;
6
7#[derive(Debug, Clone, Default)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct Accuracy {
11 correct: u64,
12 count: u64,
13}
14
15impl Metric for Accuracy {
16 type Truth = bool;
17 type Prediction = bool;
18
19 fn update(&mut self, truth: bool, prediction: bool) -> Result<(), RillError> {
20 let next_count = checked_increment(self.count, "accuracy sample")?;
21 let next_correct = if truth == prediction {
22 checked_increment(self.correct, "accuracy correct")?
23 } else {
24 self.correct
25 };
26 self.count = next_count;
27 self.correct = next_correct;
28 Ok(())
29 }
30
31 fn value(&self) -> Option<f64> {
32 if self.count == 0 {
33 None
34 } else {
35 Some(self.correct as f64 / self.count as f64)
36 }
37 }
38
39 fn samples_seen(&self) -> u64 {
40 self.count
41 }
42
43 fn reset(&mut self) {
44 self.correct = 0;
45 self.count = 0;
46 }
47}
48
49#[derive(Debug, Clone, Default)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize))]
56pub struct Precision {
57 true_positive: u64,
58 false_positive: u64,
59 samples_seen: u64,
64}
65
66#[cfg(feature = "serde")]
67impl<'de> serde::Deserialize<'de> for Precision {
68 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
69 where
70 D: serde::Deserializer<'de>,
71 {
72 #[derive(serde::Deserialize)]
73 struct PrecisionState {
74 true_positive: u64,
75 false_positive: u64,
76 samples_seen: u64,
77 }
78
79 let state = PrecisionState::deserialize(deserializer)?;
80 let confusion = state
85 .true_positive
86 .checked_add(state.false_positive)
87 .ok_or_else(|| serde::de::Error::custom("precision tp + fp overflow"))?;
88 if state.samples_seen < confusion {
89 return Err(serde::de::Error::custom("precision samples_seen < tp + fp"));
90 }
91 Ok(Precision {
92 true_positive: state.true_positive,
93 false_positive: state.false_positive,
94 samples_seen: state.samples_seen,
95 })
96 }
97}
98
99impl Metric for Precision {
100 type Truth = bool;
101 type Prediction = bool;
102
103 fn update(&mut self, truth: bool, prediction: bool) -> Result<(), RillError> {
104 let next_samples = checked_increment(self.samples_seen, "precision samples_seen")?;
105 let next_tp = if truth && prediction {
106 checked_increment(self.true_positive, "precision true positive")?
107 } else {
108 self.true_positive
109 };
110 let next_fp = if !truth && prediction {
111 checked_increment(self.false_positive, "precision false positive")?
112 } else {
113 self.false_positive
114 };
115 self.samples_seen = next_samples;
116 self.true_positive = next_tp;
117 self.false_positive = next_fp;
118 Ok(())
119 }
120
121 fn value(&self) -> Option<f64> {
122 let denominator = self.true_positive as f64 + self.false_positive as f64;
123 if denominator == 0.0 {
124 None
125 } else {
126 Some(self.true_positive as f64 / denominator)
127 }
128 }
129
130 fn samples_seen(&self) -> u64 {
131 self.samples_seen
132 }
133
134 fn reset(&mut self) {
135 self.true_positive = 0;
136 self.false_positive = 0;
137 self.samples_seen = 0;
138 }
139}
140
141#[derive(Debug, Clone, Default)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize))]
147pub struct Recall {
148 true_positive: u64,
149 false_negative: u64,
150 samples_seen: u64,
151}
152
153#[cfg(feature = "serde")]
154impl<'de> serde::Deserialize<'de> for Recall {
155 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156 where
157 D: serde::Deserializer<'de>,
158 {
159 #[derive(serde::Deserialize)]
160 struct RecallState {
161 true_positive: u64,
162 false_negative: u64,
163 samples_seen: u64,
164 }
165
166 let state = RecallState::deserialize(deserializer)?;
167 let confusion = state
170 .true_positive
171 .checked_add(state.false_negative)
172 .ok_or_else(|| serde::de::Error::custom("recall tp + fn overflow"))?;
173 if state.samples_seen < confusion {
174 return Err(serde::de::Error::custom("recall samples_seen < tp + fn"));
175 }
176 Ok(Recall {
177 true_positive: state.true_positive,
178 false_negative: state.false_negative,
179 samples_seen: state.samples_seen,
180 })
181 }
182}
183
184impl Metric for Recall {
185 type Truth = bool;
186 type Prediction = bool;
187
188 fn update(&mut self, truth: bool, prediction: bool) -> Result<(), RillError> {
189 let next_samples = checked_increment(self.samples_seen, "recall samples_seen")?;
190 let next_tp = if truth && prediction {
191 checked_increment(self.true_positive, "recall true positive")?
192 } else {
193 self.true_positive
194 };
195 let next_fn = if truth && !prediction {
196 checked_increment(self.false_negative, "recall false negative")?
197 } else {
198 self.false_negative
199 };
200 self.samples_seen = next_samples;
201 self.true_positive = next_tp;
202 self.false_negative = next_fn;
203 Ok(())
204 }
205
206 fn value(&self) -> Option<f64> {
207 let denominator = self.true_positive as f64 + self.false_negative as f64;
208 if denominator == 0.0 {
209 None
210 } else {
211 Some(self.true_positive as f64 / denominator)
212 }
213 }
214
215 fn samples_seen(&self) -> u64 {
216 self.samples_seen
217 }
218
219 fn reset(&mut self) {
220 self.true_positive = 0;
221 self.false_negative = 0;
222 self.samples_seen = 0;
223 }
224}
225
226#[derive(Debug, Clone, Default)]
231#[cfg_attr(feature = "serde", derive(serde::Serialize))]
232pub struct F1Score {
233 true_positive: u64,
234 false_positive: u64,
235 false_negative: u64,
236 samples_seen: u64,
237}
238
239#[cfg(feature = "serde")]
240impl<'de> serde::Deserialize<'de> for F1Score {
241 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
242 where
243 D: serde::Deserializer<'de>,
244 {
245 #[derive(serde::Deserialize)]
246 struct F1State {
247 true_positive: u64,
248 false_positive: u64,
249 false_negative: u64,
250 samples_seen: u64,
251 }
252
253 let state = F1State::deserialize(deserializer)?;
254 let tp_fp = state
259 .true_positive
260 .checked_add(state.false_positive)
261 .ok_or_else(|| serde::de::Error::custom("f1 tp + fp overflow"))?;
262 let confusion = tp_fp
263 .checked_add(state.false_negative)
264 .ok_or_else(|| serde::de::Error::custom("f1 tp + fp + fn overflow"))?;
265 if state.samples_seen < confusion {
266 return Err(serde::de::Error::custom("f1 samples_seen < tp + fp + fn"));
267 }
268 Ok(F1Score {
269 true_positive: state.true_positive,
270 false_positive: state.false_positive,
271 false_negative: state.false_negative,
272 samples_seen: state.samples_seen,
273 })
274 }
275}
276
277impl Metric for F1Score {
278 type Truth = bool;
279 type Prediction = bool;
280
281 fn update(&mut self, truth: bool, prediction: bool) -> Result<(), RillError> {
282 let next_samples = checked_increment(self.samples_seen, "F1 samples_seen")?;
283 let next_tp = if truth && prediction {
284 checked_increment(self.true_positive, "F1 true positive")?
285 } else {
286 self.true_positive
287 };
288 let next_fp = if !truth && prediction {
289 checked_increment(self.false_positive, "F1 false positive")?
290 } else {
291 self.false_positive
292 };
293 let next_fn = if truth && !prediction {
294 checked_increment(self.false_negative, "F1 false negative")?
295 } else {
296 self.false_negative
297 };
298 self.samples_seen = next_samples;
299 self.true_positive = next_tp;
300 self.false_positive = next_fp;
301 self.false_negative = next_fn;
302 Ok(())
303 }
304
305 fn value(&self) -> Option<f64> {
306 let denominator = 2.0 * self.true_positive as f64
307 + self.false_positive as f64
308 + self.false_negative as f64;
309 if denominator == 0.0 {
310 None
311 } else {
312 Some(2.0 * self.true_positive as f64 / denominator)
313 }
314 }
315
316 fn samples_seen(&self) -> u64 {
317 self.samples_seen
318 }
319
320 fn reset(&mut self) {
321 self.true_positive = 0;
322 self.false_positive = 0;
323 self.false_negative = 0;
324 self.samples_seen = 0;
325 }
326}
327
328#[derive(Debug, Clone)]
330#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
331pub struct LogLoss {
332 loss: BinaryLogLoss,
333 sum_loss: f64,
334 count: u64,
335}
336
337impl Default for LogLoss {
338 fn default() -> Self {
339 Self {
340 loss: BinaryLogLoss::new(),
341 sum_loss: 0.0,
342 count: 0,
343 }
344 }
345}
346
347impl Metric for LogLoss {
348 type Truth = bool;
349 type Prediction = f64;
350
351 fn update(&mut self, truth: bool, prediction: f64) -> Result<(), RillError> {
352 ensure_finite("probability", prediction)?;
353 if !(0.0..=1.0).contains(&prediction) {
354 return Err(RillError::InvalidProbability(prediction));
355 }
356 let loss = self.loss.loss(prediction, truth);
357 ensure_finite("log loss", loss)?;
358 let next_sum = checked_finite_add(self.sum_loss, loss, "log loss sum")?;
359 let next_count = checked_increment(self.count, "log loss sample")?;
360 self.sum_loss = next_sum;
361 self.count = next_count;
362 Ok(())
363 }
364
365 fn value(&self) -> Option<f64> {
366 if self.count == 0 {
367 None
368 } else {
369 Some(self.sum_loss / self.count as f64)
370 }
371 }
372
373 fn samples_seen(&self) -> u64 {
374 self.count
375 }
376
377 fn reset(&mut self) {
378 self.sum_loss = 0.0;
379 self.count = 0;
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn accuracy_basic() {
389 let mut m = Accuracy::default();
390 m.update(true, true).unwrap();
391 m.update(false, false).unwrap();
392 m.update(true, false).unwrap();
393 assert!((m.value().unwrap() - 2.0 / 3.0).abs() < 1e-12);
394 }
395
396 #[test]
397 fn precision_basic() {
398 let mut m = Precision::default();
399 m.update(true, true).unwrap(); m.update(false, true).unwrap(); m.update(true, false).unwrap(); assert!((m.value().unwrap() - 0.5).abs() < 1e-12);
403 }
404
405 #[test]
406 fn recall_basic() {
407 let mut m = Recall::default();
408 m.update(true, true).unwrap(); m.update(false, true).unwrap(); m.update(true, false).unwrap(); assert!((m.value().unwrap() - 0.5).abs() < 1e-12);
412 }
413
414 #[test]
415 fn f1_basic() {
416 let mut m = F1Score::default();
417 m.update(true, true).unwrap(); m.update(false, true).unwrap(); m.update(true, false).unwrap(); assert!((m.value().unwrap() - 0.5).abs() < 1e-12);
422 }
423
424 #[test]
425 fn f1_perfect_is_one() {
426 let mut m = F1Score::default();
427 m.update(true, true).unwrap();
428 m.update(false, false).unwrap();
429 assert!((m.value().unwrap() - 1.0).abs() < 1e-12);
430 }
431
432 #[test]
433 fn log_loss_basic() {
434 let mut m = LogLoss::default();
435 m.update(true, 0.9).unwrap();
436 m.update(false, 0.1).unwrap();
437 let expected = (-0.9_f64.ln() + -0.9_f64.ln()) / 2.0;
438 assert!((m.value().unwrap() - expected).abs() < 1e-9);
439 }
440
441 #[test]
442 fn log_loss_rejects_invalid_probability() {
443 let mut m = LogLoss::default();
444 assert!(m.update(true, 1.5).is_err());
445 assert!(m.update(true, -0.1).is_err());
446 assert!(m.update(true, f64::NAN).is_err());
447 }
448
449 #[test]
450 fn empty_metrics_return_none() {
451 assert!(Accuracy::default().value().is_none());
452 assert!(Precision::default().value().is_none());
453 assert!(Recall::default().value().is_none());
454 assert!(F1Score::default().value().is_none());
455 assert!(LogLoss::default().value().is_none());
456 }
457
458 #[test]
459 fn precision_no_predictions_returns_none() {
460 let mut m = Precision::default();
461 m.update(true, false).unwrap();
462 m.update(false, false).unwrap();
463 assert!(m.value().is_none());
464 }
465
466 #[test]
472 fn samples_seen_counts_all_observations() {
473 let mut p = Precision::default();
474 let mut r = Recall::default();
475 let mut f = F1Score::default();
476 let mut a = Accuracy::default();
477
478 let cases = [(true, true), (true, false), (false, true), (false, false)];
480 for (truth, pred) in cases {
481 p.update(truth, pred).unwrap();
482 r.update(truth, pred).unwrap();
483 f.update(truth, pred).unwrap();
484 a.update(truth, pred).unwrap();
485 }
486
487 assert_eq!(p.samples_seen(), 4);
488 assert_eq!(r.samples_seen(), 4);
489 assert_eq!(f.samples_seen(), 4);
490 assert_eq!(a.samples_seen(), 4);
491 }
492
493 #[test]
494 #[cfg(feature = "serde")]
495 fn samples_seen_overflow_is_atomic() {
496 let json = format!(
499 "{{\"true_positive\":1,\"false_positive\":1,\"samples_seen\":{}}}",
500 u64::MAX
501 );
502 let mut p: Precision = serde_json::from_str(&json).unwrap();
503 let result = p.update(true, true);
504 assert!(result.is_err(), "expected overflow");
505 assert_eq!(p.samples_seen(), u64::MAX);
506 assert_eq!(p.true_positive, 1);
507 assert_eq!(p.false_positive, 1);
508 }
509
510 #[test]
511 #[cfg(feature = "serde")]
512 fn precision_serde_rejects_missing_samples_seen() {
513 let json = "{\"true_positive\":1,\"false_positive\":1}";
516 assert!(serde_json::from_str::<Precision>(json).is_err());
517 }
518
519 #[test]
520 #[cfg(feature = "serde")]
521 fn precision_serde_rejects_inconsistent_samples_seen() {
522 let json = "{\"true_positive\":5,\"false_positive\":5,\"samples_seen\":3}";
524 assert!(serde_json::from_str::<Precision>(json).is_err());
525 }
526
527 #[test]
528 #[cfg(feature = "serde")]
529 fn recall_serde_rejects_missing_samples_seen() {
530 let json = "{\"true_positive\":1,\"false_negative\":1}";
531 assert!(serde_json::from_str::<Recall>(json).is_err());
532 }
533
534 #[test]
535 #[cfg(feature = "serde")]
536 fn f1_serde_rejects_missing_samples_seen() {
537 let json = "{\"true_positive\":1,\"false_positive\":1,\"false_negative\":1}";
538 assert!(serde_json::from_str::<F1Score>(json).is_err());
539 }
540
541 #[test]
542 #[cfg(feature = "serde")]
543 fn metric_serde_roundtrip_preserves_samples_seen() {
544 let mut p = Precision::default();
545 for _ in 0..10 {
546 p.update(true, true).unwrap();
547 }
548 let json = serde_json::to_string(&p).unwrap();
549 let restored: Precision = serde_json::from_str(&json).unwrap();
550 assert_eq!(restored.samples_seen(), 10);
551 assert_eq!(restored.true_positive, 10);
552 }
553
554 #[test]
555 fn reset_clears_samples_seen() {
556 let mut p = Precision::default();
557 p.update(true, true).unwrap();
558 p.update(false, false).unwrap();
559 assert_eq!(p.samples_seen(), 2);
560 p.reset();
561 assert_eq!(p.samples_seen(), 0);
562 assert_eq!(p.true_positive, 0);
563 assert_eq!(p.false_positive, 0);
564 }
565
566 #[test]
572 #[cfg(feature = "serde")]
573 fn precision_serde_rejects_tp_fp_overflow() {
574 let json = format!(
578 "{{\"true_positive\":{},\"false_positive\":{},\"samples_seen\":{}}}",
579 u64::MAX,
580 1u64,
581 u64::MAX
582 );
583 assert!(serde_json::from_str::<Precision>(&json).is_err());
584 }
585
586 #[test]
587 #[cfg(feature = "serde")]
588 fn recall_serde_rejects_tp_fn_overflow() {
589 let json = format!(
590 "{{\"true_positive\":{},\"false_negative\":{},\"samples_seen\":{}}}",
591 u64::MAX,
592 1u64,
593 u64::MAX
594 );
595 assert!(serde_json::from_str::<Recall>(&json).is_err());
596 }
597
598 #[test]
599 #[cfg(feature = "serde")]
600 fn f1_serde_rejects_tp_fp_overflow() {
601 let json = format!(
603 "{{\"true_positive\":{},\"false_positive\":{},\"false_negative\":0,\"samples_seen\":{}}}",
604 u64::MAX,
605 1u64,
606 u64::MAX
607 );
608 assert!(serde_json::from_str::<F1Score>(&json).is_err());
609 }
610
611 #[test]
612 #[cfg(feature = "serde")]
613 fn f1_serde_rejects_tp_fp_fn_overflow() {
614 let json = format!(
616 "{{\"true_positive\":{},\"false_positive\":0,\"false_negative\":{},\"samples_seen\":{}}}",
617 u64::MAX,
618 1u64,
619 u64::MAX
620 );
621 assert!(serde_json::from_str::<F1Score>(&json).is_err());
622 }
623
624 #[test]
625 #[cfg(feature = "serde")]
626 fn metric_serde_accepts_max_boundary() {
627 let p_json = format!(
630 "{{\"true_positive\":{},\"false_positive\":0,\"samples_seen\":{}}}",
631 u64::MAX,
632 u64::MAX
633 );
634 let p: Precision = serde_json::from_str(&p_json).unwrap();
635 assert_eq!(p.samples_seen(), u64::MAX);
636
637 let r_json = format!(
638 "{{\"true_positive\":{},\"false_negative\":0,\"samples_seen\":{}}}",
639 u64::MAX,
640 u64::MAX
641 );
642 let r: Recall = serde_json::from_str(&r_json).unwrap();
643 assert_eq!(r.samples_seen(), u64::MAX);
644
645 let f_json = format!(
646 "{{\"true_positive\":{},\"false_positive\":0,\"false_negative\":0,\"samples_seen\":{}}}",
647 u64::MAX,
648 u64::MAX
649 );
650 let f: F1Score = serde_json::from_str(&f_json).unwrap();
651 assert_eq!(f.samples_seen(), u64::MAX);
652 }
653
654 #[test]
655 #[cfg(feature = "serde")]
656 fn recall_serde_rejects_inconsistent_samples_seen() {
657 let json = "{\"true_positive\":5,\"false_negative\":5,\"samples_seen\":3}";
659 assert!(serde_json::from_str::<Recall>(json).is_err());
660 }
661
662 #[test]
663 #[cfg(feature = "serde")]
664 fn f1_serde_rejects_inconsistent_samples_seen() {
665 let json =
667 "{\"true_positive\":2,\"false_positive\":2,\"false_negative\":2,\"samples_seen\":3}";
668 assert!(serde_json::from_str::<F1Score>(json).is_err());
669 }
670}