Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2ac4fb7
Add `check_consistent_length`
jcrist Apr 13, 2026
131f6db
Add `check_all_finite`
jcrist Apr 13, 2026
532dc27
Add `check_non_negative`
jcrist Apr 13, 2026
6f02b7e
Add `_check_shape`
jcrist Apr 13, 2026
174dcd8
Skip `check_all_finite` if `assume_finite=True`
jcrist Apr 14, 2026
d1c612d
Disable multiple errors in hypothesis
jcrist Apr 16, 2026
6e6fe8f
Fixup `check_consistent_length`
jcrist Apr 16, 2026
61eb133
Add `check_array` and tests
jcrist Apr 13, 2026
f40733b
Add `check_y` and tests
jcrist Apr 15, 2026
ba3769a
Use `check_y` in classifiers
jcrist Apr 16, 2026
c475ce5
Fix tests with `cudf.pandas` enabled
jcrist Apr 16, 2026
0d4462d
Change default order to `'A'`
jcrist Apr 16, 2026
3d7bbf9
Add `check_sample_weight`
jcrist Apr 16, 2026
2891a8b
Add `check_inputs`
jcrist Apr 16, 2026
312d1a2
Error messages more consistent with sklearn
jcrist Apr 17, 2026
3496df1
A few more tests
jcrist Apr 20, 2026
d559cb6
Update xfail list
jcrist Apr 20, 2026
4160d06
`check_inputs` have y_dtype default to follow X
jcrist Apr 20, 2026
4ad68a3
Error on all zero sample_weight
jcrist Apr 21, 2026
50bbbbd
check_y error on non-str object arrays
jcrist Apr 21, 2026
09e55df
Accept large sparse if can be coerced to small sparse
jcrist Apr 21, 2026
b46d8df
Fix bug in floating integral check on large doubles
jcrist Apr 21, 2026
f913c50
Fixup check_all_finite on np.array([-inf, inf])
jcrist Apr 21, 2026
e3f12fa
Fixup check_sample_weight scalar checks
jcrist Apr 22, 2026
ef56c81
Update comment on cudf.pandas
jcrist Apr 22, 2026
b7cb21d
Fixup check_sample_weight docstring
jcrist Apr 22, 2026
4db315b
Support empty y and sample_weight
jcrist Apr 22, 2026
7466b8d
Unxfail one more test
jcrist Apr 22, 2026
ece977a
More fixups?
jcrist Apr 22, 2026
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
133 changes: 2 additions & 131 deletions python/cuml/cuml/common/classification.py
Original file line number Diff line number Diff line change
@@ -1,142 +1,13 @@
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
import warnings

import cudf
import cupy as cp
import numpy as np
import pandas as pd

from cuml.internals.array import CumlArray, cuda_ptr
from cuml.internals.input_utils import input_to_cuml_array, input_to_cupy_array
from cuml.internals.input_utils import input_to_cupy_array
from cuml.internals.output_utils import cudf_to_pandas

is_integral = cp.ReductionKernel(
"T x",
"bool out",
"ceilf(x) == x",
"a && b",
"out = a",
"true",
"is_integral",
)


def check_classification_targets(y):
"""Check if `y` is composed of valid class labels"""
if y.dtype.kind == "f" and not is_integral(y):
raise ValueError(
"Unknown label type: continuous. Maybe you are trying to fit a "
"classifier, which expects discrete classes on a regression target "
"with continuous values."
)


def preprocess_labels(
y, dtype=None, order="C", n_samples=None, allow_multitarget=False
):
"""Preprocess the `y` input to a classifier.

Parameters
----------
y : array-like
The labels for fitting, may be any type cuml supports as input.
dtype : dtype, optional
The output dtype to use for the encoded labels. If not provided,
a data-dependent integral type will be used.
order : {"C", "F"}, optional
The array order to use for the encoded labels.
n_samples : int, optional
If provided, will raise an error if the number of samples in `y`
doesn't match.
allow_multitarget : bool, optional
Whether to allow multi-target labels.

Returns
-------
y_encoded : cp.ndarray
The labels, encoded as integers in [0, n_classes - 1].
classes : np.ndarray or list[np.ndarray]
The classes as a numpy array, or a list of numpy arrays if
y is multi-target.
"""
# cudf may coerce the dtype, store the original so we can cast back later
y_dtype = y.dtype if isinstance(y, np.ndarray) else None

