最近學習機器學習,發現網上各種理論講的不少,但是對于實踐涉及很有不足。因為将自己的實踐記錄整理于此,希望對大家有所幫助。
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])
是否幸存通過0,1記錄,因為sum即為生存的人數,len即為總人數,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: [email protected]
@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: [email protected]
@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,還有其他問題歡迎大家留言讨論。