数据离散化之chimerge算法的实现(python版)(以莺尾花数据为例)

今天介绍一种数据离散化方法——chimerge,该方法的主要思想是把所有的连续数据排序并分成多个区间,每次计算相邻两个区间的卡方(chi)值,(卡方见卡方分析,)每次取卡方值最小的两个区间进行合并。
在此之前,先介绍一下chi的计算:

表一为某个特征的observe列表,即实际的

区间类别1类别2类别3i行的总数
[4.3,4.5]1001
[4.6,4.9]0123
j列的总数1124

表2为根据表一所计算出的expecte列表,即期望的,计算方式为i行的总数乘以j列的总数除以总总数

区间类别1类别2类别3
[4.3,4.5]1*1/4=0.251*1/4=0.251*2/4=0.5
[4.6,4.9]1*3/4=0.751*3/4=0.752*3/4=1.5

根据以上两个表来计算某两个区间的chi值:
chi = (1-0.25)^2 /0.25 + (0-0.25)^2/0.25 +(0-0.5)^2/0.5 + (0-0.75)^2/0.75 + (1-0.25)^2/0.25 + (2-1.5)^2/1.5

然后要做的工作就是不断计算合并了。这里提出几个疑问
1、初始化的区间应该是什么?我们在此取每个属性值为初始区间,假如我们要对身高进行离散化,某个样本的身高是174,那么我们就取区间为[174,174]
2、每次合并区间只能合并一对吗?我查了一下,好像没有什么绝对的答案,就我认为,每次可以合并多对,比如说我们计算了10对区间的chi值,刚好有3对的chi值相等且为最小值,那么我们完全可以同时对它们进行合并

贴上代码:

import sklearn.datasets as irisdata
import numpy as np


