Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ENH] ExtraTree: Add random splitting via deterministic hash to support up to 2**24 possible categories - #33972

Merged
ogrisel merged 22 commits into
scikit-learn:mainfrom
adam2392:feat/rand-cat
Aug 25, 2026
Merged

[ENH] ExtraTree: Add random splitting via deterministic hash to support up to 2**24 possible categories#33972
ogrisel merged 22 commits into
scikit-learn:mainfrom
adam2392:feat/rand-cat

Conversation

@adam2392

@adam2392 adam2392 commented May 11, 2026

Copy link
Copy Markdown
Member

Reference Issues/PRs

Follow-up to #33354 (review)

Towards: #33965

What does this implement/fix? Explain your changes.

AI usage disclosure

Used AI to find the random splitting algorithm mix uint32 function, and then help implement.

Any other comments?

Open questions:

  1. Within random splitter, allow >256 categories when random splitting? -> yes, we allow up to 2^24-1
  2. Within "best" splitter, allow random splits to be there when certain features exceed the total categories? This would enable pseudo-support for categorical variables with high cardinality -> yes, but in downstream PR
  3. Within random splitter, should we allow categorical features to evaluate many random categorical splits, or just pick one completely at random? -> no, doesn't make sense
    • right now, it's evaluate MAX_RANDOM_CATEGORICAL_SPLIT_ATTEMPTS

@cakedev0

Copy link
Copy Markdown
Contributor

Note: if you merge main/rebase, I'll be happy to take a look at the diff. The idea (deterministic hash with random seed) sounds fun and promising!

@adam2392

Copy link
Copy Markdown
Member Author

Will do. Was planning on revisiting these PRs this weekend. I can probably finish this off since it's close. Are you looking at other features in the list of #33965 ?

@cakedev0

Copy link
Copy Markdown
Contributor

Are you looking at other features in the list of #33965 ?

I commented there. But I won't have the bandwidth to really work on trees for the next few weeks.

@github-actions github-actions Bot added the CI:Linter failure The linter CI is failing on this PR label Jul 25, 2026
@github-actions github-actions Bot removed the CI:Linter failure The linter CI is failing on this PR label Jul 25, 2026
@adam2392
adam2392 marked this pull request as ready for review July 25, 2026 12:52
@adam2392

adam2392 commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

This is ready for an initial review if you want to take a quick look, and have any opinions on the open questions @cakedev0 and @lorentzenchr ?

cc: @adrinjalali and @thomasjpfan if you have any quick opinions

Afterwards, I can iterate and then this will be completely ready for review.

Comment thread doc/whats_new/upcoming_changes/sklearn.tree/33354.major-feature.rst

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

I started reviewing without having read your "open questions" 😅

My answers:

  1. Yes! Let's allow >256 categories when random splitting (=> some changes to do in _classes.py
  2. Let's not do it for now, I'll explain why in a subsequent comment.
  3. just pick one completely at random! (see my reasons in a comment of this review)

I'm really excited by this PR! It's fairly simple but so powerful! 🚀

I have a couple of minor/easy-to-resolve comments plus a critical one about MAX_RANDOM_CATEGORICAL_SPLIT_ATTEMPTS.

Comment thread sklearn/tree/_partitioner.pyx Outdated
Comment thread sklearn/tree/_utils.pxd Outdated
Comment thread sklearn/tree/_splitter.pxd Outdated
Comment thread sklearn/tree/_tree.pyx Outdated
Comment thread sklearn/tree/_splitter.pyx Outdated
Comment thread doc/whats_new/upcoming_changes/sklearn.tree/33972.major-feature.rst Outdated
Comment thread sklearn/tree/_splitter.pxd Outdated
Comment thread sklearn/tree/_tree.pyx Outdated
@cakedev0

Copy link
Copy Markdown
Contributor

Re 2 (random splits "best" splitter):

I don't think we should do it for now, as it will just be too slow: 256 x n_node_samples...

But let's revisit this idea once we have histogram-based splitting: this would allow adapting the same idea to get a 256 x n_categories complexity. Or let's try it in HGB first.
I do think it's a promising idea, even for categories with a cardinality smaller than 256: it allows a kind of "regularized approximated best split" (regularized because you don't select the best out of $2^{n_c}$ possible splits).

And we should clearly not be done it in this PR:

  • it's easy to put in a separate PR
  • I think it's quite a big change, as it requires some random splitter logic to be put into the best splitter.
  • it's a big decision, that changes the contract exposed to users ("best" is not exactly best anymore)
  • it needs some dedicated perf measures

@cakedev0 cakedev0 moved this to In progress in Labs Jul 27, 2026
@cakedev0 cakedev0 added this to Labs Jul 27, 2026
@adam2392

Copy link
Copy Markdown
Member Author

This was all started with @adrinjalali original work.

I’ll wait to see if @lorentzenchr , @thomasjpfan, @adrinjalali or perhaps @ogrisel want to take a second look.

IIRC, I think we still need one more maintainer review for this non trivial change(?) but lmk if me and Arthur are sufficient 😅

@ogrisel ogrisel moved this from In progress to PR waiting for reviews in Labs Aug 3, 2026

@ogrisel ogrisel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR. Here is a first pass of review.

We will need a follow-up PR to add the categorical_features argument to ExtraTrees ensemble classes.

Comment thread sklearn/tree/tests/test_split.py Outdated


