DecisionTree以及可视化

使用决策树方法对泰坦尼克幸存者进行预测。
数据下载链接:https://share.weiyun.com/h93y7TnI
数据集为1912年泰坦尼克号沉船事件中一些船员的个人信息以及存活状况。这些历史数据已经非分为训练集和测试集,你可以根据训练集训练出合适的模型并预测测试集中的存活状况。

1. 数据处理

  • 数据处理
def data_pross(train_data):
    age_pross(train_data)
    fare_pross(train_data)
  • 年龄处理
def age_pross(train_data):
    '''
    离散值处理age
    :param train_data:
    :return:
    '''
    for i in range(len(train_data)):
        # if 15 > train_data[i][2] >= 0:
        #     train_data[i][2] = 0
        # elif 15 <= train_data[i][2] < 20:
        #     train_data[i][2] = 1
        # elif 20 <= train_data[i][2] < 25:
        #     train_data[i][2] = 2
        # elif 25 <= train_data[i][2] < 30:
        #     train_data[i][2] = 3
        # elif 30 <= train_data[i][2] < 35:
        #     train_data[i][2] = 4
        # elif 35 <= train_data[i][2] < 40:
        #     train_data[i][2] = 5
        # elif 40 <= train_data[i][2] < 45:
        #     train_data[i][2] = 6
        # elif 45 <= train_data[i][2] < 50:
        #     train_data[i][2] = 7
        # elif 50 <= train_data[i][2] < 55:
        #     train_data[i][2] = 8
        # elif 55 <= train_data[i][2] < 60:
        #     train_data[i][2] = 9
        # else:
        #     train_data[i][2] = 10
        if 15 > train_data[i][2] >= 0:
            train_data[i][2] = 0
        elif 65 <= train_data[i][2] < 100:
            train_data[i][2] = 65
        elif train_data[i][2] >= 100:
            train_data[i][2] = 100
        else:
            train_data[i][2] = int(train_data[i][2])
  • 体热指标处理
def fare_pross(train_data):
    '''
    处理体热指标
    :param train_data:
    :return:
    '''

    for i in range(len(train_data)):
        # train_data[i][5] =
        if type(train_data[i][5]).__name__ == 'str':
            print(train_data[i])
            continue
        if train_data[i][5] >= 300:
            train_data[i][5] = 41
        elif 300 >= train_data[i][5] > 240:
            train_data[i][5] = 40
        elif 240 >= train_data[i][5] > 200:
            train_data[i][5] = 39
        elif 200 >= train_data[i][5] > 160:
            train_data[i][5] = 38
        elif 160 >= train_data[i][5] > 120:
            train_data[i][5] = 37
        elif 120 >= train_data[i][5] > 100:
            train_data[i][5] = 36
        elif 100 >= train_data[i][5] > 90:
            train_data[i][5] = 35
        elif 90 >= train_data[i][5] > 80:
            train_data[i][5] = 34
        elif 80>= train_data[i][5] > 70:
            train_data[i][5] = 33
        elif 70 >= train_data[i][5] > 65:
            train_data[i][5] = 32
        elif 65 >= train_data[i][5] > 60:
            train_data[i][5] = 31
        elif 60 >= train_data[i][5] >= 0:
            train_data[i][5] = int(int(train_data[i][5]) / 2)
        else:
            train_data[i][5] = 50

2. DecisionTree的实现

由于graphviz的接口调用是通过python实现的,因此没有选用更为适合构建树形状的C或者C++,而是依旧选择了Python。
下面是对程序中的各模块解释。

  • 代码所应用的包
from pandas import read_csv
from sklearn.model_selection import train_test_split
import numpy as np
import collections as cc
import time
import math
from graphviz import Digraph
import xlrd
import xlwt

pandas用于读取沉船数据。
由于机器学习任务目标是手动实现decisionTree,因此sklearn只用于划分数据集。
graphviz用于画图
xlrd和xlwt用于预测test

  • 公共数据区
