Skip to content

Commit a1c31ce

Browse files
committed
Random splitting with deterministic hashing
1 parent 9f060d9 commit a1c31ce

11 files changed

Lines changed: 609 additions & 113 deletions

File tree

sklearn/tree/_classes.py

Lines changed: 118 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@
6969
DTYPE = _tree.DTYPE
7070
DOUBLE = _tree.DOUBLE
7171

72+
# maximum number of categories encodable via float32
73+
MAX_FLOAT32_INT_CATEGORIES = 2**24
74+
7275
CRITERIA_CLF = {
7376
"gini": _criterion.Gini,
7477
"log_loss": _criterion.Entropy,
@@ -126,6 +129,7 @@ class BaseDecisionTree(MultiOutputMixin, BaseEstimator, metaclass=ABCMeta):
126129
"min_impurity_decrease": [Interval(Real, 0.0, None, closed="left")],
127130
"ccp_alpha": [Interval(Real, 0.0, None, closed="left")],
128131
"monotonic_cst": ["array-like", None],
132+
"n_random_categorical_splits": [Interval(Integral, 0, None, closed="left")],
129133
# TODO: ultimately change this to follow Histgradboosting
130134
"categorical_features": [
131135
"array-like",
@@ -150,6 +154,7 @@ def __init__(
150154
class_weight=None,
151155
ccp_alpha=0.0,
152156
monotonic_cst=None,
157+
n_random_categorical_splits=0,
153158
categorical_features=None,
154159
):
155160
self.criterion = criterion
@@ -165,6 +170,7 @@ def __init__(
165170
self.class_weight = class_weight
166171
self.ccp_alpha = ccp_alpha
167172
self.monotonic_cst = monotonic_cst
173+
self.n_random_categorical_splits = n_random_categorical_splits
168174
self.categorical_features = categorical_features
169175

170176
def get_depth(self):
@@ -451,11 +457,6 @@ def _fit(
451457
X, monotonic_cst, is_categorical_
452458
)
453459

454-
if has_categorical and self.splitter == "random":
455-
raise ValueError(
456-
"Categorical features are not supported with splitter='random'. "
457-
"Use splitter='best' instead."
458-
)
459460
if has_categorical and self.n_outputs_ > 1:
460461
raise ValueError(
461462
"Categorical features are not supported with multi-output targets."
@@ -474,6 +475,7 @@ def _fit(
474475
min_weight_leaf,
475476
random_state,
476477
monotonic_cst,
478+
self.n_random_categorical_splits,
477479
)
478480

479481
if is_classifier(self):
@@ -573,7 +575,8 @@ def _validate_categorical_fit(self, X, monotonic_cst, is_categorical):
573575
- no missing values,
574576
- integer-valued entries,
575577
- non-negative values,
576-
- values strictly below ``MAX_NUM_CATEGORIES``,
578+
- values supported by the selected splitter's categorical split
579+
representation,
577580
- no non-zero monotonic constraints on categorical features.
578581
579582
Parameters
@@ -614,8 +617,9 @@ def _validate_categorical_values(
614617
- no NaNs,
615618
- integer-valued,
616619
- non-negative,
617-
- below ``MAX_NUM_CATEGORIES`` (fit-time), or below the fitted per-feature
618-
upper bound ``n_categories_in_feature[j]`` (predict-time).
620+
- within the selected splitter's fit-time category-code range, or below
621+
the fitted per-feature upper bound ``n_categories_in_feature[j]``
622+
(predict-time).
619623
620624
Parameters
621625
----------
@@ -629,11 +633,23 @@ def _validate_categorical_values(
629633
categorical feature ``j`` must be in
630634
``[0, n_categories_in_feature[j] - 1]``.
631635
"""
632-
base_msg = (
633-
f"Values for categorical features should be integers in "
634-
f"[0, {MAX_NUM_CATEGORIES - 1}]."
635-
)
636636
for idx in np.where(is_categorical)[0]:
637+
if n_categories_in_feature is None:
638+
max_category = (
639+
MAX_FLOAT32_INT_CATEGORIES
640+
if (
641+
self.splitter == "random"
642+
or self.n_random_categorical_splits > 0
643+
)
644+
else MAX_NUM_CATEGORIES - 1
645+
)
646+
else:
647+
max_category = n_categories_in_feature[idx] - 1
648+
649+
base_msg = (
650+
"Values for categorical features should be integers in "
651+
f"[0, {max_category}]."
652+
)
637653
X_col = X[:, idx]
638654
if np.isnan(X_col).any():
639655
raise ValueError(
@@ -647,10 +663,17 @@ def _validate_categorical_values(
647663
X_col_int = X_col.astype(np.intp)
648664
X_col_max = np.max(X_col_int)
649665
if n_categories_in_feature is None:
650-
if X_col_max >= MAX_NUM_CATEGORIES:
666+
if X_col_max > max_category:
651667
raise ValueError(f"{base_msg} Found {X_col_max}.")
652-
counts = np.bincount(X_col_int)
653-
if np.any(counts == 0):
668+
if X_col_max < MAX_NUM_CATEGORIES:
669+
is_contiguous = not np.any(np.bincount(X_col_int) == 0)
670+
else:
671+
unique_categories = np.unique(X_col_int)
672+
is_contiguous = (
673+
unique_categories[0] == 0
674+
and unique_categories[-1] == unique_categories.size - 1
675+
)
676+
if not is_contiguous:
654677
raise ValueError(
655678
f"Categorical feature {idx} must contain contiguous "
656679
"integer categories starting at 0."
@@ -1058,6 +1081,16 @@ class DecisionTreeClassifier(ClassifierMixin, BaseDecisionTree):
10581081
10591082
.. versionadded:: 1.4
10601083
1084+
n_random_categorical_splits : int, default=0
1085+
Number of random hash-based categorical splits to evaluate for each
1086+
over-cap categorical feature with ``splitter="best"``. When set to
1087+
0, ``splitter="best"`` rejects categorical features with more than
1088+
256 categories. When positive, such features use stochastic splits
1089+
controlled by ``random_state``. This parameter is ignored for
1090+
``splitter="random"``.
1091+
1092+
.. versionadded:: 1.9
1093+
10611094
categorical_features : array-like of int or bool of shape (n_features,) or
10621095
(n_categorical_features,), default=None
10631096
Indicates which features are treated as categorical.
@@ -1067,8 +1100,15 @@ class DecisionTreeClassifier(ClassifierMixin, BaseDecisionTree):
10671100
10681101
Categorical features are only supported for dense inputs
10691102
and single-output targets.
1070-
Values of categorical features must be contiguous integers in ``[0, 255]``
1071-
(missing values are not supported).
1103+
For classifiers, categorical features are only supported for binary
1104+
classification.
1105+
Values of categorical features must be contiguous non-negative integers
1106+
(missing values are not supported). With ``splitter="best"``, category
1107+
codes must be in ``[0, 255]`` unless ``n_random_categorical_splits``
1108+
is positive, in which case over-cap categories use random hash-based
1109+
splits. With ``splitter="random"`` or an enabled random categorical
1110+
fallback, category codes must be in ``[0, 2**24]``, the integer range
1111+
preserved by the current ``np.float32`` input path.
10721112
Categorical features cannot have non-zero monotonic constraint.
10731113
10741114
When these constraints are not met, ``fit`` will raise an error.
@@ -1196,6 +1236,7 @@ def __init__(
11961236
class_weight=None,
11971237
ccp_alpha=0.0,
11981238
monotonic_cst=None,
1239+
n_random_categorical_splits=0,
11991240
categorical_features=None,
12001241
):
12011242
super().__init__(
@@ -1213,6 +1254,7 @@ def __init__(
12131254
monotonic_cst=monotonic_cst,
12141255
ccp_alpha=ccp_alpha,
12151256
categorical_features=categorical_features,
1257+
n_random_categorical_splits=n_random_categorical_splits,
12161258
)
12171259

12181260
@_fit_context(prefer_skip_nested_validation=True)
@@ -1472,6 +1514,16 @@ class DecisionTreeRegressor(RegressorMixin, BaseDecisionTree):
14721514
14731515
.. versionadded:: 1.4
14741516
1517+
n_random_categorical_splits : int, default=0
1518+
Number of random hash-based categorical splits to evaluate for each
1519+
over-cap categorical feature with ``splitter="best"``. When set to
1520+
0, ``splitter="best"`` rejects categorical features with more than
1521+
256 categories. When positive, such features use stochastic splits
1522+
controlled by ``random_state``. This parameter is ignored for
1523+
``splitter="random"``.
1524+
1525+
.. versionadded:: 1.9
1526+
14751527
categorical_features : array-like of int or bool of shape (n_features,) or
14761528
(n_categorical_features,), default=None
14771529
Indicates which features are treated as categorical.
@@ -1481,8 +1533,13 @@ class DecisionTreeRegressor(RegressorMixin, BaseDecisionTree):
14811533
14821534
Categorical features are only supported for dense inputs
14831535
and single-output targets.
1484-
Values of categorical features must be contiguous integers in ``[0, 255]``
1485-
(missing values are not supported).
1536+
Values of categorical features must be contiguous non-negative integers
1537+
(missing values are not supported). With ``splitter="best"``, category
1538+
codes must be in ``[0, 255]`` unless ``n_random_categorical_splits``
1539+
is positive, in which case over-cap categories use random hash-based
1540+
splits. With ``splitter="random"`` or an enabled random categorical
1541+
fallback, category codes must be in ``[0, 2**24]``, the integer range
1542+
preserved by the current ``np.float32`` input path.
14861543
Categorical features cannot have non-zero monotonic constraints.
14871544
14881545
When these constraints are not met, ``fit`` will raise an error.
@@ -1596,6 +1653,7 @@ def __init__(
15961653
min_impurity_decrease=0.0,
15971654
ccp_alpha=0.0,
15981655
monotonic_cst=None,
1656+
n_random_categorical_splits=0,
15991657
categorical_features=None,
16001658
):
16011659
if isinstance(criterion, str) and criterion == "friedman_mse":
@@ -1622,6 +1680,7 @@ def __init__(
16221680
ccp_alpha=ccp_alpha,
16231681
monotonic_cst=monotonic_cst,
16241682
categorical_features=categorical_features,
1683+
n_random_categorical_splits=n_random_categorical_splits,
16251684
)
16261685

16271686
@_fit_context(prefer_skip_nested_validation=True)
@@ -1854,6 +1913,16 @@ class ExtraTreeClassifier(DecisionTreeClassifier):
18541913
18551914
.. versionadded:: 1.4
18561915
1916+
n_random_categorical_splits : int, default=0
1917+
Number of random hash-based categorical splits to evaluate for each
1918+
over-cap categorical feature with ``splitter="best"``. When set to
1919+
0, ``splitter="best"`` rejects categorical features with more than
1920+
256 categories. When positive, such features use stochastic splits
1921+
controlled by ``random_state``. This parameter is ignored for
1922+
``splitter="random"``.
1923+
1924+
.. versionadded:: 1.9
1925+
18571926
categorical_features : array-like of int or bool of shape (n_features,) or
18581927
(n_categorical_features,), default=None
18591928
Indicates which features are treated as categorical.
@@ -1863,8 +1932,15 @@ class ExtraTreeClassifier(DecisionTreeClassifier):
18631932
18641933
Categorical features are only supported for dense inputs
18651934
and single-output targets.
1866-
Values of categorical features must be contiguous integers in ``[0, 255]``
1867-
(missing values are not supported).
1935+
For classifiers, categorical features are only supported for binary
1936+
classification.
1937+
Values of categorical features must be contiguous non-negative integers
1938+
(missing values are not supported). With ``splitter="best"``, category
1939+
codes must be in ``[0, 255]`` unless ``n_random_categorical_splits``
1940+
is positive, in which case over-cap categories use random hash-based
1941+
splits. With ``splitter="random"`` or an enabled random categorical
1942+
fallback, category codes must be in ``[0, 2**24]``, the integer range
1943+
preserved by the current ``np.float32`` input path.
18681944
Categorical features cannot have non-zero monotonic constraints.
18691945
18701946
When these constraints are not met, ``fit`` will raise an error.
@@ -1976,6 +2052,7 @@ def __init__(
19762052
class_weight=None,
19772053
ccp_alpha=0.0,
19782054
monotonic_cst=None,
2055+
n_random_categorical_splits=0,
19792056
categorical_features=None,
19802057
):
19812058
super().__init__(
@@ -1993,6 +2070,7 @@ def __init__(
19932070
ccp_alpha=ccp_alpha,
19942071
monotonic_cst=monotonic_cst,
19952072
categorical_features=categorical_features,
2073+
n_random_categorical_splits=n_random_categorical_splits,
19962074
)
19972075

19982076
def __sklearn_tags__(self):
@@ -2150,6 +2228,16 @@ class ExtraTreeRegressor(DecisionTreeRegressor):
21502228
21512229
.. versionadded:: 1.4
21522230
2231+
n_random_categorical_splits : int, default=0
2232+
Number of random hash-based categorical splits to evaluate for each
2233+
over-cap categorical feature with ``splitter="best"``. When set to
2234+
0, ``splitter="best"`` rejects categorical features with more than
2235+
256 categories. When positive, such features use stochastic splits
2236+
controlled by ``random_state``. This parameter is ignored for
2237+
``splitter="random"``.
2238+
2239+
.. versionadded:: 1.9
2240+
21532241
categorical_features : array-like of int or bool of shape (n_features,) or
21542242
(n_categorical_features,), default=None
21552243
Indicates which features are treated as categorical.
@@ -2159,8 +2247,13 @@ class ExtraTreeRegressor(DecisionTreeRegressor):
21592247
21602248
Categorical features are only supported for dense inputs
21612249
and single-output targets.
2162-
Values of categorical features must be contiguous integers in ``[0, 255]``
2163-
(missing values are not supported).
2250+
Values of categorical features must be contiguous non-negative integers
2251+
(missing values are not supported). With ``splitter="best"``, category
2252+
codes must be in ``[0, 255]`` unless ``n_random_categorical_splits``
2253+
is positive, in which case over-cap categories use random hash-based
2254+
splits. With ``splitter="random"`` or an enabled random categorical
2255+
fallback, category codes must be in ``[0, 2**24]``, the integer range
2256+
preserved by the current ``np.float32`` input path.
21642257
Categorical features cannot have non-zero monotonic constraints.
21652258
21662259
When these constraints are not met, ``fit`` will raise an error.
@@ -2255,6 +2348,7 @@ def __init__(
22552348
max_leaf_nodes=None,
22562349
ccp_alpha=0.0,
22572350
monotonic_cst=None,
2351+
n_random_categorical_splits=0,
22582352
categorical_features=None,
22592353
):
22602354
super().__init__(
@@ -2271,6 +2365,7 @@ def __init__(
22712365
ccp_alpha=ccp_alpha,
22722366
monotonic_cst=monotonic_cst,
22732367
categorical_features=categorical_features,
2368+
n_random_categorical_splits=n_random_categorical_splits,
22742369
)
22752370

22762371
def __sklearn_tags__(self):

sklearn/tree/_partitioner.pxd

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@ cdef class DensePartitioner:
116116
) noexcept nogil
117117
cdef intp_t partition_samples(
118118
self,
119-
float64_t current_threshold,
120-
bint missing_go_to_left
119+
const SplitRecord* current_split,
121120
) noexcept nogil
122121
cdef void partition_samples_final(
123122
self,
@@ -192,8 +191,7 @@ cdef class SparsePartitioner:
192191
) noexcept nogil
193192
cdef intp_t partition_samples(
194193
self,
195-
float64_t current_threshold,
196-
bint missing_go_to_left,
194+
const SplitRecord* current_split,
197195
) noexcept nogil
198196
cdef void partition_samples_final(
199197
self,

0 commit comments

Comments
 (0)