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