""" Скрипт для конвертации результатов экспериментов из CSV в LaTeX таблицы для lab5. Адаптирован из lab2/csv_to_tex.py для работы с форматом эволюционных стратегий. Формат входных данных: "время±стд (поколения±стд) фитнес" """ import re from pathlib import Path # Настройка цвета для выделения лучших результатов # None - только жирным, строка (например "magenta") - жирным и цветом HIGHLIGHT_COLOR = "magenta" def parse_csv_file(csv_path: str) -> tuple[str, list[list[str]]]: """ Парсит CSV файл с результатами эксперимента. Args: csv_path: Путь к CSV файлу Returns: Tuple с заголовком и данными таблицы """ with open(csv_path, "r", encoding="utf-8") as file: lines = file.readlines() # Удаляем пустые строки и берём только строки с данными clean_lines = [line.strip() for line in lines if line.strip()] # Первая строка - заголовки header = clean_lines[0] # Остальные строки - данные data_lines = clean_lines[1:] # Парсим данные data_rows = [] for line in data_lines: parts = line.split(",") if len(parts) >= 2: # mu + хотя бы одно значение data_rows.append(parts) return header, data_rows def extract_time_value(value: str) -> float | None: """ Извлекает значение времени из строки формата "X.Y±Z.W (...)". Args: value: Строка с результатом Returns: Время выполнения как float или None если значение пустое """ value = value.strip() if value == "—" or value == "" or value == "–": return None # Ищем паттерн "число.число±число" match = re.match(r"(\d+\.?\d*)±", value) if match: return float(match.group(1)) # Если нет ±, пробуем просто число перед скобкой match = re.match(r"(\d+\.?\d*)\s*\(", value) if match: return float(match.group(1)) return None def extract_generations_value(value: str) -> float | None: """ Извлекает среднее число поколений из строки формата "... (X±Y) ...". Args: value: Строка с результатом Returns: Среднее число поколений как float или None если значение пустое """ value = value.strip() if value == "—" or value == "" or value == "–": return None # Ищем паттерн "(число±число)" и берём первое число match = re.search(r"\((\d+\.?\d*)±", value) if match: return float(match.group(1)) # Если нет ±, пробуем просто число в скобках match = re.search(r"\((\d+\.?\d*)\)", value) if match: return float(match.group(1)) return None def find_best_time(data_rows: list[list[str]]) -> float | None: """ Находит минимальное время выполнения среди всех значений в таблице. Args: data_rows: Строки данных таблицы Returns: Минимальное время или None если нет валидных значений """ min_time = None for row in data_rows: for i in range(1, len(row)): # Пропускаем первую колонку (mu) time_value = extract_time_value(row[i]) if time_value is not None: if min_time is None or time_value < min_time: min_time = time_value return min_time def find_best_generations(data_rows: list[list[str]]) -> float | None: """ Находит минимальное число поколений среди всех значений в таблице. Args: data_rows: Строки данных таблицы Returns: Минимальное число поколений или None если нет валидных значений """ min_gens = None for row in data_rows: for i in range(1, len(row)): # Пропускаем первую колонку (mu) gens_value = extract_generations_value(row[i]) if gens_value is not None: if min_gens is None or gens_value < min_gens: min_gens = gens_value return min_gens def format_value( value: str, best_time: float | None = None, best_gens: float | None = None ) -> str: """ Форматирует значение для LaTeX таблицы, выделяя лучшие результаты жирным. Args: value: Строковое значение из CSV best_time: Лучшее время в таблице для сравнения best_gens: Лучшее число поколений для сравнения Returns: Отформатированное значение для LaTeX """ value = value.strip() if value == "—" or value == "" or value == "–": return "—" # Парсим значение: "время±стд (поколения±стд) фитнес" # Пример: "60.6±47.9 (37±29) 0.0000" pattern = r"(\d+\.?\d*)±(\d+\.?\d*)\s*\((\d+\.?\d*)±(\d+\.?\d*)\)\s+(\d+\.?\d+)" match = re.match(pattern, value) if not match: # Если не удалось распарсить, возвращаем как есть return value time_avg = float(match.group(1)) time_std = float(match.group(2)) gens_avg = float(match.group(3)) gens_std = float(match.group(4)) fitness = match.group(5) # Формируем части БЕЗ стандартных отклонений time_part = f"{time_avg:.1f}" gens_part = f"{gens_avg:.0f}" # Проверяем, является ли время лучшим is_best_time = best_time is not None and abs(time_avg - best_time) < 0.1 is_best_gens = best_gens is not None and abs(gens_avg - best_gens) < 0.1 # Выделяем лучшее время if is_best_time: if HIGHLIGHT_COLOR is not None: time_part = f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{time_part}}}}}" else: time_part = f"\\textbf{{{time_part}}}" # Выделяем лучшее число поколений if is_best_gens: if HIGHLIGHT_COLOR is not None: gens_part = f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{gens_part}}}}}" else: gens_part = f"\\textbf{{{gens_part}}}" # Не показываем фитнес в таблице, т.к. он всегда близок к нулю return f"{time_part} ({gens_part})" def generate_latex_table(dimension: str, header: str, data_rows: list[list[str]]) -> str: """ Генерирует LaTeX код таблицы. Args: dimension: Размерность задачи (2 или 3) header: Заголовок таблицы data_rows: Строки данных Returns: LaTeX код таблицы """ # Находим лучшее время и лучшее число поколений в таблице best_time = find_best_time(data_rows) best_gens = find_best_generations(data_rows) # Извлекаем заголовки колонок из header header_parts = header.split(",") p_mut_values = header_parts[1:] # Пропускаем "mu \ p_mut" num_cols = len(p_mut_values) latex_code = f""" \\begin{{table}}[h!] \\centering \\small \\caption{{Результаты для $n = {dimension}$. Формат: время в мс (число поколений)}} \\begin{{tabularx}}{{{0.95 if num_cols <= 5 else 1.0}\\linewidth}}{{l *{{{num_cols}}}{{Y}}}} \\toprule $\\mathbf{{\\mu \\;\\backslash\\; p_{{mut}}}}$""" # Добавляем заголовки p_mut for p_mut in p_mut_values: latex_code += f" & \\textbf{{{p_mut.strip()}}}" latex_code += " \\\\\n \\midrule\n" # Добавляем строки данных for row in data_rows: mu_value = row[0].strip() latex_code += f" \\textbf{{{mu_value}}}" # Добавляем значения для каждого p_mut for i in range(1, len(row)): value = format_value(row[i], best_time, best_gens) latex_code += f" & {value}" # Заполняем недостающие колонки если их меньше чем в заголовке for i in range(len(row) - 1, num_cols): latex_code += " & —" latex_code += " \\\\\n" latex_code += f""" \\bottomrule \\end{{tabularx}} \\label{{tab:es_results_{dimension}}} \\end{{table}}""" return latex_code def main(): """Основная функция скрипта.""" experiments_path = Path("lab5_experiments") if not experiments_path.exists(): print("Папка lab5_experiments не найдена!") return tables = [] # Обрабатываем файлы dimension_2.csv и dimension_3.csv for dimension in [2, 3]: csv_file = experiments_path / f"dimension_{dimension}.csv" if csv_file.exists(): print(f"Обрабатываем {csv_file}...") try: header, data_rows = parse_csv_file(str(csv_file)) best_time = find_best_time(data_rows) best_gens = find_best_generations(data_rows) latex_table = generate_latex_table(str(dimension), header, data_rows) tables.append(latex_table) print( f"[OK] Таблица для n={dimension} готова (лучшее время: {best_time:.1f} мс, лучшее число поколений: {best_gens:.0f})" ) except Exception as e: print(f"[ERROR] Ошибка при обработке {csv_file}: {e}") else: print(f"[ERROR] Файл {csv_file} не найден") # Сохраняем все таблицы в файл if tables: output_file = experiments_path / "tables.tex" with open(output_file, "w", encoding="utf-8") as f: f.write("% Автоматически сгенерированные LaTeX таблицы\n") f.write( "% Лучший результат по времени и по числу поколений выделены жирным отдельно\n" ) f.write("% Убедитесь, что подключен \\usepackage{tabularx}\n") if HIGHLIGHT_COLOR is not None: f.write( "% ВНИМАНИЕ: Убедитесь, что подключен \\usepackage{xcolor} для цветового выделения\n" ) f.write( "% Используйте \\newcolumntype{Y}{>{\\centering\\arraybackslash}X} перед таблицами\n\n" ) for i, table in enumerate(tables): if i > 0: f.write("\n \n") f.write(table + "\n") print(f"\n[OK] Все таблицы сохранены в файл '{output_file}'") print(f"Сгенерировано таблиц: {len(tables)}") else: print("Не найдено данных для генерации таблиц!") if __name__ == "__main__": main()