Python手撸机器学习系列(十二):连续特征决策树与缺失值决策树实现

连续值与缺失值决策树

本节代码和理论主要参考自周志华《机器学习》

ID3、C4.5以及CART决策树和剪枝代码请参考我的上一篇博客https://blog.csdn.net/qq_43601378/article/details/121360503?spm=1001.2014.3001.5502

1. 连续值决策树

对于连续特征,不能再根据连续属性的可取值对节点进行划分,而是应该使用取值范围进行划分。最简单的策略是二分法,即先将所有样本的取值排序,然后去两个样本取值的中点作为一个切分点,如有n个样本,则共计生成n-1个切分点。任选一个切分点,将样本按照小于切分点值和大于切分点值分为两部分去,计算划分后的信息增益,选取让信息增益最大的划分点作为最优切分点。

信息增益的计算方式可以改为:
G a i n ( D , a ) = max ⁡ t ∈ T a   G a i n ( D , a , t ) = max ⁡ t ∈ T a   E n t ( D ) − ∑ λ ∈ { − , + } ∣ D t λ ∣ ∣ D ∣ E n t ( D t λ ) \begin{aligned} Gain(D,a) &= \max_{t\in T_a} \ Gain(D,a,t)\\&= \max_{t\in T_a} \ Ent(D)-\sum_{\lambda \in\{-,+\}}\frac{|D_t^\lambda|}{|D|}Ent(D_t^\lambda) \end{aligned} Gain(D,a)=tTamax Gain(D,a,t)=tTamax Ent(D)λ{,+}DDtλEnt(Dtλ)
其中 λ \lambda λ表示小于切分点和大于切分点的两部分, E n t ( D ) = − ∑ k = 1 ∣ y ∣ p k l o g 2 p k Ent(D) = -\displaystyle\sum_{k=1}^{|y|} p_klog_2p_k Ent(D)=k=1ypklog2pk 与之前保持一致

代码的实现要点主要在于:

  • 离散值与连续值特征计算信息增益需要分开,所以最好将离散特征在主函数中标记出来
  • 划分特征时如果最佳特征是连续特征,需要返回特征名称以及最佳划分点的值,而离散特征只需要返回特征名称
  • 递归创建决策树时,离散特征需要判断当前数据的最优特征的取值有没有取满(比如说色泽这个特征有三个取值但是到这个节点了数据只有两个取值了),而连续特征不需要;连续特征实际上只有两个分支:对于最优划分点的是和否。
  • 在编写决策树预测函数时,仍然需要判断当前特征是连续还是离散,因为连续特征的名字(与书上保持一致)带上了最优划分点(如原始特征叫做“密度”,而决策树的特征叫做“密度<xxxx”),所以不是很好判断,这点看代码会更清晰。

话不多说,直接上代码:
西瓜数据集3.0:百度网盘自取
提取码:q4g9

代码注释非常详尽,不再赘述

import pandas as pd
import numpy as np

#计算信息熵
def cal_information_entropy(data):
    data_label = data.iloc[:,-1]
    label_class =data_label.value_counts() #总共有多少类
    Ent = 0
    for k in label_class.keys():
        p_k = label_class[k]/len(data_label)
        Ent += -p_k*np.log2(p_k)
    return Ent

#对于离散特征a,计算给定数据属性a的信息增益
def cal_information_gain(data, a):
    Ent = cal_information_entropy(data)
    feature_class = data[a].value_counts() #特征有多少种可能
    gain = 0
    for v in feature_class.keys():
        weight = feature_class[v]/data.shape[0]
        Ent_v = cal_information_entropy(data.loc[data[a] == v])
        gain += weight*Ent_v
    return Ent - gain

#对于连续特征b,计算给定数据属性b的信息增益
def cal_information_gain_continuous(data, a):
    n = len(data) #总共有n条数据,会产生n-1个划分点,选择信息增益最大的作为最优划分点
    data_a_value = sorted(data[a].values) #从小到大排序
    Ent = cal_information_entropy(data) #原始数据集的信息熵Ent(D)
    select_points = []
    for i in range(n-1):
        val = (data_a_value[i] + data_a_value[i+1]) / 2 #两个值中间取值为划分点
        data_left = data.loc[data[a]<val]
        data_right = data.loc[data[a]>val]
        ent_left = cal_information_entropy(data_left)
        ent_right = cal_information_entropy(data_right)
        result = Ent - len(data_left)/n * ent_left - len(data_right)/n * ent_right
        select_points.append([val, result])
    select_points.sort(key = lambda x : x[1], reverse= True) #按照信息增益排序
    return select_points[0][0], select_points[0][1] #返回信息增益最大的点, 以及对应的信息增益

