Skip to content

fix(compose): support object dtype in ColumnTransformer + SimpleImputer on cuDF - #8317

Open
nethum529 wants to merge 7 commits into
rapidsai:mainfrom
nethum529:fix/issue-6183-columntransformer-object-dtype
Open

fix(compose): support object dtype in ColumnTransformer + SimpleImputer on cuDF#8317
nethum529 wants to merge 7 commits into
rapidsai:mainfrom
nethum529:fix/issue-6183-columntransformer-object-dtype

Conversation

@nethum529

Copy link
Copy Markdown
Contributor

Summary

Fixes #6183.

ColumnTransformer + SimpleImputer crashed on a native cuDF DataFrame whenever categorical/string columns were involved (e.g. missing_values=pd.NA, strategy="constant"/"most_frequent" on object dtype data). impute-then-transform is one of the most common sklearn pipeline shapes, so this blocked a very common workflow.

Root cause: several layers of cuML unconditionally forced data through cupy/CumlArray regardless of dtype, but cupy has no way to represent Python object dtype (strings) at all. Fixed each layer to keep object-dtype data on host instead of crashing trying to move it to device:

  • check_array (cuml/internals/validation.py) unconditionally tried to move object-dtype input to device.
  • CumlArrayDescriptor._to_output (cuml/common/array_descriptor.py) and coerce_arrays (cuml/internals/outputs.py) — the output-coercion paths behind every @reflect-decorated method and fitted attribute — had the same device-forcing assumption.
  • ColumnTransformer._hstack (cuml/_thirdparty/sklearn/preprocessing/_column_transformer.py) used cupy's hstack unconditionally, which can't combine a numeric cupy block with an object-dtype host block.
  • _get_mask's sentinel-detection logic (cuml/thirdparty_adapters/adapters.py) crashed with a pd.NA truthiness error, since cuDF's null string values round-trip through to_numpy(dtype="object") as pd.NA (not None/nan). Also generalized _masked_column_mode to dispatch via cp.get_array_module instead of hardcoding cupy.
  • SimpleImputer._get_param_names (cuml/_thirdparty/sklearn/preprocessing/_imputation.py) omitted "missing_values" and "add_indicator", so sklearn.base.clone() (used by ColumnTransformer to clone each transformer before fitting) silently dropped a user-supplied missing_values sentinel back to the default np.nan. This looks like the closest match to the exact symptom in the original report.

Each fix targets only the previously-always-broken object-dtype path; numeric/device flows are untouched. Non-numpy extension dtypes (e.g. pandas/cudf category) are conservatively excluded from the new dtype checks rather than passed to np.dtype(), which would raise TypeError — they fall through to their own pre-existing (unrelated) failure mode.

Before / after

import cudf, pandas as pd
from cuml.compose import ColumnTransformer
from cuml.preprocessing import SimpleImputer

df = cudf.DataFrame({
    "num1": [1.0, None, 3.0],
    "cat1": ["a", None, "c"],
})
ct = ColumnTransformer([
    ("num", SimpleImputer(strategy="constant", fill_value=0), ["num1"]),
    ("cat", SimpleImputer(strategy="constant", fill_value="missing",
                           missing_values=pd.NA), ["cat1"]),
])
ct.fit_transform(df)
  • Before: ValueError: Unsupported dtype object (or, on older cuML/cuDF versions, the KeyError: 'dtype' / AttributeError: 'DataFrame' object has no attribute 'dtype' in the original report).
  • After: returns array([[1.0, 'a'], [0.0, 'missing'], [3.0, 'c']], dtype=object), matching scikit-learn's own ColumnTransformer output for the same input.

Test plan

  • Added test_column_transformer_simple_imputer_categorical_cudf in python/cuml/tests/test_compose.py, reproducing the exact issue shape (cuDF DataFrame, numeric constant-fill + categorical constant-fill + categorical most-frequent-fill columns) and asserting cuML's output matches scikit-learn's bit-for-bit.
  • Verified fails before the fix / passes after, against an installed cuml-cu12 wheel with this branch's Python sources layered on top (this environment has no compiled libcuml++, so CI is relied on for the full native build).
  • Ran the existing test_compose.py and test_preprocessing.py imputer/ColumnTransformer suites against the patched wheel — no regressions (same pre-existing, version-skew-related failure count as on unpatched code).
  • ruff check, ruff format --check, and isort --check pass on all non-_thirdparty changed files.

