Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 47 additions & 31 deletions python/cuml/cuml/metrics/_classification.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)
Comment thread
jcrist marked this conversation as resolved.
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

Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
38 changes: 22 additions & 16 deletions python/cuml/cuml/metrics/_ranking.py
Original file line number Diff line number Diff line change
@@ -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
#

Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand Down
34 changes: 27 additions & 7 deletions python/cuml/tests/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"] + [
Expand Down
Loading