# Path: DSTK/FeatureBinning/_utils.py
# def _process_nan_values(values, target, prior):
# 
#     feats = np.array(values)
#     labels = np.array(target)
#     num_classes = np.bincount(target).size
# 
#     num_nan = len(feats[np.isnan(feats)])
#     val_count = np.bincount(labels[np.isnan(feats)], minlength=num_classes)
#     if (num_nan == 0) and prior:
#         return [1 - prior, prior]
#     elif (num_nan == 0) and (prior is None):
#         return (np.ones((num_classes,), dtype='float64') / num_classes).tolist()
#     return list(val_count / num_nan)
# 
# def _get_non_nan_values_and_targets(values, target):
#     feats = np.array(values)
#     labels = np.array(target)
# 
#     return feats[~np.isnan(feats)].reshape(-1, 1), labels[~np.isnan(feats)].reshape(-1, 1)
# 
# Path: DSTK/FeatureBinning/base_binner.py
# class BaseBinner(object):
#     __metaclass__ = abc.ABCMeta
# 
#     @abc.abstractproperty
#     def splits(self):
#         raise NotImplementedError
# 
#     @splits.setter
#     def splits(self, values):
#         raise NotImplementedError
# 
#     @abc.abstractproperty
#     def values(self):
#         raise NotImplementedError
# 
#     @values.setter
#     def values(self, values):
#         raise NotImplementedError
# 
#     @abc.abstractmethod
#     def fit(self, values, targets):
#         raise NotImplementedError
# 
#     @abc.abstractproperty
#     def is_fit(self):
#         raise NotImplementedError
# 
#     @is_fit.setter
#     def is_fit(self, is_fit):
#         raise NotImplementedError
# 
#     def transform(self, values, **kwargs):
#         """
#         #     See output of 'fit_transform()'
#         #     :param feature_values:
#         #     :param class_index:
#         #     :return:
#         #     """
#         if not self.is_fit:
#             raise AssertionError("FeatureBinner has to be fit to the data first.")
# 
#         class_index = kwargs.get('class_index', 1)
#         idx = np.searchsorted(self.splits, values, side='right')
# 
#         # searchsorted on nan values gives wrong idx when input is nan
#         # as it assumes the nan value is "right" of a nan split
#         idx[np.where(np.isnan(values))[0]] -= 1
# 
#         if class_index:
#             return np.asarray(self.values)[idx][:, class_index]
#         else:
#             return np.asarray(self.values)[idx]
# 
#     def fit_transform(self, feature_values, target_values, **kwargs):
#         """
#         :param feature_values: list or array of the feature values
#         :param target_values: list or array of the corresponding labels
#         :param class_index: Index of the corresponding class in the conditional probability vector for each bucket.
#                Defaults to 1 (as mostly used for binary classification)
#         :return: list of cond_proba_buckets with corresponding conditional probabilities P( T | x in X )
#                  for a given example with value x in bin with range X to have label T and list of conditional probabilities for each value to be of class T
#         """
#         self.fit(feature_values, target_values)
#         return self.transform(feature_values, **kwargs)
# 
#     def add_bin(self, right_bin_edge, bin_value):
# 
#         if right_bin_edge in self.splits:
#             warnings.warn("Bin edge already exists.", UserWarning)
#             return self
# 
#         # use np.searchsorted instead of digitize as the latter
#         # gives the wrong result for purely non-numeric splits
#         # see corresponding test for the issue
#         idx = np.searchsorted(self.splits, right_bin_edge, side='right')
#         self.splits = np.insert(self.splits, idx, right_bin_edge).tolist()
#         self.values = np.insert(self.values, [idx], bin_value, axis=0).tolist()
# 
#         return self
# 
#     def __str__(self):
#         return self.__repr__()
# 
#     def __repr__(self):
#         sp = np.asarray(self.splits)
#         vals = np.asarray(self.values)
#         non_na_sp = sp[~np.isnan(sp)]
#         non_na_val = vals[~np.isnan(sp)]
#         na_val = vals[np.isnan(sp)].flatten()
# 
#         non_na_str = ["<= {}: {}".format(split, val) for split, val in zip(non_na_sp, non_na_val)]
#         non_na_str += ["NaN: {}".format(na_val)]
# 
#         return "\n".join(non_na_str)


