【python】Kaggle入門:titanic 的數據清洗與模型訓練

目     錄

 

0、概述

1、數據清洗

1.1 缺失值填充

1.1.1 Age填充

1.1.2 Embarked填充

1.1.3 Fare填充

1.2 異常值處理

1.3 特徵轉換

2、建模和優化

2.1、參數優化

2.2 訓練模型

2.3 交叉驗證

3、預測

4、總結


0、概述

在上一篇文章中,我們對訓練集的各個特徵已經有了初步瞭解,並提取出了九個可用的特徵:

Pclass、Age、SibSp+Parch、Fare、Sex、Name、Ticket、Cabin、Embarked。

本篇文章中,將繼續跟着該網址的第三部分和第四部分,對數據進行清洗,並且進行模型訓練。

1、數據清洗

數據清洗主要有兩部分工作:第一部分爲缺失值填充,第二部分爲異常值處理。

1.1 缺失值填充

在前一篇文章我們說過,訓練集和測試集的數據中,有部分數值爲NaN,這部分數值我們要進行填充:

主要是Age、Embarked、Fare,Survived是我們要預測的,不用填。

1.1.1 Age填充

Age缺失263個,因此要填充263個。缺失量較大。不能用平均數、中位數、衆數等直接填充,要利用算法進行預測。

這裏選擇採用隨機森林算法。有關隨機森林的知識,請參閱該網址。代碼如下:

from sklearn.ensemble import RandomForestRegressor
age_df = all_data[['Age', 'Pclass','Sex','Title']]
age_df=pd.get_dummies(age_df)
known_age = age_df[age_df.Age.notnull()].as_matrix()
unknown_age = age_df[age_df.Age.isnull()].as_matrix()
y = known_age[:, 0]
X = known_age[:, 1:]
rfr = RandomForestRegressor(random_state=0, n_estimators=100, n_jobs=-1)
rfr.fit(X, y)
predictedAges = rfr.predict(unknown_age[:, 1::])
all_data.loc[ (all_data.Age.isnull()), 'Age' ] = predictedAges 

首先要安裝sklearn這個庫,這個庫裏面集成了許多常用的機器學習算法,有了這個我們就可以當一個入門的調參工程師了。但是conda不能直接用conda install sklearn安裝這個庫,在源裏面找不到,要用如下這條命令:

conda install -c anaconda scikit-learn

纔可以。

放假回家這移動網下點外網的東西簡直欲仙欲死,恰逢國慶,飛機場又都被封,只能靠這辣雞網度日了。

隨機森林算法在這裏我們是作爲一個分類器使用的,既然是分類器,就有輸入輸出。輸入由我們自己選定,這裏選擇了三個可用的特徵:Sex、Pclass、Title。輸出爲Age。本質上就是用這三個輸入來預測輸出。

所以我們要先把所有數據中的這四個特徵拿出來,保存在age_df這個矩陣中:

嗯,1309個數據全部導入。每個數據都有四個特徵。

然後需要將上面這個矩陣向量化。如何向量化?就是將非數值的量轉爲數值量。比如說對於Sex,這一列是一個向量,每個向量元素有兩個取值male和female,那麼將其轉換爲數值量0和1,就變成兩列:Sex_male和Sex_female。對於每個Sex的值,若是male,則新的Sex_male列對應的值爲1,Sex_female的值爲0。對於Title也一樣,只不過要拓展成的列多一點。這樣,一個元素的可能取值爲n個值,就變成了n列向量,這n列向量的元素或者爲0,或者爲1。如下,是原矩陣經變換後得到的新矩陣:

這個新矩陣的最大優點爲所有元素均爲數字,可以直接輸入使用。

然後我們要將整個數據集分爲兩部分:第一部分的Age已知,第二部分的Age未知。即第一部分爲訓練集,第二部分爲要預測的數據。怎麼把這兩部分分開呢?用的就是第四行和第五行的代碼:

known_age = age_df[age_df.Age.notnull()].as_matrix()
unknown_age = age_df[age_df.Age.isnull()].as_matrix()

我們知道,Age不存在的元素值爲NaN,因此用notnull函數可以篩選出所有的Age已知的行,用isnull可以篩選出未知的行。注意無論是isnull還是notnull,返回的都是一個包含True和False對應的向量,然後用布爾下標的方法篩選出對應的行。這時返回的是一個DataFrame,我們要將這個DataFrame轉換成一個數組,因此要調用as_matrix函數。如下圖:

