Skip to content

Commit c78c84d

Browse files
authored
simpler missing values handling (scikit-learn#34335)
1 parent 9bd6b83 commit c78c84d

1 file changed

Lines changed: 46 additions & 152 deletions

File tree

sklearn/ensemble/_hist_gradient_boosting/splitting.pyx

Lines changed: 46 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ cdef class Splitter:
489489
Y_DTYPE_C feature_fraction_per_split = self.feature_fraction_per_split
490490
uint8_t [:] subsample_mask # same as npy_bool
491491
int n_subsampled_features
492+
uint8_t missing_go_to_left
492493

493494
has_interaction_cst = allowed_features is not None
494495
if has_interaction_cst:
@@ -547,31 +548,24 @@ cdef class Splitter:
547548
value, monotonic_cst[feature_idx], lower_bound,
548549
upper_bound, &split_infos[split_info_idx])
549550
else:
550-
# We will scan bins from left to right (in all cases), and
551-
# if there are any missing values, we will also scan bins
552-
# from right to left. This way, we can consider whichever
553-
# case yields the best gain: either missing values go to
554-
# the right (left to right scan) or to the left (right to
555-
# left case). See algo 3 from the XGBoost paper
551+
# We scan bins from left to right and, if there are any
552+
# missing values, we scan a second time with missing
553+
# values in the left child. This way, we can consider
554+
# whichever case yields the best gain: either missing
555+
# values go to the right or to the left. See algo 3 from
556+
# the XGBoost paper
556557
# https://arxiv.org/abs/1603.02754
557558
# Note: for the categorical features above, this isn't
558559
# needed since missing values are considered a native
559560
# category.
560-
self._find_best_bin_to_split_left_to_right(
561-
feature_idx, has_missing_values[feature_idx],
562-
histograms, n_samples, sum_gradients, sum_hessians,
563-
value, monotonic_cst[feature_idx],
564-
lower_bound, upper_bound, &split_infos[split_info_idx])
565-
566-
if has_missing_values[feature_idx]:
567-
# We need to explore both directions to check whether
568-
# sending the nans to the left child would lead to a higher
569-
# gain
570-
self._find_best_bin_to_split_right_to_left(
571-
feature_idx, histograms, n_samples,
572-
sum_gradients, sum_hessians,
561+
for missing_go_to_left in range(has_missing_values[feature_idx] + 1):
562+
563+
self._find_best_bin_to_split_numerical(
564+
feature_idx, has_missing_values[feature_idx],
565+
histograms, n_samples, sum_gradients, sum_hessians,
573566
value, monotonic_cst[feature_idx],
574-
lower_bound, upper_bound, &split_infos[split_info_idx])
567+
lower_bound, upper_bound, missing_go_to_left,
568+
&split_infos[split_info_idx])
575569

576570
# then compute best possible split among all features
577571
# split_info is set to the best of split_infos
@@ -618,7 +612,7 @@ cdef class Splitter:
618612
best_split_info_idx = split_info_idx
619613
return best_split_info_idx
620614

