-
-
Notifications
You must be signed in to change notification settings - Fork 25.8k
ENH add pos_label to confusion_matrix #26839
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
base: main
Are you sure you want to change the base?
Changes from all commits
15b8b98
1ad6db8
6a05658
72767e7
a65c6a4
47b0f33
6ca1b39
93b7936
7958dd5
d39ef7a
3d6b388
4bc2abd
74a083c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -226,13 +226,14 @@ def accuracy_score(y_true, y_pred, *, normalize=True, sample_weight=None): | |
"y_true": ["array-like"], | ||
"y_pred": ["array-like"], | ||
"labels": ["array-like", None], | ||
"pos_label": [Real, str, "boolean", None], | ||
"sample_weight": ["array-like", None], | ||
"normalize": [StrOptions({"true", "pred", "all"}), None], | ||
}, | ||
prefer_skip_nested_validation=True, | ||
) | ||
def confusion_matrix( | ||
y_true, y_pred, *, labels=None, sample_weight=None, normalize=None | ||
y_true, y_pred, *, labels=None, pos_label=None, sample_weight=None, normalize=None | ||
): | ||
"""Compute confusion matrix to evaluate the accuracy of a classification. | ||
|
||
|
@@ -260,6 +261,15 @@ def confusion_matrix( | |
If ``None`` is given, those that appear at least once | ||
in ``y_true`` or ``y_pred`` are used in sorted order. | ||
|
||
pos_label : int, float, bool or str, default=None | ||
The label of the positive class for binary classification. | ||
When `pos_label=None`, if `y_true` is in `{-1, 1}` or `{0, 1}`, | ||
`pos_label` is set to 1, otherwise an error will be raised. | ||
An error is also raised if `pos_label` is set and `y_true` is not a binary | ||
classification problem. | ||
|
||
.. versionadded:: 1.4 | ||
|
||
sample_weight : array-like of shape (n_samples,), default=None | ||
Sample weights. | ||
|
||
|
@@ -320,6 +330,19 @@ def confusion_matrix( | |
if y_type not in ("binary", "multiclass"): | ||
raise ValueError("%s is not supported" % y_type) | ||
|
||
if y_true.size == 0 and y_pred.size == 0: | ||
# early return for empty arrays avoiding all checks | ||
n_classes = 0 if labels is None else len(labels) | ||
return np.zeros((n_classes, n_classes), dtype=int) | ||
|
||
if y_type == "binary": | ||
pos_label = _check_pos_label_consistency(pos_label, y_true) | ||
elif pos_label is not None: | ||
raise ValueError( | ||
"`pos_label` should only be set when the target is binary. Got " | ||
f"{y_type} type of target instead." | ||
) | ||
|
||
if labels is None: | ||
labels = unique_labels(y_true, y_pred) | ||
else: | ||
|
@@ -382,6 +405,11 @@ def confusion_matrix( | |
cm = cm / cm.sum() | ||
cm = np.nan_to_num(cm) | ||
|
||
if pos_label is not None and pos_label != labels[-1]: | ||
adrinjalali marked this conversation as resolved.
Show resolved
Hide resolved
glemaitre marked this conversation as resolved.
Show resolved
Hide resolved
adrinjalali marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# Reorder the confusion matrix such that TP is at index | ||
# [1, 1]. | ||
cm = cm[::-1, ::-1] | ||
|
||
if cm.shape == (1, 1): | ||
warnings.warn( | ||
( | ||
|
@@ -680,7 +708,17 @@ class labels [2]_. | |
.. [3] `Wikipedia entry for the Cohen's kappa | ||
<https://en.wikipedia.org/wiki/Cohen%27s_kappa>`_. | ||
""" | ||
confusion = confusion_matrix(y1, y2, labels=labels, sample_weight=sample_weight) | ||
y_type, y1, y2 = _check_targets(y1, y2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since REF: #26820 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know how to go around that. Somehow having private function to call could help and passing the unique label could help but I would not block this PR for it. |
||
if y_type == "binary": | ||
# we can set `pos_label` to any class labels because the computation of MCC | ||
# is symmetric and invariant to `pos_label` switch. | ||
pos_label = y1[0] | ||
else: | ||
pos_label = None | ||
|
||
confusion = confusion_matrix( | ||
y1, y2, labels=labels, pos_label=pos_label, sample_weight=sample_weight | ||
) | ||
adrinjalali marked this conversation as resolved.
Show resolved
Hide resolved
|
||
n_classes = confusion.shape[0] | ||
sum0 = np.sum(confusion, axis=0) | ||
sum1 = np.sum(confusion, axis=1) | ||
|
@@ -966,12 +1004,21 @@ def matthews_corrcoef(y_true, y_pred, *, sample_weight=None): | |
if y_type not in {"binary", "multiclass"}: | ||
raise ValueError("%s is not supported" % y_type) | ||
|
||
if y_type == "binary": | ||
# we can set `pos_label` to any class labels because the computation of MCC | ||
# is symmetric and invariant to `pos_label` switch. | ||
pos_label = y_true[0] | ||
else: | ||
pos_label = None | ||
|
||
lb = LabelEncoder() | ||
lb.fit(np.hstack([y_true, y_pred])) | ||
y_true = lb.transform(y_true) | ||
y_pred = lb.transform(y_pred) | ||
|
||
C = confusion_matrix(y_true, y_pred, sample_weight=sample_weight) | ||
C = confusion_matrix( | ||
y_true, y_pred, pos_label=pos_label, sample_weight=sample_weight | ||
) | ||
t_sum = C.sum(axis=1, dtype=np.float64) | ||
p_sum = C.sum(axis=0, dtype=np.float64) | ||
n_correct = np.trace(C, dtype=np.float64) | ||
|
@@ -1921,11 +1968,18 @@ class after being classified as negative. This is the case when the | |
f"problems, got targets of type: {y_type}" | ||
) | ||
|
||
if labels is None: | ||
classes = np.unique(y_true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, we should probably call |
||
pos_label = 1 if len(classes) < 2 else classes[1] | ||
else: | ||
pos_label = labels[-1] | ||
|
||
cm = confusion_matrix( | ||
y_true, | ||
y_pred, | ||
sample_weight=sample_weight, | ||
labels=labels, | ||
pos_label=pos_label, | ||
) | ||
|
||
# Case when `y_test` contains a single class and `y_test == y_pred`. | ||
|
@@ -2396,7 +2450,17 @@ def balanced_accuracy_score(y_true, y_pred, *, sample_weight=None, adjusted=Fals | |
>>> balanced_accuracy_score(y_true, y_pred) | ||
0.625 | ||
""" | ||
C = confusion_matrix(y_true, y_pred, sample_weight=sample_weight) | ||
y_type, y_true, y_pred = _check_targets(y_true, y_pred) | ||
if y_type == "binary": | ||
# We can set `pos_label` to any value since we are computing per-class | ||
# statistics and averaging them. | ||
pos_label = y_true[0] | ||
else: | ||
pos_label = None | ||
|
||
C = confusion_matrix( | ||
y_true, y_pred, pos_label=pos_label, sample_weight=sample_weight | ||
) | ||
with np.errstate(divide="ignore", invalid="ignore"): | ||
per_class = np.diag(C) / C.sum(axis=1) | ||
if np.any(np.isnan(per_class)): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering if we are not shooting in our own foot with this
ValueError
.If we change the default
pos_label=1
then we will by default raise this error whenevery_type != "binary"
. If we choose,pos_label=1
as a default, then we need to have the same strategy than the precision recall meaning that we need to ignorepos_label
.Am I right @lucyleeow or I missed something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another advantages to implicitely ignoring
pos_label
is that we don't need anymore to call_check_target
and thus the unique function because we can pass the default value most of the time.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could set default to 1 and change this to:
_check_set_wise_labels
does similar checking:scikit-learn/sklearn/metrics/_classification.py
Lines 1549 to 1556 in e5178c5
the
precision_recall_fscore_support
functions use it and default ispos_label=1
(None
option is allowed byt not documented)(I may have gotten something wrong, this stuff gets confusing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realise you could technically just flip the labels by using the 'reorder' functionality of
labels
though it is less clear and not consistent with other places where we usepos_label
for binary case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed but once I found this out, I was surprise because the API is kind of not consistent. I find it a bit better to have consistency across metrics if possible.
The problem is that you let the case where someone pass
pos_label=1
explicitly in case other than "binary". So if we skip this test, I would rather prefer to be lenient as in other metrics.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, no raising any error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Options:
None
and mention that it is equivalent to 1 in binary case1
and completely ignore it in cases other than binary classification (do not raise error/warning)1
, in case off binary classification, raise warning ifpos_label
is not the defaultNow I do not know what my preference would be. I would think that it is okay to raise a warning when
pos_label
is not default but target is not binary. We do miss the case when the user wantspos_label
to be default. (1) has the problem ofpos_label=None
being ill-defined.But maybe more opinions are needed here. I will try to summarise in #10010
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I will try to ping more dev in #10010 such that we can settle on a solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be in favor of defaulting to
None
everywhere to make it easy to spot cases where users provide a non-default value in case we need to raise an informative error message.For the specific case of
confusion_matrix
I think it we should not pass apos_label
as the matrix is always defined irrespective of what we consider positive or not (it does not care). What matters would be to pass thelabels=class_labels
when callingconfusion_matrix
indirectly from a public function such asf1_score
which itself asks the user to specify apos_label
, so that there is no ambiguity about the meaning of the rows and columns of the CM.To make this more efficient, I think
type_of_target
could be extended to also have an alternativetype_of_target_and_class_labels
to avoid computingxp.unique
many times.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, when
y
is a dataframe orseries
we should usey.unique
to make this more efficient (pandas does not need to sort the data to compute the unique values).