Skip to content

Cleanup LabelEncoder - #8039

Merged
rapids-bot[bot] merged 12 commits into
NVIDIA:mainfrom
jcrist:cleanup-label-encoder
May 8, 2026
Merged

Cleanup LabelEncoder#8039
rapids-bot[bot] merged 12 commits into
NVIDIA:mainfrom
jcrist:cleanup-label-encoder

Conversation

@jcrist

@jcrist jcrist commented May 1, 2026

Copy link
Copy Markdown
Member

This cleans up our LabelEncoder implementation, preparing for exposing it through cuml.accel. This one needed a lot of work, and there are several breaking changes.

  • The type of LabelEncoder.classes_ is now a numpy.ndarray instead of a cudf.Series. Note that classes_ was undocumented. This mirrors the classes_ attribute in all our other classifiers (all of which also made this as a breaking change many releases ago). I view this in the same vein.
  • Likewise, since OneHotEncoder and OrdinalEncoder make use of LabelEncoder internally, the type of categories_ has changed there from list[cudf.Series] to list[numpy.ndarray]. This now better matches with sklearn, and makes sense IMO in the same way that changing classes_ above did.
  • LabelEncoder now follows our type-reflection standards, previously it always returned cudf objects (even though the docstrings implied type-reflection was followed). I view this as a bugfix.
  • LabelEncoder.inverse_transform now handles handle_unknown="ignore" as documented. Previously this would return incorrect values in the presence of missing values. Note that handle_unknown is not an argument to sklearn.preprocessing.LabelEncoder. I view this as a bugfix.

This required munging in our tests and the implementations of OrdinalEncoder/OneHotEncoder (and their dask counterparts), but beyond the changes listed above should have no other user-facing ramifications.

Stacked on top of #8038.
Part of #7317.
Precursor for #8015.

@jcrist jcrist self-assigned this May 1, 2026
@jcrist jcrist added improvement Improvement / enhancement to an existing function breaking Breaking change labels May 1, 2026
@copy-pr-bot

copy-pr-bot Bot commented May 1, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the Cython / Python Cython or Python issue label May 1, 2026
@jcrist

jcrist commented May 1, 2026

Copy link
Copy Markdown
Member Author

/ok to test d27e04a

@jcrist

jcrist commented May 1, 2026

Copy link
Copy Markdown
Member Author

/ok to test 9afcae6

@jcrist

jcrist commented May 1, 2026

Copy link
Copy Markdown
Member Author

/ok to test 7aa2c69

@jcrist
jcrist force-pushed the cleanup-label-encoder branch from 7aa2c69 to a3b6a0d Compare May 1, 2026 19:25
@jcrist

jcrist commented May 1, 2026

Copy link
Copy Markdown
Member Author

/ok to test a3b6a0d

@jcrist
jcrist force-pushed the cleanup-label-encoder branch from a3b6a0d to d7e6490 Compare May 4, 2026 21:46
@jcrist
jcrist marked this pull request as ready for review May 4, 2026 21:47
@jcrist
jcrist requested a review from a team as a code owner May 4, 2026 21:47
@jcrist
jcrist requested a review from dantegd May 4, 2026 21:47
@coderabbitai

coderabbitai Bot commented May 4, 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

Walkthrough

Label-encoding and encoder internals refactored to validate inputs, preserve and propagate index, force CUDF internal execution for per-feature encoders, and update unseen-label handling to raise ValueError; decode_labels signature extended and NumPy object coercion adjusted. Tests and dask wiring updated accordingly.

Changes

Label Encoding Refactor

