Skip to content

Commit 9f060d9

Browse files
committed
Nocats
1 parent 6e9ef2b commit 9f060d9

17 files changed

Lines changed: 1594 additions & 173 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- :class:`tree.DecisionTreeRegressor`, :class:`tree.DecisionTreeClassifier`
2+
now have native support for categorical features for binary classification and
3+
single-output regression. Categorical features can be specified with the
4+
`categorical_features` parameter. Up to 256 categories per features are supported.
5+
By `:user:`Adam Li <adam2392>`, `:user:`Arthur Lacote <cakedev0>` and :user:`Christian Lorentzen <lorentzenchr>`

sklearn/ensemble/_forest.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1512,6 +1512,7 @@ class labels (multi-output problem).
15121512
],
15131513
}
15141514
_parameter_constraints.pop("splitter")
1515+
_parameter_constraints.pop("categorical_features")
15151516

15161517
def __init__(
15171518
self,
@@ -1889,6 +1890,7 @@ class RandomForestRegressor(ForestRegressor):
18891890
**DecisionTreeRegressor._parameter_constraints,
18901891
}
18911892
_parameter_constraints.pop("splitter")
1893+
_parameter_constraints.pop("categorical_features")
18921894

18931895
def __init__(
18941896
self,
@@ -2285,6 +2287,8 @@ class labels (multi-output problem).
22852287
],
22862288
}
22872289
_parameter_constraints.pop("splitter")
2290+
# TODO: remove once randomsplitter supports categorical features
2291+
_parameter_constraints.pop("categorical_features")
22882292

22892293
def __init__(
22902294
self,
@@ -2641,6 +2645,8 @@ class ExtraTreesRegressor(ForestRegressor):
26412645
**DecisionTreeRegressor._parameter_constraints,
26422646
}
26432647
_parameter_constraints.pop("splitter")
2648+
# TODO: remove once randomsplitter supports categorical features
2649+
_parameter_constraints.pop("categorical_features")
26442650

26452651
def __init__(
26462652
self,
@@ -2904,7 +2910,13 @@ class RandomTreesEmbedding(TransformerMixin, BaseForest):
29042910
**BaseDecisionTree._parameter_constraints,
29052911
"sparse_output": ["boolean"],
29062912
}
2907-
for param in ("max_features", "ccp_alpha", "splitter", "monotonic_cst"):
2913+
for param in (
2914+
"max_features",
2915+
"ccp_alpha",
2916+
"splitter",
2917+
"monotonic_cst",
2918+
"categorical_features",
2919+
):
29082920
_parameter_constraints.pop(param)
29092921

29102922
criterion = "squared_error"

sklearn/ensemble/_gb.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ class BaseGradientBoosting(BaseEnsemble, metaclass=ABCMeta):
378378
}
379379
_parameter_constraints.pop("splitter")
380380
_parameter_constraints.pop("monotonic_cst")
381+
_parameter_constraints.pop("categorical_features")
381382

382383
@abstractmethod
383384
def __init__(

sklearn/ensemble/_gradient_boosting.pyx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ cdef void _predict_regression_tree_inplace_fast_dense(
6666
node = root_node
6767
# While node not a leaf
6868
while node.left_child != TREE_LEAF:
69-
if X[i, node.feature] <= node.threshold:
69+
if X[i, node.feature] <= node.split_value.threshold:
7070
node = root_node + node.left_child
7171
else:
7272
node = root_node + node.right_child
@@ -148,7 +148,7 @@ def _predict_regression_tree_stages_sparse(
148148
else:
149149
feature_value = 0.
150150

151-
if feature_value <= node.threshold:
151+
if feature_value <= node.split_value.threshold:
152152
node = root_node + node.left_child
153153
else:
154154
node = root_node + node.right_child

sklearn/inspection/tests/test_partial_dependence.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,57 @@ def test_partial_dependence_easy_target(est, power):
523523
assert r2 > 0.99
524524

525525

526+
def test_partial_dependence_recursion_decision_tree_categorical_target_feature():
527+
# Non-ordinal categorical signal: categories {1, 2, 5} -> 1 and
528+
# {0, 3, 4} -> 0. Recursion must route with the categorical bitset.
529+
category_values = np.arange(6, dtype=np.float64)
530+
X = np.repeat(category_values, 4).reshape(-1, 1)
531+
y = np.isin(X.ravel(), [1, 2, 5]).astype(np.float64)
532+
533+
est = DecisionTreeRegressor(
534+
categorical_features=[0], max_depth=1, random_state=0
535+
).fit(X, y)
536+
537+
recursion = partial_dependence(
538+
est, X, features=[0], method="recursion", categorical_features=[0]
539+
)
540+
brute = partial_dependence(
541+
est, X, features=[0], method="brute", categorical_features=[0]
542+
)
543+
544+
grid = recursion["grid_values"][0].reshape(-1, 1)
545+
expected = est.predict(grid)
546+
547+
assert_array_equal(recursion["grid_values"][0], category_values)
548+
assert_allclose(recursion["average"][0], brute["average"][0])
549+
assert_allclose(recursion["average"][0], expected)
550+
551+
552+
def test_partial_dependence_recursion_decision_tree_missing_target_feature():
553+
# Missing values are isolated by the split. Recursion must route np.nan
554+
# according to the fitted node's missing_go_to_left flag.
555+
X = np.array([0.0, 0.0, np.nan, np.nan, 1.0, 1.0, 1.0]).reshape(-1, 1)
556+
y = np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0])
557+
558+
est = DecisionTreeRegressor(max_depth=1, random_state=0).fit(X, y)
559+
assert est.tree_.missing_go_to_left[0]
560+
custom_values = {0: [0.0, np.nan, 1.0]}
561+
562+
recursion = partial_dependence(
563+
est, X, features=[0], method="recursion", custom_values=custom_values
564+
)
565+
brute = partial_dependence(
566+
est, X, features=[0], method="brute", custom_values=custom_values
567+
)
568+
569+
grid = recursion["grid_values"][0].reshape(-1, 1)
570+
expected = est.predict(grid)
571+
572+
assert_array_equal(recursion["grid_values"][0], custom_values[0])
573+
assert_allclose(recursion["average"][0], brute["average"][0])
574+
assert_allclose(recursion["average"][0], expected)
575+
576+
526577
@pytest.mark.parametrize(
527578
"Estimator",
528579
(

0 commit comments

Comments
 (0)