pub fn precision_recall_curve(
tp_cum: &[f64],
fp_cum: &[f64],
num_gt: usize,
rec_thrs: &[f64],
) -> (f64, Vec<(usize, f64, usize)>) {
let nd = tp_cum.len();
if nd == 0 || num_gt == 0 {
return (0.0, vec![]);
}
let num_gt_f = num_gt as f64;
let mut rc = vec![0.0f64; nd];
let mut pr = vec![0.0f64; nd];
for d in 0..nd {
rc[d] = tp_cum[d] / num_gt_f;
let total = tp_cum[d] + fp_cum[d];
pr[d] = if total > 0.0 { tp_cum[d] / total } else { 0.0 };
}
let final_recall = rc[nd - 1];
for d in (0..nd.saturating_sub(1)).rev() {
pr[d] = pr[d].max(pr[d + 1]);
}
let mut result = Vec::with_capacity(rec_thrs.len());
let mut rc_ptr = 0;
for (r_idx, &rec_thr) in rec_thrs.iter().enumerate() {
while rc_ptr < nd && rc[rc_ptr] < rec_thr {
rc_ptr += 1;
}
if rc_ptr < nd {
result.push((r_idx, pr[rc_ptr], rc_ptr));
}
}
(final_recall, result)
}
pub fn average_precision(
scores: &[f64],
matched: &[bool],
ignored: Option<&[bool]>,
num_gt: usize,
rec_thrs: &[f64],
) -> f64 {
let nd = scores.len();
if nd == 0 || num_gt == 0 || rec_thrs.is_empty() {
return 0.0;
}
let mut order: Vec<usize> = (0..nd).collect();
order.sort_by(|&a, &b| {
scores[b]
.partial_cmp(&scores[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut tp_cum = Vec::with_capacity(nd);
let mut fp_cum = Vec::with_capacity(nd);
let (mut tp, mut fp) = (0.0f64, 0.0f64);
for &i in &order {
if !ignored.is_some_and(|ig| ig[i]) {
if matched[i] {
tp += 1.0;
} else {
fp += 1.0;
}
}
tp_cum.push(tp);
fp_cum.push(fp);
}
let (_, curve) = precision_recall_curve(&tp_cum, &fp_cum, num_gt, rec_thrs);
curve.iter().map(|&(_, prec, _)| prec).sum::<f64>() / rec_thrs.len() as f64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_or_no_gt_is_zero() {
assert_eq!(precision_recall_curve(&[], &[], 5, &[0.5]), (0.0, vec![]));
assert_eq!(
precision_recall_curve(&[1.0], &[0.0], 0, &[0.5]),
(0.0, vec![])
);
}
#[test]
fn perfect_detections_precision_one() {
let tp = [1.0, 2.0, 3.0, 4.0];
let fp = [0.0, 0.0, 0.0, 0.0];
let (final_recall, curve) = precision_recall_curve(&tp, &fp, 4, &[0.0, 0.5, 1.0]);
assert!((final_recall - 1.0).abs() < 1e-12);
assert_eq!(curve.len(), 3);
for (_, p, _) in &curve {
assert!((p - 1.0).abs() < 1e-12);
}
}
#[test]
fn unreachable_recall_thresholds_omitted() {
let tp = [1.0, 1.0];
let fp = [0.0, 1.0];
let (final_recall, curve) = precision_recall_curve(&tp, &fp, 4, &[0.1, 0.25, 0.5, 1.0]);
assert!((final_recall - 0.25).abs() < 1e-12);
assert_eq!(curve.iter().map(|c| c.0).collect::<Vec<_>>(), vec![0, 1]);
}
#[test]
fn voc_interpolation_makes_precision_monotone() {
let tp = [1.0, 1.0, 2.0];
let fp = [0.0, 1.0, 1.0];
let (_, curve) = precision_recall_curve(&tp, &fp, 3, &[0.5]);
assert_eq!(curve.len(), 1);
let (_, p, ptr) = curve[0];
assert_eq!(ptr, 2);
assert!((p - 2.0 / 3.0).abs() < 1e-12);
}
}