diff --git a/python/cuml/cuml/cluster/agglomerative.pyx b/python/cuml/cuml/cluster/agglomerative.pyx index 349f1be897..01ca3119ab 100644 --- a/python/cuml/cuml/cluster/agglomerative.pyx +++ b/python/cuml/cuml/cluster/agglomerative.pyx @@ -4,11 +4,11 @@ # import numpy as np -from cuml.common import input_to_cuml_array from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals.array import CumlArray from cuml.internals.base import Base, get_handle +from cuml.internals.input_utils import validate_data from cuml.internals.mixins import ClusterMixin, CMajorInputTagMixin from cuml.internals.outputs import reflect @@ -144,9 +144,8 @@ class AgglomerativeClustering(Base, ClusterMixin, CMajorInputTagMixin): Fit the hierarchical clustering from features. """ # Validate and process inputs - X = input_to_cuml_array( - X, - order="C", + X = validate_data( + self, X, order="C", check_dtype=np.float32, convert_to_dtype=(np.float32 if convert_dtype else None), ).array diff --git a/python/cuml/cuml/cluster/dbscan.pyx b/python/cuml/cuml/cluster/dbscan.pyx index 4387a87b7f..ff627832e8 100644 --- a/python/cuml/cuml/cluster/dbscan.pyx +++ b/python/cuml/cuml/cluster/dbscan.pyx @@ -10,7 +10,7 @@ from cuml.common.doc_utils import generate_docstring from cuml.internals import logger, reflect from cuml.internals.array import CumlArray from cuml.internals.base import Base, get_handle -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import input_to_cuml_array, validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -330,11 +330,10 @@ class DBSCAN(Base, ) cdef int64_t n_rows, n_cols - X, n_rows, n_cols, _ = input_to_cuml_array( - X, - order='C', + X, n_rows, n_cols, _ = validate_data( + self, X, order='C', convert_to_dtype=(np.float32 if convert_dtype else None), - check_dtype=[np.float32, np.float64] + check_dtype=[np.float32, np.float64], ) if n_rows * n_cols > (2**31 - 1): diff --git a/python/cuml/cuml/cluster/hdbscan/hdbscan.pyx b/python/cuml/cuml/cluster/hdbscan/hdbscan.pyx index f06ae64f27..ac598b139d 100644 --- a/python/cuml/cuml/cluster/hdbscan/hdbscan.pyx +++ b/python/cuml/cuml/cluster/hdbscan/hdbscan.pyx @@ -10,6 +10,7 @@ from cuml.common.doc_utils import generate_docstring from cuml.internals import logger, reflect from cuml.internals.array import CumlArray from cuml.internals.base import Base, get_handle +from cuml.internals.input_utils import validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -933,12 +934,11 @@ class HDBSCAN(Base, InteropMixin, ClusterMixin, CMajorInputTagMixin): else: convert_to_mem_type = MemoryType.device - self._raw_data = input_to_cuml_array( - X, - order='C', + self._raw_data = validate_data( + self, X, order='C', check_dtype=[np.float32], convert_to_dtype=np.float32 if convert_dtype else None, - convert_to_mem_type=convert_to_mem_type + convert_to_mem_type=convert_to_mem_type, )[0] self._raw_data_cpu = None diff --git a/python/cuml/cuml/cluster/kmeans.pyx b/python/cuml/cuml/cluster/kmeans.pyx index 33e34c0258..a32e3452ed 100644 --- a/python/cuml/cuml/cluster/kmeans.pyx +++ b/python/cuml/cuml/cluster/kmeans.pyx @@ -6,11 +6,13 @@ import typing import numpy as np +from cuml._thirdparty.sklearn.utils.validation import check_is_fitted from cuml.common import input_to_cuml_array from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals.array import CumlArray from cuml.internals.base import Base, get_handle +from cuml.internals.input_utils import validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -517,9 +519,8 @@ class KMeans(Base, """ # Process input arrays - X_m, n_rows, n_cols, dtype = input_to_cuml_array( - X, - order="C", + X_m, n_rows, n_cols, dtype = validate_data( + self, X, order="C", convert_to_dtype=(np.float32 if convert_dtype else None), check_dtype=[np.float32, np.float64], ) @@ -635,12 +636,10 @@ class KMeans(Base, """ dtype = self.cluster_centers_.dtype - X_m, n_rows, _, _ = input_to_cuml_array( - X, - order="C", + X_m, n_rows, _, _ = validate_data( + self, X, reset=False, order="C", check_dtype=dtype, convert_to_dtype=(dtype if convert_dtype else None), - check_cols=self.n_features_in_, ) if sample_weight is None: @@ -681,6 +680,7 @@ class KMeans(Base, Predict the closest cluster each sample in X belongs to. """ + check_is_fitted(self) labels, _ = self._predict_labels_inertia(X, convert_dtype=convert_dtype) return labels @@ -694,14 +694,13 @@ class KMeans(Base, Transform X to a cluster-distance space. """ + check_is_fitted(self) dtype = self.cluster_centers_.dtype - X_m = input_to_cuml_array( - X, - order="C", + X_m = validate_data( + self, X, reset=False, order="C", check_dtype=dtype, convert_to_dtype=(dtype if convert_dtype else None), - check_cols=self.cluster_centers_.shape[1], ).array cdef int64_t n_rows = X_m.shape[0] diff --git a/python/cuml/cuml/common/exceptions.py b/python/cuml/cuml/common/exceptions.py index e02c6d3f37..2284ce6184 100644 --- a/python/cuml/cuml/common/exceptions.py +++ b/python/cuml/cuml/common/exceptions.py @@ -1,12 +1,14 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2020, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # +from sklearn.exceptions import NotFittedError as _SklearnNotFittedError -class NotFittedError(ValueError, AttributeError): + +class NotFittedError(_SklearnNotFittedError): """Exception class to raise if estimator is used before fitting. - This class inherits from both ValueError and AttributeError to help with - exception handling and backward compatibility. + Inherits from sklearn's NotFittedError so that sklearn's estimator + checks and except clauses catch it correctly. """ diff --git a/python/cuml/cuml/covariance/ledoit_wolf.py b/python/cuml/cuml/covariance/ledoit_wolf.py index 9834e77069..3c521a816c 100644 --- a/python/cuml/cuml/covariance/ledoit_wolf.py +++ b/python/cuml/cuml/covariance/ledoit_wolf.py @@ -12,7 +12,7 @@ from cuml.internals import reflect, run_in_internal_context from cuml.internals.array import CumlArray from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cupy_array +from cuml.internals.input_utils import input_to_cupy_array, validate_data from cuml.internals.interop import InteropMixin, to_cpu, to_gpu @@ -246,8 +246,10 @@ def fit(self, X, y=None, *, convert_dtype=True) -> "LedoitWolf": self : LedoitWolf Returns the instance itself. """ - X_arr, n_samples, n_features, dtype = input_to_cupy_array( + X_arr, n_samples, n_features, dtype = validate_data( + self, X, + array_output_type="cupy", check_dtype=[np.float32, np.float64], order="C", convert_to_dtype=(np.float32 if convert_dtype else None), @@ -324,10 +326,12 @@ def score(self, X_test, y=None) -> float: log_likelihood : float Log-likelihood of the data under the fitted Gaussian model. """ - X_arr, _, n_features, _ = input_to_cupy_array( + X_arr, _, n_features, _ = validate_data( + self, X_test, + reset=False, + array_output_type="cupy", check_dtype=[np.float32, np.float64], - check_cols=self.n_features_in_, order="C", ) @@ -408,10 +412,12 @@ def mahalanobis(self, X): mahalanobis_distances : ndarray of shape (n_samples,) Squared Mahalanobis distances of the observations. """ - X_arr, _, _, _ = input_to_cupy_array( + X_arr, _, _, _ = validate_data( + self, X, + reset=False, + array_output_type="cupy", check_dtype=[np.float32, np.float64], - check_cols=self.n_features_in_, order="C", ) precision = cp.asarray(self.get_precision()) diff --git a/python/cuml/cuml/decomposition/incremental_pca.py b/python/cuml/cuml/decomposition/incremental_pca.py index b262872cd7..6bc29a4183 100644 --- a/python/cuml/cuml/decomposition/incremental_pca.py +++ b/python/cuml/cuml/decomposition/incremental_pca.py @@ -14,7 +14,7 @@ from cuml.decomposition.pca import PCA from cuml.internals.array import CumlArray from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cupy_array +from cuml.internals.input_utils import input_to_cupy_array, validate_data class IncrementalPCA(PCA): @@ -224,8 +224,10 @@ def fit(self, X, y=None, *, convert_dtype=True) -> "IncrementalPCA": # is done by PCA, which IncrementalPCA inherits from. PCA's # transform and inverse transform convert the output to the # required type. - X, n_samples, n_features, _ = input_to_cupy_array( + X, n_samples, n_features, _ = validate_data( + self, X, + array_output_type="cupy", order="K", convert_to_dtype=(cp.float32 if convert_dtype else None), check_dtype=[cp.float32, cp.float64], diff --git a/python/cuml/cuml/decomposition/pca.pyx b/python/cuml/cuml/decomposition/pca.pyx index ca46030c12..14035ef755 100644 --- a/python/cuml/cuml/decomposition/pca.pyx +++ b/python/cuml/cuml/decomposition/pca.pyx @@ -14,7 +14,7 @@ from cuml.common.exceptions import NotFittedError from cuml.common.sparse_utils import is_sparse from cuml.internals.array import CumlArray from cuml.internals.base import Base, get_handle -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import input_to_cuml_array, validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -486,11 +486,13 @@ class PCA(Base, X = cupyx.scipy.sparse.coo_matrix(X) n_rows, n_cols = X.shape else: - X, n_rows, n_cols, _ = input_to_cuml_array( - X, - convert_to_dtype=(np.float32 if convert_dtype else None), + X_out = validate_data( + self, X, + convert_to_dtype=(np.float32 if convert_dtype else False), check_dtype=[np.float32, np.float64], ) + X = X_out.array + n_rows, n_cols = X_out.n_rows, X_out.n_cols self.n_samples_ = n_rows @@ -637,12 +639,12 @@ class PCA(Base, def _transform_dense(self, X, convert_dtype=True): dtype = self.components_.dtype - X_m, n_rows, n_cols, _ = input_to_cuml_array( - X, + X_out = validate_data( + self, X, reset=False, check_dtype=dtype, - convert_to_dtype=(dtype if convert_dtype else None), - check_cols=self.n_features_in_, + convert_to_dtype=(dtype if convert_dtype else False), ) + X_m, n_rows, n_cols = X_out.array, X_out.n_rows, X_out.n_cols out = CumlArray.zeros( (n_rows, self.n_components_), dtype=dtype, index=X_m.index diff --git a/python/cuml/cuml/decomposition/tsvd.pyx b/python/cuml/cuml/decomposition/tsvd.pyx index 6c3d5b6c4a..4573388484 100644 --- a/python/cuml/cuml/decomposition/tsvd.pyx +++ b/python/cuml/cuml/decomposition/tsvd.pyx @@ -4,6 +4,7 @@ # import numpy as np +from sklearn.utils.validation import check_is_fitted import cuml.internals from cuml.common import input_to_cuml_array @@ -11,6 +12,7 @@ from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals.array import CumlArray from cuml.internals.base import Base, get_handle +from cuml.internals.input_utils import validate_data from cuml.internals.interop import InteropMixin, to_cpu, to_gpu from cuml.internals.mixins import FMajorInputTagMixin @@ -306,11 +308,12 @@ class TruncatedSVD(Base, """ # Validate input - X_m, n_rows, n_cols, dtype = input_to_cuml_array( - X, - convert_to_dtype=(np.float32 if convert_dtype else None), - check_dtype=[np.float32, np.float64] + X_out = validate_data( + self, X, + convert_to_dtype=(np.float32 if convert_dtype else False), + check_dtype=[np.float32, np.float64], ) + X_m, n_rows, n_cols, dtype = X_out # Validate and initialize parameters if self.n_components > n_cols: @@ -455,13 +458,14 @@ class TruncatedSVD(Base, Perform dimensionality reduction on X. """ + check_is_fitted(self) dtype = self.components_.dtype - X_m, n_rows, _, _ = input_to_cuml_array( - X, + X_out = validate_data( + self, X, reset=False, check_dtype=dtype, - convert_to_dtype=(dtype if convert_dtype else None), - check_cols=self.n_features_in_, + convert_to_dtype=(dtype if convert_dtype else False), ) + X_m, n_rows = X_out.array, X_out.n_rows cdef paramsTSVD params params.n_components = self.n_components diff --git a/python/cuml/cuml/ensemble/randomforestclassifier.py b/python/cuml/cuml/ensemble/randomforestclassifier.py index 6733f15461..1572318a8e 100644 --- a/python/cuml/cuml/ensemble/randomforestclassifier.py +++ b/python/cuml/cuml/ensemble/randomforestclassifier.py @@ -3,6 +3,7 @@ import cupy as cp import numpy as np +from sklearn.utils.validation import check_is_fitted import cuml.internals import cuml.internals.nvtx as nvtx @@ -11,7 +12,7 @@ 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.input_utils import validate_data from cuml.internals.interop import UnsupportedOnGPU from cuml.internals.mixins import ClassifierMixin from cuml.metrics import accuracy_score @@ -221,11 +222,17 @@ def fit(self, X, y, *, convert_dtype=True) -> "RandomForestClassifier": y to be of dtype int32. This will increase memory used for the method. """ - X_m = input_to_cuml_array( + # y is only forwarded when None so that validate_data's tag-driven + # check raises ValueError for missing targets. Non-None y is skipped + # here because classifiers accept string labels that + # input_to_cuml_array cannot convert; preprocess_labels handles y + # conversion below. + X_m = validate_data( + self, X, - convert_to_dtype=(np.float32 if convert_dtype else None), + y=y if y is None else "no_validation", + convert_to_dtype=(np.float32 if convert_dtype else False), check_dtype=[np.float32, np.float64], - order="F", ).array y, classes = preprocess_labels( y, n_samples=X_m.shape[0], dtype=cp.int32 @@ -280,6 +287,16 @@ def predict( ------- y : {} """ + check_is_fitted(self) + + if X.ndim == 1: + raise ValueError( + "Expected 2D array, got 1D array instead.\n" + "Reshape your data either using array.reshape(-1, 1) if " + "your data has a single feature or array.reshape(1, -1) " + "if it contains a single sample." + ) + fil = self._get_inference_fil_model( layout=layout, default_chunk_size=default_chunk_size, diff --git a/python/cuml/cuml/ensemble/randomforestregressor.py b/python/cuml/cuml/ensemble/randomforestregressor.py index 367017c2ce..f7de708b78 100644 --- a/python/cuml/cuml/ensemble/randomforestregressor.py +++ b/python/cuml/cuml/ensemble/randomforestregressor.py @@ -1,13 +1,15 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 +import cupy as cp import numpy as np +from sklearn.utils.validation import check_is_fitted import cuml.internals.nvtx as nvtx -from cuml.common import input_to_cuml_array from cuml.common.array_descriptor import CumlArrayDescriptor 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 validate_data from cuml.internals.mixins import RegressorMixin from cuml.internals.outputs import reflect, run_in_internal_context from cuml.metrics import r2_score @@ -183,20 +185,18 @@ def fit(self, X, y, *, convert_dtype=True) -> "RandomForestRegressor": Perform Random Forest Regression on the input data """ - X_m = input_to_cuml_array( + X_out, y_out = validate_data( + self, X, - convert_to_dtype=(np.float32 if convert_dtype else None), + y, + convert_to_dtype=(np.float32 if convert_dtype else False), check_dtype=[np.float32, np.float64], - order="F", - ).array + ) + X_m = X_out.array + y_m = y_out.array - y_m = input_to_cuml_array( - y, - convert_to_dtype=(X_m.dtype if convert_dtype else None), - check_dtype=X_m.dtype, - check_rows=X_m.shape[0], - check_cols=1, - ).array + if cp.any(cp.isnan(y_m.to_output("cupy"))): + raise ValueError("Input y contains NaN.") return self._fit_forest(X_m, y_m) @@ -245,6 +245,16 @@ def predict( ------- y : {} """ + check_is_fitted(self) + + if X.ndim == 1: + raise ValueError( + "Expected 2D array, got 1D array instead.\n" + "Reshape your data either using array.reshape(-1, 1) if " + "your data has a single feature or array.reshape(1, -1) " + "if it contains a single sample." + ) + fil = self._get_inference_fil_model( layout=layout, default_chunk_size=default_chunk_size, diff --git a/python/cuml/cuml/internals/__init__.py b/python/cuml/cuml/internals/__init__.py index 692250835f..4c2696afb3 100644 --- a/python/cuml/cuml/internals/__init__.py +++ b/python/cuml/cuml/internals/__init__.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from cuml.internals.base import Base, get_handle +from cuml.internals.input_utils import validate_data from cuml.internals.internals import GraphBasedDimRedCallback from cuml.internals.outputs import ( exit_internal_context, diff --git a/python/cuml/cuml/internals/input_utils.py b/python/cuml/cuml/internals/input_utils.py index 1c40c5618d..df22a3f3f1 100644 --- a/python/cuml/cuml/internals/input_utils.py +++ b/python/cuml/cuml/internals/input_utils.py @@ -1,5 +1,5 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # @@ -12,6 +12,7 @@ import numpy as np import pandas as pd import scipy.sparse +from sklearn.utils._tags import get_tags import cuml.internals.nvtx as nvtx from cuml.internals.array import CumlArray @@ -275,6 +276,7 @@ def input_to_cuml_array( check_rows=False, fail_on_order=False, force_contiguous=True, + ensure_2d=False, ): """ Convert input X to CumlArray. @@ -345,6 +347,15 @@ def input_to_cuml_array( A new CumlArray and associated data. """ + if ensure_2d: + if X.ndim == 1: + raise ValueError( + "Expected 2D array, got 1D array instead.\n" + "Reshape your data either using array.reshape(-1, 1) if " + "your data has a single feature or array.reshape(1, -1) " + "if it contains a single sample." + ) + arr = CumlArray.from_input( X, order=order, @@ -374,6 +385,175 @@ def input_to_cuml_array( return cuml_array(array=arr, n_rows=n_rows, n_cols=n_cols, dtype=arr.dtype) +def _coerce_output(result, output_type): + """Convert the .array field of a cuml_array namedtuple.""" + if output_type == "cuml": + return result + return result._replace(array=result.array.to_output(output_type)) + + +def validate_data( + _estimator, + /, + X="no_validation", + y="no_validation", + *, + reset=True, + ensure_2d=True, + accept_sparse=False, + array_output_type="cuml", + validate_separately=False, + order="F", + check_dtype=False, + convert_to_dtype=False, + convert_to_mem_type="device", + check_cols=False, + check_rows=False, + force_contiguous=True, +): + """Validate input data and manage ``n_features_in_``. + + Wraps :func:`input_to_cuml_array` with sklearn-compatible validation: + + * Rejects ``y=None`` for supervised estimators (tag-driven). + * Enforces 2-D ``X`` by default. + * Sets ``n_features_in_`` on fit (``reset=True``) and validates it on + predict / transform (``reset=False``). + + Parameters + ---------- + _estimator : cuml.internals.base.Base + The estimator instance. Positional-only. + X : array-like or ``"no_validation"`` + Feature matrix. ``"no_validation"`` skips X validation. + y : array-like, None, or ``"no_validation"`` + Target array. ``"no_validation"`` or ``None`` skips y validation. + ``None`` is rejected when the estimator's tags indicate that y is + required (e.g. classifiers and regressors). + reset : bool, default=True + If True, set ``n_features_in_`` from X. If False, validate that X + has the expected number of features. + ensure_2d : bool, default=True + Require X to be 2-D. + accept_sparse : bool, default=False + If True, sparse X is passed through without conversion (the + estimator is responsible for handling it). The ``ensure_2d`` and + ``n_features_in_`` checks are still applied. If False (default), + sparse X is rejected by :func:`input_to_cuml_array` with a + ``TypeError``. + array_output_type : str, default="cuml" + The array type for the ``.array`` field of returned namedtuples. + ``"cuml"`` returns :class:`CumlArray` (default), ``"cupy"`` returns + cupy ndarrays, ``"numpy"`` returns numpy ndarrays. + validate_separately : False or tuple of two dicts, default=False + When a ``(X_kwargs, y_kwargs)`` tuple is given, X and y are each + validated with their own ``input_to_cuml_array`` keyword arguments. + This is the mechanism for passing different dtype / order / shape + requirements for X and y. + order, check_dtype, convert_to_dtype, check_cols, check_rows, + force_contiguous : + Forwarded to :func:`input_to_cuml_array` for X (and for y when + ``validate_separately`` is False). + + Returns + ------- + out : cuml_array or (cuml_array, cuml_array) + Validated X as a ``cuml_array`` namedtuple, or ``(X_out, y_out)`` + when y is provided. + """ + tags = get_tags(_estimator) + + if y is None and tags.target_tags.required: + raise ValueError( + f"This {_estimator.__class__.__name__} estimator " + "requires y to be passed, but the target y is None." + ) + + from cuml.common.sparse_utils import is_sparse as _is_sparse + + no_val_X = isinstance(X, str) and X == "no_validation" + no_val_y = y is None or (isinstance(y, str) and y == "no_validation") + X_is_sparse = not no_val_X and accept_sparse and _is_sparse(X) + + if validate_separately: + if no_val_X or no_val_y: + raise ValueError( + "validate_separately requires both X and y to be provided." + ) + X_kwargs, y_kwargs = validate_separately + X_kwargs.setdefault("ensure_2d", ensure_2d) + X_out = input_to_cuml_array(X, **X_kwargs) + y_kwargs.setdefault("ensure_2d", False) + y_kwargs.setdefault("check_rows", X_out.n_rows) + y_out = input_to_cuml_array(y, **y_kwargs) + else: + if not no_val_X: + if X_is_sparse: + # Sparse pass-through: skip input_to_cuml_array conversion + # but still enforce ensure_2d. + if ensure_2d: + if X.ndim == 1: + raise ValueError( + "Expected 2D array, got 1D array instead.\n" + "Reshape your data either using " + "array.reshape(-1, 1) if your data has a " + "single feature or array.reshape(1, -1) if " + "it contains a single sample." + ) + n_rows, n_cols = X.shape + X_out = cuml_array( + array=X, + n_rows=n_rows, + n_cols=n_cols, + dtype=X.dtype, + ) + else: + X_out = input_to_cuml_array( + X, + order=order, + check_dtype=check_dtype, + convert_to_dtype=convert_to_dtype, + convert_to_mem_type=convert_to_mem_type, + check_cols=check_cols, + check_rows=check_rows, + force_contiguous=force_contiguous, + ensure_2d=ensure_2d, + ) + + if not no_val_y: + y_kwargs = dict( + ensure_2d=False, + force_contiguous=force_contiguous, + ) + if not no_val_X: + y_kwargs["check_rows"] = X_out.n_rows + # When dtype conversion was requested for X, also convert y + # to X's resulting dtype. This ensures regressors get + # matching float dtypes for X and y. + if convert_to_dtype and not X_is_sparse: + y_kwargs["convert_to_dtype"] = X_out.dtype + y_out = input_to_cuml_array(y, **y_kwargs) + + # n_features_in_ management + if not no_val_X and ensure_2d: + if reset: + _estimator.n_features_in_ = X_out.n_cols + elif hasattr(_estimator, "n_features_in_"): + if X_out.n_cols != _estimator.n_features_in_: + raise ValueError( + f"X has {X_out.n_cols} features, but " + f"{_estimator.__class__.__name__} is expecting " + f"{_estimator.n_features_in_} features as input." + ) + + if no_val_y: + return _coerce_output(X_out, array_output_type) + return ( + _coerce_output(X_out, array_output_type), + _coerce_output(y_out, array_output_type), + ) + + @nvtx.annotate( message="common.input_utils.input_to_cupy_array", category="utils", @@ -390,6 +570,7 @@ def input_to_cupy_array( fail_on_order=False, force_contiguous=True, fail_on_null=True, + ensure_2d=False, ) -> cuml_array: """ Identical to input_to_cuml_array but it returns a cupy array instead of @@ -415,6 +596,7 @@ def input_to_cupy_array( fail_on_order=fail_on_order, force_contiguous=force_contiguous, convert_to_mem_type=MemoryType.device, + ensure_2d=ensure_2d, ) return out_data._replace(array=out_data.array.to_output("cupy")) diff --git a/python/cuml/cuml/kernel_ridge/kernel_ridge.py b/python/cuml/cuml/kernel_ridge/kernel_ridge.py index 336a7d5cf8..fac1716e8a 100644 --- a/python/cuml/cuml/kernel_ridge/kernel_ridge.py +++ b/python/cuml/cuml/kernel_ridge/kernel_ridge.py @@ -8,13 +8,14 @@ import numpy as np from cupy import linalg from cupyx import geterr, lapack, seterr +from sklearn.utils.validation import check_is_fitted -from cuml.common import input_to_cuml_array from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals import reflect from cuml.internals.array import CumlArray from cuml.internals.base import Base +from cuml.internals.input_utils import validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -283,23 +284,23 @@ def _get_kernel(self, X, Y=None): def fit( self, X, y, sample_weight=None, *, convert_dtype=True ) -> "KernelRidge": - ravel = False - if len(y.shape) == 1: - y = y.reshape(-1, 1) - ravel = True - - X_m = input_to_cuml_array( + X_out, y_out = validate_data( + self, X, - convert_to_dtype=(np.float32 if convert_dtype else None), + y, + convert_to_dtype=(np.float32 if convert_dtype else False), check_dtype=[np.float32, np.float64], - ).array + ) + X_m = X_out.array + y_m = y_out.array - y_m = input_to_cuml_array( - y, - check_dtype=X_m.dtype, - convert_to_dtype=(X_m.dtype if convert_dtype else None), - check_rows=X_m.shape[0], - ).array + ravel = False + if len(y_m.shape) == 1: + y_m = CumlArray(data=y_m.to_output("cupy").reshape(-1, 1)) + ravel = True + + if cp.any(cp.isnan(y_m.to_output("cupy"))): + raise ValueError("Input y contains NaN.") if X.shape[1] < 1: raise ValueError("X matrix must have at least a column") @@ -333,13 +334,15 @@ def predict(self, X, *, convert_dtype=True): C : array of shape (n_samples,) or (n_samples, n_targets) Returns predicted values. """ + check_is_fitted(self) dtype = self.X_fit_.dtype - X_m = input_to_cuml_array( + X_m = validate_data( + self, X, + reset=False, check_dtype=dtype, - convert_to_dtype=(dtype if convert_dtype else None), - check_cols=self.n_features_in_, + convert_to_dtype=(dtype if convert_dtype else False), ).array K = cp.asarray(self._get_kernel(X_m, self.X_fit_), dtype=dtype) diff --git a/python/cuml/cuml/linear_model/base.py b/python/cuml/cuml/linear_model/base.py index fd500cc530..6c1611fec6 100644 --- a/python/cuml/cuml/linear_model/base.py +++ b/python/cuml/cuml/linear_model/base.py @@ -1,13 +1,15 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # +from sklearn.utils.validation import check_is_fitted + import cuml.internals from cuml.common.doc_utils import generate_docstring from cuml.common.sparse_utils import is_sparse from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import validate_data class LinearPredictMixin: @@ -24,17 +26,14 @@ def predict(self, X, *, convert_dtype=True) -> CumlArray: """ Predicts `y` values for `X`. """ - if getattr(self, "coef_", None) is None: - raise ValueError( - "LinearModel.predict() cannot be called before fit(). " - "Please fit the model first." - ) + check_is_fitted(self, "coef_") - X = input_to_cuml_array( + X = validate_data( + self, X, + reset=False, check_dtype=self.coef_.dtype, convert_to_dtype=(self.coef_.dtype if convert_dtype else None), - check_cols=self.n_features_in_, order="K", ).array X_cp = X.to_output("cupy") @@ -64,17 +63,20 @@ class LinearClassifierMixin: @cuml.internals.reflect def decision_function(self, X, *, convert_dtype=True) -> CumlArray: """Predict confidence scores for samples.""" + check_is_fitted(self, "coef_") + if is_sparse(X): X = SparseCumlArray( X, convert_to_dtype=self.coef_.dtype ).to_output("cupy") out_index = None else: - X_m = input_to_cuml_array( + X_m = validate_data( + self, X, + reset=False, check_dtype=self.coef_.dtype, convert_to_dtype=(self.coef_.dtype if convert_dtype else None), - check_cols=self.n_features_in_, order="K", ).array out_index = X_m.index diff --git a/python/cuml/cuml/linear_model/elastic_net.py b/python/cuml/cuml/linear_model/elastic_net.py index 9618ba3a5c..5d224e395f 100644 --- a/python/cuml/cuml/linear_model/elastic_net.py +++ b/python/cuml/cuml/linear_model/elastic_net.py @@ -6,6 +6,7 @@ from cuml.common.doc_utils import generate_docstring from cuml.internals.array import CumlArray from cuml.internals.base import Base +from cuml.internals.input_utils import validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -225,6 +226,11 @@ def fit( f"Expected 0.0 <= l1_ratio <= 1.0, got {self.l1_ratio}" ) + # y is only forwarded when None so that validate_data's tag-driven + # check raises ValueError for missing targets. The solver functions + # below handle the actual X and y conversion. + validate_data(self, X, y=y if y is None else "no_validation") + if self.solver == "qn": coef, intercept, n_iter, _ = fit_qn( X, diff --git a/python/cuml/cuml/linear_model/linear_regression.pyx b/python/cuml/cuml/linear_model/linear_regression.pyx index 3c0e942b64..c4c1f88b84 100644 --- a/python/cuml/cuml/linear_model/linear_regression.pyx +++ b/python/cuml/cuml/linear_model/linear_regression.pyx @@ -13,6 +13,7 @@ from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals.array import CumlArray, cuda_ptr from cuml.internals.base import Base, get_handle +from cuml.internals.input_utils import validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -296,12 +297,14 @@ class LinearRegression(Base, Fit the model with X and y. """ - X_m = input_to_cuml_array( - X, + X_out, y_out = validate_data( + self, X, y, + order="F", convert_to_dtype=(np.float32 if convert_dtype else None), check_dtype=[np.float32, np.float64], - order="F", - ).array + ) + X_m = X_out.array + y_m = y_out.array if X_m.shape[0] < 2: raise ValueError("X matrix must have at least two rows") @@ -309,13 +312,8 @@ class LinearRegression(Base, if X_m.shape[1] < 1: raise ValueError("X matrix must have at least one column") - y_m = input_to_cuml_array( - y, - check_dtype=X_m.dtype, - convert_to_dtype=(X_m.dtype if convert_dtype else None), - check_rows=X_m.shape[0], - order="F", - ).array + if cp.any(cp.isnan(y_m.to_output("cupy"))): + raise ValueError("Input y contains NaN.") if sample_weight is not None: # Always copy the weights, all solvers mutate them diff --git a/python/cuml/cuml/linear_model/logistic_regression.py b/python/cuml/cuml/linear_model/logistic_regression.py index 7500418c81..ce9f33ee23 100644 --- a/python/cuml/cuml/linear_model/logistic_regression.py +++ b/python/cuml/cuml/linear_model/logistic_regression.py @@ -17,6 +17,7 @@ from cuml.common.doc_utils import generate_docstring from cuml.internals.array import CumlArray from cuml.internals.base import Base +from cuml.internals.input_utils import validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -306,6 +307,18 @@ def fit( """ Fit the model with X and y. """ + # y is only forwarded when None so that validate_data's tag-driven + # check raises ValueError for missing targets. Non-None y is skipped + # because classifiers accept string labels that input_to_cuml_array + # cannot convert; preprocess_labels handles y conversion below. + # accept_sparse=True because the solver handles sparse X internally. + validate_data( + self, + X, + y=y if y is None else "no_validation", + accept_sparse=True, + ) + y, classes = preprocess_labels(y) _, sample_weight = process_class_weight( classes, diff --git a/python/cuml/cuml/linear_model/ridge.pyx b/python/cuml/cuml/linear_model/ridge.pyx index 3f6aefcfa0..23b3ec31a1 100644 --- a/python/cuml/cuml/linear_model/ridge.pyx +++ b/python/cuml/cuml/linear_model/ridge.pyx @@ -9,7 +9,7 @@ from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals.array import CumlArray, cuda_ptr from cuml.internals.base import Base, get_handle -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import input_to_cuml_array, validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -404,12 +404,14 @@ class Ridge(Base, """ Fit the model with X and y. """ - X_m, n_rows, n_cols, dtype = input_to_cuml_array( - X, + X_out, y_out = validate_data( + self, X, y, + order="K", convert_to_dtype=(np.float32 if convert_dtype else None), check_dtype=[np.float32, np.float64], - order="K", ) + X_m, n_rows, n_cols, dtype = X_out + y_m, _, n_targets, _ = y_out if n_cols < 1: raise ValueError( @@ -423,13 +425,8 @@ class Ridge(Base, f"minimum of 2 is required." ) - y_m, _, n_targets, _ = input_to_cuml_array( - y, - check_dtype=dtype, - convert_to_dtype=(dtype if convert_dtype else None), - check_rows=n_rows, - order="K", - ) + if cp.any(cp.isnan(y_m.to_output("cupy"))): + raise ValueError("Input y contains NaN.") if sample_weight is not None: sample_weight_m = input_to_cuml_array( diff --git a/python/cuml/cuml/manifold/t_sne.pyx b/python/cuml/cuml/manifold/t_sne.pyx index bf6a41f78f..474345e237 100644 --- a/python/cuml/cuml/manifold/t_sne.pyx +++ b/python/cuml/cuml/manifold/t_sne.pyx @@ -15,6 +15,7 @@ from cuml.common.sparsefuncs import extract_knn_graph from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.base import Base, get_handle +from cuml.internals.input_utils import validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -598,10 +599,14 @@ class TSNE(Base, X_indices_ptr = X_m.indices.ptr X_nnz = X_m.nnz else: - X_m, n_samples, n_features, _ = input_to_cuml_array( - X, order='F', check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None) + X_out = validate_data( + self, X, + order='F', + check_dtype=np.float32, + convert_to_dtype=(np.float32 if convert_dtype else False), ) + X_m = X_out.array + n_samples, n_features = X_out.n_rows, X_out.n_cols X_ptr = X_m.ptr # Initialize TSNEParams diff --git a/python/cuml/cuml/naive_bayes/naive_bayes.py b/python/cuml/cuml/naive_bayes/naive_bayes.py index a2cb3bd7a0..00affffe9f 100644 --- a/python/cuml/cuml/naive_bayes/naive_bayes.py +++ b/python/cuml/cuml/naive_bayes/naive_bayes.py @@ -8,13 +8,18 @@ import cupy as cp import cupyx import scipy.sparse +from sklearn.utils.validation import check_is_fitted import cuml.internals.nvtx as nvtx from cuml.common import CumlArray from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cuml_array, input_to_cupy_array +from cuml.internals.input_utils import ( + input_to_cuml_array, + input_to_cupy_array, + validate_data, +) from cuml.internals.mixins import ClassifierMixin from cuml.internals.outputs import reflect from cuml.prims.label.classlabels import make_monotonic @@ -196,6 +201,16 @@ def predict(self, X, *, convert_dtype=True) -> CumlArray: Perform classification on an array of test vectors X. """ + check_is_fitted(self) + + if X.ndim == 1: + raise ValueError( + "Expected 2D array, got 1D array instead.\n" + "Reshape your data either using array.reshape(-1, 1) if " + "your data has a single feature or array.reshape(1, -1) " + "if it contains a single sample." + ) + if scipy.sparse.isspmatrix(X) or cupyx.scipy.sparse.isspmatrix(X): X = _convert_x_sparse(X) index = None @@ -365,6 +380,12 @@ def fit(self, X, y, sample_weight=None) -> "GaussianNB": sample_weight : array-like of shape (n_samples) Weights applied to individual samples. """ + if y is None: + raise ValueError( + "This GaussianNB estimator " + "requires y to be passed, but the target y is None." + ) + return self._partial_fit( X, y, @@ -386,6 +407,12 @@ def _partial_fit( sample_weight=None, convert_dtype=True, ) -> "GaussianNB": + if y is None: + raise ValueError( + "This GaussianNB estimator " + "requires y to be passed, but the target y is None." + ) + first_call = _refit or not hasattr(self, "classes_") if first_call and _classes is None: @@ -396,9 +423,14 @@ def _partial_fit( if scipy.sparse.isspmatrix(X) or cupyx.scipy.sparse.isspmatrix(X): X = _convert_x_sparse(X) else: - X = input_to_cupy_array( - X, order="K", check_dtype=[cp.float32, cp.float64, cp.int32] - ).array + X, _, _, _ = validate_data( + self, + X, + array_output_type="cupy", + order="K", + check_dtype=[cp.float32, cp.float64, cp.int32], + reset=first_call, + ) expected_y_dtype = ( cp.int32 if X.dtype in [cp.float32, cp.int32] else cp.int64 @@ -409,6 +441,9 @@ def _partial_fit( check_rows=X.shape[0], check_dtype=expected_y_dtype, ).array + + if cp.any(cp.isnan(y.astype(cp.float64))): + raise ValueError("Input y contains NaN.") if sample_weight is not None: sample_weight = input_to_cupy_array( sample_weight, @@ -843,6 +878,12 @@ def _partial_fit( _refit=False, convert_dtype=True, ) -> "_BaseDiscreteNB": + if y is None: + raise ValueError( + "This estimator " + "requires y to be passed, but the target y is None." + ) + first_call = _refit or not hasattr(self, "classes_") if self.alpha < 0: @@ -851,11 +892,14 @@ def _partial_fit( if scipy.sparse.isspmatrix(X) or cupyx.scipy.sparse.isspmatrix(X): X = _convert_x_sparse(X) else: - X = input_to_cupy_array( + X, _, _, _ = validate_data( + self, X, + array_output_type="cupy", order="K", check_dtype=[cp.float32, cp.float64, cp.int32], - ).array + reset=first_call, + ) expected_y_dtype = ( cp.int32 if X.dtype in [cp.float32, cp.int32] else cp.int64 @@ -865,6 +909,9 @@ def _partial_fit( convert_to_dtype=(expected_y_dtype if convert_dtype else False), check_dtype=expected_y_dtype, ).array + + if cp.any(cp.isnan(y.astype(cp.float64))): + raise ValueError("Input y contains NaN.") if _classes is not None: _classes, *_ = input_to_cuml_array( _classes, @@ -920,6 +967,12 @@ def fit(self, X, y, sample_weight=None) -> "_BaseDiscreteNB": Weights applied to individual samples. Currently sample weight is ignored. """ + if y is None: + raise ValueError( + "This estimator " + "requires y to be passed, but the target y is None." + ) + return self._partial_fit( X, y, _refit=True, sample_weight=sample_weight ) diff --git a/python/cuml/cuml/neighbors/kernel_density.py b/python/cuml/cuml/neighbors/kernel_density.py index 563c6515cb..50e94e5b66 100644 --- a/python/cuml/cuml/neighbors/kernel_density.py +++ b/python/cuml/cuml/neighbors/kernel_density.py @@ -12,7 +12,7 @@ from cuml.common.exceptions import NotFittedError from cuml.internals.array import CumlArray from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cuml_array, input_to_cupy_array +from cuml.internals.input_utils import input_to_cupy_array, validate_data from cuml.internals.interop import InteropMixin, UnsupportedOnGPU from cuml.internals.outputs import reflect, run_in_internal_context from cuml.internals.utils import check_random_seed @@ -302,8 +302,10 @@ def fit( if self.kernel not in VALID_KERNELS: raise ValueError(f"kernel={self.kernel!r} is not supported") - self._X, n_rows, n_cols, _ = input_to_cupy_array( + self._X, n_rows, n_cols, _ = validate_data( + self, X, + array_output_type="cupy", order="C", convert_to_dtype=(np.float32 if convert_dtype else None), check_dtype=[cp.float32, cp.float64], @@ -355,11 +357,13 @@ def score_samples(self, X, *, convert_dtype=True) -> CumlArray: if not hasattr(self, "_X"): raise NotFittedError() - X = input_to_cuml_array( + X = validate_data( + self, X, + reset=False, + array_output_type="cupy", convert_to_dtype=(self._X.dtype if convert_dtype else None), check_dtype=[self._X.dtype], - check_cols=self.n_features_in_, ).array if self.metric_params: if len(self.metric_params) != 1: diff --git a/python/cuml/cuml/neighbors/kneighbors_classifier.pyx b/python/cuml/cuml/neighbors/kneighbors_classifier.pyx index 7c7e220bbc..38e23bc04d 100644 --- a/python/cuml/cuml/neighbors/kneighbors_classifier.pyx +++ b/python/cuml/cuml/neighbors/kneighbors_classifier.pyx @@ -6,6 +6,7 @@ from __future__ import annotations import cupy as cp import numpy as np +from sklearn.utils.validation import check_is_fitted import cuml from cuml.common import input_to_cuml_array @@ -171,6 +172,12 @@ class KNeighborsClassifier(ClassifierMixin, Fit a GPU index for k-nearest neighbors classifier model. """ + if y is None: + raise ValueError( + f"This {self.__class__.__name__} estimator " + "requires y to be passed, but the target y is None." + ) + if self.weights not in ('uniform', 'distance', None) and not callable(self.weights): raise ValueError( f"weights must be 'uniform', 'distance', or a callable, got {self.weights}" @@ -205,6 +212,16 @@ class KNeighborsClassifier(ClassifierMixin, predict the labels for X """ + check_is_fitted(self) + + if X.ndim == 1: + raise ValueError( + "Expected 2D array, got 1D array instead.\n" + "Reshape your data either using array.reshape(-1, 1) if " + "your data has a single feature or array.reshape(1, -1) " + "if it contains a single sample." + ) + # Get KNN results - always get distances to compute weights knn_distances, knn_indices = self.kneighbors( X, return_distance=True, convert_dtype=convert_dtype diff --git a/python/cuml/cuml/neighbors/kneighbors_regressor.pyx b/python/cuml/cuml/neighbors/kneighbors_regressor.pyx index c92c2da6a7..41ae2fc046 100644 --- a/python/cuml/cuml/neighbors/kneighbors_regressor.pyx +++ b/python/cuml/cuml/neighbors/kneighbors_regressor.pyx @@ -2,7 +2,9 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # +import cupy as cp import numpy as np +from sklearn.utils.validation import check_is_fitted from cuml.common import input_to_cuml_array from cuml.common.doc_utils import generate_docstring @@ -170,6 +172,12 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NearestNeighbors) Fit a GPU index for k-nearest neighbors regression model. """ + if y is None: + raise ValueError( + f"This {self.__class__.__name__} estimator " + "requires y to be passed, but the target y is None." + ) + if self.weights not in ('uniform', 'distance', None) and not callable(self.weights): raise ValueError( f"weights must be 'uniform', 'distance', or a callable, got {self.weights}" @@ -184,6 +192,9 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NearestNeighbors) convert_to_dtype=(np.float32 if convert_dtype else None), ).array + if cp.any(cp.isnan(self._y.to_output("cupy"))): + raise ValueError("Input y contains NaN.") + return self @generate_docstring(convert_dtype_cast='np.float32', @@ -198,6 +209,16 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NearestNeighbors) predict the labels for X """ + check_is_fitted(self) + + if X.ndim == 1: + raise ValueError( + "Expected 2D array, got 1D array instead.\n" + "Reshape your data either using array.reshape(-1, 1) if " + "your data has a single feature or array.reshape(1, -1) " + "if it contains a single sample." + ) + # Get KNN results - always get distances to compute weights knn_distances, knn_indices = self.kneighbors( X, return_distance=True, convert_dtype=convert_dtype diff --git a/python/cuml/cuml/neighbors/nearest_neighbors.pyx b/python/cuml/cuml/neighbors/nearest_neighbors.pyx index a965090b33..cbc66ec100 100644 --- a/python/cuml/cuml/neighbors/nearest_neighbors.pyx +++ b/python/cuml/cuml/neighbors/nearest_neighbors.pyx @@ -17,7 +17,7 @@ from cuml.common.sparse_utils import is_dense, is_sparse from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.base import Base, get_handle -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import validate_data from cuml.internals.interop import InteropMixin, UnsupportedOnGPU, to_gpu from cuml.internals.mixins import CMajorInputTagMixin, SparseInputTagMixin from cuml.internals.outputs import reflect, using_output_type @@ -706,12 +706,12 @@ class NearestNeighbors(Base, self._fit_X = SparseCumlArray(X, convert_to_dtype=cp.float32) else: valid_metrics = cuml.neighbors.VALID_METRICS - self._fit_X, _, _, _ = input_to_cuml_array( - X, + self._fit_X = validate_data( + self, X, order='C', check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - ) + convert_to_dtype=(np.float32 if convert_dtype else False), + ).array self.n_samples_fit_, self.n_features_in_ = self._fit_X.shape @@ -857,11 +857,10 @@ class NearestNeighbors(Base, "data requires dense input to kneighbors()") cdef int n_rows, n_cols - X_m, n_rows, n_cols, _ = input_to_cuml_array( - X, + X_m, n_rows, n_cols, _ = validate_data( + self, X, reset=False, order="C", check_dtype=np.float32, - check_cols=self.n_features_in_, convert_to_dtype=(np.float32 if convert_dtype else False), ) diff --git a/python/cuml/cuml/random_projection/random_projection.py b/python/cuml/cuml/random_projection/random_projection.py index fb1839a97c..8cc3b85cdd 100644 --- a/python/cuml/cuml/random_projection/random_projection.py +++ b/python/cuml/cuml/random_projection/random_projection.py @@ -4,13 +4,14 @@ import cupyx.scipy.sparse as cp_sp import numpy as np import scipy.sparse as sp +from sklearn.utils.validation import check_is_fitted from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import validate_data from cuml.internals.mixins import SparseInputTagMixin from cuml.internals.outputs import reflect from cuml.internals.utils import check_random_seed @@ -81,6 +82,14 @@ def _gen_random_matrix(self, n_components, n_features, dtype): @reflect(reset=True) def fit(self, X, y=None, *, convert_dtype=True): """Generate a random projection matrix.""" + if X.ndim == 1: + raise ValueError( + "Expected 2D array, got 1D array instead.\n" + "Reshape your data either using array.reshape(-1, 1) if " + "your data has a single feature or array.reshape(1, -1) " + "if it contains a single sample." + ) + n_samples, n_features = X.shape # Prefer float32, unless `convert_dtype=False` and the input is float64 @@ -119,14 +128,17 @@ def fit(self, X, y=None, *, convert_dtype=True): @reflect def transform(self, X, *, convert_dtype=True) -> CumlArray: """Project the data by taking the matrix product with the random matrix.""" + check_is_fitted(self) # Coerce X to a cupy array or cupyx sparse matrix index = None if sp.issparse(X): X = cp_sp.csr_matrix(X) elif not cp_sp.issparse(X): - X_m = input_to_cuml_array( + X_m = validate_data( + self, X, - convert_to_dtype=(np.float32 if convert_dtype else None), + reset=False, + convert_to_dtype=(np.float32 if convert_dtype else False), check_dtype=[np.float32, np.float64], order="K", ).array diff --git a/python/cuml/cuml/svm/linear_svc.py b/python/cuml/cuml/svm/linear_svc.py index 933258b2e4..8ca755ac2f 100644 --- a/python/cuml/cuml/svm/linear_svc.py +++ b/python/cuml/cuml/svm/linear_svc.py @@ -3,6 +3,7 @@ # import cupy as cp import numpy as np +from sklearn.utils.validation import check_is_fitted import cuml.svm.linear from cuml.common.array_descriptor import CumlArrayDescriptor @@ -15,7 +16,7 @@ from cuml.common.exceptions import NotFittedError from cuml.internals.array import CumlArray from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import input_to_cuml_array, validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -244,8 +245,15 @@ def fit( self, X, y, sample_weight=None, *, convert_dtype=True ) -> "LinearSVC": """Fit the model according to the given training data.""" - X = input_to_cuml_array( + # y is only forwarded when None so that validate_data's tag-driven + # check raises ValueError for missing targets. Non-None y is skipped + # here because classifiers accept string labels that + # input_to_cuml_array cannot convert; preprocess_labels handles y + # conversion below. + X = validate_data( + self, X, + y=y if y is None else "no_validation", convert_to_dtype=(np.float32 if convert_dtype else None), check_dtype=[np.float32, np.float64], order="F", @@ -298,6 +306,7 @@ def fit( @run_in_internal_context def predict(self, X, *, convert_dtype=True): """Predict class labels for samples in X.""" + check_is_fitted(self) if self.probability: scores = self.predict_proba( X, convert_dtype=convert_dtype diff --git a/python/cuml/cuml/svm/linear_svr.py b/python/cuml/cuml/svm/linear_svr.py index 7bdb420463..5c592615f7 100644 --- a/python/cuml/cuml/svm/linear_svr.py +++ b/python/cuml/cuml/svm/linear_svr.py @@ -1,13 +1,14 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # +import cupy as cp import numpy as np import cuml.svm.linear from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import input_to_cuml_array, validate_data from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -205,20 +206,18 @@ def fit( self, X, y, sample_weight=None, *, convert_dtype=True ) -> "LinearSVR": """Fit the model according to the given training data.""" - X = input_to_cuml_array( + X_out, y_out = validate_data( + self, X, + y, convert_to_dtype=(np.float32 if convert_dtype else None), check_dtype=[np.float32, np.float64], order="F", - ).array - - y = input_to_cuml_array( - y, - check_dtype=X.dtype, - convert_to_dtype=(X.dtype if convert_dtype else None), - check_rows=X.shape[0], - check_cols=1, - ).array + ) + X = X_out.array + y = y_out.array + if cp.any(cp.isnan(y.to_output("cupy"))): + raise ValueError("Input y contains NaN.") if sample_weight is not None: sample_weight = input_to_cuml_array( diff --git a/python/cuml/cuml/svm/svc.py b/python/cuml/cuml/svm/svc.py index 26147dcebb..af53862314 100644 --- a/python/cuml/cuml/svm/svc.py +++ b/python/cuml/cuml/svm/svc.py @@ -3,6 +3,7 @@ # import cupy as cp import numpy as np +from sklearn.utils.validation import check_is_fitted from cuml.common.classification import ( decode_labels, @@ -18,6 +19,7 @@ input_to_cuml_array, input_to_host_array, input_to_host_array_with_sparse_support, + validate_data, ) from cuml.internals.interop import UnsupportedOnCPU, UnsupportedOnGPU from cuml.internals.logger import warn @@ -435,6 +437,17 @@ def fit(self, X, y, sample_weight=None, *, convert_dtype=True) -> "SVC": Fit the model with X and y. """ + # y is only forwarded when None so that validate_data's tag-driven + # check raises ValueError for missing targets. Non-None y is skipped + # because classifiers accept string labels that input_to_cuml_array + # cannot convert; preprocess_labels handles y conversion below. + # accept_sparse=True because SVC handles sparse X internally. + validate_data( + self, + X, + y=y if y is None else "no_validation", + accept_sparse=True, + ) if hasattr(self, "_multiclass"): del self._multiclass @@ -518,6 +531,7 @@ def predict(self, X, *, convert_dtype=True): Predicts the class labels for X. The returned y values are the class labels associated to sign(decision_function(X)). """ + check_is_fitted(self) if hasattr(self, "_multiclass"): inds = self._multiclass.predict(X).to_output("cupy") elif self.probability: @@ -646,12 +660,13 @@ def decision_function(self, X, *, convert_dtype=True) -> CumlArray: elif is_sparse(X): X = SparseCumlArray(X, convert_to_dtype=dtype) else: - X = input_to_cuml_array( + X = validate_data( + self, X, + reset=False, check_dtype=[dtype], convert_to_dtype=(dtype if convert_dtype else None), order="F", - check_cols=self.shape_fit_[1], # Number of features ).array return self._predict(X) diff --git a/python/cuml/cuml/svm/svr.py b/python/cuml/cuml/svm/svr.py index fdca0efafc..8706d9a789 100644 --- a/python/cuml/cuml/svm/svr.py +++ b/python/cuml/cuml/svm/svr.py @@ -1,13 +1,15 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # +import cupy as cp import numpy as np +from sklearn.utils.validation import check_is_fitted from cuml.common.doc_utils import generate_docstring from cuml.common.sparse_utils import is_sparse from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray -from cuml.internals.input_utils import input_to_cuml_array +from cuml.internals.input_utils import input_to_cuml_array, validate_data from cuml.internals.mixins import RegressorMixin from cuml.internals.outputs import reflect from cuml.svm.svm_base import SVMBase @@ -143,6 +145,11 @@ def fit(self, X, y, sample_weight=None, *, convert_dtype=True) -> "SVR": """ # Handle precomputed kernels if self.kernel == "precomputed": + if y is None: + raise ValueError( + "This SVR estimator " + "requires y to be passed, but the target y is None." + ) if is_sparse(X): raise TypeError( "Sparse precomputed kernels are not supported." @@ -159,28 +166,49 @@ def fit(self, X, y, sample_weight=None, *, convert_dtype=True) -> "SVR": f"Precomputed kernel matrix must be square, " f"got shape ({X.shape[0]}, {X.shape[1]})" ) + y = input_to_cuml_array( + y, + check_dtype=X.dtype, + convert_to_dtype=(X.dtype if convert_dtype else None), + check_rows=X.shape[0], + check_cols=1, + ).array + if cp.any(cp.isnan(y.to_output("cupy"))): + raise ValueError("Input y contains NaN.") elif is_sparse(X): + if y is None: + raise ValueError( + "This SVR estimator " + "requires y to be passed, but the target y is None." + ) X = SparseCumlArray( X, convert_to_dtype=( None if X.dtype in (np.float32, np.float64) else np.float32 ), ) + y = input_to_cuml_array( + y, + check_dtype=X.dtype, + convert_to_dtype=(X.dtype if convert_dtype else None), + check_rows=X.shape[0], + check_cols=1, + ).array + if cp.any(cp.isnan(y.to_output("cupy"))): + raise ValueError("Input y contains NaN.") else: - X = input_to_cuml_array( + X_out, y_out = validate_data( + self, X, + y, convert_to_dtype=(np.float32 if convert_dtype else None), check_dtype=[np.float32, np.float64], order="F", - ).array - - y = input_to_cuml_array( - y, - check_dtype=X.dtype, - convert_to_dtype=(X.dtype if convert_dtype else None), - check_rows=X.shape[0], - check_cols=1, - ).array + ) + X = X_out.array + y = y_out.array + if cp.any(cp.isnan(y.to_output("cupy"))): + raise ValueError("Input y contains NaN.") if sample_weight is not None: sample_weight = input_to_cuml_array( @@ -213,6 +241,7 @@ def predict(self, X, *, convert_dtype=True) -> CumlArray: number of samples used during fit. """ + check_is_fitted(self) dtype = self.support_vectors_.dtype # For precomputed kernels, check that columns match training set size @@ -231,12 +260,13 @@ def predict(self, X, *, convert_dtype=True) -> CumlArray: elif is_sparse(X): X = SparseCumlArray(X, convert_to_dtype=dtype) else: - X = input_to_cuml_array( + X = validate_data( + self, X, check_dtype=[dtype], convert_to_dtype=(dtype if convert_dtype else None), order="F", - check_cols=self.shape_fit_[1], # Number of features + reset=False, ).array return self._predict(X) diff --git a/python/cuml/tests/test_sklearn_compatibility.py b/python/cuml/tests/test_sklearn_compatibility.py index 8cecb3eacd..f7ac174488 100644 --- a/python/cuml/tests/test_sklearn_compatibility.py +++ b/python/cuml/tests/test_sklearn_compatibility.py @@ -47,8 +47,6 @@ PER_ESTIMATOR_XFAIL_CHECKS = { KMeans: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "KMeans does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "KMeans does not check n_features_in consistency", "check_sample_weights_not_an_array": "KMeans does not handle non-array sample weights", "check_sample_weights_list": "KMeans does not handle list sample weights", "check_sample_weight_equivalence_on_dense_data": "KMeans sample weight equivalence not implemented", @@ -56,14 +54,9 @@ "check_dtype_object": "KMeans does not handle object dtype", "check_estimators_nan_inf": "KMeans does not check for NaN and inf", "check_transformer_data_not_an_array": "KMeans does not handle non-array data", - "check_fit1d": "KMeans does not raise ValueError for 1D input", - "check_fit2d_predict1d": "KMeans does not handle 1D prediction input gracefully", }, KernelRidge: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "KernelRidge does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "KernelRidge does not check n_features_in consistency", - "check_sample_weights_pandas_series": "KernelRidge does not handle pandas Series sample weights", "check_sample_weights_not_an_array": "KernelRidge does not handle non-array sample weights", "check_all_zero_sample_weights_error": "KernelRidge does not validate all-zero sample weights", "check_dtype_object": "KernelRidge does not handle object dtype", @@ -75,14 +68,10 @@ "check_regressor_data_not_an_array": "KernelRidge does not handle non-array data", "check_supervised_y_2d": "KernelRidge does not handle 2D y", "check_supervised_y_no_nan": "KernelRidge does not check for NaN in y", - "check_fit1d": "KernelRidge does not raise ValueError for 1D input", - "check_fit2d_predict1d": "KernelRidge does not handle 1D prediction input gracefully", - "check_requires_y_none": "KernelRidge does not handle y=None", }, LogisticRegression: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_estimators_unfitted": "LogisticRegression does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "LogisticRegression does not check n_features_in consistency", "check_sample_weights_not_an_array": "LogisticRegression does not handle non-array sample weights", "check_sample_weights_list": "LogisticRegression does not handle list sample weights", "check_sample_weight_equivalence_on_dense_data": "LogisticRegression sample weight equivalence not implemented", @@ -98,14 +87,9 @@ "check_supervised_y_2d": "LogisticRegression does not handle 2D y", "check_class_weight_classifiers": "LogisticRegression does not handle class weights properly", "check_fit2d_1sample": "LogisticRegression does not handle single sample", - "check_fit1d": "LogisticRegression does not raise ValueError for 1D input", - "check_fit2d_predict1d": "LogisticRegression does not handle 1D prediction input gracefully", - "check_requires_y_none": "LogisticRegression does not handle y=None", }, LinearRegression: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "LinearRegression does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "LinearRegression does not check n_features_in consistency", "check_sample_weights_not_an_array": "LinearRegression does not handle non-array sample weights", "check_sample_weights_list": "LinearRegression does not handle list sample weights", "check_all_zero_sample_weights_error": "LinearRegression does not validate all-zero sample weights", @@ -118,14 +102,9 @@ "check_regressor_data_not_an_array": "LinearRegression does not handle non-array data", "check_supervised_y_no_nan": "LinearRegression does not check for NaN in y", "check_fit2d_1sample": "LinearRegression does not handle single sample", - "check_fit1d": "LinearRegression does not raise ValueError for 1D input", - "check_fit2d_predict1d": "LinearRegression does not handle 1D prediction input gracefully", - "check_requires_y_none": "LinearRegression does not handle y=None", }, Ridge: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "Ridge does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "Ridge does not check n_features_in consistency", "check_sample_weights_not_an_array": "Ridge does not handle non-array sample weights", "check_sample_weights_list": "Ridge does not handle list sample weights", "check_all_zero_sample_weights_error": "Ridge does not validate all-zero sample weights", @@ -137,13 +116,9 @@ "check_regressor_data_not_an_array": "Ridge does not handle non-array data", "check_supervised_y_2d": "Ridge does not handle 2D y", "check_supervised_y_no_nan": "Ridge does not check for NaN in y", - "check_fit1d": "Ridge does not raise ValueError for 1D input", - "check_fit2d_predict1d": "Ridge does not handle 1D prediction input gracefully", - "check_requires_y_none": "Ridge does not handle y=None", }, RandomForestRegressor: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "RandomForestRegressor does not raise NotFittedError before fit", "check_do_not_raise_errors_in_init_or_set_params": "RandomForestRegressor raises errors in init or set_params", "check_n_features_in_after_fitting": "RandomForestRegressor does not check n_features_in consistency", "check_dtype_object": "RandomForestRegressor does not handle object dtype", @@ -156,9 +131,6 @@ "check_supervised_y_2d": "RandomForestRegressor does not handle 2D y", "check_supervised_y_no_nan": "RandomForestRegressor does not check for NaN in y", "check_dict_unchanged": "RandomForestRegressor modifies input dictionaries", - "check_fit1d": "RandomForestRegressor does not raise ValueError for 1D input", - "check_fit2d_predict1d": "RandomForestRegressor does not handle 1D prediction input gracefully", - "check_requires_y_none": "RandomForestRegressor does not handle y=None", }, KNeighborsClassifier: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -175,7 +147,6 @@ "check_supervised_y_2d": "KNeighborsClassifier does not handle 2D y", "check_fit_check_is_fitted": "KNeighborsClassifier passes check_is_fitted before being fit", "check_fit2d_predict1d": "KNeighborsClassifier does not handle 1D prediction input gracefully", - "check_requires_y_none": "KNeighborsClassifier does not handle y=None", }, RandomForestClassifier: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -192,9 +163,7 @@ "check_supervised_y_no_nan": "RandomForestClassifier does not check for NaN in y", "check_supervised_y_2d": "RandomForestClassifier does not handle 2D y", "check_dict_unchanged": "RandomForestClassifier modifies input dictionaries", - "check_fit1d": "RandomForestClassifier does not raise ValueError for 1D input", "check_fit2d_predict1d": "RandomForestClassifier does not handle 1D prediction input gracefully", - "check_requires_y_none": "RandomForestClassifier does not handle y=None", }, KNeighborsRegressor: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -212,8 +181,6 @@ "check_supervised_y_2d": "KNeighborsRegressor does not handle 2D y", "check_supervised_y_no_nan": "KNeighborsRegressor does not check for NaN in y", "check_fit_check_is_fitted": "KNeighborsRegressor passes check_is_fitted before being fit", - "check_fit2d_predict1d": "KNeighborsRegressor does not handle 1D prediction input gracefully", - "check_requires_y_none": "KNeighborsRegressor does not handle y=None", }, NearestNeighbors: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -246,14 +213,10 @@ "check_methods_subset_invariance": "LinearSVC results depend on data subset", "check_dict_unchanged": "LinearSVC modifies input dictionaries", "check_fit_idempotent": "LinearSVC fit is not idempotent", - "check_fit1d": "LinearSVC does not raise ValueError for 1D input", "check_fit2d_predict1d": "LinearSVC does not handle 1D prediction input gracefully", - "check_requires_y_none": "LinearSVC does not handle y=None", }, LinearSVR: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "LinearSVR does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "LinearSVR does not check n_features_in consistency", "check_sample_weights_not_an_array": "LinearSVR does not handle non-array sample weights", "check_sample_weights_list": "LinearSVR does not handle list sample weights", "check_sample_weight_equivalence_on_dense_data": "LinearSVR sample weight equivalence not implemented", @@ -266,9 +229,6 @@ "check_regressor_data_not_an_array": "LinearSVR does not handle non-array data", "check_supervised_y_2d": "LinearSVR does not handle 2D y", "check_supervised_y_no_nan": "LinearSVR does not check for NaN in y", - "check_fit1d": "LinearSVR does not raise ValueError for 1D input", - "check_fit2d_predict1d": "LinearSVR does not handle 1D prediction input gracefully", - "check_requires_y_none": "LinearSVR does not handle y=None", }, SVC: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -295,14 +255,11 @@ "check_dict_unchanged": "SVC modifies input dictionaries", "check_fit_idempotent": "SVC fit is not idempotent", "check_fit2d_predict1d": "SVC does not handle 1D prediction input gracefully", - "check_requires_y_none": "SVC does not handle y=None", "check_sample_weights_list": "SVC does not handle list sample weights", "check_supervised_y_2d": "SVC does not warn on 1 column 2D y", }, SVR: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "SVR does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "SVR does not check n_features_in consistency", "check_sample_weights_not_an_array": "SVR does not handle non-array sample weights", "check_sample_weights_list": "SVR does not handle list sample weights", "check_sample_weight_equivalence_on_dense_data": "SVR sample weight equivalence not implemented", @@ -316,20 +273,15 @@ "check_regressor_data_not_an_array": "SVR does not handle non-array data", "check_supervised_y_2d": "SVR does not handle 2D y", "check_supervised_y_no_nan": "SVR does not check for NaN in y", - "check_fit2d_predict1d": "SVR does not handle 1D prediction input gracefully", - "check_requires_y_none": "SVR does not handle y=None", }, PCA: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_n_features_in_after_fitting": "PCA does not check n_features_in consistency", "check_dtype_object": "PCA does not handle object dtype", "check_estimators_empty_data_messages": "PCA does not handle empty data", "check_estimators_nan_inf": "PCA does not check for NaN and inf", "check_transformer_data_not_an_array": "PCA does not handle non-array data", "check_fit2d_1sample": "PCA does not handle single sample", "check_fit2d_1feature": "PCA does not handle single feature", - "check_fit1d": "PCA does not raise ValueError for 1D input", - "check_fit2d_predict1d": "PCA does not handle 1D prediction input gracefully", }, IncrementalPCA: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -338,19 +290,15 @@ "check_estimators_empty_data_messages": "IncrementalPCA does not handle empty data", "check_estimators_nan_inf": "IncrementalPCA does not check for NaN and inf", "check_transformer_data_not_an_array": "IncrementalPCA does not handle non-array data", - "check_fit2d_predict1d": "IncrementalPCA does not handle 1D prediction input gracefully", }, TruncatedSVD: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_n_features_in_after_fitting": "TruncatedSVD does not check n_features_in consistency", "check_dtype_object": "TruncatedSVD does not handle object dtype", "check_estimators_empty_data_messages": "TruncatedSVD does not handle empty data", "check_estimators_nan_inf": "TruncatedSVD does not check for NaN and inf", "check_transformer_data_not_an_array": "TruncatedSVD does not handle non-array data", "check_fit2d_1sample": "TruncatedSVD does not handle single sample", "check_fit2d_1feature": "TruncatedSVD does not handle single feature", - "check_fit1d": "TruncatedSVD does not raise ValueError for 1D input", - "check_fit2d_predict1d": "TruncatedSVD does not handle 1D prediction input gracefully", }, TSNE: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -375,8 +323,6 @@ }, Lasso: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "Lasso does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "Lasso does not check n_features_in consistency", "check_sample_weights_not_an_array": "Lasso does not handle non-array sample weights", "check_sample_weights_list": "Lasso does not handle list sample weights", "check_all_zero_sample_weights_error": "Lasso does not validate all-zero sample weights", @@ -388,14 +334,9 @@ "check_regressor_data_not_an_array": "Lasso does not handle non-array data", "check_supervised_y_2d": "Lasso does not handle 2D y", "check_supervised_y_no_nan": "Lasso does not check for NaN in y", - "check_fit1d": "Lasso does not raise ValueError for 1D input", - "check_fit2d_predict1d": "Lasso does not handle 1D prediction input gracefully", - "check_requires_y_none": "Lasso does not handle y=None", }, ElasticNet: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_estimators_unfitted": "ElasticNet does not raise NotFittedError before fit", - "check_n_features_in_after_fitting": "ElasticNet does not check n_features_in consistency", "check_sample_weights_not_an_array": "ElasticNet does not handle non-array sample weights", "check_sample_weights_list": "ElasticNet does not handle list sample weights", "check_all_zero_sample_weights_error": "ElasticNet does not validate all-zero sample weights", @@ -407,23 +348,17 @@ "check_regressor_data_not_an_array": "ElasticNet does not handle non-array data", "check_supervised_y_2d": "ElasticNet does not handle 2D y", "check_supervised_y_no_nan": "ElasticNet does not check for NaN in y", - "check_fit1d": "ElasticNet does not raise ValueError for 1D input", - "check_fit2d_predict1d": "ElasticNet does not handle 1D prediction input gracefully", - "check_requires_y_none": "ElasticNet does not handle y=None", }, KernelDensity: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_n_features_in_after_fitting": "KernelDensity does not check n_features_in consistency", "check_sample_weights_not_an_array": "KernelDensity does not handle non-array sample weights", "check_sample_weights_list": "KernelDensity does not handle list sample weights", "check_all_zero_sample_weights_error": "KernelDensity does not validate all-zero sample weights", "check_dtype_object": "KernelDensity does not handle object dtype", "check_estimators_nan_inf": "KernelDensity does not check for NaN and inf", - "check_fit1d": "KernelDensity does not raise ValueError for 1D input", }, LedoitWolf: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_n_features_in_after_fitting": "LedoitWolf does not check n_features_in consistency", "check_dtype_object": "LedoitWolf does not handle object dtype", "check_estimators_empty_data_messages": "LedoitWolf does not handle empty data", "check_estimators_nan_inf": "LedoitWolf does not check for NaN and inf", @@ -436,7 +371,6 @@ "check_dtype_object": "DBSCAN does not handle object dtype", "check_estimators_empty_data_messages": "DBSCAN does not handle empty data", "check_estimators_nan_inf": "DBSCAN does not check for NaN and inf", - "check_fit1d": "DBSCAN does not raise ValueError for 1D input", }, HDBSCAN: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -494,9 +428,7 @@ "check_classifiers_regression_target": "GaussianNB does not handle regression targets", "check_supervised_y_no_nan": "GaussianNB does not check for NaN in y", "check_supervised_y_2d": "GaussianNB does not handle 2D y", - "check_fit1d": "GaussianNB does not raise ValueError for 1D input", "check_fit2d_predict1d": "GaussianNB does not handle 1D prediction input gracefully", - "check_requires_y_none": "GaussianNB does not handle y=None", "check_sample_weights_list": "GaussianNB does not handle list sample weights", }, GaussianRandomProjection: { @@ -526,7 +458,6 @@ "check_fit_idempotent": "GaussianRandomProjection raises ValueError with small datasets", "check_fit_check_is_fitted": "GaussianRandomProjection raises ValueError with small datasets", "check_n_features_in": "GaussianRandomProjection raises ValueError with small datasets", - "check_fit2d_predict1d": "GaussianRandomProjection does not handle 1D prediction input gracefully", }, SparseRandomProjection: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -555,7 +486,6 @@ "check_fit_idempotent": "SparseRandomProjection raises ValueError with small datasets", "check_fit_check_is_fitted": "SparseRandomProjection raises ValueError with small datasets", "check_n_features_in": "SparseRandomProjection raises ValueError with small datasets", - "check_fit2d_predict1d": "SparseRandomProjection does not handle 1D prediction input gracefully", }, BernoulliNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -580,7 +510,6 @@ "check_supervised_y_no_nan": "BernoulliNB does not check for NaN in y", "check_supervised_y_2d": "BernoulliNB does not handle 2D y input gracefully", "check_fit2d_predict1d": "BernoulliNB does not handle 1D prediction input gracefully", - "check_requires_y_none": "BernoulliNB does not require y for fit", }, ComplementNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -612,7 +541,6 @@ "check_supervised_y_no_nan": "ComplementNB does not check for NaN in y", "check_supervised_y_2d": "ComplementNB does not handle 2D y input gracefully", "check_fit2d_predict1d": "ComplementNB does not handle 1D prediction input gracefully", - "check_requires_y_none": "ComplementNB does not require y for fit", }, CategoricalNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -641,7 +569,6 @@ "check_supervised_y_no_nan": "CategoricalNB does not check for NaN in y", "check_supervised_y_2d": "CategoricalNB does not handle 2D y input gracefully", "check_fit2d_predict1d": "CategoricalNB does not handle 1D prediction input gracefully", - "check_requires_y_none": "CategoricalNB does not require y for fit", }, MultinomialNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -666,7 +593,6 @@ "check_supervised_y_no_nan": "MultinomialNB does not check for NaN in y", "check_supervised_y_2d": "MultinomialNB does not handle 2D y input gracefully", "check_fit2d_predict1d": "MultinomialNB does not handle 1D prediction input gracefully", - "check_requires_y_none": "MultinomialNB does not require y for fit", }, }