221 lines
4.7 KiB
Python
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())
|
|
}
|