Skip to content

FIX thresholds should not exceed 1.0 with probabilities in roc_curve #26194

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

Merged
merged 10 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/modules/model_evaluation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,7 @@ function::
>>> tpr
array([0. , 0.5, 0.5, 1. , 1. ])
>>> thresholds
array([1.8 , 0.8 , 0.4 , 0.35, 0.1 ])
array([ inf, 0.8 , 0.4 , 0.35, 0.1 ])

Compared to metrics such as the subset accuracy, the Hamming loss, or the
F1 score, ROC doesn't require optimizing a threshold for each label.
Expand Down
5 changes: 5 additions & 0 deletions doc/whats_new/v1.3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,11 @@ Changelog
- |API| The `eps` parameter of the :func:`log_loss` has been deprecated and will be
removed in 1.5. :pr:`25299` by :user:`Omar Salman <OmarManzoor>`.

- |Fix| In :func:`metrics.roc_curve`, use the threshold value `np.inf` instead of
arbritrary `max(y_score) + 1`. This threshold is associated with the ROC curve point
`tpr=0` and `fpr=0`.
:pr:`26194` by :user:`Guillaume Lemaitre <glemaitre>`.

:mod:`sklearn.model_selection`
..............................

Expand Down
13 changes: 9 additions & 4 deletions sklearn/metrics/_ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,10 +1016,10 @@ def roc_curve(
Increasing true positive rates such that element `i` is the true
positive rate of predictions with score >= `thresholds[i]`.

thresholds : ndarray of shape = (n_thresholds,)
thresholds : ndarray of shape (n_thresholds,)
Decreasing thresholds on the decision function used to compute
fpr and tpr. `thresholds[0]` represents no instances being predicted
and is arbitrarily set to `max(y_score) + 1`.
and is arbitrarily set to `np.inf`.

See Also
--------
Expand All @@ -1036,6 +1036,10 @@ def roc_curve(
are reversed upon returning them to ensure they correspond to both ``fpr``
and ``tpr``, which are sorted in reversed order during their calculation.

An arbritrary threshold is added for the case `tpr=0` and `fpr=0` to
ensure that the curve starts at `(0, 0)`. This threshold corresponds to the
`np.inf`.

References
----------
.. [1] `Wikipedia entry for the Receiver operating characteristic
Expand All @@ -1056,7 +1060,7 @@ def roc_curve(
>>> tpr
array([0. , 0.5, 0.5, 1. , 1. ])
>>> thresholds
array([1.8 , 0.8 , 0.4 , 0.35, 0.1 ])
array([ inf, 0.8 , 0.4 , 0.35, 0.1 ])
"""
fps, tps, thresholds = _binary_clf_curve(
y_true, y_score, pos_label=pos_label, sample_weight=sample_weight
Expand All @@ -1083,7 +1087,8 @@ def roc_curve(
# to make sure that the curve starts at (0, 0)
tps = np.r_[0, tps]
fps = np.r_[0, fps]
thresholds = np.r_[thresholds[0] + 1, thresholds]
# get dtype of `y_score` even if it is an array-like
thresholds = np.r_[np.inf, thresholds]

if fps[-1] <= 0:
warnings.warn(
Expand Down
18 changes: 16 additions & 2 deletions sklearn/metrics/tests/test_ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,13 +418,13 @@ def test_roc_curve_drop_intermediate():
y_true = [0, 0, 0, 0, 1, 1]
y_score = [0.0, 0.2, 0.5, 0.6, 0.7, 1.0]
tpr, fpr, thresholds = roc_curve(y_true, y_score, drop_intermediate=True)
assert_array_almost_equal(thresholds, [2.0, 1.0, 0.7, 0.0])
assert_array_almost_equal(thresholds, [np.inf, 1.0, 0.7, 0.0])

# Test dropping thresholds with repeating scores
y_true = [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
y_score = [0.0, 0.1, 0.6, 0.6, 0.7, 0.8, 0.9, 0.6, 0.7, 0.8, 0.9, 0.9, 1.0]
tpr, fpr, thresholds = roc_curve(y_true, y_score, drop_intermediate=True)
assert_array_almost_equal(thresholds, [2.0, 1.0, 0.9, 0.7, 0.6, 0.0])
assert_array_almost_equal(thresholds, [np.inf, 1.0, 0.9, 0.7, 0.6, 0.0])


def test_roc_curve_fpr_tpr_increasing():
Expand Down Expand Up @@ -2199,3 +2199,17 @@ def test_ranking_metric_pos_label_types(metric, classes):
assert not np.isnan(metric_1).any()
assert not np.isnan(metric_2).any()
assert not np.isnan(thresholds).any()


def test_roc_curve_with_probablity_estimates(global_random_seed):
"""Check that thresholds do not exceed 1.0 when `y_score` is a probability
estimate.

Non-regression test for:
https://github.com/scikit-learn/scikit-learn/issues/26193
"""
rng = np.random.RandomState(global_random_seed)
y_true = rng.randint(0, 2, size=10)
y_score = rng.rand(10)
_, _, thresholds = roc_curve(y_true, y_score)
assert np.isinf(thresholds[0])