def random_categorical_goes_left(seed, x):
return np.array([_py_mix_uint32(seed ^ int(category)) & 1 for category in x])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add an inline comment to explain the maths behind this line of code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, let's rename category to category_idx to make it explicit that we expected the ordinal representation of categorical values.

# - for SPLIT_CATEGORICAL_BITSET: left_cat_bitset stores the set of
# categories that go to the left child;
# - for SPLIT_CATEGORICAL_HASH: left_cat_bitset[0] stores the hash seed.
BITSET_DTYPE_C left_cat_bitset

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we rename this field to left_cat_bitset_or_hashseed?

Note that since we have to also store a new split_kind field, pickling backward compat is broken between scikit-learn versions anyways.

Comment thread sklearn/tree/tests/test_split.py
Comment thread sklearn/tree/tests/test_split.py
Comment thread sklearn/tree/_utils.pxd
Comment thread sklearn/tree/_splitter.pyx Outdated
n_categories=int(tree.tree_._n_categories[ftr]),
)
elif split_kind == SPLIT_CATEGORICAL_HASH:
threshold = int(tree.tree_._left_cat_bitset[0, 0])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand this line. Maybe an inline comment could help improve readability.

@ogrisel

ogrisel commented Aug 4, 2026

Copy link
Copy Markdown
Member

This PR also need some conflict resolution and a changelog entry.

@github-actions github-actions Bot added the CI:Linter failure The linter CI is failing on this PR label Aug 4, 2026
@github-actions github-actions Bot removed the CI:Linter failure The linter CI is failing on this PR label Aug 4, 2026
@adam2392

adam2392 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review!

This PR also need some conflict resolution and a changelog entry.

Resolved conflicts, and the changelog entry error I don't understand. Perhaps it's because I changed an older changelog entry? I added one for this PR it seems

@adam2392 adam2392 left a comment

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.

I added a changelog entry in doc/whats_new/upcoming_changes/sklearn.tree/33972.major-feature.rst. Unsure why the CI is still erroring.

Comment thread sklearn/tree/tests/test_split.py
Comment thread sklearn/tree/_utils.pxd
@cakedev0

cakedev0 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The retry strategy looks good 👍 (much cleaner to implement than what I thought, nice!)

@adam2392
adam2392 requested a review from ogrisel August 14, 2026 14:32
Comment thread sklearn/tree/_splitter.pxd Outdated
Comment thread sklearn/tree/_tree.pyx
('n_node_samples', np.intp), # 8 bytes (offset 72)
('weighted_n_node_samples', np.float64), # 8 bytes (offset 80)
('missing_go_to_left', np.uint8), # 1 byte (offset 88)
('split_kind', np.int8), # 1 byte (offset 89)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess this extension does not matter much as long as we stay under 3*32 = 96 bytes (or even 2 * 64 = 128 bytes).

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.

Just added for completeness :p

@ogrisel ogrisel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Besides the resolving the pending discussion point above, LGTM. Thanks @adam2392!

n_left = current_split.pos - start
n_right = end - current_split.pos
if n_left != 0 and n_right != 0:
break

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried to investigate if it was possible to trigger an infinite loop here passing float values and declaring them as categorical values, but apparently it's not possible from the public API because of the internal ordinal encoding preprocessing.

Still, wouldn't it be safer to cap the number of retries to 100 or something (and raise a warning asking the user to report a minimal reproducer if that limit is reached)?

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.

You're saying cap the retries here directly within Cython? Sure I can add that

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.

Capped hash-seed retries at MAX_RANDOM_CATEGORICAL_SPLIT_RETRIES = 100. If that limit is reached we emit a UserWarning asking for a minimal reproducer and skip the feature. That branch is marked # pragma: no cover and left untested, since ordinal encoding makes it unreachable from the public API.

@adam2392

Copy link
Copy Markdown
Member Author

Does @cakedev0 approval count for the +2? heh.

I'll let @lorentzenchr take a look and merge if he's happy with the changes now? As next steps, I think we can now enable in Sklearn.ensemble the categorical support for the forest estimators.

@ogrisel

ogrisel commented Aug 24, 2026

Copy link
Copy Markdown
Member

Could you please duplicate doc/whats_new/upcoming_changes/sklearn.tree/33354.major-feature.rst to doc/whats_new/upcoming_changes/sklearn.tree/33972.major-feature.rst? This will register both PR numbers to the entry at the time of changelog consolidation by towncrier.

@adam2392

Copy link
Copy Markdown
Member Author

Could you please duplicate doc/whats_new/upcoming_changes/sklearn.tree/33354.major-feature.rst to doc/whats_new/upcoming_changes/sklearn.tree/33972.major-feature.rst? This will register both PR numbers to the entry at the time of changelog consolidation by towncrier.

Done. Let me know if this is what you had in mind @ogrisel

@ogrisel
ogrisel merged commit daece67 into scikit-learn:main Aug 25, 2026
37 checks passed
@github-project-automation github-project-automation Bot moved this from PR waiting for reviews to Done in Labs Aug 25, 2026
@ogrisel

ogrisel commented Aug 25, 2026

Copy link
Copy Markdown
Member

Thanks @adam2392 for the final push. Merged!

@adam2392
adam2392 deleted the feat/rand-cat branch August 26, 2026 15:23
prady0t pushed a commit to prady0t/scikit-learn that referenced this pull request Sep 2, 2026
@jeremiedbb jeremiedbb mentioned this pull request Sep 8, 2026
14 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants