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.
Files changed (50) hide show
  1. adapters/rag_templates_bridge.py +578 -0
  2. common/__init__.py +1 -0
  3. common/batch_utils.py +294 -0
  4. common/config.py +58 -0
  5. common/connection_manager.py +242 -0
  6. common/connection_pool.py +445 -0
  7. common/database.py +141 -0
  8. common/database_schema_manager.py +289 -0
  9. common/db_init_with_indexes.py +181 -0
  10. common/db_vector_utils.py +181 -0
  11. common/environment_manager.py +220 -0
  12. common/huggingface_utils.py +206 -0
  13. common/iris_client.py +92 -0
  14. common/iris_connection_manager.py +411 -0
  15. common/iris_connection_pool.py +360 -0
  16. common/iris_connector.py +71 -0
  17. common/iris_dbapi_connector.py +323 -0
  18. common/iris_index_utils.py +195 -0
  19. common/iris_sql_utils.py +704 -0
  20. common/iris_stream_reader.py +166 -0
  21. common/llm_cache_config.py +308 -0
  22. common/llm_cache_iris.py +671 -0
  23. common/llm_cache_manager.py +911 -0
  24. common/utils.py +559 -0
  25. common/vector_sql_utils.py +584 -0
  26. evaluation_framework/__init__.py +66 -0
  27. evaluation_framework/biomedical_question_generator.py +826 -0
  28. evaluation_framework/comparative_analysis_system.py +1173 -0
  29. evaluation_framework/empirical_reporting.py +1298 -0
  30. evaluation_framework/evaluation_orchestrator.py +1123 -0
  31. evaluation_framework/execute_production_evaluation.py +382 -0
  32. evaluation_framework/pmc_data_pipeline.py +1332 -0
  33. evaluation_framework/production_evaluation_config.py +353 -0
  34. evaluation_framework/ragas_metrics_framework.py +1013 -0
  35. evaluation_framework/real_production_evaluation.py +544 -0
  36. evaluation_framework/run_10k_evaluation.py +175 -0
  37. evaluation_framework/simple_test.py +364 -0
  38. evaluation_framework/statistical_evaluation_methodology.py +1016 -0
  39. evaluation_framework/test_framework.py +281 -0
  40. evaluation_framework/test_iris_connectivity.py +244 -0
  41. evaluation_framework/test_small_scale_evaluation.py +432 -0
  42. evaluation_framework/true_e2e_evaluation.py +639 -0
  43. evaluation_framework/visualization_engine.py +585 -0
  44. iris_rag/__init__.py +284 -0
  45. iris_rag/config/default_config.yaml +295 -0
  46. iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
  47. iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
  48. iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
  49. iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
  50. 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}")