-
Notifications
You must be signed in to change notification settings - Fork 1
feat(evaluation): add automatic visualization for model evaluation #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,17 @@ | |
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| import matplotlib.pyplot as plt | ||
| import seaborn as sns | ||
| from typing import Dict, Any, Optional | ||
| import warnings | ||
| import os | ||
|
|
||
| from sklearn.metrics import ( | ||
| accuracy_score, precision_score, recall_score, f1_score, | ||
| roc_auc_score, confusion_matrix, classification_report, | ||
| mean_squared_error, mean_absolute_error, r2_score | ||
| mean_squared_error, mean_absolute_error, r2_score, | ||
| roc_curve, precision_recall_curve | ||
| ) | ||
|
|
||
| from mltools.utils import Config, get_logger | ||
|
|
@@ -25,6 +29,7 @@ class ModelEvaluator: | |
| - Classification and regression support | ||
| - Confusion matrix analysis | ||
| - Performance reports | ||
| - Visualizations (Confusion Matrix, ROC Curve, Residuals) | ||
| """ | ||
|
|
||
| def __init__(self, config: Optional[Config] = None): | ||
|
|
@@ -37,12 +42,18 @@ def __init__(self, config: Optional[Config] = None): | |
| self.config = config or Config() | ||
| self.logger = get_logger('ModelEvaluator') | ||
| self.results = {} | ||
|
|
||
| # Create plots directory if it doesn't exist | ||
| self.plots_dir = 'plots' | ||
| if not os.path.exists(self.plots_dir): | ||
| os.makedirs(self.plots_dir) | ||
|
|
||
| def evaluate_classification( | ||
| self, | ||
| y_true: np.ndarray, | ||
| y_pred: np.ndarray, | ||
| y_pred_proba: Optional[np.ndarray] = None | ||
| y_pred_proba: Optional[np.ndarray] = None, | ||
| plot: bool = False | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Evaluate classification model | ||
|
|
@@ -51,6 +62,7 @@ def evaluate_classification( | |
| y_true: True labels | ||
| y_pred: Predicted labels | ||
| y_pred_proba: Predicted probabilities (optional) | ||
| plot: Whether to generate and save plots | ||
|
|
||
| Returns: | ||
| Dictionary of evaluation metrics | ||
|
|
@@ -78,7 +90,8 @@ def evaluate_classification( | |
| self.logger.warning(f"Could not compute ROC AUC: {str(e)}") | ||
| metrics['roc_auc'] = None | ||
|
|
||
| metrics['confusion_matrix'] = confusion_matrix(y_true, y_pred).tolist() | ||
| cm = confusion_matrix(y_true, y_pred) | ||
| metrics['confusion_matrix'] = cm.tolist() | ||
|
|
||
| try: | ||
| report = classification_report(y_true, y_pred, output_dict=True, zero_division=0) | ||
|
|
@@ -89,19 +102,26 @@ def evaluate_classification( | |
| self.results = metrics | ||
| self._log_results(metrics) | ||
|
|
||
| if plot: | ||
| self.plot_confusion_matrix(cm) | ||
| if y_pred_proba is not None and len(np.unique(y_true)) == 2: | ||
| self.plot_roc_curve(y_true, y_pred_proba[:, 1]) | ||
|
Comment on lines
+105
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a caller passes a 1-D array of positive-class probabilities for a binary problem (a common shape returned by some estimators / wrappers), line 108 will raise 🛠️ Proposed fix- if plot:
- self.plot_confusion_matrix(cm)
- if y_pred_proba is not None and len(np.unique(y_true)) == 2:
- self.plot_roc_curve(y_true, y_pred_proba[:, 1])
+ if plot:
+ self.plot_confusion_matrix(cm)
+ if y_pred_proba is not None and len(np.unique(y_true)) == 2:
+ proba_pos = y_pred_proba[:, 1] if y_pred_proba.ndim == 2 else y_pred_proba
+ self.plot_roc_curve(y_true, proba_pos)🤖 Prompt for AI Agents |
||
|
|
||
| return metrics | ||
|
|
||
| def evaluate_regression( | ||
| self, | ||
| y_true: np.ndarray, | ||
| y_pred: np.ndarray | ||
| y_pred: np.ndarray, | ||
| plot: bool = False | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Evaluate regression model | ||
|
|
||
| Parameters: | ||
| y_true: True values | ||
| y_pred: Predicted values | ||
| plot: Whether to generate and save plots | ||
|
|
||
| Returns: | ||
| Dictionary of evaluation metrics | ||
|
|
@@ -122,8 +142,56 @@ def evaluate_regression( | |
| self.results = metrics | ||
| self._log_results(metrics) | ||
|
|
||
| if plot: | ||
| self.plot_residuals(y_true, y_pred) | ||
|
|
||
| return metrics | ||
|
|
||
| def plot_confusion_matrix(self, cm: np.ndarray, filename: str = 'confusion_matrix.png'): | ||
| """Plot and save confusion matrix""" | ||
| plt.figure(figsize=(10, 7)) | ||
| sns.heatmap(cm, annot=True, fmt='d', cmap='Blues') | ||
| plt.title('Confusion Matrix') | ||
| plt.ylabel('Actual') | ||
| plt.xlabel('Predicted') | ||
| path = os.path.join(self.plots_dir, filename) | ||
| plt.savefig(path) | ||
| plt.close() | ||
| self.logger.info(f"Confusion matrix plot saved to {path}") | ||
|
|
||
| def plot_roc_curve(self, y_true: np.ndarray, y_score: np.ndarray, filename: str = 'roc_curve.png'): | ||
| """Plot and save ROC curve""" | ||
| fpr, tpr, _ = roc_curve(y_true, y_score) | ||
| roc_auc = roc_auc_score(y_true, y_score) | ||
|
|
||
| plt.figure(figsize=(10, 7)) | ||
| plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})') | ||
| plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--') | ||
| plt.xlim([0.0, 1.0]) | ||
| plt.ylim([0.0, 1.05]) | ||
| plt.xlabel('False Positive Rate') | ||
| plt.ylabel('True Positive Rate') | ||
| plt.title('Receiver Operating Characteristic (ROC)') | ||
| plt.legend(loc="lower right") | ||
| path = os.path.join(self.plots_dir, filename) | ||
| plt.savefig(path) | ||
| plt.close() | ||
| self.logger.info(f"ROC curve plot saved to {path}") | ||
|
|
||
| def plot_residuals(self, y_true: np.ndarray, y_pred: np.ndarray, filename: str = 'residuals.png'): | ||
| """Plot and save residuals plot""" | ||
| residuals = y_true - y_pred | ||
| plt.figure(figsize=(10, 7)) | ||
| plt.scatter(y_pred, residuals, alpha=0.5) | ||
| plt.axhline(y=0, color='r', linestyle='--') | ||
| plt.xlabel('Predicted Values') | ||
| plt.ylabel('Residuals') | ||
| plt.title('Residuals Plot') | ||
| path = os.path.join(self.plots_dir, filename) | ||
| plt.savefig(path) | ||
| plt.close() | ||
| self.logger.info(f"Residuals plot saved to {path}") | ||
|
|
||
| def _log_results(self, metrics: Dict[str, Any]): | ||
| """Log evaluation results""" | ||
| self.logger.info("Evaluation Results:") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid creating
plots/as an unconditional side effect of__init__.The directory is created on every
ModelEvaluator()instantiation, even when the caller never setsplot=True. This pollutes the current working directory (which is whatever the user happens to be running from) and surprises consumers who only want metrics. Defer creation to the point where a plot is actually being saved, and useexist_ok=Trueto avoid the TOCTOU betweenos.path.existsandos.makedirs.🛠️ Proposed fix
def __init__(self, config: Optional[Config] = None): ... self.config = config or Config() self.logger = get_logger('ModelEvaluator') self.results = {} - - # Create plots directory if it doesn't exist - self.plots_dir = 'plots' - if not os.path.exists(self.plots_dir): - os.makedirs(self.plots_dir) + self.plots_dir = 'plots' + + def _ensure_plots_dir(self) -> None: + os.makedirs(self.plots_dir, exist_ok=True)Then call
self._ensure_plots_dir()at the start of eachplot_*helper before computing the path.🤖 Prompt for AI Agents