【Python機械学習】Kaggleのタイタニック予測問題に挑戦

#はじめに
大学院で人工知能のモデルを研究している10個下の後輩に、`Kaggle`なるものを教えてもらったので、
早速、初めての機械学習、ならびにKaggleに挑戦してみました！
[Titanic - Machine Learning from Disaster](https://www.kaggle.com/c/titanic/)

ウェブマーケやってたときみたいにデータ取り込んで分析して〜っていうこの流れ
**めちゃめちゃワクワクする！！**


![スクリーンショット 2021-06-08 15.51.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/4b1e7ad4-46f3-2578-55a4-b963a5168386.png)

#`Kaggle`とは
![site-logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/a2ddf3dd-8364-91ec-1730-45ed943d74b1.png)

>Kaggleは企業や研究者がデータを投稿し、世界中の統計家やデータ分析家がその最適モデルを競い合う、予測モデリング及び分析手法関連プラットフォーム及びその運営会社である。
参照元:[wikipedia](https://ja.wikipedia.org/wiki/Kaggle)

Kaggleというのはプラットフォーム兼、運営会社なんですね。
いっぱい機械学習用のデータセットと課題が並んでいる。賞金モデルとか挑戦しがいがあるな〜


#`Titanic - Machine Learning from Disaster`の概要
>The sinking of the Titanic is one of the most infamous shipwrecks in history.
On April 15, 1912, during her maiden voyage, the widely considered “unsinkable” RMS Titanic sank after colliding with an iceberg. Unfortunately, there weren’t enough lifeboats for everyone onboard, resulting in the death of 1502 out of 2224 passengers and crew.
While there was some element of luck involved in surviving, it seems some groups of people were more likely to survive than others.
In this challenge, we ask you to build a predictive model that answers the question: “what sorts of people were more likely to survive?” using passenger data (ie name, age, gender, socio-economic class, etc).

```text:日本語翻訳(DeepL)
タイタニック号の沈没は、歴史上最も悪名高い沈没事故のひとつです。

1912年4月15日、処女航海中の「不沈艦」と言われたRMSタイタニック号は、
氷山に衝突して沈没しました。不幸にも救命ボートの数が足りず、
乗客・乗員2224名のうち1502名が亡くなりました。

生き残るには運もありますが、あるグループは他のグループよりも生き残りやすいようです。

この課題では、
乗客のデータ（名前、年齢、性別、社会経済的階級など）を使って、
「どのような人たちが生き残りやすかったのか」
という問いに答える予測モデルを構築していただきます。
```

#手順
- データの読み込み
- 欠損値の補間
    - n/aなど平均値や０で埋める
    - カテゴリー(例：男、女）は、番号にして、フラグやEnum扱いに置換
- 複数のモデルで検証し、一番良いモデルを選択
- テストデータをそのモデルに当てはめて予測
- 結果のアウトプット

#コード
##ディレクトリ構成
```text:titanic
.
├── model.py # モデルを取得するファイル
├── result
│   └── submission.csv # 結果出力ファイル(csv)
├── seed
│   ├── titanic_test.csv # テストデータ(予測用)
│   └── titanic_train.csv # 学習データ
└── titanic.py # 実行ファイル
```

##titanic.py
```bash
$ pip install scikit-learn # ver 0.24.2
```

```python:titanic.py
import numpy as np
import pandas as pd
import copy
import model
from sklearn.model_selection import cross_val_score
# ハイパーパラメータの探査アルゴリズム...今回は断念
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV 
"""
▽Data Set
・PassengerId：データにシーケンシャルでついている番号
・Survived：生存（0 = No, 1 = Yes）　訓練用データにのみ存在
・Pclass：チケットのクラス（1 = 1st, 2 = 2nd, 3 = 3rd）
・Name：名前
・Sex：性別
・Age：年齢
・SibSp：タイタニック号に乗っていた兄弟と配偶者の数
・Parch：タイタニック号に乗っていた両親と子どもの数
・Ticket：チケット番号
・Fare：旅客運賃
・Cabin：船室番号
・Embarked：乗船場（C = Cherbourg, Q = Queenstown, S = Southampton）
"""

# 使用するカラムを指定：チケットのクラス、性別、年齢、タイタニック号に乗っていた兄弟と配偶者の数、タイタニック号に乗っていた両親と子どもの数、旅客運賃、乗船場
predictors = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked"]

def exec():
    # データ読み込み
    train_raw = pd.read_csv("./seed/titanic_train.csv", dtype={"Age": np.float64}, )
    test_raw  = pd.read_csv("./seed/titanic_test.csv", dtype={"Age": np.float64}, )
    # 欠損値の補間：（※一旦、ディープコピーして別物インスタンスに)
    train_corrected, test_corrected = correct_data(copy.deepcopy(train_raw), copy.deepcopy(test_raw))
    results = []
    names = []
    models = model.get_models()
    for name, m in models:
        # 各モデルを使って交差検証を実施
        # x:使用するデータセット(カラム指定)、y:予測する値、cv:○個あるデータブロックのうち、1個を検証用データ、その他を訓練データとして使う
        result = cross_val_score(m, train_corrected[predictors], train_corrected["Survived"], cv=3)
        names.append(name)
        results.append(result)

    # 予測率が一番高いモデルを取得
    best_model = get_best_model(names, results)

    algo = None
    if best_model[0]['name'] == 'LogisticRegression':
        algo = model.LogisticRegression(max_iter=5000)
    elif best_model[0]['name'] == 'SVC':
        algo = model.SVC()
    elif best_model[0]['name'] == 'LinearSVC':
        algo = model.LinearSVC(dual=False)
    elif best_model[0]['name'] == 'KNeighbors':
        algo = model.KNeighborsClassifier()
    elif best_model[0]['name'] == 'DecisionTree':
        algo = model.DecisionTreeClassifier()
    elif best_model[0]['name'] == 'RandomForest':
        algo = model.RandomForestClassifier()
    else:
        algo = model.MLPClassifier(solver='lbfgs', random_state=0, max_iter=5000)

    # テストデータにおける予測とそのアウトプット
    output_result_as_csv(algo, train_corrected, test_corrected)

def correct_data(train_raw, test_raw):
    # テストデータのmedianをとったほうが精度向上する。
    train_raw.Age = train_raw.Age.fillna(test_raw.Age.median()) 
    train_raw.Fare = train_raw.Fare.fillna(test_raw.Fare.median())
    test_raw.Age = test_raw.Age.fillna(test_raw.Age.median())
    test_raw.Fare = test_raw.Fare.fillna(test_raw.Fare.median())

    # カテゴリーの補間
    train_data = correct_common_data(train_raw)
    test_data = correct_common_data(test_raw)

    return train_data, test_data

def correct_common_data(titanic_data):
    titanic_data.Sex = titanic_data.Sex.replace(['male', 'female'], [0, 1])
    titanic_data.Embarked = titanic_data.Embarked.fillna("S") # これはバイアスかも
    titanic_data.Embarked = titanic_data.Embarked.replace(['C', 'S', 'Q'], [0, 1, 2])
    return titanic_data

def get_best_model(names, results):
    best_model = []
    for i in range(len(names)):
        if i == 0:
            # 最初は比較対象の元となるので入れる
            best_model.append({'name':names[i], 'result':results[i].mean()}.copy())
        elif best_model[0]['result'] < results[i].mean():
            # 比較して、数値が良い方をセット
            best_model[0] = {'name':names[i], 'result':results[i].mean()}
    return best_model

def output_result_as_csv(algo, train_corrected, test_corrected):
    # グリッドサーチ/ランダムサーチによるハイパーパラメータの最適化をしたかった...断念。
    # ※ココでバグ発生する
    # param_grid = {
    #     "max_depth": [3, None], #distribution
    #     "n_estimators":[50,100,200,300,400,500],
    #     "max_features": sp_randint(1, 11),
    #     "min_samples_split": sp_randint(2, 11),
    #     "min_samples_leaf": sp_randint(1, 11),
    #     "bootstrap": [True, False],
    #     "criterion": ["gini", "entropy"]
    # }
    # gsc = GridSearchCV(algo, parameters, cv=3)
    # rsc = RandomizedSearchCV(estimator=algo, param_distributions=param_grid, cv=5, n_iter=30) # scoring="accuracy"
    # 生存者データをモデルにフィット     
    algo.fit(train_corrected[predictors], train_corrected["Survived"]) # gsc.fit
    # 欠損値補間済みのテストデータで予測
    predictions = algo.predict(test_corrected[predictors])
    # 必要カラムのみ取得
    submission = pd.DataFrame({
            "PassengerId": test_corrected["PassengerId"],
            "Survived": predictions
        })
    # CSV出力
    submission.to_csv('./result/submission.csv', index=False)

# importされたときにtitanic.py自体が実行されないようにするおまじない
if __name__ == "__main__":
    exec()

```
##`model.py`
```python:model.py
from typing import List
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC, LinearSVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neural_network import MLPClassifier

"""
# 各モデルのハイパーパラメータを探すための引数を設定するオブジェクト...断念
# model_param_set_random = {
#     LinearRegression(): {},
#     Lasso(): {},
#     Ridge(): {},
#     SVR(): {
#         "kernel": ["linear", "poly", "rbf", "sigmoid"],
#         "C": scipy.stats.uniform(0.00001, 1000)
#     },
#     RandomForestRegressor(): {
#         "n_estimators": scipy.stats.randint(10, 300),
#         "max_depth": scipy.stats.randint(1, 20)
#     }
# }

▽学習手法
・ロジスティック回帰
・サポートベクターマシン
・k-最近傍法
・決定木
・ランダムフォレスト
・ニューラルネットワーク
"""
def get_models()->list:
    models = []
    models.append(("LogisticRegression", LogisticRegression(max_iter=5000)))
    models.append(("SVC", SVC()))
    models.append(("LinearSVC", LinearSVC(dual=False)))
    models.append(("KNeighbors", KNeighborsClassifier()))
    models.append(("DecisionTree", DecisionTreeClassifier()))
    models.append(("RandomForest", RandomForestClassifier()))
    models.append(("MLPClassifier", MLPClassifier(solver='lbfgs', random_state=0, max_iter=10000)))
    return models

if __name__ == "__main__":
    get_models()
```


#実行結果：36961/45420位
**0.76315の正答率ですね！**
![スクリーンショット 2021-06-08 15.57.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/5862a061-55c4-3339-bf4c-94a2b4a02c05.png)

いいのか悪いのかも肌感でわからないこの、肌がゆさ。

まだまだモデルとか、どういったアルゴリズムで機械学習してその傾向性を導き出してるかなんて全然わかっておらず、悔しい気持ちでいっぱいですなあ。

また、モデルの精度向上なんかはもっと座学ベースで理論武装しないと感覚が掴めないですわ(｀・ω・´)


#モデルざっくり説明
| モデル名 | 説明 |
|:--|:--|
| ロジスティック回帰 | 「∫」　みたいな感じの線で予測する統計的回帰モデル |
| サポートベクターマシン | 「∴/∴」 みたいな感じで別グループにもっとも近い点を探して、その点と点を結ぶ距離が。それぞれ最大になるように分ける |
| k-最近傍法 | 「∴◎∴」　みたいな感じで学習データをベクトル空間上にプロットしておき、未知のデータが得られたら、そこから距離が近い順に任意のK個を取得し、多数決でデータが属するクラスを推定する |
| 決定木 | 「众」　みたいな感じで、分類木と回帰木を組み合わせたもので、ツリー（樹形図）によってデータを分析する手法 |
| ランダムフォレスト | 「众众众」　みたいな感じで決定木をたくさん集めたもので、それぞれの決定木で得られた結果を平均したり多数決を取ったりすることで、より汎化性能が高くなっているモデル |
| ニューラルネットワーク | 「井井井」 みたいな感じで人工ニューロン（ノード）が、学習によってシナプスの結合強度を変化させ、問題解決能力を持つようなモデル全般 |


#ハマったポイント
- 欠損値の補間が「補完」ではないことを初めて知った
    - ここの調整で結果が結構変わってきそうな予感
    - 自分はここの調整がまだまだできていない。
- モデルの選択はもう`sklearn`ライブラリに任せっきりで、自分で深く理解して選んだ感がないので、不安になる。
- テストデータを使って補間することで精度が上がることを初めて知った。
    - 補間の仕方で精度を調整できるので熟練の技が必要だ
- ハイパーパラメータを使うことで、モデルの精度を向上できるみたいだが、計算リソースを食ったり時間がかかるので、設定が巧妙じゃないとなかなか難しい。
    - もう少しお勉強が必要ですorz



#感想とまとめ

ウェブマーケやっていたときに、広告配信インプレッション数を溜め込んで、傾向性を把握するために、[タブロー](https://www.tableau.com/ja-jp)とかでグラフで可視化したりしていたので、案外とっつき易かったです。

機械学習、なるほど〜って感じ💻✨

結局やっていることって

**データを元に、傾向を把握して、その傾向にテストデータを当てはめたときに、どうなるかっていう、
既存のデータで作られた「線」の延長線上にテストデータを散りばめて
どっちですか！！？って予測してくもの**

なんだな〜って。

楽しい学問かつプログラミングでした！✨
もっと知識を深めて、データサイエンティストになりたい(☝︎ ՞ਊ ՞)☝︎


以上、ありがとうございました！


#参考記事
https://qiita.com/taki_tflare/items/8850ac5ba8b504a171aa

参考にさせていただきました！
概念をつかむのにめっちゃわかりやすかったです😊✨