kaggleのtitanic スタッキングを使った生存者予測[81.3%]

Kaggleのtitanicについてはxgboostでの予測とニューラルネットでの予測をしてみました。
[kaggleのtitanic xgboostを使った生存者予測 [80.1%]](https://qiita.com/serikatsu28/items/6cdd4d837deef13ddadd)
[kaggleのtitanic ニューラルネットを使った生存者予測 [80.4%]](https://qiita.com/serikatsu28/items/0cb55b6e376089b5587e)

今回は**xgboost、ランダムフォレスト、ニューラルネットの3つをスタッキング**して予測精度をさらに上げてみようと思います。

コードはGitHubでも公開しています。
[stacking.py](https://github.com/serikatsu28/kaggle/blob/titanic/stacking.py)

#1. スタッキングの手順
スタッキングは1つの学習器だけで学習、予測をするのではなく、**複数の学習器を組み合わせることで、予測エラーを小さくするアンサンブル学習の手法の1つ**です。

1層目に使う3つの学習器をA、B、C、最終的な予測値を出す学習器をDとしてスタッキングの流れを紹介します。

**特徴量が入った学習データをモデルの学習用のtrainデータとバリデーション用のvalidデータに分ける
↓
学習器A~Cにtrainデータを与えて学習させる
↓
学習器A~Cに特徴量が入ったx_validデータを与えてそれぞれの予測値(valid_preds)を出す
↓
学習器A~Cにtestデータを与えてそれぞれの予測値(test_preds)を出す
↓
valid_predsと目的変数が入ったy_validデータを学習器Dに渡して学習させる
↓
学習器Dにtest_predsを与えて最終的な予測値を予測させる**

これがスタッキングの流れになります。
今回は2層のスタッキングですが学習器を増やして3層以上の構造にすることも可能です。

#2. データの取得と特徴量の作成
xgboostやニューラルネットのときと同様の特徴量の処理を行います。

**ライブラリのインポートとデータの取得**

```python
import pandas as pd
import numpy as np
import os, random
import tensorflow as tf
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler

le = LabelEncoder()
sc = StandardScaler()

#乱数を固定する関数
def reset_seed(seed):
    os.environ['PYTHONHASHSEED'] = '0'
    random.seed(seed) # random関数のシードを固定
    np.random.seed(seed) # numpyのシードを固定
    tf.random.set_seed(seed) # tensorflowのシードを固定

#乱数を固定
reset_seed(28)

#データの取得
train = pd.read_csv('/kaggle/input/titanic/train.csv')
test = pd.read_csv('/kaggle/input/titanic/test.csv')
#trainデータとtestデータを統合
data = pd.concat([train,test]).reset_index(drop=True)
```
**Sexのラベリング**

```python
data['Sex'] = le.fit_transform(data['Sex'])
```
**Fareの処理**

```python
#欠損値を補完
data['Fare'] = data['Fare'].fillna(data.query('Pclass==3 & Embarked=="S"')['Fare'].median())
#階級分け
data['Fare_bin'] = 0 #デフォルト値
data.loc[(data['Fare']>=10) & (data['Fare']<50), 'Fare_bin'] = 1
data.loc[(data['Fare']>=50) & (data['Fare']<100), 'Fare_bin'] = 2
data.loc[(data['Fare']>=100), 'Fare_bin'] = 3
#標準化
data['Fare_std'] = sc.fit_transform(data[['Fare']])
```
**家族の人数'Family_size'の作成**

```python
data['Family_size'] = data['SibSp']+data['Parch']+1
data['Family_size_bin'] = 0 #デフォルト値
data.loc[(data['Family_size']>=2) & (data['Family_size']<=4),'Family_size_bin'] = 1
data.loc[(data['Family_size']>=5) & (data['Family_size']<=7),'Family_size_bin'] = 2
data.loc[(data['Family_size']>=8),'Family_size_bin'] = 3
```
**名前の敬称'Title'の作成**

```python
data['Title'] = data['Name'].map(lambda x: x.split(', ')[1].split('. ')[0])
data['Title'].replace(['Capt', 'Col', 'Major', 'Dr', 'Rev'], 'Officer', inplace=True)
data['Title'].replace(['Don', 'Sir',  'the Countess', 'Lady', 'Dona'], 'Royalty', inplace=True)
data['Title'].replace(['Mme', 'Ms'], 'Mrs', inplace=True)
data['Title'].replace(['Mlle'], 'Miss', inplace=True)
data['Title'].replace(['Jonkheer'], 'Master', inplace=True)
```

**グループごとの生存の違い'Family_survival'の作成**

```python
#名前の名字を取得して'Last_name'に入れる
data['Last_name'] = data['Name'].apply(lambda x: x.split(",")[0])

data['Family_survival'] = 0.5 #デフォルトの値
#Last_nameとFareでグルーピング
for grp, grp_df in data.groupby(['Last_name', 'Fare']):
                               
    if (len(grp_df) != 1):
        #(名字が同じ)かつ(Fareが同じ)人が2人以上いる場合
        for index, row in grp_df.iterrows():
            smax = grp_df.drop(index)['Survived'].max()
            smin = grp_df.drop(index)['Survived'].min()
            passID = row['PassengerId']
            
            if (smax == 1.0):
                data.loc[data['PassengerId'] == passID, 'Family_survival'] = 1
            elif (smin == 0.0):
                data.loc[data['PassengerId'] == passID, 'Family_survival'] = 0
            #自身以外のメンバーについて
            #1人でも生存している→1
            #生存者がいない(NaNも含む)→0
            #全員NaN→0.5
            
#チケット番号でグルーピング
for grp, grp_df in data.groupby('Ticket'):
    if (len(grp_df) != 1):
        #チケット番号が同じ人が2人以上いる場合
        #グループ内で1人でも生存者がいれば'Family_survival'を1にする
        for ind, row in grp_df.iterrows():
            if (row['Family_survival'] == 0) | (row['Family_survival']== 0.5):
                smax = grp_df.drop(ind)['Survived'].max()
                smin = grp_df.drop(ind)['Survived'].min()
                passID = row['PassengerId']
                if (smax == 1.0):
                    data.loc[data['PassengerId'] == passID, 'Family_survival'] = 1
                elif (smin == 0.0):
                    data.loc[data['PassengerId'] == passID, 'Family_survival'] = 0
```
**ランダムフォレストを使ったAgeの欠損値補完と階級分け**

```python
from sklearn.ensemble import RandomForestRegressor
#Ageの予測に使う特徴量を'age_data'にいれる
age_data = data[['Age','Pclass','Family_size','Fare_bin','Title']].copy()
#文字列になっている特徴量をラベリング
age_data['Title'] = le.fit_transform(age_data['Title'])
#Ageが欠損している行と欠損していない行に分ける
known_age = age_data[age_data['Age'].notnull()].values  
unknown_age = age_data[age_data['Age'].isnull()].values

x = known_age[:, 1:]
y = known_age[:, 0]
#ランダムフォレストで学習
rfr = RandomForestRegressor(random_state=28, n_estimators=100, n_jobs=-1)
rfr.fit(x, y)
#予測値を元のデータフレームに反映する
age_predict = rfr.predict(unknown_age[:, 1:])
data.loc[(data['Age'].isnull()), 'Age'] = np.round(age_predict,1)

#Ageの階級分け
data['Age_bin'] = 0
data.loc[(data['Age']>18) & (data['Age']<=60),'Age_bin'] = 1
data.loc[(data['Age']>60),'Age_bin'] = 2
```
**チケットの種類ごとにラベリング**

```python
#数字のみのチケットと数字とアルファベットを含むチケットに分ける
#数字のみのチケットを取得
num_ticket = data[data['Ticket'].str.match('[0-9]+')].copy()
num_ticket_index = num_ticket.index.values.tolist()
#元のdataから数字のみのチケットの行を落とした残りがアルファベットを含むチケット
num_alpha_ticket = data.drop(num_ticket_index).copy()

#数字のみのチケットの階級分け
#チケット番号は文字列になっているので数値に変換
num_ticket['Ticket'] = num_ticket['Ticket'].apply(lambda x:int(x))

num_ticket['Ticket_bin'] = 0
num_ticket.loc[(num_ticket['Ticket']>=100000) & (num_ticket['Ticket']<200000),
               'Ticket_bin'] = 1
num_ticket.loc[(num_ticket['Ticket']>=200000) & (num_ticket['Ticket']<300000),
               'Ticket_bin'] = 2
num_ticket.loc[(num_ticket['Ticket']>=300000),'Ticket_bin'] = 3

#数字とアルファベットを含むチケットの階級分け
num_alpha_ticket['Ticket_bin'] = 4
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('A.+'),'Ticket_bin'] = 5
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('C.+'),'Ticket_bin'] = 6
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('C\.*A\.*.+'),'Ticket_bin'] = 7
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('F\.C.+'),'Ticket_bin'] = 8
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('PC.+'),'Ticket_bin'] = 9
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('S\.+.+'),'Ticket_bin'] = 10
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('SC.+'),'Ticket_bin'] = 11
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('SOTON.+'),'Ticket_bin'] = 12 
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('STON.+'),'Ticket_bin'] = 13
num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('W\.*/C.+'),'Ticket_bin'] = 14

data = pd.concat([num_ticket,num_alpha_ticket]).sort_values('PassengerId')
```
**不要な特徴量を落とす**

```python
#xgboostとランダムフォレスト用のデータフレーム
data_xr = data.drop(['PassengerId','Name','Age','SibSp','Parch','Ticket',
                     'Fare','Cabin','Embarked','Fare_std','Family_size','Last_name'], axis=1)
data_xr['Title'] = le.fit_transform(data_xr['Title'])

#ニューラルネット用のデータフレーム
data_nn = data.drop(['PassengerId','Name','Age','SibSp','Parch','Ticket','Fare','Cabin',
                     'Embarked','Fare_bin','Family_size','Last_name','Ticket_bin'], axis=1)
data_nn = pd.get_dummies(data=data_nn, columns=['Title','Pclass','Family_survival'])
```
最終的に2つのデータフレームが完成しました

**xgboostとランダムフォレスト用のデータフレーム**

||Survived|Pclass|Sex|Fare_bin|Family_size_bin|Title|Family_survival|Age_bin|Ticket_bin|
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|0|0.0|3|1|0|1|2|0.5|1|5|
|1|1.0|1|0|2|1|3|0.5|1|9|
|2|1.0|3|0|0|0|1|0.5|1|13|
|3|1.0|1|0|2|1|3|0.0|1|1|
|4|0.0|3|1|0|0|2|0.5|1|3|
|...|...|...|...|...|...|...|...|...|...|
|1304|NaN|3|1|0|0|2|0.5|1|5|
|1305|NaN|1|0|3|0|5|1.0|1|9|
|1306|NaN|3|1|0|0|2|0.5|1|12|
|1307|NaN|3|1|0|0|2|0.5|1|3|
|1308|NaN|3|1|1|1|0|1.0|0|0|
1309 rows × 9 columns

**ニューラルネット用のデータフレーム**

||Survived|Sex|Fare_std|Family_size_bin|Age_bin|Title_Master|Title_Miss|Title_Mr|Title_Mrs|Title_Officer|Title_Royalty|Pclass_1|Pclass_2|Pclass_3|Family_survival_0.0|Family_survival_0.5|Family_survival_1.0|
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|0|0.0|1|-0.503176|1|1|0|0|1|0|0|0|0|0|1|0|1|0|
|1|1.0|0|0.734809|1|1|0|0|0|1|0|0|1|0|0|0|1|0|
|2|1.0|0|-0.490126|0|1|0|1|0|0|0|0|0|0|1|0|1|0|
|3|1.0|0|0.383263|1|1|0|0|0|1|0|0|1|0|0|1|0|0|
|4|0.0|1|-0.487709|0|1|0|0|1|0|0|0|0|0|1|0|1|0|
|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|
|1304|NaN|1|-0.487709|0|1|0|0|1|0|0|0|0|0|1|0|1|0|
|1305|NaN|0|1.462069|0|1|0|0|0|0|0|1|1|0|0|0|0|1|
|1306|NaN|1|-0.503176|0|1|0|0|1|0|0|0|0|0|1|0|1|0|
|1307|NaN|1|-0.487709|0|1|0|0|1|0|0|0|0|0|1|0|1|0|
|1308|NaN|1|-0.211081|1|0|1|0|0|0|0|0|0|0|1|0|0|1|
1309 rows × 17 columns

最後にそれぞれのデータフレームを学習用のtrainデータとバリデーション用のvalidデータに分けます。

```python
from sklearn.model_selection import train_test_split
#trainデータとtestデータに分ける
model_train_xr = data_xr[:891]
model_test_xr = data_xr[891:]
#特徴量と目的変数に分ける
X_xr = model_train_xr.drop('Survived', axis=1)
Y_xr = pd.DataFrame(model_train_xr['Survived'])
x_test_xr = model_test_xr.drop('Survived', axis=1)
#trainデータとvalidデータに分ける
x_train_xr, x_valid_xr, y_train_xr, y_valid_xr = train_test_split(X_xr, Y_xr, test_size=0.3, random_state=28)

#trainデータとtestデータに分ける
model_train_nn = data_nn[:891]
model_test_nn = data_nn[891:]
#特徴量と目的変数に分ける
X_nn = model_train_nn.drop('Survived', axis=1)
Y_nn = pd.DataFrame(model_train_nn['Survived'])
x_test_nn = model_test_nn.drop('Survived', axis=1)
#trainデータとvalidデータに分ける
x_train_nn, x_valid_nn, y_train_nn, y_valid_nn = train_test_split(X_nn, Y_nn, test_size=0.3, random_state=28)
```

#3. モデルの構築と予測
まずはそれぞれの予測値を入れるリストを作成します。

```python
valid_pred_list = [] # x_validでの予測値を入れる
test_pred_list = [] # x_testでの予測値を入れる
```
xgboost、ランダムフォレスト、ニューラルネットの3つのモデルを構築し、それぞれでモデルの学習＆予測を行います。

**xgboostでの予測**

```python
import xgboost as xgb
#パラメータを設定
xgb_params = {'objective':'binary:logistic',
          'max_depth':5,
          'eta': 0.1, 
          'min_child_weight':1.0,
          'gamma':0.0,
          'colsample_bytree':0.8,
          'subsample':0.8}
num_round=1000

#データフレームをxgboostに適した形に変換
dtrain = xgb.DMatrix(x_train_xr, label=y_train_xr)
dvalid = xgb.DMatrix(x_valid_xr, label=y_valid_xr)
dtest = xgb.DMatrix(x_test_xr)

#xgboostで学習
xgb_model = xgb.train(xgb_params, dtrain, num_round,
                      evals=[(dtrain,'train'),(dvalid,'eval')],early_stopping_rounds=50)

#validデータでの予測
valid_pred_proba = xgb_model.predict(dvalid)
xgb_valid_pred = np.where(valid_pred_proba >0.5,1,0)
valid_pred_list.append(xgb_valid_pred)

#testデータでの予測
test_pred_proba = xgb_model.predict(dtest)
xgb_test_pred = np.where(test_pred_proba >0.5,1,0)
test_pred_list.append(xgb_test_pred)
```
**ランダムフォレストでの予測**

```python
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV

#データフレームをnｄarrayに変換
x_train_xr = np.array(x_train_xr)
x_valid_xr = np.array(x_valid_xr)
y_train_xr = np.array(y_train_xr).ravel()
y_valid_xr = np.array(y_valid_xr).ravel()
x_test_xr = np.array(x_test_xr)

#パラメータ設定
rfc_params = {
    "n_estimators" : [5, 10, 15, 20, 30, 50, 75, 100],
    "min_samples_split" : [2, 3, 5, 10, 15, 20, 30],
    "max_depth" : [3, 5, 10, 15, 20, 30],
    "criterion" : ["gini"],
    "random_state" : [28],
    "verbose" : [False]
}

#ランダムフォレストでGridSearch&学習
rfc_model = RandomForestClassifier()
gscv = GridSearchCV(rfc_model, rfc_params, cv=4)
gscv.fit(x_train_xr, y_train_xr)

#validデータでの予測
rfc_valid_pred = gscv.predict(x_valid_xr).astype(int)
valid_pred_list.append(rfc_valid_pred)

#testデータでの予測
rfc_test_pred = gscv.predict(x_test_xr).astype(int)
test_pred_list.append(rfc_test_pred)
```
**ニューラルネットでの予測**

```python
from tensorflow.keras.layers import Dense,Dropout
from tensorflow.keras.models import Sequential
from tensorflow.keras.callbacks import EarlyStopping

#モデルの初期化
model = Sequential()
#層の構築
model.add(Dense(12, activation='relu', input_dim=16))
model.add(Dropout(0.2))
model.add(Dense(8, activation='relu'))
model.add(Dense(5, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
#モデルの構築
model.compile(optimizer = 'adam', loss='binary_crossentropy', metrics='acc')
#モデルでの学習
log = model.fit(x_train_nn, y_train_nn, epochs=5000, batch_size=32,verbose=1,
                callbacks=[EarlyStopping(monitor='val_loss',min_delta=0,patience=100,verbose=1)],
                validation_split=0.3)

#validデータでの予測
nn_valid_pred = model.predict_classes(x_valid_nn).reshape(-1)
valid_pred_list.append(nn_valid_pred)
#testデータでの予測
nn_test_pred = model.predict_classes(x_test_nn).reshape(-1)
test_pred_list.append(nn_test_pred)
```
#4. ロジスティック回帰での予測
3つのモデルで出した予測値を特徴量として最終的な予測値をロジスティック回帰でもとめてみます。

```python
from sklearn.linear_model import LogisticRegression

#リストからndarrayに変換
valid_preds = np.column_stack(valid_pred_list)
test_preds = np.column_stack(test_pred_list)
print(valid_preds.shape)
print(test_preds.shape)
#(268, 3)
#(418, 3)

#y_validを1次元配列に変換
y_valid = np.array(y_valid_nn).reshape(-1).astype(int)
#ロジスティック回帰での学習＆予測
meta_model = LogisticRegression(solver='lbfgs', max_iter=10000)
meta_model.fit(valid_preds, y_valid)
meta_pred = meta_model.predict(test_preds).astype(int)
```
最後にkaggleに提出するデータフレームを作って終了です。

```python
submission = pd.DataFrame({'PassengerId':test['PassengerId'], 'Survived':meta_pred})
submission.to_csv('titanic_stacking.csv', index=False)
```
結果はこのようになりました。
![スクリーンショット 2020-10-20 3.19.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/c4f6f392-5bca-8685-e1ff-8155efd2c542.png)
正解率は**81.3%**となり、**学習器単体での正解率よりも1%ほど高くなりました。**

スタッキングはkaggleのコンペでもよく使われている手法のため、1層目の学習器の数を増やしたり3層以上の構築をしたりなどをすることで他のコンペでも応用ができそうです。
また同じ学習器でも**決定木の深さを変えたモデルを2〜3個用意したり、層の数を変えたニューラルネットをスタッキングすることも多い**そうです。

ご意見、ご指摘などがございましたらコメント、編集リクエストをしていただけるとありがたいです。

#参考にさせていただいたサイト、書籍
[機械学習の精度を上げたい時に使われるスタッキング(アンサンブル学習) を解説](https://bigdata-tools.com/stacking/)
[ニューラルネットワークの実装（分類）](https://www.kikagaku.ai/tutorial/basic_of_deep_learning/learn/tensorflow_classification)
[Kaggleで勝つデータ分析の技術](https://www.amazon.co.jp/Kaggle%E3%81%A7%E5%8B%9D%E3%81%A4%E3%83%87%E3%83%BC%E3%82%BF%E5%88%86%E6%9E%90%E3%81%AE%E6%8A%80%E8%A1%93-%E9%96%80%E8%84%87-%E5%A4%A7%E8%BC%94-ebook/dp/B07YTDBC3Z)