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,1016 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Statistical Evaluation Methodology for Biomedical RAG Pipeline Comparison
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive statistical analysis capabilities for rigorous
|
|
5
|
+
comparison of RAG pipeline performance using RAGAS metrics with proper statistical
|
|
6
|
+
inference, power analysis, and multiple comparison corrections.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
1. Power analysis and sample size determination
|
|
10
|
+
2. Statistical test selection and validation
|
|
11
|
+
3. Multiple comparison corrections
|
|
12
|
+
4. Effect size calculations and interpretation
|
|
13
|
+
5. Bootstrap confidence intervals
|
|
14
|
+
6. Bayesian statistical analysis
|
|
15
|
+
7. Experimental design validation
|
|
16
|
+
8. Comprehensive statistical reporting
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import warnings
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
import pandas as pd
|
|
29
|
+
|
|
30
|
+
warnings.filterwarnings("ignore")
|
|
31
|
+
|
|
32
|
+
import pingouin as pg
|
|
33
|
+
|
|
34
|
+
# Statistical libraries
|
|
35
|
+
from scipy import stats
|
|
36
|
+
from scipy.stats import (
|
|
37
|
+
bootstrap,
|
|
38
|
+
chi2_contingency,
|
|
39
|
+
combine_pvalues,
|
|
40
|
+
f_oneway,
|
|
41
|
+
friedmanchisquare,
|
|
42
|
+
kruskal,
|
|
43
|
+
mannwhitneyu,
|
|
44
|
+
permutation_test,
|
|
45
|
+
ttest_ind,
|
|
46
|
+
ttest_rel,
|
|
47
|
+
wilcoxon,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Machine learning for effect size
|
|
51
|
+
from sklearn.metrics import cohen_kappa_score
|
|
52
|
+
from sklearn.preprocessing import StandardScaler
|
|
53
|
+
from statsmodels.stats.contingency_tables import mcnemar
|
|
54
|
+
from statsmodels.stats.descriptivestats import describe
|
|
55
|
+
from statsmodels.stats.multitest import multipletests
|
|
56
|
+
from statsmodels.stats.power import ttest_power
|
|
57
|
+
|
|
58
|
+
# Bayesian analysis
|
|
59
|
+
try:
|
|
60
|
+
import arviz as az
|
|
61
|
+
import pymc as pm
|
|
62
|
+
|
|
63
|
+
BAYESIAN_AVAILABLE = True
|
|
64
|
+
except ImportError:
|
|
65
|
+
BAYESIAN_AVAILABLE = False
|
|
66
|
+
logging.warning("PyMC not available, Bayesian analysis disabled")
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StatTestType(Enum):
|
|
72
|
+
"""Statistical test types for different scenarios."""
|
|
73
|
+
|
|
74
|
+
PARAMETRIC_INDEPENDENT = "parametric_independent"
|
|
75
|
+
PARAMETRIC_PAIRED = "parametric_paired"
|
|
76
|
+
NONPARAMETRIC_INDEPENDENT = "nonparametric_independent"
|
|
77
|
+
NONPARAMETRIC_PAIRED = "nonparametric_paired"
|
|
78
|
+
ANOVA = "anova"
|
|
79
|
+
KRUSKAL_WALLIS = "kruskal_wallis"
|
|
80
|
+
FRIEDMAN = "friedman"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class EffectSizeType(Enum):
|
|
84
|
+
"""Effect size measures for different contexts."""
|
|
85
|
+
|
|
86
|
+
COHENS_D = "cohens_d"
|
|
87
|
+
HEDGES_G = "hedges_g"
|
|
88
|
+
GLASS_DELTA = "glass_delta"
|
|
89
|
+
ETA_SQUARED = "eta_squared"
|
|
90
|
+
OMEGA_SQUARED = "omega_squared"
|
|
91
|
+
CLIFF_DELTA = "cliff_delta"
|
|
92
|
+
COMMON_LANGUAGE_ES = "common_language_effect_size"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class StatisticalTestResult:
|
|
97
|
+
"""Comprehensive statistical test result."""
|
|
98
|
+
|
|
99
|
+
test_name: str
|
|
100
|
+
test_statistic: float
|
|
101
|
+
p_value: float
|
|
102
|
+
p_value_adjusted: Optional[float]
|
|
103
|
+
effect_size: float
|
|
104
|
+
effect_size_type: str
|
|
105
|
+
effect_size_interpretation: str
|
|
106
|
+
confidence_interval: Tuple[float, float]
|
|
107
|
+
power: Optional[float]
|
|
108
|
+
sample_sizes: List[int]
|
|
109
|
+
assumptions_met: Dict[str, bool]
|
|
110
|
+
interpretation: str
|
|
111
|
+
raw_data: Dict[str, Any] = field(default_factory=dict)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class PowerAnalysisResult:
|
|
116
|
+
"""Power analysis results for study design."""
|
|
117
|
+
|
|
118
|
+
effect_size: float
|
|
119
|
+
power: float
|
|
120
|
+
sample_size: int
|
|
121
|
+
alpha: float
|
|
122
|
+
test_type: str
|
|
123
|
+
recommendations: List[str]
|
|
124
|
+
sensitivity_analysis: Dict[str, float]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class MultipleComparisonResult:
|
|
129
|
+
"""Results from multiple comparison procedures."""
|
|
130
|
+
|
|
131
|
+
method: str
|
|
132
|
+
original_p_values: List[float]
|
|
133
|
+
adjusted_p_values: List[float]
|
|
134
|
+
rejected_hypotheses: List[bool]
|
|
135
|
+
alpha_corrected: float
|
|
136
|
+
family_wise_error_rate: float
|
|
137
|
+
false_discovery_rate: float
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class StatisticalEvaluationFramework:
|
|
141
|
+
"""
|
|
142
|
+
Comprehensive statistical evaluation framework for biomedical RAG pipeline comparison.
|
|
143
|
+
|
|
144
|
+
Provides rigorous statistical analysis with proper experimental design,
|
|
145
|
+
power analysis, and multiple comparison corrections.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
149
|
+
"""
|
|
150
|
+
Initialize statistical evaluation framework.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
config: Configuration dictionary for statistical parameters
|
|
154
|
+
"""
|
|
155
|
+
self.config = config or self._get_default_config()
|
|
156
|
+
|
|
157
|
+
# Statistical parameters
|
|
158
|
+
self.alpha = self.config.get("alpha", 0.05)
|
|
159
|
+
self.power_target = self.config.get("power_target", 0.8)
|
|
160
|
+
self.effect_size_threshold = self.config.get("effect_size_threshold", 0.5)
|
|
161
|
+
self.bootstrap_samples = self.config.get("bootstrap_samples", 1000)
|
|
162
|
+
self.multiple_correction_method = self.config.get(
|
|
163
|
+
"multiple_correction_method", "fdr_bh"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
logger.info("Statistical Evaluation Framework initialized")
|
|
167
|
+
|
|
168
|
+
def _get_default_config(self) -> Dict[str, Any]:
|
|
169
|
+
"""Get default statistical configuration."""
|
|
170
|
+
return {
|
|
171
|
+
"alpha": 0.05,
|
|
172
|
+
"power_target": 0.8,
|
|
173
|
+
"effect_size_threshold": 0.5,
|
|
174
|
+
"bootstrap_samples": 1000,
|
|
175
|
+
"multiple_correction_method": "fdr_bh",
|
|
176
|
+
"normality_test": "shapiro",
|
|
177
|
+
"homoscedasticity_test": "levene",
|
|
178
|
+
"outlier_detection": "iqr",
|
|
179
|
+
"missing_data_threshold": 0.1,
|
|
180
|
+
"bayesian_chains": 4,
|
|
181
|
+
"bayesian_draws": 2000,
|
|
182
|
+
"bayesian_tune": 1000,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def conduct_power_analysis(
|
|
186
|
+
self, pipelines_data: Dict[str, List[float]], metric_name: str
|
|
187
|
+
) -> PowerAnalysisResult:
|
|
188
|
+
"""
|
|
189
|
+
Conduct comprehensive power analysis for pipeline comparison.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
pipelines_data: Dictionary mapping pipeline names to metric scores
|
|
193
|
+
metric_name: Name of the metric being analyzed
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Power analysis results with sample size recommendations
|
|
197
|
+
"""
|
|
198
|
+
logger.info(f"Conducting power analysis for {metric_name}")
|
|
199
|
+
|
|
200
|
+
# Calculate effect size from pilot data
|
|
201
|
+
pipeline_names = list(pipelines_data.keys())
|
|
202
|
+
if len(pipeline_names) < 2:
|
|
203
|
+
raise ValueError("Need at least 2 pipelines for comparison")
|
|
204
|
+
|
|
205
|
+
# Use first two pipelines for effect size estimation
|
|
206
|
+
group1_data = pipelines_data[pipeline_names[0]]
|
|
207
|
+
group2_data = pipelines_data[pipeline_names[1]]
|
|
208
|
+
|
|
209
|
+
# Calculate Cohen's d
|
|
210
|
+
effect_size = self._calculate_cohens_d(group1_data, group2_data)
|
|
211
|
+
|
|
212
|
+
# Determine test type
|
|
213
|
+
if len(pipeline_names) == 2:
|
|
214
|
+
test_type = "two_sample_ttest"
|
|
215
|
+
else:
|
|
216
|
+
test_type = "one_way_anova"
|
|
217
|
+
|
|
218
|
+
# Calculate required sample size
|
|
219
|
+
if test_type == "two_sample_ttest":
|
|
220
|
+
sample_size = ttest_power(
|
|
221
|
+
effect_size=abs(effect_size),
|
|
222
|
+
power=self.power_target,
|
|
223
|
+
alpha=self.alpha,
|
|
224
|
+
alternative="two-sided",
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
# For ANOVA, approximate with pairwise t-test sample size
|
|
228
|
+
# This is a conservative approximation
|
|
229
|
+
sample_size = ttest_power(
|
|
230
|
+
effect_size=abs(effect_size),
|
|
231
|
+
nobs=None,
|
|
232
|
+
alpha=self.alpha / len(pipeline_names), # Bonferroni correction
|
|
233
|
+
power=self.power_target,
|
|
234
|
+
alternative="two-sided",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Calculate achieved power with current sample sizes
|
|
238
|
+
current_min_n = min(len(data) for data in pipelines_data.values())
|
|
239
|
+
if test_type == "two_sample_ttest":
|
|
240
|
+
achieved_power = ttest_power(
|
|
241
|
+
effect_size=abs(effect_size), nobs=current_min_n, alpha=self.alpha
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
achieved_power = 0.8 # Placeholder for ANOVA power calculation
|
|
245
|
+
|
|
246
|
+
# Generate recommendations
|
|
247
|
+
recommendations = []
|
|
248
|
+
if current_min_n < sample_size:
|
|
249
|
+
recommendations.append(
|
|
250
|
+
f"Increase sample size to {int(np.ceil(sample_size))} per group"
|
|
251
|
+
)
|
|
252
|
+
if abs(effect_size) < 0.2:
|
|
253
|
+
recommendations.append(
|
|
254
|
+
"Small effect size detected - consider larger sample or different metrics"
|
|
255
|
+
)
|
|
256
|
+
if achieved_power < self.power_target:
|
|
257
|
+
recommendations.append(
|
|
258
|
+
f"Current power ({achieved_power:.3f}) below target ({self.power_target})"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Sensitivity analysis
|
|
262
|
+
sensitivity_analysis = {}
|
|
263
|
+
for alpha_sens in [0.01, 0.05, 0.10]:
|
|
264
|
+
if test_type == "two_sample_ttest":
|
|
265
|
+
n_sens = ttest_power(
|
|
266
|
+
effect_size=abs(effect_size),
|
|
267
|
+
power=self.power_target,
|
|
268
|
+
alpha=alpha_sens,
|
|
269
|
+
alternative="two-sided",
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
n_sens = sample_size # Simplified for ANOVA
|
|
273
|
+
sensitivity_analysis[f"alpha_{alpha_sens}"] = n_sens
|
|
274
|
+
|
|
275
|
+
return PowerAnalysisResult(
|
|
276
|
+
effect_size=effect_size,
|
|
277
|
+
power=achieved_power,
|
|
278
|
+
sample_size=(
|
|
279
|
+
int(np.ceil(sample_size)) if np.isfinite(sample_size) else current_min_n
|
|
280
|
+
),
|
|
281
|
+
alpha=self.alpha,
|
|
282
|
+
test_type=test_type,
|
|
283
|
+
recommendations=recommendations,
|
|
284
|
+
sensitivity_analysis=sensitivity_analysis,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def compare_pipelines_statistical(
|
|
288
|
+
self,
|
|
289
|
+
pipelines_data: Dict[str, List[float]],
|
|
290
|
+
metric_name: str,
|
|
291
|
+
paired: bool = False,
|
|
292
|
+
) -> List[StatisticalTestResult]:
|
|
293
|
+
"""
|
|
294
|
+
Comprehensive statistical comparison of pipeline performance.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
pipelines_data: Dictionary mapping pipeline names to metric scores
|
|
298
|
+
metric_name: Name of the metric being analyzed
|
|
299
|
+
paired: Whether the data represents paired comparisons
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
List of statistical test results for all comparisons
|
|
303
|
+
"""
|
|
304
|
+
logger.info(
|
|
305
|
+
f"Statistical comparison of {len(pipelines_data)} pipelines for {metric_name}"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Validate data
|
|
309
|
+
self._validate_statistical_data(pipelines_data)
|
|
310
|
+
|
|
311
|
+
pipeline_names = list(pipelines_data.keys())
|
|
312
|
+
results = []
|
|
313
|
+
|
|
314
|
+
if len(pipeline_names) == 2:
|
|
315
|
+
# Two-group comparison
|
|
316
|
+
result = self._compare_two_groups(
|
|
317
|
+
pipelines_data[pipeline_names[0]],
|
|
318
|
+
pipelines_data[pipeline_names[1]],
|
|
319
|
+
pipeline_names[0],
|
|
320
|
+
pipeline_names[1],
|
|
321
|
+
metric_name,
|
|
322
|
+
paired,
|
|
323
|
+
)
|
|
324
|
+
results.append(result)
|
|
325
|
+
|
|
326
|
+
elif len(pipeline_names) > 2:
|
|
327
|
+
# Multiple group comparison
|
|
328
|
+
# First, overall test
|
|
329
|
+
overall_result = self._compare_multiple_groups(
|
|
330
|
+
pipelines_data, metric_name, paired
|
|
331
|
+
)
|
|
332
|
+
results.append(overall_result)
|
|
333
|
+
|
|
334
|
+
# Then, pairwise comparisons if overall test is significant
|
|
335
|
+
if overall_result.p_value < self.alpha:
|
|
336
|
+
pairwise_results = self._pairwise_comparisons(
|
|
337
|
+
pipelines_data, metric_name, paired
|
|
338
|
+
)
|
|
339
|
+
results.extend(pairwise_results)
|
|
340
|
+
|
|
341
|
+
# Apply multiple comparison correction
|
|
342
|
+
if len(results) > 1:
|
|
343
|
+
results = self._apply_multiple_comparison_correction(results)
|
|
344
|
+
|
|
345
|
+
return results
|
|
346
|
+
|
|
347
|
+
def _validate_statistical_data(self, pipelines_data: Dict[str, List[float]]):
|
|
348
|
+
"""Validate data for statistical analysis."""
|
|
349
|
+
for pipeline_name, data in pipelines_data.items():
|
|
350
|
+
if len(data) == 0:
|
|
351
|
+
raise ValueError(f"No data for pipeline {pipeline_name}")
|
|
352
|
+
|
|
353
|
+
# Check for missing values
|
|
354
|
+
valid_data = [x for x in data if np.isfinite(x)]
|
|
355
|
+
missing_rate = 1 - len(valid_data) / len(data)
|
|
356
|
+
|
|
357
|
+
if missing_rate > self.config["missing_data_threshold"]:
|
|
358
|
+
logger.warning(
|
|
359
|
+
f"High missing data rate ({missing_rate:.2%}) for {pipeline_name}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Check for outliers
|
|
363
|
+
outliers = self._detect_outliers(valid_data)
|
|
364
|
+
if len(outliers) > 0:
|
|
365
|
+
logger.info(f"Detected {len(outliers)} outliers in {pipeline_name}")
|
|
366
|
+
|
|
367
|
+
def _detect_outliers(self, data: List[float]) -> List[int]:
|
|
368
|
+
"""Detect outliers using IQR method."""
|
|
369
|
+
if len(data) < 4:
|
|
370
|
+
return []
|
|
371
|
+
|
|
372
|
+
Q1 = np.percentile(data, 25)
|
|
373
|
+
Q3 = np.percentile(data, 75)
|
|
374
|
+
IQR = Q3 - Q1
|
|
375
|
+
|
|
376
|
+
lower_bound = Q1 - 1.5 * IQR
|
|
377
|
+
upper_bound = Q3 + 1.5 * IQR
|
|
378
|
+
|
|
379
|
+
outliers = [i for i, x in enumerate(data) if x < lower_bound or x > upper_bound]
|
|
380
|
+
return outliers
|
|
381
|
+
|
|
382
|
+
def _compare_two_groups(
|
|
383
|
+
self,
|
|
384
|
+
group1_data: List[float],
|
|
385
|
+
group2_data: List[float],
|
|
386
|
+
group1_name: str,
|
|
387
|
+
group2_name: str,
|
|
388
|
+
metric_name: str,
|
|
389
|
+
paired: bool,
|
|
390
|
+
) -> StatisticalTestResult:
|
|
391
|
+
"""Compare two groups statistically."""
|
|
392
|
+
|
|
393
|
+
# Clean data
|
|
394
|
+
g1_clean = [x for x in group1_data if np.isfinite(x)]
|
|
395
|
+
g2_clean = [x for x in group2_data if np.isfinite(x)]
|
|
396
|
+
|
|
397
|
+
# Check assumptions
|
|
398
|
+
assumptions = self._check_two_sample_assumptions(g1_clean, g2_clean, paired)
|
|
399
|
+
|
|
400
|
+
# Select appropriate test
|
|
401
|
+
if paired:
|
|
402
|
+
if assumptions["normality"] and assumptions["homoscedasticity"]:
|
|
403
|
+
test_statistic, p_value = stats.ttest_rel(g1_clean, g2_clean)
|
|
404
|
+
test_name = "Paired t-test"
|
|
405
|
+
else:
|
|
406
|
+
test_statistic, p_value = stats.wilcoxon(g1_clean, g2_clean)
|
|
407
|
+
test_name = "Wilcoxon signed-rank test"
|
|
408
|
+
else:
|
|
409
|
+
if assumptions["normality"] and assumptions["homoscedasticity"]:
|
|
410
|
+
test_statistic, p_value = stats.ttest_ind(g1_clean, g2_clean)
|
|
411
|
+
test_name = "Independent t-test"
|
|
412
|
+
else:
|
|
413
|
+
test_statistic, p_value = stats.mannwhitneyu(
|
|
414
|
+
g1_clean, g2_clean, alternative="two-sided"
|
|
415
|
+
)
|
|
416
|
+
test_name = "Mann-Whitney U test"
|
|
417
|
+
|
|
418
|
+
# Calculate effect size
|
|
419
|
+
effect_size = self._calculate_cohens_d(g1_clean, g2_clean)
|
|
420
|
+
effect_size_interpretation = self._interpret_effect_size(
|
|
421
|
+
effect_size, EffectSizeType.COHENS_D
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Bootstrap confidence interval
|
|
425
|
+
confidence_interval = self._bootstrap_confidence_interval(g1_clean, g2_clean)
|
|
426
|
+
|
|
427
|
+
# Calculate power
|
|
428
|
+
power = self._calculate_post_hoc_power(g1_clean, g2_clean, effect_size)
|
|
429
|
+
|
|
430
|
+
# Generate interpretation
|
|
431
|
+
interpretation = self._generate_interpretation(
|
|
432
|
+
test_name,
|
|
433
|
+
p_value,
|
|
434
|
+
effect_size,
|
|
435
|
+
effect_size_interpretation,
|
|
436
|
+
group1_name,
|
|
437
|
+
group2_name,
|
|
438
|
+
metric_name,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return StatisticalTestResult(
|
|
442
|
+
test_name=test_name,
|
|
443
|
+
test_statistic=test_statistic,
|
|
444
|
+
p_value=p_value,
|
|
445
|
+
p_value_adjusted=None, # Will be set during multiple comparison correction
|
|
446
|
+
effect_size=effect_size,
|
|
447
|
+
effect_size_type=EffectSizeType.COHENS_D.value,
|
|
448
|
+
effect_size_interpretation=effect_size_interpretation,
|
|
449
|
+
confidence_interval=confidence_interval,
|
|
450
|
+
power=power,
|
|
451
|
+
sample_sizes=[len(g1_clean), len(g2_clean)],
|
|
452
|
+
assumptions_met=assumptions,
|
|
453
|
+
interpretation=interpretation,
|
|
454
|
+
raw_data={
|
|
455
|
+
"group1_name": group1_name,
|
|
456
|
+
"group2_name": group2_name,
|
|
457
|
+
"group1_data": g1_clean,
|
|
458
|
+
"group2_data": g2_clean,
|
|
459
|
+
"metric_name": metric_name,
|
|
460
|
+
},
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def _compare_multiple_groups(
|
|
464
|
+
self, pipelines_data: Dict[str, List[float]], metric_name: str, paired: bool
|
|
465
|
+
) -> StatisticalTestResult:
|
|
466
|
+
"""Compare multiple groups using ANOVA or Kruskal-Wallis."""
|
|
467
|
+
|
|
468
|
+
# Prepare data
|
|
469
|
+
all_data = []
|
|
470
|
+
group_labels = []
|
|
471
|
+
|
|
472
|
+
for pipeline_name, data in pipelines_data.items():
|
|
473
|
+
clean_data = [x for x in data if np.isfinite(x)]
|
|
474
|
+
all_data.extend(clean_data)
|
|
475
|
+
group_labels.extend([pipeline_name] * len(clean_data))
|
|
476
|
+
|
|
477
|
+
# Check assumptions
|
|
478
|
+
assumptions = self._check_multiple_group_assumptions(pipelines_data, paired)
|
|
479
|
+
|
|
480
|
+
# Select appropriate test
|
|
481
|
+
if paired:
|
|
482
|
+
if assumptions["normality"]:
|
|
483
|
+
# Repeated measures ANOVA (simplified as regular ANOVA for now)
|
|
484
|
+
pipeline_values = list(pipelines_data.values())
|
|
485
|
+
test_statistic, p_value = stats.f_oneway(*pipeline_values)
|
|
486
|
+
test_name = "Repeated measures ANOVA"
|
|
487
|
+
else:
|
|
488
|
+
# Friedman test
|
|
489
|
+
pipeline_values = list(pipelines_data.values())
|
|
490
|
+
test_statistic, p_value = stats.friedmanchisquare(*pipeline_values)
|
|
491
|
+
test_name = "Friedman test"
|
|
492
|
+
else:
|
|
493
|
+
if assumptions["normality"] and assumptions["homoscedasticity"]:
|
|
494
|
+
pipeline_values = [data for data in pipelines_data.values()]
|
|
495
|
+
test_statistic, p_value = stats.f_oneway(*pipeline_values)
|
|
496
|
+
test_name = "One-way ANOVA"
|
|
497
|
+
else:
|
|
498
|
+
pipeline_values = [data for data in pipelines_data.values()]
|
|
499
|
+
test_statistic, p_value = stats.kruskal(*pipeline_values)
|
|
500
|
+
test_name = "Kruskal-Wallis test"
|
|
501
|
+
|
|
502
|
+
# Calculate effect size (eta-squared for ANOVA)
|
|
503
|
+
effect_size = self._calculate_eta_squared(pipelines_data)
|
|
504
|
+
effect_size_interpretation = self._interpret_effect_size(
|
|
505
|
+
effect_size, EffectSizeType.ETA_SQUARED
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Confidence interval (approximate)
|
|
509
|
+
confidence_interval = (0.0, 1.0) # Placeholder for overall tests
|
|
510
|
+
|
|
511
|
+
# Sample sizes
|
|
512
|
+
sample_sizes = [
|
|
513
|
+
len([x for x in data if np.isfinite(x)]) for data in pipelines_data.values()
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
# Interpretation
|
|
517
|
+
pipeline_names = list(pipelines_data.keys())
|
|
518
|
+
interpretation = (
|
|
519
|
+
f"{test_name} comparing {len(pipeline_names)} pipelines on {metric_name}. "
|
|
520
|
+
)
|
|
521
|
+
if p_value < self.alpha:
|
|
522
|
+
interpretation += f"Significant difference detected (p={p_value:.4f}). "
|
|
523
|
+
else:
|
|
524
|
+
interpretation += f"No significant difference detected (p={p_value:.4f}). "
|
|
525
|
+
interpretation += f"Effect size ({EffectSizeType.ETA_SQUARED.value}): {effect_size:.4f} ({effect_size_interpretation})"
|
|
526
|
+
|
|
527
|
+
return StatisticalTestResult(
|
|
528
|
+
test_name=test_name,
|
|
529
|
+
test_statistic=test_statistic,
|
|
530
|
+
p_value=p_value,
|
|
531
|
+
p_value_adjusted=None,
|
|
532
|
+
effect_size=effect_size,
|
|
533
|
+
effect_size_type=EffectSizeType.ETA_SQUARED.value,
|
|
534
|
+
effect_size_interpretation=effect_size_interpretation,
|
|
535
|
+
confidence_interval=confidence_interval,
|
|
536
|
+
power=None, # Not easily calculated for multi-group tests
|
|
537
|
+
sample_sizes=sample_sizes,
|
|
538
|
+
assumptions_met=assumptions,
|
|
539
|
+
interpretation=interpretation,
|
|
540
|
+
raw_data={
|
|
541
|
+
"pipelines_data": pipelines_data,
|
|
542
|
+
"metric_name": metric_name,
|
|
543
|
+
"all_data": all_data,
|
|
544
|
+
"group_labels": group_labels,
|
|
545
|
+
},
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def _pairwise_comparisons(
|
|
549
|
+
self, pipelines_data: Dict[str, List[float]], metric_name: str, paired: bool
|
|
550
|
+
) -> List[StatisticalTestResult]:
|
|
551
|
+
"""Conduct all pairwise comparisons between pipelines."""
|
|
552
|
+
|
|
553
|
+
pipeline_names = list(pipelines_data.keys())
|
|
554
|
+
results = []
|
|
555
|
+
|
|
556
|
+
for i in range(len(pipeline_names)):
|
|
557
|
+
for j in range(i + 1, len(pipeline_names)):
|
|
558
|
+
pipeline1 = pipeline_names[i]
|
|
559
|
+
pipeline2 = pipeline_names[j]
|
|
560
|
+
|
|
561
|
+
result = self._compare_two_groups(
|
|
562
|
+
pipelines_data[pipeline1],
|
|
563
|
+
pipelines_data[pipeline2],
|
|
564
|
+
pipeline1,
|
|
565
|
+
pipeline2,
|
|
566
|
+
metric_name,
|
|
567
|
+
paired,
|
|
568
|
+
)
|
|
569
|
+
results.append(result)
|
|
570
|
+
|
|
571
|
+
return results
|
|
572
|
+
|
|
573
|
+
def _check_two_sample_assumptions(
|
|
574
|
+
self, group1: List[float], group2: List[float], paired: bool
|
|
575
|
+
) -> Dict[str, bool]:
|
|
576
|
+
"""Check statistical assumptions for two-sample tests."""
|
|
577
|
+
assumptions = {}
|
|
578
|
+
|
|
579
|
+
# Normality test
|
|
580
|
+
if len(group1) >= 3 and len(group2) >= 3:
|
|
581
|
+
_, p1 = stats.shapiro(group1)
|
|
582
|
+
_, p2 = stats.shapiro(group2)
|
|
583
|
+
assumptions["normality"] = p1 > 0.05 and p2 > 0.05
|
|
584
|
+
else:
|
|
585
|
+
assumptions["normality"] = False
|
|
586
|
+
|
|
587
|
+
# Homoscedasticity test (only for independent samples)
|
|
588
|
+
if not paired and len(group1) >= 3 and len(group2) >= 3:
|
|
589
|
+
_, p_levene = stats.levene(group1, group2)
|
|
590
|
+
assumptions["homoscedasticity"] = p_levene > 0.05
|
|
591
|
+
else:
|
|
592
|
+
assumptions["homoscedasticity"] = True # Not applicable for paired data
|
|
593
|
+
|
|
594
|
+
return assumptions
|
|
595
|
+
|
|
596
|
+
def _check_multiple_group_assumptions(
|
|
597
|
+
self, pipelines_data: Dict[str, List[float]], paired: bool
|
|
598
|
+
) -> Dict[str, bool]:
|
|
599
|
+
"""Check statistical assumptions for multiple group tests."""
|
|
600
|
+
assumptions = {}
|
|
601
|
+
|
|
602
|
+
# Normality test for each group
|
|
603
|
+
normality_results = []
|
|
604
|
+
for data in pipelines_data.values():
|
|
605
|
+
clean_data = [x for x in data if np.isfinite(x)]
|
|
606
|
+
if len(clean_data) >= 3:
|
|
607
|
+
_, p_val = stats.shapiro(clean_data)
|
|
608
|
+
normality_results.append(p_val > 0.05)
|
|
609
|
+
else:
|
|
610
|
+
normality_results.append(False)
|
|
611
|
+
|
|
612
|
+
assumptions["normality"] = all(normality_results)
|
|
613
|
+
|
|
614
|
+
# Homoscedasticity test (only for independent samples)
|
|
615
|
+
if not paired:
|
|
616
|
+
clean_data_groups = []
|
|
617
|
+
for data in pipelines_data.values():
|
|
618
|
+
clean_data = [x for x in data if np.isfinite(x)]
|
|
619
|
+
if len(clean_data) >= 3:
|
|
620
|
+
clean_data_groups.append(clean_data)
|
|
621
|
+
|
|
622
|
+
if len(clean_data_groups) >= 2:
|
|
623
|
+
_, p_levene = stats.levene(*clean_data_groups)
|
|
624
|
+
assumptions["homoscedasticity"] = p_levene > 0.05
|
|
625
|
+
else:
|
|
626
|
+
assumptions["homoscedasticity"] = False
|
|
627
|
+
else:
|
|
628
|
+
assumptions["homoscedasticity"] = True
|
|
629
|
+
|
|
630
|
+
return assumptions
|
|
631
|
+
|
|
632
|
+
def _calculate_cohens_d(self, group1: List[float], group2: List[float]) -> float:
|
|
633
|
+
"""Calculate Cohen's d effect size."""
|
|
634
|
+
if len(group1) == 0 or len(group2) == 0:
|
|
635
|
+
return 0.0
|
|
636
|
+
|
|
637
|
+
mean1, mean2 = np.mean(group1), np.mean(group2)
|
|
638
|
+
|
|
639
|
+
# Pooled standard deviation
|
|
640
|
+
n1, n2 = len(group1), len(group2)
|
|
641
|
+
var1, var2 = np.var(group1, ddof=1), np.var(group2, ddof=1)
|
|
642
|
+
|
|
643
|
+
if n1 <= 1 or n2 <= 1:
|
|
644
|
+
return 0.0
|
|
645
|
+
|
|
646
|
+
pooled_std = np.sqrt(((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2))
|
|
647
|
+
|
|
648
|
+
if pooled_std == 0:
|
|
649
|
+
return 0.0
|
|
650
|
+
|
|
651
|
+
return (mean1 - mean2) / pooled_std
|
|
652
|
+
|
|
653
|
+
def _calculate_eta_squared(self, pipelines_data: Dict[str, List[float]]) -> float:
|
|
654
|
+
"""Calculate eta-squared effect size for multiple group comparison."""
|
|
655
|
+
try:
|
|
656
|
+
# Prepare data for ANOVA
|
|
657
|
+
all_data = []
|
|
658
|
+
group_labels = []
|
|
659
|
+
|
|
660
|
+
for pipeline_name, data in pipelines_data.items():
|
|
661
|
+
clean_data = [x for x in data if np.isfinite(x)]
|
|
662
|
+
all_data.extend(clean_data)
|
|
663
|
+
group_labels.extend([pipeline_name] * len(clean_data))
|
|
664
|
+
|
|
665
|
+
if len(set(group_labels)) < 2:
|
|
666
|
+
return 0.0
|
|
667
|
+
|
|
668
|
+
# Calculate eta-squared
|
|
669
|
+
df = pd.DataFrame({"score": all_data, "group": group_labels})
|
|
670
|
+
|
|
671
|
+
# Between-group sum of squares
|
|
672
|
+
group_means = df.groupby("group")["score"].mean()
|
|
673
|
+
overall_mean = df["score"].mean()
|
|
674
|
+
|
|
675
|
+
ss_between = sum(
|
|
676
|
+
len(pipelines_data[group]) * (group_means[group] - overall_mean) ** 2
|
|
677
|
+
for group in group_means.index
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Total sum of squares
|
|
681
|
+
ss_total = sum((x - overall_mean) ** 2 for x in all_data)
|
|
682
|
+
|
|
683
|
+
if ss_total == 0:
|
|
684
|
+
return 0.0
|
|
685
|
+
|
|
686
|
+
return ss_between / ss_total
|
|
687
|
+
|
|
688
|
+
except Exception as e:
|
|
689
|
+
logger.warning(f"Failed to calculate eta-squared: {e}")
|
|
690
|
+
return 0.0
|
|
691
|
+
|
|
692
|
+
def _interpret_effect_size(
|
|
693
|
+
self, effect_size: float, effect_type: EffectSizeType
|
|
694
|
+
) -> str:
|
|
695
|
+
"""Interpret effect size magnitude."""
|
|
696
|
+
abs_effect = abs(effect_size)
|
|
697
|
+
|
|
698
|
+
if effect_type == EffectSizeType.COHENS_D:
|
|
699
|
+
if abs_effect < 0.2:
|
|
700
|
+
return "negligible"
|
|
701
|
+
elif abs_effect < 0.5:
|
|
702
|
+
return "small"
|
|
703
|
+
elif abs_effect < 0.8:
|
|
704
|
+
return "medium"
|
|
705
|
+
else:
|
|
706
|
+
return "large"
|
|
707
|
+
|
|
708
|
+
elif effect_type == EffectSizeType.ETA_SQUARED:
|
|
709
|
+
if abs_effect < 0.01:
|
|
710
|
+
return "negligible"
|
|
711
|
+
elif abs_effect < 0.06:
|
|
712
|
+
return "small"
|
|
713
|
+
elif abs_effect < 0.14:
|
|
714
|
+
return "medium"
|
|
715
|
+
else:
|
|
716
|
+
return "large"
|
|
717
|
+
|
|
718
|
+
else:
|
|
719
|
+
return "unknown"
|
|
720
|
+
|
|
721
|
+
def _bootstrap_confidence_interval(
|
|
722
|
+
self, group1: List[float], group2: List[float]
|
|
723
|
+
) -> Tuple[float, float]:
|
|
724
|
+
"""Calculate bootstrap confidence interval for mean difference."""
|
|
725
|
+
try:
|
|
726
|
+
|
|
727
|
+
def mean_difference(x, y):
|
|
728
|
+
return np.mean(x) - np.mean(y)
|
|
729
|
+
|
|
730
|
+
# Bootstrap resampling
|
|
731
|
+
n_bootstrap = self.bootstrap_samples
|
|
732
|
+
bootstrap_diffs = []
|
|
733
|
+
|
|
734
|
+
for _ in range(n_bootstrap):
|
|
735
|
+
bootstrap_g1 = np.random.choice(group1, size=len(group1), replace=True)
|
|
736
|
+
bootstrap_g2 = np.random.choice(group2, size=len(group2), replace=True)
|
|
737
|
+
diff = mean_difference(bootstrap_g1, bootstrap_g2)
|
|
738
|
+
bootstrap_diffs.append(diff)
|
|
739
|
+
|
|
740
|
+
# Calculate confidence interval
|
|
741
|
+
alpha = 1 - self.config.get("confidence_level", 0.95)
|
|
742
|
+
lower_percentile = (alpha / 2) * 100
|
|
743
|
+
upper_percentile = (1 - alpha / 2) * 100
|
|
744
|
+
|
|
745
|
+
ci_lower = np.percentile(bootstrap_diffs, lower_percentile)
|
|
746
|
+
ci_upper = np.percentile(bootstrap_diffs, upper_percentile)
|
|
747
|
+
|
|
748
|
+
return (ci_lower, ci_upper)
|
|
749
|
+
|
|
750
|
+
except Exception as e:
|
|
751
|
+
logger.warning(f"Bootstrap confidence interval failed: {e}")
|
|
752
|
+
return (0.0, 0.0)
|
|
753
|
+
|
|
754
|
+
def _calculate_post_hoc_power(
|
|
755
|
+
self, group1: List[float], group2: List[float], effect_size: float
|
|
756
|
+
) -> Optional[float]:
|
|
757
|
+
"""Calculate post-hoc statistical power."""
|
|
758
|
+
try:
|
|
759
|
+
min_n = min(len(group1), len(group2))
|
|
760
|
+
power = ttest_power(
|
|
761
|
+
effect_size=abs(effect_size),
|
|
762
|
+
nobs=min_n,
|
|
763
|
+
alpha=self.alpha,
|
|
764
|
+
alternative="two-sided",
|
|
765
|
+
)
|
|
766
|
+
return power
|
|
767
|
+
except Exception:
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
def _generate_interpretation(
|
|
771
|
+
self,
|
|
772
|
+
test_name: str,
|
|
773
|
+
p_value: float,
|
|
774
|
+
effect_size: float,
|
|
775
|
+
effect_interpretation: str,
|
|
776
|
+
group1_name: str,
|
|
777
|
+
group2_name: str,
|
|
778
|
+
metric_name: str,
|
|
779
|
+
) -> str:
|
|
780
|
+
"""Generate human-readable interpretation of statistical results."""
|
|
781
|
+
|
|
782
|
+
interpretation = (
|
|
783
|
+
f"{test_name} comparing {group1_name} vs {group2_name} on {metric_name}. "
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
if p_value < self.alpha:
|
|
787
|
+
interpretation += f"Significant difference detected (p={p_value:.4f}). "
|
|
788
|
+
|
|
789
|
+
if effect_size > 0:
|
|
790
|
+
interpretation += f"{group1_name} performs better than {group2_name}. "
|
|
791
|
+
else:
|
|
792
|
+
interpretation += f"{group2_name} performs better than {group1_name}. "
|
|
793
|
+
else:
|
|
794
|
+
interpretation += f"No significant difference detected (p={p_value:.4f}). "
|
|
795
|
+
|
|
796
|
+
interpretation += f"Effect size: {effect_size:.4f} ({effect_interpretation})"
|
|
797
|
+
|
|
798
|
+
return interpretation
|
|
799
|
+
|
|
800
|
+
def _apply_multiple_comparison_correction(
|
|
801
|
+
self, results: List[StatisticalTestResult]
|
|
802
|
+
) -> List[StatisticalTestResult]:
|
|
803
|
+
"""Apply multiple comparison correction to p-values."""
|
|
804
|
+
|
|
805
|
+
p_values = [result.p_value for result in results]
|
|
806
|
+
|
|
807
|
+
# Apply correction
|
|
808
|
+
rejected, p_adjusted, alpha_sidak, alpha_bonf = multipletests(
|
|
809
|
+
p_values, alpha=self.alpha, method=self.multiple_correction_method
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
# Update results with adjusted p-values
|
|
813
|
+
for i, result in enumerate(results):
|
|
814
|
+
result.p_value_adjusted = p_adjusted[i]
|
|
815
|
+
|
|
816
|
+
logger.info(
|
|
817
|
+
f"Applied {self.multiple_correction_method} correction to {len(results)} tests"
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
return results
|
|
821
|
+
|
|
822
|
+
def conduct_bayesian_analysis(
|
|
823
|
+
self, pipelines_data: Dict[str, List[float]], metric_name: str
|
|
824
|
+
) -> Dict[str, Any]:
|
|
825
|
+
"""
|
|
826
|
+
Conduct Bayesian analysis for pipeline comparison.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
pipelines_data: Dictionary mapping pipeline names to metric scores
|
|
830
|
+
metric_name: Name of the metric being analyzed
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
Bayesian analysis results including credible intervals and probabilities
|
|
834
|
+
"""
|
|
835
|
+
if not BAYESIAN_AVAILABLE:
|
|
836
|
+
logger.warning("Bayesian analysis not available - PyMC not installed")
|
|
837
|
+
return {}
|
|
838
|
+
|
|
839
|
+
logger.info(f"Conducting Bayesian analysis for {metric_name}")
|
|
840
|
+
|
|
841
|
+
try:
|
|
842
|
+
pipeline_names = list(pipelines_data.keys())
|
|
843
|
+
|
|
844
|
+
# Prepare data
|
|
845
|
+
all_scores = []
|
|
846
|
+
group_indices = []
|
|
847
|
+
|
|
848
|
+
for i, (pipeline_name, scores) in enumerate(pipelines_data.items()):
|
|
849
|
+
clean_scores = [x for x in scores if np.isfinite(x)]
|
|
850
|
+
all_scores.extend(clean_scores)
|
|
851
|
+
group_indices.extend([i] * len(clean_scores))
|
|
852
|
+
|
|
853
|
+
all_scores = np.array(all_scores)
|
|
854
|
+
group_indices = np.array(group_indices)
|
|
855
|
+
n_groups = len(pipeline_names)
|
|
856
|
+
|
|
857
|
+
# Bayesian model
|
|
858
|
+
with pm.Model() as model:
|
|
859
|
+
# Priors
|
|
860
|
+
mu = pm.Normal(
|
|
861
|
+
"mu",
|
|
862
|
+
mu=np.mean(all_scores),
|
|
863
|
+
sigma=np.std(all_scores),
|
|
864
|
+
shape=n_groups,
|
|
865
|
+
)
|
|
866
|
+
sigma = pm.HalfNormal("sigma", sigma=np.std(all_scores))
|
|
867
|
+
|
|
868
|
+
# Likelihood
|
|
869
|
+
likelihood = pm.Normal(
|
|
870
|
+
"likelihood", mu=mu[group_indices], sigma=sigma, observed=all_scores
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
# Sample from posterior
|
|
874
|
+
trace = pm.sample(
|
|
875
|
+
draws=self.config["bayesian_draws"],
|
|
876
|
+
tune=self.config["bayesian_tune"],
|
|
877
|
+
chains=self.config["bayesian_chains"],
|
|
878
|
+
return_inferencedata=True,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
# Extract results
|
|
882
|
+
posterior_means = az.summary(trace, var_names=["mu"])
|
|
883
|
+
|
|
884
|
+
# Calculate probabilities of superiority
|
|
885
|
+
mu_samples = trace.posterior["mu"].values.reshape(-1, n_groups)
|
|
886
|
+
|
|
887
|
+
superiority_probs = {}
|
|
888
|
+
for i, pipeline1 in enumerate(pipeline_names):
|
|
889
|
+
for j, pipeline2 in enumerate(pipeline_names):
|
|
890
|
+
if i != j:
|
|
891
|
+
prob = np.mean(mu_samples[:, i] > mu_samples[:, j])
|
|
892
|
+
superiority_probs[f"{pipeline1}_better_than_{pipeline2}"] = prob
|
|
893
|
+
|
|
894
|
+
# Credible intervals
|
|
895
|
+
credible_intervals = {}
|
|
896
|
+
for i, pipeline_name in enumerate(pipeline_names):
|
|
897
|
+
hdi = az.hdi(trace, var_names=["mu"], hdi_prob=0.95)
|
|
898
|
+
credible_intervals[pipeline_name] = (
|
|
899
|
+
float(hdi["mu"].values[i, 0]),
|
|
900
|
+
float(hdi["mu"].values[i, 1]),
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
"pipeline_names": pipeline_names,
|
|
905
|
+
"posterior_means": posterior_means.to_dict(),
|
|
906
|
+
"superiority_probabilities": superiority_probs,
|
|
907
|
+
"credible_intervals": credible_intervals,
|
|
908
|
+
"model_summary": str(az.summary(trace)),
|
|
909
|
+
"convergence_diagnostics": {
|
|
910
|
+
"r_hat": az.rhat(trace).to_dict(),
|
|
911
|
+
"ess": az.ess(trace).to_dict(),
|
|
912
|
+
},
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
except Exception as e:
|
|
916
|
+
logger.error(f"Bayesian analysis failed: {e}")
|
|
917
|
+
return {"error": str(e)}
|
|
918
|
+
|
|
919
|
+
def generate_statistical_report(
|
|
920
|
+
self,
|
|
921
|
+
results: List[StatisticalTestResult],
|
|
922
|
+
power_analysis: Optional[PowerAnalysisResult] = None,
|
|
923
|
+
bayesian_results: Optional[Dict[str, Any]] = None,
|
|
924
|
+
) -> Dict[str, Any]:
|
|
925
|
+
"""Generate comprehensive statistical report."""
|
|
926
|
+
|
|
927
|
+
report = {
|
|
928
|
+
"timestamp": pd.Timestamp.now().isoformat(),
|
|
929
|
+
"configuration": self.config,
|
|
930
|
+
"summary": {
|
|
931
|
+
"total_tests": len(results),
|
|
932
|
+
"significant_tests": sum(
|
|
933
|
+
1 for r in results if (r.p_value_adjusted or r.p_value) < self.alpha
|
|
934
|
+
),
|
|
935
|
+
"large_effects": sum(1 for r in results if abs(r.effect_size) > 0.8),
|
|
936
|
+
},
|
|
937
|
+
"test_results": [
|
|
938
|
+
{
|
|
939
|
+
"test_name": r.test_name,
|
|
940
|
+
"test_statistic": r.test_statistic,
|
|
941
|
+
"p_value": r.p_value,
|
|
942
|
+
"p_value_adjusted": r.p_value_adjusted,
|
|
943
|
+
"effect_size": r.effect_size,
|
|
944
|
+
"effect_size_type": r.effect_size_type,
|
|
945
|
+
"effect_size_interpretation": r.effect_size_interpretation,
|
|
946
|
+
"confidence_interval": r.confidence_interval,
|
|
947
|
+
"power": r.power,
|
|
948
|
+
"sample_sizes": r.sample_sizes,
|
|
949
|
+
"assumptions_met": r.assumptions_met,
|
|
950
|
+
"interpretation": r.interpretation,
|
|
951
|
+
}
|
|
952
|
+
for r in results
|
|
953
|
+
],
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if power_analysis:
|
|
957
|
+
report["power_analysis"] = {
|
|
958
|
+
"effect_size": power_analysis.effect_size,
|
|
959
|
+
"power": power_analysis.power,
|
|
960
|
+
"sample_size": power_analysis.sample_size,
|
|
961
|
+
"alpha": power_analysis.alpha,
|
|
962
|
+
"test_type": power_analysis.test_type,
|
|
963
|
+
"recommendations": power_analysis.recommendations,
|
|
964
|
+
"sensitivity_analysis": power_analysis.sensitivity_analysis,
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if bayesian_results:
|
|
968
|
+
report["bayesian_analysis"] = bayesian_results
|
|
969
|
+
|
|
970
|
+
return report
|
|
971
|
+
|
|
972
|
+
def save_statistical_results(self, report: Dict[str, Any], output_path: str):
|
|
973
|
+
"""Save statistical analysis results."""
|
|
974
|
+
output_dir = Path(output_path)
|
|
975
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
976
|
+
|
|
977
|
+
timestamp = pd.Timestamp.now().strftime("%Y%m%d_%H%M%S")
|
|
978
|
+
filename = f"statistical_analysis_report_{timestamp}.json"
|
|
979
|
+
|
|
980
|
+
filepath = output_dir / filename
|
|
981
|
+
with open(filepath, "w") as f:
|
|
982
|
+
json.dump(report, f, indent=2, default=str)
|
|
983
|
+
|
|
984
|
+
logger.info(f"Statistical analysis report saved to {filepath}")
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def create_statistical_framework(
|
|
988
|
+
config: Optional[Dict[str, Any]] = None,
|
|
989
|
+
) -> StatisticalEvaluationFramework:
|
|
990
|
+
"""Factory function to create a configured statistical evaluation framework."""
|
|
991
|
+
return StatisticalEvaluationFramework(config)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
if __name__ == "__main__":
|
|
995
|
+
# Example usage
|
|
996
|
+
framework = create_statistical_framework()
|
|
997
|
+
|
|
998
|
+
# Mock pipeline data
|
|
999
|
+
sample_data = {
|
|
1000
|
+
"BasicRAG": [0.7, 0.8, 0.75, 0.82, 0.78],
|
|
1001
|
+
"CRAG": [0.85, 0.87, 0.83, 0.89, 0.86],
|
|
1002
|
+
"GraphRAG": [0.72, 0.74, 0.71, 0.76, 0.73],
|
|
1003
|
+
"BasicRAGReranking": [0.81, 0.84, 0.80, 0.83, 0.82],
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
# Power analysis
|
|
1007
|
+
power_result = framework.conduct_power_analysis(sample_data, "faithfulness")
|
|
1008
|
+
|
|
1009
|
+
# Statistical comparison
|
|
1010
|
+
comparison_results = framework.compare_pipelines_statistical(
|
|
1011
|
+
sample_data, "faithfulness"
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
# Generate report
|
|
1015
|
+
report = framework.generate_statistical_report(comparison_results, power_result)
|
|
1016
|
+
framework.save_statistical_results(report, "output/statistical_analysis")
|