之前有寫(xiě)過(guò)一篇關(guān)于Titanic比賽的簡(jiǎn)書(shū),這幾天上kaggle-Titanic的kernels在MostVost找了一篇排第一的kernels來(lái)看,參考鏈接,這個(gè)Kernels在模型方面做得特別好,所以,另寫(xiě)一篇簡(jiǎn)書(shū)作為總結(jié)。
流程
1.觀察數(shù)據(jù),我們要對(duì)數(shù)據(jù)有所了解,可以參考我的簡(jiǎn)書(shū)
2.特征工程以及數(shù)據(jù)清洗
3.跑模型
代碼分析
首先,導(dǎo)入我們需要用到的庫(kù)
import pandas as pd
import numpy as np
from sklearn.cross_validation import KFold
import re
import plotly.graph_objs as go
import plotly.offline as py
from sklearn.ensemble import (RandomForestClassifier, AdaBoostClassifier,
GradientBoostingClassifier, ExtraTreesClassifier)
from sklearn.svm import SVC
import xgboost as xgb
import warnings
warnings.filterwarnings('ignore') # 忽略warning
pd.set_option('display.max_columns', None) # 輸出結(jié)果顯示全部列
然后,導(dǎo)入數(shù)據(jù)
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
PassengerId = test['PassengerId']
full_data = [train, test]
接下來(lái),我們可以查看我們的數(shù)據(jù)
# 查看train集的數(shù)據(jù)
print(train.describe()) # 查看描述性統(tǒng)計(jì),只能看數(shù)值型數(shù)據(jù)。
print(train.info()) # 查看數(shù)據(jù)的信息
# print(train.head()) # 查看train的前n行數(shù)據(jù),默認(rèn)為前5行