class ChiMerge:
    
    def __init__(self, data, min_section_num):
        self.min_section_num = min_section_num
        self.dat = data
    @staticmethod
    def merge_section(index_list, observe_list):
        """
        合并区间
        :param observe_list: 原来的区间集合
        :param index_list: 要合并的位置
        :return: 新的区间集合
        """
        # print(observe_list)
        number = int(len(index_list) / 2)
        for i in range(number):
            first_section = observe_list[index_list[2 * i]]  # 要合并的第一部分
            second_section = observe_list[index_list[2 * i + 1]]  # 要合并的第二部分
            new_section = []  # 合并后的区间

            min_value = float(first_section[0].split("-")[0])
            max_value = float(second_section[0].split("-")[1])
            first_class = first_section[1] + second_section[1]
            second_class = first_section[2] + second_section[2]
            third_class = first_section[3] + second_section[3]
            new_section.append(str(min_value) + "-" + str(max_value))
            new_section.append(first_class)
            new_section.append(second_class)
            new_section.append(third_class)
            # print(new_section)

            observe_list[index_list[2 * i]] = new_section
            observe_list[index_list[2 * i + 1]] = "no"
        for i in range(number):
            observe_list.remove("no")
        return observe_list

        # for i in  range

    @staticmethod
    def comp_chi(observe_list):
        """
        根据observe列表计算每个区间的卡方
        :param observe_list:排好的observe列表
        :return:最小chi所在的索引列表
        """
        min_chi = float('inf')  # 记录最小的chi
        # print(min_chi)
        index_list = []
        for i in range(int(len(observe_list) / 2)):
            chi = 0
            a1 = observe_list[2 * i][1]  # 第一个区间的信息
            b1 = observe_list[2 * i][2]
            c1 = observe_list[2 * i][3]
            d1 = observe_list[2 * i + 1][1]  # 第二个区间的信息
            e1 = observe_list[2 * i + 1][2]
            f1 = observe_list[2 * i + 1][3]
            n = a1 + b1 + c1 + d1 + e1 + f1
            a2 = (a1 + b1 + c1) * (a1 + d1) / n
            b2 = (a1 + b1 + c1) * (b1 + e1) / n
            c2 = (a1 + b1 + c1) * (c1 + f1) / n
            d2 = (a2 + b2 + c2) * (a1 + d1) / n
            e2 = (a2 + b2 + c2) * (b1 + e1) / n
            f2 = (a2 + b2 + c2) * (c1 + f1) / n
            if a2 != 0:
                chi += (a1 - a2) ** 2 / a2
            if b2 != 0:
                chi += (b1 - b2) ** 2 / b2
            if c2 != 0:
                chi += (c1 - c2) ** 2 / c2
            if d2 != 0:
                chi += (d1 - d2) ** 2 / d2
            if e2 != 0:
                chi += (e1 - e2) ** 2 / e2
            if f2 != 0:
                chi += (f1 - f2) ** 2 / f2
            if chi < min_chi:
                index_list.clear()
                index_list.append(2 * i)
                index_list.append(2 * i + 1)
                min_chi = chi
                continue
            if chi == min_chi:
                index_list.append(2 * i)
                index_list.append(2 * i + 1)
        # print(min_chi)
        # print(index_list)
        return index_list

    @staticmethod
    def init_observe(sort_data):  # sort_data为按属性排好的数据,格式为list套list
        """
        对observe列表进行初始化
        :param sort_data:
        :return:
        """
        observe_list = []
        for i in range(len(sort_data)):  # 每个sort_data[i]代表每个区间
            max_value = 0  # 存放每个区间的最大值和最小值
            min_value = 0
            section_name = str(sort_data[i][0]).split("-")
            if len(section_name) > 1:
                min_value = float(section_name[0])
                max_value = float(section_name[1])
            else:
                min_value = max_value = float(section_name[0])
            first_class = 0
            second_class = 0
            third_class = 0

            if min_value <= sort_data[i][0] <= max_value:
                if sort_data[i][1] == 0:
                    first_class += 1
                if sort_data[i][1] == 1:
                    second_class += 1
                if sort_data[i][1] == 2:
                    third_class += 1
            section_list = [str(min_value) + "-" + str(max_value), first_class, second_class, third_class]
            observe_list.append(section_list)
        # print(observe_list)
        return observe_list

    @staticmethod
    def comp_observe(sort_data):  # sort_data为按属性排好的数据,格式为list套list
        """
        计算observe列表(除了初始化之外)
        :param sort_data:
        :return:
        """
        observe_list = []
        for i in range(len(sort_data)):  # 每个sort_data[i]代表每个区间
            max_value = 0  # 存放每个区间的最大值和最小值
            min_value = 0
            section_name = str(sort_data[i][0]).split("-")
            if len(section_name) > 1:
                min_value = float(section_name[0])
                max_value = float(section_name[1])
            else:
                min_value = max_value = float(section_name[0])
            first_class = 0
            second_class = 0
            third_class = 0
            for j in range(len(sort_data)):
                if min_value <= sort_data[j][0] <= max_value:
                    if sort_data[j][1] == 0:
                        first_class += 1
                    if sort_data[j][1] == 1:
                        second_class += 1
                    if sort_data[j][1] == 2:
                        third_class += 1
            section_list = [str(min_value) + "-" + str(max_value), first_class, second_class, third_class]
            print(section_list)

    def chi_merge(self):  # dat为原始全部数据(包括类别)

        # min_section_num = 6  # 属性最终划分成几个区间
        for i in range(self.dat.shape[1] - 1):  # 对每个属性进行离散化
            now_section_num = self.dat.shape[0]  # 初始区间数为样本数量
            now_data = self.dat[:, [i, -1]]  # 当前要进行离散化的属性数据以及所属类别
            sort_data = now_data[now_data[:, 0].argsort()].tolist()  # 按当前属性从小到大排序,格式:[属性值,类别]
            # print(sort_data)
            observe_list = self.init_observe(sort_data)  # 得到初始化的observe列表
            while now_section_num > self.min_section_num:
                # print(observe_list)
                index_list = self.comp_chi(observe_list)
                observe_list = self.merge_section(index_list, observe_list)  # 更新区间集合
                print(observe_list)
                now_section_num -= len(index_list) / 2


