class_docstr: Numeric attribute observer for classification tasks that is based on
  a Binary Search Tree.
  
  This algorithm[^1] is also referred to as exhaustive attribute observer,
  since it ends up storing all the observations between split attempts[^2].
  
  This splitter cannot perform probability density estimations, so it does not work well
  when coupled with tree leaves using naive bayes models.
  
  References
  ----------
  [^1]: Domingos, P. and Hulten, G., 2000, August. Mining high-speed data streams.
  In Proceedings of the sixth ACM SIGKDD international conference on Knowledge discovery
  and data mining (pp. 71-80).
  [^2]: Pfahringer, B., Holmes, G. and Kirkby, R., 2008, May. Handling numeric attributes in
  hoeffding trees. In Pacific-Asia Conference on Knowledge Discovery and Data Mining
  (pp. 296-307). Springer, Berlin, Heidelberg.
class_docstr_tok: ["Numeric","attribute","observer","for","classification","tasks","that","is","based","on","a","Binary","Search","Tree",".","This","algorithm","[","^1","]","is","also","referred","to","as","exhaustive","attribute","observer",",","since","it","ends","up","storing","all","the","observations","between","split","attempts","[","^2","]",".","This","splitter","can","not","perform","probability","density","estimations",",","so","it","does","not","work","well","when","coupled","with","tree","leaves","using","naive","bayes","models",".","References","--","--","--","--","--","[","^1","]",":","Domingos",",","P.","and","Hulten",",","G.",",","2000",",","August",".","Mining","high-speed","data","streams",".","In","Proceedings","of","the","sixth","ACM","SIGKDD","international","conference","on","Knowledge","discovery","and","data","mining","(","pp",".","71-80",")",".","[","^2","]",":","Pfahringer",",","B.",",","Holmes",",","G.","and","Kirkby",",","R.",",","2008",",","May",".","Handling","numeric","attributes","in","hoeffding","trees",".","In","Pacific-Asia","Conference","on","Knowledge","Discovery","and","Data","Mining","(","pp",".","296-307",")",".","Springer",",","Berlin",",","Heidelberg","."]
code: def _search_for_best_split_option(
      self,
      current_node,
      current_best_option,
      actual_parent_left,
      parent_left,
      parent_right,
      left_child,
      criterion,
      pre_split_dist,
      att_idx,
  ):
      if current_node is None:
          return current_best_option
  
      left_dist = {}
      right_dist = {}
  
      if parent_left is None:
          left_dist.update(
              dict(
                  Counter(left_dist)
                  + Counter(current_node.class_count_left)
              )
          )
          right_dist.update(
              dict(
                  Counter(right_dist)
                  + Counter(current_node.class_count_right)
              )
          )
      else:
          left_dist.update(
              dict(Counter(left_dist) + Counter(parent_left))
          )
          right_dist.update(
              dict(Counter(right_dist) + Counter(parent_right))
          )
  
          if left_child:
              # get the exact statistics of the parent value
              exact_parent_dist = {}
              exact_parent_dist.update(
                  dict(
                      Counter(exact_parent_dist)
                      + Counter(actual_parent_left)
                  )
              )
              exact_parent_dist.update(
                  dict(
                      Counter(exact_parent_dist)
                      - Counter(current_node.class_count_left)
                  )
              )
              exact_parent_dist.update(
                  dict(
                      Counter(exact_parent_dist)
                      - Counter(current_node.class_count_right)
                  )
              )
  
              # move the subtrees
              left_dist.update(
                  dict(
                      Counter(left_dist)
                      - Counter(current_node.class_count_right)
                  )
              )
              right_dist.update(
                  dict(
                      Counter(right_dist)
                      + Counter(current_node.class_count_right)
                  )
              )
  
              # move the exact value from the parent
              right_dist.update(
                  dict(Counter(right_dist) + Counter(exact_parent_dist))
              )
              left_dist.update(
                  dict(Counter(left_dist) - Counter(exact_parent_dist))
              )
          else:
              left_dist.update(
                  dict(
                      Counter(left_dist)
                      + Counter(current_node.class_count_left)
                  )
              )
              right_dist.update(
                  dict(
                      Counter(right_dist)
                      - Counter(current_node.class_count_left)
                  )
              )
  
      post_split_dists = [left_dist, right_dist]
      merit = criterion.merit_of_split(pre_split_dist, post_split_dists)
  
      if merit > current_best_option.merit:
          current_best_option = BranchFactory(
              merit, att_idx, current_node.cut_point, post_split_dists
          )
  
      current_best_option = self._search_for_best_split_option(
          current_node=current_node._left,  # noqa
          current_best_option=current_best_option,
          actual_parent_left=current_node.class_count_left,
          parent_left=post_split_dists[0],
          parent_right=post_split_dists[1],
          left_child=True,
          criterion=criterion,
          pre_split_dist=pre_split_dist,
          att_idx=att_idx,
      )
  
      current_best_option = self._search_for_best_split_option(
          current_node=current_node._right,  # noqa
          current_best_option=current_best_option,
          actual_parent_left=current_node.class_count_left,
          parent_left=post_split_dists[0],
          parent_right=post_split_dists[1],
          left_child=False,
          criterion=criterion,
          pre_split_dist=pre_split_dist,
          att_idx=att_idx,
      )
  
      return current_best_option
  
file_path: river/river/tree/splitter/exhaustive_splitter.py
imports: from __future__ import annotations
  from collections import Counter, defaultdict
  from ..utils import BranchFactory
  from .base import Splitter
label_desc: Use image node_id 5 for calling the ExhaustiveSplitter obj's underlying member method code with example usage: obj._search_for_best_split_option(current_node, current_best_option, actual_parent_left, parent_left, parent_right, left_child, criterion, pre_split_dist, att_idx) and returns: current_best_option, current_best_option