🤖 Generated with Claude Code

@nethum529
nethum529 requested a review from a team as a code owner July 3, 2026 22:54
@nethum529
nethum529 requested a review from viclafargue July 3, 2026 22:54
@copy-pr-bot

copy-pr-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the Cython / Python Cython or Python issue label Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved ColumnTransformer and SimpleImputer for object/string columns and missing-value sentinels, ensuring consistent results across host and GPU paths.
    • Kept object-dtype data on the host when device conversion isn’t supported.
    • Updated indicator and mask handling so imputations and concatenations work correctly for non-numeric “NaN-like” values.
    • Made SimpleImputer(add_indicator=...) properly reflected in parameters and compatible with cloning.
  • Tests

    • Added regression and unit coverage for object-dtype conversion, indicator behavior, sentinel handling, and parameter cloning parity with scikit-learn.

Walkthrough

Changes

Object-dtype and host data handling

Layer / File(s) Summary
Object-dtype detection and host preservation
python/cuml/cuml/internals/outputs.py, python/cuml/cuml/internals/validation.py, python/cuml/tests/test_validation.py
Object-dtype results are detected, preserved through conversion, and forced to host memory during validation.
SimpleImputer host/device dispatch
python/cuml/cuml/_thirdparty/sklearn/preprocessing/_imputation.py
Imputation fills, statistics, masks, indicator concatenation, and parameter discovery are updated for host, device, sparse, and object data.
NA-aware mask and mode handling
python/cuml/cuml/thirdparty_adapters/adapters.py, python/cuml/tests/test_adapters.py
Missing-value detection and masked statistics support NA-like sentinels across host/object and device inputs.
ColumnTransformer object-dtype stacking
python/cuml/cuml/_thirdparty/sklearn/preprocessing/_column_transformer.py
Non-sparse object-dtype blocks are moved to host memory and stacked with CPU NumPy.
Object-dtype regression coverage
python/cuml/tests/test_compose.py, python/cuml/tests/test_reflection.py
Tests cover scikit-learn parity, cloning, dtype detection, and object-array conversion behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: improvement, non-breaking

Suggested reviewers: divyegala, csadorf

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: object-dtype support in ColumnTransformer and SimpleImputer for cuDF.
Description check ✅ Passed The description is directly about the same fix and matches the changeset, with details, examples, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
python/cuml/cuml/common/array_descriptor.py (1)

76-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Object-dtype check misses DataFrame-like values (.dtypes, plural).

This only inspects .dtype, unlike the equivalent _is_object_dtype helper added in outputs.py, which also handles .dtypes for DataFrame-like values. Not a live bug today since attributes routed through this descriptor (e.g. statistics_) are always plain ndarrays, but if a future descriptor-backed attribute stores an object-dtype DataFrame, this would silently miss the bypass and fall through to CumlArray.from_input, which can't represent object dtype.

Consider reusing the _is_object_dtype helper from outputs.py here for consistency.

♻️ Suggested consolidation
-        input_value = existing.get_input_value()
-        input_dtype = getattr(input_value, "dtype", None)
-        if isinstance(input_dtype, np.dtype) and input_dtype.kind == "O":
-            return input_value
+        input_value = existing.get_input_value()
+        if _is_object_dtype(input_value):
+            return input_value
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuml/cuml/common/array_descriptor.py` around lines 76 - 87, The
object-dtype bypass in array_descriptor.py only checks input_value.dtype, so
DataFrame-like values exposing .dtypes can be missed and incorrectly fall
through to CumlArray.from_input. Update the descriptor logic in the
get_input_value path to reuse the same _is_object_dtype helper used in
outputs.py, or otherwise extend the check to cover both .dtype and .dtypes
consistently for existing.get_input_value(). Keep the existing early return
behavior for object dtype values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/cuml/cuml/_thirdparty/sklearn/preprocessing/_column_transformer.py`:
- Around line 970-979: The object-dtype guard in the column stacking logic does
not catch DataFrame outputs because it only checks X.dtype, so
string/categorical columns from passthrough or custom transformers can still
reach the device-side hstack path and fail. Update the stacking branch in the
_column_transformer helper to detect object dtypes from DataFrame-like outputs
via their per-column dtypes as well as array dtypes, and keep the existing
host-side cpu_np.hstack fallback in that case.