從圖上我們可以看到,其中有5列不是數(shù)值型的,我們需要對(duì)其進(jìn)行轉(zhuǎn)換成數(shù)值,而且Age、Cabin這兩列是有缺失值的,我們要對(duì)其進(jìn)行填充或者丟棄。
特征工程以及數(shù)據(jù)清洗
添加一些新的特征
# 添加新的特征,名字的長(zhǎng)度
train['Name_length'] = train['Name'].apply(len)
test['Name_length'] = test['Name'].apply(len)
# 乘客在船上是否有船艙
train['Has_Cabin'] = train["Cabin"].apply(lambda x: 0 if type(x) == float else 1)
test['Has_Cabin'] = test["Cabin"].apply(lambda x: 0 if type(x) == float else 1)
# 結(jié)合SibSp和Parch創(chuàng)建新的特性FamilySize
for dataset in full_data:
dataset['FamilySize'] = dataset['SibSp'] + dataset['Parch'] + 1
基于特征FamilySize創(chuàng)建新的特征IsAlone,因?yàn)橐粋€(gè)人的話,顧慮沒(méi)有那么多,只需要管好自己,生存的幾率會(huì)大點(diǎn),其中又分‘male’和‘female’,因?yàn)槲矣浀秒娪爸惺怯羞@樣的一句臺(tái)詞“讓女人和小孩先走”,所以,我們有理由相信,女性的生存率會(huì)比男性的要高。
for dataset in full_data:
dataset['IsAlone'] = 0
dataset.loc[(dataset['FamilySize'] == 1) & (dataset['Sex'] == 'male'), 'IsAlone'] = 1
dataset.loc[(dataset['FamilySize'] == 1) & (dataset['Sex'] == 'female'), 'IsAlone'] = 2
通過(guò)name,添加特征Title
# 定義從乘客名中提取新的特征[Title]的函數(shù)
def get_title(name):
title_search = re.search(' ([A-Za-z]+)\.', name)
# 如果title存在,提取并返回它。
if title_search:
return title_search.group(1)
return ""
# 創(chuàng)建一個(gè)新的特征[Title]
for dataset in full_data:
dataset['Title'] = dataset['Name'].apply(get_title)
# 將所有不常見(jiàn)的Title分組為一個(gè)“Rare”組
for dataset in full_data:
dataset['Title'] = dataset['Title'].replace(
['Lady', 'Countess', 'Capt', 'Col', 'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')
dataset['Title'] = dataset['Title'].replace('Mlle', 'Miss')
dataset['Title'] = dataset['Title'].replace('Ms', 'Miss')
dataset['Title'] = dataset['Title'].replace('Mme', 'Mrs')
缺失值填充
- Embarked只缺了兩個(gè),所以通過(guò)統(tǒng)計(jì)三個(gè)登船地點(diǎn),選出了登船人數(shù)最多的登船地點(diǎn)(s)來(lái)填充。
- Test集的Fare只有一個(gè)缺失,所以用了中位數(shù)來(lái)填充
- Age缺失的比較多,所以在[age_avg - age_std, age_avg + age_std]這個(gè)范圍取值來(lái)填充(其中age_avg是Age的平均值,age_std是Age的標(biāo)準(zhǔn)差)
# 通過(guò)統(tǒng)計(jì)三個(gè)登船地點(diǎn)人數(shù)最多的填充缺失值
for dataset in full_data:
dataset['Embarked'] = dataset['Embarked'].fillna('S')
# 缺失值填充,Test集的Fare有一個(gè)缺失,按中位數(shù)來(lái)填充,以及創(chuàng)建一個(gè)新的特征[CategoricalFare]
for dataset in full_data:
dataset['Fare'] = dataset['Fare'].fillna(train['Fare'].median())
train['CategoricalFare'] = pd.qcut(train['Fare'], 4)
# 缺失值填充,以及創(chuàng)建新的特征[CategoricalAge]
for dataset in full_data:
age_avg = dataset['Age'].mean()
age_std = dataset['Age'].std()
age_null_count = dataset['Age'].isnull().sum()
age_null_random_list = np.random.randint(age_avg - age_std, age_avg + age_std, size=age_null_count)
dataset['Age'][np.isnan(dataset['Age'])] = age_null_random_list
dataset['Age'] = dataset['Age'].astype(int)
通過(guò)Age,創(chuàng)建新的特征,一會(huì)用來(lái)給Age分組
train['CategoricalAge'] = pd.cut(train['Age'], 5)
print(train['CategoricalAge'])

從圖片可以看出,年齡分為了5個(gè)范圍,所以一會(huì)把年齡分為5組(0-4)。
分組以及轉(zhuǎn)換數(shù)值
Sex:把性別轉(zhuǎn)為0和1.
Embarked:把登船地點(diǎn)轉(zhuǎn)為0、1、2.
Fare:把費(fèi)用分為4組
Age:把年齡分為5組
for dataset in full_data:
dataset['Sex'] = dataset['Sex'].map({'female': 0, 'male': 1}).astype(int)
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}
dataset['Title'] = dataset['Title'].map(title_mapping)
dataset['Title'] = dataset['Title'].fillna(0)
dataset['Embarked'] = dataset['Embarked'].map({'S': 0, 'C': 1, 'Q': 2}).astype(int)
dataset.loc[dataset['Fare'] <= 7.91, 'Fare'] = 0
dataset.loc[(dataset['Fare'] > 7.91) & (dataset['Fare'] <= 14.454), 'Fare'] = 1
dataset.loc[(dataset['Fare'] > 14.454) & (dataset['Fare'] <= 31), 'Fare'] = 2
dataset.loc[dataset['Fare'] > 31, 'Fare'] = 3
dataset['Fare'] = dataset['Fare'].astype(int)
dataset.loc[dataset['Age'] <= 16, 'Age'] = 0
dataset.loc[(dataset['Age'] > 16) & (dataset['Age'] <= 32), 'Age'] = 1
dataset.loc[(dataset['Age'] > 32) & (dataset['Age'] <= 48), 'Age'] = 2
dataset.loc[(dataset['Age'] > 48) & (dataset['Age'] <= 64), 'Age'] = 3
dataset.loc[dataset['Age'] > 64, 'Age'] = 4
特征選擇,丟棄一些不必要的特征
drop_elements = ['PassengerId', 'Name', 'Ticket', 'Cabin', 'SibSp']
train = train.drop(drop_elements, axis=1)
train = train.drop(['CategoricalAge', 'CategoricalFare'], axis=1)
test = test.drop(drop_elements, axis=1)
# print(train.head())
print(train.describe())
# print(train.head())