#获取标签最多的那一类
def get_most_label(data):
    data_label = data.iloc[:,-1]
    label_sort = data_label.value_counts(sort=True)
    return label_sort.keys()[0]

#获取最佳划分特征
def get_best_feature(data):
    features = data.columns[:-1]
    res = {}
    for a in features:
        if a in continuous_features:
            temp_val, temp = cal_information_gain_continuous(data, a)
            res[a] = [temp_val, temp]
        else:
            temp = cal_information_gain(data, a)
            res[a] = [-1, temp] #离散值没有划分点,用-1代替

    res = sorted(res.items(),key=lambda x:x[1][1],reverse=True)
    return res[0][0],res[0][1][0]

#将数据转化为(属性值:数据)的元组形式返回,并删除之前的特征列,只针对离散数据
def drop_exist_feature(data, best_feature):
    attr = pd.unique(data[best_feature])
    new_data = [(nd, data[data[best_feature] == nd]) for nd in attr]
    new_data = [(n[0], n[1].drop([best_feature], axis=1)) for n in new_data]
    return new_data

#创建决策树
def create_tree(data):
    data_label = data.iloc[:,-1]
    if len(data_label.value_counts()) == 1: #只有一类
        return data_label.values[0]
    if all(len(data[i].value_counts()) == 1 for i in data.iloc[:,:-1].columns): #所有数据的特征值一样,选样本最多的类作为分类结果
        return get_most_label(data)
    best_feature, best_feature_val = get_best_feature(data) #根据信息增益得到的最优划分特征
    if best_feature in continuous_features: #连续值
        node_name = best_feature + '<' + str(best_feature_val)
        Tree = {node_name:{}} #用字典形式存储决策树
        Tree[node_name]['是'] = create_tree(data.loc[data[best_feature] < best_feature_val])
        Tree[node_name]['否'] = create_tree(data.loc[data[best_feature] > best_feature_val])
    else:
        Tree = {best_feature:{}}
        exist_vals = pd.unique(data[best_feature])  # 当前数据下最佳特征的取值
        if len(exist_vals) != len(column_count[best_feature]):  # 如果特征的取值相比于原来的少了
            no_exist_attr = set(column_count[best_feature]) - set(exist_vals)  # 少的那些特征
            for no_feat in no_exist_attr:
                Tree[best_feature][no_feat] = get_most_label(data)  # 缺失的特征分类为当前类别最多的
        for item in drop_exist_feature(data, best_feature):  # 根据特征值的不同递归创建决策树
            Tree[best_feature][item[0]] = create_tree(item[1])
    return Tree

#根据创建的决策树进行分类
def predict(Tree , test_data):
    first_feature = list(Tree.keys())[0]
    if (feature_name:= first_feature.split('<')[0]) in continuous_features:
        second_dict = Tree[first_feature]
        val = float(first_feature.split('<')[-1])
        input_first = test_data.get(feature_name)
        if input_first < val:
            input_value = second_dict['是']
        else:
            input_value = second_dict['否']
    else:
        second_dict = Tree[first_feature]
        input_first = test_data.get(first_feature)
        input_value = second_dict[input_first]
    if isinstance(input_value , dict): #判断分支还是不是字典
        class_label = predict(input_value, test_data)
    else:
        class_label = input_value
    return class_label

if __name__ == '__main__':
    data = pd.read_csv('西瓜数据集3.0.csv')
    # 统计每个特征的取值情况作为全局变量
    column_count = dict([(ds, list(pd.unique(data[ds]))) for ds in data.iloc[:, :-1].columns])

    test = cal_information_gain_continuous(data, '密度')
    continuous_features = ['密度', '含糖率']  #先标注连续值
    dicision_tree = create_tree(data)
    print(dicision_tree)
    test_data =  {'色泽':'青绿','根蒂':'蜷缩','敲声':'浊响','纹理':'清晰','脐部':'凹陷','触感':'硬滑','密度':0.51,'含糖率':0.3}
    result = predict(dicision_tree, test_data)
    print('预测结果为:{}'.format('好瓜' if result == 1 else '坏瓜'))

