タイタニック生存予測モデルの構築とアンサンブル学習


**はじめに**
元々エステティシャンをしてましたが、コロナで職がなくなり商社の営業へ。
務めていく中で将来が不安になり、今後コロナなどの影響を受けても働ける、
長く勤めることのできる職種を探していた時、プログラミングに興味を持ち受講をしてみようと思いました。
****

**本記事の概要**
これは、1912年に沈没したタイタニック号の乗客に関するデータセットです。
このデータセットには、乗客の年齢、性別、社会階級、乗船券の種類、
救命ボートに乗っていたかどうかなど、乗客に関する情報が入ってます。

このTitanicデータセットを用いて、乗客の生存予測を行う機械学習モデルを構築し、評価する手順を紹介します。この記事では、LightGBM、Random Forest、そしてTensorFlowを用いたニューラルネットワーク（MLP）の組み合わせを通じて、どのようにして生存確率の予測精度を向上させるかを探ろうと思います。

![9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3824981/7f80b418-d028-1267-8d99-09bd159bfb9e.jpeg)


****
**大まかな流れ**
1. データ処理と前処理
2. モデルの準備
3. モデルの訓練と評価
4. モデルのアンサンブル
5. 提出ファイルの作成
#
**1. データ処理と前処理**
まず必要なライブラリのインポートをします。
```import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import KFold
from sklearn.metrics import accuracy_score
import lightgbm as lgb
import tensorflow as tf
import random
import scipy.stats as stats
import os
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import EarlyStopping
```
#
**乱数の固定**

同じデータとパラメータで実行する時に
同じ結果を得られるようランダムシードを固定にします。
```
# 乱数を固定
tf.random.set_seed(0)
np.random.seed(0)
random.seed(0)
os.environ["PYTHONHASHSEED"] = "0"
```
#
**データの読み込み**

上から順にトレーニングデータ、テストデータ、サンプル提出ファイルデータ。読み込みます。
```
# データの読み込み
train_df = pd.read_csv('/kaggle/input/titanic/train.csv')
test_df = pd.read_csv('/kaggle/input/titanic/test.csv')
sample_sub = pd.read_csv('/kaggle/input/titanic/gender_submission.csv')
```
#

**データの結合**

トレーニングデータとテストデータを結合。データセットの区別をつけるために 
Test_Flag というフラグを設定し、新しい列を作成してテストデータをマークします。
```
# データの結合
all_df = pd.concat([train_df, test_df], axis=0).reset_index(drop=True)
all_df['Test_Flag'] = 0
all_df.loc[train_df.shape[0]:, 'Test_Flag'] = 1
```
#
**欠損値の処理**

データセットの品質を向上させるだけでなく、データの解釈性を高め、分析プロジェクトの成功率を上げるために欠損値の処理をしていきます。
欠損値の補完するために、AgeとFareの欠損値を
それぞれ極端な値の影響を受けにくくするため，中央値で補完し、Embarkedの欠損値を'NaN'で補完します。
```
# 欠損値の補完
all_df['Age'] = all_df['Age'].fillna(all_df['Age'].median())
all_df['Fare'] = all_df['Fare'].fillna(all_df['Fare'].median())
all_df['Embarked'] = all_df['Embarked'].fillna('NaN')
```
#
**特徴量のエンジニアリング**

機械学習モデルの予測精度を上げるため、新しい特徴量を作成していきます。
FareBandとAgeBand：FareとAgeを4つのビンに分割します。

家族サイズを特徴量.カテゴリ化し、家族構成が生存率に与える影響を
考慮できるようにし、モデルが特定の家族サイズに対するパターンを学習しやすくするために

家族関連の特徴量FamilySize、MedF、LargeF、Aloneという新しい特徴量を作成
カテゴリ変数をダミー変数に変換。
名前からタイトルを抽出し、数値に変換。Title_Encodeとしてエンコードします。
```
# 特徴量のエンジニアリング

all_df['FareBand'] = pd.qcut(all_df['Fare'], 4)
all_df['AgeBand'] = pd.qcut(all_df['Age'], 4)
all_df['FamilySize'] = all_df['SibSp'] + all_df['Parch'] + 1
all_df['MedF'] = all_df['FamilySize'].map(lambda s: 1 if 2 <= s <= 4 else 0)
all_df['LargeF'] = all_df['FamilySize'].map(lambda s: 1 if s >= 5 else 0)
all_df['Alone'] = all_df['FamilySize'].map(lambda s: 1 if s == 1 else 0)

# タイトルのエンコード
all_df['Title'] = all_df.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
def prepro_name_title(Title):
    if Title == 'Master':
        return 0
    elif Title == 'Miss':
        return 1
    elif Title == 'Mr':
        return 2
    elif Title == 'Mrs':
        return 3
    else:
        return 4

all_df['Title_Encode'] = all_df['Title'].map(prepro_name_title)
```
#
**ワンホットエンコーディング**

