class_docstr: Jointly trains 2 surrogate decision trees.
  Refines diff (or sim) regions by sample generation.
code: def fit(self, x1, y1, x2, y2, path=None, depth=0):
      if path is None:
          path = []
      # length check
      if len(y1) == 0 and len(y2) == 0:
          return None, None
      if len(y1) == 0:
          return None, self.fit1(x2, y2, path, depth)
      elif len(y2) == 0:
          return self.fit1(x1, y1, path, depth), None
  
      # all same check
      if self.all_same(y1) and self.all_same(y2):
          return {
              "val": y1[0],
              "depth": depth,
              "ispure": True,
              "dist": np.bincount(y1),
              "path": path,
          }, {
              "val": y2[0],
              "depth": depth,
              "ispure": True,
              "dist": np.bincount(y2),
              "path": path,
          }
  
      if self.all_same(y1):
          return {
              "val": y1[0],
              "depth": depth,
              "ispure": True,
              "dist": np.bincount(y1),
              "path": path,
          }, self.fit1(x2, y2, path, depth)
      if self.all_same(y2):
          return self.fit1(x1, y1, path, depth), {
              "val": y2[0],
              "depth": depth,
              "ispure": True,
              "dist": np.bincount(y2),
              "path": path,
          }
  
      if depth >= self.max_depth:
          # find majority class labels
          label1 = np.argmax(np.bincount(y1))
          label2 = np.argmax(np.bincount(y2))
          return {
              "val": label1,
              "depth": depth,
              "ispure": self.all_same(y1),
              "dist": np.bincount(y1),
              "path": path,
          }, {
              "val": label2,
              "depth": depth,
              "ispure": self.all_same(y2),
              "dist": np.bincount(y2),
              "path": path,
          }
  
      col1, cutoff1, entropy1 = self.find_best_split_of_all(x1, y1)
      col2, cutoff2, entropy2 = self.find_best_split_of_all(x2, y2)
      col, cutoff, entropy = self.find_best_split_of_all_double(
          x1, y1, x2, y2
      )
  
      # print(col1, cutoff1, entropy1, col2, cutoff2, entropy2, col, cutoff, entropy)
  
      if self.continue_same_prefix(
          col1,
          cutoff1,
          entropy1,
          col2,
          cutoff2,
          entropy2,
          col,
          cutoff,
          entropy,
      ):
          # print(f"cont. with e1={entropy1}, e2={entropy2}, e={entropy}")
  
          # initialize 2 par_nodes corresponding to two trees
          par_node1 = {
              "col": self.feature_names[col],
              "index_col": col,
              "cutoff": cutoff,
              "val": np.argmax(np.bincount(y1)),
              "depth": depth,
              "dist": np.bincount(y1),
          }
          # print('common',par_node1)
          par_node2 = {
              "col": self.feature_names[col],
              "index_col": col,
              "cutoff": cutoff,
              "val": np.argmax(np.bincount(y2)),
              "depth": depth,
              "dist": np.bincount(y2),
          }
  
          # splits for tree1
          y1_left = y1[x1[:, col] < cutoff]
          y1_right = y1[x1[:, col] >= cutoff]
  
          # splits for tree2
          y2_left = y2[x2[:, col] < cutoff]
          y2_right = y2[x2[:, col] >= cutoff]
  
          # create paths
          left_path = path + [
              (self.feature_names[col], "<", np.around(cutoff, 4))
          ]
          right_path = path + [
              (self.feature_names[col], ">=", np.around(cutoff, 4))
          ]
  
          par_node1["left"], par_node2["left"] = self.fit(
              x1[x1[:, col] < cutoff],
              y1_left,
              x2[x2[:, col] < cutoff],
              y2_left,
              left_path,
              depth + 1,
          )
          par_node1["right"], par_node2["right"] = self.fit(
              x1[x1[:, col] >= cutoff],
              y1_right,
              x2[x2[:, col] >= cutoff],
              y2_right,
              right_path,
              depth + 1,
          )
  
          self.depth1 += 1
          self.depth2 += 1
  
          self.tree1 = par_node1
          self.tree2 = par_node2
      else:
          print(
              f"diverge with e1={entropy1}, e2={entropy2}, e={entropy}"
          )
          # for tree1, use col1, cutoff1 to partition the data
          y1_left = y1[x1[:, col1] < cutoff1]
          y1_right = y1[x1[:, col1] >= cutoff1]
          # create paths
          left_path1 = path + [
              (self.feature_names[col1], "<", np.around(cutoff1, 4))
          ]
          right_path1 = path + [
              (self.feature_names[col1], ">=", np.around(cutoff1, 4))
          ]
          par_node1 = {
              "col": self.feature_names[col1],
              "index_col": col1,
              "cutoff": cutoff1,
              "val": np.argmax(np.bincount(y1)),
              "depth": depth,
              "dist": np.bincount(y1),
              # recursive calls for tree1
              "left": self.fit1(
                  x1[x1[:, col1] < cutoff1],
                  y1_left,
                  left_path1,
                  depth + 1,
              ),
              "right": self.fit1(
                  x1[x1[:, col1] >= cutoff1],
                  y1_right,
                  right_path1,
                  depth + 1,
              ),
          }
  
          # print('1',par_node1)
  
          # for tree2, use col2, cutoff2 to partition the data
          y2_left = y2[x2[:, col2] < cutoff2]
          y2_right = y2[x2[:, col2] >= cutoff2]
          # create paths
          left_path2 = path + [
              (self.feature_names[col2], "<", np.around(cutoff2, 4))
          ]
          right_path2 = path + [
              (self.feature_names[col2], ">=", np.around(cutoff2, 4))
          ]
          par_node2 = {
              "col": self.feature_names[col2],
              "index_col": col2,
              "cutoff": cutoff2,
              "val": np.argmax(np.bincount(y2)),
              "depth": depth,
              "dist": np.bincount(y2),
              # recursive calls for tree2
              "left": self.fit1(
                  x2[x2[:, col2] < cutoff2],
                  y2_left,
                  left_path2,
                  depth + 1,
              ),
              "right": self.fit1(
                  x2[x2[:, col2] >= cutoff2],
                  y2_right,
                  right_path2,
                  depth + 1,
              ),
          }
  
          # print('2',par_node2)
  
          self.depth1 += 1
          self.depth2 += 1
  
      return par_node1, par_node2
  
imports: import numpy
  import pandas
  from aix360.algorithms.imd.rule import Rule
label_desc: Use image node_id 5 for calling the JointSurrogateTree obj's underlying member method code with example usage: obj.fit(x1, y1, x2, y2, path, depth) and returns: par_node1,  par_node2, None,  None, None,  self, dict,  dict, dict,  self, self,  dict, dict,  dict, self,  None