dot = Digraph(name="DecisionTree", format="pdf")
dict2 = {0: 'Pclass', 1: 'Sex', 2: 'Age', 3: 'SibSp', 4: 'Parch', 5: 'Fare', 6: 'Embarked'}
count = 1
Feature_list = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']

dot用于保存图
dict2对应需要特征的字典
Feature_list为保存特征的列表

  • 读取train的csv文件
def generate_dataset(file_name):
    '''
    读取train文件数据
    :param file_name:
    :return:
    '''
    data_dict = read_csv(file_name, index_col=0)
    # 姓名,船舱号,票号三个无关特征丢弃
    data_dict.drop(['Name', 'Ticket', 'Cabin'], axis=1, inplace=True)

    #Pandas的read_csv返回的结构不是直接的字典,是以series结构发过来的。
    # 将性别转换为数值
    # print(type(data_dict['Sex']))
    # for i in range(len(data_dict)):
    #     if data_dict['Sex'] == 'male':
    #         data_dict['Sex'] = 1
    #     else:
    #         data_dict['Sex'] = 0
    data_dict['Sex'] = (data_dict['Sex'] == 'male').astype('int')

    # 按照不同的港口名称标签赋予不同的值
    lables = data_dict['Embarked'].unique().tolist()
    data_dict['Embarked'] = data_dict['Embarked'].apply(lambda n: lables.index(n))

    # 体热指标,考虑是否去零,是否选择归一化
    # data_dict['Fare'] = data_dict['Fare'].apply(lambda n: n-n % 1)
    data_dict['Fare'] = data_dict['Fare'].apply(lambda n: n)

    # Embarked 2 null less
    # Cabin 687 null  aborted
    # Age 177 null
    # 处理age和embarked的缺失值
    # 缺失值应当用概率分布生成比较好,但是比较麻烦,选择用不改变方差的平均值处理
    data_dict['Age'].fillna(data_dict['Age'].mean(), inplace=True)
    data_dict['Age'].fillna(int(data_dict['Embarked'].mean()), inplace=True)
    # int(data_dict['Embarked'].mean())
    y = data_dict['Survived'].values
    data_dict.drop('Survived', axis=1, inplace=True)
    x = data_dict.values
    return x, y, lables

数据处理,格式处理,标记数据提取,返回Embarked列表用于处理test文件

  • 最大类寻找
def FindMajorClass(label):
    '''
    找到最大的类
    :param label: 
    :return: 
    '''
    # 对label集进行统计
    # .most_common(1),从Counter中提起出现数目最多的一组
    MajorClass = cc.Counter(label).most_common(1)[0][0]

    return MajorClass
  • 信息熵计算
def Entropy(label):
    '''
    计算信息熵
    :param label: 
    :return: 
    '''
    # 所有的类
    Class = np.unique(label)

    # 对每个类统计出现次数
    ClassNum = cc.Counter(label)

    # 得到标记的总数
    Classlen = len(label)

    # 初始化熵
    H = 0

    # 遍历每一个类
    for c in Class:

        # 计算每个类出现的概率
        P = ClassNum[c] / Classlen

        # 计算经验熵
        # 这里的对数以e为底
        H += -1 * P * math.log(P,2)

    return H
  • 信息熵获取
def get_Entropy(data,label):
    '''
    获取信息熵
    :param data: 
    :param label: 
    :return: 
    '''
    FeatureNum = len(data[0])
    dataNum = len(data)
    C_H_Arr = []#放置条件熵
    for f in range(FeatureNum):

    # 所有样本点的特征f的值
        f_data = list(np.array(data).T[f])

    # 特征f的可取值
        f_value = np.unique(f_data)

    # 初始化特征f的条件熵
        C_H = 0

    # 遍历特征f的每一个可取值
        for f_v in f_value:

        # 得到f_data中值为f_v的所有index
            index = np.argwhere(f_data == f_v)
            # print(index)
            # 准备一个空列表存储满足值为f_v的所有标记
            f_label = []

            for i in index:

                # 得到该特征下满足值为f_v的对应的所有标记
                f_label.append(label[i[0]])

            # 计算f_label的熵
            f_H = Entropy(f_label)

            # 得到在该特征下,值为f_v的概率
            f_P = len(f_label) / dataNum

            # 计算条件熵
            C_H += f_P * f_H

    # 记录每个特征的条件熵
        C_H_Arr.append(C_H)
    # print(C_H_Arr)
    return C_H_Arr
  • 信息增益获取
