Skip to content

Commit 3b22ec0

Browse files
authored
MAINT revert zero_division introduced in 1.6 (scikit-learn#30230)
1 parent 200fc7c commit 3b22ec0

File tree

5 files changed

+18
-172
lines changed

5 files changed

+18
-172
lines changed

doc/whats_new/upcoming_changes/sklearn.metrics/28509.feature.rst

Lines changed: 0 additions & 3 deletions
This file was deleted.

doc/whats_new/upcoming_changes/sklearn.metrics/29210.enhancement.rst

Lines changed: 0 additions & 4 deletions
This file was deleted.

doc/whats_new/upcoming_changes/sklearn.metrics/29213.enhancement.rst

Lines changed: 0 additions & 4 deletions
This file was deleted.

sklearn/metrics/_classification.py

Lines changed: 9 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -152,16 +152,10 @@ def _check_targets(y_true, y_pred):
152152
"y_pred": ["array-like", "sparse matrix"],
153153
"normalize": ["boolean"],
154154
"sample_weight": ["array-like", None],
155-
"zero_division": [
156-
Options(Real, {0.0, 1.0, np.nan}),
157-
StrOptions({"warn"}),
158-
],
159155
},
160156
prefer_skip_nested_validation=True,
161157
)
162-
def accuracy_score(
163-
y_true, y_pred, *, normalize=True, sample_weight=None, zero_division="warn"
164-
):
158+
def accuracy_score(y_true, y_pred, *, normalize=True, sample_weight=None):
165159
"""Accuracy classification score.
166160
167161
In multilabel classification, this function computes subset accuracy:
@@ -185,13 +179,6 @@ def accuracy_score(
185179
sample_weight : array-like of shape (n_samples,), default=None
186180
Sample weights.
187181
188-
zero_division : {"warn", 0.0, 1.0, np.nan}, default="warn"
189-
Sets the value to return when there is a zero division,
190-
e.g. when `y_true` and `y_pred` are empty.
191-
If set to "warn", returns 0.0 input, but a warning is also raised.
192-
193-
versionadded:: 1.6
194-
195182
Returns
196183
-------
197184
score : float or int
@@ -234,15 +221,6 @@ def accuracy_score(
234221
y_type, y_true, y_pred = _check_targets(y_true, y_pred)
235222
check_consistent_length(y_true, y_pred, sample_weight)
236223

237-
if _num_samples(y_true) == 0:
238-
if zero_division == "warn":
239-
msg = (
240-
"accuracy() is ill-defined and set to 0.0. Use the `zero_division` "
241-
"param to control this behavior."
242-
)
243-
warnings.warn(msg, UndefinedMetricWarning)
244-
return _check_zero_division(zero_division)
245-
246224
if y_type.startswith("multilabel"):
247225
if _is_numpy_namespace(xp):
248226
differing_labels = count_nonzero(y_true - y_pred, axis=1)
@@ -651,54 +629,17 @@ def multilabel_confusion_matrix(
651629
return np.array([tn, fp, fn, tp]).T.reshape(-1, 2, 2)
652630

653631

654-
def _metric_handle_division(*, numerator, denominator, metric, zero_division):
655-
"""Helper to handle zero-division.
656-
657-
Parameters
658-
----------
659-
numerator : numbers.Real
660-
The numerator of the division.
661-
denominator : numbers.Real
662-
The denominator of the division.
663-
metric : str
664-
Name of the caller metric function.
665-
zero_division : {0.0, 1.0, "warn"}
666-
The strategy to use when encountering 0-denominator.
667-
668-
Returns
669-
-------
670-
result : numbers.Real
671-
The resulting of the division
672-
is_zero_division : bool
673-
Whether or not we encountered a zero division. This value could be
674-
required to early return `result` in the "caller" function.
675-
"""
676-
if np.isclose(denominator, 0):
677-
if zero_division == "warn":
678-
msg = f"{metric} is ill-defined and set to 0.0. Use the `zero_division` "
679-
"param to control this behavior."
680-
warnings.warn(msg, UndefinedMetricWarning, stacklevel=2)
681-
return _check_zero_division(zero_division), True
682-
return numerator / denominator, False
683-
684-
685632
@validate_params(
686633
{
687634
"y1": ["array-like"],
688635
"y2": ["array-like"],
689636
"labels": ["array-like", None],
690637
"weights": [StrOptions({"linear", "quadratic"}), None],
691638
"sample_weight": ["array-like", None],
692-
"zero_division": [
693-
StrOptions({"warn"}),
694-
Options(Real, {0.0, 1.0, np.nan}),
695-
],
696639
},
697640
prefer_skip_nested_validation=True,
698641
)
699-
def cohen_kappa_score(
700-
y1, y2, *, labels=None, weights=None, sample_weight=None, zero_division="warn"
701-
):
642+
def cohen_kappa_score(y1, y2, *, labels=None, weights=None, sample_weight=None):
702643
r"""Compute Cohen's kappa: a statistic that measures inter-annotator agreement.
703644
704645
This function computes Cohen's kappa [1]_, a score that expresses the level
@@ -737,14 +678,6 @@ class labels [2]_.
737678
sample_weight : array-like of shape (n_samples,), default=None
738679
Sample weights.
739680
740-
zero_division : {"warn", 0.0, 1.0, np.nan}, default="warn"
741-
Sets the return value when there is a zero division. This is the case when both
742-
labelings `y1` and `y2` both exclusively contain the 0 class (e. g.
743-
`[0, 0, 0, 0]`) (or if both are empty). If set to "warn", returns `0.0`, but a
744-
warning is also raised.
745-
746-
.. versionadded:: 1.6
747-
748681
Returns
749682
-------
750683
kappa : float
@@ -774,18 +707,7 @@ class labels [2]_.
774707
n_classes = confusion.shape[0]
775708
sum0 = np.sum(confusion, axis=0)
776709
sum1 = np.sum(confusion, axis=1)
777-
778-
numerator = np.outer(sum0, sum1)
779-
denominator = np.sum(sum0)
780-
expected, is_zero_division = _metric_handle_division(
781-
numerator=numerator,
782-
denominator=denominator,
783-
metric="cohen_kappa_score()",
784-
zero_division=zero_division,
785-
)
786-
787-
if is_zero_division:
788-
return expected
710+
expected = np.outer(sum0, sum1) / np.sum(sum0)
789711

