Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python | |
| """ | |
| Скрипт для объединения результатов всех экспериментов в одну Excel-таблицу с форматированием. | |
| Анализирует результаты экспериментов и создает сводную таблицу с метриками в различных разрезах. | |
| Также строит графики через seaborn и сохраняет их в отдельную директорию. | |
| """ | |
| import argparse | |
| import glob | |
| import os | |
| import matplotlib.pyplot as plt | |
| import pandas as pd | |
| import seaborn as sns | |
| from openpyxl import Workbook | |
| from openpyxl.styles import Alignment, Border, Font, PatternFill, Side | |
| from openpyxl.utils import get_column_letter | |
| from openpyxl.utils.dataframe import dataframe_to_rows | |
| def setup_plot_directory(plots_dir: str) -> None: | |
| """ | |
| Создает директорию для сохранения графиков, если она не существует. | |
| Args: | |
| plots_dir: Путь к директории для графиков | |
| """ | |
| if not os.path.exists(plots_dir): | |
| os.makedirs(plots_dir) | |
| print(f"Создана директория для графиков: {plots_dir}") | |
| else: | |
| print(f"Директория для графиков: {plots_dir}") | |
| def parse_args(): | |
| """Парсит аргументы командной строки.""" | |
| parser = argparse.ArgumentParser(description="Объединение результатов экспериментов в одну Excel-таблицу") | |
| parser.add_argument("--results-dir", type=str, default="data", | |
| help="Директория с результатами экспериментов (по умолчанию: data)") | |
| parser.add_argument("--output-file", type=str, default="combined_results.xlsx", | |
| help="Путь к выходному Excel-файлу (по умолчанию: combined_results.xlsx)") | |
| parser.add_argument("--plots-dir", type=str, default="plots", | |
| help="Директория для сохранения графиков (по умолчанию: plots)") | |
| return parser.parse_args() | |
| def parse_file_name(file_name: str) -> dict: | |
| """ | |
| Парсит имя файла и извлекает параметры эксперимента. | |
| Args: | |
| file_name: Имя файла для парсинга | |
| Returns: | |
| Словарь с параметрами (words_per_chunk, overlap_words, model) или None при ошибке | |
| """ | |
| try: | |
| # Извлекаем параметры из имени файла | |
| parts = file_name.split('_') | |
| if len(parts) < 4: | |
| return None | |
| # Ищем части с w (words) и o (overlap) | |
| words_part = None | |
| overlap_part = None | |
| for part in parts: | |
| if part.startswith('w') and part[1:].isdigit(): | |
| words_part = part[1:] | |
| elif part.startswith('o') and part[1:].isdigit(): | |
| # Убираем потенциальную часть .csv или .xlsx из overlap_part | |
| overlap_part = part[1:].split('.')[0] | |
| if words_part is None or overlap_part is None: | |
| return None | |
| # Пытаемся извлечь имя модели из оставшейся части имени файла | |
| model_part = file_name.split(f"_w{words_part}_o{overlap_part}_", 1) | |
| if len(model_part) < 2: | |
| return None | |
| # Получаем имя модели и удаляем возможное расширение файла | |
| model_name_parts = model_part[1].split('.') | |
| if len(model_name_parts) > 1: | |
| model_name_parts = model_name_parts[:-1] | |
| model_name_parts = '_'.join(model_name_parts).split('_') | |
| model_name = '/'.join(model_name_parts) | |
| return { | |
| 'words_per_chunk': int(words_part), | |
| 'overlap_words': int(overlap_part), | |
| 'model': model_name, | |
| 'overlap_percentage': round(int(overlap_part) / int(words_part) * 100, 1) | |
| } | |
| except Exception as e: | |
| print(f"Ошибка при парсинге файла {file_name}: {e}") | |
| return None | |
| def load_data_files(results_dir: str, pattern: str, file_type: str, load_function) -> pd.DataFrame: | |
| """ | |
| Общая функция для загрузки файлов данных с определенным паттерном имени. | |
| Args: | |
| results_dir: Директория с результатами | |
| pattern: Glob-паттерн для поиска файлов | |
| file_type: Тип файлов для сообщений (напр. "результатов", "метрик") | |
| load_function: Функция для загрузки конкретного типа файла | |
| Returns: | |
| DataFrame с объединенными данными или None при ошибке | |
| """ | |
| print(f"Загрузка {file_type} из {results_dir}...") | |
| # Ищем все файлы с указанным паттерном | |
| data_files = glob.glob(os.path.join(results_dir, pattern)) | |
| if not data_files: | |
| print(f"В директории {results_dir} не найдены файлы {file_type}") | |
| return None | |
| print(f"Найдено {len(data_files)} файлов {file_type}") | |
| all_data = [] | |
| for file_path in data_files: | |
| # Извлекаем информацию о стратегии и модели из имени файла | |
| file_name = os.path.basename(file_path) | |
| print(f"Обрабатываю файл: {file_name}") | |
| # Парсим параметры из имени файла | |
| params = parse_file_name(file_name) | |
| if params is None: | |
| print(f"Пропуск файла {file_name}: не удалось извлечь параметры") | |
| continue | |
| words_part = params['words_per_chunk'] | |
| overlap_part = params['overlap_words'] | |
| model_name = params['model'] | |
| overlap_percentage = params['overlap_percentage'] | |
| print(f" Параметры: words={words_part}, overlap={overlap_part}, model={model_name}") | |
| try: | |
| # Загружаем данные, используя переданную функцию | |
| df = load_function(file_path) | |
| # Добавляем информацию о стратегии и модели | |
| df['model'] = model_name | |
| df['words_per_chunk'] = words_part | |
| df['overlap_words'] = overlap_part | |
| df['overlap_percentage'] = overlap_percentage | |
| all_data.append(df) | |
| except Exception as e: | |
| print(f"Ошибка при обработке файла {file_path}: {e}") | |
| if not all_data: | |
| print(f"Не удалось загрузить ни один файл {file_type}") | |
| return None | |
| # Объединяем все данные | |
| combined_data = pd.concat(all_data, ignore_index=True) | |
| return combined_data | |
| def load_results_files(results_dir: str) -> pd.DataFrame: | |
| """ | |
| Загружает все файлы результатов из указанной директории. | |
| Args: | |
| results_dir: Директория с результатами | |
| Returns: | |
| DataFrame с объединенными результатами | |
| """ | |
| # Используем общую функцию для загрузки CSV файлов | |
| data = load_data_files( | |
| results_dir, | |
| "results_*.csv", | |
| "результатов", | |
| lambda f: pd.read_csv(f) | |
| ) | |
| if data is None: | |
| raise ValueError("Не удалось загрузить файлы с результатами") | |
| return data | |
| def load_question_metrics_files(results_dir: str) -> pd.DataFrame: | |
| """ | |
| Загружает все файлы с метриками по вопросам из указанной директории. | |
| Args: | |
| results_dir: Директория с результатами | |
| Returns: | |
| DataFrame с объединенными метриками по вопросам или None, если файлов нет | |
| """ | |
| # Используем общую функцию для загрузки Excel файлов | |
| return load_data_files( | |
| results_dir, | |
| "question_metrics_*.xlsx", | |
| "метрик по вопросам", | |
| lambda f: pd.read_excel(f) | |
| ) | |
| def prepare_summary_by_model_top_n(df: pd.DataFrame, macro_metrics: pd.DataFrame = None) -> pd.DataFrame: | |
| """ | |
| Подготавливает сводную таблицу по моделям и top_n значениям. | |
| Если доступны macro метрики, они также включаются в сводную таблицу. | |
| Args: | |
| df: DataFrame с объединенными результатами | |
| macro_metrics: DataFrame с macro метриками (опционально) | |
| Returns: | |
| DataFrame со сводной таблицей | |
| """ | |
| # Определяем группировочные колонки и метрики | |
| group_by_columns = ['model', 'top_n'] | |
| metrics = ['text_precision', 'text_recall', 'text_f1', 'doc_precision', 'doc_recall', 'doc_f1'] | |
| # Используем общую функцию для подготовки сводки | |
| return prepare_summary(df, group_by_columns, metrics, macro_metrics) | |
| def prepare_summary_by_chunking_params_top_n(df: pd.DataFrame, macro_metrics: pd.DataFrame = None) -> pd.DataFrame: | |
| """ | |
| Подготавливает сводную таблицу по параметрам чанкинга и top_n значениям. | |
| Если доступны macro метрики, они также включаются в сводную таблицу. | |
| Args: | |
| df: DataFrame с объединенными результатами | |
| macro_metrics: DataFrame с macro метриками (опционально) | |
| Returns: | |
| DataFrame со сводной таблицей | |
| """ | |
| # Определяем группировочные колонки и метрики | |
| group_by_columns = ['words_per_chunk', 'overlap_words', 'top_n'] | |
| metrics = ['text_precision', 'text_recall', 'text_f1', 'doc_precision', 'doc_recall', 'doc_f1'] | |
| # Используем общую функцию для подготовки сводки | |
| return prepare_summary(df, group_by_columns, metrics, macro_metrics) | |
| def prepare_summary(df: pd.DataFrame, group_by_columns: list, metrics: list, macro_metrics: pd.DataFrame = None) -> pd.DataFrame: | |
| """ | |
| Общая функция для подготовки сводной таблицы по указанным группировочным колонкам. | |
| Если доступны macro метрики, они также включаются в сводную таблицу. | |
| Args: | |
| df: DataFrame с объединенными результатами | |
| group_by_columns: Колонки для группировки | |
| metrics: Список метрик для расчета среднего | |
| macro_metrics: DataFrame с macro метриками (опционально) | |
| Returns: | |
| DataFrame со сводной таблицей | |
| """ | |
| # Группируем по указанным колонкам, вычисляем средние значения метрик | |
| summary = df.groupby(group_by_columns).agg({ | |
| metric: 'mean' for metric in metrics | |
| }).reset_index() | |
| # Если среди группировочных колонок есть 'overlap_words' и 'words_per_chunk', | |
| # добавляем процент перекрытия | |
| if 'overlap_words' in group_by_columns and 'words_per_chunk' in group_by_columns: | |
| summary['overlap_percentage'] = (summary['overlap_words'] / summary['words_per_chunk'] * 100).round(1) | |
| # Если доступны macro метрики, объединяем их с summary | |
| if macro_metrics is not None: | |
| # Преобразуем метрики в macro_метрики | |
| macro_metric_names = [f"macro_{metric}" for metric in metrics] | |
| # Группируем macro метрики по тем же колонкам | |
| macro_summary = macro_metrics.groupby(group_by_columns).agg({ | |
| metric: 'mean' for metric in macro_metric_names | |
| }).reset_index() | |
| # Если нужно, добавляем процент перекрытия для согласованности | |
| if 'overlap_words' in group_by_columns and 'words_per_chunk' in group_by_columns: | |
| macro_summary['overlap_percentage'] = (macro_summary['overlap_words'] / macro_summary['words_per_chunk'] * 100).round(1) | |
| merge_on = group_by_columns + ['overlap_percentage'] | |
| else: | |
| merge_on = group_by_columns | |
| # Объединяем с основной сводкой | |
| summary = pd.merge(summary, macro_summary, on=merge_on, how='left') | |
| # Сортируем по группировочным колонкам | |
| summary = summary.sort_values(group_by_columns) | |
| # Округляем метрики до 4 знаков после запятой | |
| for col in summary.columns: | |
| if any(col.endswith(suffix) for suffix in ['precision', 'recall', 'f1']): | |
| summary[col] = summary[col].round(4) | |
| return summary | |
| def prepare_best_configurations(df: pd.DataFrame, macro_metrics: pd.DataFrame = None) -> pd.DataFrame: | |
| """ | |
| Подготавливает таблицу с лучшими конфигурациями для каждой модели и различных top_n. | |
| Выбирает конфигурацию только на основе macro_text_recall и text_recall (weighted), | |
| игнорируя F1 метрики как менее важные. | |
| Args: | |
| df: DataFrame с объединенными результатами | |
| macro_metrics: DataFrame с macro метриками (опционально) | |
| Returns: | |
| DataFrame с лучшими конфигурациями | |
| """ | |
| # Выбираем ключевые значения top_n | |
| key_top_n = [10, 20, 50, 100] | |
| # Определяем источник метрик и акцентируем только на recall-метриках | |
| if macro_metrics is not None: | |
| print("Выбор лучших конфигураций на основе macro метрик (macro_text_recall)") | |
| metrics_source = macro_metrics | |
| text_recall_metric = 'macro_text_recall' | |
| doc_recall_metric = 'macro_doc_recall' | |
| else: | |
| print("Выбор лучших конфигураций на основе weighted метрик (text_recall)") | |
| metrics_source = df | |
| text_recall_metric = 'text_recall' | |
| doc_recall_metric = 'doc_recall' | |
| # Фильтруем только по ключевым значениям top_n | |
| filtered_df = metrics_source[metrics_source['top_n'].isin(key_top_n)] | |
| # Для каждой модели и top_n находим конфигурацию только с лучшим recall | |
| best_configs = [] | |
| for model in metrics_source['model'].unique(): | |
| for top_n in key_top_n: | |
| model_top_n_df = filtered_df[(filtered_df['model'] == model) & (filtered_df['top_n'] == top_n)] | |
| if len(model_top_n_df) == 0: | |
| continue | |
| # Находим конфигурацию с лучшим text_recall | |
| best_text_recall_idx = model_top_n_df[text_recall_metric].idxmax() | |
| best_text_recall_config = model_top_n_df.loc[best_text_recall_idx].copy() | |
| best_text_recall_config['metric_type'] = 'text_recall' | |
| # Находим конфигурацию с лучшим doc_recall | |
| best_doc_recall_idx = model_top_n_df[doc_recall_metric].idxmax() | |
| best_doc_recall_config = model_top_n_df.loc[best_doc_recall_idx].copy() | |
| best_doc_recall_config['metric_type'] = 'doc_recall' | |
| best_configs.append(best_text_recall_config) | |
| best_configs.append(best_doc_recall_config) | |
| if not best_configs: | |
| return pd.DataFrame() | |
| best_configs_df = pd.DataFrame(best_configs) | |
| # Выбираем и сортируем нужные столбцы | |
| cols_to_keep = ['model', 'top_n', 'metric_type', 'words_per_chunk', 'overlap_words', 'overlap_percentage'] | |
| # Добавляем столбцы метрик в зависимости от того, какие доступны | |
| if macro_metrics is not None: | |
| # Для macro метрик сначала выбираем recall-метрики | |
| recall_cols = [col for col in best_configs_df.columns if col.endswith('recall')] | |
| # Затем добавляем остальные метрики | |
| other_cols = [col for col in best_configs_df.columns if any(col.endswith(m) for m in | |
| ['precision', 'f1']) and col.startswith('macro_')] | |
| metric_cols = recall_cols + other_cols | |
| else: | |
| # Для weighted метрик сначала выбираем recall-метрики | |
| recall_cols = [col for col in best_configs_df.columns if col.endswith('recall')] | |
| # Затем добавляем остальные метрики | |
| other_cols = [col for col in best_configs_df.columns if any(col.endswith(m) for m in | |
| ['precision', 'f1']) and not col.startswith('macro_')] | |
| metric_cols = recall_cols + other_cols | |
| result = best_configs_df[cols_to_keep + metric_cols].sort_values(['model', 'top_n', 'metric_type']) | |
| return result | |
| def get_grouping_columns(sheet) -> dict: | |
| """ | |
| Определяет подходящие колонки для группировки данных на листе. | |
| Args: | |
| sheet: Лист Excel | |
| Returns: | |
| Словарь с данными о группировке или None | |
| """ | |
| # Возможные варианты группировки | |
| grouping_possibilities = [ | |
| {'columns': ['model', 'words_per_chunk', 'overlap_words']}, | |
| {'columns': ['model']}, | |
| {'columns': ['words_per_chunk', 'overlap_words']}, | |
| {'columns': ['top_n']}, | |
| {'columns': ['model', 'top_n', 'metric_type']} | |
| ] | |
| # Для каждого варианта группировки проверяем наличие всех колонок | |
| for grouping in grouping_possibilities: | |
| column_indices = {} | |
| all_columns_present = True | |
| for column_name in grouping['columns']: | |
| column_idx = None | |
| for col_idx, cell in enumerate(sheet[1], start=1): | |
| if cell.value == column_name: | |
| column_idx = col_idx | |
| break | |
| if column_idx is None: | |
| all_columns_present = False | |
| break | |
| else: | |
| column_indices[column_name] = column_idx | |
| if all_columns_present: | |
| return { | |
| 'columns': grouping['columns'], | |
| 'indices': column_indices | |
| } | |
| return None | |
| def apply_header_formatting(sheet): | |
| """ | |
| Применяет форматирование к заголовкам. | |
| Args: | |
| sheet: Лист Excel | |
| """ | |
| # Форматирование заголовков | |
| for cell in sheet[1]: | |
| cell.font = Font(bold=True) | |
| cell.fill = PatternFill(start_color="D9D9D9", end_color="D9D9D9", fill_type="solid") | |
| cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) | |
| def adjust_column_width(sheet): | |
| """ | |
| Настраивает ширину столбцов на основе содержимого. | |
| Args: | |
| sheet: Лист Excel | |
| """ | |
| # Авторазмер столбцов | |
| for column in sheet.columns: | |
| max_length = 0 | |
| column_letter = get_column_letter(column[0].column) | |
| for cell in column: | |
| if cell.value: | |
| try: | |
| if len(str(cell.value)) > max_length: | |
| max_length = len(str(cell.value)) | |
| except: | |
| pass | |
| adjusted_width = (max_length + 2) * 1.1 | |
| sheet.column_dimensions[column_letter].width = adjusted_width | |
| def apply_cell_formatting(sheet): | |
| """ | |
| Применяет форматирование к ячейкам (границы, выравнивание и т.д.). | |
| Args: | |
| sheet: Лист Excel | |
| """ | |
| # Тонкие границы для всех ячеек | |
| thin_border = Border( | |
| left=Side(style='thin'), | |
| right=Side(style='thin'), | |
| top=Side(style='thin'), | |
| bottom=Side(style='thin') | |
| ) | |
| for row in sheet.iter_rows(min_row=1, max_row=sheet.max_row, min_col=1, max_col=sheet.max_column): | |
| for cell in row: | |
| cell.border = thin_border | |
| # Форматирование числовых значений | |
| numeric_columns = [ | |
| 'text_precision', 'text_recall', 'text_f1', | |
| 'doc_precision', 'doc_recall', 'doc_f1', | |
| 'macro_text_precision', 'macro_text_recall', 'macro_text_f1', | |
| 'macro_doc_precision', 'macro_doc_recall', 'macro_doc_f1' | |
| ] | |
| for col_idx, header in enumerate(sheet[1], start=1): | |
| if header.value in numeric_columns or (header.value and str(header.value).endswith(('precision', 'recall', 'f1'))): | |
| for row_idx in range(2, sheet.max_row + 1): | |
| cell = sheet.cell(row=row_idx, column=col_idx) | |
| if isinstance(cell.value, (int, float)): | |
| cell.number_format = '0.0000' | |
| # Выравнивание для всех ячеек | |
| for row in sheet.iter_rows(min_row=2, max_row=sheet.max_row, min_col=1, max_col=sheet.max_column): | |
| for cell in row: | |
| cell.alignment = Alignment(horizontal='center', vertical='center') | |
| def apply_group_formatting(sheet, grouping): | |
| """ | |
| Применяет форматирование к группам строк. | |
| Args: | |
| sheet: Лист Excel | |
| grouping: Словарь с данными о группировке | |
| """ | |
| if not grouping or sheet.max_row <= 1: | |
| return | |
| # Для каждой строки проверяем изменение значений группировочных колонок | |
| last_values = {column: None for column in grouping['columns']} | |
| # Применяем жирную верхнюю границу к первой строке данных | |
| for col_idx in range(1, sheet.max_column + 1): | |
| cell = sheet.cell(row=2, column=col_idx) | |
| cell.border = Border( | |
| left=cell.border.left, | |
| right=cell.border.right, | |
| top=Side(style='thick'), | |
| bottom=cell.border.bottom | |
| ) | |
| for row_idx in range(2, sheet.max_row + 1): | |
| current_values = {} | |
| for column in grouping['columns']: | |
| col_idx = grouping['indices'][column] | |
| current_values[column] = sheet.cell(row=row_idx, column=col_idx).value | |
| # Если значения изменились, добавляем жирные границы | |
| values_changed = False | |
| for column in grouping['columns']: | |
| if current_values[column] != last_values[column]: | |
| values_changed = True | |
| break | |
| if values_changed and row_idx > 2: | |
| # Жирная верхняя граница для текущей строки | |
| for col_idx in range(1, sheet.max_column + 1): | |
| cell = sheet.cell(row=row_idx, column=col_idx) | |
| cell.border = Border( | |
| left=cell.border.left, | |
| right=cell.border.right, | |
| top=Side(style='thick'), | |
| bottom=cell.border.bottom | |
| ) | |
| # Жирная нижняя граница для предыдущей строки | |
| for col_idx in range(1, sheet.max_column + 1): | |
| cell = sheet.cell(row=row_idx-1, column=col_idx) | |
| cell.border = Border( | |
| left=cell.border.left, | |
| right=cell.border.right, | |
| top=cell.border.top, | |
| bottom=Side(style='thick') | |
| ) | |
| # Запоминаем текущие значения для следующей итерации | |
| for column in grouping['columns']: | |
| last_values[column] = current_values[column] | |
| # Добавляем жирную нижнюю границу для последней строки | |
| for col_idx in range(1, sheet.max_column + 1): | |
| cell = sheet.cell(row=sheet.max_row, column=col_idx) | |
| cell.border = Border( | |
| left=cell.border.left, | |
| right=cell.border.right, | |
| top=cell.border.top, | |
| bottom=Side(style='thick') | |
| ) | |
| def apply_formatting(workbook: Workbook) -> None: | |
| """ | |
| Применяет форматирование к Excel-файлу. | |
| Добавляет автофильтры для всех столбцов и улучшает визуальное представление. | |
| Args: | |
| workbook: Workbook-объект openpyxl | |
| """ | |
| for sheet_name in workbook.sheetnames: | |
| sheet = workbook[sheet_name] | |
| # Добавляем автофильтры для всех столбцов | |
| if sheet.max_row > 1: # Проверяем, что в листе есть данные | |
| sheet.auto_filter.ref = sheet.dimensions | |
| # Применяем форматирование | |
| apply_header_formatting(sheet) | |
| adjust_column_width(sheet) | |
| apply_cell_formatting(sheet) | |
| # Определяем группирующие колонки и применяем форматирование к группам | |
| grouping = get_grouping_columns(sheet) | |
| if grouping: | |
| apply_group_formatting(sheet, grouping) | |
| def create_model_comparison_plot(df: pd.DataFrame, metrics: list | str, top_n: int, plots_dir: str) -> None: | |
| """ | |
| Создает график сравнения моделей по указанным метрикам для заданного top_n. | |
| Args: | |
| df: DataFrame с данными | |
| metrics: Список метрик или одна метрика для сравнения | |
| top_n: Значение top_n для фильтрации | |
| plots_dir: Директория для сохранения графиков | |
| """ | |
| if isinstance(metrics, str): | |
| metrics = [metrics] | |
| # Фильтруем данные | |
| filtered_df = df[df['top_n'] == top_n] | |
| if len(filtered_df) == 0: | |
| print(f"Нет данных для top_n={top_n}") | |
| return | |
| # Определяем тип метрик (macro или weighted) | |
| metrics_type = "macro" if metrics[0].startswith("macro_") else "weighted" | |
| # Создаем фигуру с несколькими подграфиками | |
| fig, axes = plt.subplots(1, len(metrics), figsize=(6 * len(metrics), 8)) | |
| # Если только одна метрика, преобразуем axes в список для единообразного обращения | |
| if len(metrics) == 1: | |
| axes = [axes] | |
| # Для каждой метрики создаем subplot | |
| for i, metric in enumerate(metrics): | |
| # Группируем данные по модели | |
| columns_to_agg = {metric: 'mean'} | |
| model_data = filtered_df.groupby('model').agg(columns_to_agg).reset_index() | |
| # Сортируем по значению метрики (по убыванию) | |
| model_data = model_data.sort_values(metric, ascending=False) | |
| # Определяем цветовую схему | |
| palette = sns.color_palette("viridis", len(model_data)) | |
| # Строим столбчатую диаграмму на соответствующем subplot | |
| ax = sns.barplot(x='model', y=metric, data=model_data, palette=palette, ax=axes[i]) | |
| # Добавляем значения над столбцами | |
| for j, v in enumerate(model_data[metric]): | |
| ax.text(j, v + 0.01, f"{v:.4f}", ha='center', fontsize=8) | |
| # Устанавливаем заголовок и метки осей | |
| ax.set_title(f"{metric} (top_n={top_n})", fontsize=12) | |
| ax.set_xlabel("Модель", fontsize=10) | |
| ax.set_ylabel(f"{metric}", fontsize=10) | |
| # Поворачиваем подписи по оси X для лучшей читаемости | |
| ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right', fontsize=8) | |
| # Настраиваем макет | |
| plt.tight_layout() | |
| # Сохраняем график | |
| metric_names = '_'.join([m.replace('macro_', '') for m in metrics]) | |
| file_name = f"model_comparison_{metrics_type}_{metric_names}_top{top_n}.png" | |
| plt.savefig(os.path.join(plots_dir, file_name), dpi=300) | |
| plt.close() | |
| print(f"Создан график сравнения моделей: {file_name}") | |
| def create_top_n_plot(df: pd.DataFrame, models: list | str, metric: str, plots_dir: str) -> None: | |
| """ | |
| Создает график зависимости метрики от top_n для заданных моделей. | |
| Args: | |
| df: DataFrame с данными | |
| models: Список моделей или одна модель для сравнения | |
| metric: Название метрики | |
| plots_dir: Директория для сохранения графиков | |
| """ | |
| if isinstance(models, str): | |
| models = [models] | |
| # Создаем фигуру | |
| plt.figure(figsize=(12, 8)) | |
| # Определяем цветовую схему | |
| palette = sns.color_palette("viridis", len(models)) | |
| # Ограничиваем количество моделей для читаемости | |
| if len(models) > 5: | |
| models = models[:5] | |
| print("Слишком много моделей для графика, ограничиваем до 5") | |
| # Для каждой модели строим линию | |
| for i, model in enumerate(models): | |
| # Находим наиболее часто используемые параметры чанкинга для этой модели | |
| model_df = df[df['model'] == model] | |
| if len(model_df) == 0: | |
| print(f"Нет данных для модели {model}") | |
| continue | |
| # Группируем по параметрам чанкинга и подсчитываем частоту | |
| common_configs = model_df.groupby(['words_per_chunk', 'overlap_words']).size().reset_index(name='count') | |
| if len(common_configs) == 0: | |
| continue | |
| # Берем наиболее частую конфигурацию | |
| common_config = common_configs.sort_values('count', ascending=False).iloc[0] | |
| # Фильтруем для этой конфигурации | |
| config_df = model_df[ | |
| (model_df['words_per_chunk'] == common_config['words_per_chunk']) & | |
| (model_df['overlap_words'] == common_config['overlap_words']) | |
| ].sort_values('top_n') | |
| if len(config_df) <= 1: | |
| continue | |
| # Строим линию | |
| plt.plot(config_df['top_n'], config_df[metric], marker='o', linewidth=2, | |
| label=f"{model} (w={common_config['words_per_chunk']}, o={common_config['overlap_words']})", | |
| color=palette[i]) | |
| # Добавляем легенду, заголовок и метки осей | |
| plt.legend(title="Модель (параметры)", fontsize=10, loc='best') | |
| plt.title(f"Зависимость {metric} от top_n для разных моделей", fontsize=16) | |
| plt.xlabel("top_n", fontsize=14) | |
| plt.ylabel(metric, fontsize=14) | |
| # Включаем сетку | |
| plt.grid(True, linestyle='--', alpha=0.7) | |
| # Настраиваем макет | |
| plt.tight_layout() | |
| # Сохраняем график | |
| is_macro = "macro" if "macro" in metric else "weighted" | |
| file_name = f"top_n_comparison_{is_macro}_{metric.replace('macro_', '')}.png" | |
| plt.savefig(os.path.join(plots_dir, file_name), dpi=300) | |
| plt.close() | |
| print(f"Создан график зависимости от top_n: {file_name}") | |
| def create_chunk_size_plot(df: pd.DataFrame, model: str, metrics: list | str, top_n: int, plots_dir: str) -> None: | |
| """ | |
| Создает график зависимости метрик от размера чанка для заданной модели и top_n. | |
| Args: | |
| df: DataFrame с данными | |
| model: Название модели | |
| metrics: Список метрик или одна метрика | |
| top_n: Значение top_n | |
| plots_dir: Директория для сохранения графиков | |
| """ | |
| if isinstance(metrics, str): | |
| metrics = [metrics] | |
| # Фильтруем данные | |
| filtered_df = df[(df['model'] == model) & (df['top_n'] == top_n)] | |
| if len(filtered_df) <= 1: | |
| print(f"Недостаточно данных для модели {model} и top_n={top_n}") | |
| return | |
| # Создаем фигуру | |
| plt.figure(figsize=(14, 8)) | |
| # Определяем цветовую схему для метрик | |
| palette = sns.color_palette("viridis", len(metrics)) | |
| # Группируем по размеру чанка и проценту перекрытия | |
| # Вычисляем среднее только для указанных метрик, а не для всех столбцов | |
| columns_to_agg = {metric: 'mean' for metric in metrics} | |
| chunk_data = filtered_df.groupby(['words_per_chunk', 'overlap_percentage']).agg(columns_to_agg).reset_index() | |
| # Получаем уникальные значения процента перекрытия | |
| overlap_percentages = sorted(chunk_data['overlap_percentage'].unique()) | |
| # Настраиваем маркеры и линии для разных перекрытий | |
| markers = ['o', 's', '^', 'D', 'x', '*'] | |
| # Для каждого перекрытия строим линии с разными метриками | |
| for i, overlap in enumerate(overlap_percentages): | |
| subset = chunk_data[chunk_data['overlap_percentage'] == overlap].sort_values('words_per_chunk') | |
| for j, metric in enumerate(metrics): | |
| plt.plot(subset['words_per_chunk'], subset[metric], | |
| marker=markers[i % len(markers)], linewidth=2, | |
| label=f"{metric}, overlap={overlap}%", | |
| color=palette[j]) | |
| # Добавляем легенду и заголовок | |
| plt.legend(title="Метрика и перекрытие", fontsize=10, loc='best') | |
| plt.title(f"Зависимость метрик от размера чанка для {model} (top_n={top_n})", fontsize=16) | |
| plt.xlabel("Размер чанка (слов)", fontsize=14) | |
| plt.ylabel("Значение метрики", fontsize=14) | |
| # Включаем сетку | |
| plt.grid(True, linestyle='--', alpha=0.7) | |
| # Настраиваем макет | |
| plt.tight_layout() | |
| # Сохраняем график | |
| metrics_type = "macro" if metrics[0].startswith("macro_") else "weighted" | |
| model_name = model.replace('/', '_') | |
| metric_names = '_'.join([m.replace('macro_', '') for m in metrics]) | |
| file_name = f"chunk_size_{metrics_type}_{metric_names}_{model_name}_top{top_n}.png" | |
| plt.savefig(os.path.join(plots_dir, file_name), dpi=300) | |
| plt.close() | |
| print(f"Создан график зависимости от размера чанка: {file_name}") | |
| def create_heatmap(df: pd.DataFrame, models: list | str, metric: str, top_n: int, plots_dir: str) -> None: | |
| """ | |
| Создает тепловые карты зависимости метрики от размера чанка и процента перекрытия | |
| для заданных моделей. | |
| Args: | |
| df: DataFrame с данными | |
| models: Список моделей или одна модель | |
| metric: Название метрики | |
| top_n: Значение top_n | |
| plots_dir: Директория для сохранения графиков | |
| """ | |
| if isinstance(models, str): | |
| models = [models] | |
| # Ограничиваем количество моделей для наглядности | |
| if len(models) > 4: | |
| models = models[:4] | |
| # Создаем фигуру с подграфиками | |
| fig, axes = plt.subplots(1, len(models), figsize=(6 * len(models), 6), squeeze=False) | |
| # Для каждой модели создаем тепловую карту | |
| for i, model in enumerate(models): | |
| # Фильтруем данные для указанной модели и top_n | |
| filtered_df = df[(df['model'] == model) & (df['top_n'] == top_n)] | |
| # Проверяем, достаточно ли данных для построения тепловой карты | |
| chunk_sizes = filtered_df['words_per_chunk'].unique() | |
| overlap_percentages = filtered_df['overlap_percentage'].unique() | |
| if len(chunk_sizes) <= 1 or len(overlap_percentages) <= 1: | |
| print(f"Недостаточно данных для построения тепловой карты для модели {model} и top_n={top_n}") | |
| # Пропускаем этот subplot | |
| axes[0, i].text(0.5, 0.5, f"Недостаточно данных для {model}", | |
| horizontalalignment='center', verticalalignment='center') | |
| axes[0, i].set_title(model) | |
| axes[0, i].axis('off') | |
| continue | |
| # Создаем сводную таблицу для тепловой карты, используя только нужную метрику | |
| # Сначала выберем только колонки для pivot_table | |
| pivot_columns = ['words_per_chunk', 'overlap_percentage', metric] | |
| pivot_df = filtered_df[pivot_columns].copy() | |
| # Теперь создаем сводную таблицу | |
| pivot_data = pivot_df.pivot_table( | |
| index='words_per_chunk', | |
| columns='overlap_percentage', | |
| values=metric, | |
| aggfunc='mean' | |
| ) | |
| # Строим тепловую карту | |
| sns.heatmap(pivot_data, annot=True, fmt=".4f", cmap="viridis", | |
| linewidths=.5, annot_kws={"size": 8}, ax=axes[0, i]) | |
| # Устанавливаем заголовок и метки осей | |
| axes[0, i].set_title(model, fontsize=12) | |
| axes[0, i].set_xlabel("Процент перекрытия (%)", fontsize=10) | |
| axes[0, i].set_ylabel("Размер чанка (слов)", fontsize=10) | |
| # Добавляем общий заголовок | |
| plt.suptitle(f"Тепловые карты {metric} для разных моделей (top_n={top_n})", fontsize=16) | |
| # Настраиваем макет | |
| plt.tight_layout(rect=[0, 0, 1, 0.96]) # Оставляем место для общего заголовка | |
| # Сохраняем график | |
| is_macro = "macro" if "macro" in metric else "weighted" | |
| file_name = f"heatmap_{is_macro}_{metric.replace('macro_', '')}_top{top_n}.png" | |
| plt.savefig(os.path.join(plots_dir, file_name), dpi=300) | |
| plt.close() | |
| print(f"Созданы тепловые карты: {file_name}") | |
| def find_best_combinations(df: pd.DataFrame, metrics: list | str = None) -> pd.DataFrame: | |
| """ | |
| Находит наилучшие комбинации параметров на основе агрегированных recall-метрик. | |
| Args: | |
| df: DataFrame с данными | |
| metrics: Список метрик для анализа или None (тогда используются все recall-метрики) | |
| Returns: | |
| DataFrame с лучшими комбинациями параметров | |
| """ | |
| if metrics is None: | |
| # По умолчанию выбираем все метрики с "recall" в названии | |
| metrics = [col for col in df.columns if "recall" in col] | |
| elif isinstance(metrics, str): | |
| metrics = [metrics] | |
| print(f"Поиск лучших комбинаций на основе метрик: {metrics}") | |
| # Создаем новую метрику - сумму всех указанных recall-метрик | |
| df_copy = df.copy() | |
| df_copy['combined_recall'] = df_copy[metrics].sum(axis=1) | |
| # Находим лучшие комбинации для различных значений top_n | |
| best_combinations = [] | |
| for top_n in df_copy['top_n'].unique(): | |
| top_n_df = df_copy[df_copy['top_n'] == top_n] | |
| if len(top_n_df) == 0: | |
| continue | |
| # Находим строку с максимальным combined_recall | |
| best_idx = top_n_df['combined_recall'].idxmax() | |
| best_row = top_n_df.loc[best_idx].copy() | |
| best_row['best_for_top_n'] = top_n | |
| best_combinations.append(best_row) | |
| # Находим лучшие комбинации для разных моделей | |
| for model in df_copy['model'].unique(): | |
| model_df = df_copy[df_copy['model'] == model] | |
| if len(model_df) == 0: | |
| continue | |
| # Находим строку с максимальным combined_recall | |
| best_idx = model_df['combined_recall'].idxmax() | |
| best_row = model_df.loc[best_idx].copy() | |
| best_row['best_for_model'] = model | |
| best_combinations.append(best_row) | |
| # Находим лучшие комбинации для разных размеров чанков | |
| for chunk_size in df_copy['words_per_chunk'].unique(): | |
| chunk_df = df_copy[df_copy['words_per_chunk'] == chunk_size] | |
| if len(chunk_df) == 0: | |
| continue | |
| # Находим строку с максимальным combined_recall | |
| best_idx = chunk_df['combined_recall'].idxmax() | |
| best_row = chunk_df.loc[best_idx].copy() | |
| best_row['best_for_chunk_size'] = chunk_size | |
| best_combinations.append(best_row) | |
| # Находим абсолютно лучшую комбинацию | |
| if len(df_copy) > 0: | |
| best_idx = df_copy['combined_recall'].idxmax() | |
| best_row = df_copy.loc[best_idx].copy() | |
| best_row['absolute_best'] = True | |
| best_combinations.append(best_row) | |
| if not best_combinations: | |
| return pd.DataFrame() | |
| result = pd.DataFrame(best_combinations) | |
| # Сортируем по combined_recall (по убыванию) | |
| result = result.sort_values('combined_recall', ascending=False) | |
| print(f"Найдено {len(result)} лучших комбинаций") | |
| return result | |
| def create_best_combinations_plot(best_df: pd.DataFrame, metrics: list | str, plots_dir: str) -> None: | |
| """ | |
| Создает график сравнения лучших комбинаций параметров. | |
| Args: | |
| best_df: DataFrame с лучшими комбинациями | |
| metrics: Список метрик для визуализации | |
| plots_dir: Директория для сохранения графиков | |
| """ | |
| if isinstance(metrics, str): | |
| metrics = [metrics] | |
| if len(best_df) == 0: | |
| print("Нет данных для построения графика лучших комбинаций") | |
| return | |
| # Создаем новый признак для идентификации комбинаций | |
| best_df['combo_label'] = best_df.apply( | |
| lambda row: f"{row['model']} (w={row['words_per_chunk']}, o={row['overlap_words']}, top_n={row['top_n']})", | |
| axis=1 | |
| ) | |
| # Берем только лучшие N комбинаций для читаемости | |
| max_combos = 10 | |
| if len(best_df) > max_combos: | |
| plot_df = best_df.head(max_combos).copy() | |
| print(f"Ограничиваем график до {max_combos} лучших комбинаций") | |
| else: | |
| plot_df = best_df.copy() | |
| # Создаем длинный формат данных для seaborn | |
| plot_data = plot_df.melt( | |
| id_vars=['combo_label', 'combined_recall'], | |
| value_vars=metrics, | |
| var_name='metric', | |
| value_name='value' | |
| ) | |
| # Сортируем по суммарному recall (комбинации) и метрике (для группировки) | |
| plot_data = plot_data.sort_values(['combined_recall', 'metric'], ascending=[False, True]) | |
| # Создаем фигуру для графика | |
| plt.figure(figsize=(14, 10)) | |
| # Создаем bar plot | |
| sns.barplot( | |
| x='combo_label', | |
| y='value', | |
| hue='metric', | |
| data=plot_data, | |
| palette='viridis' | |
| ) | |
| # Настраиваем оси и заголовок | |
| plt.title('Лучшие комбинации параметров по recall-метрикам', fontsize=16) | |
| plt.xlabel('Комбинация параметров', fontsize=14) | |
| plt.ylabel('Значение метрики', fontsize=14) | |
| # Поворачиваем подписи по оси X для лучшей читаемости | |
| plt.xticks(rotation=45, ha='right', fontsize=10) | |
| # Настраиваем легенду | |
| plt.legend(title='Метрика', fontsize=12) | |
| # Добавляем сетку | |
| plt.grid(axis='y', linestyle='--', alpha=0.7) | |
| # Настраиваем макет | |
| plt.tight_layout() | |
| # Сохраняем график | |
| file_name = f"best_combinations_comparison.png" | |
| plt.savefig(os.path.join(plots_dir, file_name), dpi=300) | |
| plt.close() | |
| print(f"Создан график сравнения лучших комбинаций: {file_name}") | |
| def generate_plots(combined_results: pd.DataFrame, macro_metrics: pd.DataFrame, plots_dir: str) -> None: | |
| """ | |
| Генерирует набор графиков с помощью seaborn и сохраняет их в указанную директорию. | |
| Фокусируется в первую очередь на recall-метриках как наиболее важных. | |
| Args: | |
| combined_results: DataFrame с объединенными результатами (weighted метрики) | |
| macro_metrics: DataFrame с macro метриками | |
| plots_dir: Директория для сохранения графиков | |
| """ | |
| # Создаем директорию для графиков, если она не существует | |
| setup_plot_directory(plots_dir) | |
| # Настраиваем стиль для графиков | |
| sns.set_style("whitegrid") | |
| plt.rcParams['font.family'] = 'DejaVu Sans' | |
| # Получаем список моделей для построения графиков | |
| models = combined_results['model'].unique() | |
| top_n_values = [10, 20, 50, 100] | |
| print(f"Генерация графиков для {len(models)} моделей...") | |
| # 0. Добавляем анализ наилучших комбинаций параметров | |
| # Определяем метрики для анализа - фокусируемся на recall | |
| weighted_recall_metrics = ['text_recall', 'doc_recall'] | |
| # Находим лучшие комбинации параметров | |
| best_combinations = find_best_combinations(combined_results, weighted_recall_metrics) | |
| # Создаем график сравнения лучших комбинаций | |
| if not best_combinations.empty: | |
| create_best_combinations_plot(best_combinations, weighted_recall_metrics, plots_dir) | |
| # Если доступны macro метрики, делаем то же самое для них | |
| if macro_metrics is not None: | |
| macro_recall_metrics = ['macro_text_recall', 'macro_doc_recall'] | |
| macro_best_combinations = find_best_combinations(macro_metrics, macro_recall_metrics) | |
| if not macro_best_combinations.empty: | |
| create_best_combinations_plot(macro_best_combinations, macro_recall_metrics, plots_dir) | |
| # 1. Создаем графики сравнения моделей для weighted метрик | |
| # Фокусируемся на recall-метриках | |
| weighted_metrics = { | |
| 'text': ['text_recall'], # Только text_recall | |
| 'doc': ['doc_recall'] # Только doc_recall | |
| } | |
| for top_n in top_n_values: | |
| for metrics_group, metrics in weighted_metrics.items(): | |
| create_model_comparison_plot(combined_results, metrics, top_n, plots_dir) | |
| # 2. Если доступны macro метрики, создаем графики на их основе | |
| if macro_metrics is not None: | |
| print("Создание графиков на основе macro метрик...") | |
| macro_metrics_groups = { | |
| 'text': ['macro_text_recall'], # Только macro_text_recall | |
| 'doc': ['macro_doc_recall'] # Только macro_doc_recall | |
| } | |
| for top_n in top_n_values: | |
| for metrics_group, metrics in macro_metrics_groups.items(): | |
| create_model_comparison_plot(macro_metrics, metrics, top_n, plots_dir) | |
| # 3. Создаем графики зависимости от top_n | |
| for metrics_type, df in [("weighted", combined_results), ("macro", macro_metrics)]: | |
| if df is None: | |
| continue | |
| metrics_to_plot = [] | |
| if metrics_type == "weighted": | |
| metrics_to_plot = ['text_recall', 'doc_recall'] # Только recall-метрики | |
| else: | |
| metrics_to_plot = ['macro_text_recall', 'macro_doc_recall'] # Только macro recall-метрики | |
| for metric in metrics_to_plot: | |
| create_top_n_plot(df, models, metric, plots_dir) | |
| # 4. Для каждой модели создаем графики по размеру чанка | |
| for model in models: | |
| # Выбираем 2 значения top_n для анализа | |
| for top_n in [20, 50]: | |
| # Создаем графики с recall-метриками | |
| weighted_metrics_to_combine = ['text_recall'] | |
| create_chunk_size_plot(combined_results, model, weighted_metrics_to_combine, top_n, plots_dir) | |
| doc_metrics_to_combine = ['doc_recall'] | |
| create_chunk_size_plot(combined_results, model, doc_metrics_to_combine, top_n, plots_dir) | |
| # Если есть macro метрики, создаем соответствующие графики | |
| if macro_metrics is not None: | |
| macro_metrics_to_combine = ['macro_text_recall'] | |
| create_chunk_size_plot(macro_metrics, model, macro_metrics_to_combine, top_n, plots_dir) | |
| macro_doc_metrics_to_combine = ['macro_doc_recall'] | |
| create_chunk_size_plot(macro_metrics, model, macro_doc_metrics_to_combine, top_n, plots_dir) | |
| # 5. Создаем тепловые карты для моделей | |
| for top_n in [20, 50]: | |
| for metric_prefix in ["", "macro_"]: | |
| for metric_type in ["text_recall", "doc_recall"]: | |
| metric = f"{metric_prefix}{metric_type}" | |
| # Используем соответствующий DataFrame | |
| if metric_prefix and macro_metrics is None: | |
| continue | |
| df_to_use = macro_metrics if metric_prefix else combined_results | |
| create_heatmap(df_to_use, models, metric, top_n, plots_dir) | |
| print(f"Создание графиков завершено в директории {plots_dir}") | |
| def print_best_combinations(best_df: pd.DataFrame) -> None: | |
| """ | |
| Выводит информацию о лучших комбинациях параметров. | |
| Args: | |
| best_df: DataFrame с лучшими комбинациями | |
| """ | |
| if best_df.empty: | |
| print("Не найдено лучших комбинаций") | |
| return | |
| print("\n=== ЛУЧШИЕ КОМБИНАЦИИ ПАРАМЕТРОВ ===") | |
| # Выводим абсолютно лучшую комбинацию, если она есть | |
| absolute_best = best_df[best_df.get('absolute_best', False) == True] | |
| if not absolute_best.empty: | |
| row = absolute_best.iloc[0] | |
| print(f"\nАБСОЛЮТНО ЛУЧШАЯ КОМБИНАЦИЯ:") | |
| print(f" Модель: {row['model']}") | |
| print(f" Размер чанка: {row['words_per_chunk']} слов") | |
| print(f" Перекрытие: {row['overlap_words']} слов ({row['overlap_percentage']}%)") | |
| print(f" top_n: {row['top_n']}") | |
| # Выводим значения метрик | |
| recall_metrics = [col for col in best_df.columns if 'recall' in col and col != 'combined_recall'] | |
| for metric in recall_metrics: | |
| print(f" {metric}: {row[metric]:.4f}") | |
| print("\n=== ТОП-5 ЛУЧШИХ КОМБИНАЦИЙ ===") | |
| for i, row in best_df.head(5).iterrows(): | |
| print(f"\n#{i+1}: {row['model']}, w={row['words_per_chunk']}, o={row['overlap_words']}, top_n={row['top_n']}") | |
| # Выводим значения метрик | |
| recall_metrics = [col for col in best_df.columns if 'recall' in col and col != 'combined_recall'] | |
| for metric in recall_metrics: | |
| print(f" {metric}: {row[metric]:.4f}") | |
| print("\n=======================================") | |
| def create_combined_excel(combined_results: pd.DataFrame, question_metrics: pd.DataFrame, | |
| macro_metrics: pd.DataFrame = None, output_file: str = "combined_results.xlsx") -> None: | |
| """ | |
| Создает Excel-файл с несколькими листами, содержащими различные срезы данных. | |
| Добавляет автофильтры и применяет форматирование. | |
| Args: | |
| combined_results: DataFrame с объединенными результатами | |
| question_metrics: DataFrame с метриками по вопросам | |
| macro_metrics: DataFrame с macro метриками (опционально) | |
| output_file: Путь к выходному Excel-файлу | |
| """ | |
| print(f"Создание Excel-файла {output_file}...") | |
| # Создаем новый Excel-файл | |
| workbook = Workbook() | |
| # Удаляем стандартный лист | |
| default_sheet = workbook.active | |
| workbook.remove(default_sheet) | |
| # Подготавливаем данные для различных листов | |
| sheets_data = { | |
| "Исходные данные": combined_results, | |
| "Сводка по моделям": prepare_summary_by_model_top_n(combined_results, macro_metrics), | |
| "Сводка по чанкингу": prepare_summary_by_chunking_params_top_n(combined_results, macro_metrics), | |
| "Лучшие конфигурации": prepare_best_configurations(combined_results, macro_metrics) | |
| } | |
| # Если есть метрики по вопросам, добавляем лист с ними | |
| if question_metrics is not None: | |
| sheets_data["Метрики по вопросам"] = question_metrics | |
| # Если есть macro метрики, добавляем лист с ними | |
| if macro_metrics is not None: | |
| sheets_data["Macro метрики"] = macro_metrics | |
| # Создаем листы и добавляем данные | |
| for sheet_name, data in sheets_data.items(): | |
| if data is not None and not data.empty: | |
| sheet = workbook.create_sheet(title=sheet_name) | |
| for r in dataframe_to_rows(data, index=False, header=True): | |
| sheet.append(r) | |
| # Применяем форматирование | |
| apply_formatting(workbook) | |
| # Сохраняем файл | |
| workbook.save(output_file) | |
| print(f"Excel-файл создан: {output_file}") | |
| def calculate_macro_metrics(question_metrics: pd.DataFrame) -> pd.DataFrame: | |
| """ | |
| Вычисляет macro метрики на основе результатов по вопросам. | |
| Args: | |
| question_metrics: DataFrame с метриками по вопросам | |
| Returns: | |
| DataFrame с macro метриками | |
| """ | |
| if question_metrics is None: | |
| return None | |
| print("Вычисление macro метрик на основе метрик по вопросам...") | |
| # Группируем по конфигурации (модель, параметры чанкинга, top_n) | |
| grouped_metrics = question_metrics.groupby(['model', 'words_per_chunk', 'overlap_words', 'top_n']) | |
| # Для каждой группы вычисляем среднее значение метрик (macro) | |
| macro_metrics = grouped_metrics.agg({ | |
| 'text_precision': 'mean', # Macro precision = среднее precision по всем вопросам | |
| 'text_recall': 'mean', # Macro recall = среднее recall по всем вопросам | |
| 'text_f1': 'mean', # Macro F1 = среднее F1 по всем вопросам | |
| 'doc_precision': 'mean', | |
| 'doc_recall': 'mean', | |
| 'doc_f1': 'mean' | |
| }).reset_index() | |
| # Добавляем префикс "macro_" к названиям метрик для ясности | |
| for col in ['text_precision', 'text_recall', 'text_f1', 'doc_precision', 'doc_recall', 'doc_f1']: | |
| macro_metrics.rename(columns={col: f'macro_{col}'}, inplace=True) | |
| # Добавляем процент перекрытия | |
| macro_metrics['overlap_percentage'] = (macro_metrics['overlap_words'] / macro_metrics['words_per_chunk'] * 100).round(1) | |
| print(f"Вычислено {len(macro_metrics)} наборов macro метрик") | |
| return macro_metrics | |
| def main(): | |
| """Основная функция скрипта.""" | |
| args = parse_args() | |
| # Загружаем результаты из CSV-файлов | |
| combined_results = load_results_files(args.results_dir) | |
| # Загружаем метрики по вопросам (если есть) | |
| question_metrics = load_question_metrics_files(args.results_dir) | |
| # Вычисляем macro метрики на основе метрик по вопросам | |
| macro_metrics = calculate_macro_metrics(question_metrics) | |
| # Находим лучшие комбинации параметров | |
| best_combinations_weighted = find_best_combinations(combined_results, ['text_recall', 'doc_recall']) | |
| print_best_combinations(best_combinations_weighted) | |
| if macro_metrics is not None: | |
| best_combinations_macro = find_best_combinations(macro_metrics, ['macro_text_recall', 'macro_doc_recall']) | |
| print_best_combinations(best_combinations_macro) | |
| # Создаем объединенный Excel-файл с данными | |
| create_combined_excel(combined_results, question_metrics, macro_metrics, args.output_file) | |
| # Генерируем графики с помощью seaborn | |
| print(f"Генерация графиков и сохранение их в директорию: {args.plots_dir}") | |
| generate_plots(combined_results, macro_metrics, args.plots_dir) | |
| print("Готово! Результаты сохранены в Excel и графики созданы.") | |
| if __name__ == "__main__": | |
| main() |