從DataFrame轉換成了ndarray。

known_age的內容如下所示:

有點亂啊,不過可以看出來第一個是Age,然後是Pclass、Sex什麼的。

這部分是訓練集。訓練集的輸入和輸出要分開。觀察訓練集known_age可知,訓練集的輸入爲第二列及之後,輸出應該爲第一列。因此我們設置X爲第二列及之後,y爲第一列。

接下來是重頭戲:隨機森林的調用。sklearn庫裏面有兩個隨機森林,一個用於分類(RandomForestClassifier),一個用於迴歸(RandomForestRegressor),我們當然要用迴歸的隨機森林了。

使用隨機森林需要調用RandomForestRegressor函數,該函數的參數參見該網址

我們調整了三個參數:

random_state=0:此參數讓結果容易復現。 一個確定的隨機值將會產生相同的結果,在參數和訓練數據不變的情況下。

n_estimators=100:在利用最大投票數或平均值來預測之前,你想要建立子樹的數量。 較多的子樹可以讓模型有更好的性能,但同時讓你的代碼變慢。 你應該選擇儘可能高的值,只要你的處理器能夠承受的住,因爲這使你的預測更好更穩定。

n_jobs=-1:這個參數告訴引擎有多少處理器是它可以使用。 “-1”意味着沒有限制,而“1”值意味着它只能使用一個處理器。 

也就是說,我們將使用處理器的所有核心,建立一百棵子樹的隨機森林。隨機值取值固定,這會讓每次的結果都確定。

然後這樣隨機森林的架子就搭好了,下一步是訓練模型。我們的架子用rfr來表示。訓練模型就直接rfr.fit(X,y)即可。

利用rfr.predict進行預測,輸入參數爲unknown_age的第二列到最後一列。返回值爲預測的年齡。然後填入年齡爲空的那些行即可。

1.1.2 Embarked填充

由上文我們可以知道,Embarked就缺倆,因此我們可以很容易的把他們倆填上,不用像Age一樣用複雜的算法。而是直接觀察缺少Embarked的乘客的特徵,在不缺失Embarked的乘客中去找與之最相似的乘客,把這些乘客的Embarked填入即可。

那麼我們就要看缺少Embarked的乘客是什麼樣的:

特徵還是很明顯的:Fare都是80,Pclass都是1,Sex都是female,還都是妹子,甚至都是一家的——Ticket都一樣。那我們來找一下Fare爲80,Pclass爲1的所有乘客的資料:

不行啊,滿足這倆條件的就她們倆,那先看看Pclass爲1的所有人,這就很多,323人。

現在我們想知道,Pclass爲1的人,Embarked爲不同值時,他們的Fare的中位數爲多少,我們選擇中位數離80最近的Embarked作爲缺失值。

也就是說,我要對所有乘客進行分組,首先按Pclass分組,然後按Embarked分組,最後算出中位數。

爲什麼選Pclass和Embarked呢?因爲看這些特徵,和船票價格Fare直接相關的,只有兩個:Pclass,你Pclass不同價格自然不一樣;Embarked不同則坐船距離不同,價格也不一樣;類似坐高鐵,高鐵票價不就和距離還有幾等座有關係嘛。

如何實現呢?

all_data.groupby(by=["Pclass","Embarked"]).Fare.median()

首先調用groupby函數,這個函數很有用,它不會改變all_data的索引,但是其中每行的相對位置會發生變化,如何變呢?首先,Pclass相同的會在一起,然後,Embarked相同的會在一起。先不看後面的效果,只看groupby的效果如下:

然而失敗了,它返回的類型不是DataFrame,因此不能直接輸出。

只好看整個函數的返回了:

所以,Pclass=1的乘客中,Fare爲76的離80最近,這些乘客的Embarked爲C,因此這倆缺失值就填爲C。

all_data['Embarked'] = all_data['Embarked'].fillna('C')

1.1.3 Fare填充

由上文可知,Fare只有一個缺失值,因此和Embarked類似,先看這個缺失值的特徵:

Pclass爲3,Embarked爲S,妥了,從上文看,滿足這倆條件的Fare中位數爲8.05元,填進去即可。