790712
if weights is None:
791713
w_mat = np.ones([n_classes, n_classes], dtype=int)
@@ -798,18 +720,8 @@ class labels [2]_.
798720
else:
799721
w_mat = (w_mat - w_mat.T) ** 2
800722

801-
numerator = np.sum(w_mat * confusion)
802-
denominator = np.sum(w_mat * expected)
803-
score, is_zero_division = _metric_handle_division(
804-
numerator=numerator,
805-
denominator=denominator,
806-
metric="cohen_kappa_score()",
807-
zero_division=zero_division,
808-
)
809-
810-
if is_zero_division:
811-
return score
812-
return 1 - score
723+
k = np.sum(w_mat * confusion) / np.sum(w_mat * expected)
724+
return 1 - k
813725

814726

815727
@validate_params(
@@ -911,6 +823,8 @@ def jaccard_score(
911823
there are no negative values in predictions and labels. If set to
912824
"warn", this acts like 0, but a warning is also raised.
913825
826+
.. versionadded:: 0.24
827+
914828
Returns
915829
-------
916830
score : float or ndarray of shape (n_unique_labels,), dtype=np.float64
@@ -1015,15 +929,10 @@ def jaccard_score(
1015929
"y_true": ["array-like"],
1016930
"y_pred": ["array-like"],
1017931
"sample_weight": ["array-like", None],
1018-
"zero_division": [
1019-
Options(Real, {0.0, 1.0}),
1020-
"nan",
1021-
StrOptions({"warn"}),
1022-
],
1023932
},
1024933
prefer_skip_nested_validation=True,
1025934
)
1026-
def matthews_corrcoef(y_true, y_pred, *, sample_weight=None, zero_division="warn"):
935+
def matthews_corrcoef(y_true, y_pred, *, sample_weight=None):
1027936
"""Compute the Matthews correlation coefficient (MCC).
1028937
1029938
The Matthews correlation coefficient is used in machine learning as a
@@ -1054,13 +963,6 @@ def matthews_corrcoef(y_true, y_pred, *, sample_weight=None, zero_division="warn
1054963
1055964
.. versionadded:: 0.18
1056965
1057-
zero_division : {"warn", 0.0, 1.0, np.nan}, default="warn"
1058-
Sets the value to return when there is a zero division, i.e. when all
1059-
predictions and labels are negative. If set to "warn", this acts like 0,
1060-
but a warning is also raised.
1061-
1062-
.. versionadded:: 1.6
1063-
1064966
Returns
1065967
-------
1066968
mcc : float
@@ -1114,13 +1016,7 @@ def matthews_corrcoef(y_true, y_pred, *, sample_weight=None, zero_division="warn
11141016
cov_ytyt = n_samples**2 - np.dot(t_sum, t_sum)
11151017

11161018
if cov_ypyp * cov_ytyt == 0:
1117-
if zero_division == "warn":
1118-
msg = (
1119-
"Matthews correlation coefficient is ill-defined and being set to 0.0. "
1120-
"Use `zero_division` to control this behaviour."
1121-
)
1122-
warnings.warn(msg, UndefinedMetricWarning, stacklevel=2)
1123-
return _check_zero_division(zero_division)
1019+
return 0.0
11241020
else:
11251021
return cov_ytyp / np.sqrt(cov_ytyt * cov_ypyp)
11261022

sklearn/metrics/tests/test_classification.py

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -795,46 +795,21 @@ def test_cohen_kappa():
795795
)
796796

797797

798-
@pytest.mark.parametrize("zero_division", ["warn", 0, 1, np.nan])
799-
@pytest.mark.parametrize("y_true, y_pred", [([0], [1]), ([0, 0], [0, 1])])
800-
def test_matthews_corrcoef_zero_division(zero_division, y_true, y_pred):
801-
"""Check the behaviour of `zero_division` in `matthews_corrcoef`."""
802-
expected_result = 0.0 if zero_division == "warn" else zero_division
803-
804-
if zero_division == "warn":
805-
with pytest.warns(UndefinedMetricWarning):
806-
result = matthews_corrcoef(y_true, y_pred, zero_division=zero_division)
807-
else:
808-
result = matthews_corrcoef(y_true, y_pred, zero_division=zero_division)
809-
810-
if np.isnan(expected_result):
811-
assert np.isnan(result)
812-
else:
813-
assert result == expected_result
814-
815-
816798
@pytest.mark.parametrize("zero_division", [0, 1, np.nan])
817-
@pytest.mark.parametrize("y_true, y_pred", [([0], [0]), ([], [])])
799+
@pytest.mark.parametrize("y_true, y_pred", [([0], [0])])
818800
@pytest.mark.parametrize(
819801
"metric",
820802
[
821803
f1_score,
822804
partial(fbeta_score, beta=1),
823805
precision_score,
824806
recall_score,
825-
accuracy_score,
826-
partial(cohen_kappa_score, labels=[0, 1]),
827807
],
828808
)
829809
def test_zero_division_nan_no_warning(metric, y_true, y_pred, zero_division):
830810
"""Check the behaviour of `zero_division` when setting to 0, 1 or np.nan.
831811
No warnings should be raised.
832812
"""
833-
if metric is accuracy_score and len(y_true):
834-
pytest.skip(
835-
reason="zero_division is only used with empty y_true/y_pred for accuracy"
836-
)
837-
838813
with warnings.catch_warnings():
839814
warnings.simplefilter("error")
840815
result = metric(y_true, y_pred, zero_division=zero_division)
@@ -845,27 +820,20 @@ def test_zero_division_nan_no_warning(metric, y_true, y_pred, zero_division):
845820
assert result == zero_division
846821

847822

848-
@pytest.mark.parametrize("y_true, y_pred", [([0], [0]), ([], [])])
823+
@pytest.mark.parametrize("y_true, y_pred", [([0], [0])])
849824
@pytest.mark.parametrize(
850825
"metric",
851826
[
852827
f1_score,
853828
partial(fbeta_score, beta=1),
854829
precision_score,
855830
recall_score,
856-
accuracy_score,
857-
cohen_kappa_score,
858831
],
859832
)
860833
def test_zero_division_nan_warning(metric, y_true, y_pred):
861834
"""Check the behaviour of `zero_division` when setting to "warn".
862835
A `UndefinedMetricWarning` should be raised.
863836
"""
864-
if metric is accuracy_score and len(y_true):
865-
pytest.skip(
866-
reason="zero_division is only used with empty y_true/y_pred for accuracy"
867-
)
868-
869837
with pytest.warns(UndefinedMetricWarning):
870838
result = metric(y_true, y_pred, zero_division="warn")
871839
assert result == 0.0
@@ -937,19 +905,15 @@ def test_matthews_corrcoef():
937905

938906
# For the zero vector case, the corrcoef cannot be calculated and should
939907
# output 0
940-
assert_almost_equal(
941-
matthews_corrcoef([0, 0, 0, 0], [0, 0, 0, 0], zero_division=0), 0.0
942-
)
908+
assert_almost_equal(matthews_corrcoef([0, 0, 0, 0], [0, 0, 0, 0]), 0.0)
943909

944910
# And also for any other vector with 0 variance
945-
assert_almost_equal(
946-
matthews_corrcoef(y_true, ["a"] * len(y_true), zero_division=0), 0.0
947-
)
911+
assert_almost_equal(matthews_corrcoef(y_true, ["a"] * len(y_true)), 0.0)
948912

949913
# These two vectors have 0 correlation and hence mcc should be 0
950914
y_1 = [1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1]
951915
y_2 = [1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1]
952-
assert_almost_equal(matthews_corrcoef(y_1, y_2, zero_division=0), 0.0)
916+
assert_almost_equal(matthews_corrcoef(y_1, y_2), 0.0)
953917

954918
# Check that sample weight is able to selectively exclude
955919
mask = [1] * 10 + [0] * 10
@@ -982,17 +946,17 @@ def test_matthews_corrcoef_multiclass():
982946
# Zero variance will result in an mcc of zero
983947
y_true = [0, 1, 2]
984948
y_pred = [3, 3, 3]
985-
assert_almost_equal(matthews_corrcoef(y_true, y_pred, zero_division=0), 0.0)
949+
assert_almost_equal(matthews_corrcoef(y_true, y_pred), 0.0)
986950

987951
# Also for ground truth with zero variance
988952
y_true = [3, 3, 3]
989953
y_pred = [0, 1, 2]
990-
assert_almost_equal(matthews_corrcoef(y_true, y_pred, zero_division=0), 0.0)
954+
assert_almost_equal(matthews_corrcoef(y_true, y_pred), 0.0)
991955

992956
# These two vectors have 0 correlation and hence mcc should be 0
993957
y_1 = [0, 1, 2, 0, 1, 2, 0, 1, 2]
994958
y_2 = [1, 1, 1, 2, 2, 2, 0, 0, 0]
995-
assert_almost_equal(matthews_corrcoef(y_1, y_2, zero_division=0), 0.0)
959+
assert_almost_equal(matthews_corrcoef(y_1, y_2), 0.0)
996960

997961
# We can test that binary assumptions hold using the multiclass computation
998962
# by masking the weight of samples not in the first two classes
@@ -1011,10 +975,7 @@ def test_matthews_corrcoef_multiclass():
1011975
y_pred = [0, 0, 1, 2]
1012976
sample_weight = [1, 1, 0, 0]
1013977
assert_almost_equal(
1014-
matthews_corrcoef(
1015-
y_true, y_pred, sample_weight=sample_weight, zero_division=0.0
1016-
),
1017-
0.0,
978+
matthews_corrcoef(y_true, y_pred, sample_weight=sample_weight), 0.0
1018979
)
1019980

1020981

0 commit comments

Comments
 (0)