# 匯入資料處理套件
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 處理。
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( ) 可查看前五筆資料,大致了解每筆資料有哪些欄位及資料型態
df_data.head()
透過 info( ) 可了解整個資料集的大致情形:可發現 Age、Fare、Cabin 及 Embarked 欄位皆有缺值,須進行補值處理;Survived 只有 891 筆資料是因為只有訓練資料才有提供結果,測試資料須用訓練後的模型進行預設
df_data.info()
為了決定對哪些特徵進行預處理,最後輸入模型進行訓練,我們可以先進行探索性分析,了解各欄位特性。接下來觀察各欄位特徵:
透過 groupby( ) 可以知道整艘船有 3 種艙等,取各艙等的平均票價可發現票價差異之大(可以理解成頭等艙、商務艙、經濟艙的概念)
df_data.groupby('Pclass').mean().round(2)['Fare']
將艙等與乘客生還率畫成圖比對,可以發現艙等等級愈高,生還率愈高,可以理解為 VIP 乘客有較高的優先權獲救,或是高等艙有較好的逃生設備或離逃生口較近;無論如何都挺現實跟合理的,畢竟人家付的錢比較多
pd.crosstab(df_data['Pclass'],df_data['Survived']).plot(kind="bar")
性別的部分,由圖可知女性生還率遠高於男性,這部分也很合理,救難時女士跟小孩優先,電影都有演(無誤)
pd.crosstab(df_data['Sex'],df_data['Survived']).plot(kind="bar")
「艙等」跟「性別」乍看正好是兩個很有指標性的特徵,剛好一個有等級之分,另一個沒有,可分別作為連續型與非連續型的類別資料輸入,我們以這兩個特徵為基準,之後依序加入其他特徵,看準確率是否提升,來決定是否加入
data = df_data.copy()
fitModel(data,['Pclass'],['Sex'])
SibSp 這個欄位代表的是有多久兄弟姐妹跟配偶一起登船,我們首先畫張圖看一下,發現獨自登船的生還率相較有人陪伴的乘客低許多,推測手足或配偶在遇難時會互相協助,提高生還率
pd.crosstab(df_data['SibSp'],df_data['Survived']).plot(kind="bar")
另外一種思路是:看圖表會發現只有欄位是 1 時生還率特別高,之後欄位值愈高,生還率仍然愈低,可推測欄位值為 1 時很大機率是與配偶一起登船;因此須想辦法將配偶與兄弟姐妹的狀況分離,以獲得更高的準確率(這次 DEMO 沒做這個分析)
Parch 這個欄位代表與父母、兒女一起登船的人數,同樣畫張圖看一下,獨自登船的乘客生還率較低,而與親人一起登船的乘客生還率較高
pd.crosstab(df_data['Parch'],df_data['Survived']).plot(kind="bar")
這邊還可以做更細的分析:假設我們認知的情境是正確的:女性跟小孩有較高的優先權登上救生艇,那麼其家人一起登上救生艇的機率是否會比其他乘客更高?將父母或子女的性別跟年齡整理後作為其他特徵輸入也許可行(這次 DEMO 同樣沒做 囧)
在這次 DEMO 中,我們將 SibSp 與 Parch 兩個欄位合併為一個特徵「是否與家人同行」輸入
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()
從圖的結果來看,有無家人同行對乘客生還率有很大影響
pd.crosstab(df_data['WithFamily'],df_data['Survived']).plot(kind="bar")
將此特徵加入模型,分數比一開始提升許多
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','WithFamily'])
Ticket 這個欄位為乘客所持船票的票號,若票號重複表示這幾位乘客是一起購票,我們透過 groupby( ) 看看情況
df_sameTicket = df_data.groupby('Ticket').count()
df_sameTicket.head()
透過票號,我們可以做個假設:如果乘客並未與家人一起登船,但又與其他乘客持有共同票號,那他可能是與朋友同行。我們將符合這種情況的乘客找出來,並賦予其新的特徵
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()
畫圖來看,與朋友同行的乘客生還率較高,感覺是個指標
pd.crosstab(df_data['WithFriend'],df_data['Survived']).plot(kind="bar")
然而當我們實際把新特徵加進模型,卻發現分數降低了,表示可能有其他我們未考慮到的情況,因此暫時放棄這個特徵
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','WithFriend'])
Fare 這個欄位表示票價,在進行分析前,我們得先處理缺值問題。首先找出缺值的那筆資料
df_data[df_data['Fare'].isnull()]
雖然機率不高,但我們先看看是否有其他乘客持有相同票號,理論上他們的票價應該會相同
df_data[df_data['Ticket']=='3701']
果然沒有找到。
我們改尋找相同特徵乘客的平均票價,並將缺值補齊。這名乘客是 3 號艙等、男性、從南安普敦(Embarked = S)上船的乘客,找出符合這些條件的乘客
PclassFare = df_data.groupby(['Pclass','Embarked','Sex']).mean()['Fare']
PclassFare.get(3).round(2)
補值之後,確認下是否還有漏網之魚
df_data['Fare'] = df_data['Fare'].fillna(PclassFare.get(3)['S']['male'].round(2))
df_data[df_data['Fare'].isnull()]
看起來都有值了,最後將票價的分佈圖畫出來,可以發現分佈相當集中,但極值相距很遠
sns.distplot(df_data['Fare'])
df_data.describe()['Fare'].round(2)
票價的資料有了,但我們採用的演算法是 Random Forest,由底下眾多訓練後的 Decision Tree 來決定分類結果。為了讓 Decision Tree 能夠運行,我們要將票價轉換為類別型資料:
為了切出對預測有效果的分界線,我們先將 3 種不同艙等的票價分佈畫出來
sns.distplot(df_data[df_data['Pclass']==1]['Fare'])
df_data[df_data['Pclass']==1].describe()['Fare'].round(2)
sns.distplot(df_data[df_data['Pclass']==2]['Fare'])
df_data[df_data['Pclass']==2].describe()['Fare'].round(2)
sns.distplot(df_data[df_data['Pclass']==3]['Fare'])
df_data[df_data['Pclass']==3].describe()['Fare'].round(2)
切割的方法沒有絕對方式,我們參考 3 種不同艙等的中位數與四分位數,盡可能讓每個區間都有某艙等 50% - 75% 的人在,並讓各區間的人數差異不會太大
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()
將劃分完的票價等級作為新的特徵並畫圖,可以發現票價等級愈高,乘客生還率也愈高(畢竟這也代表艙等愈高級的意思)
pd.crosstab(df_data['FareRank'],df_data['Survived']).plot(kind="bar")
將新特徵輸入模型,得到的分數比基準高,可視為一個有效特徵
data = df_data.copy()
fitModel(data,['Pclass','FareRank'],['Sex'])
Embarked 欄位紀錄乘客是由哪個港口登船,鐵達尼號共有 3 個登船港口(C:瑟堡 Q:皇后鎮 S: 南安普敦),先確認是否有資料缺值
df_data[df_data['Embarked'].isnull()]
發現有兩筆資料缺值,並且持有相同票號,感覺是找不到其他持有相同票號的乘客了,但還是試一下
df_data[df_data['Ticket']=='113572']
沒有。只好再從相同特徵的乘客下手
首先觀察票價等級,等級 3 的乘客最多從南安普敦上船、其次是瑟堡,先將皇后鎮踢除
pd.crosstab(df_data['Embarked'],df_data['FareRank']).plot(kind="bar")
接著再看艙等,1 號艙等的乘客在瑟堡與南安普敦的比例不相上下
pd.crosstab(df_data['Embarked'],df_data['Pclass']).plot(kind="bar")
觀察性別,女性乘客較多是在南安普敦上船
pd.crosstab(df_data['Embarked'],df_data['Sex']).plot(kind="bar")
我們將這兩筆缺值的資料填上南安普敦,最後再確認一下 Embarked 這個欄位是否還有缺值
df_data['Embarked'] = df_data['Embarked'].fillna('S')
df_data.info()
把圖畫出來看一下,瑟堡的乘客生還率較高、其次是皇后鎮,最後則是南安普敦
pd.crosstab(df_data['Embarked'],df_data['Survived']).plot(kind="bar")
其實若做交叉比對,可發現從哪個港口登船與乘客的性別、年齡、票價等級皆有關聯,因此會間接影響到生還率。將此特徵加入模型,並觀察分數
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','Embarked'])
接著來看 Age 這個欄位,同樣得先處理缺值問題。我們首先看一下有年齡資料的乘客分佈
data_withAge = df_data[np.isnan(df_data['Age'])==False]
plt.hist(data_withAge['Age'])
接著將沒有年齡資料的乘客分離出來,看一下艙等分佈,可發現大多乘客來自 3 號艙等,而 3 號艙等同時也是影響乘客生還率的一個重要指標,因此若補值補得太過隨便,很有可能導致資料失真
data_withoutAge = df_data[np.isnan(df_data['Age'])==True]
pd.crosstab(data_withoutAge['Pclass'],data_withoutAge['Survived']).plot(kind="bar")
我們把所有能用上的共同特徵都用上,讓年齡的誤差盡可能縮小
df_groupAge = data_withAge.groupby(['Pclass','Sex','Embarked']).describe().round(2)['Age']
df_groupAge
最後我們將乘客依艙等、性別、登船港口一一分離,觀察其統計資料,最後以中位數來作為補值的依據,並將其指派給 NewAge 這個欄位
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)
將補值後的年齡分佈圖畫出來
sns.distplot(df_data['NewAge'])
df_data['NewAge'].describe().round(2)
同樣地,我們要將年齡資料做適當切分,這裡我們將生還者與罹難者的年齡分佈畫出來
sns.distplot(data_withAge[data_withAge['Survived']==1]['Age'])
data_withAge[data_withAge['Survived']==1]['Age'].describe().round(2)
sns.distplot(data_withAge[data_withAge['Survived']==0]['Age'])
data_withAge[data_withAge['Survived']==0]['Age'].describe().round(2)
從上面兩張圖發現,左邊 0-16 歲之間有很大的差異,接著是中間的峰值,在罹難者這邊有稍微往左偏的趨勢,為了捕捉這些變化,我們將年齡切成四個區段
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()
最後畫個圖比對一下,首先是未成年乘客的生還率最高,其次是介於青年與老年中間的人士,青年的生還率最低
pd.crosstab(df_data['AgeRank'],df_data['Survived']).plot(kind="bar")
將處理後的特徵輸入模型,分數比基準高,應該可視為特徵之一
data = df_data.copy()
fitModel(data,['Pclass','AgeRank'],['Sex'])
Cabin 這欄位紀錄的是艙號,也是缺值缺最嚴重的一個欄位。這裡我只做一個假設,即一般情況下家人、配偶、朋友會在同個艙號,而其他非上述關係卻有相同艙號的乘客,也許登船前不認識、登船後成為朋友甚至朋友以上關係的:例如傑克跟羅絲(誤)
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()
結論:我想多了。現有資料不支持這個假設,放棄這個欄位
最後一個特徵,我們直接拿已有的乘客生還率來當指標
首先我們找出持有相同票號的乘客,這些人可能是家人、可能是配偶,也可能是朋友,或是沒那麼熟的朋友(?),總之有某種關聯就對了。剩下的概念就是:假設與我關聯的這些人都生還了,那麼我的生還率應該很高(不能只有我死啊喂);反之如果其他人都死了,那我的生還率也就相對低了
df_survival = df_data.groupby('Ticket').describe()['Survived']
df_survival.head()
我們將相同票號的人找出來,計算這些人的生還率平均。如果這個票號只有一個人,或這些人沒有生還資料,我們就給予 0.5 的基本值(半死半活)
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 就是了
data = df_data.copy()
fitModel(data,['Pclass','ConnectedSurvival'],['Sex'])
探索性分析及特徵工程做完後,我們將所有認為有效的特徵加入模型訓練
data = df_data.copy()
fitModel(data,['Pclass','FareRank','AgeRank','ConnectedSurvival'],['Sex','WithFamily','Embarked'])
最後用這些特徵跟訓練結果去對測試資料進行預測,匯出結果
data = df_data.copy()
submitResult(data,['Pclass','FareRank','AgeRank','ConnectedSurvival'],['Sex','WithFamily','Embarked'])
將結果上傳至 Kaggle後,獲得 0.78 的準確率(跟我們的分數差很大,果然有 overfitting )
to be continued...