変数の全ての値を平等に扱うため、カテゴリカル変数をワンホットエンコーディングに変換。
```
# One-Hot Encoding
all_df = pd.get_dummies(all_df, columns=["Sex", "Pclass"], dtype=float)
all_df = pd.get_dummies(all_df, columns=['AgeBand', 'FareBand', 'Embarked'], dtype=float)
```
#
**トレーニングデータとテストデータの分割**

AIモデルの汎化能力を向上させるため
データをトレーニング用とテスト用に分割し、不要な列を削除します。
```
train = all_df[all_df['Test_Flag'] == 0]
test = all_df[all_df['Test_Flag'] == 1].reset_index(drop=True)
target = train['Survived']

drop_col = ['PassengerId', 'Age', 'Ticket', 'Title', 'Fare', 'Cabin', 'Test_Flag', 'Name', 'Survived']
train = train.drop(drop_col, axis=1)
test = test.drop(drop_col, axis=1)
```
#
**クロスバリデーションの設定**
モデルの汎化性能を評価するために重要です。これにより、過学習を防ぎます。
KFoldクロスバリデーションを設定し、3つのフォールドでデータを分割します。
```
# KFoldクロスバリデーションの設定
cv = KFold(n_splits=3, random_state=0, shuffle=True)

train_acc_list = []
val_acc_list = []
```
#
**モデルのハイパーパラメータ設定**

ハイパーパラメータを設定します。
LightGBMがカラム名に[]や{}を含んでいるとエラーが出るため、カラム名を変更します。
```
# LightGBMのハイパーパラメータ
lgb_params = {
    "objective": "binary",
    "metric": "binary_error",
    "force_row_wise": True,
    "seed": 0,
    'learning_rate': 0.1,
    'min_data_in_leaf': 5,
    'max_depth': 16
}

# LightGBMがカラム名に[]や{}を含んでいるとエラーが出るので、変更する
rename_dict = {
    'AgeBand_(0.169, 22.0]': 'AgeBand_1',
    'AgeBand_(22.0, 28.0]': 'AgeBand_2',
    'AgeBand_(28.0, 35.0]': 'AgeBand_3',
    'AgeBand_(35.0, 80.0]': 'AgeBand_4',
    'FareBand_(-0.001, 7.896]': 'FareBand_1',
    'FareBand_(7.896, 14.454]': 'FareBand_2',
    'FareBand_(14.454, 31.275]': 'FareBand_3',
    'FareBand_(31.275, 512.329]': 'FareBand_4'
}
```
#
**モデルの訓練**

3つのモデル（LightGBM、RandomForest、MLP）をそれぞれ訓練し
訓練データと検証データに対して各モデルの予測を行い、精度を計算します。
訓練精度と検証精度をリストに保存。
```
# クロスバリデーションでのモデル訓練
for i, (trn_index, val_index) in enumerate(cv.split(train, target)):
    print(f'Fold : {i}')
    X_train, X_val = train.loc[trn_index].rename(columns=rename_dict), train.loc[val_index].rename(columns=rename_dict)
    y_train, y_val = target[trn_index], target[val_index]

    # LightGBMの訓練
    lgb_train = lgb.Dataset(X_train, y_train)
    lgb_valid = lgb.Dataset(X_val, y_val)

    model_lgb = lgb.train(
        params=lgb_params,
        train_set=lgb_train,
        valid_sets=[lgb_train, lgb_valid],
        callbacks=[lgb.log_evaluation(period=0), lgb.early_stopping(10)]
    )

    # ランダムフォレストの訓練
    print('-' * 10 + ' Start_rf ' + '-' * 10)
    model_rf = RandomForestClassifier(
        random_state=0, max_depth=15,
        min_samples_leaf=5, min_samples_split=5
    )
    model_rf.fit(X_train, y_train)

    # MLPの訓練
    print('-' * 10 + ' Start_mlp ' + '-' * 10)
    train_pre = pd.get_dummies(train, columns=['Title_Encode'], dtype=float)
    X_train_mlp, X_val_mlp = train_pre.loc[trn_index].rename(columns=rename_dict), train_pre.loc[val_index].rename(columns=rename_dict)
    y_train_mlp, y_val_mlp = target[trn_index], target[val_index]

    model_mlp = tf.keras.models.Sequential([
        tf.keras.layers.Input((X_train_mlp.shape[1],)),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dropout(0.5),
        tf.keras.layers.Dense(16, activation='relu'),
        tf.keras.layers.Dropout(0.5),
        tf.keras.layers.Dense(16, activation='relu'),
        tf.keras.layers.Dense(2, activation='softmax')
    ])
    early_stopping = EarlyStopping(
        monitor='val_loss',
        patience=10,
        mode='auto'
    )
    model_mlp.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.01),
                      loss='categorical_crossentropy',
                      metrics=['accuracy'])

    model_mlp.fit(
        X_train_mlp, to_categorical(y_train_mlp), validation_data=(X_val_mlp, to_categorical(y_val_mlp)),
        batch_size=256, epochs=300, verbose=False, callbacks=[early_stopping]
    )

    # 予測と評価
    train_pred = np.zeros((len(y_train_mlp), 3))
    train_pred[:, 0] = np.where(model_lgb.predict(X_train) >= 0.5, 1, 0)
    train_pred[:, 1] = model_rf.predict(X_train)
    train_pred[:, 2] = np.argmax(model_mlp.predict(X_train_mlp), axis=1)

    train_acc = accuracy_score(y_train, stats.mode(train_pred, axis=1)[0])
    train_acc_list.append(train_acc)

    val_pred = np.zeros((len(y_val_mlp), 3))
    val_pred[:, 0] = np.where(model_lgb.predict(X_val) >= 0.5, 1, 0)
    val_pred[:, 1] = model_rf.predict(X_val)
    val_pred[:, 2] = np.argmax(model_mlp.predict(X_val_mlp), axis=1)

    val_acc = accuracy_score(y_val, stats.mode(val_pred, axis=1)[0])
    val_acc_list.append(val_acc)

print('-' * 10 + 'Result' + '-' * 10)
print(f'Train_acc : {train_acc_list} , Ave : {np.mean(train_acc_list)}')
print(f'Valid_acc : {val_acc_list} , Ave : {np.mean(val_acc_list)}')
```
#
**予測と提出ファイルの作成**

