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() ) }