use crate::error::{MetricsError, Result};
pub fn mean_reciprocal_rank(ranked_relevance: &[Vec<f64>]) -> Result<f64> {
if ranked_relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"ranked_relevance must not be empty".to_string(),
));
}
let mut rr_sum = 0.0;
for query_rel in ranked_relevance {
let mut found = false;
for (pos, &rel) in query_rel.iter().enumerate() {
if rel > 0.0 {
rr_sum += 1.0 / (pos as f64 + 1.0);
found = true;
break;
}
}
if !found {
}
}
Ok(rr_sum / ranked_relevance.len() as f64)
}
fn dcg_at_k(relevance: &[f64], k: usize) -> f64 {
let limit = k.min(relevance.len());
let mut dcg = 0.0;
for i in 0..limit {
let gain = (2.0_f64).powf(relevance[i]) - 1.0;
let discount = (i as f64 + 2.0).log2(); dcg += gain / discount;
}
dcg
}
pub fn ndcg(ranked_relevance: &[Vec<f64>], k: usize) -> Result<f64> {
if ranked_relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"ranked_relevance must not be empty".to_string(),
));
}
if k == 0 {
return Err(MetricsError::InvalidInput(
"k must be greater than 0".to_string(),
));
}
let mut ndcg_sum = 0.0;
let mut valid_queries = 0;
for query_rel in ranked_relevance {
let dcg = dcg_at_k(query_rel, k);
let mut ideal_rel = query_rel.clone();
ideal_rel.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let idcg = dcg_at_k(&ideal_rel, k);
if idcg > 0.0 {
ndcg_sum += dcg / idcg;
valid_queries += 1;
} else {
valid_queries += 1;
}
}
if valid_queries == 0 {
return Ok(0.0);
}
Ok(ndcg_sum / valid_queries as f64)
}
pub fn ndcg_single(relevance: &[f64], k: usize) -> Result<f64> {
if relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"relevance must not be empty".to_string(),
));
}
if k == 0 {
return Err(MetricsError::InvalidInput(
"k must be greater than 0".to_string(),
));
}
let dcg = dcg_at_k(relevance, k);
let mut ideal_rel: Vec<f64> = relevance.to_vec();
ideal_rel.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let idcg = dcg_at_k(&ideal_rel, k);
if idcg <= 0.0 {
return Ok(0.0);
}
Ok(dcg / idcg)
}
pub fn map_at_k(ranked_relevance: &[Vec<f64>], k: usize) -> Result<f64> {
if ranked_relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"ranked_relevance must not be empty".to_string(),
));
}
if k == 0 {
return Err(MetricsError::InvalidInput(
"k must be greater than 0".to_string(),
));
}
let mut ap_sum = 0.0;
for query_rel in ranked_relevance {
let limit = k.min(query_rel.len());
let mut relevant_count = 0.0;
let mut precision_sum = 0.0;
for i in 0..limit {
if query_rel[i] > 0.0 {
relevant_count += 1.0;
precision_sum += relevant_count / (i as f64 + 1.0);
}
}
let total_relevant: f64 = query_rel.iter().filter(|&&r| r > 0.0).count() as f64;
if total_relevant > 0.0 {
ap_sum += precision_sum / total_relevant;
}
}
Ok(ap_sum / ranked_relevance.len() as f64)
}
pub fn mean_average_precision(ranked_relevance: &[Vec<f64>]) -> Result<f64> {
if ranked_relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"ranked_relevance must not be empty".to_string(),
));
}
let max_k = ranked_relevance.iter().map(|q| q.len()).max().unwrap_or(0);
if max_k == 0 {
return Err(MetricsError::InvalidInput(
"All query result lists are empty".to_string(),
));
}
map_at_k(ranked_relevance, max_k)
}
pub fn precision_at_k(ranked_relevance: &[Vec<f64>], k: usize) -> Result<f64> {
if ranked_relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"ranked_relevance must not be empty".to_string(),
));
}
if k == 0 {
return Err(MetricsError::InvalidInput(
"k must be greater than 0".to_string(),
));
}
let mut prec_sum = 0.0;
for query_rel in ranked_relevance {
let limit = k.min(query_rel.len());
let relevant_in_top_k = query_rel[..limit].iter().filter(|&&r| r > 0.0).count();
prec_sum += relevant_in_top_k as f64 / k as f64;
}
Ok(prec_sum / ranked_relevance.len() as f64)
}
pub fn recall_at_k(ranked_relevance: &[Vec<f64>], k: usize) -> Result<f64> {
if ranked_relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"ranked_relevance must not be empty".to_string(),
));
}
if k == 0 {
return Err(MetricsError::InvalidInput(
"k must be greater than 0".to_string(),
));
}
let mut recall_sum = 0.0;
let mut valid_queries = 0;
for query_rel in ranked_relevance {
let total_relevant: usize = query_rel.iter().filter(|&&r| r > 0.0).count();
if total_relevant == 0 {
continue;
}
let limit = k.min(query_rel.len());
let relevant_in_top_k = query_rel[..limit].iter().filter(|&&r| r > 0.0).count();
recall_sum += relevant_in_top_k as f64 / total_relevant as f64;
valid_queries += 1;
}
if valid_queries == 0 {
return Ok(0.0);
}
Ok(recall_sum / valid_queries as f64)
}
pub fn hit_rate(ranked_relevance: &[Vec<f64>], k: usize) -> Result<f64> {
if ranked_relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"ranked_relevance must not be empty".to_string(),
));
}
if k == 0 {
return Err(MetricsError::InvalidInput(
"k must be greater than 0".to_string(),
));
}
let mut hits = 0;
for query_rel in ranked_relevance {
let limit = k.min(query_rel.len());
let has_relevant = query_rel[..limit].iter().any(|&r| r > 0.0);
if has_relevant {
hits += 1;
}
}
Ok(hits as f64 / ranked_relevance.len() as f64)
}
pub fn r_precision(ranked_relevance: &[Vec<f64>]) -> Result<f64> {
if ranked_relevance.is_empty() {
return Err(MetricsError::InvalidInput(
"ranked_relevance must not be empty".to_string(),
));
}
let mut rp_sum = 0.0;
let mut valid_queries = 0;
for query_rel in ranked_relevance {
let total_relevant: usize = query_rel.iter().filter(|&&r| r > 0.0).count();
if total_relevant == 0 {
continue;
}
let limit = total_relevant.min(query_rel.len());
let relevant_in_top_r = query_rel[..limit].iter().filter(|&&r| r > 0.0).count();
rp_sum += relevant_in_top_r as f64 / total_relevant as f64;
valid_queries += 1;
}
if valid_queries == 0 {
return Ok(0.0);
}
Ok(rp_sum / valid_queries as f64)
}
pub fn f1_at_k(ranked_relevance: &[Vec<f64>], k: usize) -> Result<f64> {
let p = precision_at_k(ranked_relevance, k)?;
let r = recall_at_k(ranked_relevance, k)?;
if p + r <= 0.0 {
return Ok(0.0);
}
Ok(2.0 * p * r / (p + r))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mrr_perfect_ranking() {
let rel = vec![vec![1.0, 0.0, 0.0], vec![1.0, 0.0, 0.0]];
let mrr = mean_reciprocal_rank(&rel).expect("should succeed");
assert!((mrr - 1.0).abs() < 1e-10);
}
#[test]
fn test_mrr_second_position() {
let rel = vec![vec![0.0, 1.0, 0.0], vec![0.0, 1.0, 0.0]];
let mrr = mean_reciprocal_rank(&rel).expect("should succeed");
assert!((mrr - 0.5).abs() < 1e-10);
}
#[test]
fn test_mrr_no_relevant() {
let rel = vec![vec![0.0, 0.0, 0.0]];
let mrr = mean_reciprocal_rank(&rel).expect("should succeed");
assert!((mrr - 0.0).abs() < 1e-10);
}
#[test]
fn test_mrr_mixed() {
let rel = vec![vec![1.0, 0.0, 0.0], vec![0.0, 0.0, 1.0]];
let mrr = mean_reciprocal_rank(&rel).expect("should succeed");
let expected = (1.0 + 1.0 / 3.0) / 2.0;
assert!((mrr - expected).abs() < 1e-10);
}
#[test]
fn test_mrr_empty_input() {
let rel: Vec<Vec<f64>> = vec![];
assert!(mean_reciprocal_rank(&rel).is_err());
}
#[test]
fn test_ndcg_perfect_binary() {
let rel = vec![vec![1.0, 1.0, 0.0, 0.0]];
let val = ndcg(&rel, 4).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_ndcg_reversed_binary() {
let rel = vec![vec![0.0, 0.0, 1.0, 1.0]];
let val = ndcg(&rel, 4).expect("should succeed");
assert!(val < 1.0);
assert!(val > 0.0);
}
#[test]
fn test_ndcg_graded_relevance() {
let rel = vec![vec![3.0, 2.0, 1.0, 0.0]];
let val = ndcg(&rel, 4).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_ndcg_single_perfect() {
let val = ndcg_single(&[3.0, 2.0, 1.0, 0.0], 4).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_ndcg_no_relevant() {
let rel = vec![vec![0.0, 0.0, 0.0]];
let val = ndcg(&rel, 3).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_ndcg_k_zero() {
let rel = vec![vec![1.0, 0.0]];
assert!(ndcg(&rel, 0).is_err());
}
#[test]
fn test_map_at_k_perfect() {
let rel = vec![vec![1.0, 1.0, 0.0, 0.0]];
let val = map_at_k(&rel, 4).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_map_at_k_spread() {
let rel = vec![vec![1.0, 0.0, 1.0, 0.0]];
let val = map_at_k(&rel, 4).expect("should succeed");
assert!((val - 5.0 / 6.0).abs() < 1e-10);
}
#[test]
fn test_map_at_k_no_relevant() {
let rel = vec![vec![0.0, 0.0, 0.0]];
let val = map_at_k(&rel, 3).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_map_at_k_cutoff() {
let rel = vec![vec![0.0, 0.0, 0.0, 1.0]];
let val = map_at_k(&rel, 2).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_map_at_k_empty_input() {
let rel: Vec<Vec<f64>> = vec![];
assert!(map_at_k(&rel, 3).is_err());
}
#[test]
fn test_mean_average_precision() {
let rel = vec![vec![1.0, 0.0, 1.0, 0.0], vec![0.0, 1.0, 0.0, 1.0]];
let val = mean_average_precision(&rel).expect("should succeed");
assert!(val > 0.0 && val <= 1.0);
}
#[test]
fn test_precision_at_k_all_relevant() {
let rel = vec![vec![1.0, 1.0, 1.0]];
let val = precision_at_k(&rel, 3).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_precision_at_k_none_relevant() {
let rel = vec![vec![0.0, 0.0, 0.0]];
let val = precision_at_k(&rel, 3).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_precision_at_k_partial() {
let rel = vec![vec![1.0, 0.0, 1.0, 0.0]];
let val = precision_at_k(&rel, 3).expect("should succeed");
assert!((val - 2.0 / 3.0).abs() < 1e-10);
}
#[test]
fn test_precision_at_k_larger_than_list() {
let rel = vec![vec![1.0, 1.0, 0.0]];
let val = precision_at_k(&rel, 5).expect("should succeed");
assert!((val - 2.0 / 5.0).abs() < 1e-10);
}
#[test]
fn test_precision_at_k_multiple_queries() {
let rel = vec![
vec![1.0, 1.0, 0.0], vec![0.0, 0.0, 1.0], ];
let val = precision_at_k(&rel, 2).expect("should succeed");
assert!((val - 0.5).abs() < 1e-10);
}
#[test]
fn test_recall_at_k_all_found() {
let rel = vec![vec![1.0, 0.0, 1.0, 0.0]];
let val = recall_at_k(&rel, 3).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_recall_at_k_partial() {
let rel = vec![vec![1.0, 0.0, 1.0, 1.0]];
let val = recall_at_k(&rel, 2).expect("should succeed");
assert!((val - 1.0 / 3.0).abs() < 1e-10);
}
#[test]
fn test_recall_at_k_none_found() {
let rel = vec![vec![0.0, 0.0, 1.0, 1.0]];
let val = recall_at_k(&rel, 2).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_recall_at_k_no_relevant_docs() {
let rel = vec![vec![0.0, 0.0, 0.0]];
let val = recall_at_k(&rel, 3).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_recall_at_k_multiple_queries() {
let rel = vec![
vec![1.0, 0.0, 1.0], vec![0.0, 1.0, 0.0], ];
let val = recall_at_k(&rel, 1).expect("should succeed");
assert!((val - 0.25).abs() < 1e-10);
}
#[test]
fn test_hit_rate_all_hit() {
let rel = vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]];
let val = hit_rate(&rel, 2).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_hit_rate_no_hit() {
let rel = vec![vec![0.0, 0.0, 1.0], vec![0.0, 0.0, 1.0]];
let val = hit_rate(&rel, 2).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_hit_rate_partial() {
let rel = vec![
vec![1.0, 0.0, 0.0], vec![0.0, 0.0, 1.0], ];
let val = hit_rate(&rel, 1).expect("should succeed");
assert!((val - 0.5).abs() < 1e-10);
}
#[test]
fn test_hit_rate_k_larger_than_list() {
let rel = vec![vec![0.0, 1.0]];
let val = hit_rate(&rel, 10).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_hit_rate_empty() {
let rel: Vec<Vec<f64>> = vec![];
assert!(hit_rate(&rel, 3).is_err());
}
#[test]
fn test_r_precision_perfect() {
let rel = vec![vec![1.0, 1.0, 0.0, 0.0]];
let val = r_precision(&rel).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_r_precision_half() {
let rel = vec![vec![1.0, 0.0, 1.0, 0.0]];
let val = r_precision(&rel).expect("should succeed");
assert!((val - 0.5).abs() < 1e-10);
}
#[test]
fn test_r_precision_none_at_top() {
let rel = vec![vec![0.0, 1.0]];
let val = r_precision(&rel).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_r_precision_no_relevant() {
let rel = vec![vec![0.0, 0.0, 0.0]];
let val = r_precision(&rel).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_f1_at_k_perfect() {
let rel = vec![vec![1.0, 1.0, 0.0]];
let val = f1_at_k(&rel, 2).expect("should succeed");
assert!((val - 1.0).abs() < 1e-10);
}
#[test]
fn test_f1_at_k_zero() {
let rel = vec![vec![0.0, 0.0, 0.0]];
let val = f1_at_k(&rel, 2).expect("should succeed");
assert!((val - 0.0).abs() < 1e-10);
}
#[test]
fn test_f1_at_k_mixed() {
let rel = vec![vec![1.0, 0.0, 1.0]];
let val = f1_at_k(&rel, 2).expect("should succeed");
assert!((val - 0.5).abs() < 1e-10);
}
#[test]
fn test_f1_at_k_k_zero() {
let rel = vec![vec![1.0, 0.0]];
assert!(f1_at_k(&rel, 0).is_err());
}
}