Source code for BNMPy.result_evaluation

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import pearsonr
from typing import Dict, List, Tuple, Optional
import pandas as pd
from .simulation_evaluator import SimulationEvaluator
from .experiment_data import ExperimentData

[docs] class ResultEvaluator: """ Evaluate optimization results by comparing simulation output with experimental data. This provides tools to assess the quality of optimized models by: 1. Simulating the optimized model on experimental conditions 2. Comparing simulation results with experimental measurements 3. Calculating correlation and other statistical metrics 4. Generating visualization plots """
[docs] def __init__(self, optimizer_result, parameter_optimizer): """ Initialize the result evaluator. Parameters: ----------- optimizer_result : OptimizeResult The optimization result to evaluate parameter_optimizer : ParameterOptimizer The parameter optimizer instance containing the experiments and configuration for simulation """ self.result = optimizer_result self.optimizer = parameter_optimizer self.pbn = parameter_optimizer.pbn self.evaluator = parameter_optimizer.evaluator self.experiments = parameter_optimizer.evaluator.experiments # Store simulation results self.simulation_results = None self.evaluation_metrics = None
[docs] def simulate_optimized_model(self) -> Dict: """ Simulate the optimized model on all experimental conditions. Returns: -------- Dict Dictionary containing simulation results for each experiment """ print("Simulating optimized model on all experimental conditions...") # Set global seed for reproducibility global_seed = self.optimizer.config.get('seed', 9) np.random.seed(global_seed) simulation_results = { 'experiments': [], 'predictions': [], 'measurements': [], 'experiment_ids': [], 'measured_nodes': [], 'predicted_values': [], 'measured_values': [] } # Ensure the PBN has the optimized parameters if hasattr(self.result, 'x') and self.result.x is not None: cij_matrix = self.evaluator._vector_to_cij_matrix(self.result.x) self.evaluator._update_pbn_parameters(cij_matrix) for i, experiment in enumerate(self.experiments): try: # Simulate experiment predicted_steady_state = self.evaluator._simulate_experiment(experiment) # Extract predictions for measured nodes exp_predictions = {} exp_measurements = {} for node_name, measured_value in experiment['measurements'].items(): if node_name in self.pbn.nodeDict: node_idx = self.pbn.nodeDict[node_name] predicted_value = predicted_steady_state[node_idx] exp_predictions[node_name] = predicted_value exp_measurements[node_name] = measured_value # Store for correlation analysis simulation_results['predicted_values'].append(predicted_value) simulation_results['measured_values'].append(measured_value) simulation_results['measured_nodes'].append(node_name) simulation_results['experiment_ids'].append(experiment.get('id', i+1)) simulation_results['experiments'].append(experiment) simulation_results['predictions'].append(exp_predictions) simulation_results['measurements'].append(exp_measurements) print(f" Experiment {i+1}: {len(exp_predictions)} nodes simulated") except Exception as e: print(f" Warning: Failed to simulate experiment {i+1}: {str(e)}") self.simulation_results = simulation_results print(f"Simulation completed: {len(simulation_results['predicted_values'])} data points") return simulation_results
[docs] def calculate_evaluation_metrics(self) -> Dict: """ Calculate evaluation metrics comparing simulation results with experimental data. Returns: -------- Dict Dictionary containing various evaluation metrics """ if self.simulation_results is None: self.simulate_optimized_model() predicted = np.array(self.simulation_results['predicted_values']) measured = np.array(self.simulation_results['measured_values']) if len(predicted) == 0 or len(measured) == 0: print("Warning: No data available for evaluation") return {} # Calculate correlation correlation, p_value = pearsonr(predicted, measured) # Calculate other metrics mse = np.mean((predicted - measured) ** 2) rmse = np.sqrt(mse) mae = np.mean(np.abs(predicted - measured)) # R-squared ss_res = np.sum((measured - predicted) ** 2) ss_tot = np.sum((measured - np.mean(measured)) ** 2) r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0 # Calculate per-node metrics node_metrics = {} nodes = set(self.simulation_results['measured_nodes']) for node in nodes: node_indices = [i for i, n in enumerate(self.simulation_results['measured_nodes']) if n == node] node_pred = predicted[node_indices] node_meas = measured[node_indices] if len(node_pred) > 1: node_corr, node_p = pearsonr(node_pred, node_meas) node_mse = np.mean((node_pred - node_meas) ** 2) node_mae = np.mean(np.abs(node_pred - node_meas)) node_metrics[node] = { 'correlation': node_corr, 'p_value': node_p, 'mse': node_mse, 'mae': node_mae, 'n_points': len(node_pred) } metrics = { 'overall': { 'correlation': correlation, 'p_value': p_value, 'mse': mse, 'rmse': rmse, 'mae': mae, 'r_squared': r_squared, 'n_points': len(predicted) }, 'per_node': node_metrics, 'optimization_result': { 'final_mse': self.result.fun, 'success': self.result.success, 'iterations': self.result.nit, 'function_evaluations': self.result.nfev } } self.evaluation_metrics = metrics return metrics
[docs] def plot_prediction_vs_experimental(self, save_path: Optional[str] = None, show_confidence_interval: bool = False, show_experiment_ids: bool = False, figsize: Tuple[int, int] = (8, 6)) -> plt.Figure: """ Create a scatter plot comparing predicted vs experimental values. Parameters: ----------- save_path : str, optional Path to save the plot. If None, displays the plot. show_confidence_interval : bool, default=True Whether to show confidence intervals show_experiment_ids : bool, default=False Whether to label points with experiment IDs figsize : Tuple[int, int], default=(8, 6) Figure size Returns: -------- plt.Figure The matplotlib figure object """ if self.simulation_results is None: self.simulate_optimized_model() if self.evaluation_metrics is None: self.calculate_evaluation_metrics() predicted = np.array(self.simulation_results['predicted_values']) measured = np.array(self.simulation_results['measured_values']) nodes = self.simulation_results['measured_nodes'] experiment_ids = self.simulation_results['experiment_ids'] if len(predicted) == 0: print("No data available for plotting") return None # Create figure fig, ax = plt.subplots(figsize=figsize) ax.scatter(measured, predicted, alpha=0.7, s=60, c='lightgreen') # Add experiment ID labels if show_experiment_ids: for i, (meas, pred, exp_id) in enumerate(zip(measured, predicted, experiment_ids)): ax.annotate(f'E{exp_id}', (meas, pred), xytext=(5, 5), textcoords='offset points', fontsize=8, alpha=0.7) # Perfect prediction line min_val = min(np.min(predicted), np.min(measured)) max_val = max(np.max(predicted), np.max(measured)) # ax.plot([min_val, max_val], [min_val, max_val], 'r--', # linewidth=2, label='Perfect prediction') # Calculate regression line from scipy import stats slope, intercept, r_value, p_value, std_err = stats.linregress(measured, predicted) # Create regression line x_reg = np.linspace(min_val, max_val, 100) y_reg = slope * x_reg + intercept ax.plot(x_reg, y_reg, 'g-', linewidth=2, alpha=0.8, label='Regression line') # Add confidence interval bands if show_confidence_interval: # Add confidence bands (approximate) residuals = predicted - (slope * measured + intercept) mse_residuals = np.mean(residuals**2) confidence_interval = 1.96 * np.sqrt(mse_residuals) # 95% CI ax.fill_between(x_reg, y_reg - confidence_interval, y_reg + confidence_interval, alpha=0.2, color='green', label='95% Confidence interval') # Formatting ax.set_xlabel('Experimental Values', fontsize=12) ax.set_ylabel('Predicted Values', fontsize=12) # key statistics metrics = self.evaluation_metrics['overall'] final_mse = self.evaluation_metrics['optimization_result']['final_mse'] # title = f'Predicted vs Experimental (r={metrics["correlation"]:.3f}, p={metrics["p_value"]:.3e}, MSE={final_mse:.6f})' title = f'Predicted vs Experimental (r={metrics["correlation"]:.3f}, p={metrics["p_value"]:.3e})' # title = f'Predicted vs Experimental' ax.set_title(title, fontsize=12, fontweight='bold') # Add grid ax.grid(True, alpha=0.3) # Legend ax.legend() # Equal aspect ratio # ax.set_aspect('equal', adjustable='box') ax.set_xlim(np.min(measured) - 0.05, np.max(measured) + 0.05) ax.set_ylim(np.min(predicted) - 0.05, np.max(predicted) + 0.05) # Tight layout plt.tight_layout() # Save or show if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') print(f"Plot saved to {save_path}") else: plt.show() return fig
[docs] def plot_residuals(self, save_path: Optional[str] = None, show_experiment_ids: bool = False, figsize: Tuple[int, int] = (9, 4)) -> plt.Figure: """ Create residual plots to assess model fit quality. Parameters: ----------- save_path : str, optional Path to save the plot show_experiment_ids : bool, default=False Whether to label points with experiment IDs figsize : Tuple[int, int], default=(9, 4) Figure size in inches Returns: -------- plt.Figure The matplotlib figure object """ if self.simulation_results is None: self.simulate_optimized_model() predicted = np.array(self.simulation_results['predicted_values']) measured = np.array(self.simulation_results['measured_values']) experiment_ids = self.simulation_results['experiment_ids'] residuals = predicted - measured if len(predicted) == 0: print("No data available for plotting") return None # Create subplots fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) # Residuals vs Predicted ax1.scatter(predicted, residuals, alpha=0.7, s=60) ax1.axhline(y=0, color='r', linestyle='--', linewidth=2) ax1.set_xlabel('Predicted Values') ax1.set_ylabel('Residuals (Predicted - Measured)') ax1.set_title('Residuals vs Predicted Values') ax1.grid(True, alpha=0.3) # Add experiment ID labels if requested if show_experiment_ids: for pred, res, exp_id in zip(predicted, residuals, experiment_ids): ax1.annotate(f'E{exp_id}', (pred, res), xytext=(5, 5), textcoords='offset points', fontsize=8, alpha=0.7) # Histogram of residuals ax2.hist(residuals, bins=min(20, len(residuals)//3), alpha=0.7, edgecolor='black') ax2.axvline(x=0, color='r', linestyle='--', linewidth=2) ax2.set_xlabel('Residuals') ax2.set_ylabel('Frequency') ax2.set_title('Distribution of Residuals') ax2.grid(True, alpha=0.3) # Add statistics mean_residual = np.mean(residuals) std_residual = np.std(residuals) ax2.text(0.02, 0.98, f'Mean: {mean_residual:.4f}\nStd: {std_residual:.4f}', transform=ax2.transAxes, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') print(f"Residual plot saved to {save_path}") else: plt.show() return fig
[docs] def generate_evaluation_report(self, save_path: Optional[str] = None) -> str: """ Generate a comprehensive evaluation report. Parameters: ----------- save_path : str, optional Path to save the report as a text file Returns: -------- str The evaluation report as a string """ if self.evaluation_metrics is None: self.calculate_evaluation_metrics() report = [] report.append("="*60) report.append("OPTIMIZATION RESULT EVALUATION REPORT") report.append("="*60) report.append("") # Overall metrics overall = self.evaluation_metrics['overall'] report.append("OVERALL PERFORMANCE:") report.append("-" * 20) report.append(f"Pearson Correlation: {overall['correlation']:.4f}") report.append(f"P-value: {overall['p_value']:.6e}") report.append(f"R-squared: {overall['r_squared']:.4f}") report.append(f"Mean Squared Error: {overall['mse']:.6f}") report.append(f"Root Mean Squared Error: {overall['rmse']:.6f}") report.append(f"Mean Absolute Error: {overall['mae']:.6f}") report.append(f"Number of data points: {overall['n_points']}") report.append("") # Optimization details opt = self.evaluation_metrics['optimization_result'] report.append("OPTIMIZATION DETAILS:") report.append("-" * 20) report.append(f"Final MSE: {opt['final_mse']:.6f}") report.append(f"Success: {opt['success']}") report.append(f"Iterations: {opt['iterations']}") report.append(f"Function evaluations: {opt['function_evaluations']}") report.append("") # Per-node metrics if self.evaluation_metrics['per_node']: report.append("PER-NODE PERFORMANCE:") report.append("-" * 20) for node, metrics in self.evaluation_metrics['per_node'].items(): report.append(f"{node}:") report.append(f" Correlation: {metrics['correlation']:.4f} (p = {metrics['p_value']:.4e})") report.append(f" MSE: {metrics['mse']:.6f}") report.append(f" MAE: {metrics['mae']:.6f}") report.append(f" Data points: {metrics['n_points']}") report.append("") report_text = "\n".join(report) if save_path: with open(save_path, 'w') as f: f.write(report_text) print(f"Evaluation report saved to {save_path}") return report_text
[docs] def export_results_to_csv(self, save_path: str): """ Export detailed results to CSV for further analysis. Parameters: ----------- save_path : str Path to save the CSV file """ if self.simulation_results is None: self.simulate_optimized_model() # Create DataFrame with all results data = { 'Experiment_ID': self.simulation_results['experiment_ids'], 'Node': self.simulation_results['measured_nodes'], 'Predicted_Value': self.simulation_results['predicted_values'], 'Measured_Value': self.simulation_results['measured_values'], 'Residual': np.array(self.simulation_results['predicted_values']) - np.array(self.simulation_results['measured_values']), 'Absolute_Error': np.abs(np.array(self.simulation_results['predicted_values']) - np.array(self.simulation_results['measured_values'])) } df = pd.DataFrame(data) df.to_csv(save_path, index=False) print(f"Results exported to {save_path}")
[docs] def evaluate_optimization_result(optimizer_result, parameter_optimizer, output_dir: str = ".", plot_residuals: bool = True, save: bool = True, detailed: bool = False, figsize: Tuple[int, int] = (8, 6), show_confidence_interval: bool = False) -> ResultEvaluator: """ Convenience function to perform a complete evaluation of optimization results. Parameters: ----------- optimizer_result : OptimizeResult The optimization result parameter_optimizer : ParameterOptimizer The parameter optimizer instance plot_residuals: bool, default=True Whether to plot residuals output_dir : str, default="." Directory to save output files save : bool, default=True Whether to save plots and reports to files. If False, displays plots. detailed : bool, default=False Whether to label dots in plots with experiment IDs figsize : Tuple[int, int], default=(8, 6) Figure size in inches for the prediction vs experimental plot show_confidence_interval: bool, default=False Whether to show confidence interval bands Returns: -------- ResultEvaluator The result evaluator instance """ import os if save: os.makedirs(output_dir, exist_ok=True) # Create evaluator evaluator = ResultEvaluator(optimizer_result, parameter_optimizer) # Calculate metrics evaluator.calculate_evaluation_metrics() # Generate plots if save: # Prediction vs experimental plot plot_path = os.path.join(output_dir, "prediction_vs_experimental.png") evaluator.plot_prediction_vs_experimental(save_path=plot_path, show_experiment_ids=detailed, figsize=figsize, show_confidence_interval=show_confidence_interval) if plot_residuals: # Residual plots residual_path = os.path.join(output_dir, "residual_analysis.png") evaluator.plot_residuals(save_path=residual_path, show_experiment_ids=detailed) # Generate report report_path = os.path.join(output_dir, "evaluation_report.txt") evaluator.generate_evaluation_report(save_path=report_path) # Export CSV csv_path = os.path.join(output_dir, "detailed_results.csv") evaluator.export_results_to_csv(csv_path) else: # Display plots without saving evaluator.plot_prediction_vs_experimental(show_experiment_ids=detailed) if plot_residuals: evaluator.plot_residuals(show_experiment_ids=detailed) # Print report to console report = evaluator.generate_evaluation_report() print(report) return evaluator
[docs] def evaluate_pbn(pbn, experiments, output_dir: str = '.', generate_plots: bool = True, generate_report: bool = True, config: dict = None): """ Evaluate a PBN directly against experiment data (list or CSV). Parameters: ----------- pbn : ProbabilisticBN The PBN object to evaluate experiments : list or str List of experiment dicts or path to CSV file output_dir : str, default='.' Directory to save output files generate_plots : bool, default=True Whether to generate plots generate_report : bool, default=True Whether to generate evaluation report config : dict, optional Simulation configuration Returns: -------- dict Dictionary with evaluation metrics and file paths """ import os os.makedirs(output_dir, exist_ok=True) from .experiment_data import ExperimentData from .simulation_evaluator import SimulationEvaluator import numpy as np import pandas as pd from scipy.stats import pearsonr import matplotlib.pyplot as plt # Load experiments if needed if isinstance(experiments, str): experiments = ExperimentData.load_from_csv(experiments) # Validate experiments ExperimentData.validate_experiments(experiments, pbn.nodeDict) # Create evaluator evaluator = SimulationEvaluator(pbn, experiments, config) # Simulate all experiments simulation_results = { 'experiments': [], 'predictions': [], 'measurements': [], 'experiment_ids': [], 'measured_nodes': [], 'predicted_values': [], 'measured_values': [] } for i, experiment in enumerate(experiments): try: predicted_steady_state = evaluator._simulate_experiment(experiment) exp_predictions = {} exp_measurements = {} for node_name, measured_value in experiment['measurements'].items(): if node_name in pbn.nodeDict: node_idx = pbn.nodeDict[node_name] predicted_value = predicted_steady_state[node_idx] exp_predictions[node_name] = predicted_value exp_measurements[node_name] = measured_value simulation_results['predicted_values'].append(predicted_value) simulation_results['measured_values'].append(measured_value) simulation_results['measured_nodes'].append(node_name) simulation_results['experiment_ids'].append(experiment.get('id', i+1)) simulation_results['experiments'].append(experiment) simulation_results['predictions'].append(exp_predictions) simulation_results['measurements'].append(exp_measurements) except Exception as e: print(f" Warning: Failed to simulate experiment {i+1}: {str(e)}") # Calculate metrics predicted = np.array(simulation_results['predicted_values']) measured = np.array(simulation_results['measured_values']) metrics = {} if len(predicted) > 0 and len(measured) > 0: correlation, p_value = pearsonr(predicted, measured) mse = np.mean((predicted - measured) ** 2) rmse = np.sqrt(mse) mae = np.mean(np.abs(predicted - measured)) ss_res = np.sum((measured - predicted) ** 2) ss_tot = np.sum((measured - np.mean(measured)) ** 2) r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0 # Per-node metrics node_metrics = {} nodes = set(simulation_results['measured_nodes']) for node in nodes: node_indices = [i for i, n in enumerate(simulation_results['measured_nodes']) if n == node] node_pred = predicted[node_indices] node_meas = measured[node_indices] if len(node_pred) > 1: node_corr, node_p = pearsonr(node_pred, node_meas) node_mse = np.mean((node_pred - node_meas) ** 2) node_mae = np.mean(np.abs(node_pred - node_meas)) node_metrics[node] = { 'correlation': node_corr, 'p_value': node_p, 'mse': node_mse, 'mae': node_mae, 'n_points': len(node_pred) } metrics = { 'overall': { 'correlation': correlation, 'p_value': p_value, 'mse': mse, 'rmse': rmse, 'mae': mae, 'r_squared': r_squared, 'n_points': len(predicted) }, 'per_node': node_metrics } else: print("Warning: No data available for evaluation.") # Plotting plot_paths = {} if generate_plots and len(predicted) > 0: # Scatter plot fig, ax = plt.subplots(figsize=(8, 6)) unique_nodes = list(set(simulation_results['measured_nodes'])) colors = plt.cm.Set3(np.linspace(0, 1, len(unique_nodes))) node_color_map = dict(zip(unique_nodes, colors)) for node in unique_nodes: node_indices = [i for i, n in enumerate(simulation_results['measured_nodes']) if n == node] node_pred = predicted[node_indices] node_meas = measured[node_indices] ax.scatter(node_meas, node_pred, c=[node_color_map[node]], label=node, alpha=0.7, s=60) min_val = min(np.min(predicted), np.min(measured)) max_val = max(np.max(predicted), np.max(measured)) from scipy import stats slope, intercept, r_value, p_value, std_err = stats.linregress(measured, predicted) x_reg = np.linspace(min_val, max_val, 100) y_reg = slope * x_reg + intercept ax.plot(x_reg, y_reg, 'g-', linewidth=2, alpha=0.8, label='Regression line') residuals = predicted - (slope * measured + intercept) mse_residuals = np.mean(residuals**2) # confidence_interval = 1.96 * np.sqrt(mse_residuals) # ax.fill_between(x_reg, y_reg - confidence_interval, y_reg + confidence_interval, alpha=0.2, color='green', label='95% Confidence interval') for i, (meas, pred, exp_id) in enumerate(zip(measured, predicted, simulation_results['experiment_ids'])): ax.annotate(f'E{exp_id}', (meas, pred), xytext=(5, 5), textcoords='offset points', fontsize=8, alpha=0.7) ax.set_xlabel('Experimental Values', fontsize=12) ax.set_ylabel('Predicted Values', fontsize=12) # title = f'Predicted vs Experimental (r={metrics["overall"]["correlation"]:.3f}, p={metrics["overall"]["p_value"]:.3e}, MSE={metrics["overall"]["mse"]:.6f})' title = f'Predicted vs Experimental (r={metrics["overall"]["correlation"]:.3f}, p={metrics["overall"]["p_value"]:.3e})' ax.set_title(title, fontsize=12, fontweight='bold') ax.grid(True, alpha=0.3) if len(unique_nodes) <= 10: ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left') else: ax.legend() # ax.set_aspect('equal', adjustable='box') ax.set_xlim(np.min(measured) - 0.05, np.max(measured) + 0.05) ax.set_ylim(np.min(predicted) - 0.05, np.max(predicted) + 0.05) plt.tight_layout() plot_path = os.path.join(output_dir, "prediction_vs_experimental.png") plt.savefig(plot_path, dpi=300, bbox_inches='tight') plot_paths['prediction_vs_experimental'] = plot_path plt.close(fig) # Residual plot fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) residuals = predicted - measured ax1.scatter(predicted, residuals, alpha=0.7, s=60) ax1.axhline(y=0, color='r', linestyle='--', linewidth=2) ax1.set_xlabel('Predicted Values') ax1.set_ylabel('Residuals (Predicted - Measured)') ax1.set_title('Residuals vs Predicted Values') ax1.grid(True, alpha=0.3) ax2.hist(residuals, bins=min(20, len(residuals)//3), alpha=0.7, edgecolor='black') ax2.axvline(x=0, color='r', linestyle='--', linewidth=2) ax2.set_xlabel('Residuals') ax2.set_ylabel('Frequency') ax2.set_title('Distribution of Residuals') ax2.grid(True, alpha=0.3) mean_residual = np.mean(residuals) std_residual = np.std(residuals) ax2.text(0.02, 0.98, f'Mean: {mean_residual:.4f}\nStd: {std_residual:.4f}', transform=ax2.transAxes, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) plt.tight_layout() residual_path = os.path.join(output_dir, "residual_analysis.png") plt.savefig(residual_path, dpi=300, bbox_inches='tight') plot_paths['residual_analysis'] = residual_path plt.close(fig) # Report report_path = None if generate_report and metrics: report_lines = [] report_lines.append("="*60) report_lines.append("PBN EVALUATION REPORT") report_lines.append("="*60) report_lines.append("") overall = metrics['overall'] report_lines.append("OVERALL PERFORMANCE:") report_lines.append("-" * 20) report_lines.append(f"Pearson Correlation: {overall['correlation']:.4f}") report_lines.append(f"P-value: {overall['p_value']:.6e}") report_lines.append(f"R-squared: {overall['r_squared']:.4f}") report_lines.append(f"Mean Squared Error: {overall['mse']:.6f}") report_lines.append(f"Root Mean Squared Error: {overall['rmse']:.6f}") report_lines.append(f"Mean Absolute Error: {overall['mae']:.6f}") report_lines.append(f"Number of data points: {overall['n_points']}") report_lines.append("") if metrics['per_node']: report_lines.append("PER-NODE PERFORMANCE:") report_lines.append("-" * 20) for node, m in metrics['per_node'].items(): report_lines.append(f"{node}:") report_lines.append(f" Correlation: {m['correlation']:.4f} (p = {m['p_value']:.4e})") report_lines.append(f" MSE: {m['mse']:.6f}") report_lines.append(f" MAE: {m['mae']:.6f}") report_lines.append(f" Data points: {m['n_points']}") report_lines.append("") report_text = "\n".join(report_lines) report_path = os.path.join(output_dir, "evaluation_report.txt") with open(report_path, 'w') as f: f.write(report_text) print(f"Evaluation report saved to {report_path}") # Export CSV csv_path = os.path.join(output_dir, "detailed_results.csv") data = { 'Experiment_ID': simulation_results['experiment_ids'], 'Node': simulation_results['measured_nodes'], 'Predicted_Value': simulation_results['predicted_values'], 'Measured_Value': simulation_results['measured_values'], 'Residual': np.array(simulation_results['predicted_values']) - np.array(simulation_results['measured_values']), 'Absolute_Error': np.abs(np.array(simulation_results['predicted_values']) - np.array(simulation_results['measured_values'])) } df = pd.DataFrame(data) df.to_csv(csv_path, index=False) print(f"Results exported to {csv_path}") return { 'metrics': metrics, 'plot_paths': plot_paths, 'report_path': report_path, 'csv_path': csv_path, 'simulation_results': simulation_results }