6969DTYPE = _tree .DTYPE
7070DOUBLE = _tree .DOUBLE
7171
72+ # maximum number of categories encodable via float32
73+ MAX_FLOAT32_INT_CATEGORIES = 2 ** 24
74+
7275CRITERIA_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 ):
0 commit comments