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)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct Precision {
53 true_positive: u64,
54 false_positive: u64,
55}
56
57impl Metric for Precision {
58 type Truth = bool;
59 type Prediction = bool;
60
61 fn update(&mut self, truth: bool, prediction: bool) -> Result<(), RillError> {
62 match (truth, prediction) {
63 (true, true) => {
64 self.true_positive =
65 checked_increment(self.true_positive, "precision true positive")?
66 }
67 (false, true) => {
68 self.false_positive =
69 checked_increment(self.false_positive, "precision false positive")?
70 }
71 _ => {}
72 }
73 Ok(())
74 }
75
76 fn value(&self) -> Option<f64> {
77 let denominator = self.true_positive as f64 + self.false_positive as f64;
78 if denominator == 0.0 {
79 None
80 } else {
81 Some(self.true_positive as f64 / denominator)
82 }
83 }
84
85 fn samples_seen(&self) -> u64 {
86 self.true_positive.saturating_add(self.false_positive)
87 }
88
89 fn reset(&mut self) {
90 self.true_positive = 0;
91 self.false_positive = 0;
92 }
93}
94
95#[derive(Debug, Clone, Default)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct Recall {
99 true_positive: u64,
100 false_negative: u64,
101}
102
103impl Metric for Recall {
104 type Truth = bool;
105 type Prediction = bool;
106
107 fn update(&mut self, truth: bool, prediction: bool) -> Result<(), RillError> {
108 match (truth, prediction) {
109 (true, true) => {
110 self.true_positive = checked_increment(self.true_positive, "recall true positive")?
111 }
112 (true, false) => {
113 self.false_negative =
114 checked_increment(self.false_negative, "recall false negative")?
115 }
116 _ => {}
117 }
118 Ok(())
119 }
120
121 fn value(&self) -> Option<f64> {
122 let denominator = self.true_positive as f64 + self.false_negative 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.true_positive.saturating_add(self.false_negative)
132 }
133
134 fn reset(&mut self) {
135 self.true_positive = 0;
136 self.false_negative = 0;
137 }
138}
139
140#[derive(Debug, Clone, Default)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143pub struct F1Score {
144 true_positive: u64,
145 false_positive: u64,
146 false_negative: u64,
147}
148
149impl Metric for F1Score {
150 type Truth = bool;
151 type Prediction = bool;
152
153 fn update(&mut self, truth: bool, prediction: bool) -> Result<(), RillError> {
154 match (truth, prediction) {
155 (true, true) => {
156 self.true_positive = checked_increment(self.true_positive, "F1 true positive")?
157 }
158 (false, true) => {
159 self.false_positive = checked_increment(self.false_positive, "F1 false positive")?
160 }
161 (true, false) => {
162 self.false_negative = checked_increment(self.false_negative, "F1 false negative")?
163 }
164 _ => {}
165 }
166 Ok(())
167 }
168
169 fn value(&self) -> Option<f64> {
170 let denominator = 2.0 * self.true_positive as f64
171 + self.false_positive as f64
172 + self.false_negative as f64;
173 if denominator == 0.0 {
174 None
175 } else {
176 Some(2.0 * self.true_positive as f64 / denominator)
177 }
178 }
179
180 fn samples_seen(&self) -> u64 {
181 self.true_positive
182 .saturating_add(self.false_positive)
183 .saturating_add(self.false_negative)
184 }
185
186 fn reset(&mut self) {
187 self.true_positive = 0;
188 self.false_positive = 0;
189 self.false_negative = 0;
190 }
191}
192
193#[derive(Debug, Clone)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
196pub struct LogLoss {
197 loss: BinaryLogLoss,
198 sum_loss: f64,
199 count: u64,
200}
201
202impl Default for LogLoss {
203 fn default() -> Self {
204 Self {
205 loss: BinaryLogLoss::new(),
206 sum_loss: 0.0,
207 count: 0,
208 }
209 }
210}
211
212impl Metric for LogLoss {
213 type Truth = bool;
214 type Prediction = f64;
215
216 fn update(&mut self, truth: bool, prediction: f64) -> Result<(), RillError> {
217 ensure_finite("probability", prediction)?;
218 if !(0.0..=1.0).contains(&prediction) {
219 return Err(RillError::InvalidProbability(prediction));
220 }
221 let loss = self.loss.loss(prediction, truth);
222 ensure_finite("log loss", loss)?;
223 let next_sum = checked_finite_add(self.sum_loss, loss, "log loss sum")?;
224 let next_count = checked_increment(self.count, "log loss sample")?;
225 self.sum_loss = next_sum;
226 self.count = next_count;
227 Ok(())
228 }
229
230 fn value(&self) -> Option<f64> {
231 if self.count == 0 {
232 None
233 } else {
234 Some(self.sum_loss / self.count as f64)
235 }
236 }
237
238 fn samples_seen(&self) -> u64 {
239 self.count
240 }
241
242 fn reset(&mut self) {
243 self.sum_loss = 0.0;
244 self.count = 0;
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn accuracy_basic() {
254 let mut m = Accuracy::default();
255 m.update(true, true).unwrap();
256 m.update(false, false).unwrap();
257 m.update(true, false).unwrap();
258 assert!((m.value().unwrap() - 2.0 / 3.0).abs() < 1e-12);
259 }
260
261 #[test]
262 fn precision_basic() {
263 let mut m = Precision::default();
264 m.update(true, true).unwrap(); m.update(false, true).unwrap(); m.update(true, false).unwrap(); assert!((m.value().unwrap() - 0.5).abs() < 1e-12);
268 }
269
270 #[test]
271 fn recall_basic() {
272 let mut m = Recall::default();
273 m.update(true, true).unwrap(); m.update(false, true).unwrap(); m.update(true, false).unwrap(); assert!((m.value().unwrap() - 0.5).abs() < 1e-12);
277 }
278
279 #[test]
280 fn f1_basic() {
281 let mut m = F1Score::default();
282 m.update(true, true).unwrap(); m.update(false, true).unwrap(); m.update(true, false).unwrap(); assert!((m.value().unwrap() - 0.5).abs() < 1e-12);
287 }
288
289 #[test]
290 fn f1_perfect_is_one() {
291 let mut m = F1Score::default();
292 m.update(true, true).unwrap();
293 m.update(false, false).unwrap();
294 assert!((m.value().unwrap() - 1.0).abs() < 1e-12);
295 }
296
297 #[test]
298 fn log_loss_basic() {
299 let mut m = LogLoss::default();
300 m.update(true, 0.9).unwrap();
301 m.update(false, 0.1).unwrap();
302 let expected = (-0.9_f64.ln() + -0.9_f64.ln()) / 2.0;
303 assert!((m.value().unwrap() - expected).abs() < 1e-9);
304 }
305
306 #[test]
307 fn log_loss_rejects_invalid_probability() {
308 let mut m = LogLoss::default();
309 assert!(m.update(true, 1.5).is_err());
310 assert!(m.update(true, -0.1).is_err());
311 assert!(m.update(true, f64::NAN).is_err());
312 }
313
314 #[test]
315 fn empty_metrics_return_none() {
316 assert!(Accuracy::default().value().is_none());
317 assert!(Precision::default().value().is_none());
318 assert!(Recall::default().value().is_none());
319 assert!(F1Score::default().value().is_none());
320 assert!(LogLoss::default().value().is_none());
321 }
322
323 #[test]
324 fn precision_no_predictions_returns_none() {
325 let mut m = Precision::default();
326 m.update(true, false).unwrap();
327 m.update(false, false).unwrap();
328 assert!(m.value().is_none());
329 }
330}