结果:

{'纹理': {'清晰': {'密度<0.3815': {'是': 0, '否': 1}}, '稍糊': {'触感': {'软粘': 1, '硬滑': 0}}, '模糊': 0}}

与周志华《机器学习》p85图4.8一致

2. 缺失值决策树

有缺失值的情况,如果把带缺失值的特征全部删除太浪费,利用带缺失值的数据需要解决两个问题:

  • 如何在特征值有缺失的情况下进行划分特征选择
  • 如果选出了最优特征,如果有的样本在这个特征上是空值,该把这个样本分到哪个特征取值中去

针对问题一,可以筛选出某个特征不含空值的数据,然后按照之前的信息增益计算公式计算不含空值的数据的信息增益,最后乘上不含空值的样本权值和占整体样本的权值和比值即可。(所有样本都有一个权值,初始化为1,并随着空值的划分而衰减)

用公式表述为:
G a i n ( D , a ) = ρ × G a i n ( D ~ , a ) = ρ × ( E n t ( D ~ ) − ∑ v = 1 V r ~ v E n t ( D ~ v ) ) \begin{aligned} Gain(D,a) &= \rho ×Gain(\tilde D,a) \\&=\rho × (Ent(\tilde D)-\displaystyle\sum_{v=1}^V\tilde r_v Ent(\tilde D^v)) \end{aligned} Gain(D,a)=ρ×Gain(D~,a)=ρ×(Ent(D~)v=1Vr~vEnt(D~v))
其中 ρ \rho ρ表示特征 a a a没有空值的样本权值和占当前样本权值和的比值, r ~ v \tilde r_v r~v表示特征某取值样本权值和占当前样本权值和的比值,与之前信息增益中的权值一致。单需要注意的是,之前信息熵中的 p k p_k pk也变为权值和的比值,而不是数量的比值。

针对问题二,当某个样本在这个特征上有值时,就划分到该值的节点上去,权值维持为1。如果没有的话,就将这个样本划分到所有的节点,并根据节点的样本数量占比衰减权值。比如有15个样本,在某个特征有3个取值,这三个取值的样本数量为7,5,3,还有一个样本在该特征去空值,则将该样本扩充到这三个子集上去,但权值分别衰减为原来的7/15,5/15以及3/15。扩充后的三个节点的样本数量为8,6,4,假设原来的权值都为1,那么扩充后的权值分别为[7个1,1个7/15]、[5个1,1个5/15]、[3个1,1个3/15]

在代码实现上,要点如下:

  • 将权值作为数据的一个特征可以便于递归处理(在pandas处理的角度)
  • 空值是指在当前特征的空值,而不是该样本完全是空值
  • 信息增益计算方式与之前大同小异,只是导入的数据需要筛选为非空的,以及比值都是权值之和的比
  • 预测函数与之前保持一致,因为决策树也是一样的结构,但是预测数据不能有空值,目前这是我代码的一个缺陷

话不多说,直接上代码:
西瓜数据集2.0a:百度网盘自取
提取码:u6ub

代码有着足够多的注释,大家应该都能看懂

import pandas
import pandas as pd
import numpy as np

#计算信息熵
def cal_information_entropy(data):
    data_label = data.iloc[:,-1] #类别标签
    label_class = data_label.value_counts() #总共有多少类
    Ent = 0
    D_W_x = sum(data['weights']) #整体数据的权值和
    for k in label_class.keys():
        D_k_wx = sum(data.loc[data_label == k]['weights']) #每个类别的权值和
        p_k = D_k_wx / D_W_x
        Ent += -p_k*np.log2(p_k)
    return Ent

#计算信息增益
#计算给定数据属性a的信息增益
def cal_information_gain(data, a, p): #p表示课本中的ρ,即非空数据占整体数据样本的比例
    Ent = cal_information_entropy(data) #整体信息熵
    feature_class = data[a].value_counts() #特征有多少种可能
    gain = 0
    for v in feature_class.keys():
        r = sum(data[data[a] == v]['weights'])/sum(data['weights']) # 课本上的r,表示该特征某一个取值的样本权值和占整体样本权值和的比值
        Ent_v = cal_information_entropy(data.loc[data[a] == v])
        gain += r*Ent_v
    return p*(Ent - gain)

