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,1173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive Comparative Analysis System for Biomedical RAG Pipeline Evaluation
|
|
3
|
+
|
|
4
|
+
This module integrates biomedical question generation, RAGAS metrics evaluation,
|
|
5
|
+
statistical analysis, and visualization to provide comprehensive pipeline comparison
|
|
6
|
+
with empirical evidence and actionable insights.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
1. End-to-end pipeline comparison workflow
|
|
10
|
+
2. Integrated question generation and evaluation
|
|
11
|
+
3. Multi-dimensional performance analysis
|
|
12
|
+
4. Statistical significance testing
|
|
13
|
+
5. Interactive visualizations and reporting
|
|
14
|
+
6. Scalable architecture for 10K+ documents
|
|
15
|
+
7. Empirical evidence generation
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import pickle
|
|
21
|
+
import time
|
|
22
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
27
|
+
|
|
28
|
+
import matplotlib.pyplot as plt
|
|
29
|
+
import numpy as np
|
|
30
|
+
import pandas as pd
|
|
31
|
+
import plotly.express as px
|
|
32
|
+
import plotly.graph_objects as go
|
|
33
|
+
import seaborn as sns
|
|
34
|
+
|
|
35
|
+
# Import our custom frameworks
|
|
36
|
+
from biomedical_question_generator import (
|
|
37
|
+
BiomedicalQuestion,
|
|
38
|
+
BiomedicalQuestionGenerator,
|
|
39
|
+
QuestionGenerationConfig,
|
|
40
|
+
create_biomedical_question_generator,
|
|
41
|
+
)
|
|
42
|
+
from plotly.subplots import make_subplots
|
|
43
|
+
from ragas_metrics_framework import (
|
|
44
|
+
BiomedicalRAGASFramework,
|
|
45
|
+
ComprehensiveRAGASResults,
|
|
46
|
+
create_biomedical_ragas_framework,
|
|
47
|
+
)
|
|
48
|
+
from statistical_evaluation_methodology import (
|
|
49
|
+
PowerAnalysisResult,
|
|
50
|
+
StatisticalEvaluationFramework,
|
|
51
|
+
StatisticalTestResult,
|
|
52
|
+
create_statistical_framework,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Pipeline imports (existing infrastructure)
|
|
56
|
+
from iris_rag.pipelines.basic import BasicRAGPipeline
|
|
57
|
+
from iris_rag.pipelines.basic_rerank import BasicRAGRerankingPipeline
|
|
58
|
+
from iris_rag.pipelines.colbert_pylate.pylate_pipeline import PyLateColBERTPipeline
|
|
59
|
+
from iris_rag.pipelines.crag import CRAGPipeline
|
|
60
|
+
from iris_rag.pipelines.graphrag import GraphRAGPipeline
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class PipelineEvaluationConfig:
|
|
67
|
+
"""Configuration for pipeline evaluation experiment."""
|
|
68
|
+
|
|
69
|
+
# Data configuration
|
|
70
|
+
total_questions: int = 1000
|
|
71
|
+
questions_per_pipeline_test: int = 100
|
|
72
|
+
sample_documents: int = 10000
|
|
73
|
+
|
|
74
|
+
# Question generation
|
|
75
|
+
question_complexity_distribution: Dict[str, float] = field(
|
|
76
|
+
default_factory=lambda: {"basic": 0.4, "intermediate": 0.4, "advanced": 0.2}
|
|
77
|
+
)
|
|
78
|
+
question_types_distribution: Dict[str, float] = field(
|
|
79
|
+
default_factory=lambda: {
|
|
80
|
+
"factual": 0.3,
|
|
81
|
+
"analytical": 0.25,
|
|
82
|
+
"procedural": 0.2,
|
|
83
|
+
"comparative": 0.15,
|
|
84
|
+
"causal": 0.1,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Evaluation configuration
|
|
89
|
+
ragas_metrics: List[str] = field(
|
|
90
|
+
default_factory=lambda: [
|
|
91
|
+
"faithfulness",
|
|
92
|
+
"answer_relevancy",
|
|
93
|
+
"context_precision",
|
|
94
|
+
"context_recall",
|
|
95
|
+
"context_utilization",
|
|
96
|
+
"answer_correctness",
|
|
97
|
+
"answer_similarity",
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Statistical configuration
|
|
102
|
+
statistical_alpha: float = 0.05
|
|
103
|
+
statistical_power_target: float = 0.8
|
|
104
|
+
multiple_correction_method: str = "fdr_bh"
|
|
105
|
+
|
|
106
|
+
# Computational configuration
|
|
107
|
+
max_workers: int = 4
|
|
108
|
+
batch_size: int = 32
|
|
109
|
+
enable_caching: bool = True
|
|
110
|
+
cache_dir: str = "cache/evaluation"
|
|
111
|
+
|
|
112
|
+
# Output configuration
|
|
113
|
+
output_dir: str = "outputs/comparative_analysis"
|
|
114
|
+
generate_plots: bool = True
|
|
115
|
+
generate_interactive_dashboard: bool = True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class PipelinePerformanceResult:
|
|
120
|
+
"""Complete performance evaluation for a single pipeline."""
|
|
121
|
+
|
|
122
|
+
pipeline_name: str
|
|
123
|
+
ragas_results: ComprehensiveRAGASResults
|
|
124
|
+
execution_times: List[float]
|
|
125
|
+
success_rate: float
|
|
126
|
+
error_messages: List[str]
|
|
127
|
+
sample_responses: List[Dict[str, Any]]
|
|
128
|
+
|
|
129
|
+
def get_metric_value(self, metric_name: str) -> Optional[float]:
|
|
130
|
+
"""Get specific metric value."""
|
|
131
|
+
metric_result = getattr(self.ragas_results, metric_name, None)
|
|
132
|
+
return metric_result.value if metric_result else None
|
|
133
|
+
|
|
134
|
+
def get_metric_confidence_interval(
|
|
135
|
+
self, metric_name: str
|
|
136
|
+
) -> Optional[Tuple[float, float]]:
|
|
137
|
+
"""Get metric confidence interval."""
|
|
138
|
+
metric_result = getattr(self.ragas_results, metric_name, None)
|
|
139
|
+
return metric_result.confidence_interval if metric_result else None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class ComparativeAnalysisResults:
|
|
144
|
+
"""Complete comparative analysis results across all pipelines."""
|
|
145
|
+
|
|
146
|
+
experiment_id: str
|
|
147
|
+
timestamp: str
|
|
148
|
+
config: PipelineEvaluationConfig
|
|
149
|
+
|
|
150
|
+
# Pipeline results
|
|
151
|
+
pipeline_results: Dict[str, PipelinePerformanceResult]
|
|
152
|
+
|
|
153
|
+
# Questions and ground truth
|
|
154
|
+
evaluation_questions: List[BiomedicalQuestion]
|
|
155
|
+
|
|
156
|
+
# Statistical analysis
|
|
157
|
+
statistical_comparisons: Dict[str, List[StatisticalTestResult]]
|
|
158
|
+
power_analysis: Dict[str, PowerAnalysisResult]
|
|
159
|
+
|
|
160
|
+
# Summary metrics
|
|
161
|
+
overall_rankings: Dict[
|
|
162
|
+
str, List[Tuple[str, float]]
|
|
163
|
+
] # metric -> [(pipeline, score), ...]
|
|
164
|
+
best_pipeline_per_metric: Dict[str, str]
|
|
165
|
+
statistical_significance_summary: Dict[str, Dict[str, Any]]
|
|
166
|
+
|
|
167
|
+
# Performance metadata
|
|
168
|
+
total_evaluation_time: float
|
|
169
|
+
total_questions_evaluated: int
|
|
170
|
+
successful_evaluations_per_pipeline: Dict[str, int]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ComparativeAnalysisSystem:
|
|
174
|
+
"""
|
|
175
|
+
Comprehensive system for comparing biomedical RAG pipelines with statistical rigor.
|
|
176
|
+
|
|
177
|
+
Orchestrates question generation, pipeline evaluation, RAGAS metrics calculation,
|
|
178
|
+
statistical analysis, and visualization generation for complete pipeline comparison.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(self, config: Optional[PipelineEvaluationConfig] = None):
|
|
182
|
+
"""
|
|
183
|
+
Initialize the comparative analysis system.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
config: Configuration for evaluation experiment
|
|
187
|
+
"""
|
|
188
|
+
self.config = config or PipelineEvaluationConfig()
|
|
189
|
+
|
|
190
|
+
# Initialize sub-frameworks
|
|
191
|
+
self.question_generator = None
|
|
192
|
+
self.ragas_framework = None
|
|
193
|
+
self.statistical_framework = None
|
|
194
|
+
|
|
195
|
+
# Pipeline registry
|
|
196
|
+
self.available_pipelines = {
|
|
197
|
+
"BasicRAG": BasicRAGPipeline,
|
|
198
|
+
"CRAG": CRAGPipeline,
|
|
199
|
+
"GraphRAG": GraphRAGPipeline,
|
|
200
|
+
"BasicRAGReranking": BasicRAGRerankingPipeline,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
self.initialized_pipelines = {}
|
|
204
|
+
|
|
205
|
+
# Setup output directories
|
|
206
|
+
self.output_dir = Path(self.config.output_dir)
|
|
207
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
|
|
209
|
+
# Setup cache
|
|
210
|
+
if self.config.enable_caching:
|
|
211
|
+
self.cache_dir = Path(self.config.cache_dir)
|
|
212
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
|
|
214
|
+
self._initialize_frameworks()
|
|
215
|
+
|
|
216
|
+
logger.info("Comparative Analysis System initialized successfully")
|
|
217
|
+
|
|
218
|
+
def _initialize_frameworks(self):
|
|
219
|
+
"""Initialize all sub-frameworks."""
|
|
220
|
+
logger.info("Initializing evaluation frameworks...")
|
|
221
|
+
|
|
222
|
+
# Question generator
|
|
223
|
+
question_config = QuestionGenerationConfig(
|
|
224
|
+
total_questions=self.config.total_questions,
|
|
225
|
+
question_types_distribution=self.config.question_types_distribution,
|
|
226
|
+
complexity_distribution=self.config.question_complexity_distribution,
|
|
227
|
+
)
|
|
228
|
+
self.question_generator = BiomedicalQuestionGenerator(question_config)
|
|
229
|
+
|
|
230
|
+
# RAGAS framework
|
|
231
|
+
ragas_config = {
|
|
232
|
+
"batch_size": self.config.batch_size,
|
|
233
|
+
"device": "cuda" if torch.cuda.is_available() else "cpu",
|
|
234
|
+
}
|
|
235
|
+
self.ragas_framework = create_biomedical_ragas_framework(ragas_config)
|
|
236
|
+
|
|
237
|
+
# Statistical framework
|
|
238
|
+
statistical_config = {
|
|
239
|
+
"alpha": self.config.statistical_alpha,
|
|
240
|
+
"power_target": self.config.statistical_power_target,
|
|
241
|
+
"multiple_correction_method": self.config.multiple_correction_method,
|
|
242
|
+
}
|
|
243
|
+
self.statistical_framework = create_statistical_framework(statistical_config)
|
|
244
|
+
|
|
245
|
+
logger.info("All frameworks initialized successfully")
|
|
246
|
+
|
|
247
|
+
def run_comprehensive_comparison(
|
|
248
|
+
self,
|
|
249
|
+
documents: List[Dict[str, Any]],
|
|
250
|
+
pipeline_names: Optional[List[str]] = None,
|
|
251
|
+
) -> ComparativeAnalysisResults:
|
|
252
|
+
"""
|
|
253
|
+
Run comprehensive comparison of RAG pipelines.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
documents: List of PMC documents for evaluation
|
|
257
|
+
pipeline_names: List of pipeline names to evaluate (default: all available)
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Complete comparative analysis results
|
|
261
|
+
"""
|
|
262
|
+
experiment_id = (
|
|
263
|
+
f"biomedical_rag_comparison_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
264
|
+
)
|
|
265
|
+
logger.info(f"Starting comprehensive comparison: {experiment_id}")
|
|
266
|
+
|
|
267
|
+
start_time = time.time()
|
|
268
|
+
|
|
269
|
+
# Default to all available pipelines
|
|
270
|
+
if pipeline_names is None:
|
|
271
|
+
pipeline_names = list(self.available_pipelines.keys())
|
|
272
|
+
|
|
273
|
+
# Initialize pipelines
|
|
274
|
+
self._initialize_pipelines(pipeline_names)
|
|
275
|
+
|
|
276
|
+
# Step 1: Generate evaluation questions
|
|
277
|
+
logger.info("Generating biomedical evaluation questions...")
|
|
278
|
+
evaluation_questions = self._generate_evaluation_questions(documents)
|
|
279
|
+
|
|
280
|
+
# Step 2: Evaluate all pipelines
|
|
281
|
+
logger.info("Evaluating all pipelines...")
|
|
282
|
+
pipeline_results = self._evaluate_all_pipelines(evaluation_questions)
|
|
283
|
+
|
|
284
|
+
# Step 3: Statistical analysis
|
|
285
|
+
logger.info("Conducting statistical analysis...")
|
|
286
|
+
statistical_results = self._conduct_statistical_analysis(pipeline_results)
|
|
287
|
+
|
|
288
|
+
# Step 4: Generate summary metrics
|
|
289
|
+
logger.info("Generating summary metrics...")
|
|
290
|
+
summary_metrics = self._generate_summary_metrics(
|
|
291
|
+
pipeline_results, statistical_results
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Create comprehensive results
|
|
295
|
+
total_time = time.time() - start_time
|
|
296
|
+
|
|
297
|
+
results = ComparativeAnalysisResults(
|
|
298
|
+
experiment_id=experiment_id,
|
|
299
|
+
timestamp=datetime.now().isoformat(),
|
|
300
|
+
config=self.config,
|
|
301
|
+
pipeline_results=pipeline_results,
|
|
302
|
+
evaluation_questions=evaluation_questions,
|
|
303
|
+
statistical_comparisons=statistical_results["comparisons"],
|
|
304
|
+
power_analysis=statistical_results["power_analysis"],
|
|
305
|
+
overall_rankings=summary_metrics["rankings"],
|
|
306
|
+
best_pipeline_per_metric=summary_metrics["best_per_metric"],
|
|
307
|
+
statistical_significance_summary=statistical_results[
|
|
308
|
+
"significance_summary"
|
|
309
|
+
],
|
|
310
|
+
total_evaluation_time=total_time,
|
|
311
|
+
total_questions_evaluated=len(evaluation_questions),
|
|
312
|
+
successful_evaluations_per_pipeline=summary_metrics["success_rates"],
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Step 5: Save results and generate reports
|
|
316
|
+
self._save_results(results)
|
|
317
|
+
|
|
318
|
+
if self.config.generate_plots:
|
|
319
|
+
self._generate_visualizations(results)
|
|
320
|
+
|
|
321
|
+
if self.config.generate_interactive_dashboard:
|
|
322
|
+
self._generate_interactive_dashboard(results)
|
|
323
|
+
|
|
324
|
+
logger.info(f"Comprehensive comparison completed in {total_time:.2f}s")
|
|
325
|
+
return results
|
|
326
|
+
|
|
327
|
+
def _initialize_pipelines(self, pipeline_names: List[str]):
|
|
328
|
+
"""Initialize specified RAG pipelines."""
|
|
329
|
+
logger.info(f"Initializing {len(pipeline_names)} pipelines...")
|
|
330
|
+
|
|
331
|
+
for pipeline_name in pipeline_names:
|
|
332
|
+
if pipeline_name not in self.available_pipelines:
|
|
333
|
+
logger.warning(f"Pipeline {pipeline_name} not available")
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
# Initialize with appropriate configuration
|
|
338
|
+
pipeline_class = self.available_pipelines[pipeline_name]
|
|
339
|
+
|
|
340
|
+
# Basic configuration for all pipelines
|
|
341
|
+
pipeline_config = {
|
|
342
|
+
"connection_manager": None, # Will use defaults
|
|
343
|
+
"config_manager": None, # Will use defaults
|
|
344
|
+
"llm_func": self._get_mock_llm_function(), # Mock for evaluation
|
|
345
|
+
"vector_store": None, # Will use defaults
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
pipeline = pipeline_class(**pipeline_config)
|
|
349
|
+
self.initialized_pipelines[pipeline_name] = pipeline
|
|
350
|
+
|
|
351
|
+
logger.info(f"Successfully initialized {pipeline_name}")
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.error(f"Failed to initialize {pipeline_name}: {e}")
|
|
355
|
+
|
|
356
|
+
def _get_mock_llm_function(self):
|
|
357
|
+
"""Get mock LLM function for evaluation."""
|
|
358
|
+
|
|
359
|
+
def mock_llm(prompt: str) -> str:
|
|
360
|
+
# Simple mock that generates relevant medical responses
|
|
361
|
+
if "diabetes" in prompt.lower():
|
|
362
|
+
return "Diabetes is a chronic condition affecting blood sugar regulation. Common symptoms include increased thirst, frequent urination, and fatigue. Treatment typically involves lifestyle modifications and medication management."
|
|
363
|
+
elif "heart" in prompt.lower() or "cardiac" in prompt.lower():
|
|
364
|
+
return "Cardiovascular conditions affect the heart and blood vessels. Risk factors include hypertension, high cholesterol, and diabetes. Prevention strategies include regular exercise and healthy diet."
|
|
365
|
+
else:
|
|
366
|
+
return "This is a medical condition that requires proper diagnosis and treatment by healthcare professionals. Symptoms and treatment options vary based on individual patient factors."
|
|
367
|
+
|
|
368
|
+
return mock_llm
|
|
369
|
+
|
|
370
|
+
def _generate_evaluation_questions(
|
|
371
|
+
self, documents: List[Dict[str, Any]]
|
|
372
|
+
) -> List[BiomedicalQuestion]:
|
|
373
|
+
"""Generate biomedical questions for evaluation."""
|
|
374
|
+
|
|
375
|
+
# Check cache first
|
|
376
|
+
cache_file = (
|
|
377
|
+
self.cache_dir / "evaluation_questions.pkl"
|
|
378
|
+
if self.config.enable_caching
|
|
379
|
+
else None
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if cache_file and cache_file.exists():
|
|
383
|
+
logger.info("Loading cached evaluation questions")
|
|
384
|
+
with open(cache_file, "rb") as f:
|
|
385
|
+
return pickle.load(f)
|
|
386
|
+
|
|
387
|
+
# Filter to biomedical documents (sample for efficiency)
|
|
388
|
+
sampled_docs = documents[: self.config.sample_documents]
|
|
389
|
+
|
|
390
|
+
# Generate questions
|
|
391
|
+
questions = self.question_generator.generate_questions_from_documents(
|
|
392
|
+
sampled_docs
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Cache results
|
|
396
|
+
if cache_file:
|
|
397
|
+
with open(cache_file, "wb") as f:
|
|
398
|
+
pickle.dump(questions, f)
|
|
399
|
+
logger.info(f"Cached {len(questions)} evaluation questions")
|
|
400
|
+
|
|
401
|
+
return questions
|
|
402
|
+
|
|
403
|
+
def _evaluate_all_pipelines(
|
|
404
|
+
self, evaluation_questions: List[BiomedicalQuestion]
|
|
405
|
+
) -> Dict[str, PipelinePerformanceResult]:
|
|
406
|
+
"""Evaluate all pipelines on the question set."""
|
|
407
|
+
|
|
408
|
+
pipeline_results = {}
|
|
409
|
+
|
|
410
|
+
# Use subset of questions for pipeline testing
|
|
411
|
+
test_questions = evaluation_questions[: self.config.questions_per_pipeline_test]
|
|
412
|
+
|
|
413
|
+
for pipeline_name, pipeline in self.initialized_pipelines.items():
|
|
414
|
+
logger.info(f"Evaluating {pipeline_name}...")
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
result = self._evaluate_single_pipeline(
|
|
418
|
+
pipeline, pipeline_name, test_questions
|
|
419
|
+
)
|
|
420
|
+
pipeline_results[pipeline_name] = result
|
|
421
|
+
|
|
422
|
+
logger.info(
|
|
423
|
+
f"Completed evaluation of {pipeline_name}: "
|
|
424
|
+
f"Success rate: {result.success_rate:.2%}"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
logger.error(f"Failed to evaluate {pipeline_name}: {e}")
|
|
429
|
+
# Create failed result
|
|
430
|
+
pipeline_results[pipeline_name] = PipelinePerformanceResult(
|
|
431
|
+
pipeline_name=pipeline_name,
|
|
432
|
+
ragas_results=None,
|
|
433
|
+
execution_times=[],
|
|
434
|
+
success_rate=0.0,
|
|
435
|
+
error_messages=[str(e)],
|
|
436
|
+
sample_responses=[],
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return pipeline_results
|
|
440
|
+
|
|
441
|
+
def _evaluate_single_pipeline(
|
|
442
|
+
self, pipeline: Any, pipeline_name: str, questions: List[BiomedicalQuestion]
|
|
443
|
+
) -> PipelinePerformanceResult:
|
|
444
|
+
"""Evaluate a single pipeline on the question set."""
|
|
445
|
+
|
|
446
|
+
pipeline_responses = []
|
|
447
|
+
execution_times = []
|
|
448
|
+
error_messages = []
|
|
449
|
+
ground_truth_answers = []
|
|
450
|
+
|
|
451
|
+
# Execute pipeline on each question
|
|
452
|
+
for question in questions:
|
|
453
|
+
try:
|
|
454
|
+
start_time = time.time()
|
|
455
|
+
|
|
456
|
+
# Query pipeline
|
|
457
|
+
response = pipeline.query(
|
|
458
|
+
question.question, top_k=5, generate_answer=True
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
execution_time = time.time() - start_time
|
|
462
|
+
execution_times.append(execution_time)
|
|
463
|
+
|
|
464
|
+
# Store response
|
|
465
|
+
pipeline_responses.append(response)
|
|
466
|
+
ground_truth_answers.append(question.ground_truth_answer)
|
|
467
|
+
|
|
468
|
+
except Exception as e:
|
|
469
|
+
error_messages.append(
|
|
470
|
+
f"Question '{question.question[:50]}...': {str(e)}"
|
|
471
|
+
)
|
|
472
|
+
logger.warning(f"Pipeline {pipeline_name} failed on question: {e}")
|
|
473
|
+
|
|
474
|
+
# Calculate success rate
|
|
475
|
+
success_rate = len(pipeline_responses) / len(questions)
|
|
476
|
+
|
|
477
|
+
# Conduct RAGAS evaluation
|
|
478
|
+
if pipeline_responses:
|
|
479
|
+
ragas_results = self.ragas_framework.evaluate_pipeline(
|
|
480
|
+
pipeline_responses, ground_truth_answers, pipeline_name
|
|
481
|
+
)
|
|
482
|
+
else:
|
|
483
|
+
ragas_results = None
|
|
484
|
+
|
|
485
|
+
# Sample responses for analysis
|
|
486
|
+
sample_responses = pipeline_responses[: min(10, len(pipeline_responses))]
|
|
487
|
+
|
|
488
|
+
return PipelinePerformanceResult(
|
|
489
|
+
pipeline_name=pipeline_name,
|
|
490
|
+
ragas_results=ragas_results,
|
|
491
|
+
execution_times=execution_times,
|
|
492
|
+
success_rate=success_rate,
|
|
493
|
+
error_messages=error_messages,
|
|
494
|
+
sample_responses=sample_responses,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def _conduct_statistical_analysis(
|
|
498
|
+
self, pipeline_results: Dict[str, PipelinePerformanceResult]
|
|
499
|
+
) -> Dict[str, Any]:
|
|
500
|
+
"""Conduct comprehensive statistical analysis."""
|
|
501
|
+
|
|
502
|
+
statistical_results = {
|
|
503
|
+
"comparisons": {},
|
|
504
|
+
"power_analysis": {},
|
|
505
|
+
"significance_summary": {},
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
# Extract metric data for each pipeline
|
|
509
|
+
for metric_name in self.config.ragas_metrics:
|
|
510
|
+
metric_data = {}
|
|
511
|
+
|
|
512
|
+
for pipeline_name, result in pipeline_results.items():
|
|
513
|
+
if result.ragas_results:
|
|
514
|
+
metric_result = getattr(result.ragas_results, metric_name, None)
|
|
515
|
+
if metric_result and metric_result.individual_scores:
|
|
516
|
+
metric_data[pipeline_name] = metric_result.individual_scores
|
|
517
|
+
|
|
518
|
+
if len(metric_data) >= 2:
|
|
519
|
+
# Power analysis
|
|
520
|
+
try:
|
|
521
|
+
power_result = self.statistical_framework.conduct_power_analysis(
|
|
522
|
+
metric_data, metric_name
|
|
523
|
+
)
|
|
524
|
+
statistical_results["power_analysis"][metric_name] = power_result
|
|
525
|
+
except Exception as e:
|
|
526
|
+
logger.warning(f"Power analysis failed for {metric_name}: {e}")
|
|
527
|
+
|
|
528
|
+
# Statistical comparisons
|
|
529
|
+
try:
|
|
530
|
+
comparison_results = (
|
|
531
|
+
self.statistical_framework.compare_pipelines_statistical(
|
|
532
|
+
metric_data, metric_name, paired=False
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
statistical_results["comparisons"][metric_name] = comparison_results
|
|
536
|
+
|
|
537
|
+
# Summarize significance
|
|
538
|
+
significant_tests = [
|
|
539
|
+
r
|
|
540
|
+
for r in comparison_results
|
|
541
|
+
if (r.p_value_adjusted or r.p_value)
|
|
542
|
+
< self.config.statistical_alpha
|
|
543
|
+
]
|
|
544
|
+
|
|
545
|
+
statistical_results["significance_summary"][metric_name] = {
|
|
546
|
+
"total_tests": len(comparison_results),
|
|
547
|
+
"significant_tests": len(significant_tests),
|
|
548
|
+
"largest_effect_size": max(
|
|
549
|
+
[abs(r.effect_size) for r in comparison_results],
|
|
550
|
+
default=0.0,
|
|
551
|
+
),
|
|
552
|
+
"significant_comparisons": [
|
|
553
|
+
{
|
|
554
|
+
"comparison": f"{r.raw_data.get('group1_name', 'unknown')} vs {r.raw_data.get('group2_name', 'unknown')}",
|
|
555
|
+
"p_value": r.p_value_adjusted or r.p_value,
|
|
556
|
+
"effect_size": r.effect_size,
|
|
557
|
+
}
|
|
558
|
+
for r in significant_tests
|
|
559
|
+
],
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
except Exception as e:
|
|
563
|
+
logger.warning(
|
|
564
|
+
f"Statistical comparison failed for {metric_name}: {e}"
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
return statistical_results
|
|
568
|
+
|
|
569
|
+
def _generate_summary_metrics(
|
|
570
|
+
self,
|
|
571
|
+
pipeline_results: Dict[str, PipelinePerformanceResult],
|
|
572
|
+
statistical_results: Dict[str, Any],
|
|
573
|
+
) -> Dict[str, Any]:
|
|
574
|
+
"""Generate summary metrics and rankings."""
|
|
575
|
+
|
|
576
|
+
summary = {"rankings": {}, "best_per_metric": {}, "success_rates": {}}
|
|
577
|
+
|
|
578
|
+
# Calculate success rates
|
|
579
|
+
for pipeline_name, result in pipeline_results.items():
|
|
580
|
+
summary["success_rates"][pipeline_name] = (
|
|
581
|
+
result.ragas_results.successful_evaluations
|
|
582
|
+
if result.ragas_results
|
|
583
|
+
else 0
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Generate rankings for each metric
|
|
587
|
+
for metric_name in self.config.ragas_metrics:
|
|
588
|
+
metric_scores = []
|
|
589
|
+
|
|
590
|
+
for pipeline_name, result in pipeline_results.items():
|
|
591
|
+
if result.ragas_results:
|
|
592
|
+
metric_value = result.get_metric_value(metric_name)
|
|
593
|
+
if metric_value is not None:
|
|
594
|
+
metric_scores.append((pipeline_name, metric_value))
|
|
595
|
+
|
|
596
|
+
# Sort by score (descending)
|
|
597
|
+
metric_scores.sort(key=lambda x: x[1], reverse=True)
|
|
598
|
+
summary["rankings"][metric_name] = metric_scores
|
|
599
|
+
|
|
600
|
+
# Best pipeline for this metric
|
|
601
|
+
if metric_scores:
|
|
602
|
+
summary["best_per_metric"][metric_name] = metric_scores[0][0]
|
|
603
|
+
|
|
604
|
+
return summary
|
|
605
|
+
|
|
606
|
+
def _save_results(self, results: ComparativeAnalysisResults):
|
|
607
|
+
"""Save comprehensive results to files."""
|
|
608
|
+
|
|
609
|
+
# Create experiment directory
|
|
610
|
+
experiment_dir = self.output_dir / results.experiment_id
|
|
611
|
+
experiment_dir.mkdir(exist_ok=True)
|
|
612
|
+
|
|
613
|
+
# Save main results
|
|
614
|
+
results_file = experiment_dir / "comprehensive_results.json"
|
|
615
|
+
|
|
616
|
+
# Convert to serializable format
|
|
617
|
+
serializable_results = {
|
|
618
|
+
"experiment_id": results.experiment_id,
|
|
619
|
+
"timestamp": results.timestamp,
|
|
620
|
+
"config": {
|
|
621
|
+
"total_questions": results.config.total_questions,
|
|
622
|
+
"questions_per_pipeline_test": results.config.questions_per_pipeline_test,
|
|
623
|
+
"sample_documents": results.config.sample_documents,
|
|
624
|
+
"statistical_alpha": results.config.statistical_alpha,
|
|
625
|
+
"ragas_metrics": results.config.ragas_metrics,
|
|
626
|
+
},
|
|
627
|
+
"pipeline_results": {
|
|
628
|
+
name: {
|
|
629
|
+
"pipeline_name": result.pipeline_name,
|
|
630
|
+
"success_rate": result.success_rate,
|
|
631
|
+
"avg_execution_time": (
|
|
632
|
+
np.mean(result.execution_times)
|
|
633
|
+
if result.execution_times
|
|
634
|
+
else 0.0
|
|
635
|
+
),
|
|
636
|
+
"ragas_scores": (
|
|
637
|
+
result.ragas_results.to_dict() if result.ragas_results else {}
|
|
638
|
+
),
|
|
639
|
+
"error_count": len(result.error_messages),
|
|
640
|
+
}
|
|
641
|
+
for name, result in results.pipeline_results.items()
|
|
642
|
+
},
|
|
643
|
+
"overall_rankings": results.overall_rankings,
|
|
644
|
+
"best_pipeline_per_metric": results.best_pipeline_per_metric,
|
|
645
|
+
"statistical_significance_summary": results.statistical_significance_summary,
|
|
646
|
+
"total_evaluation_time": results.total_evaluation_time,
|
|
647
|
+
"total_questions_evaluated": results.total_questions_evaluated,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
with open(results_file, "w") as f:
|
|
651
|
+
json.dump(serializable_results, f, indent=2, default=str)
|
|
652
|
+
|
|
653
|
+
# Save detailed statistical results
|
|
654
|
+
stats_file = experiment_dir / "statistical_analysis.json"
|
|
655
|
+
with open(stats_file, "w") as f:
|
|
656
|
+
json.dump(results.statistical_comparisons, f, indent=2, default=str)
|
|
657
|
+
|
|
658
|
+
# Save evaluation questions
|
|
659
|
+
questions_file = experiment_dir / "evaluation_questions.json"
|
|
660
|
+
questions_data = [
|
|
661
|
+
{
|
|
662
|
+
"question_id": q.question_id,
|
|
663
|
+
"question": q.question,
|
|
664
|
+
"ground_truth_answer": q.ground_truth_answer,
|
|
665
|
+
"question_type": q.question_type,
|
|
666
|
+
"complexity_level": q.complexity_level,
|
|
667
|
+
"medical_domain": q.medical_domain,
|
|
668
|
+
"confidence_score": q.confidence_score,
|
|
669
|
+
}
|
|
670
|
+
for q in results.evaluation_questions
|
|
671
|
+
]
|
|
672
|
+
with open(questions_file, "w") as f:
|
|
673
|
+
json.dump(questions_data, f, indent=2)
|
|
674
|
+
|
|
675
|
+
logger.info(f"Results saved to {experiment_dir}")
|
|
676
|
+
|
|
677
|
+
def _generate_visualizations(self, results: ComparativeAnalysisResults):
|
|
678
|
+
"""Generate comprehensive visualizations."""
|
|
679
|
+
|
|
680
|
+
experiment_dir = self.output_dir / results.experiment_id
|
|
681
|
+
plots_dir = experiment_dir / "plots"
|
|
682
|
+
plots_dir.mkdir(exist_ok=True)
|
|
683
|
+
|
|
684
|
+
# Set style
|
|
685
|
+
plt.style.use("seaborn-v0_8")
|
|
686
|
+
sns.set_palette("husl")
|
|
687
|
+
|
|
688
|
+
# 1. RAGAS Metrics Comparison (Radar Chart)
|
|
689
|
+
self._create_radar_chart(results, plots_dir)
|
|
690
|
+
|
|
691
|
+
# 2. Statistical Significance Heatmap
|
|
692
|
+
self._create_significance_heatmap(results, plots_dir)
|
|
693
|
+
|
|
694
|
+
# 3. Performance Distribution Plots
|
|
695
|
+
self._create_performance_distributions(results, plots_dir)
|
|
696
|
+
|
|
697
|
+
# 4. Execution Time Analysis
|
|
698
|
+
self._create_execution_time_analysis(results, plots_dir)
|
|
699
|
+
|
|
700
|
+
# 5. Overall Performance Summary
|
|
701
|
+
self._create_performance_summary(results, plots_dir)
|
|
702
|
+
|
|
703
|
+
logger.info(f"Visualizations saved to {plots_dir}")
|
|
704
|
+
|
|
705
|
+
def _create_radar_chart(self, results: ComparativeAnalysisResults, plots_dir: Path):
|
|
706
|
+
"""Create radar chart comparing all metrics across pipelines."""
|
|
707
|
+
|
|
708
|
+
# Prepare data
|
|
709
|
+
pipelines = list(results.pipeline_results.keys())
|
|
710
|
+
metrics = self.config.ragas_metrics
|
|
711
|
+
|
|
712
|
+
# Create subplot
|
|
713
|
+
fig = make_subplots(rows=1, cols=1, specs=[[{"type": "scatterpolar"}]])
|
|
714
|
+
|
|
715
|
+
colors = px.colors.qualitative.Set1
|
|
716
|
+
|
|
717
|
+
for i, pipeline_name in enumerate(pipelines):
|
|
718
|
+
result = results.pipeline_results[pipeline_name]
|
|
719
|
+
if not result.ragas_results:
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
values = []
|
|
723
|
+
for metric in metrics:
|
|
724
|
+
metric_value = result.get_metric_value(metric)
|
|
725
|
+
values.append(metric_value if metric_value is not None else 0.0)
|
|
726
|
+
|
|
727
|
+
# Add pipeline trace
|
|
728
|
+
fig.add_trace(
|
|
729
|
+
go.Scatterpolar(
|
|
730
|
+
r=values,
|
|
731
|
+
theta=metrics,
|
|
732
|
+
fill="toself",
|
|
733
|
+
name=pipeline_name,
|
|
734
|
+
line_color=colors[i % len(colors)],
|
|
735
|
+
)
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
fig.update_layout(
|
|
739
|
+
polar=dict(radialaxis=dict(visible=True, range=[0, 1])),
|
|
740
|
+
showlegend=True,
|
|
741
|
+
title="RAGAS Metrics Comparison Across Pipelines",
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
fig.write_html(plots_dir / "ragas_radar_chart.html")
|
|
745
|
+
fig.write_image(plots_dir / "ragas_radar_chart.png")
|
|
746
|
+
|
|
747
|
+
def _create_significance_heatmap(
|
|
748
|
+
self, results: ComparativeAnalysisResults, plots_dir: Path
|
|
749
|
+
):
|
|
750
|
+
"""Create heatmap showing statistical significance between pipelines."""
|
|
751
|
+
|
|
752
|
+
pipelines = list(results.pipeline_results.keys())
|
|
753
|
+
metrics = self.config.ragas_metrics
|
|
754
|
+
|
|
755
|
+
# Create significance matrix for each metric
|
|
756
|
+
fig, axes = plt.subplots(2, 4, figsize=(20, 10))
|
|
757
|
+
axes = axes.flatten()
|
|
758
|
+
|
|
759
|
+
for idx, metric in enumerate(metrics):
|
|
760
|
+
if idx >= len(axes):
|
|
761
|
+
break
|
|
762
|
+
|
|
763
|
+
# Create pairwise significance matrix
|
|
764
|
+
n_pipelines = len(pipelines)
|
|
765
|
+
sig_matrix = np.ones((n_pipelines, n_pipelines))
|
|
766
|
+
|
|
767
|
+
if metric in results.statistical_comparisons:
|
|
768
|
+
comparisons = results.statistical_comparisons[metric]
|
|
769
|
+
|
|
770
|
+
for comparison in comparisons:
|
|
771
|
+
if hasattr(comparison, "raw_data"):
|
|
772
|
+
group1 = comparison.raw_data.get("group1_name")
|
|
773
|
+
group2 = comparison.raw_data.get("group2_name")
|
|
774
|
+
|
|
775
|
+
if group1 in pipelines and group2 in pipelines:
|
|
776
|
+
i = pipelines.index(group1)
|
|
777
|
+
j = pipelines.index(group2)
|
|
778
|
+
|
|
779
|
+
p_val = comparison.p_value_adjusted or comparison.p_value
|
|
780
|
+
sig_matrix[i, j] = p_val
|
|
781
|
+
sig_matrix[j, i] = p_val
|
|
782
|
+
|
|
783
|
+
# Plot heatmap
|
|
784
|
+
sns.heatmap(
|
|
785
|
+
sig_matrix,
|
|
786
|
+
annot=True,
|
|
787
|
+
fmt=".3f",
|
|
788
|
+
xticklabels=pipelines,
|
|
789
|
+
yticklabels=pipelines,
|
|
790
|
+
cmap="RdYlBu_r",
|
|
791
|
+
vmin=0,
|
|
792
|
+
vmax=0.1,
|
|
793
|
+
ax=axes[idx],
|
|
794
|
+
)
|
|
795
|
+
axes[idx].set_title(f"{metric} - p-values")
|
|
796
|
+
|
|
797
|
+
# Remove extra subplots
|
|
798
|
+
for idx in range(len(metrics), len(axes)):
|
|
799
|
+
fig.delaxes(axes[idx])
|
|
800
|
+
|
|
801
|
+
plt.tight_layout()
|
|
802
|
+
plt.savefig(
|
|
803
|
+
plots_dir / "statistical_significance_heatmap.png",
|
|
804
|
+
dpi=300,
|
|
805
|
+
bbox_inches="tight",
|
|
806
|
+
)
|
|
807
|
+
plt.close()
|
|
808
|
+
|
|
809
|
+
def _create_performance_distributions(
|
|
810
|
+
self, results: ComparativeAnalysisResults, plots_dir: Path
|
|
811
|
+
):
|
|
812
|
+
"""Create distribution plots for each metric."""
|
|
813
|
+
|
|
814
|
+
metrics = self.config.ragas_metrics
|
|
815
|
+
pipelines = list(results.pipeline_results.keys())
|
|
816
|
+
|
|
817
|
+
fig, axes = plt.subplots(3, 3, figsize=(18, 15))
|
|
818
|
+
axes = axes.flatten()
|
|
819
|
+
|
|
820
|
+
for idx, metric in enumerate(metrics):
|
|
821
|
+
if idx >= len(axes):
|
|
822
|
+
break
|
|
823
|
+
|
|
824
|
+
# Collect data for violin plot
|
|
825
|
+
plot_data = []
|
|
826
|
+
|
|
827
|
+
for pipeline_name in pipelines:
|
|
828
|
+
result = results.pipeline_results[pipeline_name]
|
|
829
|
+
if result.ragas_results:
|
|
830
|
+
metric_result = getattr(result.ragas_results, metric, None)
|
|
831
|
+
if metric_result and metric_result.individual_scores:
|
|
832
|
+
for score in metric_result.individual_scores:
|
|
833
|
+
plot_data.append(
|
|
834
|
+
{
|
|
835
|
+
"Pipeline": pipeline_name,
|
|
836
|
+
"Score": score,
|
|
837
|
+
"Metric": metric,
|
|
838
|
+
}
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
if plot_data:
|
|
842
|
+
df = pd.DataFrame(plot_data)
|
|
843
|
+
sns.violinplot(data=df, x="Pipeline", y="Score", ax=axes[idx])
|
|
844
|
+
axes[idx].set_title(f"{metric} Distribution")
|
|
845
|
+
axes[idx].tick_params(axis="x", rotation=45)
|
|
846
|
+
|
|
847
|
+
# Remove extra subplots
|
|
848
|
+
for idx in range(len(metrics), len(axes)):
|
|
849
|
+
fig.delaxes(axes[idx])
|
|
850
|
+
|
|
851
|
+
plt.tight_layout()
|
|
852
|
+
plt.savefig(
|
|
853
|
+
plots_dir / "performance_distributions.png", dpi=300, bbox_inches="tight"
|
|
854
|
+
)
|
|
855
|
+
plt.close()
|
|
856
|
+
|
|
857
|
+
def _create_execution_time_analysis(
|
|
858
|
+
self, results: ComparativeAnalysisResults, plots_dir: Path
|
|
859
|
+
):
|
|
860
|
+
"""Create execution time analysis plots."""
|
|
861
|
+
|
|
862
|
+
pipelines = list(results.pipeline_results.keys())
|
|
863
|
+
|
|
864
|
+
# Prepare data
|
|
865
|
+
time_data = []
|
|
866
|
+
for pipeline_name in pipelines:
|
|
867
|
+
result = results.pipeline_results[pipeline_name]
|
|
868
|
+
for time_val in result.execution_times:
|
|
869
|
+
time_data.append(
|
|
870
|
+
{"Pipeline": pipeline_name, "Execution_Time": time_val}
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
if time_data:
|
|
874
|
+
df = pd.DataFrame(time_data)
|
|
875
|
+
|
|
876
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
|
|
877
|
+
|
|
878
|
+
# Box plot
|
|
879
|
+
sns.boxplot(data=df, x="Pipeline", y="Execution_Time", ax=ax1)
|
|
880
|
+
ax1.set_title("Execution Time Distribution by Pipeline")
|
|
881
|
+
ax1.tick_params(axis="x", rotation=45)
|
|
882
|
+
|
|
883
|
+
# Bar plot of mean times
|
|
884
|
+
mean_times = df.groupby("Pipeline")["Execution_Time"].mean()
|
|
885
|
+
mean_times.plot(kind="bar", ax=ax2)
|
|
886
|
+
ax2.set_title("Average Execution Time by Pipeline")
|
|
887
|
+
ax2.tick_params(axis="x", rotation=45)
|
|
888
|
+
|
|
889
|
+
plt.tight_layout()
|
|
890
|
+
plt.savefig(
|
|
891
|
+
plots_dir / "execution_time_analysis.png", dpi=300, bbox_inches="tight"
|
|
892
|
+
)
|
|
893
|
+
plt.close()
|
|
894
|
+
|
|
895
|
+
def _create_performance_summary(
|
|
896
|
+
self, results: ComparativeAnalysisResults, plots_dir: Path
|
|
897
|
+
):
|
|
898
|
+
"""Create overall performance summary visualization."""
|
|
899
|
+
|
|
900
|
+
pipelines = list(results.pipeline_results.keys())
|
|
901
|
+
metrics = self.config.ragas_metrics
|
|
902
|
+
|
|
903
|
+
# Create summary matrix
|
|
904
|
+
summary_data = []
|
|
905
|
+
|
|
906
|
+
for pipeline_name in pipelines:
|
|
907
|
+
result = results.pipeline_results[pipeline_name]
|
|
908
|
+
row_data = {"Pipeline": pipeline_name}
|
|
909
|
+
|
|
910
|
+
for metric in metrics:
|
|
911
|
+
metric_value = (
|
|
912
|
+
result.get_metric_value(metric) if result.ragas_results else 0.0
|
|
913
|
+
)
|
|
914
|
+
row_data[metric] = metric_value if metric_value is not None else 0.0
|
|
915
|
+
|
|
916
|
+
# Add overall score
|
|
917
|
+
row_data["Overall"] = (
|
|
918
|
+
result.ragas_results.overall_score if result.ragas_results else 0.0
|
|
919
|
+
)
|
|
920
|
+
row_data["Success_Rate"] = result.success_rate
|
|
921
|
+
|
|
922
|
+
summary_data.append(row_data)
|
|
923
|
+
|
|
924
|
+
df = pd.DataFrame(summary_data)
|
|
925
|
+
|
|
926
|
+
# Create clustered bar chart
|
|
927
|
+
x = np.arange(len(pipelines))
|
|
928
|
+
width = 0.1
|
|
929
|
+
|
|
930
|
+
fig, ax = plt.subplots(figsize=(16, 8))
|
|
931
|
+
|
|
932
|
+
for i, metric in enumerate(metrics + ["Overall"]):
|
|
933
|
+
values = df[metric].values
|
|
934
|
+
ax.bar(x + i * width, values, width, label=metric)
|
|
935
|
+
|
|
936
|
+
ax.set_xlabel("Pipelines")
|
|
937
|
+
ax.set_ylabel("Scores")
|
|
938
|
+
ax.set_title("Comprehensive Performance Summary")
|
|
939
|
+
ax.set_xticks(x + width * len(metrics) / 2)
|
|
940
|
+
ax.set_xticklabels(pipelines)
|
|
941
|
+
ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
|
|
942
|
+
ax.grid(True, alpha=0.3)
|
|
943
|
+
|
|
944
|
+
plt.tight_layout()
|
|
945
|
+
plt.savefig(plots_dir / "performance_summary.png", dpi=300, bbox_inches="tight")
|
|
946
|
+
plt.close()
|
|
947
|
+
|
|
948
|
+
def _generate_interactive_dashboard(self, results: ComparativeAnalysisResults):
|
|
949
|
+
"""Generate interactive dashboard using Plotly."""
|
|
950
|
+
|
|
951
|
+
experiment_dir = self.output_dir / results.experiment_id
|
|
952
|
+
|
|
953
|
+
# Create comprehensive dashboard HTML
|
|
954
|
+
dashboard_html = self._create_dashboard_html(results)
|
|
955
|
+
|
|
956
|
+
dashboard_file = experiment_dir / "interactive_dashboard.html"
|
|
957
|
+
with open(dashboard_file, "w") as f:
|
|
958
|
+
f.write(dashboard_html)
|
|
959
|
+
|
|
960
|
+
logger.info(f"Interactive dashboard saved to {dashboard_file}")
|
|
961
|
+
|
|
962
|
+
def _create_dashboard_html(self, results: ComparativeAnalysisResults) -> str:
|
|
963
|
+
"""Create HTML dashboard with all visualizations."""
|
|
964
|
+
|
|
965
|
+
html_template = """
|
|
966
|
+
<!DOCTYPE html>
|
|
967
|
+
<html>
|
|
968
|
+
<head>
|
|
969
|
+
<title>Biomedical RAG Pipeline Comparison Dashboard</title>
|
|
970
|
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
971
|
+
<style>
|
|
972
|
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
973
|
+
.metric-card {
|
|
974
|
+
border: 1px solid #ddd;
|
|
975
|
+
border-radius: 8px;
|
|
976
|
+
padding: 15px;
|
|
977
|
+
margin: 10px 0;
|
|
978
|
+
background-color: #f9f9f9;
|
|
979
|
+
}
|
|
980
|
+
.highlight {
|
|
981
|
+
background-color: #e7f3ff;
|
|
982
|
+
padding: 10px;
|
|
983
|
+
border-radius: 5px;
|
|
984
|
+
margin: 10px 0;
|
|
985
|
+
}
|
|
986
|
+
table {
|
|
987
|
+
border-collapse: collapse;
|
|
988
|
+
width: 100%;
|
|
989
|
+
margin: 20px 0;
|
|
990
|
+
}
|
|
991
|
+
th, td {
|
|
992
|
+
border: 1px solid #ddd;
|
|
993
|
+
padding: 8px;
|
|
994
|
+
text-align: center;
|
|
995
|
+
}
|
|
996
|
+
th { background-color: #f2f2f2; }
|
|
997
|
+
</style>
|
|
998
|
+
</head>
|
|
999
|
+
<body>
|
|
1000
|
+
<h1>Biomedical RAG Pipeline Comparison Dashboard</h1>
|
|
1001
|
+
|
|
1002
|
+
<div class="highlight">
|
|
1003
|
+
<h2>Executive Summary</h2>
|
|
1004
|
+
<p><strong>Experiment ID:</strong> {experiment_id}</p>
|
|
1005
|
+
<p><strong>Evaluation Date:</strong> {timestamp}</p>
|
|
1006
|
+
<p><strong>Total Questions Evaluated:</strong> {total_questions}</p>
|
|
1007
|
+
<p><strong>Total Evaluation Time:</strong> {total_time:.2f} seconds</p>
|
|
1008
|
+
</div>
|
|
1009
|
+
|
|
1010
|
+
<div class="metric-card">
|
|
1011
|
+
<h2>Overall Pipeline Rankings</h2>
|
|
1012
|
+
{ranking_table}
|
|
1013
|
+
</div>
|
|
1014
|
+
|
|
1015
|
+
<div class="metric-card">
|
|
1016
|
+
<h2>Best Pipeline Per Metric</h2>
|
|
1017
|
+
{best_per_metric_table}
|
|
1018
|
+
</div>
|
|
1019
|
+
|
|
1020
|
+
<div class="metric-card">
|
|
1021
|
+
<h2>Statistical Significance Summary</h2>
|
|
1022
|
+
{significance_summary}
|
|
1023
|
+
</div>
|
|
1024
|
+
|
|
1025
|
+
<div class="metric-card">
|
|
1026
|
+
<h2>Detailed RAGAS Metrics</h2>
|
|
1027
|
+
{detailed_metrics_table}
|
|
1028
|
+
</div>
|
|
1029
|
+
|
|
1030
|
+
</body>
|
|
1031
|
+
</html>
|
|
1032
|
+
"""
|
|
1033
|
+
|
|
1034
|
+
# Generate tables and content
|
|
1035
|
+
ranking_table = self._generate_ranking_table(results)
|
|
1036
|
+
best_per_metric_table = self._generate_best_per_metric_table(results)
|
|
1037
|
+
significance_summary = self._generate_significance_summary_html(results)
|
|
1038
|
+
detailed_metrics_table = self._generate_detailed_metrics_table(results)
|
|
1039
|
+
|
|
1040
|
+
return html_template.format(
|
|
1041
|
+
experiment_id=results.experiment_id,
|
|
1042
|
+
timestamp=results.timestamp,
|
|
1043
|
+
total_questions=results.total_questions_evaluated,
|
|
1044
|
+
total_time=results.total_evaluation_time,
|
|
1045
|
+
ranking_table=ranking_table,
|
|
1046
|
+
best_per_metric_table=best_per_metric_table,
|
|
1047
|
+
significance_summary=significance_summary,
|
|
1048
|
+
detailed_metrics_table=detailed_metrics_table,
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
def _generate_ranking_table(self, results: ComparativeAnalysisResults) -> str:
|
|
1052
|
+
"""Generate ranking table HTML."""
|
|
1053
|
+
|
|
1054
|
+
# Calculate overall rankings based on average performance
|
|
1055
|
+
pipeline_scores = {}
|
|
1056
|
+
|
|
1057
|
+
for pipeline_name, result in results.pipeline_results.items():
|
|
1058
|
+
if result.ragas_results and result.ragas_results.overall_score:
|
|
1059
|
+
pipeline_scores[pipeline_name] = result.ragas_results.overall_score
|
|
1060
|
+
else:
|
|
1061
|
+
pipeline_scores[pipeline_name] = 0.0
|
|
1062
|
+
|
|
1063
|
+
# Sort by score
|
|
1064
|
+
sorted_pipelines = sorted(
|
|
1065
|
+
pipeline_scores.items(), key=lambda x: x[1], reverse=True
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
html = "<table><tr><th>Rank</th><th>Pipeline</th><th>Overall Score</th></tr>"
|
|
1069
|
+
|
|
1070
|
+
for rank, (pipeline, score) in enumerate(sorted_pipelines, 1):
|
|
1071
|
+
html += f"<tr><td>{rank}</td><td>{pipeline}</td><td>{score:.4f}</td></tr>"
|
|
1072
|
+
|
|
1073
|
+
html += "</table>"
|
|
1074
|
+
return html
|
|
1075
|
+
|
|
1076
|
+
def _generate_best_per_metric_table(
|
|
1077
|
+
self, results: ComparativeAnalysisResults
|
|
1078
|
+
) -> str:
|
|
1079
|
+
"""Generate best pipeline per metric table."""
|
|
1080
|
+
|
|
1081
|
+
html = "<table><tr><th>Metric</th><th>Best Pipeline</th><th>Score</th></tr>"
|
|
1082
|
+
|
|
1083
|
+
for metric, pipeline in results.best_pipeline_per_metric.items():
|
|
1084
|
+
result = results.pipeline_results[pipeline]
|
|
1085
|
+
score = result.get_metric_value(metric)
|
|
1086
|
+
html += f"<tr><td>{metric}</td><td>{pipeline}</td><td>{score:.4f if score else 'N/A'}</td></tr>"
|
|
1087
|
+
|
|
1088
|
+
html += "</table>"
|
|
1089
|
+
return html
|
|
1090
|
+
|
|
1091
|
+
def _generate_significance_summary_html(
|
|
1092
|
+
self, results: ComparativeAnalysisResults
|
|
1093
|
+
) -> str:
|
|
1094
|
+
"""Generate statistical significance summary."""
|
|
1095
|
+
|
|
1096
|
+
html = "<ul>"
|
|
1097
|
+
|
|
1098
|
+
for metric, summary in results.statistical_significance_summary.items():
|
|
1099
|
+
html += f"<li><strong>{metric}:</strong> "
|
|
1100
|
+
html += f"{summary['significant_tests']}/{summary['total_tests']} comparisons significant "
|
|
1101
|
+
html += f"(largest effect size: {summary['largest_effect_size']:.3f})</li>"
|
|
1102
|
+
|
|
1103
|
+
html += "</ul>"
|
|
1104
|
+
return html
|
|
1105
|
+
|
|
1106
|
+
def _generate_detailed_metrics_table(
|
|
1107
|
+
self, results: ComparativeAnalysisResults
|
|
1108
|
+
) -> str:
|
|
1109
|
+
"""Generate detailed metrics table."""
|
|
1110
|
+
|
|
1111
|
+
metrics = self.config.ragas_metrics
|
|
1112
|
+
pipelines = list(results.pipeline_results.keys())
|
|
1113
|
+
|
|
1114
|
+
html = "<table><tr><th>Pipeline</th>"
|
|
1115
|
+
for metric in metrics:
|
|
1116
|
+
html += f"<th>{metric}</th>"
|
|
1117
|
+
html += "<th>Success Rate</th></tr>"
|
|
1118
|
+
|
|
1119
|
+
for pipeline_name in pipelines:
|
|
1120
|
+
result = results.pipeline_results[pipeline_name]
|
|
1121
|
+
html += f"<tr><td>{pipeline_name}</td>"
|
|
1122
|
+
|
|
1123
|
+
for metric in metrics:
|
|
1124
|
+
score = result.get_metric_value(metric)
|
|
1125
|
+
html += f"<td>{score:.4f if score else 'N/A'}</td>"
|
|
1126
|
+
|
|
1127
|
+
html += f"<td>{result.success_rate:.2%}</td></tr>"
|
|
1128
|
+
|
|
1129
|
+
html += "</table>"
|
|
1130
|
+
return html
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def create_comparative_analysis_system(
|
|
1134
|
+
config: Optional[PipelineEvaluationConfig] = None,
|
|
1135
|
+
) -> ComparativeAnalysisSystem:
|
|
1136
|
+
"""Factory function to create a configured comparative analysis system."""
|
|
1137
|
+
return ComparativeAnalysisSystem(config)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
if __name__ == "__main__":
|
|
1141
|
+
# Example usage
|
|
1142
|
+
config = PipelineEvaluationConfig(
|
|
1143
|
+
total_questions=100, # Reduced for testing
|
|
1144
|
+
questions_per_pipeline_test=20,
|
|
1145
|
+
sample_documents=100,
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
system = create_comparative_analysis_system(config)
|
|
1149
|
+
|
|
1150
|
+
# Mock documents for testing
|
|
1151
|
+
sample_documents = [
|
|
1152
|
+
{
|
|
1153
|
+
"doc_id": "pmc_001",
|
|
1154
|
+
"title": "Diabetes Management in Clinical Practice",
|
|
1155
|
+
"content": "Type 2 diabetes is characterized by insulin resistance and relative insulin deficiency. Management includes lifestyle modifications, oral hypoglycemic agents, and insulin therapy when indicated.",
|
|
1156
|
+
"source": "PMC_database",
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
"doc_id": "pmc_002",
|
|
1160
|
+
"title": "Cardiovascular Risk in Diabetic Patients",
|
|
1161
|
+
"content": "Patients with diabetes have increased cardiovascular risk. Preventive measures include blood pressure control, lipid management, and antiplatelet therapy.",
|
|
1162
|
+
"source": "PMC_database",
|
|
1163
|
+
},
|
|
1164
|
+
]
|
|
1165
|
+
|
|
1166
|
+
# Run comprehensive comparison
|
|
1167
|
+
results = system.run_comprehensive_comparison(
|
|
1168
|
+
documents=sample_documents, pipeline_names=["BasicRAG", "CRAG"] # Test subset
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
print(f"Comparison completed: {results.experiment_id}")
|
|
1172
|
+
print(f"Total evaluation time: {results.total_evaluation_time:.2f}s")
|
|
1173
|
+
print(f"Questions evaluated: {results.total_questions_evaluated}")
|