dp-zp-agent/tools/rag/evaluation/metrics.py

221 lines
4.7 KiB
Python

from __future__ import annotations
from collections import defaultdict
from typing import Any
EVALUATION_K_VALUES = (
1,
3,
5,
)
def unique_document_ranking(
results: list[dict[str, Any]],
) -> list[dict[str, Any]]:
selected: list[dict[str, Any]] = []
seen: set[str] = set()
for item in results:
document_path = str(item["document_path"])
if document_path in seen:
continue
seen.add(document_path)
selected.append(item)
return selected
def first_relevant_rank(
ranked_documents: list[dict[str, Any]],
expected_documents: set[str],
) -> int | None:
for rank, item in enumerate(
ranked_documents,
start=1,
):
if item["document_path"] in expected_documents:
return rank
return None
def recall_at_k(
ranked_documents: list[dict[str, Any]],
expected_documents: set[str],
k: int,
) -> float:
if not expected_documents:
return 0.0
retrieved = {str(item["document_path"]) for item in ranked_documents[:k]}
relevant_retrieved = retrieved & expected_documents
return len(relevant_retrieved) / len(expected_documents)
def evaluate_question_mode(
question: dict[str, Any],
mode: str,
results: list[dict[str, Any]],
) -> dict[str, Any]:
ranked_documents = unique_document_ranking(results)
expected_documents = {str(value) for value in question["expected_documents"]}
rank = first_relevant_rank(
ranked_documents,
expected_documents,
)
reciprocal_rank = 0.0 if rank is None else 1.0 / rank
row: dict[str, Any] = {
"id": question["id"],
"split": question.get("split"),
"category": question.get(
"category",
"unknown",
),
"difficulty": question.get(
"difficulty",
"unknown",
),
"question": question["question"],
"mode": mode,
"expected_documents": sorted(expected_documents),
"first_relevant_rank": rank,
"reciprocal_rank": round(
reciprocal_rank,
6,
),
"top_documents": [item["document_path"] for item in ranked_documents],
"top_source_urls": [item.get("source_url") for item in ranked_documents],
}
for k in EVALUATION_K_VALUES:
row[f"hit_at_{k}"] = 1 if (rank is not None and rank <= k) else 0
row[f"recall_at_{k}"] = round(
recall_at_k(
ranked_documents,
expected_documents,
k,
),
6,
)
return row
def average(
values: list[float],
) -> float:
if not values:
return 0.0
return sum(values) / len(values)
def aggregate_metrics(
rows: list[dict[str, Any]],
) -> dict[str, Any]:
if not rows:
return {
"questions": 0,
"hit_at_1": 0.0,
"hit_at_3": 0.0,
"hit_at_5": 0.0,
"mrr": 0.0,
"recall_at_5": 0.0,
}
return {
"questions": len(rows),
"hit_at_1": round(
average([float(row["hit_at_1"]) for row in rows]),
6,
),
"hit_at_3": round(
average([float(row["hit_at_3"]) for row in rows]),
6,
),
"hit_at_5": round(
average([float(row["hit_at_5"]) for row in rows]),
6,
),
"mrr": round(
average([float(row["reciprocal_rank"]) for row in rows]),
6,
),
"recall_at_5": round(
average([float(row["recall_at_5"]) for row in rows]),
6,
),
}
def aggregate_by_category(
rows: list[dict[str, Any]],
) -> dict[
str,
dict[str, Any],
]:
grouped: dict[
str,
list[dict[str, Any]],
] = defaultdict(list)
for row in rows:
category = str(
row.get(
"category",
"unknown",
)
)
grouped[category].append(row)
return {
category: aggregate_metrics(category_rows)
for (
category,
category_rows,
) in sorted(grouped.items())
}
def aggregate_by_difficulty(
rows: list[dict[str, Any]],
) -> dict[
str,
dict[str, Any],
]:
grouped: dict[
str,
list[dict[str, Any]],
] = defaultdict(list)
for row in rows:
difficulty = str(
row.get(
"difficulty",
"unknown",
)
)
grouped[difficulty].append(row)
return {
difficulty: aggregate_metrics(difficulty_rows)
for (
difficulty,
difficulty_rows,
) in sorted(grouped.items())
}