現在除了Survived以外都是1309個了。滿足。

1.2 異常值處理

在進行異常值處理之前,我們先來討論一個問題:什麼是異常值?

異常值就是不符合普遍規律的值。我們的代碼是遵循普遍規律的,對於不符合普遍規律的樣本不能夠很好的處理。

對於本題來說,異常值可以簡單地定義如下:

既然我們都知道婦女兒童容易獲救,成年男性不易獲救,那我看一下所有值,沒獲救的婦女兒童和獲救的成年男性不就是異常值了麼。

好像有點太武斷了,比如說獨自一人出行的夫人小姐,她們相對於一個大家庭中的夫人小姐肯定相對弱勢,獲救機率沒他們高。因此更強的異常定義應該是:在多人組成的家庭中,“婦女兒童沒獲救”和“成年男性獲救”的爲異常值。

先來看看這兩種是不是真的異常:

怎麼看是不是“多人組成的家庭”呢?根據我們上一篇對name的分析,這個aa一樣的人,應該就是一個家庭的。實際上這個aa的實際名字應該爲surname。我們找出所有乘客的surname,把相同的作爲一組來看:

all_data['Surname']=all_data['Name'].apply(lambda x:x.split(',')[0].strip())

效果如下:

surname這一列得到了所有乘客的aa值,那麼我想看某個乘客的家庭共有多少成員,然後對於某個乘客,就可以知道他屬於一個多大家庭的成員了。因此需要建新的一列FamilyGroup,用來存儲每個乘客的家庭有多少人:

Surname_Count = dict(all_data['Surname'].value_counts())
all_data['FamilyGroup'] = all_data['Surname'].apply(lambda x:Surname_Count[x])

效果如下:

來看看有多少個大家庭吧:

單身的有637個,二人家庭有266個,三人家庭有189個......那麼下一步就很簡單了:找出所有處於多人家庭的“婦女兒童”和“成年男性”。

Female_Child_Group=all_data.loc[(all_data['FamilyGroup']>=2) & ((all_data['Age']<=12) | (all_data['Sex']=='female'))]
Male_Adult_Group=all_data.loc[(all_data['FamilyGroup']>=2) & (all_data['Age']>12) & (all_data['Sex']=='male')]

兒童定義爲小於12歲,成年男性定義爲大於12歲(男性真的慘)。分別保存在兩個組中。

接下來就是家庭劃分了:按surname將兩個組的成員劃分爲不同的組,然後看各組中的生還率。先看婦女兒童把:

Female_Child=pd.DataFrame(Female_Child_Group.groupby('Surname')['Survived'].mean().value_counts())
Female_Child.columns=['GroupCount']
Female_Child

先來看第一行,新建一個DataFrame,每行的值爲“婦女兒童按surname分組,計算各組生還的平均值,查完得到平均值的計數”。有點難理解,舉例來說吧:對於每個value_counts,有一個對應的value,value值爲1,那麼生還平均值爲1,value_counts爲100,說明生還平均值爲1的有100個。生還平均值爲1代表什麼呢?代表該家庭的所有婦女兒童全部獲救。也就是說,全部獲救的家庭有100個。這太繞了:

看來全部獲救的家庭有115個,全滅的家庭有31個,其餘的都很少。

再看成年男性:

Male_Adult=pd.DataFrame(Male_Adult_Group.groupby('Surname')['Survived'].mean().value_counts())
Male_Adult.columns=['GroupCount']
Male_Adult

全部獲救的家庭有20個,全滅的有122個。。。差距太大了。

這樣我們就可以回答最開始的問題了:多人組成的家庭中,婦女兒童全滅或成年男性都獲救的家庭,佔比均很少,可以認爲是異常。我們稱前者的異常組成遇難組,後者的異常組成倖存組。來看看遇難組怎麼生成的吧:

Female_Child_Group=Female_Child_Group.groupby('Surname')['Survived'].mean()
Dead_List=set(Female_Child_Group[Female_Child_Group.apply(lambda x:x==0)].index)
print(Dead_List)

首先通過groupby生成一個數據結構,其序號爲Surname,值爲Survived的mean,這建立了一個家庭與遇難平均數的映射。然後建立一個遇難組,是一個set,其值爲剛剛生成的數據結構中值爲0的index。也就是說,遇難組存儲了所有“婦女兒童全部遇難”的家庭。對於倖存組也一樣:

