Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
18 changes: 18 additions & 0 deletions docs/source/cuml-accel/compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,24 @@ sklearn.preprocessing
``LabelBinarizer`` has no known estimator-specific ``cuml.accel`` limitations.


.. dropdown:: ``OneHotEncoder``
:name: onehotencoder

``OneHotEncoder`` will fall back to CPU in the following cases:

- If ``dtype`` is not a float or bool dtype.
- If ``drop`` is ``"if_binary"``
- If ``handle_unknown`` is ``"warn"`` or ``"infrequent_if_exist"``.
- If ``min_frequency`` is not ``None``.
- If ``max_categories`` is not ``None``.
- If ``feature_name_combiner`` is a callable.

Additional notes:
Comment thread
jcrist marked this conversation as resolved.

- cuML's encoder treats ``None`` and ``NaN`` values as identical, while
scikit-learn's encoder treats these as different categories.
Comment thread
jcrist marked this conversation as resolved.


.. dropdown:: ``TargetEncoder``
:name: targetencoder

Expand Down
10 changes: 9 additions & 1 deletion python/cuml/cuml/accel/_overrides/sklearn/preprocessing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
import numpy as np
Expand All @@ -17,6 +17,7 @@
"MinMaxScaler",
"MaxAbsScaler",
"PolynomialFeatures",
"OneHotEncoder",
"TargetEncoder",
"LabelEncoder",
"LabelBinarizer",
Expand Down Expand Up @@ -55,6 +56,13 @@ def _params_from_cpu(model):
return model.get_params(deep=False)


class OneHotEncoder(ProxyBase):
_gpu_class = cuml.preprocessing.OneHotEncoder

def _gpu_fit_transform(self, X, y=None, **fit_params):
return self._gpu.fit_transform(X, y=y, **fit_params)


class LabelEncoder(ProxyBase):
_gpu_class = cuml.preprocessing.LabelEncoder

Expand Down
73 changes: 45 additions & 28 deletions python/cuml/cuml/internals/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,51 +948,68 @@ def check_cudf(
if ensure_min_features > 1 and ensure_ndim != 2:
raise ValueError(f"{ensure_min_features=!r} requires ensure_ndim=2")

if cp_sp.issparse(array) or sp.issparse(array):
padded_input = f" for {input_name}" if input_name else ""
raise TypeError(
f"Sparse data was passed{padded_input}, but dense data is required. "
"Use '.toarray()' to convert to a dense array."
)

array_type = type(array)

# Coerce input to a cudf type.
# XXX: cudf currently doesn't support float16, any float16 input is
# automatically upcast here to float32.
# XXX: hardcode `nan_as_null=True` (cudf's default) so the behavior
# doesn't switch when cudf.pandas is active.
if isinstance(array, pd.Series):
if array.dtype == "float16":
array = array.astype("float32")
array = cudf.Series(array)
array = cudf.Series(array, nan_as_null=True)
elif isinstance(array, pd.DataFrame):
f16_cols = array.select_dtypes("float16").columns.tolist()
if f16_cols:
array = array.astype({c: "float32" for c in f16_cols})
array = cudf.DataFrame(array)
elif not isinstance(array, (cudf.DataFrame, cudf.Series)):
# Remaining array-like inputs go through check_array first (without
# device transfer) to normalize on cupy/numpy before coercion to cudf
array = check_array(
array,
mem_type=None,
ensure_2d=False,
ensure_min_samples=0,
ensure_min_features=0,
ensure_all_finite=False,
input_name=input_name,
)
array = cudf.DataFrame(array, nan_as_null=True)

if not isinstance(array, (cudf.DataFrame, cudf.Series)):
# Normalize to numpy or cupy array with minimal copying
if hasattr(array, "__cuda_array_interface__"):
array = cp.asarray(array)
elif hasattr(array, "__array__") or hasattr(
array, "__array_interface__"
):
array = np.asarray(array)
elif not isinstance(array, np.ndarray):
array = np.asarray(array, dtype=object)

if array.dtype.kind == "c":
raise ValueError("Complex data not supported")

if array.dtype == "float16":
array = array.astype("float32")
elif (
array.dtype == "object"
and array.size
and not isinstance(array.flat[0], str)
):
# XXX: cudf doesn't support coercing numeric object arrays, while
# sklearn has a common check that object arrays of floats are
# supported. To support this uncommon case, we attempt to coerce
# numeric object types here.
array = array.astype("float64")
array = (cudf.DataFrame if array.ndim == 2 else cudf.Series)(
array, dtype=(np.dtype("O") if array.dtype.kind in "U" else None)
)

array_shape = array.shape
cls = cudf.DataFrame if array.ndim == 2 else cudf.Series
if array.dtype == "object":
# For object dtype inputs, coerce back to list (cheap) to rely on
# cudf's per-column dtype inference. On failure raise an error
# compatible with what sklearn's `check_dtype_object` expects.
try:
array = cls(array.tolist(), nan_as_null=True)
except Exception as exc:
raise TypeError(
f"An object dtype {input_name or 'input'} argument must be "
"composed of strings, numbers, booleans, or nulls."
) from exc
else:
array = cls(array, nan_as_null=True)
else:
array_shape = array.shape

# Validate shape and coerce dimensionality
_check_shape(
array.shape,
array_shape,
ensure_2d=(ensure_ndim == 2 and coerce_ndim is False),
ensure_min_samples=ensure_min_samples,
ensure_min_features=ensure_min_features,
Expand Down
Loading
Loading