use super::StatsError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PrPoint {
pub recall: f64,
pub precision: f64,
pub threshold: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PrCurve {
pub points: Vec<PrPoint>,
pub average_precision: f64,
pub baseline: f64,
}
pub fn precision_recall_curve(scores: &[f64], labels: &[bool]) -> Result<PrCurve, StatsError> {
if scores.len() != labels.len() {
return Err(StatsError::LengthMismatch {
scores: scores.len(),
labels: labels.len(),
});
}
if scores.is_empty() {
return Err(StatsError::EmptyInput);
}
let total_p = labels.iter().filter(|&&l| l).count();
if total_p == 0 {
return Err(StatsError::NoPositiveLabels);
}
let baseline = total_p as f64 / labels.len() as f64;
let mut order: Vec<usize> = (0..scores.len()).collect();
order.sort_by(|&a, &b| {
scores[b]
.partial_cmp(&scores[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let p = total_p as f64;
let mut points = Vec::new();
let mut tp = 0usize;
let mut fp = 0usize;
let mut i = 0usize;
let mut prev_recall = 0.0;
let mut average_precision = 0.0;
while i < order.len() {
let score = scores[order[i]];
while i < order.len() && scores[order[i]] == score {
if labels[order[i]] {
tp += 1;
} else {
fp += 1;
}
i += 1;
}
let recall = tp as f64 / p;
let precision = if tp + fp == 0 {
1.0
} else {
tp as f64 / (tp + fp) as f64
};
average_precision += (recall - prev_recall) * precision;
prev_recall = recall;
points.push(PrPoint {
recall,
precision,
threshold: score,
});
}
Ok(PrCurve {
points,
average_precision,
baseline,
})
}