import sklearn
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from DSTK.FeatureBinning._utils import _process_nan_values, _get_non_nan_values_and_targets
from DSTK.FeatureBinning.base_binner import BaseBinner
        Note that it can handle NaN by assigning it its own range.

        E.g.

        >>> feats = [0, 1, 2, np.nan, 5, np.nan]
        >>> labels = [0, 1, 0, 1, 1, 0]

        then

        >>> tfb = DecisionTreeBinner()
        >>> tfb.fit(feats, labels, max_leaf_nodes = 3)
        >>> tfb.cond_proba_buckets
        ... [((-inf, 0.5), [1.0, 0.0]),
        ...  ((0.5, 1.5), [0.0, 1.0]),
        ...  ((1.5, inf), [0.5, 0.5]),
        ...  (nan, [0.5, 0.5])]

        which we can interpret as: given an example with label 0 there is a 25% probability of having a feature value in the range
        (-inf, 0.5).

        :param feature_values: list or array of the feature values
        :param target_values: list or array of the corresponding labels
        :param kwargs: supports all keywords of an sklearn.DecisionTreeClassifier
        :return: Nothing, but sets the self.cond_proba_buckets field
        """

        assert (values is not None) & (values != []), "feature_values cannot be None or empty"
        assert (target is not None) & (target != []), "target_values cannot be None or empty"
        assert len(values) == len(target), "feature_values and target_values must have same length"


# Path: DSTK/FeatureBinning/_utils.py
# def _process_nan_values(values, target, prior):
# 
#     feats = np.array(values)
#     labels = np.array(target)
#     num_classes = np.bincount(target).size
# 
#     num_nan = len(feats[np.isnan(feats)])
#     val_count = np.bincount(labels[np.isnan(feats)], minlength=num_classes)
#     if (num_nan == 0) and prior:
#         return [1 - prior, prior]
#     elif (num_nan == 0) and (prior is None):
#         return (np.ones((num_classes,), dtype='float64') / num_classes).tolist()
#     return list(val_count / num_nan)
# 
# def _get_non_nan_values_and_targets(values, target):
#     feats = np.array(values)
#     labels = np.array(target)
# 
#     return feats[~np.isnan(feats)].reshape(-1, 1), labels[~np.isnan(feats)].reshape(-1, 1)
# 
# Path: DSTK/FeatureBinning/base_binner.py
# class BaseBinner(object):
#     __metaclass__ = abc.ABCMeta
# 
#     @abc.abstractproperty
#     def splits(self):
#         raise NotImplementedError
# 
#     @splits.setter
#     def splits(self, values):
#         raise NotImplementedError
# 
#     @abc.abstractproperty
#     def values(self):
#         raise NotImplementedError
# 
#     @values.setter
#     def values(self, values):
#         raise NotImplementedError
# 
#     @abc.abstractmethod
#     def fit(self, values, targets):
#         raise NotImplementedError
# 
#     @abc.abstractproperty
#     def is_fit(self):
#         raise NotImplementedError
# 
#     @is_fit.setter
#     def is_fit(self, is_fit):
#         raise NotImplementedError
# 
#     def transform(self, values, **kwargs):
#         """
#         #     See output of 'fit_transform()'
#         #     :param feature_values:
#         #     :param class_index:
#         #     :return:
#         #     """
#         if not self.is_fit:
#             raise AssertionError("FeatureBinner has to be fit to the data first.")
# 
#         class_index = kwargs.get('class_index', 1)
#         idx = np.searchsorted(self.splits, values, side='right')
# 
#         # searchsorted on nan values gives wrong idx when input is nan
#         # as it assumes the nan value is "right" of a nan split
#         idx[np.where(np.isnan(values))[0]] -= 1
# 
#         if class_index:
#             return np.asarray(self.values)[idx][:, class_index]
#         else:
#             return np.asarray(self.values)[idx]
# 
#     def fit_transform(self, feature_values, target_values, **kwargs):
#         """
#         :param feature_values: list or array of the feature values
#         :param target_values: list or array of the corresponding labels
#         :param class_index: Index of the corresponding class in the conditional probability vector for each bucket.
#                Defaults to 1 (as mostly used for binary classification)
#         :return: list of cond_proba_buckets with corresponding conditional probabilities P( T | x in X )
#                  for a given example with value x in bin with range X to have label T and list of conditional probabilities for each value to be of class T
#         """
#         self.fit(feature_values, target_values)
#         return self.transform(feature_values, **kwargs)
# 
#     def add_bin(self, right_bin_edge, bin_value):
# 
#         if right_bin_edge in self.splits:
#             warnings.warn("Bin edge already exists.", UserWarning)
#             return self
# 
#         # use np.searchsorted instead of digitize as the latter
#         # gives the wrong result for purely non-numeric splits
#         # see corresponding test for the issue
#         idx = np.searchsorted(self.splits, right_bin_edge, side='right')
#         self.splits = np.insert(self.splits, idx, right_bin_edge).tolist()
#         self.values = np.insert(self.values, [idx], bin_value, axis=0).tolist()
# 
#         return self
# 
#     def __str__(self):
#         return self.__repr__()
# 
#     def __repr__(self):
#         sp = np.asarray(self.splits)
#         vals = np.asarray(self.values)
#         non_na_sp = sp[~np.isnan(sp)]
#         non_na_val = vals[~np.isnan(sp)]
#         na_val = vals[np.isnan(sp)].flatten()
# 
#         non_na_str = ["<= {}: {}".format(split, val) for split, val in zip(non_na_sp, non_na_val)]
#         non_na_str += ["NaN: {}".format(na_val)]
# 
#         return "\n".join(non_na_str)


# Path: DSTK/FeatureBinning/decision_tree_binner.py
import sklearn
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from DSTK.FeatureBinning._utils import _process_nan_values, _get_non_nan_values_and_targets
from DSTK.FeatureBinning.base_binner import BaseBinner

        Note that it can handle NaN by assigning it its own range.

        E.g.

        >>> feats = [0, 1, 2, np.nan, 5, np.nan]
        >>> labels = [0, 1, 0, 1, 1, 0]

        then

        >>> tfb = DecisionTreeBinner()
        >>> tfb.fit(feats, labels, max_leaf_nodes = 3)
        >>> tfb.cond_proba_buckets
        ... [((-inf, 0.5), [1.0, 0.0]),
        ...  ((0.5, 1.5), [0.0, 1.0]),
        ...  ((1.5, inf), [0.5, 0.5]),
        ...  (nan, [0.5, 0.5])]

        which we can interpret as: given an example with label 0 there is a 25% probability of having a feature value in the range
        (-inf, 0.5).

        :param feature_values: list or array of the feature values
        :param target_values: list or array of the corresponding labels
        :param kwargs: supports all keywords of an sklearn.DecisionTreeClassifier
        :return: Nothing, but sets the self.cond_proba_buckets field
        """

        assert (values is not None) & (values != []), "feature_values cannot be None or empty"
        assert (target is not None) & (target != []), "target_values cannot be None or empty"
        assert len(values) == len(target), "feature_values and target_values must have same length"


        non_nan_feats, non_nan_labels = _get_non_nan_values_and_targets(values, target)