最近学习机器学习,发现网上各种理论讲的不少,但是对于实践涉及很有不足。因为将自己的实践记录整理于此,希望对大家有所帮助。
Titanic乘客生存预测模型
样例说明:题目来自于知名机器学习竞赛网站kaggle:
https://www.kaggle.com/c/titanic/data
“在数据分析中首先决定哪些变量需要处理,哪些变量可以删除,因此数据探索实在是个累人的活。”
代码多有优化之处,请大家留言讨论。
一、数据收集
数据收集无需要多讲,直接从这里下载:
https://www.kaggle.com/c/titanic/data
数据集的初步了解,kaggle中对数据集有明确的说明,为方便大家,在此作个简要的中文说明。想要学习机器学习的朋友,最好还是尝试阅读英文的资料。
此数据集是描述在泰坦尼克号中乘客的信息以及乘客最终是否幸存下来。
变量及含义:
survival,是否幸存,0为否,1为是
pclass ,票的等级,1代表一等仓,2代表二等仓,3代表三等仓
sex , 性别,male:男性,female:女性
Age,年龄
sibsp,兄弟姐妹或配偶同行的数量
parch,父母或子女同行的数量
ticket,票号
fare,票价
cabin,客仓号
embarked,登船口
数据,请到如下链接下载或者到kaggle下载。
二、数据分析和数据准备
首先了解各字段的含义。此样例的目标是通过乘客数据建立预测乘客是否生存。初步估计数据变量对是否生存的影响。并根据数据探索的办法证实所提供的数据是否与乘客幸存之前存在关系。
使用pandas导入训练数据,然后进行初步探索。
我使用jupyter notebook进行数据探索。
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from Dora import Dora
import math
dataSet = pd.read_csv('train.csv')
dataSet.head()
通过head(100),查看数据大概。(结果不在此呈现,有兴趣的同学请自行运行。或下载附件
然后通过数据透视表初步分析自变量与结果是否相关。
1、Pclass:
pd.pivot_table(dataSet, index = 'Pclass',values = 'Survived', aggfunc=[np.sum,len,np.mean])
通过结果可以看到,票位等级越高,生存概率越大,看来以后还是多享受一等仓,说不定就捡了一条小命。
根据常识,名字大概不会影响是否生存。(或许有影响也说不定,哈哈,我就当作没影响,自己可以玩玩)
2、然后是性别:
pd.pivot_table(dataSet, index = 'Pclass',values = 'Survived', aggfunc=[np.sum,len,np.mean])
结果很出人意料,女性竟然比男生生存的概率要大的多。看来船上的人更挺绅士的。
3、年龄,猜测可能老幼生存概率可能要低,当然也可能和性别一样,老幼幸存概率反而更高。
年龄这一变量比较特殊,首先上面探索的时候发现年龄有不少缺失值。大部分数据是整数,小部分数据还存在
小数,有小数的年龄都小于1,数量不多仅有7条记录,姑且认为是小于1周岁的乘客。首先探索年龄变量的影响
如果有影响,则需要处理确失值,如果没影响,就不需要麻烦了。
年龄不再能够直接使用数据透视功能,因为年龄太多了,先将年龄进行分段。我这里先把年龄按每10岁一个阶段
进行分组,因此需要建立新的变量属性。代码如下:
#先建立Age2变量,并且初始化为0.当原值为NaN时,自然转化为0.
dataSet['Age2']=0
s = dataSet['Age']
for i in range(len(dataSet)):
#NaN不能作为数值计算,因此仅当>0时,再计算分段
#使用int进行取整,然后+1,这样可与NaN值区分开
if s[i]>0:
dataSet.ix[i, 'Age2']=int(s[i]/10)+1
然后再通过数据透视,并将其通过plot方法进行可视化:
s_age = pd.pivot_table(dataSet, index = 'Age2',values = 'Survived', aggfunc=[np.sum,len,np.mean]).
plt.show(s_age['mean'].plot())
通过图片发现Age2=1时,生存概率明显较高,可能因为小于10岁的孩子都是有父母照顾的,而且在2至6时
年龄对是否生存基本没有影响,查看s_age可以发现,Age2等于8或9的数据样本过少。那是不是因为10岁的
分段间距过大了呢?于是将分段间距变为5,再看同样没有大的影响,而且在60岁至70岁之间的统计结果也
没有影响了。
4、兄弟姐妹或配偶同行的数量。简单数据透视即可:
发现明显影响生存概率。
观察数据,在数量到5以上时,样本数量较少。在数量为2时生存概率最大,人数再变多,生存概率反而下降。
看来人多不一定力量大。具体影响还需要模型去分析。
5、父母子女同行的数量。简单数据透视:
有父母子女同行时,生存概率明显升高。当同行数量超过2后,样本数量明显减少,因此需要合并入2.
经验判断,父母会全力帮助子女逃生,那是否存在父母把生存机会让给了子女,造成子女生存概率上升而父母
的机会减少呢?但是数据集中没有这一信息。再多想一点,年龄是可以反映这一信息。因此应该将年龄数据
和这一数据结合再进一步分析。
s_AgeandParch = pd.pivot_table(dataSet, index = ['Age2', 'Parch'],values = 'Survived', aggfunc=[np.sum,len,np.mean])
s_AgeandParch
结果发现,年龄缺失时,父母子女同行时,概率明显升高。
然后无论年龄处于哪个年龄段,父母子女同行时,生存概率明显升高。并没有出现年龄高时(多意味着是
父母)有子女同行生存概率反而下降的现象。
6、票号,其本身大概不会影响结果(除非相信幸运数字)。但是观察发现票号本身的形式并不是一致的,
例如5位或6位纯数字,以PC开头,以W.E.P开头,等等。这些不同的开头可能代表着某种意义也说不定。
这里涉及探索工作量很大,暂时不深入分析。继续往下看。
7,票价。先看下票价的分布,发现其票价并不统一。而票价显然会和Pclass相关。那么在Pclass相同时,
票价会有影响吗?如果无关,则没有必要考虑票价的影响。
从票价分布来看,低票价数量很多,因此对票价取对数然后再分段:
大概可以发现,同一Pclass时,票价还是有影响的。但是上面对票价的分段不太科学。因此进一步优化票价的分段
,通过descrebe()取得票价的4分位值,然后重新构建Fare2。
s = dataSet['Fare']
for i in range(len(dataSet)):
if s[i]>0:
dataSet.ix[i, 'Fare2']=int(math.log(s[i]))
s_Fare = pd.pivot_table(dataSet, index = ['Pclass','Fare2'],values = 'Survived', aggfunc=[np.sum,len,np.mean])
8,客仓号缺失值过多,不用于分析。
9,登船口,可能和乘客在船上的位置有关,有可能会影响结果。
s_embarked = pd.pivot_table(dataSet, index = ['Embarked'],values = 'Survived', aggfunc=[np.sum,len,np.mean])s_embarked
通过以上分析,可以得出哪些变量对结果有影响,哪些则没有影响。
并且对数据进行了初的变换。下面则删除掉没用的数据,留下用于训练的数据。
此数据中含有多种类型的数据,让我们先用决策树试一下,预测结果怎样。
下面开始使用pycharm编写程序。
数据读入与清洗:
#!/usr/bin/python
# encoding:utf-8
"""
@author: Bruce
@contact: isunrise@foxmail.com
@file: main.py
@time: 2017/4/4 $ {TIME}
"""
import pandas as pd
import numpy as np
dataSet = pd.read_csv('train.csv')
del dataSet['PassengerId']
s = dataSet['Fare']
for i in range(len(dataSet)):
if s[i]<7.910400:
dataSet.ix[i, 'Fare2']=0
elif s[i]<14.454200:
dataSet.ix[i, 'Fare2']=1
elif s[i]<31.000000:
dataSet.ix[i, 'Fare2']=2
else:
dataSet.ix[i, 'Fare2']=3
if dataSet.ix[i, 'Parch']>2:
dataSet.ix[i, 'Parch'] = 2
if dataSet.ix[i, 'SibSp']>3:
dataSet.ix[i, 'SibSp'] = 3
#删除无用变量
del dataSet['Age']
del dataSet['Fare']
del dataSet['Cabin']
del dataSet['Name']
del dataSet['Ticket']
#将Embarked变量中的nan直译为众数‘S”
dataSet = dataSet.fillna({'Embarked':'S'})
三、数据训练
数据训练建立两个文件,如下代码为训练核心文件。实现DataFrame类型的决策树算法。
#!/usr/bin/python
# encoding:utf-8
"""
@author: Bruce
@contact: isunrise@foxmail.com
@file: dtrain.py
@time: 2017/4/3 $ {TIME}
此决策树使用pandas中的dataFrame数据实现,要求dataFrame中指定一列为y标签。调用createtree方法训练。createtree有2个参数
第一个参数中数据DataFrame,第二个参数指定dataFrame中y标签的列名为str类型。
"""
from math import log
import pandas as pd
import operator
import numpy as np
import matplotlib.pyplot as plt
#计算信息熵
def calcshannonent(dataset, re):
numentries = len(dataset)
#计算数据集每一分类数量
l = dataset.columns.tolist()
k = l.index('Survived')-1
s_k = pd.pivot_table(dataset, index=re, values=l[k], aggfunc=len)
#classlabel = set(dataset[re])
shannonent = 0.0
#每一分类的信息熵
for i in list(s_k):
prob = i / float(numentries)
shannonent += prob * log(prob, 2)
return shannonent
#对给定的数据集的指定特征的分类值进行分类
def splitdataset(dataset, axis, value):
retdataset = dataset[dataset[axis] == value]
del retdataset[axis]
return retdataset
#选择最佳分裂特征:
def chooseBestFeatureToSplit(dataset,re):
#分裂前的信息熵
baseEntroy = calcshannonent(dataset,re)
#信息增益及最佳分裂特征初始:
bestinfogain = 0.0
bestfeature = dataset.columns[1]
#对每一特征进行循环
for f in dataset.columns:
if f == 'Survived': continue
#获取当前特征的列表
featlist = dataset[f]
#确定有多少分裂值
uniqueVals = set(featlist)
#初始化当前特征信息熵为0
newEntrypoy = 0.0
#对每一分裂值计算信息熵
for value in uniqueVals:
#分裂后的数据集
subdataset = splitdataset(dataset, f ,value)
#计算分支的概率
prob = len(dataset[dataset[f]==value])/float(len(dataset))
#分裂后信息熵
newEntrypoy += prob*calcshannonent(subdataset, re)
# 计算信息增益
infogain = newEntrypoy - baseEntroy
#如果信息增益最大,则替换原信息增益,并记录最佳分裂特征
if f != 'Survived':
if (infogain > bestinfogain):
bestinfogain = infogain
bestfeature = f
#返回最佳分裂特征号
return bestfeature
#确定分枝的主分类
def majority(labellist):
classcount = {}
#分类列表中的各分类进行投票
for vote in labellist:
if vote not in classcount.keys():
classcount[vote] =0
classcount[vote] += 1
#排序后选择最多票数的分类
sortedclasscount = sorted(classcount.iteritems(), key=operator.itemgetter(1), reverse=True)
#返回最多票数的分类的标签
return sortedclasscount[0][0]
def createtree(dataset, result):
'有2个参数,第一个是dataFrame类型的数据,第个是字符串类型的y标签'
#如果数据集的分类只有一个,则返回此分类,无需子树生长
classlist = list(dataset[result].values)
if classlist.count(classlist[0]) == len(classlist):
return classlist[0]
#如果数据集仅有1列变量,加y标签有2列,则返回此数据集的分类
if len(dataset.columns) == 2:
return majority(classlist)
bestfeat = chooseBestFeatureToSplit(dataset,result)
mytree = {bestfeat: {}}
#此节点分裂为哪些分枝
uniquevals = set(dataset[bestfeat])
#对每一分枝,递归创建子树
for value in uniquevals:
mytree[bestfeat][value] = createtree(splitdataset(dataset, bestfeat,value),result)
#完成后,返回决策树
return mytree
四、模型测试
对测试数据采用同样的预处理:
dataTest = pd.read_csv('test.csv')
result = pd.read_csv('gender_submission.csv')
del dataTest['PassengerId']
s = dataTest['Fare']
for i in range(len(dataTest)):
if s[i]<7.910400:
dataTest.ix[i, 'Fare2']=0
elif s[i]<14.454200:
dataTest.ix[i, 'Fare2']=1
elif s[i]<31.000000:
dataTest.ix[i, 'Fare2']=2
else:
dataTest.ix[i, 'Fare2']=3
if dataTest.ix[i, 'Parch']>2:
dataTest.ix[i, 'Parch'] = 2
if dataTest.ix[i, 'SibSp']>3:
dataTest.ix[i, 'Parch'] = 3
#删除无用变量
del dataTest['Age']
del dataTest['Fare']
del dataTest['Cabin']
del dataTest['Name']
del dataTest['Ticket']
#将Embarked变量中的nan直译为众数‘S”
dataTest = dataTest.fillna({'Embarked':'S'})
RCount = 0
asum = 0
for i in range(len(dataTest)):
a_test = dataTest.ix[i]
r_p = dtrain.classify2(mytree, a_test)
if r_p != dataTest.ix[i, 'Survived']:
RCount += 1
asum+=1
errorT = RCount/float(891)
print errorT
然后测试:
RCount = 0
asum = 0
for i in range(len(dataTest)):
a_test = dataTest.ix[i]
r_p = dtrain.classify2(mytree, a_test)
if r_p != dataTest.ix[i, 'Survived']:
RCount += 1
asum+=1
errorT = RCount/float(891)
print errorT
最终模型在测试集上的错误率是0.073,但是模型在训练集上的误差则是15多,因此模型还有待细化。
五、结论及后续
这个实践例子中其实还有很多不足。
1,决策树没有进行剪枝。
2,决策树没有结点默认值。决策树每结点都应该有默认值。上述方法粗暴地把
所有默认值定成了0.
3,有一些细节没有更详细的处理。
4,还有其他问题欢迎大家留言讨论。