class_docstr: A single decision tree.
  
  The primary point of this object is to parse many different tree types into a common format.
  
  Attributes
  ----------
  children_left : numpy.array
      A 1d array of length #nodes. The index ``i`` of this array contains the index of
      the left-child of the ``i-th`` node in the tree. An index of -1 is used to
      represent that the ``i-th`` node is a leaf/terminal node.
  
  children_right : numpy.array
      Same as ``children_left``, except it contains the index of the right child of
      each ``i-th`` node in the tree.
  
  children_default : numpy.array
      A 1d numpy array of length #nodes. The index ``i`` of this array contains either
      the index of the left-child / right-child of the ``i-th`` node in the tree,
      depending on whether the default split (for handling missing values) is left /
      right. An index of -1 is used to represent that the ``i-th`` node is a leaf
      node.
  
  features : numpy.array
      A 1d numpy array of length #nodes. The value at the ``i-th`` position is the
      index of the feature chosen for the split at node ``i``. Leaf nodes have no
      splits, so is -1.
  
  thresholds : numpy.array
      A 1d numpy array of length #nodes. The value at the ``i-th`` position is the
      threshold used for the split at node ``i``. Leaf nodes have no thresholds, so is
      -1.
  
  values : numpy.array
      A 1d numpy array of length #nodes. The index ``i`` of this array contains the
      raw predicted value that would be produced by node ``i`` if it were a leaf node.
  
  node_sample_weight : numpy.array
      A 1d numpy array of length #nodes. The index ``i`` contains the number of
      records (usually from the training data) that falls into node ``i``.
  
  max_depth : int
      The max depth of the tree.