下一步,我們要將測試集中的異常值修改爲正常值。

這裏我其實有點疑問:根據這個操作,kaggle的評分流程應該是這樣:我自己通過算法得到test的結果,將結果發給服務器以得到分數;而我對test進行任何操作都沒有關係。我之前則認爲是測試集不能動,能動的只有train,也就是訓練集。這裏或許需要問問老師。

(PS:後來我又自己仔細想了想,這個問題抽象出來是這樣:

對於一個分類問題,樣本均具有A、B、C、d、e五個特徵,分類依據A、B、C三個特徵。然後我發現由於某種原因,d無法加入到影響分類的特徵中;但是分類不依據的d特徵中,d=1時A的特徵的表現與d=0時A的特徵的表現相反,而且d=0佔大多數,那麼,對於測試集中的樣本,我不妨對d=1的所有樣本的A全部取反,從而有利於預測。因爲kaggle僅看最後的csv結果,因此我對test進行什麼處理都是可以的)

注意,這裏的遇難組和倖存組都是根據train來的,不要看是根據all_data來的就認爲該組來自train和test,因爲test沒有survived,它怎麼能知道是否倖存呢?我們這一步實際上是根據train對test做了一個修正,使其的結果更準確。

改爲正常值的代碼如下:

train=all_data.loc[all_data['Survived'].notnull()]
test=all_data.loc[all_data['Survived'].isnull()]
test.loc[(test['Surname'].apply(lambda x:x in Dead_List)),'Sex'] = 'male'
test.loc[(test['Surname'].apply(lambda x:x in Dead_List)),'Age'] = 60
test.loc[(test['Surname'].apply(lambda x:x in Dead_List)),'Title'] = 'Mr'
test.loc[(test['Surname'].apply(lambda x:x in Survived_List)),'Sex'] = 'female'
test.loc[(test['Surname'].apply(lambda x:x in Survived_List)),'Age'] = 5
test.loc[(test['Surname'].apply(lambda x:x in Survived_List)),'Title'] = 'Miss'

我們將測試集中的所有幸存組的所有成員全改爲了女性,將遇難組中的所有成員改爲了男性。

1.3 特徵轉換

在該部分,我們要劃分測試集和訓練集,選取特徵,以及在測試集上劃分輸入和輸出的驗證。

代碼如下:

all_data=all_data[['Survived','Pclass','Sex','Age','Fare','Embarked','Title','FamilyMem','RoomNum','TicketGroup']]
all_data=pd.get_dummies(all_data)
train=all_data[all_data['Survived'].notnull()]
test=all_data[all_data['Survived'].isnull()].drop('Survived',axis=1)
X = train.as_matrix()[:,1:]
y = train.as_matrix()[:,0]

唯一要注意的就是要用dummies將矩陣數量化。

2、建模和優化

我們選擇使用隨機森林模型對題目進行求解。

2.1、參數優化

從上文可以知道,隨機森林的參數較少,我們僅對兩個參數進行優化:n_estimators和max_depth,前者爲森林中樹的數量,後者爲樹的深度。對於參數優化,我們有一個很方便的函數可以使用:GridSearchCV,該函數的使用參見該網址。代碼如下:

from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.feature_selection import SelectKBest

pipe=Pipeline([('select',SelectKBest(k=20)), 
               ('classify', RandomForestClassifier(random_state = 10, max_features = 'sqrt'))])

param_test = {'classify__n_estimators':list(range(20,50,2)), 
              'classify__max_depth':list(range(3,60,3))}
gsearch = GridSearchCV(estimator = pipe, param_grid = param_test, scoring='roc_auc', cv=10)
gsearch.fit(X,y)
print(gsearch.best_params_, gsearch.best_score_)

首先新建一個pipeline,這個pipeline可以看做是一個流水線,整個機器學習的流程就由這個pipeline規定:第一步做什麼,第二步做什麼。。。。。。最後一步確定整個問題的性質——比如說本題就是classify,分類問題。第一步是選擇特徵,這裏是選擇最好的20個特徵,函數的解釋見該網址,我們使用的是默認函數,設置k=20。默認函數只適用於分類問題——我們剛好是分類。第二步是確定功能爲classify,後面調用了隨機森林方法,參數random_state=10表示控制隨機參數爲10,這樣我們最後得到的參數纔會和參考網址的結果一樣,否則無法得到相同的結果,具體解釋參見該網址;參數max_features='sqrt'表示我們使用RF劃分時考慮的最大特徵數爲\sqrt{n}

