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
18 changes: 12 additions & 6 deletions python/cuml/cuml/internals/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,11 @@ def reflect(
provide ``None`` to disable this inference entirely; in this case the
output type is expected to be specified manually either internal or
external to the method.
reset : bool, default=False
Set to True for methods like ``fit`` that reset the reflected type on
an estimator.
reset : bool or "type", default=False
If True, both the features and reflected type are reset on the estimator.
If ``"type"``, only the reflected type is reset on the estimator.
Defaults to False, to not reset anything. Most estimators should set
``reset=True`` on any fit-like methods.
"""
# Local to avoid circular imports
import cuml.accel
Expand Down Expand Up @@ -391,9 +393,12 @@ def reflect(
if array is not None:
array = _get_param(sig, array)

if reset and (model is None or array is None):
if reset not in (True, False, "type"):
raise ValueError(f"reset={reset!r} is not supported")

if (reset is not False) and (model is None or array is None):
raise ValueError(
"`reset=True` is not valid with `array=None` or `model=None`"
f"`reset={reset}` is not valid with `array=None` or `model=None`"
)

@functools.wraps(func)
Expand All @@ -411,8 +416,9 @@ def inner(*args, **kwargs):
array_arg = np.asarray(array_arg)

with enter_internal_context() as was_external:
if reset:
if reset is not False:
model_arg._set_output_type(array_arg)
if reset is True:
check_features(model_arg, array_arg, reset=True)

res = func(*args, **kwargs)
Expand Down
36 changes: 32 additions & 4 deletions python/cuml/cuml/internals/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ def _get_n_features(X):
return len(row)
except Exception:
pass
return 1

if hasattr(X, "shape"):
shape = X.shape
Expand All @@ -82,9 +81,38 @@ def _get_n_features(X):
else:
shape = np.asarray(X).shape

# TODO: Can remove the fallback to 1 when we finish dropping support
# for 1D X inputs
return shape[1] if len(shape) >= 2 else 1
ndim = len(shape)

if ndim != 2:
import cuml.accel

if isinstance(X, (cudf.Series, pd.Series)):
msg = (
f"Expected a 2-dimensional container but got {type(X).__name__} "
"instead. Pass a DataFrame containing a single row (i.e. "
"single sample) or a single column (i.e. single feature) "
"instead."
)
else:
kind = "scalar" if ndim == 0 else f"{ndim}D"
msg = (
f"Expected 2D array, got {kind} array instead. Reshape your data "
"using array.reshape(-1, 1) if your data has a single feature, "
"or array.reshape(1, -1) if it contains a single sample."
)

if cuml.accel.enabled() or ndim > 2:
raise ValueError(msg)
else:
warnings.warn(
"Support for passing non-2-dimensional X was deprecated in 26.04 "
"and will be removed in version 26.06 of cuML. In version 26.06 this will error "
f"with the following message:\n\n{msg}",
FutureWarning,
)
Comment on lines +107 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify warnings.warn callsites and whether stacklevel is set in this file.
rg -n "warnings\.warn\(" python/cuml/cuml/internals/validation.py -A5 -B1

Repository: rapidsai/cuml

Length of output: 1277


Set warning stacklevel so deprecation points to caller

Line 107 emits warnings.warn(...) without stacklevel, so users get an internal location instead of their callsite (also matches Ruff B028).

💡 Proposed fix
         else:
             warnings.warn(
                 "Support for passing non-2-dimensional X was deprecated in 26.04 "
                 "and will be removed in 26.06. In cuml version 26.06 this will error "
                 f"with the following message:\n\n{msg}",
                 FutureWarning,
+                stacklevel=2,
             )
🧰 Tools
🪛 Ruff (0.15.5)

[warning] 107-107: No explicit stacklevel keyword argument found

Set stacklevel=2

(B028)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/cuml/internals/validation.py` around lines 107 - 112, The
FutureWarning emitted in validation.py (the warnings.warn(...) call that warns
about non-2-dimensional X deprecation) should include a stacklevel so the
warning points at the user's callsite; update that warnings.warn invocation to
pass stacklevel=2 (or an appropriate value to reach the external caller) while
keeping the message and category unchanged.

# Fallback to 1 feature until the deprecation is completed
return 1
return shape[1]
Comment on lines +84 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

3D Python sequences still bypass this new ndim check.

This branch only runs after shape inference, but _get_n_features() still returns early for nested list/tuple inputs above. A value like [[[1], [2]]] currently reports 2 features instead of raising for non-2D input, so the stricter validation is still incomplete for one of the supported raw input forms.

As per coding guidelines, python/**/**/cuml/**/*.py: Missing input dimension checks (n_samples, n_features) and not handling edge cases (empty datasets, single sample) must be addressed in Python estimators.

🧰 Tools
🪛 Ruff (0.15.5)

[warning] 107-107: No explicit stacklevel keyword argument found

Set stacklevel=2

(B028)



def _warn_or_error(exc_cls, msg):
Expand Down
4 changes: 2 additions & 2 deletions python/cuml/cuml/preprocessing/TargetEncoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ class TargetEncoder(Base, InteropMixin):
>>> test = DataFrame({'category': ['a', 'c', 'b', 'a']})

>>> encoder = TargetEncoder(output_type='numpy')
>>> train_encoded = encoder.fit_transform(train.category, train.label)
>>> test_encoded = encoder.transform(test.category)
>>> train_encoded = encoder.fit_transform(train[["category"]], train.label)
>>> test_encoded = encoder.transform(test[["category"]])
Comment on lines +177 to +178

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Finish the doc cleanup for 2D X inputs.

The example now correctly uses a 1-column DataFrame, but the fit, fit_transform, and transform parameter docs below still advertise Series/1D X inputs. That now contradicts the new deprecation path and will steer users into warnings.

As per coding guidelines, python/**/**/cuml/**/*.py: Missing docstrings for public methods, undocumented hyperparameters, or missing scikit-learn compatibility notes in documentation must be addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/cuml/preprocessing/TargetEncoder.py` around lines 177 - 178,
Update the TargetEncoder class docstrings for the public methods fit,
fit_transform, and transform to reflect that X must be 2D (e.g., a single-column
DataFrame or 2D array) rather than a 1D Series; locate the parameter docs for X
in TargetEncoder.fit, TargetEncoder.fit_transform, and TargetEncoder.transform
and change the type/description to mention DataFrame/2D array inputs (and mark
Series/1D X as deprecated if the project uses a deprecation policy), add an
explicit scikit-learn compatibility note stating 2D inputs are required, and
ensure the example in the class-level doc and these method docstrings matches
the 1-column DataFrame usage shown in the example block.

>>> print(train_encoded)
[1. 1. 0. 1.]
>>> print(test_encoded)
Expand Down
2 changes: 1 addition & 1 deletion python/cuml/cuml/preprocessing/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def __init__(
self.sparse_output = sparse_output
self.classes_ = None

@cuml.internals.reflect(reset=True)
@cuml.internals.reflect(reset="type")
def fit(self, y) -> "LabelBinarizer":
"""
Fit label binarizer
Expand Down

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions python/cuml/tests/test_coordinate_descent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
#

Expand Down Expand Up @@ -274,7 +274,7 @@ def test_lasso_predict_convert_dtype(train_dtype, test_dtype):

@pytest.mark.parametrize("cls", [cuml.ElasticNet, cuml.Lasso])
def test_set_params(cls):
x = np.linspace(0, 1, 50)
x = np.linspace(0, 1, 50)[:, None]
y = 2 * x

model = cls(alpha=0.01)
Expand Down
4 changes: 2 additions & 2 deletions python/cuml/tests/test_dbscan.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
#

Expand Down Expand Up @@ -497,7 +497,7 @@ def test_dbscan_no_calc_core_point_indices():


def test_dbscan_on_empty_array():
X = np.array([])
X = np.array([[]])
cuml_dbscan = cuDBSCAN()

with pytest.raises(ValueError):
Expand Down
9 changes: 8 additions & 1 deletion python/cuml/tests/test_label_binarizer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 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
Expand All @@ -11,6 +11,13 @@
from cuml.testing.utils import array_equal


def test_label_binarizer_no_features():
"""Ensure the features infra is never applied to LabelBinarizer"""
y = cp.asarray([1, 2, 1, 2, 1, 0])
model = LabelBinarizer().fit(y)
assert not hasattr(model, "n_features_in_")


@pytest.mark.parametrize(
"labels",
[
Expand Down
7 changes: 7 additions & 0 deletions python/cuml/tests/test_label_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ def _df_to_similarity_mat(df):
return np.pad(arr, [(arr.shape[1] - 1, 0), (0, 0)], "edge")


def test_label_encoder_no_features():
"""Ensure the features infra is never applied to LabelEncoder"""
y = cp.asarray([1, 2, 1, 2, 1, 0])
model = LabelEncoder().fit(y)
assert not hasattr(model, "n_features_in_")


@pytest.mark.parametrize("length", [10, 1000])
@pytest.mark.parametrize("cardinality", [5, 10, 50])
def test_labelencoder_fit_transform(length, cardinality):
Expand Down
Loading
Loading