class_docstr: FIGSExt (sum of trees) classifier.
  Fast Interpretable Greedy-Tree Sums (FIGS) is an algorithm for fitting concise rule-based models.
  Specifically, FIGS generalizes CART to simultaneously grow a flexible number of trees in a summation.
  The total number of splits across all the trees can be restricted by a pre-specified threshold, keeping the model interpretable.
  Experiments across a wide array of real-world datasets show that FIGS achieves state-of-the-art prediction performance when restricted to just a few splits (e.g. less than 20).
  https://arxiv.org/abs/2201.11931
docstr: Params
  ------
  _sample_weight: array-like of shape (n_samples,), default=None
      Sample weights. If None, then samples are equally weighted.
      Splits that would create child nodes with net zero or negative weight
      are ignored while searching for a split in each node.
code: def fit(
      self,
      X,
      y=None,
      feature_names=None,
      verbose=False,
      sample_weight=None,
  ):
      """
      Params
      ------
      _sample_weight: array-like of shape (n_samples,), default=None
          Sample weights. If None, then samples are equally weighted.
          Splits that would create child nodes with net zero or negative weight
          are ignored while searching for a split in each node.
      """
  
      if self.prediction_task == "classification":
          self.classes_, y = np.unique(
              y, return_inverse=True
          )  # deals with str inputs
      X, y = check_X_y(X, y)
      y = y.astype(float)
      if feature_names is not None:
          self.feature_names_ = feature_names
  
      self.trees_ = []  # list of the root nodes of added trees
      self.complexity_ = 0  # tracks the number of rules in the model
      y_predictions_per_tree = {}  # predictions for each tree
      y_residuals_per_tree = {}  # based on predictions above
  
      def _update_tree_preds(n_iter):
          for k in range(n_iter):
              for tree_num_, tree_ in enumerate(self.trees_):
                  y_residuals_per_tree[tree_num_] = deepcopy(y)
  
                  # subtract predictions of all other trees
                  for tree_num_2_ in range(len(self.trees_)):
                      if not tree_num_2_ == tree_num_:
                          y_residuals_per_tree[
                              tree_num_
                          ] -= y_predictions_per_tree[tree_num_2_]
                  tree_.update_values(
                      X, y_residuals_per_tree[tree_num_]
                  )
                  y_predictions_per_tree[
                      tree_num_
                  ] = self._predict_tree(self.trees_[tree_num_], X)
  
      # set up initial potential_splits
      # everything in potential_splits either is_root (so it can be added directly to self.trees_)
      # or it is a child of a root node that has already been added
      idxs = np.ones(X.shape[0], dtype=bool)
      node_init = self._construct_node_with_stump(
          X=X,
          y=y,
          idxs=idxs,
          tree_num=-1,
          sample_weight=sample_weight,
          max_features=self.max_features,
      )
      potential_splits = [node_init]
      if self.include_linear and idxs.sum() >= 5:
          node_init_linear = self._construct_node_linear(
              X=X,
              y=y,
              idxs=idxs,
              tree_num=-1,
              sample_weight=sample_weight,
          )
          potential_splits.append(node_init_linear)
      for node in potential_splits:
          node.setattrs(is_root=True)
      potential_splits = sorted(
          potential_splits, key=lambda x: x.impurity_reduction
      )
  
      # start the greedy fitting algorithm
      finished = False
      while len(potential_splits) > 0 and not finished:
          # print('potential_splits', [str(s) for s in potential_splits])
          # get node with max impurity_reduction (since it's sorted)
          split_node = potential_splits.pop()
  
          # don't split on node
          if split_node.impurity_reduction < self.min_impurity_decrease:
              finished = True
              break
  
          # split on node
          if verbose:
              print("\nadding " + str(split_node))
          self.complexity_ += 1
  
          # if added a tree root
          if split_node.is_root:
              # start a new tree
              self.trees_.append(split_node)
  
              # update tree_num
              for node_ in [
                  split_node,
                  split_node.left_temp,
                  split_node.right_temp,
              ]:
                  if node_ is not None:
                      node_.tree_num = len(self.trees_) - 1
  
              # add new root potential node
              node_new_root = Node(
                  is_root=True,
                  idxs=np.ones(X.shape[0], dtype=bool),
                  tree_num=-1,
                  split_or_linear=split_node.split_or_linear,
              )
              potential_splits.append(node_new_root)
  
          # add children to potential splits (note this doesn't currently add linear potential splits)
          if split_node.split_or_linear == "split":
              # assign left_temp, right_temp to be proper children
              # (basically adds them to tree in predict method)
              split_node.setattrs(
                  left=split_node.left_temp, right=split_node.right_temp
              )
  
              # add children to potential_splits
              potential_splits.append(split_node.left)
              potential_splits.append(split_node.right)
  
          # update predictions for altered tree
          for tree_num_ in range(len(self.trees_)):
              y_predictions_per_tree[tree_num_] = self._predict_tree(
                  self.trees_[tree_num_], X
              )
          # dummy 0 preds for possible new trees
          y_predictions_per_tree[-1] = np.zeros(X.shape[0])
  
          # update residuals for each tree
          # -1 is key for potential new tree
          for tree_num_ in list(range(len(self.trees_))) + [-1]:
              y_residuals_per_tree[tree_num_] = deepcopy(y)
  
              # subtract predictions of all other trees
              for tree_num_2_ in range(len(self.trees_)):
                  if not tree_num_2_ == tree_num_:
                      y_residuals_per_tree[
                          tree_num_
                      ] -= y_predictions_per_tree[tree_num_2_]
  
          _update_tree_preds(self.k1)
  
          # recompute all impurities + update potential_split children
          potential_splits_new = []
          for potential_split in potential_splits:
              y_target = y_residuals_per_tree[potential_split.tree_num]
  
              if potential_split.split_or_linear == "split":
                  # re-calculate the best split
                  potential_split_updated = (
                      self._construct_node_with_stump(
                          X=X,
                          y=y_target,
                          idxs=potential_split.idxs,
                          tree_num=potential_split.tree_num,
                          sample_weight=sample_weight,
                          max_features=self.max_features,
                      )
                  )
  
                  # need to preserve certain attributes from before (value at this split + is_root)
                  # value may change because residuals may have changed, but we want it to store the value from before
                  potential_split.setattrs(
                      feature=potential_split_updated.feature,
                      threshold=potential_split_updated.threshold,
                      impurity_reduction=potential_split_updated.impurity_reduction,
                      left_temp=potential_split_updated.left_temp,
                      right_temp=potential_split_updated.right_temp,
                  )
              elif potential_split.split_or_linear == "linear":
                  assert (
                      potential_split.is_root
                  ), "Currently, linear node only supported as root"
                  assert (
                      potential_split.idxs.sum() == X.shape[0]
                  ), "Currently, linear node only supported as root"
                  potential_split_updated = self._construct_node_linear(
                      idxs=potential_split.idxs,
                      X=X,
                      y=y_target,
                      tree_num=potential_split.tree_num,
                      sample_weight=sample_weight,
                  )
  
                  # don't need to retain anything from before (besides maybe is_root)
                  potential_split.setattrs(
                      feature=potential_split_updated.feature,
                      impurity_reduction=potential_split_updated.impurity_reduction,
                      value=potential_split_updated.value,
                  )
  
              # this is a valid split
              if potential_split.impurity_reduction is not None:
                  potential_splits_new.append(potential_split)
  
          # sort so largest impurity reduction comes last (should probs make this a heap later)
          potential_splits = sorted(
              potential_splits_new, key=lambda x: x.impurity_reduction
          )
          if verbose:
              print(self)
          if (
              self.max_rules is not None
              and self.complexity_ >= self.max_rules
          ):
              finished = True
              break
  
      _update_tree_preds(self.k2)
  
      # potentially fit linear model on the tree preds
      if self.posthoc_ridge:
          if self.prediction_task == "regression":
              self.weighted_model_ = RidgeCV(
                  alphas=(0.01, 0.1, 0.5, 1.0, 5, 10)
              )
          elif self.prediction_task == "classification":
              self.weighted_model_ = RidgeClassifierCV(
                  alphas=(0.01, 0.1, 0.5, 1.0, 5, 10)
              )
          X_feats = self._extract_tree_predictions(X)
          self.weighted_model_.fit(X_feats, y)
      return self
  
imports: from copy import deepcopy
  import numpy
  from matplotlib import pyplot
  from sklearn import datasets
  from sklearn import tree
  from sklearn.base import BaseEstimator
  from sklearn.linear_model import RidgeCV, RidgeClassifierCV
  from sklearn.model_selection import train_test_split
  from sklearn.tree import plot_tree
  from sklearn.utils import check_X_y
  from imodels.tree.viz_utils import extract_sklearn_tree_from_figs
label_desc: Use image node_id 6 for calling the FIGSExt obj's underlying member method code with example usage: obj.fit(X, y, feature_names, verbose, sample_weight) and returns: self