3ステップで行うデータ予測コンペ(titanic)

Kaggleの記事を色々と読んで、データ解析は以下の3ステップにまとめられると個人的に思いました。

ステップ１：特徴量エンジニアリング
ステップ２：学習器選択とハイパーパラメータ調整
ステップ３：アンサンブル

以上の3ステップを終えるとモデルが決定するので、そのモデルにテストデータ（予測を提出するデータ）を当てはめて予測し、提出します。(それまでは提出しないでクロスバリデーションの値で性能を評価する。)
1ステップで1回以上はクロスバリデーションをするので、計3回以上クロスバリデーションを行うことになります。

##ステップ１：特徴量エンジニアリング
EDA(探索的データ分析)などで使えそうな特徴量を考え、必要があれば新しい特徴量を作成します。
データリークなども考慮します。
また、欠損値処理などの前処理も行います。
最終的には学習器を設定して、RFE(再帰的特徴消去)によって必要ない特徴量を取り除きます。この際に評価にはクロスバリデーションを用います。
決定木系の学習器では特徴量の重要度を見ることも出来ます。
画像やテキストなどの複雑なデータの場合はニューラルネットを用いて特徴量抽出することを考えます。

##ステップ２：学習器選択とハイパーパラメータ調整
学習器は思いつくものは全て試してみるのがよいと思われます。(後でアンサンブル出来るため。)
ハイパーパラメータ調整は、グリッドサーチやランダムサーチ、最近ではoptunaによるベイズ最適化も出来ます。
これによって各学習器で最高のスコアが出来る状態を作ります。
あまりにも性能評価結果が低い学習器は削除します。

##ステップ３：アンサンブル
各学習器での結果をまとめるものです。学習器が一つの場合はアンサンブルできないのでその学習器が最終的なモデルになります。
単純に投票(vote)や平均(mean)をする方法、またはブレンダーを積み上げてスタッキングをする方法があります。
スタッキングをするときは、ブレンダーとなる学習器のハイパーパラメータ調整も必要となります。
また、アンサンブルしない方が評価結果がよいこともあり、その場合はどちらを最終的なモデルにするかは考えものです。(どちらも提出するのがよいのかもしれません。)

##クロスバリデーションについて
モデルの性能評価方法はクロスバリデーションがよく用いられます。
K-Foldで行いますが、基本的にK数は多い方がよいと思われます。（ただし、Kが多いと時間がかかる。）
また、データ不均衡などが起こらないように、StratifiedKFoldやランダム抽出を行います。
性能評価では、各Foldにおける平均のスコアだけでなく、標準偏差にも注目します。

#titanicコンペでの実装例
参考にした記事
https://www.kaggle.com/ldfreeman3/a-data-science-framework-to-achieve-99-accuracy
http://maruo51.com/2019/04/07/titanic-3/
https://yolo-kiyoshi.com/2020/01/22/post-1588/

###特徴量エンジニアリング
EDAは省略します。
RFEにはランダムフォレストを用いました。

```Python
import pandas as pd 
import numpy as np

data_train = pd.read_csv('./input/train.csv')
data_test  = pd.read_csv('./input/test.csv')
data_train_raw = data_train.copy(deep = True) #元のデータを一応コピーしておく
data_test_raw = data_test.copy(deep = True) #元のデータを一応コピーしておく
data_cleaner = [data_train, data_test]#リストとして結合すると、コピー元も一緒に操作できるので便利
#ちなみにデータフレームでの結合はdata_all = pd.concat([data_train, data_test], sort=False)で出来る

# 使わないものを削除
drop_column = ['PassengerId','Cabin', 'Ticket','Name']
data_train.drop(drop_column, axis=1, inplace = True)
#　欠損値補充
for dataset in data_cleaner:    
    dataset['Age'].fillna(dataset['Age'].median(), inplace = True)
    dataset['Embarked'].fillna(dataset['Embarked'].mode()[0], inplace = True)
    dataset['Fare'].fillna(dataset['Fare'].median(), inplace = True)
# 新しい特徴量の作成
for dataset in data_cleaner:    
    dataset['FamilySize'] = dataset ['SibSp'] + dataset['Parch'] + 1
    dataset['IsAlone'] = 1
    dataset['IsAlone'].loc[dataset['FamilySize'] > 1] = 0 
# ラベルエンコーディング
from sklearn.preprocessing import LabelEncoder
label = LabelEncoder()
for dataset in data_cleaner:    
    dataset['Sex_Code'] = label.fit_transform(dataset['Sex'])
    dataset['Embarked_Code'] = label.fit_transform(dataset['Embarked'])
drop_column = ['Sex','Embarked']
data_train.drop(drop_column, axis=1, inplace = True)
#　x,yのラベルを取得
lavel_y = "Survived"
lavel_x = data_train.columns.values[1:]

# ランダムフォレストでRFE(再帰的特徴消去)
from sklearn.model_selection import StratifiedKFold
from sklearn.feature_selection import RFECV
from sklearn.ensemble import RandomForestClassifier
NFOLD = 20 #CV分割Fold数
kf = StratifiedKFold(n_splits=NFOLD, shuffle=True, random_state=0)
rfe = RFECV(RandomForestClassifier(random_state = 0), scoring = 'accuracy', cv = kf)
rfe.fit(data_train[lavel_x], data_train[lavel_y])
f_importances = pd.DataFrame({"features":lavel_x,"select":rfe.get_support()})
print(f_importances) #RFEの結果表示
# 使用する特徴量データを確定
X_train = rfe.transform(data_train[lavel_x])
X_test = rfe.transform(data_test[lavel_x])
Y_train = data_train[lavel_y]
```