class_docstr_tok: ["A","single","decision","tree",".","The","primary","point","of","this","object","is","to","parse","many","different","tree","types","into","a","common","format",".","Attributes","--","--","--","--","--","children_left",":","numpy.array","A","1d","array","of","length","#","nodes",".","The","index","``","i","``","of","this","array","contains","the","index","of","the","left-child","of","the","``","i-th","``","node","in","the","tree",".","An","index","of","-1","is","used","to","represent","that","the","``","i-th","``","node","is","a","leaf\/terminal","node",".","children_right",":","numpy.array","Same","as","``","children_left","``",",","except","it","contains","the","index","of","the","right","child","of","each","``","i-th","``","node","in","the","tree",".","children_default",":","numpy.array","A","1d","numpy","array","of","length","#","nodes",".","The","index","``","i","``","of","this","array","contains","either","the","index","of","the","left-child","\/","right-child","of","the","``","i-th","``","node","in","the","tree",",","depending","on","whether","the","default","split","(","for","handling","missing","values",")","is","left","\/","right",".","An","index","of","-1","is","used","to","represent","that","the","``","i-th","``","node","is","a","leaf","node",".","features",":","numpy.array","A","1d","numpy","array","of","length","#","nodes",".","The","value","at","the","``","i-th","``","position","is","the","index","of","the","feature","chosen","for","the","split","at","node","``","i","``",".","Leaf","nodes","have","no","splits",",","so","is","-1.","thresholds",":","numpy.array","A","1d","numpy","array","of","length","#","nodes",".","The","value","at","the","``","i-th","``","position","is","the","threshold","used","for","the","split","at","node","``","i","``",".","Leaf","nodes","have","no","thresholds",",","so","is","-1.","values",":","numpy.array","A","1d","numpy","array","of","length","#","nodes",".","The","index","``","i","``","of","this","array","contains","the","raw","predicted","value","that","would","be","produced","by","node","``","i","``","if","it","were","a","leaf","node",".","node_sample_weight",":","numpy.array","A","1d","numpy","array","of","length","#","nodes",".","The","index","``","i","``","contains","the","number","of","records","(","usually","from","the","training","data",")","that","falls","into","node","``","i","``",".","max_depth",":","int","The","max","depth","of","the","tree","."]
code: def __init__(
      self,
      tree,
      normalize=False,
      scaling=1.0,
      data=None,
      data_missing=None,
  ):
      assert_import("cext")
  
      if safe_isinstance(
          tree, ["sklearn.tree._tree.Tree", "econml.tree._tree.Tree"]
      ):
          self.children_left = tree.children_left.astype(np.int32)
          self.children_right = tree.children_right.astype(np.int32)
          self.children_default = (
              self.children_left
          )  # missing values not supported in sklearn
          self.features = tree.feature.astype(np.int32)
          self.thresholds = tree.threshold.astype(np.float64)
          self.values = tree.value.reshape(
              tree.value.shape[0],
              tree.value.shape[1] * tree.value.shape[2],
          )
          if normalize:
              self.values = (self.values.T / self.values.sum(1)).T
          self.values = self.values * scaling
          self.node_sample_weight = tree.weighted_n_node_samples.astype(
              np.float64
          )
  
      elif isinstance(tree, dict) and "features" in tree:
          self.children_left = tree["children_left"].astype(np.int32)
          self.children_right = tree["children_right"].astype(np.int32)
          self.children_default = tree["children_default"].astype(
              np.int32
          )
          self.features = tree["features"].astype(np.int32)
          self.thresholds = tree["thresholds"]
          self.values = tree["values"] * scaling
          self.node_sample_weight = tree["node_sample_weight"]
  
      # deprecated dictionary support (with sklearn singular style "feature" and "value" names)
      elif isinstance(tree, dict) and "children_left" in tree:
          self.children_left = tree["children_left"].astype(np.int32)
          self.children_right = tree["children_right"].astype(np.int32)
          self.children_default = tree["children_default"].astype(
              np.int32
          )
          self.features = tree["feature"].astype(np.int32)
          self.thresholds = tree["threshold"]
          self.values = tree["value"] * scaling
          self.node_sample_weight = tree["node_sample_weight"]
  
      elif safe_isinstance(
          tree,
          [
              "pyspark.ml.classification.DecisionTreeClassificationModel",
              "pyspark.ml.regression.DecisionTreeRegressionModel",
          ],
      ):
          # model._java_obj.numNodes() doesn't give leaves, need to recompute the size
          def getNumNodes(node, size):
              size = size + 1
              if node.subtreeDepth() == 0:
                  return size
              else:
                  size = getNumNodes(node.leftChild(), size)
                  return getNumNodes(node.rightChild(), size)
  
          num_nodes = getNumNodes(tree._java_obj.rootNode(), 0)
          self.children_left = np.full(num_nodes, -2, dtype=np.int32)
          self.children_right = np.full(num_nodes, -2, dtype=np.int32)
          self.children_default = np.full(num_nodes, -2, dtype=np.int32)
          self.features = np.full(num_nodes, -2, dtype=np.int32)
          self.thresholds = np.full(num_nodes, -2, dtype=np.float64)
          self.values = [-2] * num_nodes
          self.node_sample_weight = np.full(
              num_nodes, -2, dtype=np.float64
          )
  
          def buildTree(index, node):
              index = index + 1
              if tree._java_obj.getImpurity() == "variance":
                  self.values[index] = [
                      node.prediction()
                  ]  # prediction for the node
              else:
                  self.values[index] = [
                      e for e in node.impurityStats().stats()
                  ]  # for gini: NDarray(numLabel): 1 per label: number of item for each label which went through this node
              self.node_sample_weight[
                  index
              ] = (
                  node.impurityStats().count()
              )  # weighted count of element through this node
  
              if node.subtreeDepth() == 0:
                  return index
              else:
                  self.features[
                      index
                  ] = (
                      node.split().featureIndex()
                  )  # index of the feature we split on, not available for leaf, int
                  if str(node.split().getClass()).endswith(
                      "tree.CategoricalSplit"
                  ):
                      # Categorical split isn't implemented, TODO: could fake it by creating a fake node to split on the exact value?
                      raise NotImplementedError(
                          "CategoricalSplit are not yet implemented"
                      )
                  self.thresholds[
                      index
                  ] = (
                      node.split().threshold()
                  )  # threshold for the feature, not available for leaf, float
  
                  self.children_left[index] = index + 1
                  idx = buildTree(index, node.leftChild())
                  self.children_right[index] = idx + 1
                  idx = buildTree(idx, node.rightChild())
                  return idx
  
          buildTree(-1, tree._java_obj.rootNode())
          # default Not supported with mlib? (TODO)
          self.children_default = self.children_left
          self.values = np.asarray(self.values)
          if normalize:
              self.values = (self.values.T / self.values.sum(1)).T
          self.values = self.values * scaling
  
      # dictionary output from LightGBM `.dump_model()`
      elif isinstance(tree, dict) and "tree_structure" in tree:
          start = tree["tree_structure"]
          num_parents = tree["num_leaves"] - 1
          num_nodes = 2 * num_parents + 1
          self.children_left = np.empty(num_nodes, dtype=np.int32)
          self.children_right = np.empty(num_nodes, dtype=np.int32)
          self.children_default = np.empty(num_nodes, dtype=np.int32)
          self.features = np.empty(num_nodes, dtype=np.int32)
          self.thresholds = np.empty(num_nodes, dtype=np.float64)
          self.values = [-2 for _ in range(num_nodes)]
          self.node_sample_weight = np.empty(
              num_nodes, dtype=np.float64
          )
  
          # BFS traversal through the tree structure
          visited, queue = [], [start]
          while queue:
              vertex = queue.pop(
                  0
              )  # TODO(perf): benchmark this against deque.popleft()
              is_branch_node = "split_index" in vertex
              if is_branch_node:
                  vsplit_idx: int = vertex["split_index"]
                  if vsplit_idx in visited:
                      continue
  
                  left_child: dict = vertex["left_child"]
                  right_child: dict = vertex["right_child"]
                  left_is_branch_node = "split_index" in left_child
                  if left_is_branch_node:
                      self.children_left[vsplit_idx] = left_child[
                          "split_index"
                      ]
                  else:
                      self.children_left[vsplit_idx] = (
                          left_child["leaf_index"] + num_parents
                      )
                  right_is_branch_node = "split_index" in right_child
                  if right_is_branch_node:
                      self.children_right[vsplit_idx] = right_child[
                          "split_index"
                      ]
                  else:
                      self.children_right[vsplit_idx] = (
                          right_child["leaf_index"] + num_parents
                      )
                  if vertex["default_left"]:
                      self.children_default[
                          vsplit_idx
                      ] = self.children_left[vsplit_idx]
                  else:
                      self.children_default[
                          vsplit_idx
                      ] = self.children_right[vsplit_idx]
  
                  self.features[vsplit_idx] = vertex["split_feature"]
                  self.thresholds[vsplit_idx] = vertex["threshold"]
                  self.values[vsplit_idx] = [vertex["internal_value"]]
                  self.node_sample_weight[vsplit_idx] = vertex[
                      "internal_count"
                  ]
                  visited.append(vsplit_idx)
                  queue.append(left_child)
                  queue.append(right_child)
              else:
                  # NOTE: If "leaf_index" is not present as a key, it means we have a
                  # stump tree. I.e., num_nodes=1.
                  vleaf_idx: int = (
                      vertex.get("leaf_index", 0) + num_parents
                  )
                  self.children_left[vleaf_idx] = -1
                  self.children_right[vleaf_idx] = -1
                  self.children_default[vleaf_idx] = -1
                  self.features[vleaf_idx] = -1
                  self.children_left[vleaf_idx] = -1
                  self.children_right[vleaf_idx] = -1
                  self.children_default[vleaf_idx] = -1
                  self.features[vleaf_idx] = -1
                  self.thresholds[vleaf_idx] = -1
                  self.values[vleaf_idx] = [vertex["leaf_value"]]
                  # FIXME: "leaf_count" currently doesn't exist if we have a stump tree.
                  # We should be technically be assigning the number of samples used to
                  # train the model as the weight here, but unfortunately this info is
                  # currently unavailable in `tree`, so we set to 0 first.
                  # cf. https://github.com/microsoft/LightGBM/issues/5962
                  self.node_sample_weight[vleaf_idx] = vertex.get(
                      "leaf_count", 0
                  )
          self.values = np.asarray(self.values)
          self.values = np.multiply(self.values, scaling)
  
      elif isinstance(tree, dict) and "nodeid" in tree:
          """Directly create tree given the JSON dump (with stats) of a XGBoost model."""
  
          def max_id(node):
              if "children" in node:
                  return max(
                      node["nodeid"],
                      *[max_id(n) for n in node["children"]],
                  )
              else:
                  return node["nodeid"]
  
          m = max_id(tree) + 1
          self.children_left = -np.ones(m, dtype=np.int32)
          self.children_right = -np.ones(m, dtype=np.int32)
          self.children_default = -np.ones(m, dtype=np.int32)
          self.features = -np.ones(m, dtype=np.int32)
          self.thresholds = np.zeros(m, dtype=np.float64)
          self.values = np.zeros((m, 1), dtype=np.float64)
          self.node_sample_weight = np.empty(m, dtype=np.float64)
  
          def extract_data(node, tree):
              i = node["nodeid"]
              tree.node_sample_weight[i] = node["cover"]
  
              if "children" in node:
                  tree.children_left[i] = node["yes"]
                  tree.children_right[i] = node["no"]
                  tree.children_default[i] = node["missing"]
                  tree.features[i] = node["split"]
                  tree.thresholds[i] = node["split_condition"]
  
                  for n in node["children"]:
                      extract_data(n, tree)
              elif "leaf" in node:
                  tree.values[i] = node["leaf"] * scaling
  
          extract_data(tree, self)
  
      elif isinstance(tree, str):
          """Build a tree from a text dump (with stats) of xgboost."""
  
          nodes = [t.lstrip() for t in tree[:-1].split("\n")]
          nodes_dict = {}
          for n in nodes:
              nodes_dict[int(n.split(":")[0])] = n.split(":")[1]
          m = max(nodes_dict.keys()) + 1
          children_left = -1 * np.ones(m, dtype="int32")
          children_right = -1 * np.ones(m, dtype="int32")
          children_default = -1 * np.ones(m, dtype="int32")
          features = -2 * np.ones(m, dtype="int32")
          thresholds = -1 * np.ones(m, dtype="float64")
          values = 1 * np.ones(m, dtype="float64")
          node_sample_weight = np.zeros(m, dtype="float64")
          values_lst = list(nodes_dict.values())
          keys_lst = list(nodes_dict.keys())
          for i in range(0, len(keys_lst)):
              value = values_lst[i]
              key = keys_lst[i]
              if "leaf" in value:
                  # Extract values
                  val = float(value.split("leaf=")[1].split(",")[0])
                  node_sample_weight_val = float(
                      value.split("cover=")[1]
                  )
                  # Append to lists
                  values[key] = val
                  node_sample_weight[key] = node_sample_weight_val
              else:
                  c_left = int(value.split("yes=")[1].split(",")[0])
                  c_right = int(value.split("no=")[1].split(",")[0])
                  c_default = int(
                      value.split("missing=")[1].split(",")[0]
                  )
                  feat_thres = value.split(" ")[0]
                  if "<" in feat_thres:
                      feature = int(feat_thres.split("<")[0][2:])
                      threshold = float(feat_thres.split("<")[1][:-1])
                  if "=" in feat_thres:
                      feature = int(feat_thres.split("=")[0][2:])
                      threshold = float(feat_thres.split("=")[1][:-1])
                  node_sample_weight_val = float(
                      value.split("cover=")[1].split(",")[0]
                  )
                  children_left[key] = c_left
                  children_right[key] = c_right
                  children_default[key] = c_default
                  features[key] = feature
                  thresholds[key] = threshold
                  node_sample_weight[key] = node_sample_weight_val
  
          self.children_left = children_left
          self.children_right = children_right
          self.children_default = children_default
          self.features = features
          self.thresholds = thresholds
          self.values = values[:, np.newaxis] * scaling
          self.node_sample_weight = node_sample_weight
      else:
          raise TypeError(
              "Unknown input to SingleTree constructor: " + str(tree)
          )
  
      # Re-compute the number of samples that pass through each node if we are given data
      if data is not None and data_missing is not None:
          self.node_sample_weight.fill(0.0)
          _cext.dense_tree_update_weights(
              self.children_left,
              self.children_right,
              self.children_default,
              self.features,
              self.thresholds,
              self.values,
              1,
              self.node_sample_weight,
              data,
              data_missing,
          )
  
      # we compute the expectations to make sure they follow the SHAP logic
      self.max_depth = _cext.compute_expectations(
          self.children_left,
          self.children_right,
          self.node_sample_weight,
          self.values,
      )
  
imports: import json
  import os
  import struct
  import time
  import warnings
  import numpy
  import pandas
  import scipy.sparse
  import scipy.special
  from packaging import version
  from ..None import maskers
  from .._explanation import Explanation
  from ..utils import assert_import, record_import_error, safe_isinstance
  from ..utils._exceptions import DimensionError, ExplainerError, InvalidFeaturePerturbationError, InvalidMaskerError, InvalidModelError
  from ..utils._legacy import DenseData
  from ._explainer import Explainer
label_desc: Use image node_id 1 to create a new SingleTree object  with example: obj = SingleTree(tree, normalize, scaling, data, data_missing)