---

Nitpick comments:
In `@python/cuml/cuml/common/array_descriptor.py`:
- Around line 76-87: The object-dtype bypass in array_descriptor.py only checks
input_value.dtype, so DataFrame-like values exposing .dtypes can be missed and
incorrectly fall through to CumlArray.from_input. Update the descriptor logic in
the get_input_value path to reuse the same _is_object_dtype helper used in
outputs.py, or otherwise extend the check to cover both .dtype and .dtypes
consistently for existing.get_input_value(). Keep the existing early return
behavior for object dtype values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6b96728f-a66c-439b-aa0b-850e020cd68e

📥 Commits

Reviewing files that changed from the base of the PR and between 857cc5a and 8fb4988.

📒 Files selected for processing (7)
  • python/cuml/cuml/_thirdparty/sklearn/preprocessing/_column_transformer.py
  • python/cuml/cuml/_thirdparty/sklearn/preprocessing/_imputation.py
  • python/cuml/cuml/common/array_descriptor.py
  • python/cuml/cuml/internals/outputs.py
  • python/cuml/cuml/internals/validation.py
  • python/cuml/cuml/thirdparty_adapters/adapters.py
  • python/cuml/tests/test_compose.py

Comment thread python/cuml/cuml/_thirdparty/sklearn/preprocessing/_column_transformer.py Outdated

@viclafargue viclafargue left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for tackling this. The overall approach makes sense to me. Keeping object-dtype/string data on host is probably the right direction, and the new ColumnTransformer + SimpleImputer regression test covers the reported workflow nicely.

I found two remaining issues that I think should be fixed before merge:

Comment thread python/cuml/cuml/internals/outputs.py Outdated
if dtypes is not None:
# DataFrame-like: dtypes is per-column.
return any(
isinstance(dt, np.dtype) and dt.kind == "O" for dt in dtypes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_is_object_dtype treats any object with .dtypes as dataframe-like, but pd.Series.dtypes/cudf.Series.dtypes is a scalar dtype, not an iterable. This makes coerce_arrays(pd.Series(...), "pandas") fail before normal output coercion:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. _is_object_dtype now checks the scalar .dtype first, so a Series (whose .dtypes is a scalar dtype, not per-column) and extension-dtype Series (category/string/tz) return without hitting the iteration branch. The per-column .dtypes path now only runs for DataFrames (which have no .dtype) and is wrapped defensively. Added a unit test covering Series / extension-dtype / DataFrame / ndarray inputs.

"verbose",
"copy"
"copy",
"add_indicator",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add_indicator=True still goes through the dense _concatenate_indicator path, which unconditionally uses cupy.hstack because np is CuPy in this file. For object-dtype imputed data, that sends the host object array back to CuPy and reproduces the same failure this PR is trying to avoid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. _concatenate_indicator now detects object-dtype imputed or indicator data and stacks on host with numpy.hstack (moving any cupy operands to host via .get()), instead of routing the host object array through cupy.hstack. The sparse path and the numeric device hstack fast path are unchanged. Added a regression test with add_indicator=True on string columns comparing cuML against scikit-learn.

…er on cuDF

impute-then-transform is one of the most common sklearn pipeline shapes,
but ColumnTransformer + SimpleImputer crashed on a native cuDF DataFrame
whenever categorical/string columns were involved (e.g. missing_values=pd.NA,
strategy="constant"/"most_frequent" on object dtype data).

Root causes, all stemming from cuML forcing data through cupy/CumlArray
regardless of dtype, even though cupy has no way to represent Python
object dtype (strings) at all:

- check_array unconditionally tried to move object-dtype input to device.
- CumlArrayDescriptor._to_output and coerce_arrays (the output-coercion
  paths behind every `@reflect`-decorated method and fitted attribute)
  had the same device-forcing assumption.
- ColumnTransformer._hstack used cupy's hstack unconditionally, which
  can't combine a numeric cupy block with an object-dtype host block.
- _get_mask's sentinel-detection logic crashed with a pd.NA truthiness
  error, since cuDF's null string values round-trip through
  `to_numpy(dtype="object")` as pd.NA (not None/nan).
- SimpleImputer._get_param_names omitted "missing_values" and
  "add_indicator", so sklearn.base.clone() (used by ColumnTransformer
  to clone each transformer before fitting) silently dropped a
  user-supplied missing_values sentinel back to the default np.nan.

Each fix targets only the previously-always-broken object-dtype path;
numeric/device flows are untouched. Non-numpy extension dtypes (e.g.
pandas/cudf `category`) are conservatively excluded from the new dtype
checks rather than passed to `np.dtype()`, which would raise TypeError.

Adds a regression test reproducing the exact issue shape (cuDF DataFrame,
numeric + categorical constant-fill + categorical most-frequent-fill
columns) and asserting cuML's output matches scikit-learn's bit-for-bit.

Closes rapidsai#6183

Signed-off-by: nethum529 <nethumweerasinghe.nw@gmail.com>
- outputs._is_object_dtype: check scalar `.dtype` before per-column
  `.dtypes`, so extension-dtype Series (category/string/tz-datetime) no
  longer raise on iteration; DataFrame per-column detection is preserved.
- ColumnTransformer._hstack and CumlArrayDescriptor: reuse the shared
  `_is_object_dtype` helper so object-dtype DataFrame outputs are caught,
  not just array `.dtype`.
- SimpleImputer._concatenate_indicator: stack object-dtype (host) imputed
  data with the indicator mask on host instead of routing the host array
  through cupy.hstack, so add_indicator=True works for string columns.
- _get_mask: drop the try/except that silently defaulted to False and
  could disable NA masking; pd.isna is total over the scalar sentinels.
- De-duplicate: _imputation.py now imports the shared _is_object_dtype
  instead of a local copy.
- Tests: add regression coverage for add_indicator=True on object data
  and for _is_object_dtype on Series / extension-dtype / DataFrame inputs.

Signed-off-by: nethum529 <nethumweerasinghe.nw@gmail.com>
Signed-off-by: nethum529 <nethumweerasinghe.nw@gmail.com>
Signed-off-by: nethum529 <nethumweerasinghe.nw@gmail.com>
@nethum529
nethum529 force-pushed the fix/issue-6183-columntransformer-object-dtype branch from 6095331 to e841e5a Compare July 21, 2026 04:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
python/cuml/cuml/thirdparty_adapters/adapters.py (1)

17-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant first branch in _get_mask.

The isinstance(value_to_mask, str) and value_to_mask == "NaN" branch (lines 19-22) is a strict subset of what _is_na_sentinel(value_to_mask) (lines 27-32) already covers — _is_na_sentinel returns value == "NaN" for strings, so the string "NaN" case always falls into the first branch before the second is ever reached, and both branches execute identical logic (cp.isnan/pd.isna). This is now dead/duplicate code left over from the refactor.

♻️ Suggested simplification
 def _get_mask(X, value_to_mask):
     """Compute the boolean mask X == missing_values."""
-    if isinstance(value_to_mask, str) and value_to_mask == "NaN":
-        if isinstance(X, cp.ndarray):
-            return cp.isnan(X)
-        return pd.isna(X)
     # NaN-like sentinels (np.nan, None, pd.NA, pd.NaT, ...) require an
     # NA-aware comparison: a plain `==` against e.g. pd.NA propagates
     # instead of returning a boolean mask, and `cp.isnan` doesn't accept
     # non-numeric scalars like pd.NA in the first place.
     if _is_na_sentinel(value_to_mask):
         if isinstance(X, cp.ndarray):
             return cp.isnan(X)
         # Host (e.g. object dtype) arrays can't use isnan - fall back to an
         # NA-aware elementwise check that also recognizes None/pd.NA.
         return pd.isna(X)
     else:
         return X == value_to_mask
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuml/cuml/thirdparty_adapters/adapters.py` around lines 17 - 34,
Remove the redundant `"NaN"` string branch from `_get_mask` and route all
NA-like values through the existing `_is_na_sentinel(value_to_mask)` branch,
preserving its `cp.isnan` handling for CuPy arrays and `pd.isna` fallback for
host data.
python/cuml/cuml/internals/outputs.py (1)

530-628: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated reshape logic between object-dtype and cp.ndarray paths.

The series/dataframe ndim-reshaping logic (lines 543-558) and the one_col_2d_as_series host-frame construction (560-570) exactly mirror the equivalent block for cp.ndarray at lines 595-622. Any future fix/behavior change to one path risks silently diverging from the other.

♻️ Suggested extraction
+def _reshape_for_output(obj, output_type, one_col_2d_as_series):
+    if output_type == "series":
+        if obj.ndim == 2:
+            if obj.shape[1] == 1:
+                obj = obj.flatten()
+            else:
+                raise ValueError(
+                    "Only single dimensional arrays can be transformed to"
+                    " Series."
+                )
+        elif obj.ndim == 0:
+            obj = obj[None]
+    elif output_type == "dataframe":
+        if obj.ndim == 1:
+            obj = obj[:, None]
+        elif obj.ndim == 0:
+            obj = obj[None, None]
+    return obj

Then call this helper from both the object-dtype and cp.ndarray branches instead of duplicating the block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuml/cuml/internals/outputs.py` around lines 530 - 628, Extract the
shared series/dataframe reshaping and one_col_2d_as_series frame-construction
logic from the object-dtype and cp.ndarray branches into a helper, then call it
from both paths. Preserve the existing ndim validation, reshaping, index
handling, and host/device frame types while leaving each branch’s output
conversion behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@python/cuml/cuml/internals/outputs.py`:
- Around line 530-628: Extract the shared series/dataframe reshaping and
one_col_2d_as_series frame-construction logic from the object-dtype and
cp.ndarray branches into a helper, then call it from both paths. Preserve the
existing ndim validation, reshaping, index handling, and host/device frame types
while leaving each branch’s output conversion behavior unchanged.

In `@python/cuml/cuml/thirdparty_adapters/adapters.py`:
- Around line 17-34: Remove the redundant `"NaN"` string branch from `_get_mask`
and route all NA-like values through the existing
`_is_na_sentinel(value_to_mask)` branch, preserving its `cp.isnan` handling for
CuPy arrays and `pd.isna` fallback for host data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 423d5527-6bd5-4396-a200-e6c4bfe278c5

📥 Commits

Reviewing files that changed from the base of the PR and between 6095331 and e841e5a.

📒 Files selected for processing (9)
  • python/cuml/cuml/_thirdparty/sklearn/preprocessing/_column_transformer.py
  • python/cuml/cuml/_thirdparty/sklearn/preprocessing/_imputation.py
  • python/cuml/cuml/internals/outputs.py
  • python/cuml/cuml/internals/validation.py
  • python/cuml/cuml/thirdparty_adapters/adapters.py
  • python/cuml/tests/test_adapters.py
  • python/cuml/tests/test_compose.py
  • python/cuml/tests/test_reflection.py
  • python/cuml/tests/test_validation.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • python/cuml/cuml/internals/validation.py
  • python/cuml/cuml/_thirdparty/sklearn/preprocessing/_column_transformer.py
  • python/cuml/tests/test_compose.py
  • python/cuml/cuml/_thirdparty/sklearn/preprocessing/_imputation.py

@viclafargue

Copy link
Copy Markdown
Contributor

/ok to test e841e5a

Signed-off-by: nethum529 <nethumweerasinghe.nw@gmail.com>
Signed-off-by: nethum529 <nethumweerasinghe.nw@gmail.com>
@nethum529

Copy link
Copy Markdown
Contributor Author

The 42 failing jobs on the last run are not from this PR. The conda test jobs die during env solve (nvforest/librmm 26.10 nightlies pin cuda-version >=13, conflicting with the cuda 12.2 matrix, plus a rapids-xgboost pin with no viable option), and the wheel jobs die on import with a pylibraft/rmm nightly ABI mismatch (undefined symbol pool_memory_resource_impl). The same failure set shows on main's own build workflow and on unrelated PRs like #8402, so a rerun once the nightly channels heal should clear it.

Also pushed two small commits making convert_arrays raise TypeError for object dtype with output_type cupy/numba, matching the ClassLabels.to_output convention, with tests.

@viclafargue

Copy link
Copy Markdown
Contributor

/ok to test 51f0af8

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/cuml/cuml/internals/outputs.py (1)

59-83: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add stacklevel=2 to the deprecation warning.

Without an explicit stack level, users see outputs.py as the warning location instead of the call site, making the migration warning less actionable. As per coding guidelines, flake8 is required for Python changes; this triggers Ruff B028.

Proposed fix
         warnings.warn(
             f"`output_type={output_type!r}` was deprecated in version 26.08 "
             "and will be removed in version 26.10. Please use "
             f"`output_type={alt!r}` instead.{suffix}",
             FutureWarning,
+            stacklevel=2,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuml/cuml/internals/outputs.py` around lines 59 - 83, Update the
warnings.warn call in warn_if_output_type_deprecated to pass stacklevel=2,
ensuring the deprecation warning points to the caller’s call site while
preserving the existing warning message and category.

Sources: Coding guidelines, Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@python/cuml/cuml/internals/outputs.py`:
- Around line 59-83: Update the warnings.warn call in
warn_if_output_type_deprecated to pass stacklevel=2, ensuring the deprecation
warning points to the caller’s call site while preserving the existing warning
message and category.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b7cdade8-0c73-4142-8d9f-33049a82caee

📥 Commits

Reviewing files that changed from the base of the PR and between c9bab09 and 51f0af8.

📒 Files selected for processing (3)
  • python/cuml/cuml/internals/outputs.py
  • python/cuml/tests/test_compose.py
  • python/cuml/tests/test_reflection.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/cuml/tests/test_compose.py
  • python/cuml/tests/test_reflection.py

# NA-aware comparison: a plain `==` against e.g. pd.NA propagates
# instead of returning a boolean mask, and `cp.isnan` doesn't accept
# non-numeric scalars like pd.NA in the first place.
if _is_na_sentinel(value_to_mask):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pd.isna(value_to_mask) conflates None, np.nan, and pd.NA, so each sentinel masks all NA-like values. This can impute values the user did not select. Please special-case pd.NA; preserve distinct handling for None and np.nan.

values, counts = xp.unique(
arr[feature_mask_idxs, i], return_counts=True
)
count_max = counts.max()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If a column is entirely missing, counts is empty and counts.max() raises. Please handle counts.size == 0 by storing NaN, so SimpleImputer can drop that column like sklearn.

# crashing when attempting the device conversion below. This is required
# for estimators (e.g. SimpleImputer) that explicitly support categorical
# / string data with an object dtype.
if isinstance(dtype, np.dtype) and dtype.kind == "O":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This silently changes an explicit mem_type="device" request to host output, violating check_array’s documented return-type contract. Could this host fallback be scoped to the imputer path or made an explicit opt-in?

if isinstance(host_df, pd.DataFrame):
# cudf cannot represent every mixed object DataFrame.
# Preserve the host DataFrame instead of losing its data.
return host_df

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This can return a pandas DataFrame even when output_type="cudf" was explicitly requested. Please raise a clear error for unsupported object layouts instead of silently returning a different output type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Cython / Python Cython or Python issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ColumnTransformer with SimpleImputer fails on DataFrame input with “DataFrame object has no attribute dtype”

3 participants