def InforGain(data, label):
    '''
    获取信息增益
    :param data: 
    :param label: 
    :return: 
    '''
    # 计算当前结点的经验熵
    H = Entropy(label)

    # 计算当前结点的经验条件熵
    C_H_Arr = get_Entropy(data, label)

    # 得到最大的信息增益
    IG = [H - num for num in C_H_Arr]
    IGMax = max(IG)

    # 得到最大信息增益对应的特征
    BestFeature = IG.index(IGMax)
    #print(BestFeature)
    return BestFeature, IGMax
  • 数据集划分
def SplitDataSet(data, label, feature):
    '''
    数据集划分
    :param data: 
    :param label: 
    :param feature: 
    :return: 
    '''
    # 样本数
    SampleNum = len(label)

    # 转置data
    data_T = np.transpose(data)

    # 获得最佳特征的可取值
    feature_value = np.unique(data_T[feature])

    # 准备两个列表,用来存放分割后的子集
    datasets = []
    labelsets = []

    # 遍历最佳特征的每个取值
    for f in feature_value:

        datasets_sub = []
        labelsets_sub = []

        # enumerate不仅遍历元素,同时遍历元素的下标
        # 此处遍历每个样本在最佳特征的取值和下标
        for Index, num in enumerate(data_T[feature]):

            # 当data中的某个样本的该特征=f时,获得它的index
            if num == f:

                # 将用于划分该样本点的最佳特征从数据集中去除
                # 去除后在下一次的迭代中将不再考虑这个特征
                data_temp = data[Index]
                del data_temp[feature]

                # 存储划分后的子集
                # 此时得到的仅为最佳特征的一个取值下的子集
                datasets_sub.append(data_temp)
                labelsets_sub.append(label[Index])

        # 存储根据最佳特征的不同取值划分的子集
        datasets.append(datasets_sub)
        labelsets.append(labelsets_sub)

    return datasets, labelsets
  • 决策树构建
def CreateTree(pre_train_data, pre_train_label, epsilon, parent_dot="DecisionTree"):
    '''
    构建决策树
    '''   
    # 类别去重
    Class = np.unique(pre_train_label)

    # 如果对于当前的标签集合而言,类别只有一个
    # 说明这个结点是叶结点,返回这个类
    if len(Class) == 1:
        return Class[0]

    # 如果已经没有特征可以进行分类了,返回当前label集中数目最多的类
    if len(pre_train_data[0]) == 0:
        return FindMajorClass(pre_train_label)

    # 其它情况下,需要继续对结点进行分类,计算信息增益
    # 得到信息增益最大的特征,及其信息增益
    BestFeature, IGMax = InforGain(pre_train_data, pre_train_label)
    # feature_name = get_best_feature_name(BestFeature)
    # print('Best is {}, type is {}, feature name is '.format(BestFeature, type(BestFeature)))
    # 如果最佳特征的信息增益小于一个我们自己设定的阈值
    # 则采用当前标记中数目最多的类
    if IGMax < epsilon:
        return FindMajorClass(pre_train_label)

    # 构建树
    treeDict = {BestFeature:{}}
    # dot.node(name=parent_dot, label=parent_dot)
    # 树生成后,对数据集根据最佳特征进行划分
    subdatasets, sublabelsets = SplitDataSet(pre_train_data, pre_train_label, BestFeature)

    # 子集的个数
    setsNum = len(sublabelsets)

    # 对子集进行迭代,创建子树
    for i in range(setsNum):

        # 这里运用的迭代思想
        # 即在一个自定义函数中调用自己
        treeDict[BestFeature][i] = CreateTree(subdatasets[i], sublabelsets[i], epsilon, )
        string = dict2[BestFeature]

    return treeDict
  • Survived值预测
