text: """
  Discretizers classes, to be used in lime_tabular
  """
  import numpy as np
  import sklearn
  import sklearn.tree
  from sklearn.utils import check_random_state
  from sklearn.preprocessing import KBinsDiscretizer
  from abc import ABCMeta, abstractmethod
  from mdlp.discretization import MDLP
  from sklearn.cluster import KMeans
  from yellowbrick.cluster import KElbowVisualizer
  import matplotlib.pyplot as plt
  
  class BaseDiscretizer():
      """
      Abstract class - Build a class that inherits from this class to implement
      a custom discretizer.
      Method bins() is to be redefined in the child class, as it is the actual
      custom part of the discretizer.
      """
  
      __metaclass__ = ABCMeta  # abstract class
  
      def __init__(self, data, categorical_features, feature_names, labels=None, random_state=None):
          """Initializer
          Args:
              data: numpy 2d array
              categorical_features: list of indices (ints) corresponding to the
                  categorical columns. These features will not be discretized.
                  Everything else will be considered continuous, and will be
                  discretized.
              categorical_names: map from int to list of names, where
                  categorical_names[x][y] represents the name of the yth value of
                  column x.
              feature_names: list of names (strings) corresponding to the columns
                  in the training data.
          """
          self.to_discretize = ([x for x in range(data.shape[1])
                                if x not in categorical_features])
          self.names = {}
          self.lambdas = {}
          self.means = {}
          self.stds = {}
          self.mins = {}
          self.maxs = {}
          self.random_state = check_random_state(random_state)
          self.feature_names = feature_names
  
          # To override when implementing a custom binning
          bins = self.bins(data, labels)
          bins = [np.unique(x) for x in bins]
          self.bins_size = dict()
  
          for feature, qts in zip(self.to_discretize, bins):
              n_bins = qts.shape[0]  # Actually number of borders (= #bins-1)
              boundaries = np.min(data[:, feature]), np.max(data[:, feature])
              name = feature_names[feature]
              self.bins_size[name] = n_bins
  
              self.names[feature] = ['%s <= %.2f' % (name, qts[0])]
              for i in range(n_bins - 1):
                  self.names[feature].append('%.2f < %s <= %.2f' %
                                             (qts[i], name, qts[i + 1]))
              self.names[feature].append('%s > %.2f' % (name, qts[n_bins - 1]))
  
              self.lambdas[feature] = lambda x, qts=qts: np.searchsorted(qts, x)
              discretized = self.lambdas[feature](data[:, feature])
  
              self.means[feature] = []
              self.stds[feature] = []
              for x in range(n_bins + 1):
                  selection = data[discretized == x, feature]
                  mean = 0 if len(selection) == 0 else np.mean(selection)
                  self.means[feature].append(mean)
                  std = 0 if len(selection) == 0 else np.std(selection)
                  std += 0.00000000001
                  self.stds[feature].append(std)
              self.mins[feature] = [boundaries[0]] + qts.tolist()
              self.maxs[feature] = qts.tolist() + [boundaries[1]]
  
      @abstractmethod
      def bins(self, data, labels):
          """
          To be overridden
          Returns for each feature to discretize the boundaries
          that form each bin of the discretizer
          """
          raise NotImplementedError("Must override bins() method")
  
      def discretize(self, data):
          """Discretizes the data.
          Args:
              data: numpy 2d or 1d array
          Returns:
              numpy array of same dimension, discretized.
          """
          ret = data.copy()
          for feature in self.lambdas:
              if len(data.shape) == 1:
                  ret[feature] = int(self.lambdas[feature](ret[feature]))
              else:
                  ret[:, feature] = self.lambdas[feature](
                      ret[:, feature]).astype(int)
          return ret
  
      def undiscretize(self, data):
          ret = data.copy()
          for feature in self.means:
              mins = self.mins[feature]
              maxs = self.maxs[feature]
              means = self.means[feature]
              stds = self.stds[feature]
  
              def get_inverse(q):
                  return max(mins[q],
                             min(self.random_state.normal(means[q], stds[q]), maxs[q]))
              if len(data.shape) == 1:
                  q = int(ret[feature])
                  ret[feature] = get_inverse(q)
              else:
                  ret[:, feature] = (
                      [get_inverse(int(x)) for x in ret[:, feature]])
          return ret
  
  
  class QuartileDiscretizer(BaseDiscretizer):
      def __init__(self, data, categorical_features, feature_names, labels=None, random_state=None):
  
          BaseDiscretizer.__init__(self, data, categorical_features,
                                   feature_names, labels=labels,
                                   random_state=random_state)
  
      def bins(self, data, labels):
          bins = []
          for feature in self.to_discretize:
              qts = np.array(np.percentile(data[:, feature], [25, 50, 75]))
              bins.append(qts)
          return bins
  
  
  class DecileDiscretizer(BaseDiscretizer):
      def __init__(self, data, categorical_features, feature_names, labels=None, random_state=None):
          BaseDiscretizer.__init__(self, data, categorical_features,
                                   feature_names, labels=labels,
                                   random_state=random_state)
  
      def bins(self, data, labels):
          bins = []
          for feature in self.to_discretize:
              qts = np.array(np.percentile(data[:, feature],
                                           [10, 20, 30, 40, 50, 60, 70, 80, 90]))
              bins.append(qts)
          return bins
  
  class MDLPDiscretizer(BaseDiscretizer):
      def __init__(self, data, categorical_features, feature_names, labels=None, random_state=None):
          if(labels is None):
              raise ValueError('Labels must be not None when using \
                               MDLPDiscretizer')
          BaseDiscretizer.__init__(self, data, categorical_features,
                                   feature_names, labels=labels,
                                   random_state=random_state)
  
      def bins(self, data, labels):
          self.transformer = MDLP()
          discretize_data = self.transformer.fit_transform(data, labels)
          bins = []
          for i in range (len(set(labels))):
              intervals = set(self.transformer.cat2intervals(discretize_data, i))
              feature_interval = []
              for i in range (len(intervals)):
                  interval = intervals.pop()
                  feature_interval.append(interval[0])
                  feature_interval.append(interval[1])
              feature_interval = set(feature_interval)
              feature_interval.discard(float('inf'))
              feature_interval.discard(float('-inf'))
              array = [x for x in feature_interval]
              bins.append(np.array(array))
          return bins
  
  class EntropyDiscretizer(BaseDiscretizer):
      def __init__(self, data, categorical_features, feature_names, labels=None, random_state=None):
          if(labels is None):
              raise ValueError('Labels must be not None when using \
                               EntropyDiscretizer')
          BaseDiscretizer.__init__(self, data, categorical_features,
                                   feature_names, labels=labels,
                                   random_state=random_state)
  
      def bins(self, data, labels):
          bins = []
          for feature in self.to_discretize:
              # Entropy splitting / at most 8 bins so max_depth=3
              dt = sklearn.tree.DecisionTreeClassifier(criterion='entropy',
                                                       max_depth=3,
                                                       random_state=self.random_state)
              x = np.reshape(data[:, feature], (-1, 1))
              dt.fit(x, labels)
              qts = dt.tree_.threshold[np.where(dt.tree_.children_left > -1)]
  
              if qts.shape[0] == 0:
                  qts = np.array([np.median(data[:, feature])])
              else:
                  qts = np.sort(qts)
  
              bins.append(qts)
  
          return bins
  
  class KMeansDiscretizer(BaseDiscretizer):
      def __init__(self, data, categorical_features, feature_names, filename, labels=None, random_state=None):
          self.filename = filename
          BaseDiscretizer.__init__(self, data, categorical_features,
                                   feature_names, labels=labels,
                                   random_state=random_state)
  
      def bins(self, data, labels):
          bins = []
          for feature in self.to_discretize:
              x = np.reshape(data[:, feature], (-1, 1))
              model = KMeans()
              visualizer = KElbowVisualizer(model, k=(3,12))
              visualizer.fit(x)
              n_bins = visualizer.elbow_value_
              kmeans = KBinsDiscretizer(n_bins=n_bins, encode='ordinal', strategy='kmeans')
              if self.filename != "":
                  plt.show(block=False)
                  plt.savefig(self.filename + "/" + str(self.feature_names[feature]) + "_elbow_visualisation.png")
                  plt.close('all')
              kmeans.fit(x)
              qts = []
              for biner in kmeans.bin_edges_:
                  qts.append(biner)
              qts = np.array(qts)
  
              if qts.shape[0] == 0:
                  qts = np.array([np.median(data[:, feature])])
              else:
                  qts = np.sort(qts)
  
              bins.append(qts)
          return bins
  