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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import cudf
import cupy as np
import numpy as cpu_np
import numba
import pandas as pd
import scipy.sparse as sp_sparse
Expand All @@ -35,7 +36,13 @@

import cuml
from cuml.internals.global_settings import _global_settings_data
from cuml.internals.validation import check_is_fitted, check_features, check_array
from cuml.internals.mixins import DeprecatedGetFeatureNamesMixin
from cuml.internals.validation import (
check_is_fitted,
check_features,
check_array,
check_input_features,
)

from ..preprocessing._function_transformer import FunctionTransformer
from ..utils.skl_dependencies import (
Expand Down Expand Up @@ -421,7 +428,12 @@ def _message_with_time(source, message, time):
return "%s%s%s" % (start_message, dots_len * '.', end_message)


class ColumnTransformer(TransformerMixin, BaseComposition, BaseEstimator):
class ColumnTransformer(
DeprecatedGetFeatureNamesMixin,
TransformerMixin,
BaseComposition,
BaseEstimator,
):
"""Applies transformers to columns of an array or dataframe.

This estimator allows different columns or column subsets of the input
Expand Down Expand Up @@ -747,38 +759,58 @@ def named_transformers_(self):
return Bunch(**{name: trans for name, trans, _
in self.transformers_})

def get_feature_names(self):
"""Get feature names from all transformers.
def get_feature_names_out(self, input_features=None):
"""Get output feature names for transformation.

Parameters
----------
input_features : array-like of str or None, default=None
Input feature names.

Returns
-------
feature_names : list of strings
Names of the features produced by transform.
feature_names_out : numpy.ndarray of str objects.
Transformed feature names.
"""
check_is_fitted(self)
feature_names = []
for name, trans, column, _ in self._iter(fitted=True):
if trans == 'drop' or (
hasattr(column, '__len__') and not len(column)):
input_features = check_input_features(self, input_features)
out = []
for trans_name, trans, col, _ in self._iter(fitted=True):
if trans == "drop":
continue
if trans == 'passthrough':
if hasattr(self, '_df_columns'):
if ((not isinstance(column, slice))
and all(isinstance(col, str) for col in column)):
feature_names.extend(column)
else:
feature_names.extend(self._df_columns[column])

# Determine column subset to pass to trans
Comment thread
jcrist marked this conversation as resolved.
if isinstance(col, slice):
inputs = input_features[col].tolist()
elif isinstance(col, int):
inputs = [input_features[col]]
elif isinstance(col, str):
inputs = [col]
else:
col = cpu_np.asarray(col, dtype=object)
if all(isinstance(c, str) for c in col):
inputs = col.tolist()
elif all(isinstance(c, bool) for c in col):
inputs = input_features[col.astype(bool)].tolist()
else:
indices = np.arange(self._n_features)
feature_names.extend(['x%d' % i for i in indices[column]])
inputs = [input_features[c] for c in col]
Comment thread
jcrist marked this conversation as resolved.

if not len(inputs):
continue
if not hasattr(trans, 'get_feature_names'):
raise AttributeError("Transformer %s (type %s) does not "
"provide get_feature_names."
% (str(name), type(trans).__name__))
feature_names.extend([name + "__" + f for f in
trans.get_feature_names()])
return feature_names

if trans == 'passthrough':
names = inputs
elif not hasattr(trans, 'get_feature_names_out'):
raise AttributeError(
f"Transformer {trans_name!s} (type {type(trans).__name__}) "
"does not provide get_feature_names_out."
)
else:
names = trans.get_feature_names_out(inputs)

out.extend([f"{trans_name}__{name}" for name in names])

return cpu_np.array(out, dtype=object)

def _update_fitted_transformers(self, transformers):
# transformers are fitted; excludes 'drop' cases
Expand Down
56 changes: 34 additions & 22 deletions python/cuml/cuml/_thirdparty/sklearn/preprocessing/_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,18 @@

from cuml.common.sparse import csr_row_normalize_l1, csr_row_normalize_l2
from cuml.internals.interop import InteropMixin
from cuml.internals.mixins import AllowNaNTagMixin, SparseInputTagMixin
from cuml.internals.mixins import (
AllowNaNTagMixin,
SparseInputTagMixin,
DeprecatedGetFeatureNamesMixin,
)
from cuml.internals.outputs import using_output_type, mlfunc, ReflectedAttr
from cuml.internals.validation import check_is_fitted, check_array, check_inputs
from cuml.internals.validation import (
check_is_fitted,
check_array,
check_inputs,
check_input_features,
)
from cuml.thirdparty_adapters.sparsefuncs_fast import csr_polynomial_expansion

from ..utils.extmath import _incremental_mean_and_var, row_norms
Expand Down Expand Up @@ -1480,10 +1489,13 @@ def robust_scale(X, *, axis=0, with_centering=True, with_scaling=True,
return X


class PolynomialFeatures(TransformerMixin,
BaseEstimator,
AllowNaNTagMixin,
SparseInputTagMixin):
class PolynomialFeatures(
DeprecatedGetFeatureNamesMixin,
TransformerMixin,
BaseEstimator,
AllowNaNTagMixin,
SparseInputTagMixin,
):
"""Generate polynomial and interaction features.

Generate a new feature matrix consisting of all polynomial combinations
Expand Down Expand Up @@ -1584,35 +1596,35 @@ def powers_(self):
minlength=self.n_input_features_)
for c in combinations])

def get_feature_names(self, input_features=None):
"""
Return feature names for output features
def get_feature_names_out(self, input_features=None):
"""Get output feature names for transformation.

Parameters
----------
input_features : list of string, length n_features, optional
String names for input features if available. By default,
"x0", "x1", ... "xn_features" is used.
input_features : array-like of str or None, default=None
Input feature names.

Returns
-------
output_feature_names : list of string, length n_output_features

feature_names_out : numpy.ndarray of str objects.
Transformed feature names.
"""
powers = self.powers_
if input_features is None:
input_features = ['x%d' % i for i in range(powers.shape[1])]
check_is_fitted(self)
input_features = check_input_features(self, input_features)
feature_names = []
for row in powers:
for row in self.powers_:
inds = cpu_np.where(row)[0]
if len(inds):
name = " ".join("%s^%d" % (input_features[ind], exp)
if exp != 1 else input_features[ind]
for ind, exp in zip(inds, row[inds]))
name = " ".join(
f"{input_features[ind]}^{exp}"
if exp != 1
else input_features[ind]
for ind, exp in zip(inds, row[inds])
)
else:
name = "1"
feature_names.append(name)
return feature_names
return cpu_np.asarray(feature_names, dtype=object)

@mlfunc(set_input_type=True)
def fit(self, X, y=None) -> "PolynomialFeatures":
Expand Down
13 changes: 1 addition & 12 deletions python/cuml/cuml/feature_extraction/_tfidf_vectorizer.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 & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Portions of this code are derived from the scikit-learn feature_extraction
Expand Down Expand Up @@ -282,14 +282,3 @@ def transform(self, raw_documents):
"""
X = super().transform(raw_documents)
return self._tfidf.transform(X, copy=False)

def get_feature_names(self):
"""
Array mapping from feature integer indices to feature name.

Returns
-------
feature_names : Series
A list of feature names.
"""
return super().get_feature_names()
24 changes: 14 additions & 10 deletions python/cuml/cuml/feature_extraction/_vectorizers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
import numbers
Expand All @@ -15,6 +15,7 @@
import cuml.internals.logger as logger
from cuml.common.sparse import csr_row_normalize_l1, csr_row_normalize_l2
from cuml.feature_extraction._stop_words import ENGLISH_STOP_WORDS
from cuml.internals.mixins import DeprecatedGetFeatureNamesMixin

CUPY_SPARSE_DTYPES = [cp.float32, cp.float64, cp.complex64, cp.complex128]

Expand Down Expand Up @@ -384,7 +385,7 @@ def _term_frequency(X):
return term_freq["count"].values


class CountVectorizer(_VectorizerMixin):
class CountVectorizer(DeprecatedGetFeatureNamesMixin, _VectorizerMixin):
"""
Convert a collection of text documents to a matrix of token counts

Expand Down Expand Up @@ -752,18 +753,21 @@ def inverse_transform(self, X):
vocab = Series(self.vocabulary_)
return [vocab[X[i, :].indices] for i in range(X.shape[0])]

def get_feature_names(self):
"""
Array mapping from feature integer indices to feature name.
def get_feature_names_out(self, input_features=None):
"""Get output feature names for transformation.

Parameters
----------
input_features : array-like of str or None, default=None
Not used, present here for API consistency by convention.

Returns
-------

feature_names : Series
A list of feature names.

feature_names_out : numpy.ndarray of str objects.
Transformed feature names.
"""
return self.vocabulary_
# TODO: use `check_is_fitted` once this class subclasses from `Base`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This TODO will also be handled shortly in a followup.

return self.vocabulary_.to_numpy(dtype=object)
Comment thread
jcrist marked this conversation as resolved.
Comment thread
jcrist marked this conversation as resolved.


class HashingVectorizer(_VectorizerMixin):
Expand Down
26 changes: 24 additions & 2 deletions python/cuml/cuml/internals/mixins.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#

import warnings
from dataclasses import dataclass, field

from sklearn.utils import (
Expand All @@ -17,6 +17,28 @@
from cuml.common.doc_utils import generate_docstring
from cuml.internals.outputs import ClassLabels, mlfunc


class DeprecatedGetFeatureNamesMixin:
"""Defines the deprecated `get_feature_names` method."""

def get_feature_names(self, input_features=None):
"""Return feature names for output features.

.. deprecated:: 26.10

This method was deprecated in version 26.10 and will be removed in
version 26.12. Please use `get_feature_names_out` instead.
"""
warnings.warn(
"`get_feature_names` was deprecated in version 26.10 and will be "
"removed in version 26.12. Please use `get_feature_names_out` instead",
FutureWarning,
)
Comment thread
jcrist marked this conversation as resolved.
return self.get_feature_names_out(
input_features=input_features
).tolist()


###############################################################################
# Tag Functionality Mixin #
###############################################################################
Expand Down
49 changes: 49 additions & 0 deletions python/cuml/cuml/internals/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"check_is_fitted",
"check_random_seed",
"check_features",
"check_input_features",
"check_consistent_length",
"check_all_finite",
"check_non_negative",
Expand Down Expand Up @@ -333,6 +334,54 @@ def check_features(estimator, X, reset=False) -> None:
)


def check_input_features(estimator, input_features=None):
"""Check `input_features` and generate names if needed.

Mainly useful when implementing `get_feature_names_out`.

Parameters
----------
input_features : array-like of str or None, default=None
Input features.

- If `input_features` is `None`, then `feature_names_in_` is used as
input feature names. If `feature_names_in_` is not defined, then the
following input feature names are generated: `["x0", "x1", ...,
"x(n_features_in_ - 1)"]`.
- If `input_features` is an array-like, then `input_features` must
match `feature_names_in_` if `feature_names_in_` is defined.

Returns
-------
feature_names_in : numpy.ndarray[str] or None
Validated input feature names.
"""
feature_names_in = getattr(estimator, "feature_names_in_", None)

if input_features is not None:
input_features = np.asarray(input_features, dtype=object)
if feature_names_in is not None and not np.array_equal(
feature_names_in, input_features
):
raise ValueError(
"input_features is not equal to feature_names_in_"
)

elif len(input_features) != estimator.n_features_in_:
raise ValueError(
"input_features should have length equal to number of "
f"features ({estimator.n_features_in_}), got {len(input_features)}"
)
return input_features

if feature_names_in is not None:
return feature_names_in

return np.asarray(
[f"x{i}" for i in range(estimator.n_features_in_)], dtype=object
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)


def check_consistent_length(*arrays) -> None:
"""Check whether all inputs have the same number of samples.

Expand Down
Loading
Loading