def Predict(data, tree):
    '''
    Survived值预测
    :param data: 
    :param tree: 
    :return: 
    '''
    # 初始化Class
    Class = -1
    
    # 当Class被赋予了新值,也就是说该样本点被分类,则停止循环
    while Class == -1:
        
        # 获得当前结点的key和value
        # key代表结点中需要对哪一个特征进行判断
        # value代表结点的可取值
        (key, value), = tree.items()
        
        # 该样本在结点所需判断的特征的值
        feature_value = data[key]
        if feature_value in value :
       # print(feature_value)
        # 如果判断下来,其值还是字典
        # 那么就说明还在内部结点,要继续往下分
            if type(value[feature_value]).__name__ == 'dict':

                # 将该内部结点及其子树设为新的树
                tree = value[feature_value]
                
                # 删除该结点所对应的特征
              #  del data[key]

            # 如果判断下来是不是字典了,说明到达叶节点
            if type(value[feature_value]).__name__ != 'dict':

                # 则返回叶结点对应的分类
                Class = value[feature_value]
                #print(Class)
        else : 
            keyss = value.keys()
            thevalue = [999,999]
            for i in keyss:
                temp = abs(i-feature_value)
                if (temp < thevalue[0]):
                    thevalue[0] = temp
                    thevalue[1] = i
            feature_value = thevalue[1]
           # print(feature_value)
            if type(value[feature_value]).__name__ == 'dict':

                # 将该内部结点及其子树设为新的树
                tree = value[feature_value]
                
                # 删除该结点所对应的特征
                # del data[key]

            # 如果判断下来是不是字典了,说明到达叶节点
            if type(value[feature_value]).__name__ != 'dict':

                # 则返回叶结点对应的分类
                Class = value[feature_value]
                #print(Class)
    return Class
  • 计算正确率
def Classifier(test_data, test_label, tree):
    
    # 测试集测试样本数量
    TestNum = len(test_label)
    
    # 初始化分类错误的个数
    errorCnt = 0
    
    # 遍历每一个测试样本点
    for i in range(TestNum):
            Class = Predict(test_data[i], tree)

            if Class != test_label[i]:
                errorCnt += 1
    
    # 计算正确率
    Acc = 1 - (errorCnt / TestNum)
    
    return Acc

3. 可视化

