Spaces:
Sleeping
Sleeping
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
""" | |
Скрипт для визуализации агрегированных результатов тестирования RAG. | |
Читает данные из Excel-файла, сгенерированного aggregate_results.py, | |
и строит различные графики для анализа влияния параметров на метрики. | |
""" | |
import argparse | |
import json | |
import os | |
import matplotlib.pyplot as plt | |
import pandas as pd | |
import seaborn as sns | |
# --- Настройки --- | |
DEFAULT_RESULTS_FILE = "data/output/aggregated_results.xlsx" # Файл с агрегированными данными | |
DEFAULT_PLOTS_DIR = "data/output/plots" # Куда сохранять графики | |
# Настройки графиков | |
plt.rcParams['font.family'] = 'DejaVu Sans' # Шрифт с поддержкой кириллицы | |
sns.set_style("whitegrid") | |
FIGSIZE = (16, 10) # Увеличенный размер для сложных графиков | |
DPI = 300 | |
PALETTE = "viridis" # Цветовая палитра | |
# --- Маппинг названий столбцов (копия из aggregate_results.py) --- | |
COLUMN_NAME_MAPPING = { | |
# Параметры запуска из pipeline.py | |
'run_id': 'ID Запуска', | |
'model_name': 'Модель', | |
'chunking_strategy': 'Стратегия Чанкинга', | |
'strategy_params': 'Параметры Стратегии', | |
'process_tables': 'Обраб. Таблиц', | |
'top_n': 'Top N', | |
'use_injection': 'Сборка Контекста', | |
'use_qe': 'Query Expansion', | |
'neighbors_included': 'Вкл. Соседей', | |
'similarity_threshold': 'Порог Схожести', | |
# Идентификаторы из датасета (для детальных результатов) | |
'question_id': 'ID Вопроса', | |
'question_text': 'Текст Вопроса', | |
# Детальные метрики из pipeline.py | |
'chunk_text_precision': 'Точность (Чанк-Текст)', | |
'chunk_text_recall': 'Полнота (Чанк-Текст)', | |
'chunk_text_f1': 'F1 (Чанк-Текст)', | |
'found_puncts': 'Найдено Пунктов', | |
'total_puncts': 'Всего Пунктов', | |
'relevant_chunks': 'Релевантных Чанков', | |
'total_chunks_in_top_n': 'Всего Чанков в Топ-N', | |
'assembly_punct_recall': 'Полнота (Сборка-Пункт)', | |
'assembled_context_preview': 'Предпросмотр Сборки', | |
# 'top_chunk_ids': 'Индексы Топ-Чанков', # Списки, могут плохо отображаться | |
# 'top_chunk_similarities': 'Схожести Топ-Чанков', # Списки | |
# Агрегированные метрики (добавляются в calculate_aggregated_metrics) | |
'weighted_chunk_text_precision': 'Weighted Точность (Чанк-Текст)', | |
'weighted_chunk_text_recall': 'Weighted Полнота (Чанк-Текст)', | |
'weighted_chunk_text_f1': 'Weighted F1 (Чанк-Текст)', | |
'weighted_assembly_punct_recall': 'Weighted Полнота (Сборка-Пункт)', | |
'macro_chunk_text_precision': 'Macro Точность (Чанк-Текст)', | |
'macro_chunk_text_recall': 'Macro Полнота (Чанк-Текст)', | |
'macro_chunk_text_f1': 'Macro F1 (Чанк-Текст)', | |
'macro_assembly_punct_recall': 'Macro Полнота (Сборка-Пункт)', | |
'micro_text_precision': 'Micro Точность (Текст)', | |
'micro_text_recall': 'Micro Полнота (Текст)', | |
'micro_text_f1': 'Micro F1 (Текст)', | |
} | |
# --- Конец маппинга --- | |
def parse_args(): | |
"""Парсит аргументы командной строки.""" | |
parser = argparse.ArgumentParser(description="Визуализация результатов тестирования RAG") | |
parser.add_argument("--results-file", type=str, default=DEFAULT_RESULTS_FILE, | |
help=f"Путь к Excel-файлу с агрегированными результатами (по умолчанию: {DEFAULT_RESULTS_FILE})") | |
parser.add_argument("--plots-dir", type=str, default=DEFAULT_PLOTS_DIR, | |
help=f"Директория для сохранения графиков (по умолчанию: {DEFAULT_PLOTS_DIR})") | |
parser.add_argument("--sheet-name", type=str, default="Агрегированные метрики", | |
help="Название листа в Excel-файле для чтения данных") | |
return parser.parse_args() | |
def setup_plots_directory(plots_dir: str) -> None: | |
"""Создает директорию для графиков, если она не существует.""" | |
if not os.path.exists(plots_dir): | |
os.makedirs(plots_dir) | |
print(f"Создана директория для графиков: {plots_dir}") | |
else: | |
print(f"Использование существующей директории для графиков: {plots_dir}") | |
def load_aggregated_data(file_path: str, sheet_name: str) -> pd.DataFrame: | |
"""Загружает данные из указанного листа Excel-файла.""" | |
print(f"Загрузка данных из файла: {file_path}, лист: {sheet_name}") | |
try: | |
df = pd.read_excel(file_path, sheet_name=sheet_name) | |
print(f"Загружено {len(df)} строк.") | |
print(f"Колонки: {df.columns.tolist()}") | |
# Добавим проверку на необходимые колонки (РУССКИЕ НАЗВАНИЯ) | |
required_cols_rus = [ | |
COLUMN_NAME_MAPPING['model_name'], COLUMN_NAME_MAPPING['chunking_strategy'], | |
COLUMN_NAME_MAPPING['strategy_params'], COLUMN_NAME_MAPPING['process_tables'], | |
COLUMN_NAME_MAPPING['top_n'], COLUMN_NAME_MAPPING['use_injection'], | |
COLUMN_NAME_MAPPING['use_qe'], COLUMN_NAME_MAPPING['neighbors_included'], | |
COLUMN_NAME_MAPPING['similarity_threshold'] | |
] | |
# Проверяем только те, что есть в маппинге | |
missing_required = [col for col in required_cols_rus if col not in df.columns] | |
if missing_required: | |
print(f"Предупреждение: Не все ожидаемые колонки параметров найдены в данных: {missing_required}") | |
# --- Добавим парсинг strategy_params из JSON строки в словарь --- | |
params_col = COLUMN_NAME_MAPPING['strategy_params'] | |
if params_col in df.columns: | |
def safe_json_loads(x): | |
try: | |
# Обработка NaN и пустых строк | |
if pd.isna(x) or not isinstance(x, str) or not x.strip(): | |
return {} | |
return json.loads(x) | |
except (json.JSONDecodeError, TypeError): | |
return {} # Возвращаем пустой словарь при ошибке | |
df[params_col] = df[params_col].apply(safe_json_loads) | |
# Создаем строковое представление для группировки и лейблов | |
df[f"{params_col}_str"] = df[params_col].apply( | |
lambda d: json.dumps(d, sort_keys=True, ensure_ascii=False) | |
) | |
print(f"Колонка '{params_col}' преобразована из JSON строк.") | |
# -------------------------------------------------------------- | |
return df | |
except FileNotFoundError: | |
print(f"Ошибка: Файл не найден: {file_path}") | |
return pd.DataFrame() | |
except ValueError as e: | |
print(f"Ошибка: Лист '{sheet_name}' не найден в файле {file_path}. Доступные листы: {pd.ExcelFile(file_path).sheet_names}") | |
return pd.DataFrame() | |
except Exception as e: | |
print(f"Ошибка при чтении Excel файла: {e}") | |
return pd.DataFrame() | |
# --- Функции построения графиков --- # | |
def plot_metric_vs_top_n( | |
df: pd.DataFrame, | |
metric_name_rus: str, # Ожидаем русское имя метрики | |
fixed_strategy: str | None, | |
fixed_strategy_params: str | None, # Ожидаем строку JSON или None | |
plots_dir: str | |
) -> None: | |
""" | |
Строит график зависимости метрики от top_n для разных моделей | |
(при фиксированных параметрах чанкинга). | |
Разделяет линии по значению use_injection. | |
Использует русские названия колонок. | |
""" | |
# Используем русские названия колонок из маппинга | |
metric_col_rus = metric_name_rus # Передаем уже готовое русское имя | |
top_n_col_rus = COLUMN_NAME_MAPPING['top_n'] | |
model_col_rus = COLUMN_NAME_MAPPING['model_name'] | |
injection_col_rus = COLUMN_NAME_MAPPING['use_injection'] | |
strategy_col_rus = COLUMN_NAME_MAPPING['chunking_strategy'] | |
params_str_col_rus = f"{COLUMN_NAME_MAPPING['strategy_params']}_str" # Используем строковое представление | |
if metric_col_rus not in df.columns: | |
print(f"График пропущен: Колонка '{metric_col_rus}' не найдена.") | |
return | |
plot_df = df.copy() | |
# Фильтруем по параметрам чанкинга, если задано | |
chunk_suffix = "all_strategies_all_params" | |
if fixed_strategy and strategy_col_rus in plot_df.columns: | |
plot_df = plot_df[plot_df[strategy_col_rus] == fixed_strategy] | |
chunk_suffix = f"strategy_{fixed_strategy}" | |
# Фильтруем по строковому представлению параметров | |
if fixed_strategy_params and params_str_col_rus in plot_df.columns: | |
plot_df = plot_df[plot_df[params_str_col_rus] == fixed_strategy_params] | |
# Генерируем короткий хэш для параметров в названии файла | |
params_hash = hash(fixed_strategy_params) # Хэш от строки | |
chunk_suffix += f"_params-{params_hash:x}" # Hex hash | |
if plot_df.empty: | |
print(f"График Metric vs Top-N пропущен: Нет данных для strategy={fixed_strategy}, params={fixed_strategy_params}") | |
return | |
plt.figure(figsize=FIGSIZE) | |
sns.lineplot( | |
data=plot_df, | |
x=top_n_col_rus, | |
y=metric_col_rus, | |
hue=model_col_rus, | |
style=injection_col_rus, # Разные стили линий для True/False | |
markers=True, | |
markersize=8, | |
linewidth=2, | |
palette=PALETTE | |
) | |
plt.title(f"Зависимость {metric_col_rus} от top_n ({chunk_suffix})") | |
plt.xlabel("Top N") | |
plt.ylabel(metric_col_rus.replace("_", " ").title()) | |
plt.legend(title="Модель / Сборка", bbox_to_anchor=(1.05, 1), loc='upper left') | |
plt.grid(True, linestyle='--', alpha=0.7) | |
plt.tight_layout(rect=[0, 0, 0.85, 1]) # Оставляем место для легенды | |
filename = f"plot_{metric_col_rus.replace(' ', '_').replace('(', '').replace(')', '')}_vs_top_n_{chunk_suffix}.png" | |
filepath = os.path.join(plots_dir, filename) | |
plt.savefig(filepath, dpi=DPI) | |
plt.close() | |
print(f"Создан график: {filepath}") | |
def plot_injection_comparison( | |
df: pd.DataFrame, | |
metric_name_rus: str, # Ожидаем русское имя метрики | |
plots_dir: str | |
) -> None: | |
""" | |
Сравнивает метрики с использованием и без использования сборки контекста | |
в виде парных столбчатых диаграмм для разных моделей и параметров чанкинга. | |
Использует русские названия колонок. | |
""" | |
# Русские названия колонок | |
metric_col_rus = metric_name_rus | |
injection_col_rus = COLUMN_NAME_MAPPING['use_injection'] | |
model_col_rus = COLUMN_NAME_MAPPING['model_name'] | |
strategy_col_rus = COLUMN_NAME_MAPPING['chunking_strategy'] | |
params_str_col_rus = f"{COLUMN_NAME_MAPPING['strategy_params']}_str" | |
tables_col_rus = COLUMN_NAME_MAPPING['process_tables'] | |
qe_col_rus = COLUMN_NAME_MAPPING['use_qe'] | |
neighbors_col_rus = COLUMN_NAME_MAPPING['neighbors_included'] | |
top_n_col_rus = COLUMN_NAME_MAPPING['top_n'] | |
threshold_col_rus = COLUMN_NAME_MAPPING['similarity_threshold'] | |
if metric_col_rus not in df.columns or injection_col_rus not in df.columns: | |
print(f"График сравнения сборки пропущен: Колонки '{metric_col_rus}' или '{injection_col_rus}' не найдены.") | |
return | |
plot_df = df.copy() | |
# Используем русские названия при создании лейбла | |
plot_df['config_label'] = plot_df.apply( | |
lambda r: ( | |
f"{r.get(model_col_rus, 'N/A')}\n" | |
f"Стратегия: {r.get(strategy_col_rus, 'N/A')}\n" | |
# Используем строковое представление параметров | |
f"Параметры: {r.get(params_str_col_rus, '{}')[:30]}...\n" | |
f"Табл: {r.get(tables_col_rus, 'N/A')}, QE: {r.get(qe_col_rus, 'N/A')}, Соседи: {r.get(neighbors_col_rus, 'N/A')}\n" | |
f"TopN: {int(r.get(top_n_col_rus, 0))}, Порог: {r.get(threshold_col_rus, 0):.2f}" | |
), | |
axis=1 | |
) | |
# Оставляем только строки, где есть и True, и False для данного флага | |
# Группируем по config_label, считаем уникальные значения флага use_injection | |
counts = plot_df.groupby('config_label')[injection_col_rus].nunique() | |
configs_with_both = counts[counts >= 2].index # Используем >= 2 на случай дубликатов | |
plot_df = plot_df[plot_df['config_label'].isin(configs_with_both)] | |
if plot_df.empty: | |
print(f"График сравнения сборки пропущен: Нет конфигураций с обоими вариантами {injection_col_rus}.") | |
return | |
# Ограничим количество конфигураций для читаемости (по средней метрике) | |
top_configs = plot_df.groupby('config_label')[metric_col_rus].mean().nlargest(10).index # Уменьшил до 10 | |
plot_df = plot_df[plot_df['config_label'].isin(top_configs)] | |
if plot_df.empty: | |
print(f"График сравнения сборки пропущен: Не осталось данных после фильтрации топ-конфигураций.") | |
return | |
plt.figure(figsize=(FIGSIZE[0]*0.9, FIGSIZE[1]*0.7)) # Уменьшил размер | |
sns.barplot( | |
data=plot_df, | |
x='config_label', | |
y=metric_col_rus, | |
hue=injection_col_rus, | |
palette=PALETTE | |
) | |
plt.title(f"Сравнение {metric_col_rus} с/без {injection_col_rus}") | |
plt.xlabel("Конфигурация") | |
plt.ylabel(metric_col_rus) | |
plt.xticks(rotation=60, ha='right', fontsize=8) # Уменьшил шрифт, увеличил поворот | |
plt.legend(title=injection_col_rus) | |
plt.grid(True, axis='y', linestyle='--', alpha=0.7) | |
plt.tight_layout() | |
filename = f"plot_{metric_col_rus.replace(' ', '_').replace('(', '').replace(')', '')}_injection_comparison.png" | |
filepath = os.path.join(plots_dir, filename) | |
plt.savefig(filepath, dpi=DPI) | |
plt.close() | |
print(f"Создан график: {filepath}") | |
# --- Новая функция для сравнения булевых флагов --- | |
def plot_boolean_flag_comparison( | |
df: pd.DataFrame, | |
metric_name_rus: str, # Ожидаем русское имя метрики | |
flag_column_eng: str, # Ожидаем английское имя флага для поиска в маппинге | |
plots_dir: str | |
) -> None: | |
""" | |
Сравнивает метрики при True/False значениях указанного булева флага | |
в виде парных столбчатых диаграмм для разных конфигураций. | |
Использует русские названия колонок. | |
""" | |
# Русские названия колонок | |
metric_col_rus = metric_name_rus | |
try: | |
flag_col_rus = COLUMN_NAME_MAPPING[flag_column_eng] | |
except KeyError: | |
print(f"Ошибка: Английское имя флага '{flag_column_eng}' не найдено в COLUMN_NAME_MAPPING.") | |
return | |
model_col_rus = COLUMN_NAME_MAPPING['model_name'] | |
strategy_col_rus = COLUMN_NAME_MAPPING['chunking_strategy'] | |
params_str_col_rus = f"{COLUMN_NAME_MAPPING['strategy_params']}_str" | |
injection_col_rus = COLUMN_NAME_MAPPING['use_injection'] | |
top_n_col_rus = COLUMN_NAME_MAPPING['top_n'] | |
# Другие флаги | |
tables_col_rus = COLUMN_NAME_MAPPING['process_tables'] | |
qe_col_rus = COLUMN_NAME_MAPPING['use_qe'] | |
neighbors_col_rus = COLUMN_NAME_MAPPING['neighbors_included'] | |
if metric_col_rus not in df.columns or flag_col_rus not in df.columns: | |
print(f"График сравнения флага '{flag_col_rus}' пропущен: Колонки '{metric_col_rus}' или '{flag_col_rus}' не найдены.") | |
return | |
plot_df = df.copy() | |
# Создаем обобщенный лейбл конфигурации, исключая сам флаг | |
plot_df['config_label'] = plot_df.apply( | |
lambda r: ( | |
f"{r.get(model_col_rus, 'N/A')}\n" | |
f"Стратегия: {r.get(strategy_col_rus, 'N/A')} Параметры: {r.get(params_str_col_rus, '{}')[:20]}...\n" | |
f"Сборка: {r.get(injection_col_rus, 'N/A')}, TopN: {int(r.get(top_n_col_rus, 0))}" | |
# Динамически добавляем другие флаги, кроме сравниваемого | |
+ (f", Табл: {r.get(tables_col_rus, 'N/A')}" if flag_col_rus != tables_col_rus else "") | |
+ (f", QE: {r.get(qe_col_rus, 'N/A')}" if flag_col_rus != qe_col_rus else "") | |
+ (f", Соседи: {r.get(neighbors_col_rus, 'N/A')}" if flag_col_rus != neighbors_col_rus else "") | |
), | |
axis=1 | |
) | |
# Оставляем только строки, где есть и True, и False для данного флага | |
counts = plot_df.groupby('config_label')[flag_col_rus].nunique() | |
configs_with_both = counts[counts >= 2].index # Используем >= 2 | |
plot_df = plot_df[plot_df['config_label'].isin(configs_with_both)] | |
if plot_df.empty: | |
print(f"График сравнения флага '{flag_col_rus}' пропущен: Нет конфигураций с обоими вариантами {flag_col_rus}.") | |
return | |
# Ограничим количество конфигураций для читаемости (по средней метрике) | |
top_configs = plot_df.groupby('config_label')[metric_col_rus].mean().nlargest(10).index # Уменьшил до 10 | |
plot_df = plot_df[plot_df['config_label'].isin(top_configs)] | |
if plot_df.empty: | |
print(f"График сравнения флага '{flag_col_rus}' пропущен: Не осталось данных после фильтрации топ-конфигураций.") | |
return | |
plt.figure(figsize=(FIGSIZE[0]*0.9, FIGSIZE[1]*0.7)) # Уменьшил размер | |
sns.barplot( | |
data=plot_df, | |
x='config_label', | |
y=metric_col_rus, | |
hue=flag_col_rus, | |
palette=PALETTE | |
) | |
plt.title(f"Сравнение {metric_col_rus} в зависимости от '{flag_col_rus}'") | |
plt.xlabel("Конфигурация") | |
plt.ylabel(metric_col_rus) | |
plt.xticks(rotation=60, ha='right', fontsize=8) # Уменьшил шрифт, увеличил поворот | |
plt.legend(title=f"{flag_col_rus}") | |
plt.grid(True, axis='y', linestyle='--', alpha=0.7) | |
plt.tight_layout() | |
filename = f"plot_{metric_col_rus.replace(' ', '_').replace('(', '').replace(')', '')}_{flag_column_eng}_comparison.png" | |
filepath = os.path.join(plots_dir, filename) | |
plt.savefig(filepath, dpi=DPI) | |
plt.close() | |
print(f"Создан график: {filepath}") | |
# --- Основная функция --- | |
def main(): | |
"""Основная функция скрипта.""" | |
args = parse_args() | |
setup_plots_directory(args.plots_dir) | |
df = load_aggregated_data(args.results_file, args.sheet_name) | |
if df.empty: | |
print("Нет данных для построения графиков. Завершение.") | |
return | |
# Определяем метрики для построения графиков (используем английские ключи для поиска русских имен) | |
metric_keys = [ | |
'weighted_chunk_text_recall', 'weighted_chunk_text_f1', 'weighted_assembly_punct_recall', | |
'macro_chunk_text_recall', 'macro_chunk_text_f1', 'macro_assembly_punct_recall', | |
'micro_text_recall', 'micro_text_f1' | |
] | |
# Получаем существующие русские имена метрик в DataFrame | |
existing_metrics_rus = [COLUMN_NAME_MAPPING.get(key) for key in metric_keys if COLUMN_NAME_MAPPING.get(key) in df.columns] | |
# Определяем фиксированные параметры для некоторых графиков | |
strategy_col_rus = COLUMN_NAME_MAPPING.get('chunking_strategy') | |
params_str_col_rus = f"{COLUMN_NAME_MAPPING.get('strategy_params')}_str" | |
model_col_rus = COLUMN_NAME_MAPPING.get('model_name') | |
fixed_strategy_example = df[strategy_col_rus].unique()[0] if strategy_col_rus in df.columns and len(df[strategy_col_rus].unique()) > 0 else None | |
fixed_strategy_params_example = None | |
if fixed_strategy_example and params_str_col_rus in df.columns: | |
params_list = df[df[strategy_col_rus] == fixed_strategy_example][params_str_col_rus].unique() | |
if len(params_list) > 0: | |
fixed_strategy_params_example = params_list[0] | |
fixed_model_example = df[model_col_rus].unique()[0] if model_col_rus in df.columns and len(df[model_col_rus].unique()) > 0 else None | |
fixed_top_n_example = 20 | |
print("--- Построение графиков ---") | |
# 1. Графики Metric vs Top-N | |
print("\n1. Зависимость метрик от Top-N:") | |
for metric_name_rus in existing_metrics_rus: | |
# Проверяем, что метрика не micro (у micro нет зависимости от top_n) | |
if 'Micro' in metric_name_rus: | |
continue | |
plot_metric_vs_top_n( | |
df, metric_name_rus, | |
fixed_strategy_example, fixed_strategy_params_example, | |
args.plots_dir | |
) | |
# 2. Графики Metric vs Chunking | |
print("\n2. Зависимость метрик от параметров чанкинга: [Пропущено - требует переосмысления]") | |
# plot_metric_vs_chunking(...) # Закомментировано | |
# 3. Графики сравнения Use Injection | |
print("\n3. Сравнение метрик с/без сборки контекста:") | |
for metric_name_rus in existing_metrics_rus: | |
plot_injection_comparison(df, metric_name_rus, args.plots_dir) | |
# 4. Графики сравнения других булевых флагов | |
boolean_flags_eng = ['process_tables', 'use_qe', 'neighbors_included'] | |
print("\n4. Сравнение метрик в зависимости от булевых флагов:") | |
for flag_eng in boolean_flags_eng: | |
flag_rus = COLUMN_NAME_MAPPING.get(flag_eng) | |
if not flag_rus or flag_rus not in df.columns: | |
print(f" Пропуск сравнения для флага: '{flag_eng}' (колонка '{flag_rus}' не найдена)") | |
continue | |
print(f" Сравнение для флага: '{flag_rus}'") | |
for metric_name_rus in existing_metrics_rus: | |
plot_boolean_flag_comparison(df, metric_name_rus, flag_eng, args.plots_dir) | |
print("\n--- Построение графиков завершено ---") | |
if __name__ == "__main__": | |
main() |