【機械学習入門・初心者】Kaggleのチュートリアルコンペティションに挑戦してみた

# はじめに
ulgeek Advent Calender 24日目です。

[Kaggle](https://www.kaggle.com)とは、データ解析スキルを競うコンペティションのプラットフォームです。企業や研究者がデータを投稿し、世界中のデータサイエンティストが最適なモデル、解析手法を競います。

コンペティションへは基本誰でも参加できます。参加する方法は[こちら](https://qiita.com/taka4sato/items/802c494fdebeaa7f43b7)にとても分かりやすく解説されていますので、是非読んでみて下さい。

最近だと、メルカリ（US）によるコンペティション開催も話題になりました。こちらを例にコンペティションを簡単に説明します。

メルカリは、機械学習を採用している「価格査定機能の精度を高める」ために、コンペティションを開催しました。これがコンペティションのテーマです。

コンペティションには、メルカリが持っている実際のデータが提供されます。
世界中のコンペティション参加者は、そのデータを解析してモデルを構築し、価格を予測します。
その後、予測結果をKaggleに提出し、最も正確に予測できた人が優勝です。

メルカリのコンペティションは、総額$100,000が上位3名を対象に賞金が支払われるようです。
賞金と引き換えに、解析手法とモデルをメルカリに提供する仕組みになっています。
賞金が出るとなると燃えますね！

他に考えられるコンペティション参加のモチベーションとしては、

- 世界中の優秀な人達と競争ができる
- 高品質な実際のデータ・セットで解析ができる
- 自身の解析に対するフィードバックが得られる

などが考えられるでしょうか。

# 参加したコンペティション
今回、私が参加したコンペティションは、チュートリアルコンペティションと呼ばれる初心者向けのコンペティションです。チュートリアルなので賞金はありません。
テーマは、「[タイタニックの生存者予測](https://www.kaggle.com/c/titanic)」です。

データは以下の3種類が与えられます。

- gender_submission.csv：結果のデータ形式を示しています。
- test.csv：テストデータです。このデータから生存者を予測します。
- train.csv：学習データです。このデータを学習させモデルを作ります。

データ解析を進める際は、「test.csv」と「train.csv」を扱います。
2種類のデータには、タイタニックの乗客の情報がそれぞれ入っています。
2種類のデータの大きな違いは、「Survived（生存かどうか）」のカラム有無です。
「train.csv」には「Survived」カラムがありますが、「test.csv」にはありません。

もうお分かりだと思いますが、「test.csv」の「Survived（生存かどうか）」を予測することが今回のミッションです。

# 今回のデータ解析手法
私の場合、データ解析や機械学習は、全くの初心者でなにから手をつけてたらよいか全く検討が付きませんでした。
そんな私みたいな人のために、チュートリアルにおすすめの解析手法が公開されています。

なので今回は、その解析手法を参考にしながら予測をしてみようと思います。

参考にした解析手法
[introduction-to-ensembling-stacking-in-python](https://www.kaggle.com/arthurtok/introduction-to-ensembling-stacking-in-python)

ちなみに、Kaggleでは、解析手法とプログラムのことをカーネルといいます。
そして、Kaggleから提供されている機能を使うことで、カーネルをフォークして、実行、データ提出まで全て完了できてしまいます。また、プログラムを実行するためのリソースをKaggle上で提供してくれているのも良心的ですね。

それでは、具体的にどのような解析手法かという話に進みましょう。
カーネルのタイトルにあるように、「アンサンブル学習」の「Stacking（スタッキング）」という手法です。

まず「アンサンブル学習」ですが、機械学習において、単一の学習器ではなく、複数の学習器を組み合わせる手法のことを指します。

次に「Stacking（スタッキング）」ですが、アンサンブル学習の1つで、最初の学習器の出力結果を次の学習器へ入力に使う手法です。つまり、モデルを積み上げ、多段階に学習と予測を繰り返すことで複数の学習器を組み合わせるということです。

他にも、アンサンブル学習には、ブートストラップサンプリングという統計手法で得たデータを複数の学習器に学習させて予測する「Bagging（バギング）」、学習器を連続的に学習させることで、予測精度を向上させる「Boosting（ブースティング）」が存在します。

# データの確認
それでは、データ解析を始めていきます。
まずは、学習データを確認します。

train.csv

| PassengerId      | Survived  | Pclass      | Name                                             | Sex    | Age  | SibSp | Parch |Ticket             | Fare     | Cabin | Embarked |
|:-----------------|:----------|:------------|:-------------------------------------------------|:-------|:-----|:------|:------|:------------------|:---------|:------|:---------|
| 1                | 0         | 3           | Braund, Mr. Owen Harris                          | male   | 22.0 | 1     | 0     |A/5 21171          | 7.2500   | NaN   | S        |
| 2                | 1         | 1           | Cumings, Mrs. John Bradley (Florence Briggs Th...| female | 38.0 | 1     | 0     |PC 17599           | 71.2833  | C85   | C        |
| 3                | 1         | 3           | Heikkinen, Miss. Laina                           | female | 26.0 | 0     | 0     |STON/O2. 3101282   | 7.9250   | NaN   | S        |

各カラムの意味は以下です。

- PassengerID：ユニークなID
- Survived：生存かどうか(0 = NO, 1 = Yes)
- Pclass：乗客の階級
- Name：名前
- Sex：性別
- Age：年齢
- SibSp：同乗した兄弟、配偶者の人数
- Parch：同乗した親、子供の人数
- Ticket：チケット番号
- Fare：旅客運賃
- Cabin：客室
- Embarked：乗船した港

次に学習データの欠損値を確認します。
pythonのpandasでtrain.csvを読み込み、`train.isnull().sum()`の結果が以下です。

```
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
```

いくつか欠損値があるので、補完する必要がありそうです。
記載するのを割愛しましたが、テストデータについても同様に欠損値を確認します。

また、参考にしたカーネルでは、seabornやmatplotlib、plotlyを使ってヒートマップなどのデータを可視化し、データの相関関係なども確認していました。
外れ値が含まれていないかや、学習データとテストデータを比較しデータの分布の類似性の確認などをすることで、どのデータを使ってモデルを作るのかを決定します。

今回は記載する分量の関係上、予測結果をKaggleに提出することを優先させたので、データの可視化については、割愛します。

# モデルの作成、学習と予測
データの確認をしたので、解析するモデルをプログラミングしていきます。
今回は以下の学習モデルを組み合わせます。

- Random Forest
- Extra Trees classifier
- AdaBoost classifer
- Gradient Boosting classifer
- Support Vector Machine

作成したプログラムを分割して簡単に説明していきます。
細かい説明は、プログラムのコメントを参照してください。
使用している学習モデルや設定しているパラメータの説明については今回割愛します。

まずは、ライブラリのインポート。

```python3
# 使用するライブラリのインポート
import pandas as pd
import numpy as np
import re
import sklearn
import xgboost as xgb

# 使用するモデルのインポート
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier, ExtraTreesClassifier
from sklearn.svm import SVC
from sklearn.cross_validation import KFold

import warnings
warnings.filterwarnings('ignore')
```

データセットを読み込みます。

```python3
train = pd.read_csv('input/train.csv')
test = pd.read_csv('input/test.csv')

PassengerId = test['PassengerId']
full_data = [train, test]
```

すこし長くなりますが、データの整理・整形と新しい特徴量を追加していきます。
ここで欠損値も補完します。

```python3
# データの整理・整形、新しい特徴量の追加
# 乗客の名前の長さを追加
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)

# 同乗した兄弟、配偶者の人数（SibSp）と同乗した親、子供の人数（Parch）からファミリーサイズを追加する
for dataset in full_data:
    dataset['FamilySize'] = dataset['SibSp'] + dataset['Parch'] + 1
# ファミリーサイズから1名の乗客かどうかを追加する
for dataset in full_data:
    dataset['IsAlone'] = 0
    dataset.loc[dataset['FamilySize'] == 1, 'IsAlone'] = 1

# 乗船した港（Embarked）に欠損値があるため、Southamptonで補完する
# C = Cherbourg, Q = Queenstown, S = Southampton
for dataset in full_data:
    dataset['Embarked'] = dataset['Embarked'].fillna('S')

# 運賃（Fare）に欠損値があるため、中央値で補完する
for dataset in full_data:
    dataset['Fare'] = dataset['Fare'].fillna(train['Fare'].median())

# 年齢（Age）に欠損値があるため、平均と標準偏差から乱数を生成し、補完する
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)

# 乗客の名前から敬称（MrやMsなど）を抽出してくる関数を定義する
def get_title(name):
    title_search = re.search(' ([A-Za-z]+)\.', name)
    if title_search:
        return title_search.group(1)
    return ""

# 乗客の名前（Name）から敬称を抽出し、追加する
for dataset in full_data:
    dataset['Title'] = dataset['Name'].apply(get_title)
# 敬称（Title）をグルーピングしたもので書き換える
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')

# データの整形
for dataset in full_data:
    # 性別（Sex）を定数化する
    dataset['Sex'] = dataset['Sex'].map( {'female': 0, 'male': 1} ).astype(int)
    
    # 敬称（Title）を定数化する
    title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}
    dataset['Title'] = dataset['Title'].map(title_mapping)
    # 敬称（Title）がないレコードを0埋めする
    dataset['Title'] = dataset['Title'].fillna(0)
    
    # 乗船した港（Embarked）を定数化する
    dataset['Embarked'] = dataset['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)
    
    # 運賃（Fare）を定数化する
    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)
    
    # 年齢（Age）を定数化する
    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)
test  = test.drop(drop_elements, axis = 1)
```

これでやっと学習させるためのデータ準備ができたので、学習と予測へ進みます。
学習させる途中で登場する交差検証（クロスバリデーション）は、より精度の良いパラメータを設定するための検証方法で、この検証をすることで、より過学習せず汎化性能の高いモデルを作ることができます。

```python3
ntrain = train.shape[0]
ntest = test.shape[0]

# 交差検証の回数
NFOLDS = 5
# 同じ乱数を発生させるための固定値
SEED = 0
# パラメータの検証、学習モデルの精度を評価
# 交差検証（クロスバリデーション）でパラメータを検証し、精度がよく、過学習が起こらないパラメータを決定する
kf = KFold(ntrain, n_folds= NFOLDS, random_state=SEED)

# Sklearn classifier を拡張
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 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)

```

モデルのパラメータを定義し、第1段階の学習と予測をしていきます。
学習データとテストデータを5つの学習器で学習、予測します。
第1段階の結果は、第2段階のインプットとなるため、学習データとテストデータがアウトプットになります。

```python3
# モデルのパラメータ定義
# Random Forest のパラメータ
rf_params = { 'n_jobs': -1, 'n_estimators': 500, 'warm_start': True, 'max_depth': 6, 'min_samples_leaf': 2, 'max_features' : 'sqrt', 'verbose': 0 }
# Extra Trees のパラメータ
et_params = { 'n_jobs': -1, 'n_estimators':500, 'max_depth': 8, 'min_samples_leaf': 2, 'verbose': 0 }
# AdaBoost のパラメータ
ada_params = { 'n_estimators': 500, 'learning_rate' : 0.75 }
# Gradient Boosting のパラメータ
gb_params = { 'n_estimators': 500, 'max_depth': 5, 'min_samples_leaf': 2, 'verbose': 0 }
# Support Vector Classifier のパラメータ 
svc_params = { 'kernel' : 'linear', 'C' : 0.025 }

# モデルのオブジェクト生成
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)

# 学習データの生存（Survived）データ、学習データ、テストデータで配列を作成する
y_train = train['Survived'].ravel()
train = train.drop(['Survived'], axis=1)
x_train = train.values
x_test = test.values

# 第1段階の学習と予測を実行する
et_oof_train, et_oof_test = get_oof(et, x_train, y_train, x_test) # Extra Trees Classifier
rf_oof_train, rf_oof_test = get_oof(rf,x_train, y_train, x_test) # Random Forest Classifier
ada_oof_train, ada_oof_test = get_oof(ada, x_train, y_train, x_test) # AdaBoost Classifier
gb_oof_train, gb_oof_test = get_oof(gb,x_train, y_train, x_test) # Gradient Boost Classifier
svc_oof_train, svc_oof_test = get_oof(svc,x_train, y_train, x_test) # Support Vector Classifier
```

続いて、第1段階の予測結果をインプットとし、第2段階の予測をします。
最後に第2段階の予測結果から提出用のCSVファイルを出力します。

```python3
# 第1段階の予測結果をインプット
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)
# 第2段階の学習と予測を実行する
gbm = xgb.XGBClassifier(n_estimators= 2000, max_depth= 4, min_child_weight= 2, 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)

# 予測結果をCSV出力
StackingSubmission = pd.DataFrame({ 'PassengerId': PassengerId,'Survived': predictions })
StackingSubmission.to_csv("StackingSubmission.csv", index=False)
```

# 予測結果の提出
実際にKaggleへ予測結果を提出してみました。
スコアは0.799904で、1709位（9815チーム中）でした。

![結果.png](https://qiita-image-store.s3.amazonaws.com/0/217344/59920995-2d98-5fac-c542-f98c65c765e0.png)

# 最後に
公開されているカーネルを見よう見まねで進めましたが、データ解析の進め方の全体像を掴むことができました。
データの確認やデータの整形のように、何のデータを使って解析するのかを決める工程が重要であることが理解できました。

今回使用した学習器に対するに理解や解析手法の理解を進めていきたいです。
次のステップとしては、チュートリアルコンペティションではなく、賞金が出るコンペティションに参加してみようと思います。