Skip to content

Commit 329a1cf

Browse files
StefanieSengerglemaitreadrinjalali
authored
ENH Add zero_division param to cohen_kappa_score (scikit-learn#29210)
Co-authored-by: Guillaume Lemaitre <g.lemaitre58@gmail.com> Co-authored-by: Adrin Jalali <adrin.jalali@gmail.com>
1 parent 51c8e0e commit 329a1cf

File tree

4 files changed

+83
-10
lines changed

4 files changed

+83
-10
lines changed

doc/modules/model_evaluation.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ The function :func:`cohen_kappa_score` computes `Cohen's kappa
605605
This measure is intended to compare labelings by different human annotators,
606606
not a classifier versus a ground truth.
607607

608-
The kappa score (see docstring) is a number between -1 and 1.
608+
The kappa score is a number between -1 and 1.
609609
Scores above .8 are generally considered good agreement;
610610
zero or lower means no agreement (practically random labels).
611611

@@ -614,9 +614,9 @@ but not for multilabel problems (except by manually computing a per-label score)
614614
and not for more than two annotators.
615615

616616
>>> from sklearn.metrics import cohen_kappa_score
617-
>>> y_true = [2, 0, 2, 2, 0, 1]
618-
>>> y_pred = [0, 0, 2, 2, 0, 2]
619-
>>> cohen_kappa_score(y_true, y_pred)
617+
>>> labeling1 = [2, 0, 2, 2, 0, 1]
618+
>>> labeling2 = [0, 0, 2, 2, 0, 2]
619+
>>> cohen_kappa_score(labeling1, labeling2)
620620
0.4285714285714286
621621

622622
.. _confusion_matrix:

doc/whats_new/v1.6.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ Changelog
133133
whether to raise an exception if a subset of the scorers in multimetric scoring fails
134134
or to return an error code. :pr:`28992` by :user:`Stefanie Senger <StefanieSenger>`.
135135

136+
- |Enhancement| Adds `zero_division` to :func:`cohen_kappa_score`. When there is a
137+
division by zero, the metric is undefined and this value is returned.
138+
:pr:`29210` by :user:`Marc Torrellas Socastro <marctorsoc>` and
139+
:user:`Stefanie Senger <StefanieSenger>`.
140+
136141
:mod:`sklearn.model_selection`
137142
..............................
138143

sklearn/metrics/_classification.py

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -610,17 +610,54 @@ def multilabel_confusion_matrix(
610610
return np.array([tn, fp, fn, tp]).T.reshape(-1, 2, 2)
611611

612612

613+
def _metric_handle_division(*, numerator, denominator, metric, zero_division):
614+
"""Helper to handle zero-division.
615+
616+
Parameters
617+
----------
618+
numerator : numbers.Real
619+
The numerator of the division.
620+
denominator : numbers.Real
621+
The denominator of the division.
622+
metric : str
623+
Name of the caller metric function.
624+
zero_division : {0.0, 1.0, "warn"}
625+
The strategy to use when encountering 0-denominator.
626+
627+
Returns
628+
-------
629+
result : numbers.Real
630+
The resulting of the division
631+
is_zero_division : bool
632+
Whether or not we encountered a zero division. This value could be
633+
required to early return `result` in the "caller" function.
634+
"""
635+
if np.isclose(denominator, 0):
636+
if zero_division == "warn":
637+
msg = f"{metric} is ill-defined and set to 0.0. Use the `zero_division` "
638+
"param to control this behavior."
639+
warnings.warn(msg, UndefinedMetricWarning, stacklevel=2)
640+
return _check_zero_division(zero_division), True
641+
return numerator / denominator, False
642+
643+
613644
@validate_params(
614645
{
615646
"y1": ["array-like"],
616647
"y2": ["array-like"],
617648
"labels": ["array-like", None],
618649
"weights": [StrOptions({"linear", "quadratic"}), None],
619650
"sample_weight": ["array-like", None],
651+
"zero_division": [
652+
StrOptions({"warn"}),
653+
Options(Real, {0.0, 1.0, np.nan}),
654+
],
620655
},
621656
prefer_skip_nested_validation=True,
622657
)
623-
def cohen_kappa_score(y1, y2, *, labels=None, weights=None, sample_weight=None):
658+
def cohen_kappa_score(
659+
y1, y2, *, labels=None, weights=None, sample_weight=None, zero_division="warn"
660+
):
624661
r"""Compute Cohen's kappa: a statistic that measures inter-annotator agreement.
625662
626663
This function computes Cohen's kappa [1]_, a score that expresses the level
@@ -653,12 +690,20 @@ class labels [2]_.
653690
``y1`` or ``y2`` are used.
654691
655692
weights : {'linear', 'quadratic'}, default=None
656-
Weighting type to calculate the score. `None` means no weighted;
657-
"linear" means linear weighted; "quadratic" means quadratic weighted.
693+
Weighting type to calculate the score. `None` means not weighted;
694+
"linear" means linear weighting; "quadratic" means quadratic weighting.
658695
659696
sample_weight : array-like of shape (n_samples,), default=None
660697
Sample weights.
661698
699+
zero_division : {"warn", 0.0, 1.0, np.nan}, default="warn"
700+
Sets the return value when there is a zero division. This is the case when both
701+
labelings `y1` and `y2` both exclusively contain the 0 class (e. g.
702+
`[0, 0, 0, 0]`) (or if both are empty). If set to "warn", returns `0.0`, but a
703+
warning is also raised.
704+
705+
.. versionadded:: 1.6
706+
662707
Returns
663708
-------
664709
kappa : float
@@ -688,7 +733,18 @@ class labels [2]_.
688733
n_classes = confusion.shape[0]
689734
sum0 = np.sum(confusion, axis=0)
690735
sum1 = np.sum(confusion, axis=1)
691-
expected = np.outer(sum0, sum1) / np.sum(sum0)
736+
737+
numerator = np.outer(sum0, sum1)
738+
denominator = np.sum(sum0)
739+
expected, is_zero_division = _metric_handle_division(
740+
numerator=numerator,
741+
denominator=denominator,
742+
metric="cohen_kappa_score()",
743+
zero_division=zero_division,
744+
)
745+
746+
if is_zero_division:
747+
return expected
692748

693749
if weights is None:
694750
w_mat = np.ones([n_classes, n_classes], dtype=int)
@@ -701,8 +757,18 @@ class labels [2]_.
701757
else:
702758
w_mat = (w_mat - w_mat.T) ** 2
703759

704-
k = np.sum(w_mat * confusion) / np.sum(w_mat * expected)
705-
return 1 - k
760+
numerator = np.sum(w_mat * confusion)
761+
denominator = np.sum(w_mat * expected)
762+
score, is_zero_division = _metric_handle_division(
763+
numerator=numerator,
764+
denominator=denominator,
765+
metric="cohen_kappa_score()",
766+
zero_division=zero_division,
767+
)
768+
769+
if is_zero_division:
770+
return score
771+
return 1 - score
706772

707773

708774
@validate_params(

sklearn/metrics/tests/test_classification.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,7 @@ def test_matthews_corrcoef_nan():
810810
partial(fbeta_score, beta=1),
811811
precision_score,
812812
recall_score,
813+
partial(cohen_kappa_score, labels=[0, 1]),
813814
],
814815
)
815816
def test_zero_division_nan_no_warning(metric, y_true, y_pred, zero_division):
@@ -834,6 +835,7 @@ def test_zero_division_nan_no_warning(metric, y_true, y_pred, zero_division):
834835
partial(fbeta_score, beta=1),
835836
precision_score,
836837
recall_score,
838+
cohen_kappa_score,
837839
],
838840
)
839841
def test_zero_division_nan_warning(metric, y_true, y_pred):

0 commit comments

Comments
 (0)