#[derive(Debug, Default, Clone)]
pub struct Scorecard {
brier_sum: f64,
brier_n: u64,
bins: [u64; 5],
bin_hits: [u64; 5],
choice_hit: u64,
choice_n: u64,
}
impl Scorecard {
pub fn add_noul(&mut self, probability: f64, truth: bool) {
let y = if truth { 1.0 } else { 0.0 };
let err = probability - y;
self.brier_sum += err * err;
self.brier_n += 1;
let bin = (probability.clamp(0.0, 0.999) * 5.0) as usize;
self.bins[bin] += 1;
if truth {
self.bin_hits[bin] += 1;
}
}
pub fn add_choice(&mut self, matched: bool) {
self.choice_n += 1;
if matched {
self.choice_hit += 1;
}
}
pub fn brier(&self) -> Option<f64> {
if self.brier_n == 0 {
None
} else {
Some(self.brier_sum / self.brier_n as f64)
}
}
pub fn choice_accuracy(&self) -> Option<f64> {
if self.choice_n == 0 {
None
} else {
Some(self.choice_hit as f64 / self.choice_n as f64)
}
}
pub fn bins(&self) -> impl Iterator<Item = (usize, u64, f64)> + '_ {
self.bins.iter().enumerate().filter_map(|(index, count)| {
if *count == 0 {
None
} else {
Some((index, *count, self.bin_hits[index] as f64 / *count as f64))
}
})
}
pub fn is_empty(&self) -> bool {
self.brier_n == 0 && self.choice_n == 0
}
}
#[cfg(test)]
mod tests {
use super::Scorecard;
#[test]
fn a_confident_wrong_noul_scores_worse_than_a_low_one() {
let mut wrong = Scorecard::default();
wrong.add_noul(0.9, false);
let mut closer = Scorecard::default();
closer.add_noul(0.1, false);
assert!(wrong.brier().unwrap() > closer.brier().unwrap());
}
#[test]
fn choice_accuracy_counts_matches() {
let mut card = Scorecard::default();
card.add_choice(true);
card.add_choice(false);
assert_eq!(card.choice_accuracy().unwrap(), 0.5);
}
}