跑模型
這部分是這個(gè)kernels的重點(diǎn),用的是Stacking。Stacking使用第一級(jí)分類器的預(yù)測(cè)作為對(duì)第二級(jí)模型的訓(xùn)練輸入。我們使用了(RandomForestClassifier, AdaBoostClassifier,GradientBoostingClassifier, ExtraTreesClassifier,Support Vector Classifier)這5個(gè)分類器的預(yù)測(cè)作為下一個(gè)分類器(xgboost)的特征。
在下面的代碼中,我們編寫(xiě)了一個(gè)類SklearnHelper,它允許擴(kuò)展所有Sklearn分類器所共有的內(nèi)置方法(如train、predict和fit)。這消除了冗余,因?yàn)槿绻覀兿胝{(diào)用5個(gè)不同的分類器,就不需要編寫(xiě)相同的方法5次。
# 一些有用的參數(shù),下面會(huì)用到
ntrain = train.shape[0]
ntest = test.shape[0]
SEED = 0
NFOLDS = 5
kf = KFold(ntrain, n_folds=NFOLDS, random_state=SEED)
class SklearnHelper(object):
def __init__(self, clf, seed=0, params=None):
params['random_state'] = seed
self.clf = clf(**params)
def train(self, x_train, y_train):
self.clf.fit(x_train, y_train)
def predict(self, x):
return self.clf.predict(x)
def fit(self, x, y):
return self.clf.fit(x, y)
def feature_importances(self, x, y):
return self.clf.fit(x, y).feature_importances_
def get_oof(clf, x_train, y_train, x_test):
oof_train = np.zeros((ntrain,))
oof_test = np.zeros((ntest,))
oof_test_skf = np.empty((NFOLDS, ntest))
for i, (train_index, test_index) in enumerate(kf):
x_tr = x_train[train_index]
y_tr = y_train[train_index]
x_te = x_train[test_index]
clf.train(x_tr, y_tr)
oof_train[test_index] = clf.predict(x_te)
oof_test_skf[i, :] = clf.predict(x_test)
oof_test[:] = oof_test_skf.mean(axis=0)
return oof_train.reshape(-1, 1), oof_test.reshape(-1, 1)
現(xiàn)在讓我們準(zhǔn)備五個(gè)學(xué)習(xí)模型作為我們的第一級(jí)分類。這些模型都可以通過(guò)Sklearn庫(kù)方便地調(diào)用,如下所示
1.Random Forest classifier
2.Extra Trees classifier
3.AdaBoost classifer
4.Gradient Boosting classifer
5.Support Vector Machine
輸入上述分類器的參數(shù)
# 隨機(jī)森林的參數(shù)
rf_params = {
'n_jobs': -1,
'n_estimators': 100,
'warm_start': True,
#'max_features': 0.2,
'max_depth': 6,
'min_samples_leaf': 2,
'max_features': 'sqrt',
'verbose': 0
}
# Extra Trees的參數(shù)
et_params = {
'n_jobs': -1,
'n_estimators': 100,
#'max_features': 0.5,
'max_depth': 8,
'min_samples_leaf': 2,
'verbose': 0
}
# AdaBoost的參數(shù)
ada_params = {
'n_estimators': 100,
'learning_rate': 0.01
}
# Gradient Boosting的參數(shù)
gb_params = {
'n_estimators': 100,
#'max_features': 0.2,
'max_depth': 5,
'min_samples_leaf': 2,
'verbose': 0
}
# Support Vector Classifier的參數(shù)
svc_params = {
'kernel': 'linear',
'C': 0.025
}
第一級(jí)分類器
# 通過(guò)前面定義的SklearnHelper類創(chuàng)建5個(gè)對(duì)象來(lái)表示5個(gè)學(xué)習(xí)模型
rf = SklearnHelper(clf=RandomForestClassifier, seed=SEED, params=rf_params)
et = SklearnHelper(clf=ExtraTreesClassifier, seed=SEED, params=et_params)
ada = SklearnHelper(clf=AdaBoostClassifier, seed=SEED, params=ada_params)
gb = SklearnHelper(clf=GradientBoostingClassifier, seed=SEED, params=gb_params)
svc = SklearnHelper(clf=SVC, seed=SEED, params=svc_params)
# 創(chuàng)建包含train、test的Numpy數(shù)組,以提供給我們的模型
y_train = train['Survived'].ravel()
train = train.drop(['Survived'], axis=1)
x_train = train.values
# test = test.drop(['Parch', 'Embarked', 'Has_Cabin', 'IsAlone'], axis=1)
x_test = test.values
#這些將會(huì)作為新的特征被使用
et_oof_train, et_oof_test = get_oof(et, x_train, y_train, x_test) # Extra Trees
rf_oof_train, rf_oof_test = get_oof(rf, x_train, y_train, x_test) # Random Forest
ada_oof_train, ada_oof_test = get_oof(ada, x_train, y_train, x_test) # AdaBoost
gb_oof_train, gb_oof_test = get_oof(gb, x_train, y_train, x_test) # Gradient Boost
svc_oof_train, svc_oof_test = get_oof(svc, x_train, y_train, x_test) # Support Vector Classifier
現(xiàn)在已經(jīng)獲得了我們的第一級(jí)預(yù)測(cè),我們可以把它看作是一組新的特性,作為下一個(gè)分類器的訓(xùn)練數(shù)據(jù)。
查看各個(gè)特征對(duì)上述分類器的重要性
rf_features = rf.feature_importances(x_train, y_train)
et_features = et.feature_importances(x_train, y_train)
ada_features = ada.feature_importances(x_train, y_train)
gb_features = gb.feature_importances(x_train, y_train)
cols = train.columns.values
feature_dataframe = pd.DataFrame({'features': cols,
'Random Forest feature importances': rf_features,
'Extra Trees feature importances': et_features,
'AdaBoost feature importances': ada_features,
'Gradient Boost feature importances': gb_features})
feature_dataframe['mean'] = feature_dataframe.mean(axis=1) # axis = 1 computes the mean row-wise
print(feature_dataframe.head(11))


畫(huà)圖查看各個(gè)分類器的相關(guān)性
base_predictions_train = pd.DataFrame( {'RandomForest': rf_oof_train.ravel(),
'ExtraTrees': et_oof_train.ravel(),
'AdaBoost': ada_oof_train.ravel(),
'GradientBoost': gb_oof_train.ravel()
})
data = [
go.Heatmap(
z= base_predictions_train.astype(float).corr().values ,
x=base_predictions_train.columns.values,
y= base_predictions_train.columns.values,
colorscale='Viridis',
showscale=True,
reversescale = True
)
]
py.iplot(data, filename='labelled-heatmap')

,這些模型彼此之間的相關(guān)性越低,得分越高。
第二級(jí)分類器xgboost
x_train = np.concatenate((et_oof_train, rf_oof_train, ada_oof_train, gb_oof_train, svc_oof_train), axis=1)
x_test = np.concatenate((et_oof_test, rf_oof_test, ada_oof_test, gb_oof_test, svc_oof_test), axis=1)
gbm = xgb.XGBClassifier(
#learning_rate=0.01,
n_estimators=2000,
max_depth=4,
min_child_weight=2,
# gamma=1,
gamma=0.9,
subsample=0.8,
colsample_bytree=0.8,
objective='binary:logistic',
nthread=-1,
scale_pos_weight=1).fit(x_train, y_train)
predictions = gbm.predict(x_test)
提交
StackingSubmission = pd.DataFrame({'PassengerId': PassengerId,
'Survived': predictions})
StackingSubmission.to_csv("StackingSubmission.csv", index=False)
提交后的分?jǐn)?shù),排名如下


總結(jié)
相比于其他的kernels,這個(gè)kernels的特征工程方面做的不突出(但還是比我之前的簡(jiǎn)書(shū)好很多,哈哈哈),突出的方面是用了新的方法Stacking,這個(gè)其他人在Titanic比賽中沒(méi)有用到過(guò)的,這也是他排第一的原因。
進(jìn)一步改善的步驟
必須指出的是,上述步驟只是顯示了一個(gè)非常簡(jiǎn)單的方法。聽(tīng)說(shuō)過(guò)在Kaggle的最高級(jí)別比賽中創(chuàng)建的組合,其中包括stacked classifiers的巨大組合,以及超過(guò)2級(jí)的stacking級(jí)別。