#!/usr/bin/env python # coding: utf-8 # # 鐵達尼號乘客生還率預測 # ### 匯入套件與資料 # In[1]: # 匯入資料處理套件 import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 匯入機器學習套件 from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score # 匯入訓練資料與測試資料 df_train = pd.read_csv('/Users/walkerchen/Documents/Kaggle/titanic/train.csv') df_test = pd.read_csv('/Users/walkerchen/Documents/Kaggle/titanic/test.csv') # 合併訓練資料與測試資料、重設索引(否則會重複),方便待會探索性分析與特徵工程 df_data = df_train.append(df_test, sort=False) df_data = df_data.reset_index(drop=True) # ### 定義訓練與預測函數 # 因訓練資料沒有很多,使用 Logistic Regression 怕很難迭代出適當權重來畫出分界線,故使用抗噪能力與分類能力都有不錯成績的 Random Forest 來作為分類演算法。 # 在進行訓練跟預測前,會將特徵分兩類傳入:一為連續型類別資料,可直接傳入;另一為非連續型類別資料,須經過 One-Hot Encoding 處理。 # In[2]: rf = RandomForestClassifier(n_estimators=400, criterion='gini', max_depth=None, min_samples_split=2, # default = 2 min_samples_leaf=5, # default = 1 max_features='auto', max_leaf_nodes=None, n_jobs=None, random_state=None, oob_score=True) def fitModel(data, cv, ohe): train_set = data[:len(df_train)] test_set = data[len(df_train):] X = train_set Y = train_set['Survived'] rf.fit(X[cv].join(pd.get_dummies(train_set[ohe], prefix=ohe)), Y) print('Base oob score : %.5f' %(rf.oob_score_)) def submitResult(data, cv, ohe): test_set = data[len(df_train):] X = test_set[cv].join(pd.get_dummies(test_set[ohe], prefix=ohe)) Y = rf.predict(X) result = pd.DataFrame({'PassengerId': test_set['PassengerId'], 'Survived': Y.astype(int)}) result.to_csv('titanic_result.csv', index=False) # ### 查看資料 # 透過 head( ) 可查看前五筆資料,大致了解每筆資料有哪些欄位及資料型態 # In[3]: df_data.head() # 透過 info( ) 可了解整個資料集的大致情形:可發現 Age、Fare、Cabin 及 Embarked 欄位皆有缺值,須進行補值處理;Survived 只有 891 筆資料是因為只有訓練資料才有提供結果,測試資料須用訓練後的模型進行預設 # In[4]: df_data.info() # ## 探索性分析 # 為了決定對哪些特徵進行預處理,最後輸入模型進行訓練,我們可以先進行探索性分析,了解各欄位特性。接下來觀察各欄位特徵: # ### Pclass (艙等) # 透過 groupby( ) 可以知道整艘船有 3 種艙等,取各艙等的平均票價可發現票價差異之大(可以理解成頭等艙、商務艙、經濟艙的概念) # In[5]: df_data.groupby('Pclass').mean().round(2)['Fare'] # 將艙等與乘客生還率畫成圖比對,可以發現艙等等級愈高,生還率愈高,可以理解為 VIP 乘客有較高的優先權獲救,或是高等艙有較好的逃生設備或離逃生口較近;無論如何都挺現實跟合理的,畢竟人家付的錢比較多 # In[6]: pd.crosstab(df_data['Pclass'],df_data['Survived']).plot(kind="bar") # ### Sex (性別) # 性別的部分,由圖可知女性生還率遠高於男性,這部分也很合理,救難時女士跟小孩優先,電影都有演(無誤) # In[7]: pd.crosstab(df_data['Sex'],df_data['Survived']).plot(kind="bar") # 「艙等」跟「性別」乍看正好是兩個很有指標性的特徵,剛好一個有等級之分,另一個沒有,可分別作為連續型與非連續型的類別資料輸入,我們以這兩個特徵為基準,之後依序加入其他特徵,看準確率是否提升,來決定是否加入 # In[8]: data = df_data.copy() fitModel(data,['Pclass'],['Sex']) # # SibSp (兄弟姐妹/配偶) # SibSp 這個欄位代表的是有多久兄弟姐妹跟配偶一起登船,我們首先畫張圖看一下,發現獨自登船的生還率相較有人陪伴的乘客低許多,推測手足或配偶在遇難時會互相協助,提高生還率 # In[9]: pd.crosstab(df_data['SibSp'],df_data['Survived']).plot(kind="bar") # 另外一種思路是:看圖表會發現只有欄位是 1 時生還率特別高,之後欄位值愈高,生還率仍然愈低,可推測欄位值為 1 時很大機率是與配偶一起登船;因此須想辦法將配偶與兄弟姐妹的狀況分離,以獲得更高的準確率(這次 DEMO 沒做這個分析) # ### Parch (父母/子女) # Parch 這個欄位代表與父母、兒女一起登船的人數,同樣畫張圖看一下,獨自登船的乘客生還率較低,而與親人一起登船的乘客生還率較高 # In[10]: pd.crosstab(df_data['Parch'],df_data['Survived']).plot(kind="bar") # 這邊還可以做更細的分析:假設我們認知的情境是正確的:女性跟小孩有較高的優先權登上救生艇,那麼其家人一起登上救生艇的機率是否會比其他乘客更高?將父母或子女的性別跟年齡整理後作為其他特徵輸入也許可行(這次 DEMO 同樣沒做 囧) # ### WithFamily (是否與家人同行) # 在這次 DEMO 中,我們將 SibSp 與 Parch 兩個欄位合併為一個特徵「是否與家人同行」輸入 # In[11]: def getWithFamily(item): if item['Parch'] + item['SibSp'] > 0: return 'T' else: return 'F' for index, item in df_data.iterrows(): df_data.loc[index,'WithFamily'] = getWithFamily(item) df_data.groupby('WithFamily').count() # 從圖的結果來看,有無家人同行對乘客生還率有很大影響 # In[12]: pd.crosstab(df_data['WithFamily'],df_data['Survived']).plot(kind="bar") # 將此特徵加入模型,分數比一開始提升許多 # In[13]: data = df_data.copy() fitModel(data,['Pclass'],['Sex','WithFamily']) # ### Ticket (票號) # Ticket 這個欄位為乘客所持船票的票號,若票號重複表示這幾位乘客是一起購票,我們透過 groupby( ) 看看情況 # In[14]: df_sameTicket = df_data.groupby('Ticket').count() df_sameTicket.head() # ### WithFriend (是否與朋友同行) # 透過票號,我們可以做個假設:如果乘客並未與家人一起登船,但又與其他乘客持有共同票號,那他可能是與朋友同行。我們將符合這種情況的乘客找出來,並賦予其新的特徵 # In[15]: index_sameTicket = df_sameTicket[df_sameTicket['PassengerId']>1].index def getWithFriend(index, item): if (item['WithFamily'] == 'F' and item['Ticket'] in np.array(index_sameTicket)): return 'T' else: return 'F' for index, item in df_data.iterrows(): df_data.loc[index,'WithFriend'] = getWithFriend(index, item) df_data.groupby('WithFriend').count() # 畫圖來看,與朋友同行的乘客生還率較高,感覺是個指標 # In[16]: pd.crosstab(df_data['WithFriend'],df_data['Survived']).plot(kind="bar") # 然而當我們實際把新特徵加進模型,卻發現分數降低了,表示可能有其他我們未考慮到的情況,因此暫時放棄這個特徵 # In[17]: data = df_data.copy() fitModel(data,['Pclass'],['Sex','WithFriend']) # ### Fare (票價) # Fare 這個欄位表示票價,在進行分析前,我們得先處理缺值問題。首先找出缺值的那筆資料 # In[18]: df_data[df_data['Fare'].isnull()] # 雖然機率不高,但我們先看看是否有其他乘客持有相同票號,理論上他們的票價應該會相同 # In[19]: df_data[df_data['Ticket']=='3701'] # 果然沒有找到。 # 我們改尋找相同特徵乘客的平均票價,並將缺值補齊。這名乘客是 3 號艙等、男性、從南安普敦(Embarked = S)上船的乘客,找出符合這些條件的乘客 # In[20]: PclassFare = df_data.groupby(['Pclass','Embarked','Sex']).mean()['Fare'] PclassFare.get(3).round(2) # 補值之後,確認下是否還有漏網之魚 # In[21]: df_data['Fare'] = df_data['Fare'].fillna(PclassFare.get(3)['S']['male'].round(2)) df_data[df_data['Fare'].isnull()] # 看起來都有值了,最後將票價的分佈圖畫出來,可以發現分佈相當集中,但極值相距很遠 # In[22]: sns.distplot(df_data['Fare']) df_data.describe()['Fare'].round(2) # ### FareRank (票價等級) # 票價的資料有了,但我們採用的演算法是 Random Forest,由底下眾多訓練後的 Decision Tree 來決定分類結果。為了讓 Decision Tree 能夠運行,我們要將票價轉換為類別型資料: # 為了切出對預測有效果的分界線,我們先將 3 種不同艙等的票價分佈畫出來 # #### 1 號艙等 # In[23]: sns.distplot(df_data[df_data['Pclass']==1]['Fare']) df_data[df_data['Pclass']==1].describe()['Fare'].round(2) # #### 2 號艙等 # In[24]: sns.distplot(df_data[df_data['Pclass']==2]['Fare']) df_data[df_data['Pclass']==2].describe()['Fare'].round(2) # #### 3 號艙等 # In[25]: sns.distplot(df_data[df_data['Pclass']==3]['Fare']) df_data[df_data['Pclass']==3].describe()['Fare'].round(2) # 切割的方法沒有絕對方式,我們參考 3 種不同艙等的中位數與四分位數,盡可能讓每個區間都有某艙等 50% - 75% 的人在,並讓各區間的人數差異不會太大 # In[26]: def getFareRank(item): if (item['Fare'] <= 10): return 1 elif (item['Fare'] <= 28): return 2 elif (item['Fare'] <= 80): return 3 elif (item['Fare'] <= 150): return 4 else: return 4 for index, item in df_data.iterrows(): df_data.loc[index,'FareRank'] = getFareRank(item) df_data['FareRank'] = pd.to_numeric(df_data['FareRank'],downcast='integer') df_data.groupby('FareRank').count() # 將劃分完的票價等級作為新的特徵並畫圖,可以發現票價等級愈高,乘客生還率也愈高(畢竟這也代表艙等愈高級的意思) # In[27]: pd.crosstab(df_data['FareRank'],df_data['Survived']).plot(kind="bar") # 將新特徵輸入模型,得到的分數比基準高,可視為一個有效特徵 # In[28]: data = df_data.copy() fitModel(data,['Pclass','FareRank'],['Sex']) # ### Embarked (登船港口) # Embarked 欄位紀錄乘客是由哪個港口登船,鐵達尼號共有 3 個登船港口(C:瑟堡 Q:皇后鎮 S: 南安普敦),先確認是否有資料缺值 # In[29]: df_data[df_data['Embarked'].isnull()] # 發現有兩筆資料缺值,並且持有相同票號,感覺是找不到其他持有相同票號的乘客了,但還是試一下 # In[30]: df_data[df_data['Ticket']=='113572'] # 沒有。只好再從相同特徵的乘客下手 # 首先觀察票價等級,等級 3 的乘客最多從南安普敦上船、其次是瑟堡,先將皇后鎮踢除 # In[31]: pd.crosstab(df_data['Embarked'],df_data['FareRank']).plot(kind="bar") # 接著再看艙等,1 號艙等的乘客在瑟堡與南安普敦的比例不相上下 # In[32]: pd.crosstab(df_data['Embarked'],df_data['Pclass']).plot(kind="bar") # 觀察性別,女性乘客較多是在南安普敦上船 # In[33]: pd.crosstab(df_data['Embarked'],df_data['Sex']).plot(kind="bar") # 我們將這兩筆缺值的資料填上南安普敦,最後再確認一下 Embarked 這個欄位是否還有缺值 # In[34]: df_data['Embarked'] = df_data['Embarked'].fillna('S') df_data.info() # 把圖畫出來看一下,瑟堡的乘客生還率較高、其次是皇后鎮,最後則是南安普敦 # In[35]: pd.crosstab(df_data['Embarked'],df_data['Survived']).plot(kind="bar") # 其實若做交叉比對,可發現從哪個港口登船與乘客的性別、年齡、票價等級皆有關聯,因此會間接影響到生還率。將此特徵加入模型,並觀察分數 # In[36]: data = df_data.copy() fitModel(data,['Pclass'],['Sex','Embarked']) # ### Age (年齡) # 接著來看 Age 這個欄位,同樣得先處理缺值問題。我們首先看一下有年齡資料的乘客分佈 # In[37]: data_withAge = df_data[np.isnan(df_data['Age'])==False] plt.hist(data_withAge['Age']) # 接著將沒有年齡資料的乘客分離出來,看一下艙等分佈,可發現大多乘客來自 3 號艙等,而 3 號艙等同時也是影響乘客生還率的一個重要指標,因此若補值補得太過隨便,很有可能導致資料失真 # In[38]: data_withoutAge = df_data[np.isnan(df_data['Age'])==True] pd.crosstab(data_withoutAge['Pclass'],data_withoutAge['Survived']).plot(kind="bar") # 我們把所有能用上的共同特徵都用上,讓年齡的誤差盡可能縮小 # In[39]: df_groupAge = data_withAge.groupby(['Pclass','Sex','Embarked']).describe().round(2)['Age'] df_groupAge # 最後我們將乘客依艙等、性別、登船港口一一分離,觀察其統計資料,最後以中位數來作為補值的依據,並將其指派給 NewAge 這個欄位 # In[40]: def getAge(item): if (np.isnan(item['Age'])): return df_groupAge.loc[(item['Pclass'],item['Sex'],item['Embarked']),'50%'] else: return item['Age'] for index, item in df_data.iterrows(): df_data.loc[index,'NewAge'] = getAge(item) df_data.describe()['NewAge'].round(2) # 將補值後的年齡分佈圖畫出來 # In[41]: sns.distplot(df_data['NewAge']) df_data['NewAge'].describe().round(2) # ### AgeRank (年齡等級) # 同樣地,我們要將年齡資料做適當切分,這裡我們將生還者與罹難者的年齡分佈畫出來 # #### 生還者 # In[42]: sns.distplot(data_withAge[data_withAge['Survived']==1]['Age']) data_withAge[data_withAge['Survived']==1]['Age'].describe().round(2) # #### 罹難者 # In[43]: sns.distplot(data_withAge[data_withAge['Survived']==0]['Age']) data_withAge[data_withAge['Survived']==0]['Age'].describe().round(2) # 從上面兩張圖發現,左邊 0-16 歲之間有很大的差異,接著是中間的峰值,在罹難者這邊有稍微往左偏的趨勢,為了捕捉這些變化,我們將年齡切成四個區段 # In[44]: def getAgeRank(age): if (age <= 16): return 1 elif (age <= 26): return 2 elif (age <= 36): return 3 else: return 4 for index, item in df_data.iterrows(): df_data.loc[index,'AgeRank'] = getAgeRank(item['NewAge']) df_data['AgeRank'] = pd.to_numeric(df_data['AgeRank'],downcast='integer') df_data.groupby('AgeRank').count() # 最後畫個圖比對一下,首先是未成年乘客的生還率最高,其次是介於青年與老年中間的人士,青年的生還率最低 # In[45]: pd.crosstab(df_data['AgeRank'],df_data['Survived']).plot(kind="bar") # 將處理後的特徵輸入模型,分數比基準高,應該可視為特徵之一 # In[46]: data = df_data.copy() fitModel(data,['Pclass','AgeRank'],['Sex']) # ### Cabin (艙號) # Cabin 這欄位紀錄的是艙號,也是缺值缺最嚴重的一個欄位。這裡我只做一個假設,即一般情況下家人、配偶、朋友會在同個艙號,而其他非上述關係卻有相同艙號的乘客,也許登船前不認識、登船後成為朋友甚至朋友以上關係的:例如傑克跟羅絲(誤) # In[47]: data_newFriend = df_data.groupby('Cabin').count() carbinIndex = data_newFriend[data_newFriend['PassengerId']>1].index def getWithNewFirend(index, item): if (index in np.array(carbinIndex) ): return 'T' else: return 'F' for index, item in df_data.iterrows(): df_data.loc[index,'WithNewFriend'] = getWithNewFirend(index, item) df_data[df_data['WithNewFriend']==1].info() # 結論:我想多了。現有資料不支持這個假設,放棄這個欄位 # ### ConnectedSurvival (生還關聯性) # 最後一個特徵,我們直接拿已有的乘客生還率來當指標 # 首先我們找出持有相同票號的乘客,這些人可能是家人、可能是配偶,也可能是朋友,或是沒那麼熟的朋友(?),總之有某種關聯就對了。剩下的概念就是:假設與我關聯的這些人都生還了,那麼我的生還率應該很高(不能只有我死啊喂);反之如果其他人都死了,那我的生還率也就相對低了 # In[48]: df_survival = df_data.groupby('Ticket').describe()['Survived'] df_survival.head() # 我們將相同票號的人找出來,計算這些人的生還率平均。如果這個票號只有一個人,或這些人沒有生還資料,我們就給予 0.5 的基本值(半死半活) # In[49]: def getConnectedSurvival(item): if (np.isnan(df_survival.loc[item['Ticket'],'mean']) or df_survival.loc[item['Ticket'],'count'] == 1): return 0.5 else: return df_survival.loc[item['Ticket'],'mean'] for index, item in df_data.iterrows(): df_data.loc[index,'ConnectedSurvival'] = getConnectedSurvival(item) df_data.groupby('ConnectedSurvival').count() # 最後將這個特徵加進模型,分數大幅提升,但同時也要擔心 overfitting 就是了 # In[50]: data = df_data.copy() fitModel(data,['Pclass','ConnectedSurvival'],['Sex']) # ## Submit # 探索性分析及特徵工程做完後,我們將所有認為有效的特徵加入模型訓練 # In[51]: data = df_data.copy() fitModel(data,['Pclass','FareRank','AgeRank','ConnectedSurvival'],['Sex','WithFamily','Embarked']) # 最後用這些特徵跟訓練結果去對測試資料進行預測,匯出結果 # In[52]: data = df_data.copy() submitResult(data,['Pclass','FareRank','AgeRank','ConnectedSurvival'],['Sex','WithFamily','Embarked']) # 將結果上傳至 Kaggle後,獲得 0.78 的準確率(跟我們的分數差很大,果然有 overfitting ) # to be continued...