Skip to content

feat(evaluation): add automatic visualization for model evaluation - #1

Open
raofal-msodeh wants to merge 1 commit into
Alqudimi:mainfrom
raofal-msodeh:feat/add-evaluation-visualizations
Open

raofal-msodeh wants to merge 1 commit into
Alqudimi:mainfrom
raofal-msodeh:feat/add-evaluation-visualizations

Conversation

@raofal-msodeh

@raofal-msodeh raofal-msodeh commented Apr 24, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added optional visualization support for model evaluation, automatically generating and saving confusion matrices, ROC curves (binary classification), and residual plots.
    • Evaluation plots are saved to a dedicated plots/ directory.
  • Documentation

    • Updated examples demonstrating how to enable and use the new visualization features.

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

These changes add optional visualization support to the ModelEvaluator class, enabling automatic generation and saving of plots (confusion matrix, ROC curve for binary classification, and residuals for regression) when enabled via a plot parameter. The README is updated to document this capability in the classification example.

Changes

Cohort / File(s) Summary
Documentation
README.md
Updated classification example code to show use of predict_proba() and plot=True parameter, documenting that plots are written to plots/ directory.
Visualization Feature
mltools/evaluation/evaluator.py
Added optional plot parameter to evaluate_classification() and evaluate_regression() methods. Imports plotting libraries and filesystem utilities. Creates plots/ directory in __init__. Implements three new public plotting methods (plot_confusion_matrix(), plot_roc_curve(), plot_residuals()) that conditionally generate and save figures based on the plot flag.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A plotter of plots, with matplotlib's might,
Confusion matrices rendered just right,
ROC curves dancing with residual flair,
Saved in the plots/ directory with care! 📊

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(evaluation): add automatic visualization for model evaluation' directly and clearly summarizes the main change: adding visualization capabilities to the evaluation module.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
mltools/evaluation/evaluator.py (2)

47-47: Make the plots directory configurable.

