如何在 Titanic Kaggle Challenge 中獲得0.8134分
由 Kaggle 主持的 泰坦尼克號生存挑戰賽 是一項競賽,其目標是基於一組描述乘客的變數,例如他的年齡,性別或乘客在船上的等級,來預測特定乘客是生存或死亡。
我一直在玩 Titanic 資料集,我最近在公共排行榜上獲得了0.8134的準確度分數。當我寫這篇文章時,我在所有 Kagglers 中排名前4%。
這篇文章將分享我的解決方案。
為了使本教程更具“學術性”以便任何人都能從中受益,我將首先從探索性資料分析(EDA)開始,然後我將遵循特徵工程並最終呈現我設定的預測模型。
<!--more-->
在這個 jupyter 筆記本中,我將在每個級別的管道中使用 Python。
本教程涉及的主要庫是:
- Pandas 用於資料操作和接入(ingestion)
- Matplotlib 和 seaborn 用於資料視覺化
- Numpy 用於多維陣列計算
- sklearn 用於機器學習和預測建模
安裝過程
安裝這些軟體包的一種非常簡單的方法是下載並安裝 Conda ,它是將以上所有包封裝起來的發行版。此發行版適用於所有平臺(Windows,Linux 和 Mac OSX)。
特別注意
這是我作為博主和機器學習從業者的第一次嘗試。
如果您對我所做的程式碼或假設有疑問,請不要猶豫,在下面的評論部分發表評論。
如果您對如何改進膝上型電腦也有建議,請聯絡我。
本教程可在我的 github 帳戶中找到。
譯者注:本翻譯在 qiwihui 下。
希望你已經在計算機上設定了所有內容。讓我們開始吧。
I - 探索性資料分析
正如在不同的資料專案中,我們將首先開始深入研究資料並建立我們的第一個直覺。
在本節中,我們將做四件事。
- 資料提取:我們將載入資料集並首先檢視它。
- 清潔:我們將填寫缺失值。
- 繪圖:我們將建立一些有趣的圖表,這些圖表(希望)可以發現數據中的相關性和隱藏的見解。
- 假設:我們將從圖表中提出假設。
我們稍微調整了這款筆記本的風格,以便畫圖居中。
from IPython.core.display import HTML HTML(""" <style> .output_png { display: table-cell; text-align: center; vertical-align: middle; } </style> """);
匯入有用的包。
%matplotlib inline import warnings warnings.filterwarnings('ignore') warnings.filterwarnings('ignore', category=DeprecationWarning) import pandas as pd pd.options.display.max_columns = 100 from matplotlib import pyplot as plt import numpy as np import seaborn as sns import pylab as plot params = { 'axes.labelsize': "large", 'xtick.labelsize': 'x-large', 'legend.fontsize': 20, 'figure.dpi': 150, 'figure.figsize': [25, 7] } plot.rcParams.update(params)
有兩個資料集:訓練集和測試集。
我們將使用訓練集來構建我們的預測模型,用測試集來對其進行評分並生成輸出檔案以在Kaggle評估系統上提交。
我們將在本文末尾看到這個過程是如何完成的。
現在讓我們開始載入訓練集。
data = pd.read_csv('./data/train.csv')
print data.shape
(891, 12)
我們得到:
- 891 行
- 12 列
Pandas 允許你鳥瞰資料。
data.head()
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
1 | 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
2 | 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
3 | 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S |
4 | 5 | 0 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S |
Survived
列是 目標變數 。 如果 Survived
為 1,乘客倖免於難,否則他已經死了。這是我們要預測的變數。
其他變數描述了乘客。 它們是 特徵 。
PassengerId Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
Pandas 允許您對數字特徵進行高階簡單的統計描述。
這可以使用 describe
方法完成。
data.describe()
<div>
PassengerId | Survived | Pclass | Age | SibSp | Parch | Fare | |
---|---|---|---|---|---|---|---|
count | 891.000000 | 891.000000 | 891.000000 | 714.000000 | 891.000000 | 891.000000 | 891.000000 |
mean | 446.000000 | 0.383838 | 2.308642 | 29.699118 | 0.523008 | 0.381594 | 32.204208 |
std | 257.353842 | 0.486592 | 0.836071 | 14.526497 | 1.102743 | 0.806057 | 49.693429 |
min | 1.000000 | 0.000000 | 1.000000 | 0.420000 | 0.000000 | 0.000000 | 0.000000 |
25% | 223.500000 | 0.000000 | 2.000000 | 20.125000 | 0.000000 | 0.000000 | 7.910400 |
50% | 446.000000 | 0.000000 | 3.000000 | 28.000000 | 0.000000 | 0.000000 | 14.454200 |
75% | 668.500000 | 1.000000 | 3.000000 | 38.000000 | 1.000000 | 0.000000 | 31.000000 |
max | 891.000000 | 1.000000 | 3.000000 | 80.000000 | 8.000000 | 6.000000 | 512.329200 |
</div>
count
變數顯示 Age
列中缺少177個值。
一種解決方案是用中值年齡填充空值。我們也可以用平均年齡來估算,但中位數對異常值更為穩健。
data['Age'] = data['Age'].fillna(data['Age'].median())
讓我們看一下結果。
data.describe()
<div>
PassengerId | Survived | Pclass | Age | SibSp | Parch | Fare | |
---|---|---|---|---|---|---|---|
count | 891.000000 | 891.000000 | 891.000000 | 891.000000 | 891.000000 | 891.000000 | 891.000000 |
mean | 446.000000 | 0.383838 | 2.308642 | 29.361582 | 0.523008 | 0.381594 | 32.204208 |
std | 257.353842 | 0.486592 | 0.836071 | 13.019697 | 1.102743 | 0.806057 | 49.693429 |
min | 1.000000 | 0.000000 | 1.000000 | 0.420000 | 0.000000 | 0.000000 | 0.000000 |
25% | 223.500000 | 0.000000 | 2.000000 | 22.000000 | 0.000000 | 0.000000 | 7.910400 |
50% | 446.000000 | 0.000000 | 3.000000 | 28.000000 | 0.000000 | 0.000000 | 14.454200 |
75% | 668.500000 | 1.000000 | 3.000000 | 35.000000 | 1.000000 | 0.000000 | 31.000000 |
max | 891.000000 | 1.000000 | 3.000000 | 80.000000 | 8.000000 | 6.000000 | 512.329200 |
</div>
完美。
我們現在製作一些圖表。讓我們根據性別來看待生存。
data['Died'] = 1 - data['Survived']
data.groupby('Sex').agg('sum')[['Survived', 'Died']].plot(kind='bar', figsize=(25, 7), stacked=True, colors=['g', 'r']);
看起來男性乘客更容易死亡。讓我們繪製相同的圖形,但用比例代替。
data.groupby('Sex').agg('mean')[['Survived', 'Died']].plot(kind='bar', figsize=(25, 7), stacked=True, colors=['g', 'r']);
性別變數似乎是一種歧視性特徵。女性更有可能生存。
現在讓我們將生存與年齡變數聯絡起來。
fig = plt.figure(figsize=(25, 7)) sns.violinplot(x='Sex', y='Age', hue='Survived', data=data, split=True, palette={0: "r", 1: "g"} );
正如我們在上面的圖表中看到並通過以下方式驗證:
- 女性的生存率高於男性,如較大的女性綠色直方圖所示
現在,我們看到:
-
年齡為男性乘客的生存:
- 年輕的男性傾向於生存
* 20至40歲之間的大量乘客死亡
- 年齡似乎沒有對女性生存產生直接影響
以下小提琴情節證實,在遇到威脅的情況下,水手和船長遵守一條舊的行為準則:“ 婦女和兒童優先! ”。
對嗎?
現在讓我們關注每位乘客的票價,看看它如何影響生存。
figure = plt.figure(figsize=(25, 7)) plt.hist([data[data['Survived'] == 1]['Fare'], data[data['Survived'] == 0]['Fare']], stacked=True, color = ['g','r'], bins = 50, label = ['Survived','Dead']) plt.xlabel('Fare') plt.ylabel('Number of passengers') plt.legend();
票價較低的乘客更容易死亡。
換句話說,擁有更昂貴門票,因此更重要的社會地位的乘客似乎首先獲救。
好的,這很好。 現在讓我們將年齡,票價和生存結合在一張圖表上。
plt.figure(figsize=(25, 7)) ax = plt.subplot() ax.scatter(data[data['Survived'] == 1]['Age'], data[data['Survived'] == 1]['Fare'], c='green', s=data[data['Survived'] == 1]['Fare']) ax.scatter(data[data['Survived'] == 0]['Age'], data[data['Survived'] == 0]['Fare'], c='red', s=data[data['Survived'] == 0]['Fare']);
圓圈的大小與票價成正比。
在 x 軸上,我們有年齡,在 y 軸,我們考慮票價。
我們可以觀察不同的叢集:
- x = 20 和 x = 45 之間的大綠點:票價最高的成人
- x = 10 和 x = 45 之間的小紅點,船上較低級別的成年人
- x = 0 和 x = 7 之間的小密集點:這些是被儲存的孩子
事實上,票價與我們在下面的圖表中看到的類別相關。
ax = plt.subplot() ax.set_ylabel('Average fare') data.groupby('Pclass').mean()['Fare'].plot(kind='bar', figsize=(25, 7), ax = ax);
現在讓我們看看登船地點如何影響生存。
fig = plt.figure(figsize=(25, 7)) sns.violinplot(x='Embarked', y='Fare', hue='Survived', data=data, split=True, palette={0: "r", 1: "g"});
似乎登船地點 C 的票價範圍更廣,因此支付最高價格的乘客是那些倖存的乘客。
我們也看到這種情況發生在登船地點 S 而不是登船地點 Q。
現在讓我們停止資料探索並切換到下一部分。
II - 特徵工程
在前一部分中,我們調查了資料並發現了一些有趣的相關性。
在這一部分中,我們將看到如何處理和轉換這些變數,使資料變得可以通過機器學習演算法進行管理。
我們還將建立或“設計”在構建模型時有用的其他功能。
我們將在此過程中看到如何處理文字變數(如乘客姓名)並將此資訊整合到我們的模型中。
為了更加清晰,我們將程式碼分散在單獨的函式中。
但首先,讓我們定義一個列印函式,斷言是否已經處理了一個特徵。
def status(feature): print 'Processing', feature, ': ok'
載入資料
啟動機器學習問題的一個技巧是將訓練集一起附加到測試集。
我們將使用訓練集進行特徵工程以防止資訊洩漏。然後我們將這些變數新增到測試集中。
讓我們載入訓練集和測試集並將它們合在一起。
def get_combined_data(): # reading train data train = pd.read_csv('./data/train.csv') # reading test data test = pd.read_csv('./data/test.csv') # extracting and then removing the targets from the training data targets = train.Survived train.drop(['Survived'], 1, inplace=True) # merging train data and test data for future feature engineering # we'll also remove the PassengerID since this is not an informative feature combined = train.append(test) combined.reset_index(inplace=True) combined.drop(['index', 'PassengerId'], inplace=True, axis=1) return combined
combined = get_combined_data()
讓我們看一下資料的維度。
print combined.shape
(1309, 10)
訓練集和測試集被合併。您可能會注意到總行數(1309)是訓練集和測試集中行數的精確總和。
combined.head()
<div>
Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
2 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
3 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S |
4 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S |
</div>
提取乘客稱謂
在檢視乘客姓名時,人們可能想知道如何處理它們以提取有用的資訊。
如果你仔細看看這些第一個例子:
- Braund, Mr. Owen Harris
- Heikkinen, Miss. Laina
- Oliva y Ocana, Dona. Fermina
- Peter, Master. Michael J
你會注意到每個名字都有一個稱謂!這可能是一個簡單的小姐(Miss.)或太太(Mrs.),但它有時可能像 Master,Sir 或 Dona 那樣更復雜。在這種情況下,我們可以通過簡單地解析稱謂並提取標題並轉換為二進位制變數來引入有關社會地位的其他資訊。
讓我們看看我們將如何在下面的函式中執行此操作。
讓我們先來看看在訓練集中有什麼不同的稱謂。
titles = set() for name in data['Name']: titles.add(name.split(',')[1].split('.')[0].strip())
print titles
set(['Sir', 'Major', 'the Countess', 'Don', 'Mlle', 'Capt', 'Dr', 'Lady', 'Rev', 'Mrs', 'Jonkheer', 'Master', 'Ms', 'Mr', 'Mme', 'Miss', 'Col'])
Title_Dictionary = { "Capt": "Officer", "Col": "Officer", "Major": "Officer", "Jonkheer": "Royalty", "Don": "Royalty", "Sir" : "Royalty", "Dr": "Officer", "Rev": "Officer", "the Countess":"Royalty", "Mme": "Mrs", "Mlle": "Miss", "Ms": "Mrs", "Mr" : "Mr", "Mrs" : "Mrs", "Miss" : "Miss", "Master" : "Master", "Lady" : "Royalty" } def get_titles(): # we extract the title from each name combined['Title'] = combined['Name'].map(lambda name:name.split(',')[1].split('.')[0].strip()) # a map of more aggregated title # we map each title combined['Title'] = combined.Title.map(Title_Dictionary) status('Title') return combined
此函式解析名稱並提取稱謂。 然後,它將稱謂對映到稱謂類別。
我們選擇:
- Officer
- Royalty
- Mr
- Mrs
- Miss
- Master
讓我們執行一下!
combined = get_titles()
Processing Title : ok
combined.head()
<div>
Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | Title | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S | Mr |
1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C | Mrs |
2 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S | Miss |
3 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S | Mrs |
4 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S | Mr |
</div>
讓我們檢查一下稱謂是否填寫正確。
combined[combined['Title'].isnull()]
<div>
Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | Title | |
---|---|---|---|---|---|---|---|---|---|---|---|
1305 | 1 | Oliva y Ocana, Dona. Fermina | female | 39.0 | 0 | 0 | PC 17758 | 108.9 | C105 | C | NaN |
</div>
在1305行中確實存在 NaN 值。實際上相應的名稱是 Oliva y Ocana, **Dona**. Fermina
。
在訓練資料集中沒有遇到這個標題。
很好,現在我們有一個名為 Title 的附加列來包含這些資訊。
處理年齡(Age)
我們在第一部分中看到 Age
變數缺少177個值。這是一個很大的數字(約佔資料集的13%)。簡單地用平均年齡或中位年齡替換它們可能不是最佳解決方案,因為年齡可能因乘客的類別和類別而不同。
為了理解原因,讓我們按性別(Sex),稱謂(Title)和乘客類(Pclass)對我們的資料集進行分組,併為每個子集計算中位數年齡。
為了避免測試集中的資料洩漏,我們使用訓練集填寫訓練中的缺失年齡,並且我們使用從訓練集計算的值來填充測試集中的年齡。
訓練級中缺少的年齡數
print combined.iloc[:891].Age.isnull().sum()
測試集中缺少的年齡數
print combined.iloc[891:].Age.isnull().sum()
grouped_train = combined.iloc[:891].groupby(['Sex','Pclass','Title']) grouped_median_train = grouped_train.median() grouped_median_train = grouped_median_train.reset_index()[['Sex', 'Pclass', 'Title', 'Age']]
grouped_median_train.head()
<div>
Sex | Pclass | Title | Age | |
---|---|---|---|---|
0 | female | 1 | Miss | 30.0 |
1 | female | 1 | Mrs | 40.0 |
2 | female | 1 | Officer | 49.0 |
3 | female | 1 | Royalty | 40.5 |
4 | female | 2 | Miss | 24.0 |
</div>
此 dataframe 將幫助我們根據不同的標準估算缺失的年齡值。
檢視中位年齡列,看看這個值如何根據 Sex
, Pclass
和 Title
組合在一起。
例如:
- 如果乘客是女性,則來自 Pclass 1 和來自王室(royalty),中位年齡為40.5歲。
- 如果乘客是男性,來自 Pclass 3,擁有 Mr 稱謂,則年齡中位數為26歲。
讓我們建立一個函式,根據這些不同的屬性填充 組合 中的缺失年齡。
def fill_age(row): condition = ( (grouped_median_train['Sex'] == row['Sex']) & (grouped_median_train['Title'] == row['Title']) & (grouped_median_train['Pclass'] == row['Pclass']) ) return grouped_median_train[condition]['Age'].values[0] def process_age(): global combined # a function that fills the missing values of the Age variable combined['Age'] = combined.apply(lambda row: fill_age(row) if np.isnan(row['Age']) else row['Age'], axis=1) status('age') return combined
combined = process_age()
Processing age : ok
完美。失蹤的年齡已被取代。
但是,我們注意到票價(Fare)中缺少1個值,登船位置(Embarked)有兩個缺失值,而船艙位置(Cabin)有很多缺失值。我們稍後會處理這些變數。
我們現在處理名字。
def process_names(): global combined # we clean the Name variable combined.drop('Name', axis=1, inplace=True) # encoding in dummy variable titles_dummies = pd.get_dummies(combined['Title'], prefix='Title') combined = pd.concat([combined, titles_dummies], axis=1) # removing the title variable combined.drop('Title', axis=1, inplace=True) status('names') return combined
此函式會刪除 Name
列,我們不再使用它,因為我們建立了 Title
列。
然後我們使用虛擬編碼(dummy encoding)對稱謂值進行編碼。
您可以瞭解虛擬編碼以及如何在 Pandas 中輕鬆完成此操作。
combined = process_names()
Processing names : ok
combined.head()
<div>
Pclass | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | Title_Master | Title_Miss | Title_Mr | Title_Mrs | Title_Officer | Title_Royalty | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 3 | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S | 0 | 0 | 1 | 0 | 0 | 0 |
1 | 1 | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C | 0 | 0 | 0 | 1 | 0 | 0 |
2 | 3 | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S | 0 | 1 | 0 | 0 | 0 | 0 |
3 | 1 | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S | 0 | 0 | 0 | 1 | 0 | 0 |
4 | 3 | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S | 0 | 0 | 1 | 0 | 0 | 0 |
</div>
如你看到的 :
- 不再有名字特徵。
-
出現了新的變數(Title_X)。這些特徵是二進位制的。
- 例如,如果 Title_Mr = 1,則相應的稱謂為 Mr。
處理票價(Fare)
讓我們通過在訓練集上計算的平均票價估算缺失的票價值。
def process_fares(): global combined # there's one missing fare value - replacing it with the mean. combined.Fare.fillna(combined.iloc[:891].Fare.mean(), inplace=True) status('fare') return combined
此函式用平均值替換一個缺失的票價(Fare)值。
combined = process_fares()
Processing fare : ok
處理登船位置(Embarked)
def process_embarked(): global combined # two missing embarked values - filling them with the most frequent one in the trainset(S) combined.Embarked.fillna('S', inplace=True) # dummy encoding embarked_dummies = pd.get_dummies(combined['Embarked'], prefix='Embarked') combined = pd.concat([combined, embarked_dummies], axis=1) combined.drop('Embarked', axis=1, inplace=True) status('embarked') return combined
此函式用最常用的 Embarked
值替換了兩個缺失的 Embarked
值。
combined = process_embarked()
Processing embarked : ok
combined.head()
<div>
Pclass | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Title_Master | Title_Miss | Title_Mr | Title_Mrs | Title_Officer | Title_Royalty | Embarked_C | Embarked_Q | Embarked_S | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 3 | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
1 | 1 | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 |
2 | 3 | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
3 | 1 | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
4 | 3 | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
</div>
處理船艙位置(Cabin)
train_cabin, test_cabin = set(), set() for c in combined.iloc[:891]['Cabin']: try: train_cabin.add(c[0]) except: train_cabin.add('U') for c in combined.iloc[891:]['Cabin']: try: test_cabin.add(c[0]) except: test_cabin.add('U')
print train_cabin
set(['A', 'C', 'B', 'E', 'D', 'G', 'F', 'U', 'T'])
print test_cabin
set(['A', 'C', 'B', 'E', 'D', 'G', 'F', 'U'])
我們在測試集中沒有任何不存在於訓練集中的船艙位置字母。
def process_cabin(): global combined # replacing missing cabins with U (for Uknown) combined.Cabin.fillna('U', inplace=True) # mapping each Cabin value with the cabin letter combined['Cabin'] = combined['Cabin'].map(lambda c: c[0]) # dummy encoding ... cabin_dummies = pd.get_dummies(combined['Cabin'], prefix='Cabin') combined = pd.concat([combined, cabin_dummies], axis=1) combined.drop('Cabin', axis=1, inplace=True) status('cabin') return combined
此函式將 NaN
值替換為 U(表示 Unknow )。 然後它將每個 Cabin
值對映到第一個字母。
然後它再次使用虛擬編碼對艙位值進行編碼。
combined = process_cabin()
Processing cabin : ok
好了,沒有缺失值了。
combined.head()
<div>
Pclass | Sex | Age | SibSp | Parch | Ticket | Fare | Title_Master | Title_Miss | Title_Mr | Title_Mrs | Title_Officer | Title_Royalty | Embarked_C | Embarked_Q | Embarked_S | Cabin_A | Cabin_B | Cabin_C | Cabin_D | Cabin_E | Cabin_F | Cabin_G | Cabin_T | Cabin_U | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 3 | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
1 | 1 | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 3 | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
3 | 1 | female | 35.0 | 1 | 0 | 113803 | 53.1000 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
4 | 3 | male | 35.0 | 0 | 0 | 373450 | 8.0500 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
</div>
處理性別(Sex)
def process_sex(): global combined # mapping string values to numerical one combined['Sex'] = combined['Sex'].map({'male':1, 'female':0}) status('Sex') return combined
此函式將字串值 male
和 female
分別對映到1和0。
combined = process_sex()
Processing Sex : ok
處理乘客等級(Pclass)
def process_pclass(): global combined # encoding into 3 categories: pclass_dummies = pd.get_dummies(combined['Pclass'], prefix="Pclass") # adding dummy variable combined = pd.concat([combined, pclass_dummies],axis=1) # removing "Pclass" combined.drop('Pclass',axis=1,inplace=True) status('Pclass') return combined
此函式使用虛擬編碼對 Pclass(1,2,3)的值進行編碼。
combined = process_pclass()
Processing Pclass : ok
處理船票號碼(Ticket)
讓我們首先看看我們的資料集中不同的船票號碼字首
def cleanTicket(ticket): ticket = ticket.replace('.', '') ticket = ticket.replace('/', '') ticket = ticket.split() ticket = map(lambda t : t.strip(), ticket) ticket = list(filter(lambda t : not t.isdigit(), ticket)) if len(ticket) > 0: return ticket[0] else: return 'XXX'
tickets = set() for t in combined['Ticket']: tickets.add(cleanTicket(t))
print len(tickets)
def process_ticket(): global combined # a function that extracts each prefix of the ticket, returns 'XXX' if no prefix (i.e the ticket is a digit) def cleanTicket(ticket): ticket = ticket.replace('.','') ticket = ticket.replace('/','') ticket = ticket.split() ticket = map(lambda t : t.strip(), ticket) ticket = filter(lambda t : not t.isdigit(), ticket) if len(ticket) > 0: return ticket[0] else: return 'XXX' # Extracting dummy variables from tickets: combined['Ticket'] = combined['Ticket'].map(cleanTicket) tickets_dummies = pd.get_dummies(combined['Ticket'], prefix='Ticket') combined = pd.concat([combined, tickets_dummies], axis=1) combined.drop('Ticket', inplace=True, axis=1) status('Ticket') return combined
combined = process_ticket()
Processing Ticket : ok
處理家庭
這部分包括根據家庭的大小建立新變數(大小是我們建立的另一個變數)。
這種新變數的建立是在一個現實的假設下完成的:大家庭聚集在一起,因此他們比單獨旅行的人更有可能獲救。
def process_family(): global combined # introducing a new feature : the size of families (including the passenger) combined['FamilySize'] = combined['Parch'] + combined['SibSp'] + 1 # introducing other features based on the family size combined['Singleton'] = combined['FamilySize'].map(lambda s: 1 if s == 1 else 0) combined['SmallFamily'] = combined['FamilySize'].map(lambda s: 1 if 2 <= s <= 4 else 0) combined['LargeFamily'] = combined['FamilySize'].map(lambda s: 1 if 5 <= s else 0) status('family') return combined
此函式引入了4個新特徵:
FamilySize Sigleton SmallFamily LargeFamily
combined = process_family()
Processing family : ok
print combined.shape
(1309, 67)
最後我們得到了67個特徵。
combined.head()
<div>
Sex | Age | SibSp | Parch | Fare | Title_Master | Title_Miss | Title_Mr | Title_Mrs | Title_Officer | Title_Royalty | Embarked_C | Embarked_Q | Embarked_S | Cabin_A | Cabin_B | Cabin_C | Cabin_D | Cabin_E | Cabin_F | Cabin_G | Cabin_T | Cabin_U | Pclass_1 | Pclass_2 | Pclass_3 | Ticket_A | Ticket_A4 | Ticket_A5 | Ticket_AQ3 | Ticket_AQ4 | Ticket_AS | Ticket_C | Ticket_CA | Ticket_CASOTON | Ticket_FC | Ticket_FCC | Ticket_Fa | Ticket_LINE | Ticket_LP | Ticket_PC | Ticket_PP | Ticket_PPP | Ticket_SC | Ticket_SCA3 | Ticket_SCA4 | Ticket_SCAH | Ticket_SCOW | Ticket_SCPARIS | Ticket_SCParis | Ticket_SOC | Ticket_SOP | Ticket_SOPP | Ticket_SOTONO2 | Ticket_SOTONOQ | Ticket_SP | Ticket_STONO | Ticket_STONO2 | Ticket_STONOQ | Ticket_SWPP | Ticket_WC | Ticket_WEP | Ticket_XXX | FamilySize | Singleton | SmallFamily | LargeFamily | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 22.0 | 1 | 0 | 7.2500 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 1 | 0 |
1 | 0 | 38.0 | 1 | 0 | 71.2833 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 1 | 0 |
2 | 0 | 26.0 | 0 | 0 | 7.9250 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
3 | 0 | 35.0 | 1 | 0 | 53.1000 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 2 | 0 | 1 | 0 |
4 | 1 | 35.0 | 0 | 0 | 8.0500 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 |
</div>
III - 模型
在這一部分中,我們根據我們建立的特徵建立統計模型。您可以將此模型視為一個盒子,它可以處理任何新乘客的資訊,並決定他是否能夠倖存。
有各種各樣的模型可供使用,從邏輯迴歸到決策樹,以及更復雜的模型,如隨機森林和梯度提升樹。
我們將使用隨機森林。Random Froests 在 Kaggle 比賽中證明了很高的有效性。
有關為什麼集合方法表現良好的更多詳細資訊,您可以參考這些帖子:
回到我們的問題,我們現在必須:
1.將組合資料集分成訓練集和測試集。
2.使用訓練集建立預測模型。
3.使用訓練集評估模型。
4.使用測試集測試模型,並生成並輸出提交檔案。
請記住,我們必須重複 2 和 3 直到達到可接受的評估分數。
讓我們首先匯入需要用到的函式包。
from sklearn.pipeline import make_pipeline from sklearn.ensemble import RandomForestClassifier from sklearn.ensemble.gradient_boosting import GradientBoostingClassifier from sklearn.feature_selection import SelectKBest from sklearn.model_selection import StratifiedKFold from sklearn.model_selection import GridSearchCV from sklearn.model_selection import cross_val_score from sklearn.feature_selection import SelectFromModel from sklearn.linear_model import LogisticRegression, LogisticRegressionCV
為了評估我們的模型,我們將使用5折交叉驗證(5-fold cross validation),因為它是在比賽排行榜中使用的指標。
為此,我們將定義一個小的評分函式。
def compute_score(clf, X, y, scoring='accuracy'): xval = cross_val_score(clf, X, y, cv = 5, scoring=scoring) return np.mean(xval)
從組合資料集中恢復訓練集和測試集是一項簡單的任務。
def recover_train_test_target(): global combined targets = pd.read_csv('./data/train.csv', usecols=['Survived'])['Survived'].values train = combined.iloc[:891] test = combined.iloc[891:] return train, test, targets
train, test, targets = recover_train_test_target()
特徵選擇
到目前為止,我們已經提出了30多個特徵。這個數字非常大。
在完成特徵工程時,我們通常傾向於通過選擇捕獲基本特徵的“正確”數量的特徵來減少維度。
事實上,特徵選擇帶來許多好處:
- 它減少了資料之間的冗餘
- 它加快了訓練過程
- 它減少過擬合
基於樹的估算器可用於計算特徵重要性,而這些重要性又可用於丟棄不相關的特徵。
clf = RandomForestClassifier(n_estimators=50, max_features='sqrt') clf = clf.fit(train, targets)
讓我們看看每個特徵的重要性。
features = pd.DataFrame() features['feature'] = train.columns features['importance'] = clf.feature_importances_ features.sort_values(by=['importance'], ascending=True, inplace=True) features.set_index('feature', inplace=True)
features.plot(kind='barh', figsize=(25, 25))
<matplotlib.axes._subplots.AxesSubplot at 0x117ff2a10>
正如您可能注意到的那樣,與 Title_Mr
, Age
, Fare
和 Sex
相關聯非常重要。
與 Passenger_Id
也有重要的相關性。
現在讓我們將我們的訓練集和測試集轉換為更緊湊的資料集。
model = SelectFromModel(clf, prefit=True) train_reduced = model.transform(train) print train_reduced.shape
(891, 12)
test_reduced = model.transform(test) print test_reduced.shape
(418, 12)
好極了! 現在我們的特徵減少了很多。
我們將看看我們是否會使用訓練集的減少版或完整版。
讓我們嘗試不同的基礎模型
logreg = LogisticRegression() logreg_cv = LogisticRegressionCV() rf = RandomForestClassifier() gboost = GradientBoostingClassifier() models = [logreg, logreg_cv, rf, gboost]
for model in models: print 'Cross-validation of : {0}'.format(model.__class__) score = compute_score(clf=model, X=train_reduced, y=targets, scoring='accuracy') print 'CV score = {0}'.format(score) print '****'
Cross-validation of : <class 'sklearn.linear_model.logistic.LogisticRegression'> CV score = 0.818195097715 **** Cross-validation of : <class 'sklearn.linear_model.logistic.LogisticRegressionCV'> CV score = 0.81818240172 **** Cross-validation of : <class 'sklearn.ensemble.forest.RandomForestClassifier'> CV score = 0.808183171282 **** Cross-validation of : <class 'sklearn.ensemble.gradient_boosting.GradientBoostingClassifier'> CV score = 0.824917697684 ****
超引數調整
正如建模部分的開頭所提到的,我們將使用隨機森林模型。它可能不是這項任務的最佳模型,但我們將展示如何調整。這項工作可以應用於不同的模型。
隨機森林非常方便。然而,它們會帶有一些引數進行調整,以便為預測任務獲得最佳模型。
要了解有關隨機森林的更多資訊,請參閱此 連結 。
此外,我們將使用全部訓練集。
# turn run_gs to True if you want to run the gridsearch again. run_gs = False if run_gs: parameter_grid = { 'max_depth' : [4, 6, 8], 'n_estimators': [50, 10], 'max_features': ['sqrt', 'auto', 'log2'], 'min_samples_split': [2, 3, 10], 'min_samples_leaf': [1, 3, 10], 'bootstrap': [True, False], } forest = RandomForestClassifier() cross_validation = StratifiedKFold(n_splits=5) grid_search = GridSearchCV(forest, scoring='accuracy', param_grid=parameter_grid, cv=cross_validation, verbose=1 ) grid_search.fit(train, targets) model = grid_search parameters = grid_search.best_params_ print('Best score: {}'.format(grid_search.best_score_)) print('Best parameters: {}'.format(grid_search.best_params_)) else: parameters = {'bootstrap': False, 'min_samples_leaf': 3, 'n_estimators': 50, 'min_samples_split': 10, 'max_features': 'sqrt', 'max_depth': 6} model = RandomForestClassifier(**parameters) model.fit(train, targets)
現在通過掃描超引數的幾個組合來構建模型,我們可以生成一個輸出檔案以在 Kaggle 上提交。
output = model.predict(test).astype(int) df_output = pd.DataFrame() aux = pd.read_csv('./data/test.csv') df_output['PassengerId'] = aux['PassengerId'] df_output['Survived'] = output df_output[['PassengerId','Survived']].to_csv('./predictions/gridsearch_rf.csv', index=False)
[BONUS] 混合不同模型
我沒有親自上傳基於模型混合的提交,但這是你可以這麼做:
trained_models = [] for model in models: model.fit(train, targets) trained_models.append(model) predictions = [] for model in trained_models: predictions.append(model.predict_proba(test)[:, 1]) predictions_df = pd.DataFrame(predictions).T predictions_df['out'] = predictions_df.mean(axis=1) predictions_df['PassengerId'] = aux['PassengerId'] predictions_df['out'] = predictions_df['out'].map(lambda s: 1 if s >= 0.5 else 0) predictions_df = predictions_df[['PassengerId', 'out']] predictions_df.columns = ['PassengerId', 'Survived']
predictions_df.to_csv('./predictions/blending_base_models.csv', index=False)
為了獲得良好的混合提交,基本模型應該是不同的,並且它們的相關性是不相關的。
IV - 結論
在本文中,我們探討了 Kaggle 帶給我們的一個有趣的資料集。
我們瀏覽了資料科學管道的基本要點:
- 資料探索和視覺化:制定假設的第一步
- 資料清理
- 特徵工程
- 特徵選擇
- 超引數調整
- 提交
- 混合
如果您想測試和使用它,可以將此部落格下載為筆記本: 我的 github repo
譯者注:此中文翻譯地址為: qiwihui 的 github repo
關於這一挑戰的文章很多,所以顯然還有改進的餘地。
以下是我建議的後續步驟:
- 挖掘更多資料並最終構建新特徵。
- 嘗試不同的模型:邏輯迴歸,Gradient Boosted Tree,XGboost 等。
- 嘗試整合學習技巧(堆疊)
- 執行 auto-ML 框架
如果你能找到改善我的解決方案的方法,我會非常高興。這可以讓我更新文章,絕對給你信任。所以請隨時發表評論。