Layer / File(s) Summary
API Enhancement
python/cuml/cuml/common/classification.py
decode_labels adds optional index parameter and attaches it to returned CumlArray, cudf.DataFrame, or cudf.Series. When coercing to NumPy and requested dtype=="object", to_numpy(dtype=None) is used.
Core Implementation
python/cuml/cuml/preprocessing/_label.py
LabelEncoder reimplemented to use check_y/check_cudf/decode_labels. fit/fit_transform/transform/inverse_transform now preserve input index via CumlArray(..., index=index), validate inputs with helpers, and raise ValueError for unseen labels when handle_unknown="error". Added classes_ attribute and updated estimator metadata (_get_param_names, __sklearn_is_fitted__, _more_static_tags).
Encoder Integration
python/cuml/cuml/preprocessing/encoders.py
BaseEncoder._fit constructs per-feature LabelEncoder(output_type="cudf"). OneHotEncoder and OrdinalEncoder transform/fit/inverse paths run under internal CUDF context (@run_in_internal_context, cuml.using_output_type("cudf")). Encoded index dtypes upcast via np.min_scalar_type(max_value); dropped-category mask and feature-name iteration logic simplified.
Dask Integration
python/cuml/cuml/dask/preprocessing/_label.py
Imports simplified; LabelEncoder.fit computes y.unique().compute(), fits LE(output_type="cudf") on uniques, and unconditionally sets self.datatype = "cudf". Removed prior sorting/type-inference branch.
Validation Docs
python/cuml/cuml/internals/validation.py
Clarified check_cudf docstring for ensure_ndim describing returned container types for 1, 2, and None.
Tests / Call Sites
python/cuml/tests/*, python/cuml/tests/dask/*
Tests updated to expect ValueError with message matching "y contains previously unseen labels" instead of KeyError. Tests adapted for CUDF-backed outputs (LabelEncoder(output_type="cudf")), comparing classes_ and category codes with np.testing.assert_array_equal and relaxing/adjusting dtype expectations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • rapidsai/cuml#8056: Modifies decode_labels to accept and propagate an optional index parameter (very closely related).
  • rapidsai/cuml#8051: Also modifies decode_labels to accept/propagate an index argument (directly related).
  • rapidsai/cuml#7830: Changes encoder category/dtype handling and drop/index mask logic (overlaps with encoders.py updates).

Suggested reviewers

  • viclafargue
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.43% 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 'Cleanup LabelEncoder' clearly and concisely summarizes the main objective of the PR, which is a comprehensive refactoring of the LabelEncoder implementation.
Description check ✅ Passed The description is directly related to the changeset, providing clear details about breaking changes, bug fixes, and the scope of affected components (LabelEncoder, OneHotEncoder, OrdinalEncoder, and their tests).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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

Caution

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

⚠️ Outside diff range comments (2)
python/cuml/cuml/dask/preprocessing/_label.py (1)

166-170: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Docstring states KeyError but implementation now raises ValueError.

The docstring mentions raising KeyError for unseen categories, but the underlying LabelEncoder.transform now raises ValueError. Update the docstring to match the new behavior.

📝 Proposed fix
         Raises
         ------
-        KeyError
+        ValueError
             if a category appears that was not seen in `fit`
🤖 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/dask/preprocessing/_label.py` around lines 166 - 170, Update
the docstring for the method/class that documents raising on unseen categories
to reflect the actual exception thrown: replace the "KeyError" mention with
"ValueError" (or state that LabelEncoder.transform raises ValueError for unseen
categories) so the docstring matches the implementation (see
LabelEncoder.transform in _label.py).
python/cuml/tests/test_ordinal_encoder.py (1)

119-121: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a real assertion for the array unknown-path check.

Line 120 computes np.isnan(encoded[0, 0]) but does not assert it, so the as_array=True branch can pass even when behavior is wrong.

Proposed fix
     encoded = enc.transform(Y)
     if as_array:
-        np.isnan(encoded[0, 0])
+        assert np.isnan(encoded[0, 0])
     else:
         assert pd.isna(encoded.iloc[0, 0])

As per coding guidelines: python/**/test_*.py: “Test files must ... test different input types (cuDF, pandas, NumPy)”.

🤖 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/tests/test_ordinal_encoder.py` around lines 119 - 121, The test's
unknown-value path for NumPy arrays currently computes np.isnan(encoded[0, 0])
but doesn't assert it; update the branch guarded by as_array to assert that
np.isnan(encoded[0, 0]) is True (or use np.testing.assert_true/np.assert to
explicitly check NaN) so the as_array=True path fails when encoding is
incorrect; locate the as_array conditional and the variable encoded and replace
the bare expression with an explicit assertion using np.isnan(encoded[0, 0]).
🤖 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/preprocessing/_label.py`:
- Line 208: Replace the unsupported np.concat call with np.concatenate to
maintain compatibility with numpy>=1.23; specifically, update the expression
using the classes variable (currently written as np.concat([classes, [None]]))
to np.concatenate([classes, [None]]) in the preprocessing label code (look for
the classes variable assignment in _label.py) so the code works across NumPy
1.23–2.x.

---

Outside diff comments:
In `@python/cuml/cuml/dask/preprocessing/_label.py`:
- Around line 166-170: Update the docstring for the method/class that documents
raising on unseen categories to reflect the actual exception thrown: replace the
"KeyError" mention with "ValueError" (or state that LabelEncoder.transform
raises ValueError for unseen categories) so the docstring matches the
implementation (see LabelEncoder.transform in _label.py).

In `@python/cuml/tests/test_ordinal_encoder.py`:
- Around line 119-121: The test's unknown-value path for NumPy arrays currently
computes np.isnan(encoded[0, 0]) but doesn't assert it; update the branch
guarded by as_array to assert that np.isnan(encoded[0, 0]) is True (or use
np.testing.assert_true/np.assert to explicitly check NaN) so the as_array=True
path fails when encoding is incorrect; locate the as_array conditional and the
variable encoded and replace the bare expression with an explicit assertion
using np.isnan(encoded[0, 0]).
🪄 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: 74302a2f-f268-4f90-b700-eaf0a07dda4e

