泰坦尼克问题是一个比较经典的案例,此次实验的目的在于用决策树进行乘客的生存预测,数据集中的具体字段为:
数据 | 含义 |
---|---|
PassengerId | 乘客编号 |
Survived | 是否幸存 |
Pclass | 船票等级 |
Name | 乘客姓名 |
Sex | 乘客性别 |
SibSp | 亲戚数量(兄妹、配偶数) |
Parch | 亲戚数量(父母、子女数) |
Ticket | 船票号码 |
Fare | 船票价格 |
Cabin | 船舱 |
Embarked | 登录港口 |
直接使用老师给的数据集,也不需要进行数据分割,直接就是891条训练集和418条测试集,直接加载进来(想要清洗过后的数据的话可以留下言)
import pandas as pd
# 数据加载
train_data = pd.read_csv('./titanic/train.csv')
test_data = pd.read_csv('./titanic/test.csv')
gender_data = pd.read_csv('./titanic/gender_submission.csv')
输出一下数据的基本信息
# 数据探索
print(train_data.info())
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 891 non-null int64
1 Survived 891 non-null int64
2 Pclass 891 non-null int64
3 Name 891 non-null object
4 Sex 891 non-null object
5 Age 714 non-null float64
6 SibSp 891 non-null int64
7 Parch 891 non-null int64
8 Ticket 891 non-null object
9 Fare 891 non-null float64
10 Cabin 204 non-null object
11 Embarked 889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
None
可以看到它展示了:行数、数据完整度、数据类型、字段名、数据类型数、内存使用情况
可以看到数据中Age、Fare、Cabin、Embarked存在缺失,Age数值型,代表年龄。我们可以用平均值补齐,Fare是船票价格,同理。
# 数据清洗
train_data['Age'].fillna(train_data['Age'].mean(), inplace=True)
test_data['Age'].fillna(test_data['Age'].mean(), inplace=True)
train_data['Fare'].fillna(train_data['Fare'].mean, inplace=True)
test_data['Fare'].fillna(test_data['Fare'].mean, inplace=True)
Cabin为船舱信息,由于训练集和测试集中有大量的缺失数据,盲目补齐会影响决策,所以不进行补齐。
Embarked是港口信息,我们可以使用value_counts()方法进行观察:
print(train_data['Embarked'].value_counts())
S 644
C 168
Q 77
Name: Embarked, dtype: int64
S的占比超过了70%,所以我们可以用s值进行补齐
train_data['Embarked'].fillna('S', inplace=True)
test_data['Embarked'].fillna('S', inplace=True)
通过数据探索,我们可以发现PassengerId为乘客的编号,对分类没有作用,可以舍弃,Name为乘客姓名,也可以舍弃,Cabin缺失值太多,也要舍弃,Ticket为船票号码,杂乱无章,可以舍弃,剩余的字段包括:Pclass、Sex、Age、SibSp、Parch 和 Embarked,这些属性分别表示了乘客的船票等级、性别、年龄、亲戚数量以及船仓
# 特征选择
features=['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Embarked']
train_features = train_data[features]
train_labels = train_data['Survived']
test_features = test_data[features]
test_labels = gender_data.values.tolist()
特征值中,有一些是字符串类型的值,不利于后续的运算,需要转成数字的类型,我们可以用klearn 特征选择中的 DictVectorizer 类进行上述操作:
from sklearn.feature_extraction import DictVectorizer
dvec = DictVectorizer(sparse=False)
# train_features = dvec.fit(train_features.to_dict(orient='records'))
train_features = dvec.fit_transform(train_features.to_dict(orient='records'))
test_features = dvec.transform(test_features.to_dict(orient='records'))
names=dvec.feature_names_
print(names)
['Age', 'Embarked=C', 'Embarked=Q', 'Embarked=S', 'Parch', 'Pclass', 'Sex=female', 'Sex=male', 'SibSp']
至此我们就得到了一个9个特征值,891个样本的9列的特征矩阵。
使用ID3算法构建决策树,即创建DecisionTreeClassifier的时候,设置criterion=‘entropy’
from sklearn.tree import DecisionTreeClassifier
# 构造ID3决策树
clf = DecisionTreeClassifier(criterion='entropy')
# 决策树训练
clf.fit(train_features, train_labels)
DecisionTreeClassifier(criterion='entropy')
可以用Grapviz进行决策树的可视化,图片可以在本notebook的同级目录找到
# 决策树可视化
import pydot
from sklearn import tree
tree.export_graphviz(clf, out_file="./tree.dot",feature_names=names)
(graph,) = pydot.graph_from_dot_file('tree.dot')
graph.write_png('tree.png')
from PIL import Image
display(Image.open("./tree.png"))
首先得到测试集的特征矩阵,然后使用训练好的clf树进行预测。
import numpy as np
# 决策树预测
pred_labels = clf.predict(test_features)
# 得到决策树准确率
cnt = 0
for i in range(len(pred_labels)):
if pred_labels[i] == test_labels[i][1]:
cnt += 1
print("准确率为%.2f"%(cnt/len(pred_labels)))
准确率为0.76
之前布置的作业中有写过ID3算法,此处直接使用
from math import log
features1 = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Embarked', 'Survived']
features2 = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Embarked']
train_features1 = np.array(train_data[features1]).tolist()
test_features1 = np.array(test_data[features2]).tolist()
# 计算标准熵
def calcShannonEnt(dataset):
# 返回数据集行数
num_data = len(dataset)
shannonEnt = 0.0 # 经验熵
# print(num_data)
label_count = {} # 保存每个标签(label)出现次数的字典
# 对每组特征向量进行统计
for i in dataset:
# 当使用负数索引时,python将从右开始往左数,因此 -1 是最后一个元素的位置
label = i[-1] # 提取标签信息
if label not in label_count.keys(): # 如果标签没有放入统计次数的字典,添加进去
label_count[label] = 0
label_count[label] += 1 # label计数
# print(label)
# print(label_count) # 输出每个标签(label)的出现次数
# 计算经验熵
for key in label_count.keys():
Prob = float(label_count[key]) / num_data # 选择该标签的概率
shannonEnt -= Prob * log(Prob, 2) # 利用公式计算
return shannonEnt # 返回经验熵
# 划分子集,来求条件熵
def splitDataSet(dataset, axis, value):
# 创建返回的数据集列表
retDataSet = []
# 遍历数据集
for featVec in dataset:
if featVec[axis] == value:
# 去掉axis特征
reducedFeatVec = featVec[:axis]
# print("reducedFeatVec",reducedFeatVec)
# 将符合条件的添加到返回的数据集
reducedFeatVec.extend(featVec[axis + 1:])
# print("reducedFeatVec=",reducedFeatVec)
retDataSet.append(reducedFeatVec)
# print("retDataSet=",retDataSet)
# 返回划分后的数据集
return retDataSet
# 计算信息增益最大特征
def chooseBestFeatureToSplit(dataset, label):
num_data = float(len(dataset)) # 数据集行数
num_label = len(dataset[0]) - 1 # 特征数量
# 计数数据集的香农熵,即什么都没有做时根据已知数据集计算出来的熵
shannonEnt = calcShannonEnt(dataset)
best_information_value = 0.0 # 将最佳信息增益初始化为0
best_label_axis = -1 # 最优特征的索引值
# 遍历所有特征
for i in range(num_label):
# 获取dataSet的第i个所有特征
label_list = [example[i] for example in dataset]
# 创建set集合{},元素不可重复
label_set = set(label_list)
# print(f'label_list = {label_list}')
# print(f'label_set = {label_set}')
condition_Ent = 0.0 # #经验条件熵,初始化条件熵为0
# 计算信息增益
for label in label_set:
# set_after_split划分后的子集
set_after_split = splitDataSet(dataset, i, label)
# 计算子集的概率
Prob = len(set_after_split) / num_data
# 根据公式计算经验条件熵
condition_Ent += Prob * calcShannonEnt(set_after_split)
# 计算信息增益的公式
imformation_value = shannonEnt - condition_Ent
# 打印出每个特征的信息增益
# print("第%d个特征%s的增益为%.3f" % (i, label[i], imformation_value))
# print("第%d个特征的增益为%.3f" % (i,information_value))
if imformation_value > best_information_value: # 比较出最佳信息增益
# 更新信息增益,找到最大的信息增益
best_information_value = imformation_value
# 记录信息增益最大的特征的索引值
best_label_axis = i
# 返回信息增益最大特征的索引值
return best_label_axis
# ID3算法核心:以每个结点上的信息增益为选择的标准来递归的构建决策树
def createTree(dataSet, label):
# 取分类标签
class_list = [example[-1] for example in dataSet]
# 如果类别完全相同,则停止继续划分
if class_list.count(class_list[0]) == len(class_list): # count() 方法用于统计某个元素在列表中出现的次数。
return class_list[0]
bestFeat = chooseBestFeatureToSplit(dataSet, label) # 选择最优特征
bestFeatLabel = label[bestFeat] # 最优特征的标签
# print(bestFeat,bestFeatLabel)
# 根据最优特征的标签生成树
mytree = {bestFeatLabel: {}}
# 删除已经使用的特征标签
del (label[bestFeat])
# 得到训练集中所有最优特征的属性值
clasify_label_value = [example[bestFeat] for example in dataSet]
# set 可以去掉重复的属性值
set_clasify_label_value = set(clasify_label_value)
# 遍历特征,创建决策树
for value in set_clasify_label_value:
new_label = label[:] # 子集合
# 构建数据的子集合,并进行递归
mytree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), new_label)
return mytree
# 测试效果函数
import random
def classify(inputTree, testVec, labels):
# 获取决策树节点
firstStr = next(iter(inputTree)) # iter() 函数用来生成迭代器。iter(object[, sentinel])object -- 支持迭代的集合对象。
# 下一个字典
if firstStr == 'Survived':
return random.randrange(0, 1, 1) # 对于特殊情况随机给出判断
secondDict = inputTree[firstStr]
featIndex = labels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], testVec,labels)
else:
classLabel = secondDict[key]
else:
return random.randrange(0, 1, 1)# 对于特殊情况随机给出判断
return classLabel
clf1 = createTree(train_features1, features1)
i = 0
cnt1 = 0
# 得到决策树准确率
for test in test_features1:
if test_labels[i][1] == classify(clf1,test,features2):
cnt1 += 1
i+=1
print("准确率为%.2f"%(cnt/len(test_features1)))
准确率为0.76