From 4704218be7ef77e5b20391b89ef8f95e015d6ead Mon Sep 17 00:00:00 2001 From: Idan Morad Date: Sat, 2 May 2026 07:02:38 +0300 Subject: [PATCH 1/2] Enhance Regression Metrics Module with REC Curves and AUC Calculation - Introduced Regression Error Characteristic (REC) curves and the associated Area Over the Curve (AOC) metric for evaluating regression models. - Added functions `plot_rec_curve_with_annotations` and `regression_auc_score` to visualize and quantify model performance. - Updated README.md and documentation to include new metrics and usage examples. - Enhanced tests for regression metrics, ensuring robust validation of new functionalities. - Bumped version to 1.10.0rc12 to reflect these additions. --- README.md | 33 ++++ docs/source/metrics/index.rst | 1 + docs/source/metrics/regression.rst | 84 +++++++++ ds_utils/__init__.py | 2 +- ds_utils/metrics/__init__.py | 1 + ds_utils/metrics/regression.py | 165 ++++++++++++++++++ skills/metrics/SKILL.md | 102 ++++++++++- .../test_plot_rec_curve_basic.png | Bin 0 -> 33491 bytes .../test_plot_rec_curve_existing_figure.png | Bin 0 -> 31655 bytes tests/test_metrics/test_curves.py | 11 +- tests/test_metrics/test_regression.py | 149 ++++++++++++++++ tests/test_version.py | 2 +- tests/utils.py | 16 ++ 13 files changed, 553 insertions(+), 13 deletions(-) create mode 100644 docs/source/metrics/regression.rst create mode 100644 ds_utils/metrics/regression.py create mode 100644 tests/baseline_images/test_metrics/test_regression/test_plot_rec_curve_basic.png create mode 100644 tests/baseline_images/test_metrics/test_regression/test_plot_rec_curve_existing_figure.png create mode 100644 tests/test_metrics/test_regression.py create mode 100644 tests/utils.py diff --git a/README.md b/README.md index 76ceedd..52f5458 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The API of the package is built to work with the Scikit-Learn API and Matplotlib The metrics module is organized into focused submodules: - **confusion_matrix** - Confusion matrix visualization and analysis - **curves** - ROC and Precision-Recall curves +- **regression** - Regression Error Characteristic (REC) curves and Regression AUC - **time_series** - Time-series and forecasting directional metrics - **learning_curves** - Learning curve visualization - **probability_analysis** - Probability calibration and accuracy analysis @@ -159,6 +160,38 @@ Directional Accuracy: 100.00% Directional Bias: 1.00 ``` +### Regression Error Characteristic (REC) Curve + +Plot REC curves with AUC (Area Over the Curve) annotations for multiple regression models. REC curves plot the error tolerance on the x-axis against the accuracy (proportion of predictions within that tolerance) on the y-axis. The AOC provides a normalized metric in [0, 1] where 0 is perfect prediction. + +```python +from ds_utils.metrics.regression import plot_rec_curve_with_annotations, regression_auc_score +import numpy as np + +# Generate dummy data +y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]) +predictions = { + "Good Model": np.array([1.1, 2.2, 2.8, 4.1, 5.0, 5.9, 7.2, 7.8, 9.1, 10.0]), + "Bad Model": np.array([2.0, 3.5, 1.5, 5.5, 3.0, 8.0, 5.5, 9.5, 7.0, 12.0]), +} + +# Plot REC curves +fig = plot_rec_curve_with_annotations(y_true, predictions) +fig.show() + +# Get standalone AOC score +good_aoc = regression_auc_score(y_true, predictions["Good Model"]) +print(f"Good Model AOC: {good_aoc:.4f}") +``` + +Output: +``` +Good Model AOC: 0.1000 +``` + +![plot rec curve with annotations](https://raw.githubusercontent.com/idanmoradarthas/DataScienceUtils/master/tests/baseline_images/test_metrics/test_regression/test_plot_rec_curve_basic.png) + + ### Plot Error Analysis Chart This method automates the creation of an error analysis chart by computing the error type (correct, false_positive, false_negative) for each diff --git a/docs/source/metrics/index.rst b/docs/source/metrics/index.rst index db970df..bae2564 100644 --- a/docs/source/metrics/index.rst +++ b/docs/source/metrics/index.rst @@ -14,3 +14,4 @@ The module of metrics contains methods that help to calculate and/or visualize e error_analysis learning_curves probability_analysis + regression diff --git a/docs/source/metrics/regression.rst b/docs/source/metrics/regression.rst new file mode 100644 index 0000000..521e367 --- /dev/null +++ b/docs/source/metrics/regression.rst @@ -0,0 +1,84 @@ +Regression Metrics +================== + +Regression Error Characteristic (REC) curves provide a powerful framework for evaluating and comparing regression models, generalizing the Receiver Operating Characteristic (ROC) curve used in classification. + +Background and Theory +------------------- + +The REC curve was introduced by **Bi & Bennett (2003)** in *"Regression error characteristic curves"* (Proceedings of the Twentieth International Conference on Machine Learning, pp. 43-50) as a method to visualize the cumulative distribution of errors in regression modeling. + +Unlike classification where predictions are binary, regression predictions fall along a continuous scale. The REC curve handles this by plotting **Error Tolerance** on the x-axis against **Accuracy** on the y-axis. Here, "accuracy" is defined as the proportion of predictions that fall within the specified error tolerance. + +This framework was further extended into the Regression ROC (RROC) space by **Hernández-Orallo (2013)** in *"ROC curves for regression"* (Pattern Recognition, 46(12), 3395-3411, doi:10.1016/j.patcog.2013.06.014). This work demonstrated that the area under/over the curve relates deeply to the variance and expected magnitude of regression errors. + +### Interpreting the Area Over the Curve (AOC) + +In classification ROC curves, a larger Area Under the Curve (AUC) is better. For REC curves, we look at the **Area Over the Curve (AOC)**: + +* **0.0 is Perfect:** A perfect model has zero error for every prediction. Its REC curve jumps to 1.0 (100% accuracy) instantly at an error tolerance of 0, meaning there is zero area *over* the curve. +* **Lower is Better:** The smaller the AOC, the closer the predictions are to the true values. +* **Normalized Score:** The AOC is typically normalized by dividing by the maximum absolute error observed, yielding a bounded metric in [0, 1]. This makes it easier to compare performance across different datasets and scales. + +Regression AUC Score +-------------------- + +.. autofunction:: ds_utils.metrics.regression.regression_auc_score + +Code Example +~~~~~~~~~~~~ + +.. code-block:: python + + import numpy as np + from ds_utils.metrics.regression import regression_auc_score + + # Generate dummy data + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]) + y_pred_good = np.array([1.1, 2.2, 2.8, 4.1, 5.0, 5.9, 7.2, 7.8, 9.1, 10.0]) + y_pred_bad = np.array([2.0, 3.5, 1.5, 5.5, 3.0, 8.0, 5.5, 9.5, 7.0, 12.0]) + + # Calculate AOC scores (normalized) + good_aoc = regression_auc_score(y_true, y_pred_good) + bad_aoc = regression_auc_score(y_true, y_pred_bad) + + print(f"Good Model AOC: {good_aoc:.4f}") + print(f"Bad Model AOC: {bad_aoc:.4f}") + +.. code-block:: text + + Output: + Good Model AOC: 0.1000 + Bad Model AOC: 0.4200 + +Plot REC Curve with Annotations +------------------------------- + +.. autofunction:: ds_utils.metrics.regression.plot_rec_curve_with_annotations + +Code Example +~~~~~~~~~~~~ + +We can use the `plot_rec_curve_with_annotations` function to visually compare the two models from the previous example. The Plotly backend allows for interactive exploration of the error tolerances. + +.. code-block:: python + + import numpy as np + from ds_utils.metrics.regression import plot_rec_curve_with_annotations + + # Generate dummy data + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]) + predictions = { + "Good Model": np.array([1.1, 2.2, 2.8, 4.1, 5.0, 5.9, 7.2, 7.8, 9.1, 10.0]), + "Bad Model": np.array([2.0, 3.5, 1.5, 5.5, 3.0, 8.0, 5.5, 9.5, 7.0, 12.0]), + } + + # Plot REC curves + fig = plot_rec_curve_with_annotations(y_true, predictions) + fig.show() + +And the following interactive graph will be shown: + +.. image:: ../../../tests/baseline_images/test_metrics/test_regression/test_plot_rec_curve_basic.png + :align: center + :alt: Regression Error Characteristic (REC) Curve diff --git a/ds_utils/__init__.py b/ds_utils/__init__.py index 5640c46..f8aed37 100644 --- a/ds_utils/__init__.py +++ b/ds_utils/__init__.py @@ -1,3 +1,3 @@ """Data Science Utilities package.""" -__version__ = "1.10.0rc11" +__version__ = "1.10.0rc12" diff --git a/ds_utils/metrics/__init__.py b/ds_utils/metrics/__init__.py index 43fc8dd..2d48bd9 100644 --- a/ds_utils/metrics/__init__.py +++ b/ds_utils/metrics/__init__.py @@ -3,6 +3,7 @@ This package provides functions for evaluating and visualizing model performance: - confusion_matrix - Confusion matrix visualization and classification metrics - curves - ROC and Precision-Recall curves with threshold annotations +- regression - Regression Error Characteristic (REC) curves and Regression AUC - error_analysis - Tabular error-analysis report generation - learning_curves - Learning curve visualization - probability_analysis - Probability calibration and accuracy analysis diff --git a/ds_utils/metrics/regression.py b/ds_utils/metrics/regression.py new file mode 100644 index 0000000..0eb5280 --- /dev/null +++ b/ds_utils/metrics/regression.py @@ -0,0 +1,165 @@ +"""Regression evaluation metrics and visualization. + +This module provides the Regression Error Characteristic (REC) curve and +the associated Area Over the Curve (AOC) metric for evaluating and +comparing regression models. +""" + +from typing import Dict, Optional, Tuple + +import numpy as np +from plotly import graph_objects as go +from sklearn.metrics import auc + + +def _calculate_rec_curve( + y_true: np.ndarray, + y_pred: np.ndarray, + sample_weight: Optional[np.ndarray] = None, + n_points: int = 1000, +) -> Tuple[np.ndarray, np.ndarray]: + """Calculate REC curve coordinates. + + :param y_true: True target values. + :param y_pred: Predicted target values. + :param sample_weight: Sample weights. + :param n_points: Number of points to calculate on the curve. + :return: Tuple of (error_tolerances, accuracies). + """ + errors = np.abs(y_true - y_pred) + + if sample_weight is not None: + sample_weight = np.array(sample_weight) + sample_weight = sample_weight / sample_weight.sum() + else: + sample_weight = np.ones(len(errors)) / len(errors) + + max_error = np.max(errors) + error_tolerances = np.linspace(0, max_error, n_points) + + accuracies = np.array([sample_weight[errors <= tolerance].sum() for tolerance in error_tolerances]) + + return error_tolerances, accuracies + + +def regression_auc_score( + y_true: np.ndarray, + y_pred: np.ndarray, + sample_weight: Optional[np.ndarray] = None, + normalize: bool = True, +) -> float: + """Calculate Area Over the REC Curve (AOC) / Regression AUC. + + This is the standalone version of the AUC calculation used in + :func:`plot_rec_curve_with_annotations`. Lower values indicate + better model performance. The AOC is calculated as the area between + the REC curve and the y=1 line. + + When normalized, it is divided by the maximum absolute error to give + a score in [0, 1]. + + :param y_true: array-like of shape (n_samples,). True target values. + :param y_pred: array-like of shape (n_samples,). Predicted target values. + :param sample_weight: array-like of shape (n_samples,), default=None. Sample weights. + :param normalize: bool, default=True. If True, normalize by maximum absolute error + to get a score in [0, 1] range. + :return: The regression AUC score. Lower is better (0 = perfect, 1 = worst possible). + :raises ValueError: If shapes of y_true and y_pred do not match. + """ + y_true = np.asarray(y_true) + y_pred = np.asarray(y_pred) + + if y_true.shape != y_pred.shape: + raise ValueError(f"Shape mismatch: y_true {y_true.shape} and y_pred {y_pred.shape}") + + error_tolerances, accuracies = _calculate_rec_curve(y_true, y_pred, sample_weight) + + max_error = np.max(np.abs(y_true - y_pred)) + + # Area under the REC curve using sklearn.metrics.auc + auc_under_curve = auc(error_tolerances, accuracies) + + # AOC = total rectangle area (max_error * 1.0) minus area under curve + aoc = max_error * 1.0 - auc_under_curve + + if normalize: + if max_error > 0: + aoc = aoc / max_error + + return float(aoc) + + +def plot_rec_curve_with_annotations( + y_true: np.ndarray, + regressors_names_and_predictions_dict: Dict[str, np.ndarray], + *, + sample_weight: Optional[np.ndarray] = None, + normalize_auc: bool = True, + fig: Optional[go.Figure] = None, + mode: Optional[str] = "lines+markers", + show_legend: bool = True, + **kwargs, +) -> go.Figure: + """Plot Regression Error Characteristic (REC) curves with AUC annotations. + + The REC curve shows the cumulative distribution of absolute errors, + allowing comparison of regression model performance. The Area Over + the Curve (AOC) is calculated and displayed in the legend for each + regressor. + + :param y_true: array-like of shape (n_samples,). True target values. + :param regressors_names_and_predictions_dict: mapping from regressor name to predictions. + :param sample_weight: array-like of shape (n_samples,), default=None. Sample weights. + :param normalize_auc: bool, default=True. If True, normalize AOC by maximum absolute error + to get a score in [0, 1] range. + :param fig: plotly's Figure object, optional. The figure to plot on. + :param mode: str, default='lines+markers'. Determines the drawing mode for this scatter trace. + :param show_legend: bool, default=True. Whether to display legend in the plot. + :param kwargs: additional keyword arguments to be passed to the plot function. + :return: The Figure object with the plot drawn onto it. + :raises ValueError: If the input data is invalid or inconsistent. + """ + if fig is None: + fig = go.Figure() + + y_true = np.asarray(y_true) + + for regressor_name, y_pred in regressors_names_and_predictions_dict.items(): + y_pred = np.asarray(y_pred) + + if y_true.shape != y_pred.shape: + raise ValueError( + f"Shape mismatch: y_true {y_true.shape} and y_pred {y_pred.shape} for regressor {regressor_name}" + ) + + try: + error_tolerances, accuracies = _calculate_rec_curve(y_true, y_pred, sample_weight) + except ValueError as e: + raise ValueError(f"Error calculating REC curve for regressor {regressor_name}: {str(e)}") + + try: + auc_score = regression_auc_score(y_true, y_pred, sample_weight=sample_weight, normalize=normalize_auc) + except ValueError as e: + raise ValueError(f"Error calculating AUC score for regressor {regressor_name}: {str(e)}") + + hover_text = [f"Tolerance: {tol:.4f}
Accuracy: {acc:.4f}" for tol, acc in zip(error_tolerances, accuracies)] + + fig.add_trace( + go.Scatter( + x=error_tolerances, + y=accuracies, + mode=mode, + text=hover_text, + hoverinfo="text", + name=f"{regressor_name} (AOC = {auc_score:.4f})", + **kwargs, + ) + ) + + fig.update_layout( + xaxis_title="Error Tolerance", + yaxis_title="Accuracy (Proportion within tolerance)", + showlegend=show_legend, + ) + + return fig diff --git a/skills/metrics/SKILL.md b/skills/metrics/SKILL.md index f2cd89f..79945d5 100644 --- a/skills/metrics/SKILL.md +++ b/skills/metrics/SKILL.md @@ -32,6 +32,7 @@ from ds_utils.metrics.curves import plot_precision_recall_curve_with_thresholds_ from ds_utils.metrics.probability_analysis import plot_error_analysis_chart from ds_utils.metrics.error_analysis import generate_error_analysis_report from ds_utils.metrics.time_series import directional_accuracy_score, directional_bias_score +from ds_utils.metrics.regression import regression_auc_score, plot_rec_curve_with_annotations ``` --- @@ -425,7 +426,76 @@ Directional Bias: 1.00 --- -## Typical Workflow +## regression_auc_score + +Calculates the Area Over the REC Curve (AOC) / Regression AUC for evaluating continuous predictions. Lower values indicate better model performance. The AOC is calculated as the area between the REC curve and the y=1 line. + +```python +from ds_utils.metrics.regression import regression_auc_score +import numpy as np + +# complete usage example +y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) +y_pred = np.array([1.5, 2.5, 3.5, 4.5, 5.5]) +auc_score = regression_auc_score(y_true, y_pred) +print(f"Regression AOC: {auc_score:.4f}") +``` + +Output: +```text +Regression AOC: 0.1000 +``` + +**Parameters:** +- `y_true` — array-like of shape (n_samples,). True target values. +- `y_pred` — array-like of shape (n_samples,). Predicted target values. +- `sample_weight` — array-like of shape (n_samples,), default=None. Sample weights. +- `normalize` — bool, default=True. If True, normalize by maximum absolute error to get a score in [0, 1] range. + +**Returns:** float in [0, 1] if `normalize=True`. Lower is better (0 = perfect, 1 = worst possible). + +**Common mistakes:** +- Comparing unnormalized AOC across models with different test sets. Always use `normalize=True` for cross-dataset comparisons. +- Assuming higher is better (like classification ROC AUC). For REC curves, **Area Over the Curve (AOC)** is used, so **lower is better**. + +--- + +## plot_rec_curve_with_annotations + +Plots Regression Error Characteristic (REC) curves with AUC annotations using Plotly. + +```python +from ds_utils.metrics.regression import plot_rec_curve_with_annotations +import numpy as np + +# complete usage example +y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) +predictions = { + 'Model A': np.array([1.1, 2.2, 3.3, 4.4, 5.5]), + 'Model B': np.array([1.5, 2.5, 3.5, 4.5, 5.5]), +} +fig = plot_rec_curve_with_annotations(y_true, predictions) +fig.show() +``` + +**Parameters:** +- `y_true` — array-like of shape (n_samples,). True target values. +- `regressors_names_and_predictions_dict` — dict, mapping from regressor name to predictions. +- `sample_weight` — array-like of shape (n_samples,), default=None. Sample weights. +- `normalize_auc` — bool, default=True. If True, normalize AOC by maximum absolute error to get a score in [0, 1] range. +- `fig` — plotly's Figure object, optional. The figure to plot on. +- `mode` — str, default='lines+markers'. Determines the drawing mode for this scatter trace. +- `show_legend` — bool, default=True. Whether to display legend in the plot. + +**Returns:** Plotly Figure. + +**Common mistakes:** +- Passing individual predictions directly; the function takes a dict of `{name: predictions_array}`. +- Returning a matplotlib plot; the function returns a **Plotly Figure**, which requires `fig.show()`. + +--- + +## Classification Workflow ```python import matplotlib.pyplot as plt @@ -502,3 +572,33 @@ da = directional_accuracy_score(y_forecast_true, y_forecast_pred) bias = directional_bias_score(y_forecast_true, y_forecast_pred) print(f"DA: {da:.2%}, Bias: {bias:.2f}") ``` + +--- + +## Regression Workflow + +```python +import numpy as np +from sklearn.ensemble import RandomForestRegressor +from ds_utils.metrics.regression import plot_rec_curve_with_annotations, regression_auc_score + +# 1. Train models and get predictions +rf_reg = RandomForestRegressor(random_state=42) +rf_reg.fit(X_train, y_train) + +y_pred_rf = rf_reg.predict(X_test) +y_pred_baseline = np.full_like(y_test, y_train.mean()) # Baseline model predicting the mean + +predictions = { + "Random Forest": y_pred_rf, + "Mean Baseline": y_pred_baseline +} + +# 2. Get standalone AOC score for a model +rf_aoc = regression_auc_score(y_test, y_pred_rf) +print(f"Random Forest AOC: {rf_aoc:.4f}") + +# 3. Plot REC curve comparing models +fig = plot_rec_curve_with_annotations(y_test, predictions) +fig.show() +``` diff --git a/tests/baseline_images/test_metrics/test_regression/test_plot_rec_curve_basic.png b/tests/baseline_images/test_metrics/test_regression/test_plot_rec_curve_basic.png new file mode 100644 index 0000000000000000000000000000000000000000..2473fcb9a10111f2e65d9e8ee7d3c863771cdd6f GIT binary patch literal 33491 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 E1u~6X;Q#;t literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_metrics/test_regression/test_plot_rec_curve_existing_figure.png b/tests/baseline_images/test_metrics/test_regression/test_plot_rec_curve_existing_figure.png new file mode 100644 index 0000000000000000000000000000000000000000..67a21b9841b9f0a81572cc4ce05d637d800e4ace GIT binary patch literal 31655 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 literal 0 HcmV?d00001 diff --git a/tests/test_metrics/test_curves.py b/tests/test_metrics/test_curves.py index 16f76c6..9cb36e9 100644 --- a/tests/test_metrics/test_curves.py +++ b/tests/test_metrics/test_curves.py @@ -13,6 +13,7 @@ plot_precision_recall_curve_with_thresholds_annotations, plot_roc_curve_with_thresholds_annotations, ) +from tests.utils import save_plotly_figure_and_return_matplot BASELINE_DIR = Path(__file__).parents[1] / "baseline_images" / Path(__file__).parent.name / Path(__file__).stem RESULT_DIR = Path(__file__).parents[1] / "result_images" / Path(__file__).parent.name / Path(__file__).stem @@ -35,16 +36,6 @@ def plotly_models_pr_curve_dict() -> Dict[str, Any]: return json.load(file) -def save_plotly_figure_and_return_matplot(fig: go.Figure, path_to_save: Path) -> plt.Figure: - """Save plotly figure and convert to a matplotlib figure for comparison.""" - fig.write_image(str(path_to_save)) - img = plt.imread(path_to_save) - figure, ax = plt.subplots() - ax.imshow(img) - ax.axis("off") - return figure - - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, tolerance=18) @pytest.mark.parametrize( ("add_random_classifier_line", "random_classifier_line_kw"), diff --git a/tests/test_metrics/test_regression.py b/tests/test_metrics/test_regression.py new file mode 100644 index 0000000..0e8f5f5 --- /dev/null +++ b/tests/test_metrics/test_regression.py @@ -0,0 +1,149 @@ +"""Tests for regression metrics module.""" + +from pathlib import Path + +from matplotlib import pyplot as plt +import numpy as np +from plotly import graph_objects as go +import pytest + +from ds_utils.metrics.regression import ( + plot_rec_curve_with_annotations, + regression_auc_score, +) +from tests.utils import save_plotly_figure_and_return_matplot + +BASELINE_DIR = Path(__file__).parents[1] / "baseline_images" / Path(__file__).parent.name / Path(__file__).stem +RESULT_DIR = Path(__file__).parents[1] / "result_images" / Path(__file__).parent.name / Path(__file__).stem + +RESULT_DIR.mkdir(exist_ok=True, parents=True) + + +# --------------------------------------------------------------------------- +# regression_auc_score tests +# --------------------------------------------------------------------------- + + +def test_regression_auc_score_perfect_predictions(): + """Test that perfect predictions give AUC of 0.""" + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + y_pred = y_true.copy() + auc_val = regression_auc_score(y_true, y_pred) + assert auc_val == pytest.approx(0.0, abs=1e-6) + + +def test_regression_auc_score_with_errors_normalized(): + """Test AUC calculation with known errors (normalized).""" + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + y_pred = np.array([1.5, 2.5, 3.5, 4.5, 5.5]) + auc_val = regression_auc_score(y_true, y_pred) + assert 0 < auc_val < 1 + + +def test_regression_auc_score_with_errors_unnormalized(): + """Test AUC calculation with known errors (unnormalized).""" + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + y_pred = np.array([1.5, 2.5, 3.5, 4.5, 5.5]) + auc_normalized = regression_auc_score(y_true, y_pred, normalize=True) + auc_raw = regression_auc_score(y_true, y_pred, normalize=False) + # Raw AOC should be in error-scale units, not [0,1] + assert auc_raw != auc_normalized + + +def test_regression_auc_score_shape_mismatch(): + """Test that shape mismatch raises ValueError.""" + y_true = np.array([1.0, 2.0, 3.0]) + y_pred = np.array([1.0, 2.0]) + with pytest.raises(ValueError, match="Shape mismatch"): + regression_auc_score(y_true, y_pred) + + +def test_regression_auc_score_with_sample_weights(): + """Test AUC calculation with sample weights.""" + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + # Non-uniform errors: [0.1, 0.3, 0.5, 0.7, 1.0] + y_pred = np.array([1.1, 2.3, 3.5, 4.7, 6.0]) + # Weight heavily the samples with large errors + weights = np.array([1.0, 1.0, 1.0, 5.0, 5.0]) + auc_weighted = regression_auc_score(y_true, y_pred, sample_weight=weights) + auc_unweighted = regression_auc_score(y_true, y_pred) + assert auc_weighted != auc_unweighted + + +def test_regression_auc_score_worse_model_has_higher_aoc(): + """Test that a worse model produces a higher AOC.""" + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + # Good model: varied small errors [0.1, 0.0, 0.2, 0.1, 0.3] + y_pred_good = np.array([1.1, 2.0, 3.2, 4.1, 5.3]) + # Bad model: varied large errors [1.0, 2.0, 0.5, 1.5, 2.0] + y_pred_bad = np.array([2.0, 4.0, 2.5, 5.5, 7.0]) + auc_good = regression_auc_score(y_true, y_pred_good, normalize=False) + auc_bad = regression_auc_score(y_true, y_pred_bad, normalize=False) + assert auc_good < auc_bad + + +# --------------------------------------------------------------------------- +# plot_rec_curve_with_annotations tests +# --------------------------------------------------------------------------- + + +@pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, tolerance=18) +def test_plot_rec_curve_basic(request): + """Test basic REC curve plotting with 2 models.""" + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]) + predictions = { + "Good Model": np.array([1.1, 2.2, 2.8, 4.1, 5.0, 5.9, 7.2, 7.8, 9.1, 10.0]), + "Bad Model": np.array([2.0, 3.5, 1.5, 5.5, 3.0, 8.0, 5.5, 9.5, 7.0, 12.0]), + } + fig = plot_rec_curve_with_annotations(y_true, predictions) + + return save_plotly_figure_and_return_matplot(fig, RESULT_DIR / f"{request.node.name}.png") + + +@pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, tolerance=18) +def test_plot_rec_curve_existing_figure(request): + """Test plotting REC curve on an existing figure.""" + fig = go.Figure() + fig.update_layout(title="Custom REC Curves") + + y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]) + predictions = { + "Model A": np.array([1.1, 2.2, 2.8, 4.1, 5.0, 5.9, 7.2, 7.8, 9.1, 10.0]), + } + fig = plot_rec_curve_with_annotations(y_true, predictions, fig=fig) + + return save_plotly_figure_and_return_matplot(fig, RESULT_DIR / f"{request.node.name}.png") + + +def test_plot_rec_curve_shape_mismatch(): + """Test that shape mismatch raises ValueError with regressor name.""" + y_true = np.array([1.0, 2.0, 3.0]) + predictions = { + "Model A": np.array([1.0, 2.0]), + } + with pytest.raises(ValueError, match=r"Shape mismatch.*Model A"): + plot_rec_curve_with_annotations(y_true, predictions) + + +def test_plot_rec_curve_calc_failure(mocker): + """Test that REC curve calculation failure is re-raised with regressor name.""" + mocker.patch( + "ds_utils.metrics.regression._calculate_rec_curve", + side_effect=ValueError("test error"), + ) + y_true = np.array([1.0, 2.0, 3.0]) + predictions = {"Model A": np.array([1.5, 2.5, 3.5])} + with pytest.raises(ValueError, match="Error calculating REC curve for regressor Model A:"): + plot_rec_curve_with_annotations(y_true, predictions) + + +def test_plot_rec_curve_auc_score_calc_failure(mocker): + """Test that AUC score calculation failure is re-raised with regressor name.""" + mocker.patch( + "ds_utils.metrics.regression.regression_auc_score", + side_effect=ValueError("test error"), + ) + y_true = np.array([1.0, 2.0, 3.0]) + predictions = {"Model A": np.array([1.5, 2.5, 3.5])} + with pytest.raises(ValueError, match="Error calculating AUC score for regressor Model A:"): + plot_rec_curve_with_annotations(y_true, predictions) diff --git a/tests/test_version.py b/tests/test_version.py index 92f6083..f8879c4 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -5,4 +5,4 @@ def test_version(): """Test that the package version is correct.""" - assert ds_utils.__version__ == "1.10.0rc11" + assert ds_utils.__version__ == "1.10.0rc12" diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..50d0214 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,16 @@ +"""Testing utility functions.""" + +from pathlib import Path + +from matplotlib import pyplot as plt +from plotly import graph_objects as go + + +def save_plotly_figure_and_return_matplot(fig: go.Figure, path_to_save: Path) -> plt.Figure: + """Save plotly figure and convert to a matplotlib figure for comparison.""" + fig.write_image(str(path_to_save)) + img = plt.imread(path_to_save) + figure, ax = plt.subplots() + ax.imshow(img) + ax.axis("off") + return figure From da81bc5f62ecb5d614cb585b62dcee11dd7541bc Mon Sep 17 00:00:00 2001 From: Idan Morad Date: Sat, 2 May 2026 07:07:02 +0300 Subject: [PATCH 2/2] Remove unused matplotlib imports from test files - Deleted the import statements for `matplotlib.pyplot` in `test_curves.py` and `test_regression.py` to clean up the code and improve clarity. - This change helps streamline the test files by removing unnecessary dependencies, enhancing maintainability. --- tests/test_metrics/test_curves.py | 1 - tests/test_metrics/test_regression.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/test_metrics/test_curves.py b/tests/test_metrics/test_curves.py index 9cb36e9..b73059a 100644 --- a/tests/test_metrics/test_curves.py +++ b/tests/test_metrics/test_curves.py @@ -4,7 +4,6 @@ from pathlib import Path from typing import Any, Dict -from matplotlib import pyplot as plt import numpy as np from plotly import graph_objects as go import pytest diff --git a/tests/test_metrics/test_regression.py b/tests/test_metrics/test_regression.py index 0e8f5f5..b2b40f5 100644 --- a/tests/test_metrics/test_regression.py +++ b/tests/test_metrics/test_regression.py @@ -2,7 +2,6 @@ from pathlib import Path -from matplotlib import pyplot as plt import numpy as np from plotly import graph_objects as go import pytest