# 匯入資料處理套件
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()
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0.0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
1 | 2 | 1.0 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
2 | 3 | 1.0 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
3 | 4 | 1.0 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S |
4 | 5 | 0.0 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S |
透過 info( ) 可了解整個資料集的大致情形:可發現 Age、Fare、Cabin 及 Embarked 欄位皆有缺值,須進行補值處理;Survived 只有 891 筆資料是因為只有訓練資料才有提供結果,測試資料須用訓練後的模型進行預設
df_data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1309 entries, 0 to 1308 Data columns (total 12 columns): PassengerId 1309 non-null int64 Survived 891 non-null float64 Pclass 1309 non-null int64 Name 1309 non-null object Sex 1309 non-null object Age 1046 non-null float64 SibSp 1309 non-null int64 Parch 1309 non-null int64 Ticket 1309 non-null object Fare 1308 non-null float64 Cabin 295 non-null object Embarked 1307 non-null object dtypes: float64(3), int64(4), object(5) memory usage: 122.8+ KB
為了決定對哪些特徵進行預處理,最後輸入模型進行訓練,我們可以先進行探索性分析,了解各欄位特性。接下來觀察各欄位特徵:
透過 groupby( ) 可以知道整艘船有 3 種艙等,取各艙等的平均票價可發現票價差異之大(可以理解成頭等艙、商務艙、經濟艙的概念)
df_data.groupby('Pclass').mean().round(2)['Fare']
Pclass 1 87.51 2 21.18 3 13.30 Name: Fare, dtype: float64
將艙等與乘客生還率畫成圖比對,可以發現艙等等級愈高,生還率愈高,可以理解為 VIP 乘客有較高的優先權獲救,或是高等艙有較好的逃生設備或離逃生口較近;無論如何都挺現實跟合理的,畢竟人家付的錢比較多
pd.crosstab(df_data['Pclass'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1e75da90>
性別的部分,由圖可知女性生還率遠高於男性,這部分也很合理,救難時女士跟小孩優先,電影都有演(無誤)
pd.crosstab(df_data['Sex'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1e66fa58>
「艙等」跟「性別」乍看正好是兩個很有指標性的特徵,剛好一個有等級之分,另一個沒有,可分別作為連續型與非連續型的類別資料輸入,我們以這兩個特徵為基準,之後依序加入其他特徵,看準確率是否提升,來決定是否加入
data = df_data.copy()
fitModel(data,['Pclass'],['Sex'])
Base oob score : 0.72278
SibSp 這個欄位代表的是有多久兄弟姐妹跟配偶一起登船,我們首先畫張圖看一下,發現獨自登船的生還率相較有人陪伴的乘客低許多,推測手足或配偶在遇難時會互相協助,提高生還率
pd.crosstab(df_data['SibSp'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1eb156d8>
另外一種思路是:看圖表會發現只有欄位是 1 時生還率特別高,之後欄位值愈高,生還率仍然愈低,可推測欄位值為 1 時很大機率是與配偶一起登船;因此須想辦法將配偶與兄弟姐妹的狀況分離,以獲得更高的準確率(這次 DEMO 沒做這個分析)
Parch 這個欄位代表與父母、兒女一起登船的人數,同樣畫張圖看一下,獨自登船的乘客生還率較低,而與親人一起登船的乘客生還率較高
pd.crosstab(df_data['Parch'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1ec37cc0>
這邊還可以做更細的分析:假設我們認知的情境是正確的:女性跟小孩有較高的優先權登上救生艇,那麼其家人一起登上救生艇的機率是否會比其他乘客更高?將父母或子女的性別跟年齡整理後作為其他特徵輸入也許可行(這次 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()
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
WithFamily | ||||||||||||
F | 790 | 537 | 790 | 790 | 790 | 590 | 790 | 790 | 790 | 789 | 131 | 788 |
T | 519 | 354 | 519 | 519 | 519 | 456 | 519 | 519 | 519 | 519 | 164 | 519 |
從圖的結果來看,有無家人同行對乘客生還率有很大影響
pd.crosstab(df_data['WithFamily'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1ed443c8>
將此特徵加入模型,分數比一開始提升許多
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','WithFamily'])
Base oob score : 0.80247
Ticket 這個欄位為乘客所持船票的票號,若票號重複表示這幾位乘客是一起購票,我們透過 groupby( ) 看看情況
df_sameTicket = df_data.groupby('Ticket').count()
df_sameTicket.head()
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Fare | Cabin | Embarked | WithFamily | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Ticket | ||||||||||||
110152 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
110413 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
110465 | 2 | 2 | 2 | 2 | 2 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
110469 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
110489 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
透過票號,我們可以做個假設:如果乘客並未與家人一起登船,但又與其他乘客持有共同票號,那他可能是與朋友同行。我們將符合這種情況的乘客找出來,並賦予其新的特徵
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()
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
WithFriend | |||||||||||||
F | 1182 | 802 | 1182 | 1182 | 1182 | 945 | 1182 | 1182 | 1182 | 1181 | 252 | 1182 | 1182 |
T | 127 | 89 | 127 | 127 | 127 | 101 | 127 | 127 | 127 | 127 | 43 | 125 | 127 |
畫圖來看,與朋友同行的乘客生還率較高,感覺是個指標
pd.crosstab(df_data['WithFriend'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1eec60f0>
然而當我們實際把新特徵加進模型,卻發現分數降低了,表示可能有其他我們未考慮到的情況,因此暫時放棄這個特徵
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','WithFriend'])
Base oob score : 0.72840
Fare 這個欄位表示票價,在進行分析前,我們得先處理缺值問題。首先找出缺值的那筆資料
df_data[df_data['Fare'].isnull()]
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | WithFriend | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1043 | 1044 | NaN | 3 | Storey, Mr. Thomas | male | 60.5 | 0 | 0 | 3701 | NaN | NaN | S | F | F |
雖然機率不高,但我們先看看是否有其他乘客持有相同票號,理論上他們的票價應該會相同
df_data[df_data['Ticket']=='3701']
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | WithFriend | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1043 | 1044 | NaN | 3 | Storey, Mr. Thomas | male | 60.5 | 0 | 0 | 3701 | NaN | NaN | S | F | F |
果然沒有找到。
我們改尋找相同特徵乘客的平均票價,並將缺值補齊。這名乘客是 3 號艙等、男性、從南安普敦(Embarked = S)上船的乘客,找出符合這些條件的乘客
PclassFare = df_data.groupby(['Pclass','Embarked','Sex']).mean()['Fare']
PclassFare.get(3).round(2)
Embarked Sex C female 13.83 male 9.78 Q female 9.79 male 10.98 S female 18.08 male 13.15 Name: Fare, dtype: float64
補值之後,確認下是否還有漏網之魚
df_data['Fare'] = df_data['Fare'].fillna(PclassFare.get(3)['S']['male'].round(2))
df_data[df_data['Fare'].isnull()]
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | WithFriend |
---|
看起來都有值了,最後將票價的分佈圖畫出來,可以發現分佈相當集中,但極值相距很遠
sns.distplot(df_data['Fare'])
df_data.describe()['Fare'].round(2)
count 1309.00 mean 33.28 std 51.74 min 0.00 25% 7.90 50% 14.45 75% 31.28 max 512.33 Name: Fare, dtype: float64
票價的資料有了,但我們採用的演算法是 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)
count 323.00 mean 87.51 std 80.45 min 0.00 25% 30.70 50% 60.00 75% 107.66 max 512.33 Name: Fare, dtype: float64
sns.distplot(df_data[df_data['Pclass']==2]['Fare'])
df_data[df_data['Pclass']==2].describe()['Fare'].round(2)
count 277.00 mean 21.18 std 13.61 min 0.00 25% 13.00 50% 15.05 75% 26.00 max 73.50 Name: Fare, dtype: float64
sns.distplot(df_data[df_data['Pclass']==3]['Fare'])
df_data[df_data['Pclass']==3].describe()['Fare'].round(2)
count 709.00 mean 13.30 std 11.49 min 0.00 25% 7.75 50% 8.05 75% 15.25 max 69.55 Name: Fare, dtype: float64
切割的方法沒有絕對方式,我們參考 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()
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | WithFriend | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
FareRank | ||||||||||||||
1 | 491 | 336 | 491 | 491 | 491 | 338 | 491 | 491 | 491 | 491 | 13 | 491 | 491 | 491 |
2 | 450 | 303 | 450 | 450 | 450 | 380 | 450 | 450 | 450 | 450 | 55 | 450 | 450 | 450 |
3 | 253 | 178 | 253 | 253 | 253 | 220 | 253 | 253 | 253 | 253 | 128 | 251 | 253 | 253 |
4 | 115 | 74 | 115 | 115 | 115 | 108 | 115 | 115 | 115 | 115 | 99 | 115 | 115 | 115 |
將劃分完的票價等級作為新的特徵並畫圖,可以發現票價等級愈高,乘客生還率也愈高(畢竟這也代表艙等愈高級的意思)
pd.crosstab(df_data['FareRank'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f422898>
將新特徵輸入模型,得到的分數比基準高,可視為一個有效特徵
data = df_data.copy()
fitModel(data,['Pclass','FareRank'],['Sex'])
Base oob score : 0.77666
Embarked 欄位紀錄乘客是由哪個港口登船,鐵達尼號共有 3 個登船港口(C:瑟堡 Q:皇后鎮 S: 南安普敦),先確認是否有資料缺值
df_data[df_data['Embarked'].isnull()]
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | WithFriend | FareRank | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
61 | 62 | 1.0 | 1 | Icard, Miss. Amelie | female | 38.0 | 0 | 0 | 113572 | 80.0 | B28 | NaN | F | T | 3 |
829 | 830 | 1.0 | 1 | Stone, Mrs. George Nelson (Martha Evelyn) | female | 62.0 | 0 | 0 | 113572 | 80.0 | B28 | NaN | F | T | 3 |
發現有兩筆資料缺值,並且持有相同票號,感覺是找不到其他持有相同票號的乘客了,但還是試一下
df_data[df_data['Ticket']=='113572']
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | WithFriend | FareRank | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
61 | 62 | 1.0 | 1 | Icard, Miss. Amelie | female | 38.0 | 0 | 0 | 113572 | 80.0 | B28 | NaN | F | T | 3 |
829 | 830 | 1.0 | 1 | Stone, Mrs. George Nelson (Martha Evelyn) | female | 62.0 | 0 | 0 | 113572 | 80.0 | B28 | NaN | F | T | 3 |
沒有。只好再從相同特徵的乘客下手
首先觀察票價等級,等級 3 的乘客最多從南安普敦上船、其次是瑟堡,先將皇后鎮踢除
pd.crosstab(df_data['Embarked'],df_data['FareRank']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1eea4940>
接著再看艙等,1 號艙等的乘客在瑟堡與南安普敦的比例不相上下
pd.crosstab(df_data['Embarked'],df_data['Pclass']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f5ebf98>
觀察性別,女性乘客較多是在南安普敦上船
pd.crosstab(df_data['Embarked'],df_data['Sex']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f6e2828>
我們將這兩筆缺值的資料填上南安普敦,最後再確認一下 Embarked 這個欄位是否還有缺值
df_data['Embarked'] = df_data['Embarked'].fillna('S')
df_data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1309 entries, 0 to 1308 Data columns (total 15 columns): PassengerId 1309 non-null int64 Survived 891 non-null float64 Pclass 1309 non-null int64 Name 1309 non-null object Sex 1309 non-null object Age 1046 non-null float64 SibSp 1309 non-null int64 Parch 1309 non-null int64 Ticket 1309 non-null object Fare 1309 non-null float64 Cabin 295 non-null object Embarked 1309 non-null object WithFamily 1309 non-null object WithFriend 1309 non-null object FareRank 1309 non-null int8 dtypes: float64(3), int64(4), int8(1), object(7) memory usage: 144.5+ KB
把圖畫出來看一下,瑟堡的乘客生還率較高、其次是皇后鎮,最後則是南安普敦
pd.crosstab(df_data['Embarked'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f7d87b8>
其實若做交叉比對,可發現從哪個港口登船與乘客的性別、年齡、票價等級皆有關聯,因此會間接影響到生還率。將此特徵加入模型,並觀察分數
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','Embarked'])
Base oob score : 0.81145
接著來看 Age 這個欄位,同樣得先處理缺值問題。我們首先看一下有年齡資料的乘客分佈
data_withAge = df_data[np.isnan(df_data['Age'])==False]
plt.hist(data_withAge['Age'])
(array([ 72., 62., 274., 250., 161., 108., 65., 41., 10., 3.]), array([ 0.17 , 8.153, 16.136, 24.119, 32.102, 40.085, 48.068, 56.051, 64.034, 72.017, 80. ]), <a list of 10 Patch objects>)
接著將沒有年齡資料的乘客分離出來,看一下艙等分佈,可發現大多乘客來自 3 號艙等,而 3 號艙等同時也是影響乘客生還率的一個重要指標,因此若補值補得太過隨便,很有可能導致資料失真
data_withoutAge = df_data[np.isnan(df_data['Age'])==True]
pd.crosstab(data_withoutAge['Pclass'],data_withoutAge['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f9899b0>
我們把所有能用上的共同特徵都用上,讓年齡的誤差盡可能縮小
df_groupAge = data_withAge.groupby(['Pclass','Sex','Embarked']).describe().round(2)['Age']
df_groupAge
count | mean | std | min | 25% | 50% | 75% | max | |||
---|---|---|---|---|---|---|---|---|---|---|
Pclass | Sex | Embarked | ||||||||
1 | female | C | 65.0 | 38.11 | 12.94 | 16.00 | 26.0 | 38.00 | 48.00 | 64.0 |
Q | 2.0 | 35.00 | 2.83 | 33.00 | 34.0 | 35.00 | 36.00 | 37.0 | ||
S | 66.0 | 36.05 | 15.70 | 2.00 | 23.0 | 35.00 | 47.75 | 76.0 | ||
male | C | 63.0 | 40.05 | 14.74 | 6.00 | 27.5 | 39.00 | 50.00 | 71.0 | |
Q | 1.0 | 44.00 | NaN | 44.00 | 44.0 | 44.00 | 44.00 | 44.0 | ||
S | 87.0 | 41.71 | 14.59 | 0.92 | 31.0 | 42.00 | 50.50 | 80.0 | ||
2 | female | C | 11.0 | 19.36 | 9.74 | 1.00 | 15.5 | 23.00 | 25.50 | 30.0 |
Q | 1.0 | 30.00 | NaN | 30.00 | 30.0 | 30.00 | 30.00 | 30.0 | ||
S | 91.0 | 28.46 | 13.01 | 0.92 | 20.5 | 28.00 | 36.00 | 60.0 | ||
male | C | 13.0 | 27.27 | 9.55 | 1.00 | 25.0 | 29.00 | 31.00 | 41.0 | |
Q | 4.0 | 53.75 | 12.69 | 35.00 | 51.5 | 59.00 | 61.25 | 62.0 | ||
S | 141.0 | 30.49 | 13.84 | 0.67 | 23.0 | 29.00 | 39.00 | 70.0 | ||
3 | female | C | 22.0 | 16.82 | 12.86 | 0.75 | 9.0 | 15.00 | 18.75 | 45.0 |
Q | 21.0 | 24.33 | 7.42 | 15.00 | 18.0 | 22.00 | 30.00 | 39.0 | ||
S | 109.0 | 22.85 | 12.60 | 0.17 | 17.0 | 22.00 | 30.00 | 63.0 | ||
male | C | 38.0 | 24.13 | 9.70 | 0.42 | 20.0 | 24.25 | 29.75 | 45.5 | |
Q | 21.0 | 26.74 | 17.60 | 2.00 | 19.0 | 25.00 | 32.00 | 70.5 | ||
S | 290.0 | 26.15 | 11.42 | 0.33 | 20.0 | 25.00 | 32.00 | 74.0 |
最後我們將乘客依艙等、性別、登船港口一一分離,觀察其統計資料,最後以中位數來作為補值的依據,並將其指派給 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)
count 1309.00 mean 29.20 std 13.28 min 0.17 25% 22.00 50% 26.00 75% 36.00 max 80.00 Name: NewAge, dtype: float64
將補值後的年齡分佈圖畫出來
sns.distplot(df_data['NewAge'])
df_data['NewAge'].describe().round(2)
count 1309.00 mean 29.20 std 13.28 min 0.17 25% 22.00 50% 26.00 75% 36.00 max 80.00 Name: NewAge, dtype: float64
同樣地,我們要將年齡資料做適當切分,這裡我們將生還者與罹難者的年齡分佈畫出來
sns.distplot(data_withAge[data_withAge['Survived']==1]['Age'])
data_withAge[data_withAge['Survived']==1]['Age'].describe().round(2)
count 290.00 mean 28.34 std 14.95 min 0.42 25% 19.00 50% 28.00 75% 36.00 max 80.00 Name: Age, dtype: float64
sns.distplot(data_withAge[data_withAge['Survived']==0]['Age'])
data_withAge[data_withAge['Survived']==0]['Age'].describe().round(2)
count 424.00 mean 30.63 std 14.17 min 1.00 25% 21.00 50% 28.00 75% 39.00 max 74.00 Name: Age, dtype: float64
從上面兩張圖發現,左邊 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()
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | WithFriend | FareRank | NewAge | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
AgeRank | ||||||||||||||||
1 | 143 | 107 | 143 | 143 | 143 | 134 | 143 | 143 | 143 | 143 | 21 | 143 | 143 | 143 | 143 | 143 |
2 | 538 | 348 | 538 | 538 | 538 | 339 | 538 | 538 | 538 | 538 | 55 | 538 | 538 | 538 | 538 | 538 |
3 | 302 | 215 | 302 | 302 | 302 | 282 | 302 | 302 | 302 | 302 | 73 | 302 | 302 | 302 | 302 | 302 |
4 | 326 | 221 | 326 | 326 | 326 | 291 | 326 | 326 | 326 | 326 | 146 | 326 | 326 | 326 | 326 | 326 |
最後畫個圖比對一下,首先是未成年乘客的生還率最高,其次是介於青年與老年中間的人士,青年的生還率最低
pd.crosstab(df_data['AgeRank'],df_data['Survived']).plot(kind="bar")
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f37d160>
將處理後的特徵輸入模型,分數比基準高,應該可視為特徵之一
data = df_data.copy()
fitModel(data,['Pclass','AgeRank'],['Sex'])
Base oob score : 0.79461
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()
<class 'pandas.core.frame.DataFrame'> Int64Index: 0 entries Data columns (total 18 columns): PassengerId 0 non-null int64 Survived 0 non-null float64 Pclass 0 non-null int64 Name 0 non-null object Sex 0 non-null object Age 0 non-null float64 SibSp 0 non-null int64 Parch 0 non-null int64 Ticket 0 non-null object Fare 0 non-null float64 Cabin 0 non-null object Embarked 0 non-null object WithFamily 0 non-null object WithFriend 0 non-null object FareRank 0 non-null int8 NewAge 0 non-null float64 AgeRank 0 non-null int8 WithNewFriend 0 non-null object dtypes: float64(4), int64(4), int8(2), object(8) memory usage: 0.0+ bytes
結論:我想多了。現有資料不支持這個假設,放棄這個欄位
最後一個特徵,我們直接拿已有的乘客生還率來當指標
首先我們找出持有相同票號的乘客,這些人可能是家人、可能是配偶,也可能是朋友,或是沒那麼熟的朋友(?),總之有某種關聯就對了。剩下的概念就是:假設與我關聯的這些人都生還了,那麼我的生還率應該很高(不能只有我死啊喂);反之如果其他人都死了,那我的生還率也就相對低了
df_survival = df_data.groupby('Ticket').describe()['Survived']
df_survival.head()
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
Ticket | ||||||||
110152 | 3.0 | 1.000000 | 0.00000 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
110413 | 3.0 | 0.666667 | 0.57735 | 0.0 | 0.5 | 1.0 | 1.0 | 1.0 |
110465 | 2.0 | 0.000000 | 0.00000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
110469 | 0.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
110489 | 0.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
我們將相同票號的人找出來,計算這些人的生還率平均。如果這個票號只有一個人,或這些人沒有生還資料,我們就給予 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()
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | WithFamily | WithFriend | FareRank | NewAge | AgeRank | WithNewFriend | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ConnectedSurvival | ||||||||||||||||||
0.000000 | 129 | 110 | 129 | 129 | 129 | 102 | 129 | 129 | 129 | 129 | 9 | 129 | 129 | 129 | 129 | 129 | 129 | 129 |
0.250000 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 0 | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
0.333333 | 3 | 3 | 3 | 3 | 3 | 0 | 3 | 3 | 3 | 3 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
0.500000 | 977 | 615 | 977 | 977 | 977 | 767 | 977 | 977 | 977 | 977 | 187 | 977 | 977 | 977 | 977 | 977 | 977 | 977 |
0.666667 | 36 | 33 | 36 | 36 | 36 | 35 | 36 | 36 | 36 | 36 | 15 | 36 | 36 | 36 | 36 | 36 | 36 | 36 |
0.714286 | 8 | 7 | 8 | 8 | 8 | 4 | 8 | 8 | 8 | 8 | 0 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
0.750000 | 16 | 12 | 16 | 16 | 16 | 14 | 16 | 16 | 16 | 16 | 6 | 16 | 16 | 16 | 16 | 16 | 16 | 16 |
1.000000 | 136 | 107 | 136 | 136 | 136 | 120 | 136 | 136 | 136 | 136 | 78 | 136 | 136 | 136 | 136 | 136 | 136 | 136 |
最後將這個特徵加進模型,分數大幅提升,但同時也要擔心 overfitting 就是了
data = df_data.copy()
fitModel(data,['Pclass','ConnectedSurvival'],['Sex'])
Base oob score : 0.87205
探索性分析及特徵工程做完後,我們將所有認為有效的特徵加入模型訓練
data = df_data.copy()
fitModel(data,['Pclass','FareRank','AgeRank','ConnectedSurvival'],['Sex','WithFamily','Embarked'])
Base oob score : 0.86308
最後用這些特徵跟訓練結果去對測試資料進行預測,匯出結果
data = df_data.copy()
submitResult(data,['Pclass','FareRank','AgeRank','ConnectedSurvival'],['Sex','WithFamily','Embarked'])
將結果上傳至 Kaggle後,獲得 0.78 的準確率(跟我們的分數差很大,果然有 overfitting )
to be continued...