diff --git a/python/cuml/cuml/metrics/cluster/adjusted_rand_index.pyx b/python/cuml/cuml/metrics/cluster/adjusted_rand_index.pyx index 957127e182..ac0993d22e 100644 --- a/python/cuml/cuml/metrics/cluster/adjusted_rand_index.pyx +++ b/python/cuml/cuml/metrics/cluster/adjusted_rand_index.pyx @@ -2,10 +2,10 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # -import cupy as cp +import numpy as np -from cuml.common import input_to_cuml_array from cuml.internals import get_handle +from cuml.internals.validation import check_array, check_consistent_length from libc.stdint cimport uintptr_t from pylibraft.common.handle cimport handle_t @@ -38,19 +38,35 @@ def adjusted_rand_score(labels_true, labels_pred, convert_dtype=True) -> float: handle = get_handle() cdef handle_t* handle_ = handle.getHandle() - labels_true, n_rows, _, _ = \ - input_to_cuml_array(labels_true, order='C', check_dtype=cp.int32, - convert_to_dtype=(cp.int32 if convert_dtype - else None)) - - labels_pred, _, _, _ = \ - input_to_cuml_array(labels_pred, order='C', check_dtype=cp.int32, - convert_to_dtype=(cp.int32 if convert_dtype - else None)) + labels_true = check_array( + labels_true, + ensure_2d=False, + ensure_min_samples=0, + order='C', + dtype=np.int32, + convert_dtype=convert_dtype, + input_name='labels_true', + ) + labels_pred = check_array( + labels_pred, + ensure_2d=False, + ensure_min_samples=0, + order='C', + dtype=np.int32, + convert_dtype=convert_dtype, + input_name='labels_pred', + ) + if labels_true.ndim != 1 or labels_pred.ndim != 1: + raise ValueError( + "labels_true and labels_pred must be 1D arrays, got shapes " + f"{labels_true.shape} and {labels_pred.shape}" + ) + check_consistent_length(labels_true, labels_pred) + cdef int n_rows = labels_true.shape[0] rand_score = adjusted_rand_index(handle_[0], - labels_true.ptr, - labels_pred.ptr, - n_rows) + labels_true.data.ptr, + labels_pred.data.ptr, + n_rows) return rand_score diff --git a/python/cuml/cuml/metrics/cluster/completeness_score.pyx b/python/cuml/cuml/metrics/cluster/completeness_score.pyx index f92db1513f..605e9d3117 100644 --- a/python/cuml/cuml/metrics/cluster/completeness_score.pyx +++ b/python/cuml/cuml/metrics/cluster/completeness_score.pyx @@ -60,8 +60,8 @@ def cython_completeness_score(labels_true, labels_pred) -> float: labels_pred ) - cdef uintptr_t ground_truth_ptr = y_true.ptr - cdef uintptr_t preds_ptr = y_pred.ptr + cdef uintptr_t ground_truth_ptr = y_true.data.ptr + cdef uintptr_t preds_ptr = y_pred.data.ptr com = completeness_score(handle_[0], ground_truth_ptr, diff --git a/python/cuml/cuml/metrics/cluster/entropy.pyx b/python/cuml/cuml/metrics/cluster/entropy.pyx index e5135636ee..9aa020905c 100644 --- a/python/cuml/cuml/metrics/cluster/entropy.pyx +++ b/python/cuml/cuml/metrics/cluster/entropy.pyx @@ -8,7 +8,7 @@ import cupy as cp import numpy as np from cuml.internals import get_handle -from cuml.internals.input_utils import input_to_cupy_array +from cuml.internals.validation import check_array from libc.stdint cimport uintptr_t from pylibraft.common.handle cimport handle_t @@ -44,11 +44,20 @@ def cython_entropy(clustering, base=None) -> float: handle = get_handle() cdef handle_t *handle_ = handle.getHandle() - clustering, n_rows, _, _ = input_to_cupy_array( + clustering = check_array( clustering, - check_dtype=np.int32, - check_cols=1 + ensure_2d=False, + order='C', + dtype=np.int32, + input_name='clustering', ) + if clustering.ndim == 2 and clustering.shape[1] != 1: + raise ValueError( + "clustering must have shape (n_samples,) or (n_samples, 1), got " + f"{clustering.shape}" + ) + clustering = clustering.ravel() + cdef int n_rows = clustering.shape[0] lower_class_range = cp.min(clustering).item() upper_class_range = cp.max(clustering).item() @@ -56,7 +65,7 @@ def cython_entropy(clustering, base=None) -> float: S = entropy(handle_[0], clustering_ptr, - n_rows, + n_rows, lower_class_range, upper_class_range) diff --git a/python/cuml/cuml/metrics/cluster/homogeneity_score.pyx b/python/cuml/cuml/metrics/cluster/homogeneity_score.pyx index e1ce8e67b4..fa829fafec 100644 --- a/python/cuml/cuml/metrics/cluster/homogeneity_score.pyx +++ b/python/cuml/cuml/metrics/cluster/homogeneity_score.pyx @@ -61,8 +61,8 @@ def cython_homogeneity_score(labels_true, labels_pred) -> float: labels_pred ) - cdef uintptr_t ground_truth_ptr = y_true.ptr - cdef uintptr_t preds_ptr = y_pred.ptr + cdef uintptr_t ground_truth_ptr = y_true.data.ptr + cdef uintptr_t preds_ptr = y_pred.data.ptr hom = homogeneity_score(handle_[0], ground_truth_ptr, diff --git a/python/cuml/cuml/metrics/cluster/mutual_info_score.pyx b/python/cuml/cuml/metrics/cluster/mutual_info_score.pyx index 3aef63b82a..9e98eea959 100644 --- a/python/cuml/cuml/metrics/cluster/mutual_info_score.pyx +++ b/python/cuml/cuml/metrics/cluster/mutual_info_score.pyx @@ -63,8 +63,8 @@ def cython_mutual_info_score(labels_true, labels_pred) -> float: labels_pred ) - cdef uintptr_t ground_truth_ptr = y_true.ptr - cdef uintptr_t preds_ptr = y_pred.ptr + cdef uintptr_t ground_truth_ptr = y_true.data.ptr + cdef uintptr_t preds_ptr = y_pred.data.ptr mi = mutual_info_score(handle_[0], ground_truth_ptr, diff --git a/python/cuml/cuml/metrics/cluster/silhouette_score.pyx b/python/cuml/cuml/metrics/cluster/silhouette_score.pyx index 3cb4ff1c6b..ecef4d959f 100644 --- a/python/cuml/cuml/metrics/cluster/silhouette_score.pyx +++ b/python/cuml/cuml/metrics/cluster/silhouette_score.pyx @@ -5,8 +5,8 @@ import cupy as cp import numpy as np -from cuml.common import input_to_cuml_array from cuml.internals import get_handle +from cuml.internals.validation import check_array, check_consistent_length from cuml.metrics.pairwise_distances import _determine_metric from libc.stdint cimport uintptr_t @@ -72,62 +72,69 @@ def _silhouette_coeff( if chunksize is None: chunksize = 40000 - data, n_rows, n_cols, dtype = input_to_cuml_array( + data = check_array( X, order='C', - convert_to_dtype=(np.float32 if convert_dtype - else None), - check_dtype=[np.float32, np.float64], + dtype=[np.float32, np.float64], + convert_dtype=convert_dtype, + input_name='X', ) + cdef int n_rows = data.shape[0] + cdef int n_cols = data.shape[1] + dtype = data.dtype - labels, _, _, _ = input_to_cuml_array( + labels = check_array( labels, + ensure_2d=False, order='C', - convert_to_dtype=np.int32 + dtype=np.int32, + input_name='labels', ) + if labels.ndim != 1: + raise ValueError( + f"labels must be a 1D array, got shape {labels.shape}" + ) + check_consistent_length(data, labels) - # Use cp.unique with return_inverse to get monotonic labels efficiently - labels_cupy = labels.to_output(output_type='cupy', output_dtype='int') - unique_labels, inverse = cp.unique(labels_cupy, return_inverse=True) - n_labels = unique_labels.shape[0] - - mono_labels, _, _, _ = input_to_cuml_array( - inverse, - order='C', - convert_to_dtype=np.int32 - ) + # Use cp.unique with return_inverse to get monotonic labels efficiently. + unique_labels, inverse = cp.unique(labels, return_inverse=True) + cdef int n_labels = unique_labels.shape[0] + mono_labels = cp.ascontiguousarray(inverse, dtype=np.int32) cdef uintptr_t scores_ptr if sil_scores is None: scores_ptr = NULL else: - sil_scores = input_to_cuml_array( + sil_scores = check_array( sil_scores, - convert_to_dtype=(dtype if convert_dtype - else None), - check_dtype=dtype)[0] - - scores_ptr = sil_scores.ptr + ensure_2d=False, + order='C', + dtype=[dtype], + convert_dtype=convert_dtype, + input_name='sil_scores', + ensure_all_finite=False, # output buffer may be uninitialized + ) + scores_ptr = sil_scores.data.ptr metric = _determine_metric(metric) if dtype == np.float32: return silhouette_score(handle_[0], - data.ptr, - n_rows, - n_cols, - mono_labels.ptr, - n_labels, + data.data.ptr, + n_rows, + n_cols, + mono_labels.data.ptr, + n_labels, scores_ptr, chunksize, metric) elif dtype == np.float64: return silhouette_score(handle_[0], - data.ptr, - n_rows, - n_cols, - mono_labels.ptr, - n_labels, + data.data.ptr, + n_rows, + n_cols, + mono_labels.data.ptr, + n_labels, scores_ptr, chunksize, metric) diff --git a/python/cuml/cuml/metrics/cluster/utils.py b/python/cuml/cuml/metrics/cluster/utils.py index fdddaa1005..714b1a1552 100644 --- a/python/cuml/cuml/metrics/cluster/utils.py +++ b/python/cuml/cuml/metrics/cluster/utils.py @@ -1,10 +1,10 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # -import cupy as cp +import numpy as np -from cuml.common import input_to_cuml_array +from cuml.internals.validation import check_array, check_consistent_length from cuml.metrics.utils import sorted_unique_labels from cuml.prims.label import make_monotonic @@ -12,26 +12,43 @@ def prepare_cluster_metric_inputs(labels_true, labels_pred): """Helper function to avoid code duplication for homogeneity score, mutual info score and completeness score. + + Returns ``(y_true, y_pred, n_rows, lower_class_range, upper_class_range)`` + where ``y_true`` and ``y_pred`` are C-contiguous int32 ``cupy.ndarray`` + arrays whose label values have been remapped to the contiguous range + ``[0, len(classes) - 1]``. """ - y_true, n_rows, _, dtype = input_to_cuml_array( + y_true = check_array( labels_true, - check_dtype=[cp.int32, cp.int64], - check_cols=1, - deepcopy=True, # deepcopy because we call make_monotonic inplace below + ensure_2d=False, + ensure_min_samples=0, + order="C", + dtype=np.int32, + input_name="labels_true", ) - - y_pred, _, _, _ = input_to_cuml_array( + y_pred = check_array( labels_pred, - check_dtype=dtype, - check_rows=n_rows, - check_cols=1, - deepcopy=True, # deepcopy because we call make_monotonic inplace below + ensure_2d=False, + ensure_min_samples=0, + order="C", + dtype=np.int32, + input_name="labels_pred", ) + if y_true.ndim != 1 or y_pred.ndim != 1: + raise ValueError( + "labels_true and labels_pred must be 1D arrays, got shapes " + f"{y_true.shape} and {y_pred.shape}" + ) + check_consistent_length(y_true, y_pred) + n_rows = y_true.shape[0] classes = sorted_unique_labels(y_true, y_pred) - make_monotonic(y_true, classes=classes, copy=False) - make_monotonic(y_pred, classes=classes, copy=False) + # Make copies so that we never mutate the caller's input arrays. + # ``make_monotonic`` with ``copy=True`` returns new cupy arrays; we + # use those for the downstream Cython callers. + y_true, _ = make_monotonic(y_true, classes=classes, copy=True) + y_pred, _ = make_monotonic(y_pred, classes=classes, copy=True) # Those values are only correct because we used make_monotonic lower_class_range = 0 diff --git a/python/cuml/cuml/metrics/cluster/v_measure.pyx b/python/cuml/cuml/metrics/cluster/v_measure.pyx index 4efcc7939a..ebca124934 100644 --- a/python/cuml/cuml/metrics/cluster/v_measure.pyx +++ b/python/cuml/cuml/metrics/cluster/v_measure.pyx @@ -67,8 +67,8 @@ def cython_v_measure(labels_true, labels_pred, beta=1.0) -> float: labels_pred ) - cdef uintptr_t ground_truth_ptr = y_true.ptr - cdef uintptr_t preds_ptr = y_pred.ptr + cdef uintptr_t ground_truth_ptr = y_true.data.ptr + cdef uintptr_t preds_ptr = y_pred.data.ptr v_measure_value = v_measure(handle_[0], ground_truth_ptr,