diff --git a/python/cuml/cuml/metrics/_classification.py b/python/cuml/cuml/metrics/_classification.py index ae0e245b64..38e9e1977d 100644 --- a/python/cuml/cuml/metrics/_classification.py +++ b/python/cuml/cuml/metrics/_classification.py @@ -1,15 +1,19 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # import cudf import cupy as cp import numpy as np -from cuml.internals.input_utils import input_to_cupy_array +from cuml.internals.validation import ( + check_array, + check_consistent_length, + check_sample_weight, +) -def _input_to_cupy_or_cudf_series(x, check_rows=None): +def _input_to_cupy_or_cudf_series(x): """Coerce the input to a 1D cupy array or cudf Series. For classification problems we need to support the full range @@ -21,25 +25,28 @@ def _input_to_cupy_or_cudf_series(x, check_rows=None): if isinstance(x, cudf.Series): # Drop the index so comparisons don't try to align on index out = x.reset_index(drop=True) - n_cols = 1 else: try: - out, _, n_cols, _ = input_to_cupy_array(x) - out = out.squeeze() # ensure 1D - except ValueError: - # Unsupported dtype, use cudf instead + out = check_array( + x, + ensure_2d=False, + ensure_all_finite=False, + mem_type="device", + order=None, + ) + except (ValueError, TypeError): + # Unsupported dtype (e.g. strings), use cudf instead # Drop the index so comparisons don't try to align on index out = cudf.Series(x, nan_as_null=False, copy=False).reset_index( drop=True ) - n_cols = 1 - - n_rows = len(out) - - if n_cols > 1: - raise ValueError(f"Expected 1 column but got {n_cols} columns.") - if check_rows is not None and n_rows != check_rows: - raise ValueError(f"Expected {check_rows} rows but got {n_rows} rows.") + else: + if out.ndim > 1: + if out.shape[1] > 1: + raise ValueError( + f"Expected 1 column but got {out.shape[1]} columns." + ) + out = out.squeeze() # ensure 1D return out @@ -68,7 +75,9 @@ def accuracy_score(y_true, y_pred, *, sample_weight=None, normalize=True): """ y_true = _input_to_cupy_or_cudf_series(y_true) - y_pred = _input_to_cupy_or_cudf_series(y_pred, check_rows=len(y_true)) + y_pred = _input_to_cupy_or_cudf_series(y_pred) + + check_consistent_length(y_true, y_pred) # Categorical dtypes in cudf currently don't coerce nicely on equality, # we need to manually cast to cudf.Series and align dtypes. @@ -84,13 +93,10 @@ def accuracy_score(y_true, y_pred, *, sample_weight=None, normalize=True): y_pred.dtype ) - if sample_weight is not None: - sample_weight = input_to_cupy_array( - sample_weight, - check_dtype=[np.float32, np.float64, np.int32, np.int64], - check_cols=1, - check_rows=len(y_true), - ).array.squeeze() # ensure 1D + if ( + sample_weight := check_sample_weight(sample_weight, dtype=np.float64) + ) is not None: + check_consistent_length(y_true, sample_weight) correct = y_true == y_pred @@ -150,21 +156,31 @@ def log_loss( The logarithm used is the natural logarithm (base-e). """ - y_true, n_rows, n_cols, ytype = input_to_cupy_array( - y_true, check_dtype=[np.int32, np.int64, np.float32, np.float64] + y_true = check_array( + y_true, + ensure_2d=False, + dtype=(np.int32, np.int64, np.float32, np.float64), + ensure_non_negative=True, + input_name="y_true", ) if y_true.dtype.kind == "f" and np.any(y_true != y_true.astype(int)): raise ValueError("'y_true' can only have integer values") - if y_true.min() < 0: - raise ValueError("'y_true' cannot have negative values") - y_pred, _, _, _ = input_to_cupy_array( + y_pred = check_array( y_pred, - check_dtype=[np.int32, np.int64, np.float32, np.float64], - check_rows=n_rows, + ensure_2d=False, + dtype=(np.float32, np.float64), + input_name="y_pred", ) + check_consistent_length(y_true, y_pred) + + if ( + sample_weight := check_sample_weight(sample_weight, dtype=np.float64) + ) is not None: + check_consistent_length(y_true, sample_weight) + y_true_max = y_true.max() if (y_pred.ndim == 1 and y_true_max > 1) or ( y_pred.ndim > 1 and y_pred.shape[1] <= y_true_max diff --git a/python/cuml/cuml/metrics/_ranking.py b/python/cuml/cuml/metrics/_ranking.py index 355f86d3b5..ba400bbdb8 100644 --- a/python/cuml/cuml/metrics/_ranking.py +++ b/python/cuml/cuml/metrics/_ranking.py @@ -1,5 +1,5 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # @@ -11,7 +11,7 @@ import cuml.internals from cuml.internals.array import CumlArray -from cuml.internals.input_utils import input_to_cupy_array +from cuml.internals.validation import check_array, check_consistent_length @cuml.internals.reflect @@ -76,16 +76,19 @@ def precision_recall_curve( [0.35 0.4 0.8 ] """ - y_true, n_rows, n_cols, ytype = input_to_cupy_array( - y_true, check_dtype=[np.int32, np.int64, np.float32, np.float64] + y_true = check_array( + y_true, + ensure_2d=False, + dtype=(np.int32, np.int64, np.float32, np.float64), + input_name="y_true", ) - - y_score, _, _, _ = input_to_cupy_array( + y_score = check_array( probs_pred, - check_dtype=[np.int32, np.int64, np.float32, np.float64], - check_rows=n_rows, - check_cols=n_cols, + ensure_2d=False, + dtype=(np.int32, np.int64, np.float32, np.float64), + input_name="probs_pred", ) + check_consistent_length(y_true, y_score) if cp.any(y_true) == 0: raise ValueError( @@ -140,16 +143,19 @@ def roc_auc_score(y_true, y_score): 0.75 """ - y_true, n_rows, n_cols, ytype = input_to_cupy_array( - y_true, check_dtype=[np.int32, np.int64, np.float32, np.float64] + y_true = check_array( + y_true, + ensure_2d=False, + dtype=(np.int32, np.int64, np.float32, np.float64), + input_name="y_true", ) - - y_score, _, _, _ = input_to_cupy_array( + y_score = check_array( y_score, - check_dtype=[np.int32, np.int64, np.float32, np.float64], - check_rows=n_rows, - check_cols=n_cols, + ensure_2d=False, + dtype=(np.int32, np.int64, np.float32, np.float64), + input_name="y_score", ) + check_consistent_length(y_true, y_score) return _binary_roc_auc_score(y_true, y_score) diff --git a/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml b/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml index e15c04478b..f7d71d7274 100644 --- a/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml +++ b/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml @@ -694,10 +694,6 @@ - "sklearn.linear_model.tests.test_ridge::test_ridge_regression_unpenalized_vstacked_X[42-wide-True-saga]" - "sklearn.linear_model.tests.test_ridge::test_ridge_regression_unpenalized_vstacked_X[42-wide-True-sparse_cg]" - "sklearn.linear_model.tests.test_ridge::test_ridge_regression_unpenalized_vstacked_X[42-wide-True-svd]" - - "sklearn.neighbors.tests.test_neighbors::test_neighbor_classifiers_loocv[auto-nn_model0]" - - "sklearn.neighbors.tests.test_neighbors::test_neighbor_classifiers_loocv[ball_tree-nn_model0]" - - "sklearn.neighbors.tests.test_neighbors::test_neighbor_classifiers_loocv[brute-nn_model0]" - - "sklearn.neighbors.tests.test_neighbors::test_neighbor_classifiers_loocv[kd_tree-nn_model0]" - "sklearn.tests.test_common::test_estimators[ElasticNet()-check_sample_weight_equivalence_on_sparse_data]" - "sklearn.tests.test_common::test_estimators[Lasso()-check_sample_weight_equivalence_on_sparse_data]" - "sklearn.tests.test_common::test_estimators[LogisticRegression()-check_sample_weight_equivalence_on_dense_data]" diff --git a/python/cuml/tests/test_metrics.py b/python/cuml/tests/test_metrics.py index de67706f41..2cb8a8e13e 100644 --- a/python/cuml/tests/test_metrics.py +++ b/python/cuml/tests/test_metrics.py @@ -247,19 +247,39 @@ def test_accuracy_score_errors(): arr_4 = np.array([1, 2, 3, 4]) arr_3x3 = np.ones((3, 3)) - with pytest.raises(ValueError, match="Expected 3 rows"): + with pytest.raises(ValueError, match="inconsistent number of samples"): cuml.metrics.accuracy_score(arr_3, arr_4) - with pytest.raises(ValueError, match="Expected 3 rows"): + with pytest.raises(ValueError, match="inconsistent number of samples"): cuml.metrics.accuracy_score(arr_3, arr_3, sample_weight=arr_4) - for true, pred, sw in [ - (arr_3x3, arr_3, None), - (arr_3, arr_3x3, None), - (arr_3, arr_3, arr_3x3), + for true, pred in [ + (arr_3x3, arr_3), + (arr_3, arr_3x3), ]: with pytest.raises(ValueError, match="Expected 1 column"): - cuml.metrics.accuracy_score(true, pred, sample_weight=sw) + cuml.metrics.accuracy_score(true, pred) + + with pytest.raises(ValueError, match="1D array"): + cuml.metrics.accuracy_score(arr_3, arr_3, sample_weight=arr_3x3) + + +def test_accuracy_score_scalar_sample_weight(): + y_true = np.array([0, 1, 1, 0]) + y_pred = np.array([0, 1, 0, 0]) + + expected = cuml.metrics.accuracy_score(y_true, y_pred) + assert ( + cuml.metrics.accuracy_score(y_true, y_pred, sample_weight=1.0) + == expected + ) + assert ( + cuml.metrics.accuracy_score(y_true, y_pred, sample_weight=2.5) + == expected + ) + assert cuml.metrics.accuracy_score( + y_true, y_pred, sample_weight=1.0, normalize=False + ) == cuml.metrics.accuracy_score(y_true, y_pred, normalize=False) dataset_names = ["noisy_circles", "noisy_moons", "aniso"] + [