///|
fn ideal_dcg(
relevances : Array[Int],
cutoff : Int,
gain_scheme : GainScheme,
) -> Double {
if cutoff <= 0 {
return 0.0
}
let copied = relevances.copy()
copied.sort_by(fn(a, b) { b - a })
let limit = Int::min(cutoff, copied.length())
let mut score = 0.0
for index in 0.. QueryEvaluation {
let normalized_cutoffs = unique_positive_cutoffs(config.cutoffs)
let relevance_map = build_relevance_map(qrels)
let relevant_levels : Array[Int] = []
let mut relevant_total = 0
for _, relevance in relevance_map {
relevant_levels.push(relevance)
if relevance >= config.relevant_threshold {
relevant_total += 1
}
}
let metrics : Map[String, Double] = Map([])
for cutoff in normalized_cutoffs {
let top = Int::min(cutoff, run.length())
let mut hits = 0
let mut judged_hits = 0
let mut average_precision = 0.0
let mut first_rr = 0.0
let mut dcg = 0.0
for index in 0..= config.relevant_threshold {
hits += 1
if first_rr == 0.0 {
first_rr = reciprocal_rank(index)
}
average_precision += to_ratio(hits, index + 1)
}
}
let denom = if relevant_total <= 0 { 1 } else { relevant_total }
let best_dcg = ideal_dcg(relevant_levels, cutoff, config.gain_scheme)
metrics[metric_name("recall", cutoff)] = to_ratio(hits, denom)
metrics[metric_name("precision", cutoff)] = to_ratio(hits, cutoff)
metrics[metric_name("hit_rate", cutoff)] = if hits > 0 { 1.0 } else { 0.0 }
metrics[metric_name("mrr", cutoff)] = first_rr
metrics[metric_name("map", cutoff)] = if relevant_total <= 0 {
0.0
} else {
average_precision / Double::from_int(relevant_total)
}
metrics[metric_name("ndcg", cutoff)] = if best_dcg == 0.0 {
0.0
} else {
dcg / best_dcg
}
metrics[metric_name("judged", cutoff)] = to_ratio(judged_hits, cutoff)
}
for cutoff in normalized_cutoffs {
let advanced = evaluate_metric_family(
qrels,
run,
cutoff,
config.relevant_threshold,
config.gain_scheme,
)
for name, value in advanced {
metrics[name] = value
}
}
{ query_id, relevant_total, retrieved_total: run.length(), metrics }
}
///|
fn summarize_queries(
queries : Array[QueryEvaluation],
) -> Array[AggregateMetric] {
let values : Map[String, Array[Double]] = Map([])
for query in queries {
for name, value in query.metrics {
let bucket = values.get_or_init(name, fn() { [] })
bucket.push(value)
}
}
let summary : Array[AggregateMetric] = []
for name, bucket in values {
if bucket.is_empty() {
continue
}
let mut total = 0.0
let mut min_value = bucket[0]
let mut max_value = bucket[0]
for value in bucket {
total += value
min_value = Double::min(min_value, value)
max_value = Double::max(max_value, value)
}
summary.push({
name,
mean: total / Double::from_int(bucket.length()),
min: min_value,
max: max_value,
})
}
summary.sort_by(fn(a, b) { a.name.compare(b.name) })
summary
}
///|
pub fn evaluate_benchmark(
qrels : Array[JudgedDoc],
run : Array[RetrievedDoc],
config? : EvalConfig = EvalConfig::default(),
) -> BenchmarkReport {
let qrels_by_query = group_qrels(qrels)
let run_by_query = group_runs(run)
let query_ids = sorted_query_ids(qrels_by_query, run_by_query)
let queries : Array[QueryEvaluation] = []
for query_id in query_ids {
let query_qrels = qrels_by_query.get_or_default(query_id, [])
let query_run = run_by_query.get_or_default(query_id, [])
queries.push(evaluate_query(query_id, query_qrels, query_run, config~))
}
{
cutoffs: unique_positive_cutoffs(config.cutoffs),
relevant_threshold: config.relevant_threshold,
query_count: queries.length(),
queries,
summary: summarize_queries(queries),
}
}