注意到我們使用RF時,要考慮兩類參數,第一類是森林的框架參數:有多少棵樹啊;第二類是每棵樹的參數:隨機值啊,最大特徵數啊,樹高啊,上面只考慮了部分參數:隨機值和最大特徵數,這些直接確定了,沒有經過調參,下面要輸入需要調參的參數。我們需要指定一些值,然後網格搜索就會分別遍歷這些值,比如說這裏我們對n_estimators和Max_depth分別指定了一些值,那麼網格搜索則會對這些值全部配對,最後找出表現最好的配對。

之後就是用GridSearch來遍歷了,GridSearch參數參見該網址

輸出效果如圖,跑了小一分鐘:

正是由於我們確定了隨機過程,才能得到和原作者一樣的結果。兩個超參數這就選擇好了。

2.2 訓練模型

既然我們已經確定好參數的值,就可以吧這些值帶入隨機森林中進行預測了:

from sklearn.pipeline import make_pipeline
select = SelectKBest(k = 20)
clf = RandomForestClassifier(random_state = 10, warm_start = True, 
                                  n_estimators = 26,
                                  max_depth = 6, 
                                  max_features = 'sqrt')
pipeline = make_pipeline(select, clf)
pipeline.fit(X, y)

首先仍然是新建一個pipeline,這裏使用了另外一種建立pipeline的方法,因爲函數參數太多了,直接扔進去太長了不好看。

第一個參數是特徵選擇,仍然是20個,第二個參數爲隨機森林函數,random_state不用去管了;warm_start爲熱啓動參數,默認爲False,如果爲True,則下一次訓練是以追加樹的形式進行(重新使用上一次的調用作爲初始化);下面三個參數則是我們在上面搜索得到的。之後開始訓練即可。

PS:看上去第一步的參數優化的都是超參數,第二步纔會對RF本身的參數進行優化啊。

效果如下:

驚了,算的很快。但是看上去也沒訓練什麼參數啊。不太清楚調用fit是幹嘛用了。查閱文獻後發現fit的確會確定參數。

2.3 交叉驗證

代碼如下:

from sklearn import cross_validation, metrics
cv_score = cross_validation.cross_val_score(pipeline, X, y, cv= 10)
print("CV Score : Mean - %.7g | Std - %.7g " % (np.mean(cv_score), np.std(cv_score)))

看上去很簡單,只需要調用一個cross_validation.cross_val_score函數即可,它可以直接用train作爲驗證集,不用我們自己去花紋,用cv這個參數來控制即可。

注意新版的sklearn的cross_validation被廢棄,改用model_selection即可,代碼如下:

from sklearn import model_selection, metrics
cv_score = model_selection.cross_val_score(pipeline, X, y, cv= 10)
print("CV Score : Mean - %.7g | Std - %.7g " % (np.mean(cv_score), np.std(cv_score)))

效果如下:

3、預測

既然我們的模型已經建好了,那下一步就可以開始對test進行預測咯:

predictions = pipeline.predict(test)
submission = pd.DataFrame({"PassengerId": PassengerId, "Survived": predictions.astype(np.int32)})
submission.to_csv(r"h:\kaggle\submission1.csv", index=False)

預測出來就是一個csv文件,就不截圖了。

4、總結

由於我還沒有系統的學習這些算法的具體使用,因此相對於上一篇文章,本篇文章略顯倉促,由於個人能力有限,沒能對RF、GridSearch、CV等函數的原理作充分說明,僅是簡略的說明了一下它們的參數的含義。這還需要在以後的生活中多加練習。

在數據清洗環節,我學習了對於缺失數據如何進行填充,若缺失較多,可以利用RF進行迴歸分析;若缺失較少,則利用特徵類似的樣本進行填充即可;還學習了對於異常點的處理:對異常點可以進行一定的修正使得預測結果更爲理想。

在建模和優化環節,我學習了完整的一套建模流程:選擇模型,建pipeline,給超參數賦值,利用GridSearch選擇最優超參數,利用fit訓練模型,利用cv進行交叉驗證,最後利用Predict進行預測。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章