#获取标签最多的那一类
def get_most_label(data):
    data_label = data.iloc[:,-1]
    label_sort = data_label.value_counts(sort=True)
    return label_sort.keys()[0]

#挑选最优特征,即信息增益最大的特征
def get_best_feature(data):
    features = data.columns[1:-1]
    res = {}
    for a in features:
        data_not_null = data.dropna(axis=0,subset = [a]) #该特征不为空的数据
        p = sum(data_not_null['weights']) / sum(data['weights']) #占比
        temp = cal_information_gain(data_not_null, a, p ) #用非空的数据去算信息增益,最后乘上p
        res[a] = temp
    res = sorted(res.items(),key=lambda x:x[1],reverse=True) #按照信息增益排名
    return res[0][0]

##将数据转化为(属性值:数据)的元组形式返回,并删除之前的特征列
def drop_exist_feature(data, best_feature):
    attr = pd.unique(data.dropna(axis=0,subset = [best_feature])[best_feature]) #最佳划分特征的取值可能,先不包括空值
    res = []
    data_non = data[data[best_feature].isna()] #该特征为空的数据
    for val in attr:
        new_data = data[data[best_feature] == val]
        p = len(new_data) / len(data) #计算当前取值占比
        if len(data_non) > 0: #如果有的话
            data_non_cp = data_non.copy()
            data_non_cp['weights'] *= p #权值变小
            new_data = new_data.append(data_non_cp) #并入数据
        res.append((val, new_data))
    final_data = [(n[0], n[1].drop([best_feature], axis=1)) for n in res] #删除用过的特征
    return final_data

#创建决策树
def create_tree(data):
    data_label = data.iloc[:,-1]
    if len(data_label.value_counts()) == 1: #只有一类
        return data_label.values[0]
    if all(len(data[i].value_counts()) == 1 for i in data.iloc[:,:-1].columns): #所有数据的特征值一样,选样本最多的类作为分类结果
        return get_most_label(data)
    best_feature = get_best_feature(data) #根据信息增益得到的最优划分特征
    Tree = {best_feature:{}} #用字典形式存储决策树
    exist_vals = pd.unique(data[best_feature])  # 当前数据下最佳特征的取值
    if len(exist_vals) != len(column_count[best_feature]):  # 如果特征的取值相比于原来的少了
        no_exist_attr = set(column_count[best_feature]) - set(exist_vals)  # 少的那些特征
        for no_feat in no_exist_attr:
            Tree[best_feature][no_feat] = get_most_label(data)  # 缺失的特征分类为当前类别最多的
    for item in drop_exist_feature(data,best_feature): #根据特征值的不同递归创建决策树
        Tree[best_feature][item[0]] = create_tree(item[1])
    return Tree

def predict(Tree , test_data):
    first_feature = list(Tree.keys())[0]
    second_dict = Tree[first_feature]
    input_first = test_data.get(first_feature)
    input_value = second_dict[input_first]
    if isinstance(input_value , dict): #判断分支还是不是字典
        class_label = predict(input_value, test_data)
    else:
        class_label = input_value
    return class_label

if __name__ == '__main__':
    data = pd.read_csv('西瓜数据集2.0a.csv')
    # 统计每个特征的取值情况作为全局变量, 空值不算做一个取值
    column_count = dict([(ds, list(pd.unique(data.dropna(axis=0,subset = [ds])[ds]))) for ds in data.iloc[:, :-1].columns])
    data.insert(0, 'weights', 1) #插入每个样本权值
    tree = create_tree(data)
    print(tree)

结果:

{'纹理': {'清晰': {'根蒂': {'蜷缩': 1, '稍蜷': {'色泽': {'浅白': 1, '青绿': 1, '乌黑': {'触感': {'软粘': 0, '硬滑': 1}}}}, '硬挺': 0}}, '稍糊': {'敲声': {'浊响': {'脐部': {'平坦': 1, '稍凹': 1, '凹陷': 0}}, '沉闷': 0, '清脆': 0}}, '模糊': {'色泽': {'浅白': 0, '乌黑': 1, '青绿': 0}}}}

与周志华《机器学习》p89图4.9完全一致(同一层节点顺序可能有所变化)

3.联系方式

有错误或问题欢迎评论区指出,或邮件联系:

1759412770@qq.com

  • 11
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锌a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值