-
Notifications
You must be signed in to change notification settings - Fork 660
Rewrite OneHotEncoder and OrdinalEncoder
#8490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
47e77bc
Rewrite `OneHotEncoder`, `OrdinalEncoder`
jcrist a091d4d
Add `OneHotEncoder` and `OrdinalEncoder` to sklearn compat tests
jcrist 33f4134
Split out unique logic, preparing for dask implementations
jcrist 262b130
Update dask implementations
jcrist 47d6768
Respond to feedback
jcrist 7dba783
Fix `KBinsDiscretizer` tests
jcrist 6ecba6c
Some cudf.pandas compat fixes
jcrist 4f459b7
Clean out unused testing utils
jcrist 3793328
Fix `fit_transform` reflection handling in encoders
jcrist 4f6e1ef
Error in `OrdinalEncoder` for non-float dtype if NaN needed
jcrist d887cc4
Fixup unknown value errors in OrdinalEncoder
jcrist a22412b
Update umap xfail list
jcrist f2bf7ec
maybe now
jcrist 9fc9991
wtf
jcrist File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,14 @@ | ||
| # 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 | ||
| # | ||
| from collections.abc import Sequence | ||
|
|
||
| from dask_cudf import DataFrame as dcDataFrame | ||
| from dask_cudf import Series as dcSeries | ||
| from toolz import first | ||
| import dask_cudf | ||
|
|
||
| from cuml.dask.common.base import ( | ||
| BaseEstimator, | ||
| DelayedInverseTransformMixin, | ||
| DelayedTransformMixin, | ||
| ) | ||
| from cuml.dask.common.dask_arr_utils import to_dask_cudf | ||
|
|
||
|
|
||
| class DelayedFitTransformMixin: | ||
|
|
@@ -41,55 +38,57 @@ class OneHotEncoder( | |
| ): | ||
| """ | ||
| Encode categorical features as a one-hot numeric array. | ||
| The input to this transformer should be a dask_cuDF.DataFrame or cupy | ||
| dask.Array, denoting the values taken on by categorical features. | ||
|
|
||
| The input to this transformer should be an array-like of integers or | ||
| strings, denoting the values taken on by categorical (discrete) features. | ||
| The features are encoded using a one-hot (aka 'one-of-K' or 'dummy') | ||
| encoding scheme. This creates a binary column for each category and | ||
| returns a sparse matrix or dense array (depending on the ``sparse`` | ||
| returns a sparse matrix or dense array (depending on the ``sparse_output`` | ||
| parameter). | ||
|
|
||
| By default, the encoder derives the categories based on the unique values | ||
| in each feature. Alternatively, you can also specify the `categories` | ||
| manually. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| categories : 'auto', cupy.ndarray or cudf.DataFrame, default='auto' | ||
| Categories (unique values) per feature. All categories are expected to | ||
| fit on one GPU. | ||
| categories : 'auto' or a list of array-like, default='auto' | ||
| Categories (unique values) per feature: | ||
|
|
||
| - 'auto' : Determine categories automatically from the training data. | ||
| - list : ``categories[i]`` holds the categories expected in the ith | ||
| column. | ||
|
|
||
| - DataFrame/ndarray : ``categories[col]`` holds the categories expected | ||
| in the feature col. | ||
|
|
||
| drop : 'first', None or a dict, default=None | ||
| drop : 'first', None, or array-like of shape (n_features,), default=None | ||
| Specifies a methodology to use to drop one of the categories per | ||
| feature. This is useful in situations where perfectly collinear | ||
| features cause problems, such as when feeding the resulting data | ||
| into a neural network or an unregularized regression. | ||
| into an unregularized linear regression model. | ||
|
|
||
| - None : retain all features (the default). | ||
| However, dropping one category breaks the symmetry of the original | ||
| representation and can therefore induce a bias in downstream models, | ||
| for instance for penalized linear classification or regression models. | ||
|
|
||
| - None : retain all features (the default). | ||
| - 'first' : drop the first category in each feature. If only one | ||
| category is present, the feature will be dropped entirely. | ||
|
|
||
| - Dict : ``drop[col]`` is the category in feature col that | ||
| - array : ``drop[i]`` is the category in feature ``X[:, i]`` that | ||
| should be dropped. | ||
|
|
||
| sparse : bool, default=False | ||
| This feature was deactivated and will give an exception when True. | ||
| The reason is because sparse matrix are not fully supported by cupy | ||
| yet, causing incorrect values when computing one hot encodings. | ||
| See https://github.com/cupy/cupy/issues/3223 | ||
| dtype : number type, default=np.float | ||
| Desired datatype of transform's output. | ||
| sparse_output : bool, default=True | ||
| When ``True``, transform returns a sparse matrix/array in CSR format. | ||
|
|
||
| dtype : dtype, default=np.float32 | ||
| Desired dtype of transformed output. | ||
|
|
||
| handle_unknown : {'error', 'ignore'}, default='error' | ||
| Whether to raise an error or ignore if an unknown categorical feature | ||
| is present during transform (default is to raise). When this parameter | ||
| is set to 'ignore' and an unknown category is encountered during | ||
| transform, the resulting one-hot encoded columns for this feature | ||
| will be all zeros. In the inverse transform, an unknown category | ||
| will be denoted as None. | ||
| Specifies the way unknown categories are handled during :meth:`transform`. | ||
|
|
||
| - 'error' : Raise an error if an unknown category is present during transform. | ||
| - 'ignore' : When an unknown category is encountered during | ||
| transform, the resulting one-hot encoded columns for this feature | ||
| will be all zeros. In the inverse transform, an unknown category | ||
| will be denoted as None. | ||
| """ | ||
|
|
||
| def fit(self, X): | ||
|
|
@@ -104,14 +103,22 @@ def fit(self, X): | |
| ------- | ||
| self | ||
| """ | ||
| from cuml.preprocessing.onehotencoder_mg import OneHotEncoderMG | ||
| from cuml.preprocessing import OneHotEncoder | ||
|
|
||
| el = first(X) if isinstance(X, Sequence) else X | ||
| self.datatype = ( | ||
| "cudf" if isinstance(el, (dcDataFrame, dcSeries)) else "cupy" | ||
| ) | ||
| model = OneHotEncoder(**self.kwargs) | ||
|
|
||
| self._set_internal_model(OneHotEncoderMG(**self.kwargs).fit(X)) | ||
| if isinstance(X, dask_cudf.DataFrame): | ||
| self.datatype = model._input_type = model.output_type = "cudf" | ||
| else: | ||
| self.datatype = model._input_type = model.output_type = "cupy" | ||
| X = to_dask_cudf(X, client=self.client) | ||
|
|
||
| X_list = self.client.compute( | ||
| [X.iloc[:, i].drop_duplicates() for i in range(X.shape[1])], | ||
| sync=True, | ||
| ) | ||
| model._fit(X_list, unique=True) | ||
| self._set_internal_model(model) | ||
|
|
||
| return self | ||
|
|
||
|
|
@@ -130,18 +137,19 @@ def transform(self, X, delayed=True): | |
| out : Dask cuDF DataFrame or CuPy backed Dask Array | ||
| Distributed object containing the transformed input. | ||
| """ | ||
| output_collection_type = ( | ||
| "cupy" if self.kwargs.get("sparse_output", True) else self.datatype | ||
| ) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously this method always returned a cupy array, leading to some user issues. This now follows the same output type handling logic as the single GPU model - if |
||
| return self._transform( | ||
| X, | ||
| n_dims=2, | ||
| delayed=delayed, | ||
| output_dtype=self._get_internal_model().dtype, | ||
| output_collection_type="cupy", | ||
| output_collection_type=output_collection_type, | ||
| ) | ||
|
|
||
| def inverse_transform(self, X, delayed=True): | ||
| """Convert the data back to the original representation. In case unknown | ||
| categories are encountered (all zeros in the one-hot encoding), ``None`` is used | ||
| to represent this category. | ||
| """Convert the data back to the original representation. | ||
|
|
||
| Parameters | ||
| ---------- | ||
|
|
@@ -173,29 +181,30 @@ class OrdinalEncoder( | |
| ): | ||
| """Encode categorical features as an integer array. | ||
|
|
||
| The input to this transformer should be an :py:class:`dask_cudf.DataFrame` or a | ||
| :py:class:`dask.array.Array` backed by cupy, denoting the unique values taken on by | ||
| categorical (discrete) features. The features are converted to ordinal | ||
| integers. This results in a single column of integers (0 to n_categories - 1) per | ||
| feature. | ||
| The input to this transformer should be an array-like of integers or | ||
| strings, denoting the values taken on by categorical (discrete) features. | ||
| The features are converted to ordinal integers. This results in | ||
| a single column of integers (0 to n_categories - 1) per feature. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| categories : :py:class:`cupy.ndarray` or :py:class`cudf.DataFrameq, default='auto' | ||
| Categories (unique values) per feature. All categories are expected to | ||
| fit on one GPU. | ||
| categories : 'auto' or a list of array-like, default='auto' | ||
| Categories (unique values) per feature: | ||
|
|
||
| - 'auto' : Determine categories automatically from the training data. | ||
| - DataFrame/ndarray : ``categories[col]`` holds the categories expected | ||
| in the feature col. | ||
| - list : ``categories[i]`` holds the categories expected in the ith | ||
| column. | ||
|
|
||
| The used categories can be found in the ``categories_`` attribute. | ||
|
|
||
| dtype : number type, default=np.float64 | ||
| Desired dtype of output. | ||
|
|
||
| handle_unknown : {'error', 'ignore'}, default='error' | ||
| Whether to raise an error or ignore if an unknown categorical feature is | ||
| present during transform (default is to raise). When this parameter is set | ||
| to 'ignore' and an unknown category is encountered during transform, the | ||
| resulting encoded value would be null when output type is cudf | ||
| dataframe. | ||
| verbose : int or boolean, default=False | ||
| Sets logging level. It must be one of `cuml.common.logger.level_*`. See | ||
| :ref:`verbosity-levels` for more info. | ||
| When set to 'error' an error will be raised in case an unknown | ||
| categorical feature is present during transform. When set to 'ignore', | ||
| the encoded value of unknown categories will be set to NaN. In | ||
| :meth:`inverse_transform`, an unknown category will be denoted as None. | ||
| """ | ||
|
|
||
| def fit(self, X): | ||
|
|
@@ -211,14 +220,22 @@ def fit(self, X): | |
| ------- | ||
| self | ||
| """ | ||
| from cuml.preprocessing.ordinalencoder_mg import OrdinalEncoderMG | ||
| from cuml.preprocessing import OrdinalEncoder | ||
|
|
||
| el = first(X) if isinstance(X, Sequence) else X | ||
| self.datatype = ( | ||
| "cudf" if isinstance(el, (dcDataFrame, dcSeries)) else "cupy" | ||
| ) | ||
| model = OrdinalEncoder(**self.kwargs) | ||
|
|
||
| self._set_internal_model(OrdinalEncoderMG(**self.kwargs).fit(X)) | ||
| if isinstance(X, dask_cudf.DataFrame): | ||
| self.datatype = model._input_type = model.output_type = "cudf" | ||
| else: | ||
| self.datatype = model._input_type = model.output_type = "cupy" | ||
| X = to_dask_cudf(X, client=self.client) | ||
|
|
||
| X_list = self.client.compute( | ||
| [X.iloc[:, i].drop_duplicates() for i in range(X.shape[1])], | ||
| sync=True, | ||
| ) | ||
| model._fit(X_list, unique=True) | ||
| self._set_internal_model(model) | ||
|
|
||
| return self | ||
|
|
||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having a weird
*MGmodel that does some dask stuff, we now do all dask computations before forwarding to the normal single GPU model code path.