""" A9 Results Aggregation Script Aggregates all cross-validation results from the cv_results folders and produces summary tables for the A9_Report.ipynb notebook. Usage: python aggregate_results.py Output: - results_summary.csv: All experiment results in one table - results_summary.json: Same data in JSON format - Prints summary to console """ import os import json import re import pandas as pd from pathlib import Path # Result directories RESULT_DIRS = { 'different_models': 'cv_results_different_models', 'conv1d_variants': 'cv_results_conv1D_variants', 'optimizer': 'cv_results_optimizer', 'loss_optimizer': 'cv_results_loss_optimizer', } def parse_summary_txt(filepath): """Parse summary.txt file to extract metrics.""" results = [] with open(filepath, 'r') as f: content = f.read() # Find all model sections with their metrics # Pattern matches lines like " Optimizer: SGD, Loss: MSE" or just model names sections = re.split(r'\n(?=[A-Z_]+(?:_[A-Z]+)*\n-{10,})', content) for section in sections: if 'Val RMSE:' in section or 'Test RMSE:' in section: lines = section.strip().split('\n') model_name = lines[0].strip() if lines else 'unknown' # Skip separator lines if model_name.startswith('-'): continue row = {'model': model_name.lower()} # Extract metrics using regex val_rmse = re.search(r'Val RMSE:\s*([\d.]+)\s*±\s*([\d.]+)', section) val_mae = re.search(r'Val MAE:\s*([\d.]+)\s*±\s*([\d.]+)', section) val_r2 = re.search(r'Val R²:\s*([\d.]+)\s*±\s*([\d.]+)', section) test_rmse = re.search(r'Test RMSE:\s*([\d.]+)', section) test_mae = re.search(r'Test MAE:\s*([\d.]+)', section) test_r2 = re.search(r'Test R²:\s*([\d.]+)', section) best_fold = re.search(r'Best Fold:\s*(\d+)', section) if val_rmse: row['val_rmse_mean'] = float(val_rmse.group(1)) row['val_rmse_std'] = float(val_rmse.group(2)) if val_mae: row['val_mae_mean'] = float(val_mae.group(1)) row['val_mae_std'] = float(val_mae.group(2)) if val_r2: row['val_r2_mean'] = float(val_r2.group(1)) row['val_r2_std'] = float(val_r2.group(2)) if test_rmse: row['test_rmse'] = float(test_rmse.group(1)) if test_mae: row['test_mae'] = float(test_mae.group(1)) if test_r2: row['test_r2'] = float(test_r2.group(1)) if best_fold: row['best_fold'] = int(best_fold.group(1)) # Only add if we found some metrics if len(row) > 1: results.append(row) return results def parse_summary_header(filepath): """Parse the header summary section of summary.txt.""" results = [] with open(filepath, 'r') as f: content = f.read() # Pattern 1: Standard format (DENSE:, CONV1D:, CONV1D_V3: etc.) # Fixed to properly capture model names with underscores and numbers pattern1 = r'([A-Z][A-Z0-9_]*):\s*\n\s*Best Fold:\s*(\d+)\s*\n\s*Val RMSE:\s*([\d.]+)\s*±\s*([\d.]+)\s*\n\s*Val MAE:\s*([\d.]+)\s*±\s*([\d.]+)\s*\n\s*Val R²:\s*([\d.]+)\s*±\s*([\d.]+)(?:\s*\n\s*Test RMSE:\s*([\d.]+))?(?:\s*\n\s*Test MAE:\s*([\d.]+))?(?:\s*\n\s*Test R²:\s*([\d.]+))?' # Pattern 2: Optimizer format (Optimizer: SGD, etc.) pattern2 = r'Optimizer:\s*([A-Z]+)(?:,\s*Loss:\s*([A-Z]+))?\s*\n\s*Best Fold:\s*(\d+)\s*\n\s*Val RMSE:\s*([\d.]+)\s*±\s*([\d.]+)\s*\n\s*Val MAE:\s*([\d.]+)\s*±\s*([\d.]+)\s*\n\s*Val R²:\s*([\d.]+)\s*±\s*([\d.]+)(?:\s*\n\s*Test RMSE:\s*([\d.]+))?(?:\s*\n\s*Test MAE:\s*([\d.]+))?(?:\s*\n\s*Test R²:\s*([\d.]+))?' seen_models = set() # Try pattern 1 matches = re.finditer(pattern1, content, re.MULTILINE) for match in matches: model_name = match.group(1).lower() # Skip detailed sections (those that start with model name and have "-----" after) # We only want the summary sections at the top pos = match.start() next_line_start = content.find('\n', match.end()) + 1 if next_line_start < len(content): next_line = content[next_line_start:next_line_start+50] if '---' in next_line: continue # Skip duplicates if model_name in seen_models: continue seen_models.add(model_name) row = { 'model': model_name, 'best_fold': int(match.group(2)), 'val_rmse_mean': float(match.group(3)), 'val_rmse_std': float(match.group(4)), 'val_mae_mean': float(match.group(5)), 'val_mae_std': float(match.group(6)), 'val_r2_mean': float(match.group(7)), 'val_r2_std': float(match.group(8)), } if match.group(9): row['test_rmse'] = float(match.group(9)) if match.group(10): row['test_mae'] = float(match.group(10)) if match.group(11): row['test_r2'] = float(match.group(11)) results.append(row) # Try pattern 2 for optimizer/loss variants matches = re.finditer(pattern2, content, re.MULTILINE) for match in matches: optimizer = match.group(1).lower() loss = match.group(2).lower() if match.group(2) else 'mse' model_name = f"conv1d_v3_{optimizer}_{loss}" # Skip duplicates if model_name in seen_models: continue seen_models.add(model_name) row = { 'model': model_name, 'best_fold': int(match.group(3)), 'val_rmse_mean': float(match.group(4)), 'val_rmse_std': float(match.group(5)), 'val_mae_mean': float(match.group(6)), 'val_mae_std': float(match.group(7)), 'val_r2_mean': float(match.group(8)), 'val_r2_std': float(match.group(9)), } if match.group(10): row['test_rmse'] = float(match.group(10)) if match.group(11): row['test_mae'] = float(match.group(11)) if match.group(12): row['test_r2'] = float(match.group(12)) results.append(row) return results def main(): script_dir = Path(__file__).parent os.chdir(script_dir) all_results = [] print("=" * 60) print("A9 Results Aggregation") print("=" * 60) # Load results from each experiment directory by parsing summary.txt for exp_name, dir_name in RESULT_DIRS.items(): summary_path = Path(dir_name) / 'summary.txt' print(f"\nLoading from {summary_path}...") if not summary_path.exists(): print(f" Warning: {summary_path} not found") continue results = parse_summary_header(summary_path) print(f" Found {len(results)} model results") for row in results: row['experiment'] = exp_name all_results.append(row) if not all_results: print("\nNo results found!") return # Create DataFrame df = pd.DataFrame(all_results) # Reorder columns cols = ['experiment', 'model', 'best_fold', 'val_r2_mean', 'val_r2_std', 'val_rmse_mean', 'val_rmse_std', 'val_mae_mean', 'val_mae_std', 'test_r2', 'test_rmse', 'test_mae'] df = df[[c for c in cols if c in df.columns]] # Sort by test_r2 descending, then val_r2_mean df['sort_key'] = df['test_r2'].fillna(df['val_r2_mean']) df = df.sort_values('sort_key', ascending=False).drop('sort_key', axis=1) # Save to CSV df.to_csv('results_summary.csv', index=False) print(f"\nSaved to results_summary.csv ({len(df)} rows)") # Save to JSON df.to_json('results_summary.json', orient='records', indent=2) print(f"Saved to results_summary.json") # Print summary tables print("\n" + "=" * 60) print("SUMMARY: All Experiments Ranked by R²") print("=" * 60) print(df.to_string(index=False, float_format=lambda x: f'{x:.4f}' if pd.notna(x) else 'N/A')) # Best model print("\n" + "=" * 60) print("CHAMPION MODEL") print("=" * 60) best = df.iloc[0] print(f"Model: {best['model']}") print(f"Experiment: {best['experiment']}") if pd.notna(best.get('val_r2_mean')): print(f"Validation R²: {best['val_r2_mean']:.4f} ± {best['val_r2_std']:.4f}") print(f"Validation RMSE: {best['val_rmse_mean']:.4f} ± {best['val_rmse_std']:.4f}") if pd.notna(best.get('test_r2')): print(f"Test R²: {best['test_r2']:.4f}") print(f"Test RMSE: {best['test_rmse']:.4f}") # Dead ends (R² < 0.85) print("\n" + "=" * 60) print("DEAD ENDS (R² < 0.85)") print("=" * 60) r2_col = 'test_r2' if 'test_r2' in df.columns else 'val_r2_mean' dead_ends = df[df[r2_col] < 0.85] if len(dead_ends) > 0: print(dead_ends[['experiment', 'model', 'val_r2_mean', 'test_r2']].to_string(index=False)) else: print("None found") # Group summaries print("\n" + "=" * 60) print("EXPERIMENT SUMMARIES") print("=" * 60) for exp_name in RESULT_DIRS.keys(): exp_df = df[df['experiment'] == exp_name] if len(exp_df) > 0: print(f"\n{exp_name.upper()}:") print(exp_df[['model', 'val_r2_mean', 'val_rmse_mean', 'test_r2', 'test_rmse']].to_string(index=False)) if __name__ == '__main__': main()