iris-vector-rag 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adapters/rag_templates_bridge.py +578 -0
- common/__init__.py +1 -0
- common/batch_utils.py +294 -0
- common/config.py +58 -0
- common/connection_manager.py +242 -0
- common/connection_pool.py +445 -0
- common/database.py +141 -0
- common/database_schema_manager.py +289 -0
- common/db_init_with_indexes.py +181 -0
- common/db_vector_utils.py +181 -0
- common/environment_manager.py +220 -0
- common/huggingface_utils.py +206 -0
- common/iris_client.py +92 -0
- common/iris_connection_manager.py +411 -0
- common/iris_connection_pool.py +360 -0
- common/iris_connector.py +71 -0
- common/iris_dbapi_connector.py +323 -0
- common/iris_index_utils.py +195 -0
- common/iris_sql_utils.py +704 -0
- common/iris_stream_reader.py +166 -0
- common/llm_cache_config.py +308 -0
- common/llm_cache_iris.py +671 -0
- common/llm_cache_manager.py +911 -0
- common/utils.py +559 -0
- common/vector_sql_utils.py +584 -0
- evaluation_framework/__init__.py +66 -0
- evaluation_framework/biomedical_question_generator.py +826 -0
- evaluation_framework/comparative_analysis_system.py +1173 -0
- evaluation_framework/empirical_reporting.py +1298 -0
- evaluation_framework/evaluation_orchestrator.py +1123 -0
- evaluation_framework/execute_production_evaluation.py +382 -0
- evaluation_framework/pmc_data_pipeline.py +1332 -0
- evaluation_framework/production_evaluation_config.py +353 -0
- evaluation_framework/ragas_metrics_framework.py +1013 -0
- evaluation_framework/real_production_evaluation.py +544 -0
- evaluation_framework/run_10k_evaluation.py +175 -0
- evaluation_framework/simple_test.py +364 -0
- evaluation_framework/statistical_evaluation_methodology.py +1016 -0
- evaluation_framework/test_framework.py +281 -0
- evaluation_framework/test_iris_connectivity.py +244 -0
- evaluation_framework/test_small_scale_evaluation.py +432 -0
- evaluation_framework/true_e2e_evaluation.py +639 -0
- evaluation_framework/visualization_engine.py +585 -0
- iris_rag/__init__.py +284 -0
- iris_rag/config/default_config.yaml +295 -0
- iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
- iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
- iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
- iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
- iris_vector_rag-0.2.1.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive RAGAS Metrics Framework for Biomedical RAG Evaluation
|
|
3
|
+
|
|
4
|
+
This module implements all key RAGAS metrics with biomedical domain adaptations,
|
|
5
|
+
statistical rigor, and robust error handling for systematic pipeline comparison.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
1. All 7 core RAGAS metrics implementation
|
|
9
|
+
2. Biomedical domain-specific adaptations
|
|
10
|
+
3. Statistical significance testing
|
|
11
|
+
4. Confidence interval calculation
|
|
12
|
+
5. Batch processing for scalability
|
|
13
|
+
6. Comprehensive error handling and fallbacks
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
import warnings
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import pandas as pd
|
|
27
|
+
|
|
28
|
+
# Statistical analysis
|
|
29
|
+
from scipy import stats
|
|
30
|
+
from scipy.stats import bootstrap
|
|
31
|
+
|
|
32
|
+
warnings.filterwarnings("ignore")
|
|
33
|
+
|
|
34
|
+
import torch
|
|
35
|
+
|
|
36
|
+
# NLP and ML libraries
|
|
37
|
+
from sentence_transformers import SentenceTransformer, util
|
|
38
|
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
39
|
+
from sklearn.metrics.pairwise import cosine_similarity
|
|
40
|
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline
|
|
41
|
+
|
|
42
|
+
# SpaCy import with fallback
|
|
43
|
+
try:
|
|
44
|
+
import spacy
|
|
45
|
+
|
|
46
|
+
SPACY_AVAILABLE = True
|
|
47
|
+
except (ImportError, ValueError) as e:
|
|
48
|
+
SPACY_AVAILABLE = False
|
|
49
|
+
warnings.warn(f"SpaCy not available: {e}. Some NLP features will be disabled.")
|
|
50
|
+
|
|
51
|
+
# RAGAS specific imports
|
|
52
|
+
try:
|
|
53
|
+
from ragas import evaluate
|
|
54
|
+
from ragas.metrics import (
|
|
55
|
+
answer_correctness,
|
|
56
|
+
answer_relevancy,
|
|
57
|
+
answer_similarity,
|
|
58
|
+
context_precision,
|
|
59
|
+
context_recall,
|
|
60
|
+
context_utilization,
|
|
61
|
+
faithfulness,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
RAGAS_AVAILABLE = True
|
|
65
|
+
except ImportError:
|
|
66
|
+
RAGAS_AVAILABLE = False
|
|
67
|
+
logging.warning("RAGAS library not available, using custom implementations")
|
|
68
|
+
|
|
69
|
+
logger = logging.getLogger(__name__)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class RAGASResult:
|
|
74
|
+
"""Structured RAGAS evaluation result with statistical measures."""
|
|
75
|
+
|
|
76
|
+
metric_name: str
|
|
77
|
+
value: float
|
|
78
|
+
confidence_interval: Tuple[float, float]
|
|
79
|
+
sample_size: int
|
|
80
|
+
standard_error: float
|
|
81
|
+
computation_time: float
|
|
82
|
+
error_message: Optional[str] = None
|
|
83
|
+
individual_scores: List[float] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class ComprehensiveRAGASResults:
|
|
88
|
+
"""Complete RAGAS evaluation results for a pipeline."""
|
|
89
|
+
|
|
90
|
+
pipeline_name: str
|
|
91
|
+
timestamp: str
|
|
92
|
+
total_queries: int
|
|
93
|
+
successful_evaluations: int
|
|
94
|
+
|
|
95
|
+
# Core RAGAS metrics
|
|
96
|
+
faithfulness: Optional[RAGASResult] = None
|
|
97
|
+
answer_relevancy: Optional[RAGASResult] = None
|
|
98
|
+
context_precision: Optional[RAGASResult] = None
|
|
99
|
+
context_recall: Optional[RAGASResult] = None
|
|
100
|
+
context_utilization: Optional[RAGASResult] = None
|
|
101
|
+
answer_correctness: Optional[RAGASResult] = None
|
|
102
|
+
answer_similarity: Optional[RAGASResult] = None
|
|
103
|
+
|
|
104
|
+
# Aggregate scores
|
|
105
|
+
overall_score: Optional[float] = None
|
|
106
|
+
weighted_score: Optional[float] = None
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
109
|
+
"""Convert to dictionary for serialization."""
|
|
110
|
+
result = {
|
|
111
|
+
"pipeline_name": self.pipeline_name,
|
|
112
|
+
"timestamp": self.timestamp,
|
|
113
|
+
"total_queries": self.total_queries,
|
|
114
|
+
"successful_evaluations": self.successful_evaluations,
|
|
115
|
+
"overall_score": self.overall_score,
|
|
116
|
+
"weighted_score": self.weighted_score,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Add metric results
|
|
120
|
+
for metric_name in [
|
|
121
|
+
"faithfulness",
|
|
122
|
+
"answer_relevancy",
|
|
123
|
+
"context_precision",
|
|
124
|
+
"context_recall",
|
|
125
|
+
"context_utilization",
|
|
126
|
+
"answer_correctness",
|
|
127
|
+
"answer_similarity",
|
|
128
|
+
]:
|
|
129
|
+
metric_result = getattr(self, metric_name)
|
|
130
|
+
if metric_result:
|
|
131
|
+
result[metric_name] = {
|
|
132
|
+
"value": metric_result.value,
|
|
133
|
+
"confidence_interval": metric_result.confidence_interval,
|
|
134
|
+
"sample_size": metric_result.sample_size,
|
|
135
|
+
"standard_error": metric_result.standard_error,
|
|
136
|
+
"computation_time": metric_result.computation_time,
|
|
137
|
+
"error_message": metric_result.error_message,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class BiomedicalRAGASFramework:
|
|
144
|
+
"""
|
|
145
|
+
Comprehensive RAGAS evaluation framework adapted for biomedical domain
|
|
146
|
+
with statistical rigor and scalability for enterprise evaluation.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
150
|
+
"""
|
|
151
|
+
Initialize the RAGAS framework with biomedical adaptations.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
config: Configuration dictionary for customizing evaluation
|
|
155
|
+
"""
|
|
156
|
+
self.config = config or self._get_default_config()
|
|
157
|
+
self.nlp = None
|
|
158
|
+
self.embedding_model = None
|
|
159
|
+
self.biomedical_qa_model = None
|
|
160
|
+
self.entailment_model = None
|
|
161
|
+
|
|
162
|
+
self._initialize_models()
|
|
163
|
+
|
|
164
|
+
# Metric weights for biomedical domain
|
|
165
|
+
self.metric_weights = {
|
|
166
|
+
"faithfulness": 0.25, # Critical for medical accuracy
|
|
167
|
+
"answer_relevancy": 0.20, # Important for user satisfaction
|
|
168
|
+
"context_precision": 0.15, # Efficiency metric
|
|
169
|
+
"context_recall": 0.15, # Completeness metric
|
|
170
|
+
"answer_correctness": 0.20, # Critical for medical accuracy
|
|
171
|
+
"answer_similarity": 0.05, # Less critical than correctness
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
logger.info("Biomedical RAGAS Framework initialized successfully")
|
|
175
|
+
|
|
176
|
+
def _get_default_config(self) -> Dict[str, Any]:
|
|
177
|
+
"""Get default configuration for biomedical RAGAS evaluation."""
|
|
178
|
+
return {
|
|
179
|
+
"embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
|
|
180
|
+
"biomedical_qa_model": "dmis-lab/biobert-base-cased-v1.1-squad",
|
|
181
|
+
"entailment_model": "microsoft/deberta-large-mnli",
|
|
182
|
+
"batch_size": 32,
|
|
183
|
+
"max_context_length": 2048,
|
|
184
|
+
"confidence_level": 0.95,
|
|
185
|
+
"bootstrap_samples": 1000,
|
|
186
|
+
"similarity_threshold": 0.7,
|
|
187
|
+
"device": "cuda" if torch.cuda.is_available() else "cpu",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
def _initialize_models(self):
|
|
191
|
+
"""Initialize all required models for RAGAS evaluation."""
|
|
192
|
+
logger.info("Initializing biomedical NLP models for RAGAS evaluation...")
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Embedding model for semantic similarity
|
|
196
|
+
self.embedding_model = SentenceTransformer(
|
|
197
|
+
self.config["embedding_model"], device=self.config["device"]
|
|
198
|
+
)
|
|
199
|
+
logger.info(f"Loaded embedding model: {self.config['embedding_model']}")
|
|
200
|
+
|
|
201
|
+
# Biomedical QA model for answer validation
|
|
202
|
+
self.biomedical_qa_model = pipeline(
|
|
203
|
+
"question-answering",
|
|
204
|
+
model=self.config["biomedical_qa_model"],
|
|
205
|
+
device=0 if self.config["device"] == "cuda" else -1,
|
|
206
|
+
)
|
|
207
|
+
logger.info(
|
|
208
|
+
f"Loaded biomedical QA model: {self.config['biomedical_qa_model']}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Entailment model for faithfulness assessment
|
|
212
|
+
self.entailment_model = pipeline(
|
|
213
|
+
"text-classification",
|
|
214
|
+
model=self.config["entailment_model"],
|
|
215
|
+
device=0 if self.config["device"] == "cuda" else -1,
|
|
216
|
+
)
|
|
217
|
+
logger.info(f"Loaded entailment model: {self.config['entailment_model']}")
|
|
218
|
+
|
|
219
|
+
# SpaCy for text processing
|
|
220
|
+
try:
|
|
221
|
+
self.nlp = spacy.load("en_core_sci_sm") # Biomedical spaCy model
|
|
222
|
+
except OSError:
|
|
223
|
+
self.nlp = spacy.load("en_core_web_sm")
|
|
224
|
+
logger.warning("Using standard spaCy model instead of biomedical")
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Failed to initialize models: {e}")
|
|
228
|
+
raise
|
|
229
|
+
|
|
230
|
+
def evaluate_pipeline(
|
|
231
|
+
self,
|
|
232
|
+
pipeline_results: List[Dict[str, Any]],
|
|
233
|
+
ground_truth_answers: List[str],
|
|
234
|
+
pipeline_name: str,
|
|
235
|
+
) -> ComprehensiveRAGASResults:
|
|
236
|
+
"""
|
|
237
|
+
Comprehensive RAGAS evaluation of a RAG pipeline.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
pipeline_results: List of pipeline query results with 'query', 'answer', 'contexts'
|
|
241
|
+
ground_truth_answers: List of ground truth answers
|
|
242
|
+
pipeline_name: Name of the pipeline being evaluated
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Comprehensive RAGAS evaluation results
|
|
246
|
+
"""
|
|
247
|
+
logger.info(f"Starting comprehensive RAGAS evaluation for {pipeline_name}")
|
|
248
|
+
start_time = time.time()
|
|
249
|
+
|
|
250
|
+
# Validate input data
|
|
251
|
+
self._validate_evaluation_inputs(pipeline_results, ground_truth_answers)
|
|
252
|
+
|
|
253
|
+
# Initialize results
|
|
254
|
+
results = ComprehensiveRAGASResults(
|
|
255
|
+
pipeline_name=pipeline_name,
|
|
256
|
+
timestamp=time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
257
|
+
total_queries=len(pipeline_results),
|
|
258
|
+
successful_evaluations=0,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Prepare evaluation data
|
|
262
|
+
evaluation_data = self._prepare_evaluation_data(
|
|
263
|
+
pipeline_results, ground_truth_answers
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Evaluate each metric
|
|
267
|
+
metrics_to_evaluate = [
|
|
268
|
+
("faithfulness", self._evaluate_faithfulness),
|
|
269
|
+
("answer_relevancy", self._evaluate_answer_relevancy),
|
|
270
|
+
("context_precision", self._evaluate_context_precision),
|
|
271
|
+
("context_recall", self._evaluate_context_recall),
|
|
272
|
+
("context_utilization", self._evaluate_context_utilization),
|
|
273
|
+
("answer_correctness", self._evaluate_answer_correctness),
|
|
274
|
+
("answer_similarity", self._evaluate_answer_similarity),
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
successful_metrics = 0
|
|
278
|
+
for metric_name, metric_func in metrics_to_evaluate:
|
|
279
|
+
try:
|
|
280
|
+
logger.info(f"Evaluating {metric_name}...")
|
|
281
|
+
metric_result = metric_func(evaluation_data)
|
|
282
|
+
setattr(results, metric_name, metric_result)
|
|
283
|
+
if metric_result.error_message is None:
|
|
284
|
+
successful_metrics += 1
|
|
285
|
+
logger.info(
|
|
286
|
+
f"{metric_name}: {metric_result.value:.4f} ± {metric_result.standard_error:.4f}"
|
|
287
|
+
)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Failed to evaluate {metric_name}: {e}")
|
|
290
|
+
error_result = RAGASResult(
|
|
291
|
+
metric_name=metric_name,
|
|
292
|
+
value=0.0,
|
|
293
|
+
confidence_interval=(0.0, 0.0),
|
|
294
|
+
sample_size=0,
|
|
295
|
+
standard_error=0.0,
|
|
296
|
+
computation_time=0.0,
|
|
297
|
+
error_message=str(e),
|
|
298
|
+
)
|
|
299
|
+
setattr(results, metric_name, error_result)
|
|
300
|
+
|
|
301
|
+
# Calculate aggregate scores
|
|
302
|
+
results.overall_score = self._calculate_overall_score(results)
|
|
303
|
+
results.weighted_score = self._calculate_weighted_score(results)
|
|
304
|
+
results.successful_evaluations = successful_metrics
|
|
305
|
+
|
|
306
|
+
total_time = time.time() - start_time
|
|
307
|
+
logger.info(
|
|
308
|
+
f"RAGAS evaluation completed in {total_time:.2f}s. "
|
|
309
|
+
f"Overall score: {results.overall_score:.4f}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return results
|
|
313
|
+
|
|
314
|
+
def _validate_evaluation_inputs(
|
|
315
|
+
self, pipeline_results: List[Dict[str, Any]], ground_truth_answers: List[str]
|
|
316
|
+
):
|
|
317
|
+
"""Validate inputs for RAGAS evaluation."""
|
|
318
|
+
if len(pipeline_results) != len(ground_truth_answers):
|
|
319
|
+
raise ValueError(
|
|
320
|
+
"Mismatch between pipeline results and ground truth answers"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
required_keys = ["query", "answer", "contexts"]
|
|
324
|
+
for i, result in enumerate(pipeline_results):
|
|
325
|
+
for key in required_keys:
|
|
326
|
+
if key not in result:
|
|
327
|
+
raise ValueError(f"Missing key '{key}' in pipeline result {i}")
|
|
328
|
+
if not result[key]:
|
|
329
|
+
logger.warning(f"Empty {key} in pipeline result {i}")
|
|
330
|
+
|
|
331
|
+
def _prepare_evaluation_data(
|
|
332
|
+
self, pipeline_results: List[Dict[str, Any]], ground_truth_answers: List[str]
|
|
333
|
+
) -> List[Dict[str, Any]]:
|
|
334
|
+
"""Prepare data for RAGAS evaluation."""
|
|
335
|
+
evaluation_data = []
|
|
336
|
+
|
|
337
|
+
for i, (result, ground_truth) in enumerate(
|
|
338
|
+
zip(pipeline_results, ground_truth_answers)
|
|
339
|
+
):
|
|
340
|
+
# Ensure contexts is a list of strings
|
|
341
|
+
contexts = result["contexts"]
|
|
342
|
+
if isinstance(contexts, str):
|
|
343
|
+
contexts = [contexts]
|
|
344
|
+
elif not isinstance(contexts, list):
|
|
345
|
+
contexts = [str(contexts)]
|
|
346
|
+
|
|
347
|
+
# Clean and validate data
|
|
348
|
+
evaluation_item = {
|
|
349
|
+
"question": str(result["query"]).strip(),
|
|
350
|
+
"answer": str(result["answer"]).strip() if result["answer"] else "",
|
|
351
|
+
"contexts": [str(ctx).strip() for ctx in contexts if str(ctx).strip()],
|
|
352
|
+
"ground_truth": str(ground_truth).strip(),
|
|
353
|
+
"index": i,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Skip if critical data is missing
|
|
357
|
+
if (
|
|
358
|
+
evaluation_item["question"]
|
|
359
|
+
and evaluation_item["answer"]
|
|
360
|
+
and evaluation_item["contexts"]
|
|
361
|
+
and evaluation_item["ground_truth"]
|
|
362
|
+
):
|
|
363
|
+
evaluation_data.append(evaluation_item)
|
|
364
|
+
else:
|
|
365
|
+
logger.warning(f"Skipping evaluation item {i} due to missing data")
|
|
366
|
+
|
|
367
|
+
logger.info(f"Prepared {len(evaluation_data)} valid evaluation items")
|
|
368
|
+
return evaluation_data
|
|
369
|
+
|
|
370
|
+
def _evaluate_faithfulness(
|
|
371
|
+
self, evaluation_data: List[Dict[str, Any]]
|
|
372
|
+
) -> RAGASResult:
|
|
373
|
+
"""
|
|
374
|
+
Evaluate faithfulness: How factually accurate is the answer based on the context.
|
|
375
|
+
|
|
376
|
+
Uses entailment models to check if the answer is supported by the retrieved context.
|
|
377
|
+
"""
|
|
378
|
+
start_time = time.time()
|
|
379
|
+
scores = []
|
|
380
|
+
|
|
381
|
+
for item in evaluation_data:
|
|
382
|
+
try:
|
|
383
|
+
answer = item["answer"]
|
|
384
|
+
contexts = item["contexts"]
|
|
385
|
+
|
|
386
|
+
# Combine contexts
|
|
387
|
+
combined_context = " ".join(contexts)
|
|
388
|
+
|
|
389
|
+
# Check entailment between answer and context
|
|
390
|
+
entailment_result = self.entailment_model(
|
|
391
|
+
f"Premise: {combined_context} Hypothesis: {answer}"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Extract faithfulness score
|
|
395
|
+
if entailment_result[0]["label"] == "ENTAILMENT":
|
|
396
|
+
score = entailment_result[0]["score"]
|
|
397
|
+
elif entailment_result[0]["label"] == "NEUTRAL":
|
|
398
|
+
score = 0.5
|
|
399
|
+
else: # CONTRADICTION
|
|
400
|
+
score = 1.0 - entailment_result[0]["score"]
|
|
401
|
+
|
|
402
|
+
scores.append(score)
|
|
403
|
+
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.warning(f"Failed to evaluate faithfulness for item: {e}")
|
|
406
|
+
scores.append(0.0)
|
|
407
|
+
|
|
408
|
+
return self._compute_metric_statistics(
|
|
409
|
+
"faithfulness", scores, time.time() - start_time
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def _evaluate_answer_relevancy(
|
|
413
|
+
self, evaluation_data: List[Dict[str, Any]]
|
|
414
|
+
) -> RAGASResult:
|
|
415
|
+
"""
|
|
416
|
+
Evaluate answer relevancy: How relevant is the answer to the question.
|
|
417
|
+
|
|
418
|
+
Uses semantic similarity between question and answer embeddings.
|
|
419
|
+
"""
|
|
420
|
+
start_time = time.time()
|
|
421
|
+
scores = []
|
|
422
|
+
|
|
423
|
+
# Batch encode questions and answers
|
|
424
|
+
questions = [item["question"] for item in evaluation_data]
|
|
425
|
+
answers = [item["answer"] for item in evaluation_data]
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
question_embeddings = self.embedding_model.encode(
|
|
429
|
+
questions, batch_size=self.config["batch_size"]
|
|
430
|
+
)
|
|
431
|
+
answer_embeddings = self.embedding_model.encode(
|
|
432
|
+
answers, batch_size=self.config["batch_size"]
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Calculate cosine similarities
|
|
436
|
+
similarities = util.cos_sim(question_embeddings, answer_embeddings)
|
|
437
|
+
scores = [similarities[i][i].item() for i in range(len(questions))]
|
|
438
|
+
|
|
439
|
+
except Exception as e:
|
|
440
|
+
logger.error(
|
|
441
|
+
f"Batch processing failed, falling back to individual processing: {e}"
|
|
442
|
+
)
|
|
443
|
+
for item in evaluation_data:
|
|
444
|
+
try:
|
|
445
|
+
question_emb = self.embedding_model.encode([item["question"]])
|
|
446
|
+
answer_emb = self.embedding_model.encode([item["answer"]])
|
|
447
|
+
similarity = cosine_similarity(question_emb, answer_emb)[0][0]
|
|
448
|
+
scores.append(similarity)
|
|
449
|
+
except Exception:
|
|
450
|
+
scores.append(0.0)
|
|
451
|
+
|
|
452
|
+
return self._compute_metric_statistics(
|
|
453
|
+
"answer_relevancy", scores, time.time() - start_time
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def _evaluate_context_precision(
|
|
457
|
+
self, evaluation_data: List[Dict[str, Any]]
|
|
458
|
+
) -> RAGASResult:
|
|
459
|
+
"""
|
|
460
|
+
Evaluate context precision: What fraction of retrieved context is relevant.
|
|
461
|
+
|
|
462
|
+
Measures how much of the retrieved context is actually useful for answering the question.
|
|
463
|
+
"""
|
|
464
|
+
start_time = time.time()
|
|
465
|
+
scores = []
|
|
466
|
+
|
|
467
|
+
for item in evaluation_data:
|
|
468
|
+
try:
|
|
469
|
+
question = item["question"]
|
|
470
|
+
contexts = item["contexts"]
|
|
471
|
+
|
|
472
|
+
relevant_contexts = 0
|
|
473
|
+
total_contexts = len(contexts)
|
|
474
|
+
|
|
475
|
+
if total_contexts == 0:
|
|
476
|
+
scores.append(0.0)
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
for context in contexts:
|
|
480
|
+
# Use QA model to check if context can answer the question
|
|
481
|
+
try:
|
|
482
|
+
qa_result = self.biomedical_qa_model(
|
|
483
|
+
question=question, context=context
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Consider context relevant if QA confidence is high
|
|
487
|
+
if qa_result["score"] > self.config["similarity_threshold"]:
|
|
488
|
+
relevant_contexts += 1
|
|
489
|
+
|
|
490
|
+
except Exception:
|
|
491
|
+
# Fallback to embedding similarity
|
|
492
|
+
question_emb = self.embedding_model.encode([question])
|
|
493
|
+
context_emb = self.embedding_model.encode([context])
|
|
494
|
+
similarity = cosine_similarity(question_emb, context_emb)[0][0]
|
|
495
|
+
|
|
496
|
+
if similarity > self.config["similarity_threshold"]:
|
|
497
|
+
relevant_contexts += 1
|
|
498
|
+
|
|
499
|
+
precision = relevant_contexts / total_contexts
|
|
500
|
+
scores.append(precision)
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
logger.warning(f"Failed to evaluate context precision for item: {e}")
|
|
504
|
+
scores.append(0.0)
|
|
505
|
+
|
|
506
|
+
return self._compute_metric_statistics(
|
|
507
|
+
"context_precision", scores, time.time() - start_time
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def _evaluate_context_recall(
|
|
511
|
+
self, evaluation_data: List[Dict[str, Any]]
|
|
512
|
+
) -> RAGASResult:
|
|
513
|
+
"""
|
|
514
|
+
Evaluate context recall: How much of the relevant information was retrieved.
|
|
515
|
+
|
|
516
|
+
Measures if the retrieved context contains information present in ground truth.
|
|
517
|
+
"""
|
|
518
|
+
start_time = time.time()
|
|
519
|
+
scores = []
|
|
520
|
+
|
|
521
|
+
for item in evaluation_data:
|
|
522
|
+
try:
|
|
523
|
+
ground_truth = item["ground_truth"]
|
|
524
|
+
contexts = item["contexts"]
|
|
525
|
+
|
|
526
|
+
if not contexts:
|
|
527
|
+
scores.append(0.0)
|
|
528
|
+
continue
|
|
529
|
+
|
|
530
|
+
# Combine all contexts
|
|
531
|
+
combined_context = " ".join(contexts)
|
|
532
|
+
|
|
533
|
+
# Use embedding similarity between ground truth and contexts
|
|
534
|
+
gt_embedding = self.embedding_model.encode([ground_truth])
|
|
535
|
+
context_embedding = self.embedding_model.encode([combined_context])
|
|
536
|
+
|
|
537
|
+
similarity = cosine_similarity(gt_embedding, context_embedding)[0][0]
|
|
538
|
+
scores.append(similarity)
|
|
539
|
+
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.warning(f"Failed to evaluate context recall for item: {e}")
|
|
542
|
+
scores.append(0.0)
|
|
543
|
+
|
|
544
|
+
return self._compute_metric_statistics(
|
|
545
|
+
"context_recall", scores, time.time() - start_time
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def _evaluate_context_utilization(
|
|
549
|
+
self, evaluation_data: List[Dict[str, Any]]
|
|
550
|
+
) -> RAGASResult:
|
|
551
|
+
"""
|
|
552
|
+
Evaluate context utilization: How effectively was the retrieved context used.
|
|
553
|
+
|
|
554
|
+
Measures how much of the provided context is reflected in the generated answer.
|
|
555
|
+
"""
|
|
556
|
+
start_time = time.time()
|
|
557
|
+
scores = []
|
|
558
|
+
|
|
559
|
+
for item in evaluation_data:
|
|
560
|
+
try:
|
|
561
|
+
answer = item["answer"]
|
|
562
|
+
contexts = item["contexts"]
|
|
563
|
+
|
|
564
|
+
if not contexts:
|
|
565
|
+
scores.append(0.0)
|
|
566
|
+
continue
|
|
567
|
+
|
|
568
|
+
# Calculate semantic overlap between answer and contexts
|
|
569
|
+
combined_context = " ".join(contexts)
|
|
570
|
+
|
|
571
|
+
answer_embedding = self.embedding_model.encode([answer])
|
|
572
|
+
context_embedding = self.embedding_model.encode([combined_context])
|
|
573
|
+
|
|
574
|
+
utilization = cosine_similarity(answer_embedding, context_embedding)[0][
|
|
575
|
+
0
|
|
576
|
+
]
|
|
577
|
+
scores.append(utilization)
|
|
578
|
+
|
|
579
|
+
except Exception as e:
|
|
580
|
+
logger.warning(f"Failed to evaluate context utilization for item: {e}")
|
|
581
|
+
scores.append(0.0)
|
|
582
|
+
|
|
583
|
+
return self._compute_metric_statistics(
|
|
584
|
+
"context_utilization", scores, time.time() - start_time
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
def _evaluate_answer_correctness(
|
|
588
|
+
self, evaluation_data: List[Dict[str, Any]]
|
|
589
|
+
) -> RAGASResult:
|
|
590
|
+
"""
|
|
591
|
+
Evaluate answer correctness: How factually correct is the answer.
|
|
592
|
+
|
|
593
|
+
Combines semantic similarity with factual accuracy assessment.
|
|
594
|
+
"""
|
|
595
|
+
start_time = time.time()
|
|
596
|
+
scores = []
|
|
597
|
+
|
|
598
|
+
# Batch processing for efficiency
|
|
599
|
+
answers = [item["answer"] for item in evaluation_data]
|
|
600
|
+
ground_truths = [item["ground_truth"] for item in evaluation_data]
|
|
601
|
+
|
|
602
|
+
try:
|
|
603
|
+
# Semantic similarity component
|
|
604
|
+
answer_embeddings = self.embedding_model.encode(
|
|
605
|
+
answers, batch_size=self.config["batch_size"]
|
|
606
|
+
)
|
|
607
|
+
gt_embeddings = self.embedding_model.encode(
|
|
608
|
+
ground_truths, batch_size=self.config["batch_size"]
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
similarities = util.cos_sim(answer_embeddings, gt_embeddings)
|
|
612
|
+
semantic_scores = [similarities[i][i].item() for i in range(len(answers))]
|
|
613
|
+
|
|
614
|
+
# Factual accuracy component using entailment
|
|
615
|
+
for i, (answer, gt) in enumerate(zip(answers, ground_truths)):
|
|
616
|
+
try:
|
|
617
|
+
# Check if answer entails ground truth and vice versa
|
|
618
|
+
entailment1 = self.entailment_model(
|
|
619
|
+
f"Premise: {answer} Hypothesis: {gt}"
|
|
620
|
+
)
|
|
621
|
+
entailment2 = self.entailment_model(
|
|
622
|
+
f"Premise: {gt} Hypothesis: {answer}"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Calculate factual score
|
|
626
|
+
factual_score = 0.0
|
|
627
|
+
if entailment1[0]["label"] == "ENTAILMENT":
|
|
628
|
+
factual_score += entailment1[0]["score"] * 0.5
|
|
629
|
+
if entailment2[0]["label"] == "ENTAILMENT":
|
|
630
|
+
factual_score += entailment2[0]["score"] * 0.5
|
|
631
|
+
|
|
632
|
+
# Combine semantic and factual scores
|
|
633
|
+
combined_score = 0.6 * semantic_scores[i] + 0.4 * factual_score
|
|
634
|
+
scores.append(combined_score)
|
|
635
|
+
|
|
636
|
+
except Exception:
|
|
637
|
+
# Fallback to semantic similarity only
|
|
638
|
+
scores.append(semantic_scores[i])
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.error(f"Batch processing failed: {e}")
|
|
642
|
+
# Fallback to individual processing
|
|
643
|
+
for item in evaluation_data:
|
|
644
|
+
try:
|
|
645
|
+
answer_emb = self.embedding_model.encode([item["answer"]])
|
|
646
|
+
gt_emb = self.embedding_model.encode([item["ground_truth"]])
|
|
647
|
+
similarity = cosine_similarity(answer_emb, gt_emb)[0][0]
|
|
648
|
+
scores.append(similarity)
|
|
649
|
+
except Exception:
|
|
650
|
+
scores.append(0.0)
|
|
651
|
+
|
|
652
|
+
return self._compute_metric_statistics(
|
|
653
|
+
"answer_correctness", scores, time.time() - start_time
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
def _evaluate_answer_similarity(
|
|
657
|
+
self, evaluation_data: List[Dict[str, Any]]
|
|
658
|
+
) -> RAGASResult:
|
|
659
|
+
"""
|
|
660
|
+
Evaluate answer similarity: Semantic similarity between answer and ground truth.
|
|
661
|
+
|
|
662
|
+
Pure semantic similarity measure using embeddings.
|
|
663
|
+
"""
|
|
664
|
+
start_time = time.time()
|
|
665
|
+
scores = []
|
|
666
|
+
|
|
667
|
+
# Batch encode for efficiency
|
|
668
|
+
answers = [item["answer"] for item in evaluation_data]
|
|
669
|
+
ground_truths = [item["ground_truth"] for item in evaluation_data]
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
answer_embeddings = self.embedding_model.encode(
|
|
673
|
+
answers, batch_size=self.config["batch_size"]
|
|
674
|
+
)
|
|
675
|
+
gt_embeddings = self.embedding_model.encode(
|
|
676
|
+
ground_truths, batch_size=self.config["batch_size"]
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
similarities = util.cos_sim(answer_embeddings, gt_embeddings)
|
|
680
|
+
scores = [similarities[i][i].item() for i in range(len(answers))]
|
|
681
|
+
|
|
682
|
+
except Exception as e:
|
|
683
|
+
logger.error(f"Batch processing failed: {e}")
|
|
684
|
+
for item in evaluation_data:
|
|
685
|
+
try:
|
|
686
|
+
answer_emb = self.embedding_model.encode([item["answer"]])
|
|
687
|
+
gt_emb = self.embedding_model.encode([item["ground_truth"]])
|
|
688
|
+
similarity = cosine_similarity(answer_emb, gt_emb)[0][0]
|
|
689
|
+
scores.append(similarity)
|
|
690
|
+
except Exception:
|
|
691
|
+
scores.append(0.0)
|
|
692
|
+
|
|
693
|
+
return self._compute_metric_statistics(
|
|
694
|
+
"answer_similarity", scores, time.time() - start_time
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
def _compute_metric_statistics(
|
|
698
|
+
self, metric_name: str, scores: List[float], computation_time: float
|
|
699
|
+
) -> RAGASResult:
|
|
700
|
+
"""Compute comprehensive statistics for a metric."""
|
|
701
|
+
if not scores:
|
|
702
|
+
return RAGASResult(
|
|
703
|
+
metric_name=metric_name,
|
|
704
|
+
value=0.0,
|
|
705
|
+
confidence_interval=(0.0, 0.0),
|
|
706
|
+
sample_size=0,
|
|
707
|
+
standard_error=0.0,
|
|
708
|
+
computation_time=computation_time,
|
|
709
|
+
error_message="No valid scores computed",
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Remove any NaN or infinite values
|
|
713
|
+
valid_scores = [s for s in scores if np.isfinite(s)]
|
|
714
|
+
|
|
715
|
+
if not valid_scores:
|
|
716
|
+
return RAGASResult(
|
|
717
|
+
metric_name=metric_name,
|
|
718
|
+
value=0.0,
|
|
719
|
+
confidence_interval=(0.0, 0.0),
|
|
720
|
+
sample_size=0,
|
|
721
|
+
standard_error=0.0,
|
|
722
|
+
computation_time=computation_time,
|
|
723
|
+
error_message="No valid finite scores",
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
# Basic statistics
|
|
727
|
+
mean_score = np.mean(valid_scores)
|
|
728
|
+
std_score = np.std(valid_scores, ddof=1) if len(valid_scores) > 1 else 0.0
|
|
729
|
+
standard_error = (
|
|
730
|
+
std_score / np.sqrt(len(valid_scores)) if len(valid_scores) > 0 else 0.0
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# Confidence interval using bootstrap
|
|
734
|
+
confidence_interval = self._compute_confidence_interval(valid_scores)
|
|
735
|
+
|
|
736
|
+
return RAGASResult(
|
|
737
|
+
metric_name=metric_name,
|
|
738
|
+
value=mean_score,
|
|
739
|
+
confidence_interval=confidence_interval,
|
|
740
|
+
sample_size=len(valid_scores),
|
|
741
|
+
standard_error=standard_error,
|
|
742
|
+
computation_time=computation_time,
|
|
743
|
+
individual_scores=valid_scores,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
def _compute_confidence_interval(self, scores: List[float]) -> Tuple[float, float]:
|
|
747
|
+
"""Compute confidence interval using bootstrap resampling."""
|
|
748
|
+
if len(scores) < 2:
|
|
749
|
+
return (scores[0], scores[0]) if scores else (0.0, 0.0)
|
|
750
|
+
|
|
751
|
+
try:
|
|
752
|
+
# Bootstrap resampling
|
|
753
|
+
data = np.array(scores)
|
|
754
|
+
|
|
755
|
+
def bootstrap_mean(data):
|
|
756
|
+
return np.mean(np.random.choice(data, size=len(data), replace=True))
|
|
757
|
+
|
|
758
|
+
bootstrap_means = [
|
|
759
|
+
bootstrap_mean(data) for _ in range(self.config["bootstrap_samples"])
|
|
760
|
+
]
|
|
761
|
+
|
|
762
|
+
# Calculate confidence interval
|
|
763
|
+
alpha = 1 - self.config["confidence_level"]
|
|
764
|
+
lower_percentile = (alpha / 2) * 100
|
|
765
|
+
upper_percentile = (1 - alpha / 2) * 100
|
|
766
|
+
|
|
767
|
+
ci_lower = np.percentile(bootstrap_means, lower_percentile)
|
|
768
|
+
ci_upper = np.percentile(bootstrap_means, upper_percentile)
|
|
769
|
+
|
|
770
|
+
return (ci_lower, ci_upper)
|
|
771
|
+
|
|
772
|
+
except Exception as e:
|
|
773
|
+
logger.warning(f"Bootstrap confidence interval failed: {e}")
|
|
774
|
+
# Fallback to normal approximation
|
|
775
|
+
mean_val = np.mean(scores)
|
|
776
|
+
se = np.std(scores) / np.sqrt(len(scores))
|
|
777
|
+
z_score = stats.norm.ppf(1 - (1 - self.config["confidence_level"]) / 2)
|
|
778
|
+
margin = z_score * se
|
|
779
|
+
return (mean_val - margin, mean_val + margin)
|
|
780
|
+
|
|
781
|
+
def _calculate_overall_score(self, results: ComprehensiveRAGASResults) -> float:
|
|
782
|
+
"""Calculate overall score as simple average of available metrics."""
|
|
783
|
+
metric_values = []
|
|
784
|
+
|
|
785
|
+
for metric_name in [
|
|
786
|
+
"faithfulness",
|
|
787
|
+
"answer_relevancy",
|
|
788
|
+
"context_precision",
|
|
789
|
+
"context_recall",
|
|
790
|
+
"context_utilization",
|
|
791
|
+
"answer_correctness",
|
|
792
|
+
"answer_similarity",
|
|
793
|
+
]:
|
|
794
|
+
metric_result = getattr(results, metric_name)
|
|
795
|
+
if metric_result and metric_result.error_message is None:
|
|
796
|
+
metric_values.append(metric_result.value)
|
|
797
|
+
|
|
798
|
+
return np.mean(metric_values) if metric_values else 0.0
|
|
799
|
+
|
|
800
|
+
def _calculate_weighted_score(self, results: ComprehensiveRAGASResults) -> float:
|
|
801
|
+
"""Calculate weighted score using biomedical domain-specific weights."""
|
|
802
|
+
weighted_sum = 0.0
|
|
803
|
+
total_weight = 0.0
|
|
804
|
+
|
|
805
|
+
for metric_name, weight in self.metric_weights.items():
|
|
806
|
+
metric_result = getattr(results, metric_name)
|
|
807
|
+
if metric_result and metric_result.error_message is None:
|
|
808
|
+
weighted_sum += metric_result.value * weight
|
|
809
|
+
total_weight += weight
|
|
810
|
+
|
|
811
|
+
return weighted_sum / total_weight if total_weight > 0 else 0.0
|
|
812
|
+
|
|
813
|
+
def compare_pipelines(
|
|
814
|
+
self, pipeline_results: Dict[str, ComprehensiveRAGASResults]
|
|
815
|
+
) -> Dict[str, Any]:
|
|
816
|
+
"""
|
|
817
|
+
Statistical comparison of multiple pipeline results.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
pipeline_results: Dictionary mapping pipeline names to their RAGAS results
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
Comprehensive comparison analysis with statistical significance tests
|
|
824
|
+
"""
|
|
825
|
+
logger.info(f"Comparing {len(pipeline_results)} pipelines...")
|
|
826
|
+
|
|
827
|
+
comparison = {
|
|
828
|
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
829
|
+
"pipelines_compared": list(pipeline_results.keys()),
|
|
830
|
+
"metric_comparisons": {},
|
|
831
|
+
"overall_ranking": [],
|
|
832
|
+
"statistical_significance": {},
|
|
833
|
+
"best_pipeline_per_metric": {},
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
# Compare each metric across pipelines
|
|
837
|
+
for metric_name in [
|
|
838
|
+
"faithfulness",
|
|
839
|
+
"answer_relevancy",
|
|
840
|
+
"context_precision",
|
|
841
|
+
"context_recall",
|
|
842
|
+
"context_utilization",
|
|
843
|
+
"answer_correctness",
|
|
844
|
+
"answer_similarity",
|
|
845
|
+
]:
|
|
846
|
+
|
|
847
|
+
metric_data = {}
|
|
848
|
+
individual_scores = {}
|
|
849
|
+
|
|
850
|
+
for pipeline_name, results in pipeline_results.items():
|
|
851
|
+
metric_result = getattr(results, metric_name)
|
|
852
|
+
if metric_result and metric_result.error_message is None:
|
|
853
|
+
metric_data[pipeline_name] = metric_result.value
|
|
854
|
+
individual_scores[pipeline_name] = metric_result.individual_scores
|
|
855
|
+
|
|
856
|
+
if len(metric_data) > 1:
|
|
857
|
+
# Statistical comparison
|
|
858
|
+
comparison["metric_comparisons"][metric_name] = metric_data
|
|
859
|
+
|
|
860
|
+
# Find best pipeline for this metric
|
|
861
|
+
best_pipeline = max(metric_data, key=metric_data.get)
|
|
862
|
+
comparison["best_pipeline_per_metric"][metric_name] = {
|
|
863
|
+
"pipeline": best_pipeline,
|
|
864
|
+
"score": metric_data[best_pipeline],
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
# Statistical significance testing
|
|
868
|
+
if len(individual_scores) >= 2:
|
|
869
|
+
significance_results = self._test_statistical_significance(
|
|
870
|
+
individual_scores
|
|
871
|
+
)
|
|
872
|
+
comparison["statistical_significance"][
|
|
873
|
+
metric_name
|
|
874
|
+
] = significance_results
|
|
875
|
+
|
|
876
|
+
# Overall ranking
|
|
877
|
+
overall_scores = {}
|
|
878
|
+
for pipeline_name, results in pipeline_results.items():
|
|
879
|
+
if results.weighted_score is not None:
|
|
880
|
+
overall_scores[pipeline_name] = results.weighted_score
|
|
881
|
+
|
|
882
|
+
comparison["overall_ranking"] = sorted(
|
|
883
|
+
overall_scores.items(), key=lambda x: x[1], reverse=True
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
return comparison
|
|
887
|
+
|
|
888
|
+
def _test_statistical_significance(
|
|
889
|
+
self, score_groups: Dict[str, List[float]]
|
|
890
|
+
) -> Dict[str, Any]:
|
|
891
|
+
"""Test statistical significance between pipeline performance."""
|
|
892
|
+
pipeline_names = list(score_groups.keys())
|
|
893
|
+
significance_results = {
|
|
894
|
+
"test_type": "mannwhitneyu", # Non-parametric test
|
|
895
|
+
"pairwise_comparisons": {},
|
|
896
|
+
"overall_p_value": None,
|
|
897
|
+
"significant_differences": [],
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
# Pairwise comparisons
|
|
901
|
+
for i in range(len(pipeline_names)):
|
|
902
|
+
for j in range(i + 1, len(pipeline_names)):
|
|
903
|
+
pipeline1, pipeline2 = pipeline_names[i], pipeline_names[j]
|
|
904
|
+
scores1, scores2 = score_groups[pipeline1], score_groups[pipeline2]
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
# Mann-Whitney U test (non-parametric)
|
|
908
|
+
statistic, p_value = stats.mannwhitneyu(
|
|
909
|
+
scores1, scores2, alternative="two-sided"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
comparison_key = f"{pipeline1}_vs_{pipeline2}"
|
|
913
|
+
significance_results["pairwise_comparisons"][comparison_key] = {
|
|
914
|
+
"statistic": statistic,
|
|
915
|
+
"p_value": p_value,
|
|
916
|
+
"significant": p_value < 0.05,
|
|
917
|
+
"effect_size": self._calculate_effect_size(scores1, scores2),
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if p_value < 0.05:
|
|
921
|
+
better_pipeline = (
|
|
922
|
+
pipeline1
|
|
923
|
+
if np.mean(scores1) > np.mean(scores2)
|
|
924
|
+
else pipeline2
|
|
925
|
+
)
|
|
926
|
+
significance_results["significant_differences"].append(
|
|
927
|
+
{
|
|
928
|
+
"comparison": comparison_key,
|
|
929
|
+
"p_value": p_value,
|
|
930
|
+
"better_pipeline": better_pipeline,
|
|
931
|
+
}
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
except Exception as e:
|
|
935
|
+
logger.warning(
|
|
936
|
+
f"Statistical test failed for {pipeline1} vs {pipeline2}: {e}"
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
return significance_results
|
|
940
|
+
|
|
941
|
+
def _calculate_effect_size(
|
|
942
|
+
self, scores1: List[float], scores2: List[float]
|
|
943
|
+
) -> float:
|
|
944
|
+
"""Calculate Cohen's d effect size."""
|
|
945
|
+
try:
|
|
946
|
+
mean1, mean2 = np.mean(scores1), np.mean(scores2)
|
|
947
|
+
pooled_std = np.sqrt(
|
|
948
|
+
(
|
|
949
|
+
(len(scores1) - 1) * np.var(scores1, ddof=1)
|
|
950
|
+
+ (len(scores2) - 1) * np.var(scores2, ddof=1)
|
|
951
|
+
)
|
|
952
|
+
/ (len(scores1) + len(scores2) - 2)
|
|
953
|
+
)
|
|
954
|
+
return (mean1 - mean2) / pooled_std if pooled_std > 0 else 0.0
|
|
955
|
+
except Exception:
|
|
956
|
+
return 0.0
|
|
957
|
+
|
|
958
|
+
def save_results(
|
|
959
|
+
self,
|
|
960
|
+
results: Union[ComprehensiveRAGASResults, Dict[str, Any]],
|
|
961
|
+
output_path: str,
|
|
962
|
+
):
|
|
963
|
+
"""Save RAGAS evaluation results to file."""
|
|
964
|
+
output_dir = Path(output_path)
|
|
965
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
966
|
+
|
|
967
|
+
if isinstance(results, ComprehensiveRAGASResults):
|
|
968
|
+
# Single pipeline results
|
|
969
|
+
filename = f"ragas_results_{results.pipeline_name}_{results.timestamp.replace(':', '-').replace(' ', '_')}.json"
|
|
970
|
+
data = results.to_dict()
|
|
971
|
+
else:
|
|
972
|
+
# Comparison results
|
|
973
|
+
filename = f"ragas_comparison_{results['timestamp'].replace(':', '-').replace(' ', '_')}.json"
|
|
974
|
+
data = results
|
|
975
|
+
|
|
976
|
+
filepath = output_dir / filename
|
|
977
|
+
with open(filepath, "w") as f:
|
|
978
|
+
json.dump(data, f, indent=2, default=str)
|
|
979
|
+
|
|
980
|
+
logger.info(f"RAGAS results saved to {filepath}")
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def create_biomedical_ragas_framework(
|
|
984
|
+
config: Optional[Dict[str, Any]] = None,
|
|
985
|
+
) -> BiomedicalRAGASFramework:
|
|
986
|
+
"""Factory function to create a configured biomedical RAGAS framework."""
|
|
987
|
+
return BiomedicalRAGASFramework(config)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
if __name__ == "__main__":
|
|
991
|
+
# Example usage
|
|
992
|
+
framework = create_biomedical_ragas_framework()
|
|
993
|
+
|
|
994
|
+
# Mock evaluation data
|
|
995
|
+
sample_pipeline_results = [
|
|
996
|
+
{
|
|
997
|
+
"query": "What are the symptoms of diabetes?",
|
|
998
|
+
"answer": "Common symptoms include increased thirst, frequent urination, and fatigue.",
|
|
999
|
+
"contexts": [
|
|
1000
|
+
"Diabetes mellitus symptoms include polydipsia, polyuria, and general weakness."
|
|
1001
|
+
],
|
|
1002
|
+
}
|
|
1003
|
+
]
|
|
1004
|
+
|
|
1005
|
+
sample_ground_truth = [
|
|
1006
|
+
"Diabetes symptoms include excessive thirst, frequent urination, fatigue, and blurred vision."
|
|
1007
|
+
]
|
|
1008
|
+
|
|
1009
|
+
results = framework.evaluate_pipeline(
|
|
1010
|
+
sample_pipeline_results, sample_ground_truth, "sample_pipeline"
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
framework.save_results(results, "output/ragas_results")
|