if __name__ == '__main__':
    # 花萼长度、花萼宽度、花瓣长度、花瓣宽度
    data_attr = irisdata.load_iris()['data']
    cla = irisdata.load_iris()['target']
    # comp_entropy(cla)

    merge_data = np.append(data_attr, cla.reshape(150, 1), axis=1)
    section_num = 8
    ss = ChiMerge(merge_data, section_num)
    ss.chi_merge()

对四个属性进行离散化后的结果:
在这里插入图片描述
可以看出大部分区间内的样本是属于同一类别的,只有个别区间效果不太好,但是也可以接受,毕竟我们只是简单的计算了chi。

此代码的缺陷:
1、对最后划分区间数目没有进行很好的确定(在代码中我们取的是5),最笨的方法当然是在有限的空间内进行穷举了,这时我们就需要一个概念来度量划分结果的好坏,比如划分的纯度、分类的准确率啥的,具体的还请大佬们赐教。
2、有可能存在这样一种情况,前一个区间的后值和后一个区间的前值相等,这在逻辑上是讲不通的,而且在应用分类时还会遇到问题,产生这种情况的原因是属性的某个取值可能有多个,在排序时它们都排在一块,那么在合并时就可能出现这种情况了。

最后,大家有什么好的解决方案可以留下评论,感谢!


对缺陷一的改进:
为了找到对每个属性的最佳划分,我们采用信息增益的方式来进行度量。信息增益详情可见信息增益

话不多说,直接贴上代码,此代码会输出每一轮合并的结果以及每一个属性的最佳的划分。

import sklearn.datasets as irisdata
import numpy as np


