鐵達尼號乘客生還率預測

匯入套件與資料

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()
Out[3]:
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 筆資料是因為只有訓練資料才有提供結果,測試資料須用訓練後的模型進行預設

In [4]:
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

探索性分析

為了決定對哪些特徵進行預處理,最後輸入模型進行訓練,我們可以先進行探索性分析,了解各欄位特性。接下來觀察各欄位特徵:

Pclass (艙等)

透過 groupby( ) 可以知道整艘船有 3 種艙等,取各艙等的平均票價可發現票價差異之大(可以理解成頭等艙、商務艙、經濟艙的概念)

In [5]:
df_data.groupby('Pclass').mean().round(2)['Fare']
Out[5]:
Pclass
1    87.51
2    21.18
3    13.30
Name: Fare, dtype: float64

將艙等與乘客生還率畫成圖比對,可以發現艙等等級愈高,生還率愈高,可以理解為 VIP 乘客有較高的優先權獲救,或是高等艙有較好的逃生設備或離逃生口較近;無論如何都挺現實跟合理的,畢竟人家付的錢比較多

In [6]:
pd.crosstab(df_data['Pclass'],df_data['Survived']).plot(kind="bar")
Out[6]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1e75da90>

Sex (性別)

性別的部分,由圖可知女性生還率遠高於男性,這部分也很合理,救難時女士跟小孩優先,電影都有演(無誤)

In [7]:
pd.crosstab(df_data['Sex'],df_data['Survived']).plot(kind="bar")
Out[7]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1e66fa58>

「艙等」跟「性別」乍看正好是兩個很有指標性的特徵,剛好一個有等級之分,另一個沒有,可分別作為連續型與非連續型的類別資料輸入,我們以這兩個特徵為基準,之後依序加入其他特徵,看準確率是否提升,來決定是否加入

In [8]:
data = df_data.copy()
fitModel(data,['Pclass'],['Sex'])
Base oob score : 0.72278

SibSp (兄弟姐妹/配偶)

SibSp 這個欄位代表的是有多久兄弟姐妹跟配偶一起登船,我們首先畫張圖看一下,發現獨自登船的生還率相較有人陪伴的乘客低許多,推測手足或配偶在遇難時會互相協助,提高生還率

In [9]:
pd.crosstab(df_data['SibSp'],df_data['Survived']).plot(kind="bar")
Out[9]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1eb156d8>

另外一種思路是:看圖表會發現只有欄位是 1 時生還率特別高,之後欄位值愈高,生還率仍然愈低,可推測欄位值為 1 時很大機率是與配偶一起登船;因此須想辦法將配偶與兄弟姐妹的狀況分離,以獲得更高的準確率(這次 DEMO 沒做這個分析)

Parch (父母/子女)

Parch 這個欄位代表與父母、兒女一起登船的人數,同樣畫張圖看一下,獨自登船的乘客生還率較低,而與親人一起登船的乘客生還率較高

In [10]:
pd.crosstab(df_data['Parch'],df_data['Survived']).plot(kind="bar")
Out[10]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1ec37cc0>

這邊還可以做更細的分析:假設我們認知的情境是正確的:女性跟小孩有較高的優先權登上救生艇,那麼其家人一起登上救生艇的機率是否會比其他乘客更高?將父母或子女的性別跟年齡整理後作為其他特徵輸入也許可行(這次 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()
Out[11]:
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

從圖的結果來看,有無家人同行對乘客生還率有很大影響

In [12]:
pd.crosstab(df_data['WithFamily'],df_data['Survived']).plot(kind="bar")
Out[12]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1ed443c8>

將此特徵加入模型,分數比一開始提升許多

In [13]:
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','WithFamily'])
Base oob score : 0.80247

Ticket (票號)

Ticket 這個欄位為乘客所持船票的票號,若票號重複表示這幾位乘客是一起購票,我們透過 groupby( ) 看看情況

In [14]:
df_sameTicket = df_data.groupby('Ticket').count()
df_sameTicket.head()
Out[14]:
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

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()
Out[15]:
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

畫圖來看,與朋友同行的乘客生還率較高,感覺是個指標

In [16]:
pd.crosstab(df_data['WithFriend'],df_data['Survived']).plot(kind="bar")
Out[16]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1eec60f0>

然而當我們實際把新特徵加進模型,卻發現分數降低了,表示可能有其他我們未考慮到的情況,因此暫時放棄這個特徵

In [17]:
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','WithFriend'])
Base oob score : 0.72840

Fare (票價)

Fare 這個欄位表示票價,在進行分析前,我們得先處理缺值問題。首先找出缺值的那筆資料

In [18]:
df_data[df_data['Fare'].isnull()]
Out[18]:
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

雖然機率不高,但我們先看看是否有其他乘客持有相同票號,理論上他們的票價應該會相同

In [19]:
df_data[df_data['Ticket']=='3701']
Out[19]:
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)上船的乘客,找出符合這些條件的乘客