由于决策树的输出数据结构为字典嵌套,并且特征名为数字代指,因此需要设计算法来获取特证名,以及利用递归算法构造节点与边。
关于graphviz的安装说明可见上一博客(https://blog.csdn.net/qq_44459787/article/details/110496977

  • 利用决策树结构构造节点与边
def graphviz_tree(treedic : dict, flag_list=[0, 0, 0, 0, 0, 0], parent_key= None):
    for k, v in treedic.items():
        temp_key = parent_key + str(k)
        if type(v).__name__ == 'dict':
            for kindex, vdict in v.items():
                index, feature_name = get_best_feature_name(kindex, flag_list)
                dot.node(name=temp_key, label=feature_name)
                dot.edge(parent_key, temp_key, label=str(k))
                graphviz_tree(vdict, flag_list, parent_key=temp_key)
                flag_list[index] = 0
        else:
            dot.node(name=temp_key, label=str(v))
            dot.edge(parent_key, temp_key, label=str(k))
  • 画图
def draw_viz(treedic: dict, flag_list = [0, 0, 0, 0, 0, 0, 0]):
    for k, v in treedic.items():
        index, featureName = get_best_feature_name(k, flag_list)
        key = str(k)
        dic = v
        graphviz_tree(v, flag_list, key)
  • 根据字典特征获取特征名
def get_best_feature_name(flag, flag_list):
    for i in range(len(Feature_list)):
        if flag == 0 and flag_list[i] == 0:
            flag_list[i] = 1
            return i, Feature_list[i]
        elif flag == 0 and flag_list[i] != 0:
            continue
        elif flag_list[i] != 0:
            continue
        else:
            flag = flag - 1
  • 测试数据载入与初始化
def gen_test_dict(lables_list:list):
    test_data = xlrd.open_workbook('TEST.xlsx')
    sheet = test_data.sheet_by_name('Sheet1')

    test_data_list = []

    for i in range(sheet.nrows):
        if i == 0:
            continue
        dlist = sheet.row_values(i)
        # sex处理
        if dlist[1] == 'male':
            dlist[1] = 1
        else:
            dlist[1] = 0
        # embark处理
        for index, j in enumerate(lables_list):
            if j == dlist[-1]:
                dlist[-1] = index
                break
        # fare空值处理
        if dlist[5] == '':
            dlist[5] = 0
        test_data_list.append(dlist)
    fare_pross(test_data_list)
    return test_data_list
  • 预测结果并保存
def forecast_data(test_data_list:list, tree):
    workbook = xlwt.Workbook(encoding='utf-8')
    worksheet = workbook.add_sheet('Forecasted')
    worksheet.write(0, 0, label='Survived')
    for i, data in enumerate(test_data_list):
        Class = Predict(data, tree)
        worksheet.write(i + 1, 0, label=str(Class))
        # print("index:{}\tClass:{}\n".format(i, str(Class)))
    workbook.save('Forecasted.xls')

4. 主函数

if __name__ == "__main__":
    # 准备一个空列表记录不同分组情况下的正确率
    file_name = r'D:\Pythonwork\DecisionTree\train.csv'

    # 训练接数据导入
    x, y, lables_list = generate_dataset(file_name)

    xtrain, xtest, ytrain, ytest = train_test_split(x, y, test_size=0.2)
    xtrain = xtrain.tolist()
    xtest = xtest.tolist()
    print(len(xtest))
    # data_pross(xtrain)
    # data_pross(xtest)
    # age_pross(xtest)
    # age_pross(xtrain)
    fare_pross(xtrain)
    fare_pross(xtest)
    print(xtrain)

    # 创建决策树
    print('start creating tree')
    start = time.time()

    tree = CreateTree(xtrain, ytrain, 0.2)
    draw_viz(tree)
    print(tree)
    # dot.render(filename='MyPicture', directory="./", view=True)
    print(tree)
    end = time.time()
    print('end creating tree')
    print('create tree time: ', end - start)
    # print(tree)

    # 测试模型

    print('start testing')
    start = time.time()

    accurate = Classifier(xtest, ytest, tree)
    print('Accurate:', accurate)

    end = time.time()
    print('end testing')
    print('test time: ', end - start)

    #利用决策树预测


    test_data_list = gen_test_dict(lables_list)
    forecast_data(test_data_list, tree)
    # print(test_data_list)

5. 正确率分析
最初由于数据集的不稳定性,以及统计正确率算法存在缺陷,导致正确率仅在50%浮动。对正确率算法改进后正确率提升至65%,而在做了额外的年龄处理和体热特征的离散阶梯化处理之后,正确率提高到了最高82%,且基本可以稳定到75%以上。而预测的结果保存在TEST的xls文件中。
在这里插入图片描述

6. 可视化结果分析
由于可视化结果图过大,因此以附件的形式保存决策树的结构图。此处为文字描述分析:
根据决策树可以发现,年龄和体热特征是对决策树影响最大的特征。在得知年龄之后我们可以基本的做出一个预判。如果无法仅从年龄得出结论的话,则需要再了解体热特征。在知道了这两项结论之后就基本可以得到该乘客的生存情况。再向下排,就是性别因素影响因子最大。
从常识推断来看,年龄较小或者较大的都缺乏自保能力,而女性乘客由于体力等因素也在事故中不易存活。对应老弱病残还是很有道理的。而体热特征则表示的是在水中浸泡较久的乘客体热都会比较低,而低体温则很容易在沉船遇难的时候死亡无法幸存。由数据来看,体热与生存率是正相关关系,符合逻辑。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值