class ChiMerge:

    # def __init__(self, data, min_section_num):
    #     self.min_section_num = min_section_num
    #     self.dat = data
    def __init__(self, data_att, data_cla, max_section):
        self.dat = np.append(data_att, data_cla.reshape(150, 1), axis=1)

        self.max_section = max_section

    # 计算初始信息熵
    @staticmethod
    def comp_init_entropy(cla_set):
        first_cla = 0
        second_cla = 0
        third_cla = 0
        for i in range(len(cla_set)):
            if cla_set[i] == 0:
                first_cla += 1
            if cla_set[i] == 1:
                second_cla += 1
            if cla_set[i] == 2:
                third_cla += 1

        n = len(cla_set)
        info = -first_cla / n * np.log2(first_cla / n) \
               - second_cla / n * np.log2(second_cla / n) \
               - third_cla / n * np.log2(third_cla / n)
        print(info)

    @staticmethod
    def merge_section(index_list, observe_list):

        """
        合并区间
        :param observe_list: 原来的区间集合
        :param index_list: 要合并的位置
        :return: 新的区间集合
        """
        # print(observe_list)
        number = int(len(index_list) / 2)
        for i in range(number):
            first_section = observe_list[index_list[2 * i]]  # 要合并的第一部分
            second_section = observe_list[index_list[2 * i + 1]]  # 要合并的第二部分
            new_section = []  # 合并后的区间

            min_value = float(first_section[0].split("-")[0])
            max_value = float(second_section[0].split("-")[1])
            first_class = first_section[1] + second_section[1]
            second_class = first_section[2] + second_section[2]
            third_class = first_section[3] + second_section[3]
            new_section.append(str(min_value) + "-" + str(max_value))
            new_section.append(first_class)
            new_section.append(second_class)
            new_section.append(third_class)
            # print(new_section)

            observe_list[index_list[2 * i]] = new_section
            observe_list[index_list[2 * i + 1]] = "no"
        for i in range(number):
            observe_list.remove("no")
        return observe_list

        # for i in  range

    @staticmethod
    def comp_chi(observe_list):
        """
        根据observe列表计算每个区间的卡方
        :param observe_list:排好的observe列表
        :return:最小chi所在的索引列表
        """
        min_chi = float('inf')  # 记录最小的chi
        # print(min_chi)
        index_list = []
        for i in range(int(len(observe_list) / 2)):
            chi = 0
            a1 = observe_list[2 * i][1]  # 第一个区间的信息
            b1 = observe_list[2 * i][2]
            c1 = observe_list[2 * i][3]
            d1 = observe_list[2 * i + 1][1]  # 第二个区间的信息
            e1 = observe_list[2 * i + 1][2]
            f1 = observe_list[2 * i + 1][3]
            n = a1 + b1 + c1 + d1 + e1 + f1
            a2 = (a1 + b1 + c1) * (a1 + d1) / n
            b2 = (a1 + b1 + c1) * (b1 + e1) / n
            c2 = (a1 + b1 + c1) * (c1 + f1) / n
            d2 = (a2 + b2 + c2) * (a1 + d1) / n
            e2 = (a2 + b2 + c2) * (b1 + e1) / n
            f2 = (a2 + b2 + c2) * (c1 + f1) / n
            if a2 != 0:
                chi += (a1 - a2) ** 2 / a2
            if b2 != 0:
                chi += (b1 - b2) ** 2 / b2
            if c2 != 0:
                chi += (c1 - c2) ** 2 / c2
            if d2 != 0:
                chi += (d1 - d2) ** 2 / d2
            if e2 != 0:
                chi += (e1 - e2) ** 2 / e2
            if f2 != 0:
                chi += (f1 - f2) ** 2 / f2
            if chi < min_chi:
                index_list.clear()
                index_list.append(2 * i)
                index_list.append(2 * i + 1)
                min_chi = chi
                continue
            if chi == min_chi:
                index_list.append(2 * i)
                index_list.append(2 * i + 1)
        # print(min_chi)
        # print(index_list)
        return index_list

    @staticmethod
    def init_observe(sort_data):  # sort_data为按属性排好的数据,格式为list套list
        """
        对observe列表进行初始化
        :param sort_data:
        :return:
        """
        observe_list = []
        for i in range(len(sort_data)):  # 每个sort_data[i]代表每个区间
            max_value = 0  # 存放每个区间的最大值和最小值
            min_value = 0
            section_name = str(sort_data[i][0]).split("-")
            if len(section_name) > 1:
                min_value = float(section_name[0])
                max_value = float(section_name[1])
            else:
                min_value = max_value = float(section_name[0])
            first_class = 0
            second_class = 0
            third_class = 0

            if min_value <= sort_data[i][0] <= max_value:
                if sort_data[i][1] == 0:
                    first_class += 1
                if sort_data[i][1] == 1:
                    second_class += 1
                if sort_data[i][1] == 2:
                    third_class += 1
            section_list = [str(min_value) + "-" + str(max_value), first_class, second_class, third_class]
            observe_list.append(section_list)
        # print(observe_list)
        return observe_list

    @staticmethod
    def comp_observe(sort_data):  # sort_data为按属性排好的数据,格式为list套list
        """
        计算observe列表(除了初始化之外)
        :param sort_data:
        :return:
        """
        observe_list = []
        for i in range(len(sort_data)):  # 每个sort_data[i]代表每个区间
            max_value = 0  # 存放每个区间的最大值和最小值
            min_value = 0
            section_name = str(sort_data[i][0]).split("-")
            if len(section_name) > 1:
                min_value = float(section_name[0])
                max_value = float(section_name[1])
            else:
                min_value = max_value = float(section_name[0])
            first_class = 0
            second_class = 0
            third_class = 0
            for j in range(len(sort_data)):
                if min_value <= sort_data[j][0] <= max_value:
                    if sort_data[j][1] == 0:
                        first_class += 1
                    if sort_data[j][1] == 1:
                        second_class += 1
                    if sort_data[j][1] == 2:
                        third_class += 1
            section_list = [str(min_value) + "-" + str(max_value), first_class, second_class, third_class]
            print(section_list)

    def chi_merge(self):  # dat为原始全部数据(包括类别)

        # min_section_num = 6  # 属性最终划分成几个区间
        for i in range(self.dat.shape[1] - 1):  # 对每个属性进行离散化
            now_section_num = self.dat.shape[0]  # 初始区间数为样本数量
            now_data = self.dat[:, [i, -1]]  # 当前要进行离散化的属性数据以及所属类别
            sort_data = now_data[now_data[:, 0].argsort()].tolist()  # 按当前属性从小到大排序,格式:[属性值,类别]
            # print(sort_data)
            observe_list = self.init_observe(sort_data)  # 得到初始化的observe列表
            while now_section_num > self.min_section_num:
                # print(observe_list)
                index_list = self.comp_chi(observe_list)
                observe_list = self.merge_section(index_list, observe_list)  # 更新区间集合
                # print(observe_list)
                now_section_num -= len(index_list) / 2
            print(observe_list)

    def comp_entropy(self, section_list):
        """

        :param section_list:
        :return: 当前划分的信息熵
        """
        sam_number = self.dat.shape[0]  # 总的样本数量
        final_entropy = 0
        for section in section_list:
            now_node_sam_number = section[1] + section[2] + section[3]
            now_node_entropy = 0
            if section[1] != 0:
                now_node_entropy += -(section[1] / now_node_sam_number) * (np.log2(section[1] / now_node_sam_number))
            if section[2] != 0:
                now_node_entropy += -(section[2] / now_node_sam_number) * (np.log2(section[2] / now_node_sam_number))
            if section[3] != 0:
                now_node_entropy += -(section[3] / now_node_sam_number) * (np.log2(section[3] / now_node_sam_number))

            # now_node_entropy = -(section[1] / now_node_sam_number) * (np.log2(section[1] / now_node_sam_number)) \
            #                    - (section[2] / now_node_sam_number) * (np.log2(section[2] / now_node_sam_number)) \
            #                    - (section[3] / now_node_sam_number) * (np.log2(section[3] / now_node_sam_number))
            final_entropy += (now_node_sam_number / sam_number) * now_node_entropy
        return final_entropy

    def find_best_merge(self):
        """
        寻找最合适的划分(根据信息熵)
        :return:
        """
        for i in range(self.dat.shape[1] - 1):  # 对每个属性

            print("第" + str(i + 1) + "个属性开始")
            # print(self.dat.shape[1] - 1)
            mini_entropy = float('inf')  # 存放某属性各种划分的最小熵
            best_section_info = []  # 存放最佳划分的区间数和区间信息
            for j in range(self.max_section):  # 划分的区间数为j
                # print(self.max_section)
                print("第" + str(i + 1) + "个属性" + "区间数为" + str(j + 1))
                now_section_num = self.dat.shape[0]  # 初始区间数为样本数量
                # print(now_section_num)
                now_data = self.dat[:, [i, -1]]  # 当前要进行离散化的属性数据以及所属类别
                # print(now_data)
                sort_data = now_data[now_data[:, 0].argsort()].tolist()  # 按当前属性从小到大排序,格式:[属性值,类别]
                observe_list = self.init_observe(sort_data)  # 得到初始化的observe列表
                k = 1
                while now_section_num > j + 1:
                    print(now_section_num)
                    # print(j + 1)

                    print("第" + str(i + 1) + "个属性" + "区间数为" + str(j + 1) + "第" + str(k) + "轮")
                    index_list = self.comp_chi(observe_list)  # 返回最小chi值的索引列表
                    observe_list = self.merge_section(index_list, observe_list)  # 更新区间集合
                    now_section_num -= len(index_list) / 2
                    k += 1
                # 此时划分区间数为j已完成,可以计算当前的信息熵
                # print(observe_list)
                now_section_entropy = self.comp_entropy(observe_list)

                if now_section_entropy < mini_entropy:
                    best_section_info.clear()
                    mini_entropy = now_section_entropy
                    best_section_info.append(j+1)
                    best_section_info.append(observe_list)
            print(best_section_info)


if __name__ == '__main__':
    # 花萼长度、花萼宽度、花瓣长度、花瓣宽度
    data_attr = irisdata.load_iris()['data']
    cla = irisdata.load_iris()['target']
    # merge_data = np.append(data_attr, cla.reshape(150, 1), axis=1)
    section_num = 5
    max_section_num = 40
    ss = ChiMerge(data_attr, cla, max_section_num)  # 寻找区间数目小于max_section_num的划分
    ss.find_best_merge()
    # ss.chi_merge()
    # ss.comp_init_entropy(cla)

下次有时间的话再把缺陷二解决,拜拜。

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值