# 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/_utils.py
# def _naive_bayes_bins(target, prior, num_classes=2):
#     if len(target) > 1:
#         cnts = np.bincount(target, minlength=num_classes)
#         return list(cnts / cnts.sum())
#     else:
#         return [1 - prior, prior]
# 
# def _filter_special_values(values, filter_values):
#     feats = np.array(values)
#     filters = np.array(filter_values)
# 
#     special_vals_idx_dct = dict()
#     for val in filters:
#         if np.isnan(val):
#             idx = np.where(np.isnan(feats))[0]
#         else:
#             idx = np.where(feats == val)[0]
# 
#         special_vals_idx_dct.update({str(val): idx})
# 
#     all_special_idx = list()
#     for vals in special_vals_idx_dct.values():
#         all_special_idx += vals.tolist()
# 
#     all_special_idx = np.asarray(all_special_idx)
#     all_idx = np.arange(0, len(feats), 1, dtype=int)
#     all_non_special_idx = [idx for idx in all_idx if idx not in all_special_idx]
#     special_vals_idx_dct.update({'regular': all_non_special_idx})
# 
#     return special_vals_idx_dct


import numpy as np
import scipy.stats as st
from DSTK.FeatureBinning.base_binner import BaseBinner
from DSTK.FeatureBinning._utils import _naive_bayes_bins, _filter_special_values
from __future__ import division


class _Node(object):

    def __init__(self, id, threshold, left_child, right_child, is_leaf, counts, statistic):
        self.id = id
        self.threshold = threshold
        self.left_child = left_child
        self.right_child = right_child
        self.is_leaf = is_leaf
        self.counts = counts
        self.statistic = statistic

# 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/_utils.py
# def _naive_bayes_bins(target, prior, num_classes=2):
#     if len(target) > 1:
#         cnts = np.bincount(target, minlength=num_classes)
#         return list(cnts / cnts.sum())
#     else:
#         return [1 - prior, prior]
# 
# def _filter_special_values(values, filter_values):
#     feats = np.array(values)
#     filters = np.array(filter_values)
# 
#     special_vals_idx_dct = dict()
#     for val in filters:
#         if np.isnan(val):
#             idx = np.where(np.isnan(feats))[0]
#         else:
#             idx = np.where(feats == val)[0]
# 
#         special_vals_idx_dct.update({str(val): idx})
# 
#     all_special_idx = list()
#     for vals in special_vals_idx_dct.values():
#         all_special_idx += vals.tolist()
# 
#     all_special_idx = np.asarray(all_special_idx)
#     all_idx = np.arange(0, len(feats), 1, dtype=int)
#     all_non_special_idx = [idx for idx in all_idx if idx not in all_special_idx]
#     special_vals_idx_dct.update({'regular': all_non_special_idx})
# 
#     return special_vals_idx_dct


# Path: DSTK/FeatureBinning/conditional_inference_binner.py
import numpy as np
import scipy.stats as st
from DSTK.FeatureBinning.base_binner import BaseBinner
from DSTK.FeatureBinning._utils import _naive_bayes_bins, _filter_special_values

from __future__ import division


class _Node(object):

    def __init__(self, id, threshold, left_child, right_child, is_leaf, counts, statistic):
        self.id = id
        self.threshold = threshold
        self.left_child = left_child
        self.right_child = right_child
        self.is_leaf = is_leaf
        self.counts = counts
        self.statistic = statistic

        self.cond_proba = counts / counts.sum()