Skip to content

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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

glemaitre
Copy link
Member

@glemaitre glemaitre commented Jul 14, 2023

While working on #26120, we figure out that we don't have a pos_label parameter in the confusion_matrix function. To follow the documentation and since we don't report the column/index name, we should not change the positions of the TP/TN/FP/FN.

pos_label allows flipping the matrix in case this is not the expected positive label.

I also added the pos_label to the ConfusionMatrixDisplay to be complete.

@github-actions
Copy link

github-actions bot commented Jul 14, 2023

✔️ Linting Passed

All linting checks passed. Your pull request is in excellent shape! ☀️

Generated for commit: 74a083c. Link to the linter CI: here

Copy link
Member

@adrinjalali adrinjalali left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@glemaitre
Copy link
Member Author

And the current CIs failure shows that we currently badly handle the ambiguity ;)

@glemaitre
Copy link
Member Author

We should probably make a similar change to the class_likelihood_ratios class as well.

Copy link
Contributor

@OmarManzoor OmarManzoor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @glemaitre. Just a few minor changes mostly related to the docs otherwise looks good overall.

Co-authored-by: Omar Salman <omar.salman@arbisoft.com>
Copy link
Member

@adrinjalali adrinjalali left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also have a test to check that passing pos_label correctly inverts the output matrix?

@glemaitre
Copy link
Member Author

Could we also have a test to check that passing pos_label correctly inverts the output matrix?

This test is already integrated with some parametrization in others, e.g.

https://github.com/scikit-learn/scikit-learn/pull/26839/files#diff-a76654897c4d4bc4ed46a93a74acd44319aac42e96411ec816220e09d897e493R468-R475

Copy link
Member

@thomasjpfan thomasjpfan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I am concerned with all the extra calls to _check_targets, which adds more calls to unique.

:mod:`sklearn.pipeline`
.......................

- |Feature| :class:`pipeline.Pipeline` now supports metadata routing according
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a merge error.

@@ -670,7 +698,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since _check_targets call unique, this adds more overhead to the computation.

REF: #26820

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

@glemaitre
Copy link
Member Author

pinging @OmarManzoor, @thomasjpfan, and @lucyleeow on this one.

@lucyleeow lucyleeow closed this Nov 9, 2023
@lucyleeow lucyleeow reopened this Nov 9, 2023
@lucyleeow
Copy link
Member

Sorry mis-click.

In light of #10010 (comment), do we want to set default to pos_label=1 and not use None ? This would be inline with precision_recall_fscore_support.


if y_type == "binary":
pos_label = _check_pos_label_consistency(pos_label, y_true)
elif pos_label is not None:
Copy link
Member Author

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 whenever y_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 ignore pos_label.

Am I right @lucyleeow or I missed something?

Copy link
Member Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hen we will by default raise this error whenever y_type != "binary"

I think we could set default to 1 and change this to:

Suggested change
elif pos_label is not None:
elif pos_label != 1:

_check_set_wise_labels does similar checking:

elif pos_label not in (None, 1):
warnings.warn(
"Note that pos_label (set to %r) is ignored when "
"average != 'binary' (got %r). You may use "
"labels=[pos_label] to specify a single positive class."
% (pos_label, average),
UserWarning,
)

the precision_recall_fscore_support functions use it and default is pos_label=1 (None option is allowed byt not documented)

(I may have gotten something wrong, this stuff gets confusing)

Copy link
Member

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 use pos_label for binary case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'reorder' functionality of labels

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.

I think we could set default to 1 and change this to:

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean you would not raise an error if pos_label is not the default value and it is not the binary case?

Yes, no raising any error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Options:

  1. default is None and mention that it is equivalent to 1 in binary case
  2. default is 1 and completely ignore it in cases other than binary classification (do not raise error/warning)
  3. default is 1, in case off binary classification, raise warning if pos_label is not the default

Now 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 wants pos_label to be default. (1) has the problem of pos_label=None being ill-defined.

But maybe more opinions are needed here. I will try to summarise in #10010

Copy link
Member Author

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.

Copy link
Member

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 a pos_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 the labels=class_labels when calling confusion_matrix indirectly from a public function such as f1_score which itself asks the user to specify a pos_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 alternative type_of_target_and_class_labels to avoid computing xp.unique many times.

Copy link
Member

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 or series we should use y.unique to make this more efficient (pandas does not need to sort the data to compute the unique values).

@@ -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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we should probably call _unique instead just to be sure.
I assume that we could introduce a pos_label as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants