From 3d0236991cb9db3e64defe7b649c218c4f7cafee Mon Sep 17 00:00:00 2001 From: "p.attard" Date: Mon, 8 Mar 2021 14:22:22 +0100 Subject: [PATCH 01/16] raise an error when the 'pred_decision' shape is not consistent with number of classes with a multiclass target case. New test in order to check this situation in 'test_classification.py' --- sklearn/metrics/_classification.py | 8 ++++++++ sklearn/metrics/tests/test_classification.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index 708bde662e765..f12e76b2b55f0 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -2371,6 +2371,14 @@ def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): y_true = column_or_1d(y_true) y_true_unique = np.unique(labels if labels is not None else y_true) if y_true_unique.size > 2: + if (pred_decision.ndim == 1) or \ + (labels is not None and pred_decision.ndim > 1 and + np.size(y_true_unique) != pred_decision.shape[1]): + raise ValueError("The shape of pred_decision is not " + "consistent with the number of classes. " + "pred_decision shape must be " + "(n_samples, n_classes) with " + "multiclass target") if (labels is None and pred_decision.ndim > 1 and (np.size(y_true_unique) != pred_decision.shape[1])): raise ValueError("Please include all labels in y_true " diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index c32e9c89ada47..9e766d6e7d9ee 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -2135,6 +2135,25 @@ def test_hinge_loss_multiclass_missing_labels_with_labels_none(): hinge_loss(y_true, pred_decision) +def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): + y_true = [2, 1, 0, 1, 0, 1, 1] + pred_decision = np.array([0, 1, 2, 1, 0, 2, 1]) + error_message = ("The shape of pred_decision is not " + "consistent with the number of classes. " + "pred_decision shape must be " + "(n_samples, n_classes) with " + "multiclass target") + + assert_raise_message(ValueError, error_message, hinge_loss, + y_true=y_true, pred_decision=pred_decision) + pred_decision = [[0, 1], [0, 1], [0, 1], [0, 1], + [2, 0], [0, 1], [1, 0]] + labels = [0, 1, 2] + assert_raise_message(ValueError, error_message, hinge_loss, + y_true=y_true, pred_decision=pred_decision, + labels=labels) + + def test_hinge_loss_multiclass_with_missing_labels(): pred_decision = np.array([ [+0.36, -0.17, -0.58, -0.99], From 86197f34924f492576eecef12eca1a7e4357026b Mon Sep 17 00:00:00 2001 From: "p.attard" Date: Mon, 8 Mar 2021 14:41:46 +0100 Subject: [PATCH 02/16] add the asset_raise_message import --- sklearn/metrics/tests/test_classification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 9e766d6e7d9ee..55873f4f934a3 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -15,6 +15,7 @@ from sklearn.datasets import make_multilabel_classification from sklearn.preprocessing import label_binarize, LabelBinarizer from sklearn.utils.validation import check_random_state +from sklearn.utils._testing import assert_raise_message from sklearn.utils._testing import assert_almost_equal from sklearn.utils._testing import assert_array_equal from sklearn.utils._testing import assert_array_almost_equal From feee8f5582c2b25e1cb3d846b5b42012834c127c Mon Sep 17 00:00:00 2001 From: "p.attard" Date: Mon, 8 Mar 2021 15:22:02 +0100 Subject: [PATCH 03/16] add changelog --- doc/whats_new/v1.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/whats_new/v1.0.rst b/doc/whats_new/v1.0.rst index 3086d91b28f5d..575fbdbdd2813 100644 --- a/doc/whats_new/v1.0.rst +++ b/doc/whats_new/v1.0.rst @@ -134,6 +134,11 @@ Changelog class methods and will be removed in 1.2. :pr:`18543` by `Guillaume Lemaitre`_. +- |Enhancement| A fix to raise an error when ``pred_decision`` is 1d + whereas it is a multiclass classification or when ``pred_decision`` + parameter is not consistent with ``labels`` parameter. + :pr:`19643` by :user:`Pierre Attard `. + :mod:`sklearn.naive_bayes` .......................... From cc6462f79c91a0ecd1412441d2c1d8126a0b76a4 Mon Sep 17 00:00:00 2001 From: PierreAttard Date: Wed, 10 Mar 2021 20:41:48 +0100 Subject: [PATCH 04/16] cleaner test in order to raise the right error --- sklearn/metrics/_classification.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index f12e76b2b55f0..d772dab0a4853 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -2371,18 +2371,18 @@ def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): y_true = column_or_1d(y_true) y_true_unique = np.unique(labels if labels is not None else y_true) if y_true_unique.size > 2: - if (pred_decision.ndim == 1) or \ - (labels is not None and pred_decision.ndim > 1 and - np.size(y_true_unique) != pred_decision.shape[1]): - raise ValueError("The shape of pred_decision is not " - "consistent with the number of classes. " - "pred_decision shape must be " - "(n_samples, n_classes) with " - "multiclass target") - if (labels is None and pred_decision.ndim > 1 and - (np.size(y_true_unique) != pred_decision.shape[1])): - raise ValueError("Please include all labels in y_true " - "or pass labels as third argument") + if pred_decision.ndim <= 1 or \ + y_true_unique.size != pred_decision.shape[1]: + if labels is not None or pred_decision.ndim <= 1: + raise ValueError("The shape of pred_decision is not " + "consistent with the number of classes. " + "pred_decision shape must be " + "(n_samples, n_classes) with " + "multiclass target") + else: + raise ValueError("Please include all labels in y_true " + "or pass labels as third argument") + if labels is None: labels = y_true_unique le = LabelEncoder() From 50b2878490bad7552101ebe7c1191e66a65ca81b Mon Sep 17 00:00:00 2001 From: PierreAttard Date: Thu, 11 Mar 2021 09:13:48 +0100 Subject: [PATCH 05/16] all tests for valid multiclass decision inside a function. --- sklearn/metrics/_classification.py | 56 ++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index d772dab0a4853..7fa3fd19ce616 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -2285,6 +2285,48 @@ def log_loss(y_true, y_pred, *, eps=1e-15, normalize=True, sample_weight=None, return _weighted_sum(loss, sample_weight, normalize) +def _check_valid_multiclass_decision_shape(y_true_unique, pred_decision, + labels): + """Check if the inputs are consistent. + + If it is a multiclass problem (it means thare are more than 2 classes + to classify), `pred_decision` parameter can't be 1d array. The column + number must be equal to classes number. + + If not every classes are present in `y_true`, `labels` parameter + must be filled in. + + Parameters + ---------- + y_true_unique : array-like + + pred_decision : array-like + + labels : array-like + """ + + if pred_decision.ndim <= 1: + raise ValueError("The shape of pred_decision can not be 1d array" + "with a multiclass target. " + "pred_decision shape must be (n_samples, n_classes)") + + invalid_decision_shape = pred_decision.ndim > 1 and \ + y_true_unique.size != pred_decision.shape[1] + + if labels is None: + if invalid_decision_shape: + raise ValueError("Please include all labels in y_true " + "or pass labels as third argument") + elif invalid_decision_shape: + raise ValueError("The shape of pred_decision is not " + "consistent with the number of classes. " + "pred_decision shape must be " + "(n_samples, n_classes) with " + "multiclass target") + + return + + @_deprecate_positional_args def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): """Average hinge loss (non-regularized). @@ -2370,19 +2412,11 @@ def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): pred_decision = check_array(pred_decision, ensure_2d=False) y_true = column_or_1d(y_true) y_true_unique = np.unique(labels if labels is not None else y_true) + if y_true_unique.size > 2: - if pred_decision.ndim <= 1 or \ - y_true_unique.size != pred_decision.shape[1]: - if labels is not None or pred_decision.ndim <= 1: - raise ValueError("The shape of pred_decision is not " - "consistent with the number of classes. " - "pred_decision shape must be " - "(n_samples, n_classes) with " - "multiclass target") - else: - raise ValueError("Please include all labels in y_true " - "or pass labels as third argument") + _check_valid_multiclass_decision_shape(y_true_unique, pred_decision, + labels) if labels is None: labels = y_true_unique le = LabelEncoder() From f8554005a3e3819d576e86066775d6e6efa4a8d9 Mon Sep 17 00:00:00 2001 From: PierreAttard Date: Thu, 11 Mar 2021 09:15:26 +0100 Subject: [PATCH 06/16] `assert_raise_message` replaced by `pytest.raises`. import of assert_raise_message removed --- sklearn/metrics/tests/test_classification.py | 25 +++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 55873f4f934a3..28c9524960bd4 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -15,7 +15,6 @@ from sklearn.datasets import make_multilabel_classification from sklearn.preprocessing import label_binarize, LabelBinarizer from sklearn.utils.validation import check_random_state -from sklearn.utils._testing import assert_raise_message from sklearn.utils._testing import assert_almost_equal from sklearn.utils._testing import assert_array_equal from sklearn.utils._testing import assert_array_almost_equal @@ -2137,22 +2136,26 @@ def test_hinge_loss_multiclass_missing_labels_with_labels_none(): def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): + # test for inconsistency between multiclass problem and pred_decision argument y_true = [2, 1, 0, 1, 0, 1, 1] pred_decision = np.array([0, 1, 2, 1, 0, 2, 1]) - error_message = ("The shape of pred_decision is not " - "consistent with the number of classes. " - "pred_decision shape must be " - "(n_samples, n_classes) with " - "multiclass target") + error_message = (r"The shape of pred_decision can not be 1d array" + "with a multiclass target. " + "pred_decision shape must be \(n_samples, n_classes\)") + with pytest.raises(ValueError, match=error_message): + hinge_loss(y_true=y_true, pred_decision=pred_decision) - assert_raise_message(ValueError, error_message, hinge_loss, - y_true=y_true, pred_decision=pred_decision) + # test for inconsistency between pred_decision shape and labels number pred_decision = [[0, 1], [0, 1], [0, 1], [0, 1], [2, 0], [0, 1], [1, 0]] labels = [0, 1, 2] - assert_raise_message(ValueError, error_message, hinge_loss, - y_true=y_true, pred_decision=pred_decision, - labels=labels) + error_message = (r"The shape of pred_decision is not " + "consistent with the number of classes. " + "pred_decision shape must be " + "\(n_samples, n_classes\) with " + "multiclass target") + with pytest.raises(ValueError, match=error_message): + hinge_loss(y_true=y_true, pred_decision=pred_decision, labels=labels) def test_hinge_loss_multiclass_with_missing_labels(): From 02ad7f75a7d97dfa3ffefc91cd48f282170732b1 Mon Sep 17 00:00:00 2001 From: PierreAttard Date: Thu, 11 Mar 2021 09:16:11 +0100 Subject: [PATCH 07/16] line with less than 80 characters. --- doc/whats_new/v1.0.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/whats_new/v1.0.rst b/doc/whats_new/v1.0.rst index 895d59a4d4fde..a8bb18dca2f2e 100644 --- a/doc/whats_new/v1.0.rst +++ b/doc/whats_new/v1.0.rst @@ -165,9 +165,9 @@ Changelog class methods and will be removed in 1.2. :pr:`18543` by `Guillaume Lemaitre`_. -- |Enhancement| A fix to raise an error in :func:`metrics.hinge_loss` when ``pred_decision`` is 1d - whereas it is a multiclass classification or when ``pred_decision`` - parameter is not consistent with ``labels`` parameter. +- |Enhancement| A fix to raise an error in :func:`metrics.hinge_loss` when + ``pred_decision`` is 1d whereas it is a multiclass classification or when + ``pred_decision`` parameter is not consistent with ``labels`` parameter. :pr:`19643` by :user:`Pierre Attard `. - |Feature| :func:`metrics.mean_pinball_loss` exposes the pinball loss for From c2937c2f2ebb97a1ec617068f0673775614e6ea5 Mon Sep 17 00:00:00 2001 From: PierreAttard Date: Thu, 11 Mar 2021 09:24:59 +0100 Subject: [PATCH 08/16] Code standard modification E127 and E501 --- sklearn/metrics/_classification.py | 4 ++-- sklearn/metrics/tests/test_classification.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index 7fa3fd19ce616..0d6b4df89c171 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -2310,8 +2310,8 @@ def _check_valid_multiclass_decision_shape(y_true_unique, pred_decision, "with a multiclass target. " "pred_decision shape must be (n_samples, n_classes)") - invalid_decision_shape = pred_decision.ndim > 1 and \ - y_true_unique.size != pred_decision.shape[1] + invalid_decision_shape = (pred_decision.ndim > 1 and + y_true_unique.size != pred_decision.shape[1]) if labels is None: if invalid_decision_shape: diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 28c9524960bd4..61bb5758eb4da 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -2136,7 +2136,8 @@ def test_hinge_loss_multiclass_missing_labels_with_labels_none(): def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): - # test for inconsistency between multiclass problem and pred_decision argument + # test for inconsistency between multiclass problem and pred_decision + # argument y_true = [2, 1, 0, 1, 0, 1, 1] pred_decision = np.array([0, 1, 2, 1, 0, 2, 1]) error_message = (r"The shape of pred_decision can not be 1d array" From f2d5a7b240b44a373e9feea70c6829c7162f297c Mon Sep 17 00:00:00 2001 From: "p.attard" Date: Thu, 11 Mar 2021 09:38:20 +0100 Subject: [PATCH 09/16] resolve W605 invalid escape sequence --- sklearn/metrics/tests/test_classification.py | 24 +++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 61bb5758eb4da..278769dcdffd5 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -1,4 +1,3 @@ - from functools import partial from itertools import product from itertools import chain @@ -50,6 +49,7 @@ from scipy.spatial.distance import hamming as sp_hamming + ############################################################################### # Utilities for testing @@ -102,7 +102,6 @@ def make_prediction(dataset=None, binary=False): # Tests def test_classification_report_dictionary_output(): - # Test performance report with dictionary output iris = datasets.load_iris() y_true, y_pred, _ = make_prediction(dataset=iris, binary=False) @@ -135,7 +134,7 @@ def test_classification_report_dictionary_output(): target_names=iris.target_names, output_dict=True) # assert the 2 dicts are equal. - assert(report.keys() == expected_report.keys()) + assert (report.keys() == expected_report.keys()) for key in expected_report: if key == 'accuracy': assert isinstance(report[key], float) @@ -616,9 +615,9 @@ def test_cohen_kappa(): y2 = np.array([0] * 50 + [1] * 40 + [2] * 10) assert_almost_equal(cohen_kappa_score(y1, y2), .9315, decimal=4) assert_almost_equal(cohen_kappa_score(y1, y2, - weights="linear"), 0.9412, decimal=4) + weights="linear"), 0.9412, decimal=4) assert_almost_equal(cohen_kappa_score(y1, y2, - weights="quadratic"), 0.9541, decimal=4) + weights="quadratic"), 0.9541, decimal=4) @ignore_warnings @@ -782,7 +781,7 @@ def mcc_safe(y_true, y_pred): mcc_denominator = activity * pos_rate * (1 - activity) * (1 - pos_rate) return mcc_numerator / np.sqrt(mcc_denominator) - def random_ys(n_points): # binary + def random_ys(n_points): # binary x_true = rng.random_sample(n_points) x_pred = x_true + 0.2 * (rng.random_sample(n_points) - 0.5) y_true = (x_true > 0.5) @@ -1237,7 +1236,7 @@ def test_multilabel_hamming_loss(): assert hamming_loss(y1, np.zeros(y1.shape)) == 4 / 6 assert hamming_loss(y2, np.zeros(y1.shape)) == 0.5 assert hamming_loss(y1, y2, sample_weight=w) == 1. / 12 - assert hamming_loss(y1, 1-y2, sample_weight=w) == 11. / 12 + assert hamming_loss(y1, 1 - y2, sample_weight=w) == 11. / 12 assert hamming_loss(y1, np.zeros_like(y1), sample_weight=w) == 2. / 3 # sp_hamming only works with 1-D arrays assert hamming_loss(y1[0], y2[0]) == sp_hamming(y1[0], y2[0]) @@ -1436,6 +1435,7 @@ def test_jaccard_score_zero_division_set_value(zero_division, expected_score): assert score == pytest.approx(expected_score) assert len(record) == 0 + @ignore_warnings def test_precision_recall_f1_score_multilabel_1(): # Test precision_recall_f1_score on a crafted multilabel example @@ -1758,7 +1758,6 @@ def test_prf_warnings(): # average of per-label scores f, w = precision_recall_fscore_support, UndefinedMetricWarning for average in [None, 'weighted', 'macro']: - msg = ('Precision and F-score are ill-defined and ' 'being set to 0.0 in labels with no predicted samples.' ' Use `zero_division` parameter to control' @@ -1834,7 +1833,6 @@ def test_prf_no_warnings_if_zero_division_set(zero_division): # average of per-label scores f = precision_recall_fscore_support for average in [None, 'weighted', 'macro']: - assert_no_warnings(f, [0, 1, 2], [1, 1, 2], average=average, zero_division=zero_division) @@ -2140,9 +2138,9 @@ def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): # argument y_true = [2, 1, 0, 1, 0, 1, 1] pred_decision = np.array([0, 1, 2, 1, 0, 2, 1]) - error_message = (r"The shape of pred_decision can not be 1d array" + error_message = ("The shape of pred_decision can not be 1d array" "with a multiclass target. " - "pred_decision shape must be \(n_samples, n_classes\)") + r"pred_decision shape must be \(n_samples, n_classes\)") with pytest.raises(ValueError, match=error_message): hinge_loss(y_true=y_true, pred_decision=pred_decision) @@ -2150,10 +2148,10 @@ def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): pred_decision = [[0, 1], [0, 1], [0, 1], [0, 1], [2, 0], [0, 1], [1, 0]] labels = [0, 1, 2] - error_message = (r"The shape of pred_decision is not " + error_message = ("The shape of pred_decision is not " "consistent with the number of classes. " "pred_decision shape must be " - "\(n_samples, n_classes\) with " + r"\(n_samples, n_classes\) with " "multiclass target") with pytest.raises(ValueError, match=error_message): hinge_loss(y_true=y_true, pred_decision=pred_decision, labels=labels) From e30624e65cd2520cf4f15761d90c7b57101badbe Mon Sep 17 00:00:00 2001 From: "p.attard" Date: Thu, 11 Mar 2021 09:41:34 +0100 Subject: [PATCH 10/16] resolve E501 line too long --- sklearn/metrics/tests/test_classification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 278769dcdffd5..25d6cb80784dc 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -616,8 +616,8 @@ def test_cohen_kappa(): assert_almost_equal(cohen_kappa_score(y1, y2), .9315, decimal=4) assert_almost_equal(cohen_kappa_score(y1, y2, weights="linear"), 0.9412, decimal=4) - assert_almost_equal(cohen_kappa_score(y1, y2, - weights="quadratic"), 0.9541, decimal=4) + assert_almost_equal(cohen_kappa_score(y1, y2, weights="quadratic"), + 0.9541, decimal=4) @ignore_warnings From a01947c760c94d8cc3fb77cfd457d48e53ec6ebe Mon Sep 17 00:00:00 2001 From: "p.attard" Date: Thu, 11 Mar 2021 17:22:00 +0100 Subject: [PATCH 11/16] remove function use only one time. 1d pred_decision for a multiclass terget. --- sklearn/metrics/_classification.py | 60 ++++++++---------------------- 1 file changed, 16 insertions(+), 44 deletions(-) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index 0d6b4df89c171..ee15f905c79c3 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -2285,48 +2285,6 @@ def log_loss(y_true, y_pred, *, eps=1e-15, normalize=True, sample_weight=None, return _weighted_sum(loss, sample_weight, normalize) -def _check_valid_multiclass_decision_shape(y_true_unique, pred_decision, - labels): - """Check if the inputs are consistent. - - If it is a multiclass problem (it means thare are more than 2 classes - to classify), `pred_decision` parameter can't be 1d array. The column - number must be equal to classes number. - - If not every classes are present in `y_true`, `labels` parameter - must be filled in. - - Parameters - ---------- - y_true_unique : array-like - - pred_decision : array-like - - labels : array-like - """ - - if pred_decision.ndim <= 1: - raise ValueError("The shape of pred_decision can not be 1d array" - "with a multiclass target. " - "pred_decision shape must be (n_samples, n_classes)") - - invalid_decision_shape = (pred_decision.ndim > 1 and - y_true_unique.size != pred_decision.shape[1]) - - if labels is None: - if invalid_decision_shape: - raise ValueError("Please include all labels in y_true " - "or pass labels as third argument") - elif invalid_decision_shape: - raise ValueError("The shape of pred_decision is not " - "consistent with the number of classes. " - "pred_decision shape must be " - "(n_samples, n_classes) with " - "multiclass target") - - return - - @_deprecate_positional_args def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): """Average hinge loss (non-regularized). @@ -2415,8 +2373,22 @@ def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): if y_true_unique.size > 2: - _check_valid_multiclass_decision_shape(y_true_unique, pred_decision, - labels) + if pred_decision.ndim <= 1: + raise ValueError("The shape of pred_decision can not be 1d array" + "with a multiclass target. pred_decision shape " + "must be (n_samples, n_classes)") + + # pred_decision.ndim > 1 is true + if y_true_unique.size != pred_decision.shape[1]: + if labels is None: + raise ValueError("Please include all labels in y_true " + "or pass labels as third argument") + else: + raise ValueError("The shape of pred_decision is not " + "consistent with the number of classes. " + "pred_decision shape must be " + "(n_samples, n_classes) with " + "multiclass target") if labels is None: labels = y_true_unique le = LabelEncoder() From 0d3b31e85d1a77932a45ecf92033acc558c7969a Mon Sep 17 00:00:00 2001 From: "p.attard" Date: Thu, 11 Mar 2021 17:46:42 +0100 Subject: [PATCH 12/16] revert accidental cosmetic changes caused by an editor. --- sklearn/metrics/tests/test_classification.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 25d6cb80784dc..7cbc4dc562b39 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -1,3 +1,4 @@ + from functools import partial from itertools import product from itertools import chain @@ -49,7 +50,6 @@ from scipy.spatial.distance import hamming as sp_hamming - ############################################################################### # Utilities for testing @@ -102,6 +102,7 @@ def make_prediction(dataset=None, binary=False): # Tests def test_classification_report_dictionary_output(): + # Test performance report with dictionary output iris = datasets.load_iris() y_true, y_pred, _ = make_prediction(dataset=iris, binary=False) @@ -134,7 +135,7 @@ def test_classification_report_dictionary_output(): target_names=iris.target_names, output_dict=True) # assert the 2 dicts are equal. - assert (report.keys() == expected_report.keys()) + assert(report.keys() == expected_report.keys()) for key in expected_report: if key == 'accuracy': assert isinstance(report[key], float) @@ -615,9 +616,9 @@ def test_cohen_kappa(): y2 = np.array([0] * 50 + [1] * 40 + [2] * 10) assert_almost_equal(cohen_kappa_score(y1, y2), .9315, decimal=4) assert_almost_equal(cohen_kappa_score(y1, y2, - weights="linear"), 0.9412, decimal=4) - assert_almost_equal(cohen_kappa_score(y1, y2, weights="quadratic"), - 0.9541, decimal=4) + weights="linear"), 0.9412, decimal=4) + assert_almost_equal(cohen_kappa_score(y1, y2, + weights="quadratic"), 0.9541, decimal=4) @ignore_warnings @@ -781,7 +782,7 @@ def mcc_safe(y_true, y_pred): mcc_denominator = activity * pos_rate * (1 - activity) * (1 - pos_rate) return mcc_numerator / np.sqrt(mcc_denominator) - def random_ys(n_points): # binary + def random_ys(n_points): # binary x_true = rng.random_sample(n_points) x_pred = x_true + 0.2 * (rng.random_sample(n_points) - 0.5) y_true = (x_true > 0.5) @@ -1236,7 +1237,7 @@ def test_multilabel_hamming_loss(): assert hamming_loss(y1, np.zeros(y1.shape)) == 4 / 6 assert hamming_loss(y2, np.zeros(y1.shape)) == 0.5 assert hamming_loss(y1, y2, sample_weight=w) == 1. / 12 - assert hamming_loss(y1, 1 - y2, sample_weight=w) == 11. / 12 + assert hamming_loss(y1, 1-y2, sample_weight=w) == 11. / 12 assert hamming_loss(y1, np.zeros_like(y1), sample_weight=w) == 2. / 3 # sp_hamming only works with 1-D arrays assert hamming_loss(y1[0], y2[0]) == sp_hamming(y1[0], y2[0]) @@ -1435,7 +1436,6 @@ def test_jaccard_score_zero_division_set_value(zero_division, expected_score): assert score == pytest.approx(expected_score) assert len(record) == 0 - @ignore_warnings def test_precision_recall_f1_score_multilabel_1(): # Test precision_recall_f1_score on a crafted multilabel example @@ -1758,6 +1758,7 @@ def test_prf_warnings(): # average of per-label scores f, w = precision_recall_fscore_support, UndefinedMetricWarning for average in [None, 'weighted', 'macro']: + msg = ('Precision and F-score are ill-defined and ' 'being set to 0.0 in labels with no predicted samples.' ' Use `zero_division` parameter to control' @@ -1833,6 +1834,7 @@ def test_prf_no_warnings_if_zero_division_set(zero_division): # average of per-label scores f = precision_recall_fscore_support for average in [None, 'weighted', 'macro']: + assert_no_warnings(f, [0, 1, 2], [1, 1, 2], average=average, zero_division=zero_division) From 4b8839e0173fbabb35e9e1a60f5a12cdf9afc379 Mon Sep 17 00:00:00 2001 From: PierreAttard Date: Thu, 11 Mar 2021 19:44:49 +0100 Subject: [PATCH 13/16] Update doc/whats_new/v1.0.rst Co-authored-by: Olivier Grisel --- doc/whats_new/v1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats_new/v1.0.rst b/doc/whats_new/v1.0.rst index a8bb18dca2f2e..06764d2be6003 100644 --- a/doc/whats_new/v1.0.rst +++ b/doc/whats_new/v1.0.rst @@ -167,7 +167,7 @@ Changelog - |Enhancement| A fix to raise an error in :func:`metrics.hinge_loss` when ``pred_decision`` is 1d whereas it is a multiclass classification or when - ``pred_decision`` parameter is not consistent with ``labels`` parameter. + ``pred_decision`` parameter is not consistent with the ``labels`` parameter. :pr:`19643` by :user:`Pierre Attard `. - |Feature| :func:`metrics.mean_pinball_loss` exposes the pinball loss for From 9bd35c239662ba63cd53a924b554944f4018fb58 Mon Sep 17 00:00:00 2001 From: PierreAttard Date: Thu, 11 Mar 2021 21:22:08 +0100 Subject: [PATCH 14/16] Add precision to error messages. --- sklearn/metrics/_classification.py | 14 +++++++---- sklearn/metrics/tests/test_classification.py | 26 +++++++++++--------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/sklearn/metrics/_classification.py b/sklearn/metrics/_classification.py index ee15f905c79c3..97ee5a2e01340 100644 --- a/sklearn/metrics/_classification.py +++ b/sklearn/metrics/_classification.py @@ -2374,9 +2374,11 @@ def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): if y_true_unique.size > 2: if pred_decision.ndim <= 1: - raise ValueError("The shape of pred_decision can not be 1d array" + raise ValueError("The shape of pred_decision cannot be 1d array" "with a multiclass target. pred_decision shape " - "must be (n_samples, n_classes)") + "must be (n_samples, n_classes), that is " + f"({y_true.shape[0]}, {y_true_unique.size})." + f" Got: {pred_decision.shape}") # pred_decision.ndim > 1 is true if y_true_unique.size != pred_decision.shape[1]: @@ -2386,9 +2388,11 @@ def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None): else: raise ValueError("The shape of pred_decision is not " "consistent with the number of classes. " - "pred_decision shape must be " - "(n_samples, n_classes) with " - "multiclass target") + "With a multiclass target, pred_decision " + "shape must be " + "(n_samples, n_classes), that is " + f"({y_true.shape[0]}, {y_true_unique.size}). " + f"Got: {pred_decision.shape}") if labels is None: labels = y_true_unique le = LabelEncoder() diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 7cbc4dc562b39..2925bcdc6bad4 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -4,6 +4,7 @@ from itertools import chain from itertools import permutations import warnings +import re import numpy as np from scipy import linalg @@ -2138,24 +2139,27 @@ def test_hinge_loss_multiclass_missing_labels_with_labels_none(): def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): # test for inconsistency between multiclass problem and pred_decision # argument - y_true = [2, 1, 0, 1, 0, 1, 1] + y_true = np.array([2, 1, 0, 1, 0, 1, 1]) pred_decision = np.array([0, 1, 2, 1, 0, 2, 1]) - error_message = ("The shape of pred_decision can not be 1d array" - "with a multiclass target. " - r"pred_decision shape must be \(n_samples, n_classes\)") - with pytest.raises(ValueError, match=error_message): + error_message = ("The shape of pred_decision cannot be 1d array" + "with a multiclass target. pred_decision shape " + "must be (n_samples, n_classes), that is " + f"({y_true.shape[0]}, {np.unique(y_true).size}). " + f"Got: ({y_true.shape[0]},)") + with pytest.raises(ValueError, match=re.escape(error_message)): hinge_loss(y_true=y_true, pred_decision=pred_decision) # test for inconsistency between pred_decision shape and labels number - pred_decision = [[0, 1], [0, 1], [0, 1], [0, 1], - [2, 0], [0, 1], [1, 0]] + pred_decision = np.array([[0, 1], [0, 1], [0, 1], [0, 1], + [2, 0], [0, 1], [1, 0]]) labels = [0, 1, 2] error_message = ("The shape of pred_decision is not " "consistent with the number of classes. " - "pred_decision shape must be " - r"\(n_samples, n_classes\) with " - "multiclass target") - with pytest.raises(ValueError, match=error_message): + "With a multiclass target, pred_decision " + "shape must be (n_samples, n_classes), that is " + f"({y_true.shape[0]}, {np.unique(y_true).size}). " + f"Got: ({y_true.shape[0]}, {pred_decision.shape[1]})") + with pytest.raises(ValueError, match=re.escape(error_message)): hinge_loss(y_true=y_true, pred_decision=pred_decision, labels=labels) From fd2f658db8e6b2e257fafa3e26c562e56fb9c0b7 Mon Sep 17 00:00:00 2001 From: PierreAttard Date: Fri, 12 Mar 2021 18:37:43 +0100 Subject: [PATCH 15/16] Update sklearn/metrics/tests/test_classification.py Co-authored-by: Olivier Grisel --- sklearn/metrics/tests/test_classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 2925bcdc6bad4..335ae5f8dd780 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -2145,7 +2145,7 @@ def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): "with a multiclass target. pred_decision shape " "must be (n_samples, n_classes), that is " f"({y_true.shape[0]}, {np.unique(y_true).size}). " - f"Got: ({y_true.shape[0]},)") + f"Got: {pred_decision.shape}") with pytest.raises(ValueError, match=re.escape(error_message)): hinge_loss(y_true=y_true, pred_decision=pred_decision) From 6cb0dad2896675077ba60abce910707a0a80c562 Mon Sep 17 00:00:00 2001 From: "p.attard" Date: Fri, 12 Mar 2021 18:41:50 +0100 Subject: [PATCH 16/16] hard code expected and observed values in unitest --- sklearn/metrics/tests/test_classification.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sklearn/metrics/tests/test_classification.py b/sklearn/metrics/tests/test_classification.py index 2925bcdc6bad4..7b634e88f2275 100644 --- a/sklearn/metrics/tests/test_classification.py +++ b/sklearn/metrics/tests/test_classification.py @@ -2144,8 +2144,7 @@ def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): error_message = ("The shape of pred_decision cannot be 1d array" "with a multiclass target. pred_decision shape " "must be (n_samples, n_classes), that is " - f"({y_true.shape[0]}, {np.unique(y_true).size}). " - f"Got: ({y_true.shape[0]},)") + "(7, 3). Got: (7,)") with pytest.raises(ValueError, match=re.escape(error_message)): hinge_loss(y_true=y_true, pred_decision=pred_decision) @@ -2157,8 +2156,7 @@ def test_hinge_loss_multiclass_no_consistent_pred_decision_shape(): "consistent with the number of classes. " "With a multiclass target, pred_decision " "shape must be (n_samples, n_classes), that is " - f"({y_true.shape[0]}, {np.unique(y_true).size}). " - f"Got: ({y_true.shape[0]}, {pred_decision.shape[1]})") + "(7, 3). Got: (7, 2)") with pytest.raises(ValueError, match=re.escape(error_message)): hinge_loss(y_true=y_true, pred_decision=pred_decision, labels=labels)