@@ -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