class_docstr: This object adapts decision trees constructed by the various libraries such as scikit-learn's and XGBoost's
  DecisionTree(Regressor|Classifier) to dtreeviz.  As part of the construction process, the samples
  considered at decision and leaf nodes are saved as a big dictionary for use by the nodes.
  
  The decision trees for classifiers and regressors from scikit-learn and
  XGBoost etc... are built for efficiency, not ease of tree walking. This class
  wraps all of that information in an easy-to-use and consistent interface
  that hides the details of the various decision tree libraries.
  
  Field leaves is list of shadow leaf nodes.
  Field internal is list of shadow non-leaf nodes.
  Field root is the shadow tree root.
class_docstr_tok: ["This","object","adapts","decision","trees","constructed","by","the","various","libraries","such","as","scikit-learn","'s","and","XGBoost's","DecisionTree","(","Regressor|Classifier",")","to","dtreeviz",".","As","part","of","the","construction","process",",","the","samples","considered","at","decision","and","leaf","nodes","are","saved","as","a","big","dictionary","for","use","by","the","nodes",".","The","decision","trees","for","classifiers","and","regressors","from","scikit-learn","and","XGBoost","etc","...","are","built","for","efficiency",",","not","ease","of","tree","walking",".","This","class","wraps","all","of","that","information","in","an","easy-to-use","and","consistent","interface","that","hides","the","details","of","the","various","decision","tree","libraries",".","Field","leaves","is","list","of","shadow","leaf","nodes",".","Field","internal","is","list","of","shadow","non-leaf","nodes",".","Field","root","is","the","shadow","tree","root","."]
code: def get_split_node_heights(
      self, X_train, y_train, nbins
  ) -> Mapping[int, int]:
      class_values = np.unique(y_train)
      node_heights = {}
      for node in self.internal:
          # print(f"node feature {node.feature_name()}, id {node.id}")
          X_feature = X_train[:, node.feature()]
          if node.is_categorical_split():
              overall_feature_range = (
                  0,
                  len(np.unique(X_train[:, node.feature()])) - 1,
              )
          else:
              overall_feature_range = (
                  np.min(X_feature),
                  np.max(X_feature),
              )
  
          bins = np.linspace(
              overall_feature_range[0],
              overall_feature_range[1],
              nbins + 1,
          )
          X, y = X_feature[node.samples()], y_train[node.samples()]
  
          # in case there is a categorical split node, we can convert the values to numbers because we need them
          # only for getting the distribution values
          if node.is_categorical_split():
              X = pd.Series(X).astype("category").cat.codes
  
          X_hist = [X[y == cl] for cl in class_values]
          height_of_bins = np.zeros(nbins)
          for i, _ in enumerate(class_values):
              hist, foo = np.histogram(
                  X_hist[i], bins=bins, range=overall_feature_range
              )
              height_of_bins += hist
          node_heights[node.id] = np.max(height_of_bins)
          # print(f"\tmax={np.max(height_of_bins):2.0f}, heights={list(height_of_bins)}, {len(height_of_bins)} bins")
      return node_heights
  
imports: from abc import ABC, abstractmethod
  from numbers import Number
  from typing import List, Tuple, Mapping
  import numpy
  import pandas
  import sklearn
  from dtreeviz import utils
label_desc: Use image node_id 29 for calling the ShadowDecTree obj's underlying member method code with example usage: obj.get_split_node_heights(X_train, y_train, nbins) and returns: node_heights