-
Notifications
You must be signed in to change notification settings - Fork 662
fix(compose): support object dtype in ColumnTransformer + SimpleImputer on cuDF #8317
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
|
|
||
| import cupy as np | ||
| import numpy as cpu_np | ||
| import pandas as pd | ||
| from cupyx.scipy import sparse | ||
|
|
||
| import cuml | ||
|
|
@@ -29,11 +30,11 @@ | |
| StringInputTagMixin, | ||
| _ensure_transformer_tags, | ||
| ) | ||
| from cuml.internals.outputs import mlfunc, ReflectedAttr | ||
| from cuml.internals.outputs import ReflectedAttr, _is_object_dtype, mlfunc | ||
| from cuml.internals.validation import ( | ||
| check_is_fitted, | ||
| check_inputs, | ||
| check_input_features, | ||
| check_inputs, | ||
| check_is_fitted, | ||
| ) | ||
|
|
||
| from ....thirdparty_adapters import ( | ||
|
|
@@ -144,15 +145,24 @@ def _concatenate_indicator(self, X_imputed, X_indicator): | |
| if not self.add_indicator: | ||
| return X_imputed | ||
|
|
||
| hstack = sparse.hstack if sparse.issparse(X_imputed) else np.hstack | ||
| if X_indicator is None: | ||
| raise ValueError( | ||
| "Data from the missing indicator are not provided. Call " | ||
| "_fit_indicator and _transform_indicator in the imputer " | ||
| "implementation." | ||
| ) | ||
|
|
||
| return hstack((X_imputed, X_indicator)) | ||
| if sparse.issparse(X_imputed): | ||
| return sparse.hstack((X_imputed, X_indicator)) | ||
|
|
||
| if _is_object_dtype(X_imputed) or _is_object_dtype(X_indicator): | ||
| arrays = [ | ||
| array.get() if isinstance(array, np.ndarray) else array | ||
| for array in (X_imputed, X_indicator) | ||
| ] | ||
| return cpu_np.hstack(arrays) | ||
|
|
||
| return np.hstack((X_imputed, X_indicator)) | ||
|
|
||
| def __sklearn_tags__(self): | ||
| tags = super().__sklearn_tags__() | ||
|
|
@@ -264,10 +274,12 @@ def __init__(self, *, missing_values=np.nan, strategy="mean", | |
| @classmethod | ||
| def _get_param_names(cls): | ||
| return super()._get_param_names() + [ | ||
| "missing_values", | ||
| "strategy", | ||
| "fill_value", | ||
| "verbose", | ||
| "copy" | ||
| "copy", | ||
| "add_indicator", | ||
|
Contributor
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.
Contributor
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. Fixed. |
||
| ] | ||
|
|
||
| def _validate_input(self, X, in_fit): | ||
|
|
@@ -288,13 +300,15 @@ def _validate_input(self, X, in_fit): | |
| ensure_all_finite = "allow-nan" | ||
|
|
||
| try: | ||
| mem_type = "host" if _is_object_dtype(X) else "device" | ||
| X = check_inputs( | ||
| self, | ||
| X, | ||
| accept_sparse="csc", | ||
| dtype=dtype, | ||
| ensure_all_finite=ensure_all_finite, | ||
| copy=self.copy, | ||
| mem_type=mem_type, | ||
| reset=in_fit, | ||
| ) | ||
| except ValueError as ve: | ||
|
|
@@ -305,6 +319,18 @@ def _validate_input(self, X, in_fit): | |
| else: | ||
| raise ve | ||
|
|
||
| # Object nulls reach this host path as NaN. Restore pd.NA for its | ||
| # identity-based path, but reject NaN when None was requested, | ||
| # matching scikit-learn's validation semantics. | ||
| if _is_object_dtype(X): | ||
| if hasattr(X, "flags") and not X.flags.writeable: | ||
| X = X.copy() | ||
| nan_mask = _get_mask(X, np.nan) | ||
| if self.missing_values is pd.NA: | ||
| X[nan_mask] = pd.NA | ||
| elif self.missing_values is None and nan_mask.any(): | ||
| raise ValueError("Input contains NaN") | ||
|
|
||
| _check_inputs_dtype(X, self.missing_values) | ||
| if X.dtype.kind not in ("i", "u", "f", "O"): | ||
| raise ValueError("SimpleImputer does not support data with dtype " | ||
|
|
@@ -427,7 +453,8 @@ def _dense_fit(self, X, strategy, missing_values, fill_value): | |
|
|
||
| # Constant | ||
| elif strategy == "constant": | ||
| return np.full(X.shape[1], fill_value, dtype=X.dtype) | ||
| xp = np.get_array_module(X) | ||
| return xp.full(X.shape[1], fill_value, dtype=X.dtype) | ||
|
|
||
| @mlfunc | ||
| def transform(self, X): | ||
|
|
@@ -443,24 +470,27 @@ def transform(self, X): | |
| X = self._validate_input(X, in_fit=False) | ||
| X_indicator = super()._transform_indicator(X) | ||
|
|
||
| statistics = self.statistics_ | ||
| # Use the stored value for internal computation. ColumnTransformer | ||
| # may request cupy output, which cannot reflect object statistics. | ||
| statistics = type(self).statistics_.get_raw(self) | ||
|
|
||
| if X.shape[1] != statistics.shape[0]: | ||
| raise ValueError("X has %d features per sample, expected %d" | ||
| % (X.shape[1], self.statistics_.shape[0])) | ||
| % (X.shape[1], statistics.shape[0])) | ||
|
|
||
| # Delete the invalid columns if strategy is not constant | ||
| if self.strategy == "constant": | ||
| valid_statistics = statistics | ||
| else: | ||
| xp = np.get_array_module(statistics) | ||
| # same as np.isnan but also works for object dtypes | ||
| invalid_mask = _get_mask(statistics, np.nan) | ||
| valid_mask = np.logical_not(invalid_mask) | ||
| valid_mask = xp.logical_not(invalid_mask) | ||
| valid_statistics = statistics[valid_mask] | ||
| valid_statistics_indexes = np.flatnonzero(valid_mask) | ||
| valid_statistics_indexes = xp.flatnonzero(valid_mask) | ||
|
|
||
| if invalid_mask.any(): | ||
| missing = np.arange(X.shape[1])[invalid_mask] | ||
| missing = xp.arange(X.shape[1])[invalid_mask] | ||
| if self.verbose: | ||
| warnings.warn("Deleting features without " | ||
| "observed values: %s" % missing) | ||
|
|
@@ -485,9 +515,10 @@ def transform(self, X): | |
| if self.strategy == "constant": | ||
| X[mask] = valid_statistics[0] | ||
| else: | ||
| for i, vi in enumerate(valid_statistics_indexes): | ||
| feature_idxs = np.flatnonzero(mask[:, vi]) | ||
| X[feature_idxs, vi] = valid_statistics[i] | ||
| xp = np.get_array_module(mask) | ||
| for i in range(valid_statistics.shape[0]): | ||
| feature_idxs = xp.flatnonzero(mask[:, i]) | ||
| X[feature_idxs, i] = valid_statistics[i] | ||
|
|
||
| X = super()._concatenate_indicator(X, X_indicator) | ||
| return X | ||
|
|
@@ -508,7 +539,11 @@ def get_feature_names_out(self, input_features=None): | |
| """ | ||
| check_is_fitted(self) | ||
| input_features = check_input_features(self, input_features) | ||
| non_missing_mask = np.logical_not(_get_mask(self.statistics_, np.nan)).get() | ||
| statistics = type(self).statistics_.get_raw(self) | ||
| xp = np.get_array_module(statistics) | ||
| non_missing_mask = xp.logical_not(_get_mask(statistics, np.nan)) | ||
| if isinstance(non_missing_mask, np.ndarray): | ||
| non_missing_mask = non_missing_mask.get() | ||
| names = input_features[non_missing_mask] | ||
| if self.add_indicator: | ||
| indicator_names = self.indicator_.get_feature_names_out(input_features) | ||
|
|
@@ -657,9 +692,11 @@ def _get_missing_features_info(self, X): | |
| imputer_mask = sparse.csc_matrix(imputer_mask) | ||
|
|
||
| if self.features == 'all': | ||
| features_indices = np.arange(X.shape[1]) | ||
| xp = np.get_array_module(imputer_mask) | ||
| features_indices = xp.arange(X.shape[1]) | ||
| else: | ||
| features_indices = np.flatnonzero(n_missing) | ||
| xp = np.get_array_module(n_missing) | ||
| features_indices = xp.flatnonzero(n_missing) | ||
|
|
||
| return imputer_mask, features_indices | ||
|
|
||
|
|
@@ -668,11 +705,13 @@ def _validate_input(self, X, in_fit): | |
| ensure_all_finite = True | ||
| else: | ||
| ensure_all_finite = "allow-nan" | ||
| mem_type = "host" if _is_object_dtype(X) else "device" | ||
| X = check_inputs( | ||
| self, | ||
| X, | ||
| accept_sparse=('csc', 'csr'), | ||
| ensure_all_finite=ensure_all_finite, | ||
| mem_type=mem_type, | ||
| reset=in_fit, | ||
| ) | ||
| _check_inputs_dtype(X, self.missing_values) | ||
|
|
@@ -802,10 +841,13 @@ def get_feature_names_out(self, input_features=None): | |
| check_is_fitted(self) | ||
| input_features = check_input_features(self, input_features) | ||
| prefix = self.__class__.__name__.lower() | ||
| features = type(self).features_.get_raw(self) | ||
| if isinstance(features, np.ndarray): | ||
| features = features.get() | ||
| return cpu_np.asarray( | ||
| [ | ||
| f"{prefix}_{feature_name}" | ||
| for feature_name in input_features[self.features_.get()] | ||
| for feature_name in input_features[features] | ||
| ], | ||
| dtype=object, | ||
| ) | ||
|
|
||