Hardcoding 'plots' ties the output location to the CWD of whichever process imports the evaluator. Surfacing it through Config (e.g. self.config.evaluation.get('plots_dir', 'plots')) or as a constructor argument would let users redirect output for tests, notebooks, or pipelines without monkey-patching.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mltools/evaluation/evaluator.py` at line 47, The plots directory is hardcoded
via self.plots_dir = 'plots'; make it configurable by reading from the Evaluator
constructor or Config: update the Evaluator.__init__ (or the class constructor
that sets self.plots_dir) to accept an optional plots_dir parameter and/or pull
from self.config.evaluation.get('plots_dir', 'plots') and assign that value to
self.plots_dir so callers (or tests/pipelines) can override the output location
without monkey-patching.

150-193: Default filenames silently overwrite prior plots across evaluations.

plot_confusion_matrix, plot_roc_curve, and plot_residuals all default to a fixed filename. If a user evaluates several models (or compares a regression and a re-run) in the same session/CWD, each call clobbers the previous PNG with no warning. Consider including a caller-supplied tag (model name / timestamp) in the default, or at least documenting the overwrite behavior.

🛠️ Sketch
-    def plot_confusion_matrix(self, cm: np.ndarray, filename: str = 'confusion_matrix.png'):
+    def plot_confusion_matrix(self, cm: np.ndarray, filename: Optional[str] = None):
         """Plot and save confusion matrix"""
+        filename = filename or 'confusion_matrix.png'
+        self._ensure_plots_dir()
         plt.figure(figsize=(10, 7))
         ...

Same treatment for the other two helpers; alternatively expose a tag parameter on the public evaluate_* methods that gets threaded through.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mltools/evaluation/evaluator.py` around lines 150 - 193, The three plotting
helpers (plot_confusion_matrix, plot_roc_curve, plot_residuals) currently use
fixed default filenames and silently overwrite prior plots; change their
signatures to accept an optional tag: str = None (or accept filename:
Optional[str] = None) and when no explicit filename is provided, build a unique
filename by appending a caller-supplied tag if given or a timestamp (e.g.
YYYYmmdd_HHMMSS) to the base name before saving; update callers (or public
evaluate_* methods if present) to thread a tag through to these functions so
multi-run outputs do not clobber each other and ensure the logger message uses
the final path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mltools/evaluation/evaluator.py`:
- Around line 105-108: The code assumes y_pred_proba is 2-D (using
y_pred_proba[:, 1]) which breaks if callers pass a 1-D array of positive-class
probabilities; update evaluate_classification (and any ROC/AUC sites) to
normalize y_pred_proba once: if y_pred_proba is 1-D treat it as the
positive-class probabilities, otherwise extract column 1 from a 2-D array, store
that normalized 1-D array (e.g., pos_probs) and use pos_probs for ROC/AUC calls
and for plot_roc_curve to avoid IndexError and duplicate logic; reference
symbols: evaluate_classification, y_pred_proba, plot_roc_curve, and any ROC/AUC
computation lines.
- Around line 45-49: The constructor ModelEvaluator.__init__ currently creates
the 'plots' directory unconditionally via self.plots_dir — defer this side
effect: remove the os.path.exists/os.makedirs calls from __init__, keep
self.plots_dir = 'plots', add a helper method _ensure_plots_dir that calls
os.makedirs(self.plots_dir, exist_ok=True), and call self._ensure_plots_dir() at
the start of each plotting helper (any methods named plot_*). This ensures the
directory is created only when a plot is actually saved and avoids the TOCTOU
race by using exist_ok=True.

---

Nitpick comments:
In `@mltools/evaluation/evaluator.py`:
- Line 47: The plots directory is hardcoded via self.plots_dir = 'plots'; make
it configurable by reading from the Evaluator constructor or Config: update the
Evaluator.__init__ (or the class constructor that sets self.plots_dir) to accept
an optional plots_dir parameter and/or pull from
self.config.evaluation.get('plots_dir', 'plots') and assign that value to
self.plots_dir so callers (or tests/pipelines) can override the output location
without monkey-patching.
- Around line 150-193: The three plotting helpers (plot_confusion_matrix,
plot_roc_curve, plot_residuals) currently use fixed default filenames and
silently overwrite prior plots; change their signatures to accept an optional
tag: str = None (or accept filename: Optional[str] = None) and when no explicit
filename is provided, build a unique filename by appending a caller-supplied tag
if given or a timestamp (e.g. YYYYmmdd_HHMMSS) to the base name before saving;
update callers (or public evaluate_* methods if present) to thread a tag through
to these functions so multi-run outputs do not clobber each other and ensure the
logger message uses the final path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 712977b3-1850-43e4-a76b-01defae49a4b

📥 Commits

Reviewing files that changed from the base of the PR and between a6a321d and a4ad927.

📒 Files selected for processing (2)
  • README.md
  • mltools/evaluation/evaluator.py

Comment on lines +45 to +49

# 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid creating plots/ as an unconditional side effect of __init__.

The directory is created on every ModelEvaluator() instantiation, even when the caller never sets plot=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 use exist_ok=True to avoid the TOCTOU between os.path.exists and os.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 each plot_* helper before computing the path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mltools/evaluation/evaluator.py` around lines 45 - 49, The constructor
ModelEvaluator.__init__ currently creates the 'plots' directory unconditionally
via self.plots_dir — defer this side effect: remove the
os.path.exists/os.makedirs calls from __init__, keep self.plots_dir = 'plots',
add a helper method _ensure_plots_dir that calls os.makedirs(self.plots_dir,
exist_ok=True), and call self._ensure_plots_dir() at the start of each plotting
helper (any methods named plot_*). This ensures the directory is created only
when a plot is actually saved and avoids the TOCTOU race by using exist_ok=True.

Comment on lines +105 to +108
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])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

y_pred_proba[:, 1] assumes a 2-D probability array.

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 IndexError: too many indices for array. The ROC AUC computation on line 82 has the same issue, but here the failure happens after evaluate_classification has already logged metrics, which is more confusing. Consider normalizing the shape once and reusing it.

🛠️ 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
Verify each finding against the current code and only fix it if needed.

In `@mltools/evaluation/evaluator.py` around lines 105 - 108, The code assumes
y_pred_proba is 2-D (using y_pred_proba[:, 1]) which breaks if callers pass a
1-D array of positive-class probabilities; update evaluate_classification (and
any ROC/AUC sites) to normalize y_pred_proba once: if y_pred_proba is 1-D treat
it as the positive-class probabilities, otherwise extract column 1 from a 2-D
array, store that normalized 1-D array (e.g., pos_probs) and use pos_probs for
ROC/AUC calls and for plot_roc_curve to avoid IndexError and duplicate logic;
reference symbols: evaluate_classification, y_pred_proba, plot_roc_curve, and
any ROC/AUC computation lines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant