3030
3131
3232@pytest .mark .parametrize ("TreeClassifier" , TREE_BASED_CLASSIFIER_CLASSES )
33+ @pytest .mark .parametrize (
34+ "sparse_splitter, with_missing" ,
35+ [
36+ (False , False ),
37+ (True , False ),
38+ (False , True ),
39+ ],
40+ ids = ["dense-without-missing" , "sparse-without-missing" , "dense-with-missing" ],
41+ )
3342@pytest .mark .parametrize ("depth_first_builder" , (True , False ))
34- @pytest .mark .parametrize ("sparse_splitter" , (True , False ))
3543@pytest .mark .parametrize ("csc_container" , CSC_CONTAINERS )
3644def test_monotonic_constraints_classifications (
3745 TreeClassifier ,
38- depth_first_builder ,
3946 sparse_splitter ,
47+ depth_first_builder ,
48+ with_missing ,
4049 global_random_seed ,
4150 csc_container ,
4251):
@@ -72,9 +81,13 @@ def test_monotonic_constraints_classifications(
7281 max_leaf_nodes = n_samples_train ,
7382 )
7483 if hasattr (est , "random_state" ):
75- est .set_params (** { " random_state" : global_random_seed } )
84+ est .set_params (random_state = global_random_seed )
7685 if hasattr (est , "n_estimators" ):
77- est .set_params (** {"n_estimators" : 5 })
86+ est .set_params (n_estimators = 5 )
87+ if with_missing :
88+ generator = np .random .default_rng (seed = global_random_seed )
89+ mask = generator .choice (2 , size = X_train .shape ).astype (bool )
90+ X_train [mask ] = np .nan
7891 if sparse_splitter :
7992 X_train = csc_container (X_train )
8093 est .fit (X_train , y_train )
@@ -95,14 +108,23 @@ def test_monotonic_constraints_classifications(
95108
96109
97110@pytest .mark .parametrize ("TreeRegressor" , TREE_BASED_REGRESSOR_CLASSES )
111+ @pytest .mark .parametrize (
112+ "sparse_splitter, with_missing" ,
113+ [
114+ (False , False ),
115+ (True , False ),
116+ (False , True ),
117+ ],
118+ ids = ["dense-without-missing" , "sparse-without-missing" , "dense-with-missing" ],
119+ )
98120@pytest .mark .parametrize ("depth_first_builder" , (True , False ))
99- @pytest .mark .parametrize ("sparse_splitter" , (True , False ))
100121@pytest .mark .parametrize ("criterion" , ("absolute_error" , "squared_error" ))
101122@pytest .mark .parametrize ("csc_container" , CSC_CONTAINERS )
102123def test_monotonic_constraints_regressions (
103124 TreeRegressor ,
104- depth_first_builder ,
105125 sparse_splitter ,
126+ depth_first_builder ,
127+ with_missing ,
106128 criterion ,
107129 global_random_seed ,
108130 csc_container ,
@@ -145,7 +167,11 @@ def test_monotonic_constraints_regressions(
145167 if hasattr (est , "random_state" ):
146168 est .set_params (random_state = global_random_seed )
147169 if hasattr (est , "n_estimators" ):
148- est .set_params (** {"n_estimators" : 5 })
170+ est .set_params (n_estimators = 5 )
171+ if with_missing :
172+ generator = np .random .default_rng (seed = global_random_seed )
173+ mask = generator .choice (2 , size = X_train .shape ).astype (bool )
174+ X_train [mask ] = np .nan
149175 if sparse_splitter :
150176 X_train = csc_container (X_train )
151177 est .fit (X_train , y_train )
@@ -190,29 +216,6 @@ def test_multiple_output_raises(TreeClassifier):
190216 est .fit (X , y )
191217
192218
193- @pytest .mark .parametrize (
194- "Tree" ,
195- [
196- DecisionTreeClassifier ,
197- DecisionTreeRegressor ,
198- ExtraTreeClassifier ,
199- ExtraTreeRegressor ,
200- ],
201- )
202- def test_missing_values_raises (Tree ):
203- X , y = make_classification (
204- n_samples = 100 , n_features = 5 , n_classes = 2 , n_informative = 3 , random_state = 0
205- )
206- X [0 , 0 ] = np .nan
207- monotonic_cst = np .zeros (X .shape [1 ])
208- monotonic_cst [0 ] = 1
209- est = Tree (max_depth = None , monotonic_cst = monotonic_cst , random_state = 0 )
210-
211- msg = "Input X contains NaN"
212- with pytest .raises (ValueError , match = msg ):
213- est .fit (X , y )
214-
215-
216219@pytest .mark .parametrize ("TreeClassifier" , TREE_BASED_CLASSIFIER_CLASSES )
217220def test_bad_monotonic_cst_raises (TreeClassifier ):
218221 X = [[1 , 2 ], [3 , 4 ], [5 , 6 ], [7 , 8 ], [9 , 10 ]]
@@ -307,11 +310,17 @@ def test_1d_opposite_monotonicity_cst_data(TreeRegressor):
307310
308311
309312@pytest .mark .parametrize ("TreeRegressor" , TREE_REGRESSOR_CLASSES )
313+ @pytest .mark .parametrize ("with_missing" , (True , False ))
310314@pytest .mark .parametrize ("monotonic_sign" , (- 1 , 1 ))
311315@pytest .mark .parametrize ("depth_first_builder" , (True , False ))
312316@pytest .mark .parametrize ("criterion" , ("absolute_error" , "squared_error" ))
313317def test_1d_tree_nodes_values (
314- TreeRegressor , monotonic_sign , depth_first_builder , criterion , global_random_seed
318+ TreeRegressor ,
319+ with_missing ,
320+ monotonic_sign ,
321+ depth_first_builder ,
322+ criterion ,
323+ global_random_seed ,
315324):
316325 # Adaptation from test_nodes_values in test_monotonic_constraints.py
317326 # in sklearn.ensemble._hist_gradient_boosting
@@ -351,10 +360,15 @@ def test_1d_tree_nodes_values(
351360 criterion = criterion ,
352361 random_state = global_random_seed ,
353362 )
363+ if with_missing :
364+ generator = np .random .default_rng (seed = global_random_seed )
365+ mask = generator .choice (2 , size = X .shape , p = [0.8 , 0.2 ]).astype (bool )
366+ X [mask ] = np .nan
354367 clf .fit (X , y )
355368
356369 assert_1d_reg_tree_children_monotonic_bounded (clf .tree_ , monotonic_sign )
357- assert_1d_reg_monotonic (clf , monotonic_sign , np .min (X ), np .max (X ), 100 )
370+ min_x , max_x = np .nanmin (X ), np .nanmax (X )
371+ assert_1d_reg_monotonic (clf , monotonic_sign , min_x , max_x , 100 )
358372
359373
360374def assert_nd_reg_tree_children_monotonic_bounded (tree_ , monotonic_cst ):
@@ -379,7 +393,7 @@ def assert_nd_reg_tree_children_monotonic_bounded(tree_, monotonic_cst):
379393 # Split node: check and update bounds for the children.
380394 i_left = tree_ .children_left [i ]
381395 i_right = tree_ .children_right [i ]
382- # unpack value from nx1x1 array
396+ # unpack value from nx1x1 array (middle_value after clipping)
383397 middle_value = (tree_ .value [i_left ][0 ][0 ] + tree_ .value [i_right ][0 ][0 ]) / 2
384398
385399 if monotonic_cst [feature ] == 0 :
@@ -460,11 +474,17 @@ def test_assert_nd_reg_tree_children_monotonic_bounded():
460474
461475
462476@pytest .mark .parametrize ("TreeRegressor" , TREE_REGRESSOR_CLASSES )
477+ @pytest .mark .parametrize ("with_missing" , (True , False ))
463478@pytest .mark .parametrize ("monotonic_sign" , (- 1 , 1 ))
464479@pytest .mark .parametrize ("depth_first_builder" , (True , False ))
465480@pytest .mark .parametrize ("criterion" , ("absolute_error" , "squared_error" ))
466481def test_nd_tree_nodes_values (
467- TreeRegressor , monotonic_sign , depth_first_builder , criterion , global_random_seed
482+ TreeRegressor ,
483+ with_missing ,
484+ monotonic_sign ,
485+ depth_first_builder ,
486+ criterion ,
487+ global_random_seed ,
468488):
469489 # Build tree with several features, and make sure the nodes
470490 # values respect the monotonicity constraints.
@@ -508,5 +528,9 @@ def test_nd_tree_nodes_values(
508528 criterion = criterion ,
509529 random_state = global_random_seed ,
510530 )
531+ if with_missing :
532+ generator = np .random .default_rng (seed = global_random_seed )
533+ mask = generator .choice (2 , size = X .shape , p = [0.8 , 0.2 ]).astype (bool )
534+ X [mask ] = np .nan
511535 clf .fit (X , y )
512536 assert_nd_reg_tree_children_monotonic_bounded (clf .tree_ , monotonic_cst )
0 commit comments