# No cuda container supports all dtypes. Here we coerce to cupy when
# possible, falling back to cudf Series/DataFrame otherwise.
if isinstance(y, np.ndarray) and y.dtype.kind in "iufb":
y = cp.asarray(y)
elif isinstance(y, pd.DataFrame):
y = cudf.DataFrame(y)
elif isinstance(y, pd.Series):
y = cudf.Series(y)
elif not isinstance(y, (cp.ndarray, cudf.DataFrame, cudf.Series)):
# Non-numeric dtype, always go through cudf
y = input_to_cuml_array(y, convert_to_mem_type=False).array
if y.dtype.kind in "iufb":
y = y.to_output("cupy")
else:
y = (cudf.DataFrame if y.ndim == 2 else cudf.Series)(
y, dtype=(np.dtype("O") if y.dtype.kind in "U" else None)
)

# Validate dimensionality, ensuring 1D/2D y is as expected
if y.ndim == 2 and y.shape[1] == 1:
warnings.warn(
"A column-vector y was passed when a 1d array was expected. Please "
"change the shape of y to (n_samples,), for example using ravel()."
)
y = y.iloc[:, 0] if isinstance(y, cudf.DataFrame) else y.ravel()
elif allow_multitarget and y.ndim not in (1, 2):
raise ValueError(
f"y should be a 1d or 2d array, got an array of shape {y.shape} instead."
)
elif not allow_multitarget and y.ndim != 1:
raise ValueError(
f"y should be a 1d array, got an array of shape {y.shape} instead."
)

# Validate correct number of samples
if n_samples is not None and y.shape[0] != n_samples:
raise ValueError(
f"Expected `y` with {n_samples} samples, got {y.shape[0]}"
)

def _encode(y):
"""Encode `y` to codes and classes"""
check_classification_targets(y)
if isinstance(y, cudf.Series):
y = y.astype("category")
codes = cp.asarray(y.cat.codes)
classes = y.cat.categories.to_numpy()
# cudf will sometimes translate non-numeric dtypes. Coerce back to
# the input dtype if the input was originally a numpy array.
if y_dtype is not None:
classes = classes.astype(y_dtype, copy=False)
else:
classes, codes = cp.unique(y, return_inverse=True)
classes = classes.get()
return codes, classes

if y.ndim == 1:
y_encoded, classes = _encode(y)
if dtype is not None:
y_encoded = y_encoded.astype(dtype, copy=False)
else:
getter = y.iloc if isinstance(y, cudf.DataFrame) else y
encoded_cols, classes = zip(
*(_encode(getter[:, i]) for i in range(y.shape[1]))
)
classes = list(classes)
if dtype is None:
dtype = cp.result_type(*(c.dtype for c in encoded_cols))
y_encoded = cp.empty(shape=y.shape, dtype=dtype, order=order)
for i, col in enumerate(encoded_cols):
y_encoded[:, i] = col

return y_encoded, classes


def decode_labels(y_encoded, classes, output_type="cupy"):
"""Convert encoded labels back into their original classes.
Expand Down
9 changes: 4 additions & 5 deletions python/cuml/cuml/ensemble/randomforestclassifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
import cuml.internals
import cuml.internals.nvtx as nvtx
from cuml.common.array_descriptor import CumlArrayDescriptor
from cuml.common.classification import decode_labels, preprocess_labels
from cuml.common.classification import decode_labels
from cuml.common.doc_utils import generate_docstring, insert_into_docstring
from cuml.ensemble.randomforest_common import BaseRandomForestModel
from cuml.internals.array import CumlArray
from cuml.internals.input_utils import input_to_cuml_array
from cuml.internals.interop import UnsupportedOnGPU
from cuml.internals.mixins import ClassifierMixin
from cuml.internals.validation import check_features
from cuml.internals.validation import check_features, check_y
from cuml.metrics import accuracy_score


Expand Down Expand Up @@ -222,15 +222,14 @@ def fit(self, X, y, *, convert_dtype=True) -> "RandomForestClassifier":
y to be of dtype int32. This will increase memory used for
the method.
"""
y, classes = check_y(y, dtype=cp.int32, return_classes=True)
X_m = input_to_cuml_array(
X,
convert_to_dtype=(np.float32 if convert_dtype else None),
check_dtype=[np.float32, np.float64],
order="F",
check_rows=y.shape[0],
).array
y, classes = preprocess_labels(
y, n_samples=X_m.shape[0], dtype=cp.int32
)
self.classes_ = classes
self.n_classes_ = len(classes)
y_m = CumlArray(data=y)
Expand Down
Loading
Loading