pub fn dcg_at_k(relevances: &[f64], k: usize) -> f64 {
let n = relevances.len().min(k);
let mut acc = 0.0;
for (i, rel) in relevances.iter().take(n).enumerate() {
let gain = (2f64.powf(*rel)) - 1.0;
let discount = ((i + 2) as f64).log2();
acc += gain / discount;
}
acc
}
pub fn ndcg_at_k(relevances: &[f64], k: usize) -> f64 {
let mut ideal = relevances.to_vec();
ideal.sort_by(|a, b| b.total_cmp(a));
let idcg = dcg_at_k(&ideal, k);
if idcg == 0.0 {
return 0.0;
}
dcg_at_k(relevances, k) / idcg
}
pub fn average_precision_at_k(is_relevant: &[bool], total_relevant: usize, k: usize) -> f64 {
if total_relevant == 0 {
return 0.0;
}
let n = is_relevant.len().min(k);
let mut hits: f64 = 0.0;
let mut acc: f64 = 0.0;
for (i, rel) in is_relevant.iter().take(n).enumerate() {
if *rel {
hits += 1.0;
let precision_at_i = hits / (i + 1) as f64;
acc += precision_at_i;
}
}
acc / total_relevant as f64
}
pub fn mean_average_precision_at_k(per_query: &[(Vec<bool>, usize)], k: usize) -> f64 {
if per_query.is_empty() {
return 0.0;
}
let n = per_query.len() as f64;
let sum: f64 = per_query
.iter()
.map(|(rel, total)| average_precision_at_k(rel, *total, k))
.sum();
sum / n
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) {
assert!((a - b).abs() < tol, "expected {a} ~ {b} within {tol}");
}
#[test]
fn dcg_matches_known_value() {
let dcg = dcg_at_k(&[3.0, 2.0, 3.0, 0.0, 1.0, 2.0], 6);
approx_eq(dcg, 13.848, 0.005);
}
#[test]
fn ndcg_perfect_ranking_is_one() {
let r = vec![3.0, 2.0, 1.0, 0.0];
approx_eq(ndcg_at_k(&r, 4), 1.0, 1e-9);
}
#[test]
fn ndcg_reversed_ranking_is_below_one() {
let r = vec![0.0, 1.0, 2.0, 3.0];
let n = ndcg_at_k(&r, 4);
assert!(n > 0.0 && n < 1.0, "{n}");
}
#[test]
fn average_precision_known_pattern() {
let ap = average_precision_at_k(&[true, false, true, false, false], 2, 5);
approx_eq(ap, 0.8333, 0.001);
}
#[test]
fn map_averages_per_query() {
let q1 = (vec![true, false, true], 2);
let q2 = (vec![false, true, true], 2);
let map = mean_average_precision_at_k(&[q1, q2], 3);
approx_eq(map, 0.7083, 0.001);
}
#[test]
fn empty_input_is_zero() {
assert_eq!(dcg_at_k(&[], 5), 0.0);
assert_eq!(ndcg_at_k(&[], 5), 0.0);
assert_eq!(average_precision_at_k(&[], 0, 5), 0.0);
assert_eq!(mean_average_precision_at_k(&[], 5), 0.0);
}
}