621-
cdef void _find_best_bin_to_split_left_to_right(
615+
cdef void _find_best_bin_to_split_numerical(
622616
Splitter self,
623617
unsigned int feature_idx,
624618
uint8_t has_missing_values,
@@ -630,27 +624,26 @@ cdef class Splitter:
630624
signed char monotonic_cst,
631625
Y_DTYPE_C lower_bound,
632626
Y_DTYPE_C upper_bound,
627+
uint8_t missing_go_to_left,
633628
split_info_struct * split_info) noexcept nogil: # OUT
634629
"""Find best bin to split on for a given feature.
635630
636631
Splits that do not satisfy the splitting constraints
637632
(min_gain_to_split, etc.) are discarded here.
638633
639-
We scan node from left to right. This version is called whether there
640-
are missing values or not. If any, missing values are assigned to the
641-
right node.
634+
We scan node from left to right. When missing_go_to_left is True,
635+
the scan starts by adding the missing-value bin to the left child
636+
before scanning the non-missing bins.
642637
"""
643638
cdef:
644-
unsigned int bin_idx
639+
int bin_idx
640+
unsigned int hist_bin_idx
641+
int n_bins_non_missing = self.n_bins_non_missing[feature_idx]
645642
unsigned int n_samples_left
646643
unsigned int n_samples_right
647644
unsigned int n_samples_ = n_samples
648-
# We set the 'end' variable such that the last non-missing-values
649-
# bin never goes to the left child (which would result in and
650-
# empty right child), unless there are missing values, since these
651-
# would go to the right child.
652-
unsigned int end = \
653-
self.n_bins_non_missing[feature_idx] - 1 + has_missing_values
645+
int start
646+
int end
654647
Y_DTYPE_C sum_hessian_left
655648
Y_DTYPE_C sum_hessian_right
656649
Y_DTYPE_C sum_gradient_left
@@ -663,16 +656,28 @@ cdef class Splitter:
663656
Y_DTYPE_C best_sum_gradient_left
664657
unsigned int best_bin_idx
665658
unsigned int best_n_samples_left
666-
Y_DTYPE_C best_gain = -1
659+
Y_DTYPE_C best_gain = split_info.gain
667660
hist_struct hist
668661

669662
sum_gradient_left, sum_hessian_left = 0., 0.
670663
n_samples_left = 0
671664

672665
loss_current_node = _loss_from_value(value, sum_gradients)
673666

674-
for bin_idx in range(end):
675-
hist = histograms[feature_idx, bin_idx]
667+
# When missing_go_to_left is true, bin_idx == -1 maps to the missing
668+
# bin. Missing values are always in the last bin.
669+
start = -missing_go_to_left
670+
# Do not put the last non-missing bin in the left child unless
671+
# missing values can still go to the right child.
672+
end = n_bins_non_missing - 1 + has_missing_values * (1 - missing_go_to_left)
673+
674+
for bin_idx in range(start, end):
675+
if bin_idx == -1:
676+
hist_bin_idx = self.missing_values_bin_idx
677+
else:
678+
hist_bin_idx = bin_idx
679+
680+
hist = histograms[feature_idx, hist_bin_idx]
676681
n_samples_left += hist.count
677682
n_samples_right = n_samples_ - n_samples_left
678683

@@ -686,6 +691,12 @@ cdef class Splitter:
686691
sum_gradient_left += hist.sum_gradients
687692
sum_gradient_right = sum_gradients - sum_gradient_left
688693

694+
if bin_idx == -1:
695+
# This iteration only adds missing values to the left child.
696+
# The corresponding all-non-missing-values-on-one-side split
697+
# is already considered when missing_go_to_left is False.
698+
continue
699+
689700
if n_samples_left < self.min_samples_leaf:
690701
continue
691702
if n_samples_right < self.min_samples_leaf:
@@ -717,124 +728,7 @@ cdef class Splitter:
717728
if found_better_split:
718729
split_info.gain = best_gain
719730
split_info.bin_idx = best_bin_idx
720-
# we scan from left to right so missing values go to the right
721-
split_info.missing_go_to_left = False
722-
split_info.sum_gradient_left = best_sum_gradient_left
723-
split_info.sum_gradient_right = sum_gradients - best_sum_gradient_left
724-
split_info.sum_hessian_left = best_sum_hessian_left
725-
split_info.sum_hessian_right = sum_hessians - best_sum_hessian_left
726-
split_info.n_samples_left = best_n_samples_left
727-
split_info.n_samples_right = n_samples - best_n_samples_left
728-
729-
# We recompute best values here but it's cheap
730-
split_info.value_left = compute_node_value(
731-
split_info.sum_gradient_left, split_info.sum_hessian_left,
732-
lower_bound, upper_bound, self.l2_regularization)
733-
734-
split_info.value_right = compute_node_value(
735-
split_info.sum_gradient_right, split_info.sum_hessian_right,
736-
lower_bound, upper_bound, self.l2_regularization)
737-
738-
cdef void _find_best_bin_to_split_right_to_left(
739-
self,
740-
unsigned int feature_idx,
741-
const hist_struct [:, ::1] histograms, # IN
742-
unsigned int n_samples,
743-
Y_DTYPE_C sum_gradients,
744-
Y_DTYPE_C sum_hessians,
745-
Y_DTYPE_C value,
746-
signed char monotonic_cst,
747-
Y_DTYPE_C lower_bound,
748-
Y_DTYPE_C upper_bound,
749-
split_info_struct * split_info) noexcept nogil: # OUT
750-
"""Find best bin to split on for a given feature.
751-
752-
Splits that do not satisfy the splitting constraints
753-
(min_gain_to_split, etc.) are discarded here.
754-
755-
We scan node from right to left. This version is only called when
756-
there are missing values. Missing values are assigned to the left
757-
child.
758-
759-
If no missing value are present in the data this method isn't called
760-
since only calling _find_best_bin_to_split_left_to_right is enough.
761-
"""
762-
763-
cdef:
764-
unsigned int bin_idx
765-
unsigned int n_samples_left
766-
unsigned int n_samples_right
767-
unsigned int n_samples_ = n_samples
768-
Y_DTYPE_C sum_hessian_left
769-
Y_DTYPE_C sum_hessian_right
770-
Y_DTYPE_C sum_gradient_left
771-
Y_DTYPE_C sum_gradient_right
772-
Y_DTYPE_C loss_current_node
773-
Y_DTYPE_C gain
774-
unsigned int start = self.n_bins_non_missing[feature_idx] - 2
775-
uint8_t found_better_split = False
776-
777-
Y_DTYPE_C best_sum_hessian_left
778-
Y_DTYPE_C best_sum_gradient_left
779-
unsigned int best_bin_idx
780-
unsigned int best_n_samples_left
781-
Y_DTYPE_C best_gain = split_info.gain # computed during previous scan
782-
hist_struct hist
783-
784-
sum_gradient_right, sum_hessian_right = 0., 0.
785-
n_samples_right = 0
786-
787-
loss_current_node = _loss_from_value(value, sum_gradients)
788-
789-
for bin_idx in range(start, -1, -1):
790-
hist = histograms[feature_idx, bin_idx + 1]
791-
n_samples_right += hist.count
792-
n_samples_left = n_samples_ - n_samples_right
793-
794-
if self.hessians_are_constant:
795-
sum_hessian_right += hist.count
796-
else:
797-
sum_hessian_right += \
798-
hist.sum_hessians
799-
sum_hessian_left = sum_hessians - sum_hessian_right
800-
801-
sum_gradient_right += \
802-
hist.sum_gradients
803-
sum_gradient_left = sum_gradients - sum_gradient_right
804-
805-
if n_samples_right < self.min_samples_leaf:
806-
continue
807-
if n_samples_left < self.min_samples_leaf:
808-
# won't get any better
809-
break
810-
811-
if sum_hessian_right < self.min_hessian_to_split:
812-
continue
813-
if sum_hessian_left < self.min_hessian_to_split:
814-
# won't get any better (hessians are > 0 since loss is convex)
815-
break
816-
817-
gain = _split_gain(sum_gradient_left, sum_hessian_left,
818-
sum_gradient_right, sum_hessian_right,
819-
loss_current_node,
820-
monotonic_cst,
821-
lower_bound,
822-
upper_bound,
823-
self.l2_regularization)
824-
825-
if gain > best_gain and gain > self.min_gain_to_split:
826-
found_better_split = True
827-
best_gain = gain
828-
best_bin_idx = bin_idx
829-
best_sum_gradient_left = sum_gradient_left
830-
best_sum_hessian_left = sum_hessian_left
831-
best_n_samples_left = n_samples_left
832-
833-
if found_better_split:
834-
split_info.gain = best_gain
835-
split_info.bin_idx = best_bin_idx
836-
# we scan from right to left so missing values go to the left
837-
split_info.missing_go_to_left = True
731+
split_info.missing_go_to_left = missing_go_to_left
838732
split_info.sum_gradient_left = best_sum_gradient_left
839733
split_info.sum_gradient_right = sum_gradients - best_sum_gradient_left
840734
split_info.sum_hessian_left = best_sum_hessian_left

0 commit comments

Comments
 (0)