决策树中的连续值与缺失值处理的实现思路

    科技2025-06-03  33

    连续值处理

    由于连续数据不能划分,所以要使用连续属性离散化技术,其中最简单的方法就是二分法(bi-partition)。 首先要构造一个候选划分点集合:从数据集中取相邻两个元素的平均值,然后构成一个待选的节点,假设一个数据集共有n个元素,则可以生成n-1个 候选分割点。 以西瓜数据集3.0为例的密度为例:

    编号,色泽,根蒂,敲声,纹理,脐部,触感,密度,含糖率,好瓜 1,青绿,蜷缩,浊响,清晰,凹陷,硬滑,0.697,0.46,2,乌黑,蜷缩,沉闷,清晰,凹陷,硬滑,0.774,0.376,3,乌黑,蜷缩,浊响,清晰,凹陷,硬滑,0.634,0.264,4,青绿,蜷缩,沉闷,清晰,凹陷,硬滑,0.608,0.318,5,浅白,蜷缩,浊响,清晰,凹陷,硬滑,0.556,0.215,6,青绿,稍蜷,浊响,清晰,稍凹,软粘,0.403,0.237,7,乌黑,稍蜷,浊响,稍糊,稍凹,软粘,0.481,0.149,8,乌黑,稍蜷,浊响,清晰,稍凹,硬滑,0.437,0.211,9,乌黑,稍蜷,沉闷,稍糊,稍凹,硬滑,0.666,0.091,10,青绿,硬挺,清脆,清晰,平坦,软粘,0.243,0.267,11,浅白,硬挺,清脆,模糊,平坦,硬滑,0.245,0.057,12,浅白,蜷缩,浊响,模糊,平坦,软粘,0.343,0.099,13,青绿,稍蜷,浊响,稍糊,凹陷,硬滑,0.639,0.161,14,浅白,稍蜷,沉闷,稍糊,凹陷,硬滑,0.657,0.198,15,乌黑,稍蜷,浊响,清晰,稍凹,软粘,0.36,0.37,16,浅白,蜷缩,浊响,模糊,平坦,硬滑,0.593,0.042,17,青绿,蜷缩,沉闷,稍糊,稍凹,硬滑,0.719,0.103,

    说一下思路:以密度这个属性为例,首先,取出来这一列的数据,然后用一个for循环遍历一遍 循环的终止条件是n-1 ,使用这个式子生成一个candidate_set 备选划分点数据集

    for x in range(density.shape[0]-1): candidate_sets.append((density[x]+density[x+1])/2)

    大概就是上面描述的样子。有了这个备选集合,我们就要拿着这个集合给数据集进行划分,备选元素中每一个都要作为标准进行划分,并求出来划分前后的信息增益【不清楚出怎么求得可以看这一篇文章 西瓜书中的决策树算法实现(ID3) 】然后选择信息增益最大是分割点 。下面用到一些函数split_data()将数据集划分,Ent()求数据集得信息熵,是上述链接中建立得,要想知其所以然得话,还是看上述链接。

    def find_opt_discretization(D,candidate_sets): origin_entry=Ent(D)# 计算原始得信息熵 gains=[]# 建立列表 用于保存这些信息增益数据 for element in candidate_sets: d_dict=split_data(D,element)# 将数据集根据所给出的密度值划分为两个集合 set_1,set_2=np.array(d_dict['D1']),np.array(d_dict['D2']) proportion=set_1.shape[0]/D.shape[0]#计算分割后子数据集对原有数据集的比例 new_entry=proportion*Ent(set_1)+(1-proportion)*Ent(set_2)# 求出新的信息熵 gains.append(origin_entry-new_entry) return max(gains),candidate_sets[gains.index(max(gains))] # 这一句candidate_sets[gains.index(max(gains))] ,需要拆分来看,先找到gains中得最大值,继而找到最大值得索引,然后在根据索引找到对应得划分点。

    这个连续数据得划分系统全部代码如下:

    from numpy.ma import fabs import pandas as pd import numpy as np df=pd.read_csv("ml3.0.csv") density=np.array(sorted(np.array(df['密度']))) D=np.array(df[['密度','好瓜']]) candidate_sets=[] for x in range(density.shape[0]-1): candidate_sets.append((density[x]+density[x+1])/2) def collect(D): D=np.array(D) if D.shape[0]==0: return 0.0000000001 count= 0 for i in D: count=count+1 if i[-1]=='是' else count return count /(D.shape[0]) def Ent(D): p=collect(D)+0.0000000000001 if (fabs(1.0 - p) < 0.000001) or (fabs(0.0 - p) < 0.000001): return -1 #表示完全分割开了 return -p*np.log2(p)-(1-p)*np.log2(1-p) def split_data(D,density): temp_dict={"D1":[],"D2":[]} for i in D: if i[0]<density: temp_dict['D1'].append(i) else: temp_dict['D2'].append(i) return temp_dict #然后计算每一个划分点的信息增益 def find_opt_discretization(D,candidate_sets): origin_entry=Ent(D) gains=[] for element in candidate_sets: d_dict=split_data(D,element) set_1,set_2=np.array(d_dict['D1']),np.array(d_dict['D2']) proportion=set_1.shape[0]/D.shape[0] new_entry=proportion*Ent(set_1)+(1-proportion)*Ent(set_2) gains.append(origin_entry-new_entry) return max(gains),candidate_sets[gains.index(max(gains))] print(find_opt_discretization(D,candidate_sets))

    运行结果:

    (0.4977333780516909, 0.38149999999999995)

    只要将其嵌入到原来得决策树函数中就可以了。

    缺失值处理

    大部分数据都是有缺失项得,所以确实值得处理很重要。 【】在此时 突然想到 根据聚类 进行缺失值的补充。嘿嘿,不过现在还没有开始学习数据挖掘,在这里挖一个坑吧! 等以后学过数据挖掘后过来填坑(2020/10/8)

    编号,色泽,根蒂,敲声,纹理,脐部,触感,好瓜 1,,蜷缩,浊响,清晰,凹陷,硬滑,2,乌黑,蜷缩,沉闷,清晰,凹陷,,3,乌黑,蜷缩,,清晰,凹陷,硬滑,4,青绿,蜷缩,沉闷,清晰,凹陷,硬滑,5,,蜷缩,浊响,清晰,凹陷,硬滑,6,青绿,稍蜷,浊响,清晰,,软粘,7,乌黑,稍蜷,浊响,稍糊,稍凹,软粘,8,乌黑,稍蜷,浊响,,稍凹,硬滑,9,乌黑,,沉闷,稍糊,稍凹,硬滑,10,青绿,硬挺,清脆,,平坦,软粘,11,浅白,硬挺,清脆,模糊,平坦,,12,浅白,蜷缩,,模糊,平坦,软粘,13,,稍蜷,浊响,稍糊,凹陷,硬滑,14,浅白,稍蜷,沉闷,稍糊,凹陷,硬滑,15,乌黑,稍蜷,浊响,清晰,,软粘,16,浅白,蜷缩,浊响,模糊,平坦,硬滑,17,青绿,,沉闷,稍糊,稍凹,硬滑,

    下面使用书中介绍的方法, 定义时间: 数据集D , 属性a, D中在属性a上没有缺失的样本子集D1, 属性a中无缺失样本的比例 ρ \rho ρ 属性a无缺样本中第k类所占的比例 ρ k \rho_k ρk 属性a无缺样本中属性a取值为v的比例 r v r_v rv 定义完毕。

    有几个显然: 属性a无缺样本中第k类所占的比例之和,肯定是1 属性a无缺样本中属性a取值为v的比例 之和,肯定是1

    有了上面的定义,我们来分析下有缺失值下的信息增益公式。 G a i n ( D , a ) = ρ ∗ ( E n t ( D ) − ∑ v = 1 V r v E n t ( D v ) ) Gain(D,a)=\rho *(Ent(D)-\sum_{v=1}^{V}r_vEnt(D^v) ) Gain(D,a)=ρ(Ent(D)v=1VrvEnt(Dv))

    【】写道这里,我感觉只是用了未缺失的a属性取值,直接对缺失值视而不见啊。像极了用不缺失的值构建一个集合,然后用这个新的集合进行运算。可是这个也隐藏着许多问题,比如如果数据集中a的一个属性的取值全部丢失了,但是测试集中包含有该属性,怎么办?这些都是以后要考虑的

    下面就是解决方案啦,如果对于一个样本进行属性划分, 如果样本在属性a上的取值已知,那很好办,只要将该样本划分给对应的分支就可以了。 并保持x的权值不变。 但是,如果样本在属性a上的取值未知,那将a 划分到所有的子节点,但权值要根据所在属性在属性a中所占的比例,对没错,就是 r v r _v rv, 就是将 w x 修 正 为 r v ∗ w x w_x修正为r_v*w_x wxrvwx

    下面写写具体的实现步骤: 在以前的基础上,现在需要一个过滤器,过滤到那些有缺失值的样本,不如就叫他 null_filter 吧,这个函数肯定需要一个数据集D ,就是原始数据集,并且还需要一个属性,因为你是要过滤某个属性的缺失值。综上所述,可以得到一个函数

    def null_filter(D,a): return D1 #返回过滤后的数据集

    下面考虑下,这个函数要如何操作: 查看一条记录,如果对应的属性不空,就复制一份留下来, 如果为空,就跳过。 然后查看下一条 这明显是for循环啊

    def null_filter(D,a): #首先要拿到a的索引,这个是直前定义的一个列表 static_arrt 记录了各个属性在样本中的位置 index = static_attr.index(a) D1=[] for i in D: if i[index]!=NAN: D1.append(i) return np.array(D1)#返回过滤后的数据集

    下面对这个功能做一个单元测试吧;

    import pandas as pd import numpy as np df=pd.read_csv("ml2.0a.csv") static_attr=df.columns.values.tolist()[1:]#这里的属性 不改变,仅仅作为索引 df = df.fillna("-")# 使用"-" 去代替空值。 D=np.array(df[static_attr]) def null_filter(D,a): index=static_attr.index(a) D1=[] for i in D: if i[index]!='-': D1.append(i) return np.array(D1) print(null_filter(D,"色泽"))

    输出结果:

    [['乌黑' '蜷缩' '沉闷' '清晰' '凹陷' '-' '是'] ['乌黑' '蜷缩' '-' '清晰' '凹陷' '硬滑' '是'] ['青绿' '蜷缩' '沉闷' '清晰' '凹陷' '硬滑' '是'] ['青绿' '稍蜷' '浊响' '清晰' '-' '软粘' '是'] ['乌黑' '稍蜷' '浊响' '稍糊' '稍凹' '软粘' '是'] ['乌黑' '稍蜷' '浊响' '-' '稍凹' '硬滑' '是'] ['乌黑' '-' '沉闷' '稍糊' '稍凹' '硬滑' '否'] ['青绿' '硬挺' '清脆' '-' '平坦' '软粘' '否'] ['浅白' '硬挺' '清脆' '模糊' '平坦' '-' '否'] ['浅白' '蜷缩' '-' '模糊' '平坦' '软粘' '否'] ['浅白' '稍蜷' '沉闷' '稍糊' '凹陷' '硬滑' '否'] ['乌黑' '稍蜷' '浊响' '清晰' '-' '软粘' '否'] ['浅白' '蜷缩' '浊响' '模糊' '平坦' '硬滑' '否'] ['青绿' '-' '沉闷' '稍糊' '稍凹' '硬滑' '否']]

    【思考开始 错误内容可以忽略 】 应该这个样子就可以了。 但是如何把它安装到决策树中,也是比较复杂。还有权重的问题,加在哪里,怎么加这些都是问题啊! 先来考虑权重吧,每个样本都要加,对吧? 那麽说 每一个样本就会与一个对应的列表存放权重对吧? 比如

    ['乌黑' '蜷缩' '沉闷' '清晰' '凹陷' '-' '是'] #对应的矩阵就应该是 [ 1, 1, 1, 1, 1, 1, 1 ]

    也就是说会有一个同维的系数矩阵与之对应。通过原矩阵的序号进行匹配。那是都要有一个函数,我们叫他get_weight() 用于获得一个样本的权重。然后并可以修改这个权重,那说,这个矩阵应该是一个全局属性,定义在函数之外。

    #crate a weight matrix weight= np.ones(D.shape)#创建一个同维的矩阵用于记录权重。 def getweight(i): #这里i是一条记录 # 现在要将每条记录加上一个编号,用于联系样本和权重。 index=i[0]-1# 序号和索引差一 return weight[index]

    【思考结束 下面内容可以继续收看 】 不对不对,不是只维护一个权重矩阵,是这样的,每一条记录都会有一个自己的 权重,然后默认是1,但是如果碰到一个缺失属性,那么它就会被复制三份,然后被分配到三个组中,这样的继续划分 所以说媒体条记录都会多一列,用来存档权重。不如就用第一行的编号来存储。嗯,这些都做到了,代码上已经实现了,但是做出来的决策树和书本上的有点不一样。我在去检查一下。现在差不多搞定了:第一轮的信息增益如下,和西瓜书上一致。

    色泽信息增益: 0.25196581765610376 根蒂信息增益: 0.17117826469169223 敲声信息增益: 0.14480291082659255 纹理信息增益: 0.42356026795367896 脐部信息增益: 0.2888253235152557 触感信息增益: 0.005713029850070782

    现在还是从权重开始说起,原来每个样本没有属性缺失,所以等我们算一个数据集正负样本的时候,”是“就是正样本,“否”就是负样本 。但是有了权重之后,一切都不一样了,比如

    [[1 '-' '稍蜷' '浊响' '稍糊' '凹陷' '硬滑' '否'] [0.3'乌黑' '稍蜷' '浊响' '-' '稍凹' '硬滑' '是']]

    这种情况下,正负样本的比例就变成了 p = 0.3 ∗ 1 1 ∗ 1 p=\frac{0.3*1 } { 1 *1} p=110.31 所以,在计算正负样本比例的时候,要注意这一点。 下面是修正之后的collect 函数

    def collect(D): D=np.array(D) if D.shape[0]==0:# 一个小平滑 return 0.0000000001 count= 0 for i in D:# 就是在这里乘了i[0]这个系数 count=count+1*i[0] if i[-1]=='是' else count return count /(D.shape[0])

    然后信息熵的计算并没有什么新的修改

    def Ent(D): p=collect(D)+0.0000000000001 if (fabs(1.0 - p) < 0.000001) or (fabs(0.0 - p) < 0.000001): return 0 #表示完全分割开了 return -p*np.log2(p)-(1-p)*np.log2(1-p)

    信息增益的话,也有一点改动,正如上文所提到的,根本不需要做出很大变化,只要过滤掉那些缺少的值就可以了,然后之后的计算没有发生变化。不过要在过滤之前,保留下数据集的大小,用于以后计算确实比例。

    def Gain(D,a): old_size=D.shape[0] D=null_filter(D,a) new_size=D.shape[0] G=Ent(D) index = static_attr.index(a) lis=attr_dict[a] temp_dict=try_split_data(D,a) sum=0 for x in lis: d=np.array(temp_dict[x]) t=(d.shape[0]/D.shape[0])*Ent(d) # print(Ent(d)) sum+=t return (new_size/old_size)*(G-sum)

    对于集合的划分,做了较大的改动,而且还拆分为两个了,原因如下: 当我们计算信息增益的时候,缺失值是不参与的,但是当我们进行划分的时候,缺失值需要参与。 于是我用try_split_data表示原来的split_data 只是进行计算信息增益时的假划分。用real_split_data 表示真正的划分,也就是带权重的划分;

    def real_split_data(D,a): index = static_attr.index(a) lis=attr_dict[a] temp_dict={} count={}# 用于记录每种属性所占的个数 for i in lis: temp_dict[i]=[] count[i]=0 total=0 #记录共有多少个非空的数据 for i in D:# 这个数据集只处理不缺失的值 for at in lis: if i[index]==at: temp_dict[at].append(i) count[at]+=1 total+=1 break; #等常规值处理完之后处理下缺失值: for i in D:#处理缺失值,修改权重 并添加到每一个分支 if i[index]=='-': for at in lis: i[0]=count[at]/total# 修改权重 temp_dict[at].append(i)#添加到每一个分支 return temp_dict

    还有一处需要修改,因为当时我们创建属性字典的时候,默认没有缺失文件的,现在有缺失值,如果不除去缺失值,那么分类会出问题。所以修改为这样

    attr_dict={}#用于记录每一个属性的取值 for x in attr: temp=np.array(df[x]) t=list(set(temp)) t.remove('-') attr_dict[x]=t

    嗯哼? 到这里就差不多了。下面看源代码

    from numpy.ma import fabs import pandas as pd import numpy as np df=pd.read_csv("ml2.0a.csv") static_attr=df.columns.values.tolist()[0:]#这里的属性 不改变,仅仅作为索引' df['编号'] = 1# 将编号哪一列全部初始化为1 ,df 操作起来真好,真方便。 df = df.fillna("-")# 使用"-" 去代替空值。 data_org=np.array(df[static_attr]) attr=static_attr[1:-1]# 此处记录的是备选属性 weight= np.ones((data_org.shape[0],len(attr)))#创建一个同维的矩阵用于记录权重。 attr_dict={}#用于记录每一个属性的取值 for x in attr: temp=np.array(df[x]) t=list(set(temp)) t.remove('-') attr_dict[x]=t #到这里,准备工作应该差不多了。 # 下面把信息熵那些搬过来 def label_is_same(D): l=[D[i][-1] for i in range(len(D))] return len(list(set(l)))==1 def all_attr_is_same(D,d_attr): if D.shape[0]==0:return True #如果是空集 for x in d_attr: index=static_attr.index(x) for i in D[0:] : if i[index]!=D[0][index]: return False return True # 统计某个属性中,正样本和负样本的比例: def collect(D): D=np.array(D) if D.shape[0]==0: return 0.0000000001 count= 0 for i in D: count=count+1*i[0] if i[-1]=='是' else count return count /(D.shape[0]) def Ent(D): p=collect(D)+0.0000000000001 if (fabs(1.0 - p) < 0.000001) or (fabs(0.0 - p) < 0.000001): return 0 #表示完全分割开了 return -p*np.log2(p)-(1-p)*np.log2(1-p) def null_filter(D,a): index=static_attr.index(a) D1=[] for i in D: if i[index]!='-': D1.append(i) return np.array(D1) def real_split_data(D,a): index = static_attr.index(a) lis=attr_dict[a] temp_dict={} count={} for i in lis: temp_dict[i]=[] count[i]=0 total=0 for i in D: for at in lis: if i[index]==at: temp_dict[at].append(i) count[at]+=1 total+=1 break; #等常规值处理完之后处理下缺失值: for i in D: if i[index]=='-': for at in lis: i[0]=count[at]/total temp_dict[at].append(i) return temp_dict def try_split_data(D,a): index = static_attr.index(a) lis=attr_dict[a] temp_dict={} for i in lis: temp_dict[i]=[] for i in D: for at in lis: if i[index]==at: temp_dict[at].append(i) break; return temp_dict def Gain(D,a): old_size=D.shape[0] D=null_filter(D,a) new_size=D.shape[0] G=Ent(D) index = static_attr.index(a) lis=attr_dict[a] temp_dict=try_split_data(D,a) sum=0 for x in lis: d=np.array(temp_dict[x]) # print("____________________start_________________") # print(d) # print("____________________end_________________") t=(d.shape[0]/D.shape[0])*Ent(d) # print(Ent(d)) sum+=t return (new_size/old_size)*(G-sum) def select_opt_attr(D,d_attr): li=[] for x in d_attr: li.append(Gain(D,x)) return d_attr[li.index(max(li))] tree={} def TreeGenerate(D,d_attr,node,father):#ID3算法 if type(D)==dict:return if D.shape[0]==0: node['final']="undefind" return if label_is_same(D): node['final']=D[-1][-1] return if len(d_attr)==0 or all_attr_is_same(D,d_attr):# 属性集为空 或者所有样本在说有属性上取值相同 node['final']='是' if collect(D)>0.5 else '否' return #选择最优的划分属性: opt_attr=select_opt_attr(D,d_attr) #根据最优属性划分集合: attr_dict=real_split_data(D,opt_attr) d_attr.remove(opt_attr) node[opt_attr]={} for i in attr_dict: d1=np.array(attr_dict[i]) node[opt_attr][i]={} TreeGenerate(d1,d_attr[:],node[opt_attr][i],i) TreeGenerate(data_org,attr,tree,"root") print(tree) # for i in static_attr[1:-1]: # print(i+"信息增益:",Gain(data_org,i))

    运行结果 :

    D:\PYTHON\python.exe C:/Users/dell/Desktop/python/DecisionTree/Missing_value.py {'纹理': {'清晰': {'根蒂': {'稍蜷': {'色泽': {'青绿': {'final': '是'}, '浅白': {'final': 'undefind'}, '乌黑': {'触感': {'硬滑': {'final': '是'}, '软粘': {'final': '否'}}}}}, '硬挺': {'final': '否'}, '蜷缩': {'final': '是'}}}, '稍糊': {'敲声': {'沉闷': {'final': '否'}, '清脆': {'final': '否'}, '浊响': {'触感': {'硬滑': {'脐部': {'稍凹': {'final': '是'}, '平坦': {'final': 'undefind'}, '凹陷': {'final': '否'}}}, '软粘': {'final': '是'}}}}}, '模糊': {'色泽': {'青绿': {'final': '否'}, '浅白': {'final': '否'}, '乌黑': {'final': '是'}}}}} Process finished with exit code 0

    就这样,一篇文章写了一天,现在是21:42 分 。差不多可以收拾下回宿舍了。 今天是国庆假期的最后一天,祝大家复工快乐!

    Processed: 0.009, SQL: 8