qualia_core_db/solvers/learning/metrics/
classification.rs1use crate::solvers::statistics::correlation::rank_into;
5
6pub fn accuracy(y_true: &[usize], y_pred: &[usize]) -> Option<f64> {
9 let n = y_true.len();
10 if n == 0 || n != y_pred.len() {
11 return None;
12 }
13 let correct = y_true.iter().zip(y_pred).filter(|(a, b)| a == b).count();
14 Some(correct as f64 / n as f64)
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct ConfusionBinary {
20 pub tp: u64,
21 pub fp: u64,
22 pub tn: u64,
23 pub fn_: u64,
24}
25
26impl ConfusionBinary {
27 pub fn total(&self) -> u64 {
28 self.tp + self.fp + self.tn + self.fn_
29 }
30 pub fn accuracy(&self) -> f64 {
31 let t = self.total();
32 if t == 0 {
33 return 0.0;
34 }
35 (self.tp + self.tn) as f64 / t as f64
36 }
37 pub fn precision(&self) -> f64 {
39 let d = self.tp + self.fp;
40 if d == 0 {
41 0.0
42 } else {
43 self.tp as f64 / d as f64
44 }
45 }
46 pub fn recall(&self) -> f64 {
48 let d = self.tp + self.fn_;
49 if d == 0 {
50 0.0
51 } else {
52 self.tp as f64 / d as f64
53 }
54 }
55 pub fn specificity(&self) -> f64 {
57 let d = self.tn + self.fp;
58 if d == 0 {
59 0.0
60 } else {
61 self.tn as f64 / d as f64
62 }
63 }
64 pub fn f1(&self) -> f64 {
66 let (p, r) = (self.precision(), self.recall());
67 if p + r == 0.0 {
68 0.0
69 } else {
70 2.0 * p * r / (p + r)
71 }
72 }
73}
74
75pub fn confusion_binary(y_true: &[bool], y_pred: &[bool]) -> Option<ConfusionBinary> {
77 let n = y_true.len();
78 if n == 0 || n != y_pred.len() {
79 return None;
80 }
81 let mut c = ConfusionBinary {
82 tp: 0,
83 fp: 0,
84 tn: 0,
85 fn_: 0,
86 };
87 for (&t, &p) in y_true.iter().zip(y_pred) {
88 match (t, p) {
89 (true, true) => c.tp += 1,
90 (false, true) => c.fp += 1,
91 (false, false) => c.tn += 1,
92 (true, false) => c.fn_ += 1,
93 }
94 }
95 Some(c)
96}
97
98pub fn roc_auc(scores: &[f64], labels: &[bool]) -> Option<f64> {
103 let n = scores.len();
104 if n == 0 || n != labels.len() {
105 return None;
106 }
107 let n_pos = labels.iter().filter(|&&l| l).count();
108 let n_neg = n - n_pos;
109 if n_pos == 0 || n_neg == 0 {
110 return None; }
112 let mut idx = vec![0usize; n];
113 let mut ranks = vec![0.0f64; n];
114 rank_into(scores, &mut idx, &mut ranks)?;
115 let sum_pos_ranks: f64 = labels
116 .iter()
117 .zip(ranks.iter())
118 .filter(|(&l, _)| l)
119 .map(|(_, &r)| r)
120 .sum();
121 let n_pos_f = n_pos as f64;
122 Some((sum_pos_ranks - n_pos_f * (n_pos_f + 1.0) / 2.0) / (n_pos_f * n_neg as f64))
123}
124
125pub fn log_loss(probs: &[f64], labels: &[bool]) -> Option<f64> {
128 let n = probs.len();
129 if n == 0 || n != labels.len() {
130 return None;
131 }
132 const EPS: f64 = 1e-15;
133 let mut s = 0.0;
134 for (&p, &y) in probs.iter().zip(labels) {
135 let p = p.clamp(EPS, 1.0 - EPS);
136 s += if y { -p.ln() } else { -(1.0 - p).ln() };
137 }
138 Some(s / n as f64)
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn accuracy_basic() {
147 assert!((accuracy(&[0, 1, 2, 1], &[0, 1, 1, 1]).unwrap() - 0.75).abs() < 1e-12);
148 assert_eq!(accuracy(&[], &[]), None);
149 }
150
151 #[test]
152 fn confusion_rates() {
153 let t = [true, true, false, false, false, true];
155 let p = [true, true, true, false, false, false];
156 let c = confusion_binary(&t, &p).unwrap();
157 assert_eq!((c.tp, c.fp, c.tn, c.fn_), (2, 1, 2, 1));
158 assert!((c.precision() - 2.0 / 3.0).abs() < 1e-12);
159 assert!((c.recall() - 2.0 / 3.0).abs() < 1e-12);
160 assert!((c.f1() - 2.0 / 3.0).abs() < 1e-12);
161 assert!((c.accuracy() - 4.0 / 6.0).abs() < 1e-12);
162 }
163
164 #[test]
165 fn auc_perfect_and_random() {
166 let scores = [0.1, 0.2, 0.3, 0.8, 0.9, 1.0];
168 let labels = [false, false, false, true, true, true];
169 assert!((roc_auc(&scores, &labels).unwrap() - 1.0).abs() < 1e-12);
170 let rev = [true, true, true, false, false, false];
172 assert!(roc_auc(&scores, &rev).unwrap().abs() < 1e-12);
173 assert_eq!(roc_auc(&scores, &[true; 6]), None);
175 }
176
177 #[test]
178 fn auc_known_value() {
179 let scores = [0.2, 0.4, 0.6, 0.8];
181 let labels = [false, true, false, true];
182 assert!((roc_auc(&scores, &labels).unwrap() - 0.75).abs() < 1e-12);
183 }
184
185 #[test]
186 fn log_loss_rewards_confident_correct() {
187 let confident = log_loss(&[0.99, 0.01], &[true, false]).unwrap();
188 let unsure = log_loss(&[0.5, 0.5], &[true, false]).unwrap();
189 assert!(confident < unsure);
190 }
191}