In [20]:
PclassFare = df_data.groupby(['Pclass','Embarked','Sex']).mean()['Fare']
PclassFare.get(3).round(2)
Out[20]:
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

補值之後,確認下是否還有漏網之魚

In [21]:
df_data['Fare'] = df_data['Fare'].fillna(PclassFare.get(3)['S']['male'].round(2))
df_data[df_data['Fare'].isnull()]
Out[21]:
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked WithFamily WithFriend

看起來都有值了,最後將票價的分佈圖畫出來,可以發現分佈相當集中,但極值相距很遠

In [22]:
sns.distplot(df_data['Fare'])
df_data.describe()['Fare'].round(2)
Out[22]:
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

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)
Out[23]:
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

2 號艙等

In [24]:
sns.distplot(df_data[df_data['Pclass']==2]['Fare'])
df_data[df_data['Pclass']==2].describe()['Fare'].round(2)
Out[24]:
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

3 號艙等

In [25]:
sns.distplot(df_data[df_data['Pclass']==3]['Fare'])
df_data[df_data['Pclass']==3].describe()['Fare'].round(2)
Out[25]:
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% 的人在,並讓各區間的人數差異不會太大

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()
Out[26]:
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

將劃分完的票價等級作為新的特徵並畫圖,可以發現票價等級愈高,乘客生還率也愈高(畢竟這也代表艙等愈高級的意思)

In [27]:
pd.crosstab(df_data['FareRank'],df_data['Survived']).plot(kind="bar")
Out[27]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f422898>

將新特徵輸入模型,得到的分數比基準高,可視為一個有效特徵

In [28]:
data = df_data.copy()
fitModel(data,['Pclass','FareRank'],['Sex'])
Base oob score : 0.77666

Embarked (登船港口)

Embarked 欄位紀錄乘客是由哪個港口登船,鐵達尼號共有 3 個登船港口(C:瑟堡 Q:皇后鎮 S: 南安普敦),先確認是否有資料缺值

In [29]:
df_data[df_data['Embarked'].isnull()]
Out[29]:
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

發現有兩筆資料缺值,並且持有相同票號,感覺是找不到其他持有相同票號的乘客了,但還是試一下

In [30]:
df_data[df_data['Ticket']=='113572']
Out[30]:
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 的乘客最多從南安普敦上船、其次是瑟堡,先將皇后鎮踢除

In [31]:
pd.crosstab(df_data['Embarked'],df_data['FareRank']).plot(kind="bar")
Out[31]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1eea4940>

接著再看艙等,1 號艙等的乘客在瑟堡與南安普敦的比例不相上下

In [32]:
pd.crosstab(df_data['Embarked'],df_data['Pclass']).plot(kind="bar")
Out[32]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f5ebf98>

觀察性別,女性乘客較多是在南安普敦上船

In [33]:
pd.crosstab(df_data['Embarked'],df_data['Sex']).plot(kind="bar")
Out[33]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f6e2828>

我們將這兩筆缺值的資料填上南安普敦,最後再確認一下 Embarked 這個欄位是否還有缺值

In [34]:
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

把圖畫出來看一下,瑟堡的乘客生還率較高、其次是皇后鎮,最後則是南安普敦

In [35]:
pd.crosstab(df_data['Embarked'],df_data['Survived']).plot(kind="bar")
Out[35]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f7d87b8>

其實若做交叉比對,可發現從哪個港口登船與乘客的性別、年齡、票價等級皆有關聯,因此會間接影響到生還率。將此特徵加入模型,並觀察分數