テストデータに対して各モデルの予測を行い
予測結果をまとめて、提出ファイルを作成します。

```
# 予測結果をサブミットするファイル形式に変更
test_pred = np.zeros((len(test), 3))
test_mlp = pd.get_dummies(test, columns=['Title_Encode'], dtype=float).rename(columns=rename_dict)

test_pred[:, 0] = np.where(model_lgb.predict(test) >= 0.5, 1, 0)
test_pred[:, 1] = model_rf.predict(test.rename(columns=rename_dict))
test_pred[:, 2] = np.argmax(model_mlp.predict(test_mlp), axis=1)

# 提出ファイルを出力
sample_sub = pd.read_csv('/kaggle/input/titanic/gender_submission.csv')
sample_sub["Survived"] = stats.mode(test_pred, axis=1)[0]
sample_sub["Survived"] = sample_sub["Survived"].astype('int8')
sample_sub.to_csv("submission.csv", index=False)
```
#
**結果**

Fold : 0
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Info] Number of positive: 231, number of negative: 363
[LightGBM] [Info] Total Bins 65
[LightGBM] [Info] Number of data points in the train set: 594, number of used features: 23
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.388889 -> initscore=-0.451985
[LightGBM] [Info] Start training from score -0.451985
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[5]	training's binary_error: 0.148148	valid_1's binary_error: 0.171717
---------- Start_rf ----------
---------- Start_mlp ----------
19/19 ━━━━━━━━━━━━━━━━━━━━ 0s 18ms/step
10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step 
Fold : 1
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Info] Number of positive: 238, number of negative: 356
[LightGBM] [Info] Total Bins 66
[LightGBM] [Info] Number of data points in the train set: 594, number of used features: 23
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.400673 -> initscore=-0.402660
[LightGBM] [Info] Start training from score -0.402660
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[4]	training's binary_error: 0.164983	valid_1's binary_error: 0.13468
---------- Start_rf ----------
---------- Start_mlp ----------
19/19 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step
10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step 
Fold : 2
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Info] Number of positive: 215, number of negative: 379
[LightGBM] [Info] Total Bins 66
[LightGBM] [Info] Number of data points in the train set: 594, number of used features: 23
[LightGBM] [Warning] Accuracy may be bad since you didn't explicitly set num_leaves OR 2^max_depth > num_leaves. (num_leaves=31).
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.361953 -> initscore=-0.566898
[LightGBM] [Info] Start training from score -0.566898
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[6]	training's binary_error: 0.136364	valid_1's binary_error: 0.212121
---------- Start_rf ----------
---------- Start_mlp ----------
19/19 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step
10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step 
----------Result----------
Train_acc : [0.8484848484848485, 0.8434343434343434, 0.8602693602693603] , Ave : 0.8507295173961841
Valid_acc : [0.8181818181818182, 0.8282828282828283, 0.8148148148148148] , Ave : 0.8204264870931538
14/14 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step 
#
**まとめ**
LightGBM、RandomForest、MLPの3つのモデルを使用してタイタニックデータセットを学習し、それぞれのモデルをアンサンブルすることで、予測精度を向上させることができました。

トレーニングデータでの平均正答率は約 85%、バリデーションデータでの平均正答率は約 82%です。これはタイタニックの生存予測において十分な性能ではないかなと思います。

**今後の活用**

データ分析の仕事に携われる仕事をするのはもちろんですが、まずは今まで学習してきた事をもっと自分の物にしていきたいです。そしてタイタニックデータセットで構築したアンサンブル学習の手法を応用することで、他の多くの機械学習プロジェクトでも成果を上げ、顧客管理やリスク管理などに活用していきたいと思います。


****

補足：**「このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています」**