📥 Commits

Reviewing files that changed from the base of the PR and between b932c49 and d7e6490.

📒 Files selected for processing (11)
  • python/cuml/cuml/common/classification.py
  • python/cuml/cuml/dask/preprocessing/_label.py
  • python/cuml/cuml/internals/validation.py
  • python/cuml/cuml/preprocessing/_label.py
  • python/cuml/cuml/preprocessing/encoders.py
  • python/cuml/tests/dask/test_dask_label_encoder.py
  • python/cuml/tests/dask/test_dask_one_hot_encoder.py
  • python/cuml/tests/dask/test_dask_ordinal_encoder.py
  • python/cuml/tests/test_label_encoder.py
  • python/cuml/tests/test_one_hot_encoder.py
  • python/cuml/tests/test_ordinal_encoder.py

Comment thread python/cuml/cuml/preprocessing/_label.py Outdated

@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

🤖 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/preprocessing/_label.py`:
- Around line 188-213: The inverse transform unpacks index from check_y but
doesn't pass it to decode_labels, causing loss of the input index on
inverse_transform; update the call site (the decode_labels invocation in the
method containing check_y and codes handling) to pass the unpacked index (i.e.,
call decode_labels with the index=index argument along with codes, classes, and
output_type) so inverse_transform preserves the original index like
fit_transform and transform do.
🪄 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: 7defa29c-539e-4873-8cf0-be57af5f7607

📥 Commits

Reviewing files that changed from the base of the PR and between d7e6490 and e73e8dc.

📒 Files selected for processing (1)
  • python/cuml/cuml/preprocessing/_label.py

Comment thread python/cuml/cuml/preprocessing/_label.py Outdated
@jcrist
jcrist force-pushed the cleanup-label-encoder branch from f39444c to 1fdca94 Compare May 4, 2026 22:16
rapids-bot Bot pushed a commit that referenced this pull request May 6, 2026
When returning pandas/cudf outputs, we want the index of the output to be aligned with the index of the input. This PR plumbs through `index` in `decode_labels`, adds a test for all classifiers, then applies the necessary fixes and plumbing so the output has an aligned index.

In the long run I hope to move index alignment handling to the `reflect` decorator so method code doesn't need to worry about this, but for now this is the most idiomatic and cleanest solution.

Split out and expanded from #8039.

Authors:
  - Jim Crist-Harif (https://github.com/jcrist)

Approvers:
  - Simon Adorf (https://github.com/csadorf)

URL: #8056
@jcrist
jcrist force-pushed the cleanup-label-encoder branch from 1fdca94 to e573a0b Compare May 6, 2026 19:10
Comment thread python/cuml/cuml/common/classification.py
Comment thread python/cuml/cuml/preprocessing/_label.py Outdated
jcrist added 6 commits May 7, 2026 18:02
- Simplifies internals
- Applies standard type reflection
- Moves `classes_` to be a numpy array instead of a cudf object
- Cleans up docstrings
- Improves reliability across input and output types
Other encoders and tests needed some updating after the fallout of the
`LabelEncoder` changes. This is mostly cosmetic and not user-facing.
`OneHotEncoder` and `OrdinalEncoder` don't use our standard reflection
machinery, which is annoying. We have to use the context manager in all
method-local call sites to ensure things properly work and aren't
overridden - if these classes used `reflect` like a good citizen then we
wouldn't have this problem.
@jcrist
jcrist force-pushed the cleanup-label-encoder branch from e573a0b to 170805f Compare May 7, 2026 23:17

@csadorf csadorf 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.

Not sure about the failures, but my concerns have been addressed.

@jcrist

jcrist commented May 8, 2026

Copy link
Copy Markdown
Member Author

The failures are on main as well, due to recent upstream change to the kmeans implementation in cuvs: NVIDIA/cuvs#2015. 4 new tests passing, and the new failure in 2 runs is a parameter combo of a test already marked as flaky. I have no reason to suspect a serious regression, so just updated the xfail-list here to get things passing.

@csadorf

csadorf commented May 8, 2026

Copy link
Copy Markdown
Contributor

Created #8068 as an independent fix.

@csadorf

csadorf commented May 8, 2026

Copy link
Copy Markdown
Contributor

/merge

@rapids-bot
rapids-bot Bot merged commit 6600848 into NVIDIA:main May 8, 2026
93 checks passed
@jcrist
jcrist deleted the cleanup-label-encoder branch May 8, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking change Cython / Python Cython or Python issue improvement Improvement / enhancement to an existing function

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants