code: def _harmonize_tensor(
      new_feature_idxs,
      new_bounds,
      new_bins,
      old_feature_idxs,
      old_bounds,
      old_bins,
      old_mapping,
      old_tensor,
      bin_evidence_weight,
  ):
      # TODO: don't pass in new_bound and old_bounds.  We use the bounds to proportion
      # weights at the tail ends of the graphs, but the problem with that is that
      # you can have outliers that'll stretch the weight very thin.  If you have an
      # old_min of -10000000 but the lowest old cut is at 0.  If you have a new cut
      # at -100, it'll put very very close to 0 weight in the region from -100 to 0.
      # Instead of using the min/max to proportionate, we should start from the
      # lowest new_bins cut and then find all the other models that have that exact
      # same lowest cut (averaging their results). There must be at least 1 model with that cut.  After we
      # find that other model, we can use the weights in existing bin_weights to
      # proportionate the regions from the new lowest bin cut to the old lowest bin cut.
      # Do the same for the highest bin cut.  One issue is that the model(s) that have
      # the exact lowest bin cut are unlikely to share the lowest old cut, so we
      # proportionate the bin in the other model to the other model's next cut that is
      # greater than the old model's lowest cut.
      # eg:  new:      |    |            |   |    |
      #      old:                        |        |
      #   other1:      |    |   proprotion   |
      #   other2:      |        proportion        |
      # One wrinkle is that for pairs, we'll be using the pair cuts and we need to
      # one-dimensionalize any existing pair weights onto their respective 1D axies
      # before proportionating them.  Annother issue is that we might not even have
      # another term_feature that uses some particular feature that we use in our model
      # so we don't have any weights.  We can solve that issue by dropping any feature's
      # bins for terms that we have no information for.  After we do this we'll have
      # guaranteed that we only have new bin cuts for feature axies that we have inside
      # the bin level that we're handling!
  
      old_feature_idxs = list(old_feature_idxs)
  
      axes = []
      for feature_idx in new_feature_idxs:
          old_idx = old_feature_idxs.index(feature_idx)
          old_feature_idxs[
              old_idx
          ] = -1  # in case we have duplicate feature idxs
          axes.append(old_idx)
  
      if bin_evidence_weight is not None:
          bin_evidence_weight = bin_evidence_weight.transpose(
              tuple(axes)
          )
  
      n_multiclasses = 1
      if len(axes) != old_tensor.ndim:
          # multiclass. The last dimension always stays put
          axes.append(len(axes))
          n_multiclasses = old_tensor.shape[-1]
  
      old_tensor = old_tensor.transpose(tuple(axes))
  
      mapping = []
      lookups = []
      percentages = []
      for feature_idx in new_feature_idxs:
          old_bin_levels = old_bins[feature_idx]
          old_feature_bins = old_bin_levels[
              min(len(old_bin_levels), len(old_feature_idxs)) - 1
          ]
  
          mapping_levels = old_mapping[feature_idx]
          old_feature_mapping = mapping_levels[
              min(len(mapping_levels), len(old_feature_idxs)) - 1
          ]
          if old_feature_mapping is None:
              old_feature_mapping = list(
                  (x,)
                  for x in range(
                      len(old_feature_bins)
                      + (2 if isinstance(old_feature_bins, dict) else 3)
                  )
              )
          mapping.append(old_feature_mapping)
  
          new_bin_levels = new_bins[feature_idx]
          new_feature_bins = new_bin_levels[
              min(len(new_bin_levels), len(new_feature_idxs)) - 1
          ]
  
          if isinstance(new_feature_bins, dict):
              # categorical feature
  
              old_reversed = dict()
              for category, bin_idx in old_feature_bins.items():
                  category_list = old_reversed.get(bin_idx)
                  if category_list is None:
                      old_reversed[bin_idx] = [category]
                  else:
                      category_list.append(category)
  
              new_reversed = dict()
              for category, bin_idx in new_feature_bins.items():
                  category_list = new_reversed.get(bin_idx)
                  if category_list is None:
                      new_reversed[bin_idx] = [category]
                  else:
                      category_list.append(category)
              new_reversed = sorted(new_reversed.items())
  
              lookup = [0]
              percentage = [1.0]
              for _, new_categories in new_reversed:
                  # if there are two items in new_categories then they should both resolve
                  # to the same index in old_feature_bins otherwise they would have been
                  # split into two categories
                  old_bin_idx = old_feature_bins.get(
                      new_categories[0], -1
                  )
                  if 0 <= old_bin_idx:
                      percentage.append(
                          len(new_categories)
                          / len(old_reversed[old_bin_idx])
                      )
                  else:
                      # map to the unknown bin for scores, but take no percentage of the weight
                      percentage.append(0.0)
                  lookup.append(old_bin_idx)
              percentage.append(1.0)
              lookup.append(-1)
          else:
              # continuous feature
  
              lookup = list(
                  np.searchsorted(
                      old_feature_bins, new_feature_bins, side="left"
                  )
                  + 1
              )
              lookup.append(len(old_feature_bins) + 1)
  
              percentage = [1.0]
              if len(new_feature_bins) == 0:
                  percentage.append(1.0)
              else:
                  for new_idx_minus_one, old_idx in enumerate(lookup):
                      if new_idx_minus_one == 0:
                          new_low = new_bounds[feature_idx, 0]
                          # TODO: if nan OR out of bounds from the cuts, estimate it.
                          # If -inf or +inf, change it to min/max for float
                      else:
                          new_low = new_feature_bins[
                              new_idx_minus_one - 1
                          ]
  
                      if len(new_feature_bins) <= new_idx_minus_one:
                          new_high = new_bounds[feature_idx, 1]
                          # TODO: if nan OR out of bounds from the cuts, estimate it.
                          # If -inf or +inf, change it to min/max for float
                      else:
                          new_high = new_feature_bins[new_idx_minus_one]
  
                      if old_idx == 1:
                          old_low = old_bounds[feature_idx, 0]
                          # TODO: if nan OR out of bounds from the cuts, estimate it.
                          # If -inf or +inf, change it to min/max for float
                      else:
                          old_low = old_feature_bins[old_idx - 2]
  
                      if len(old_feature_bins) < old_idx:
                          old_high = old_bounds[feature_idx, 1]
                          # TODO: if nan OR out of bounds from the cuts, estimate it.
                          # If -inf or +inf, change it to min/max for float
                      else:
                          old_high = old_feature_bins[old_idx - 1]
  
                      if old_high <= new_low or new_high <= old_low:
                          # if there are bins in the area above where the old data extended, then
                          # we'll have zero contribution in the old data where these new bins are
                          # located
                          percentage.append(0.0)
                      else:
                          if new_low < old_low:
                              # this can't happen except at the lowest bin where the new min can be
                              # lower than the old min.  In that case we know the old data
                              # had zero contribution between the new min to the old min.
                              new_low = old_low
  
                          if old_high < new_high:
                              # this can't happen except at the lowest bin where the new max can be
                              # higher than the old max.  In that case we know the old data
                              # had zero contribution between the new max to the old max.
                              new_high = old_high
  
                          percentage.append(
                              (new_high - new_low)
                              / (old_high - old_low)
                          )
  
              percentage.append(1.0)
              lookup.insert(0, 0)
              lookup.append(-1)
  
          lookups.append(lookup)
          percentages.append(percentage)
  
      new_shape = tuple(len(lookup) for lookup in lookups)
      n_cells = np.prod(new_shape)
  
      if 1 < n_multiclasses:
          # for multiclass we need to add another dimension for the scores of each class
          new_shape += (n_multiclasses,)
  
      lookups.reverse()
      percentages.reverse()
      mapping.reverse()
  
      # now we need to inflate it
      intermediate_shape = (
          (n_cells, n_multiclasses) if 1 < n_multiclasses else n_cells
      )
      new_tensor = np.empty(intermediate_shape, np.float64)
      for cell_idx in range(n_cells):
          remainder = cell_idx
          old_reversed_bin_idxs = []
          frac = 1.0
          for lookup, percentage in zip(lookups, percentages):
              n_bins = len(lookup)
              new_bin_idx = remainder % n_bins
              remainder //= n_bins
              old_reversed_bin_idxs.append(lookup[new_bin_idx])
              frac *= percentage[new_bin_idx]
  
          cell_map = [
              map_bins[bin_idx]
              for map_bins, bin_idx in zip(
                  mapping, old_reversed_bin_idxs
              )
          ]
          n_cells2 = np.prod([len(x) for x in cell_map])
          val = (
              0
              if n_multiclasses == 1
              else np.zeros(n_multiclasses, np.float64)
          )
          total_weight = 0.0
          for cell2_idx in range(n_cells2):
              remainder2 = cell2_idx
              old_reversed_bin2_idxs = []
              for lookup2 in cell_map:
                  n_bins2 = len(lookup2)
                  new_bin2_idx = remainder2 % n_bins2
                  remainder2 //= n_bins2
                  old_reversed_bin2_idxs.append(lookup2[new_bin2_idx])
              update = old_tensor[
                  tuple(reversed(old_reversed_bin2_idxs))
              ]
              if n_cells2 == 1:
                  # if there's just one cell, which is typical, don't
                  # incur the floating point loss in precision
                  val = update
              else:
                  if bin_evidence_weight is not None:
                      evidence_weight = bin_evidence_weight[
                          tuple(reversed(old_reversed_bin2_idxs))
                      ]
                      update *= evidence_weight
                      total_weight += evidence_weight
                  val += update
          if bin_evidence_weight is None:
              # we're doing a bin weight and NOT a score tensor
              val *= frac
          elif total_weight != 0.0:
              # we're doing scores and we need to take a weighted average
              # but if the total_weight is zero then val should be zero and
              # our update should still be zero, which it already is
              val = val / total_weight
          new_tensor[cell_idx] = val
      new_tensor = new_tensor.reshape(new_shape)
      return new_tensor
  
file_path: interpret/python/interpret-core/interpret/glassbox/_ebm/_merge_ebms.py
imports: from math import isnan
  from ._utils import remove_unused_higher_bins, order_terms, generate_term_names, process_terms, convert_categorical_to_continuous, deduplicate_bins
  from ...utils._native import Native
  import numpy
  import warnings
  from itertools import count, chain
  import logging
label_desc: Use image node_id 1 for calling a global function with example usage: _harmonize_tensor(new_feature_idxs, new_bounds, new_bins, old_feature_idxs, old_bounds, old_bins, old_mapping, old_tensor, bin_evidence_weight) and returns: new_tensor