In [36]:
data = df_data.copy()
fitModel(data,['Pclass'],['Sex','Embarked'])
Base oob score : 0.81145

Age (年齡)

接著來看 Age 這個欄位,同樣得先處理缺值問題。我們首先看一下有年齡資料的乘客分佈

In [37]:
data_withAge = df_data[np.isnan(df_data['Age'])==False]
plt.hist(data_withAge['Age'])
Out[37]:
(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 號艙等同時也是影響乘客生還率的一個重要指標,因此若補值補得太過隨便,很有可能導致資料失真

In [38]:
data_withoutAge = df_data[np.isnan(df_data['Age'])==True]
pd.crosstab(data_withoutAge['Pclass'],data_withoutAge['Survived']).plot(kind="bar")
Out[38]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f9899b0>

我們把所有能用上的共同特徵都用上,讓年齡的誤差盡可能縮小

In [39]:
df_groupAge = data_withAge.groupby(['Pclass','Sex','Embarked']).describe().round(2)['Age']
df_groupAge
Out[39]:
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 這個欄位

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)
Out[40]:
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

將補值後的年齡分佈圖畫出來

In [41]:
sns.distplot(df_data['NewAge'])
df_data['NewAge'].describe().round(2)
Out[41]:
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

AgeRank (年齡等級)

同樣地,我們要將年齡資料做適當切分,這裡我們將生還者與罹難者的年齡分佈畫出來

生還者

In [42]:
sns.distplot(data_withAge[data_withAge['Survived']==1]['Age'])
data_withAge[data_withAge['Survived']==1]['Age'].describe().round(2)
Out[42]:
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

罹難者

In [43]:
sns.distplot(data_withAge[data_withAge['Survived']==0]['Age'])
data_withAge[data_withAge['Survived']==0]['Age'].describe().round(2)
Out[43]:
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 歲之間有很大的差異,接著是中間的峰值,在罹難者這邊有稍微往左偏的趨勢,為了捕捉這些變化,我們將年齡切成四個區段

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()
Out[44]:
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

最後畫個圖比對一下,首先是未成年乘客的生還率最高,其次是介於青年與老年中間的人士,青年的生還率最低

In [45]:
pd.crosstab(df_data['AgeRank'],df_data['Survived']).plot(kind="bar")
Out[45]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a1f37d160>

將處理後的特徵輸入模型,分數比基準高,應該可視為特徵之一

In [46]:
data = df_data.copy()
fitModel(data,['Pclass','AgeRank'],['Sex'])
Base oob score : 0.79461

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()
<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

結論:我想多了。現有資料不支持這個假設,放棄這個欄位

ConnectedSurvival (生還關聯性)

最後一個特徵,我們直接拿已有的乘客生還率來當指標

首先我們找出持有相同票號的乘客,這些人可能是家人、可能是配偶,也可能是朋友,或是沒那麼熟的朋友(?),總之有某種關聯就對了。剩下的概念就是:假設與我關聯的這些人都生還了,那麼我的生還率應該很高(不能只有我死啊喂);反之如果其他人都死了,那我的生還率也就相對低了

In [48]:
df_survival = df_data.groupby('Ticket').describe()['Survived']
df_survival.head()
Out[48]:
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 的基本值(半死半活)

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()
Out[49]:
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 就是了

In [50]:
data = df_data.copy()
fitModel(data,['Pclass','ConnectedSurvival'],['Sex'])
Base oob score : 0.87205

Submit

探索性分析及特徵工程做完後,我們將所有認為有效的特徵加入模型訓練

In [51]:
data = df_data.copy()
fitModel(data,['Pclass','FareRank','AgeRank','ConnectedSurvival'],['Sex','WithFamily','Embarked'])
Base oob score : 0.86308

最後用這些特徵跟訓練結果去對測試資料進行預測,匯出結果

In [52]:
data = df_data.copy()
submitResult(data,['Pclass','FareRank','AgeRank','ConnectedSurvival'],['Sex','WithFamily','Embarked'])

將結果上傳至 Kaggle後,獲得 0.78 的準確率(跟我們的分數差很大,果然有 overfitting )

to be continued...