###学習器選択とハイパーパラメータ調整
ランダムフォレスト、XGBoost、LightGBMを学習器として、Optunaでそれぞれのハイパーパラメータサーチを行いました。

```Python
from sklearn.model_selection import cross_validate
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
import optuna

# ランダムフォレスト
def objective(trial):
    param_grid_rfc = {
        "max_depth": trial.suggest_int("max_depth", 5, 15),
        "min_samples_leaf": trial.suggest_int("min_samples_leaf", 1, 5),
        'min_samples_split': trial.suggest_int("min_samples_split", 7, 15),
        "criterion": trial.suggest_categorical("criterion", ["gini", "entropy"]),
        'max_features': trial.suggest_int("max_features", 3, 10),
        "random_state": 0
    }
    model = RandomForestClassifier(**param_grid_rfc)
    scores = cross_validate(model, X=X_train, y=Y_train, cv=kf)
    return scores['test_score'].mean()
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(study.best_params)
print(study.best_value)
rfc_best_param = study.best_params

# XGBoost
def objective(trial):   
    param_grid_xgb = {
        'min_child_weight': trial.suggest_int("min_child_weight", 1, 5),
        'gamma': trial.suggest_discrete_uniform("gamma", 0.1, 1.0, 0.1),
        'subsample': trial.suggest_discrete_uniform("subsample", 0.5, 1.0, 0.1),
        'colsample_bytree': trial.suggest_discrete_uniform("colsample_bytree", 0.5, 1.0, 0.1),
        'max_depth': trial.suggest_int("max_depth", 3, 10),
        "random_state": 0
    }
    model = XGBClassifier(**param_grid_xgb)
    scores = cross_validate(model, X=X_train, y=Y_train, cv=kf)
    return scores['test_score'].mean()
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(study.best_params)
print(study.best_value)
xgb_best_param = study.best_params

# LightGBM
def objective(trial):
    param_grid_lgb = {
        'num_leaves': trial.suggest_int("num_leaves", 3, 10),
        'learning_rate': trial.suggest_loguniform("learning_rate", 1e-8, 1.0),
        'max_depth': trial.suggest_int("max_depth", 3, 10),
        "random_state": 0
    }
    model = LGBMClassifier(**param_grid_lgb)
    scores = cross_validate(model, X=X_train, y=Y_train, cv=kf)
    return scores['test_score'].mean()
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(study.best_params)
print(study.best_value)
lgb_best_param = study.best_params
```

###アンサンブル
多数決(majority rule voting)でアンサンブルしました。

```Python
from sklearn.ensemble import VotingClassifier
estimators = [
    ('rfc', RandomForestClassifier(**rfc_best_param)),
    ('xgb', XGBClassifier(**xgb_best_param)),
    ('lgb', LGBMClassifier(**lgb_best_param)),
]
voting = VotingClassifier(estimators)
scores = cross_validate(voting, X=X_train, y=Y_train, cv=kf)
print(scores["test_score"].mean())
print(scores["test_score"].std())
```
アンサンブルの結果が一番良さそうなのでそのモデルで予測して提出ファイルを作ります。

```Python
voting.fit(X_train, Y_train)
data_test['Survived'] = voting.predict(X_test)
submit = data_test[['PassengerId','Survived']]
submit.to_csv("./submit.csv", index=False)
```

#最後に
クロスバリデーションでの値は0.85ほどでまあまあ良さそうでしたが、PLBではあまり良くありませんでした。
特徴量をどうつくるか、RFEでの学習器の選択、KFoldの数、学習器の選択、ハイパーパラメータの範囲の選択、アンサンブル方法の選択など、工夫できるところがいくつもあります。それらによってもっと改善できると思います。