【入門】Titanic号乗客の生死を予想しよう！(3)

# はじめまして
始めまして！株式会社 Panta Rheiの「かず」と申します。[pandasista](https://x.com/sotsogprinciple) という名前でXをやっております。普段はデータをねりねりしたりCEOをしたりしています。

![IMG_0587.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540865/dd51afda-2b66-ccba-f810-4efdac1860d4.jpeg)
本記事は4年前に勉強会で用いた記事になります！
ぜひいいねを押してってください🙌

# 導入

本記事の構成は4部となっており
1. <B>[Titanicデータセットと探索的データ分析(EDA)](https://qiita.com/realmadridmarcelo/items/f64431a25af3c87a3552)</B>
1. <B>[探索的データ分析(EDA)の続きと特徴量エンジニアリング](https://qiita.com/realmadridmarcelo/items/9cd476d839fb9c36554b)</B>
1. <B>[特徴量エンジニアリングの続きとAIモデルへの適用](https://qiita.com/realmadridmarcelo/items/507b659a830e2c3928d6)</B>
1. <B>[djangoを用いた簡単なTitanic乗客生死予測アプリケーションの作成](https://qiita.com/realmadridmarcelo/items/ec5ca723ec935ee90d75)</B>

という内容でお届けしています。
本記事は3章目！特徴量エンジニアリングと実際のモデルへの適用を紹介します！

以下の記事を読んだ後に本Titanicシリーズを読むと段階的に理解ができます。
1. [【実装】　ボストンの住宅価格推測AIを作ろう【前編】](https://qiita.com/realmadridmarcelo/items/0bfcbde3e4092ea5c86e)
1. [【実装】　ボストンの住宅価格推測AIを作ろう【後編】](https://qiita.com/realmadridmarcelo/items/0ac4490de2007e43f65c)

# 続き
さて、今回からついにAIの実装となります。前回の振り返りを少しだけすると、
「EDAをして」「各カラムごとに前処理する方針を定める」といった感じでした。その方針は、

・PClass…ワンホットエンコーディング
・Name…削除
・Sex…数字化(0or1)
・Age…中央値で欠損値埋め
・SibSp…そのまま
・Parch…そのまま
・Ticket…削除
・Fare…そのまま
・Cabin…削除
・Embarked…ワンホットエンコーディング

といった形でした。これからこの処理をまとめて行う関数を実装していきましょう。

# データを取得

```python:INPUT
import pandas as pd

train_data = pd.read_csv('titanic/train.csv')
test_data = pd.read_csv('titanic/test.csv')
```

# 前処理関数の実装

```python:INPUT
def pretreatment(_df):
    _df = _df.drop(['Name', 'Ticket', 'Cabin'], axis=1)  # 不要カラムの削除
    _df['Age'] = _df['Age'].fillna(_df['Age'].median())  # Ageカラムの欠損値埋め
    _df = pd.get_dummies(_df, columns=['Sex'], drop_first=True, prefix='', prefix_sep='')  # 男性を1、女性を0と数字化
    _df = pd.get_dummies(_df, columns=['Pclass', 'Embarked'])  #ワンホットエンコーディング
    return _df
```


```python:INPUT
treated_train_data = pretreatment(train_data)
treated_train_data
```

OUTPUT
<img width="872" alt="スクリーンショット 2020-05-24 22.10.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540865/f66515d5-6629-b05c-deec-d3abe80a11c6.png">

```python:INPUT
treated_test_data = pretreatment(test_data)
treated_test_data
```

OUTPUT
<img width="799" alt="スクリーンショット 2020-05-24 22.13.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540865/52a1ced7-845b-f137-e807-4faecc17cd63.png">

しっかりtrain_dataにもtest_dataにも処理が適用されています。処理関数を実装したら、しっかりと出力を確認しましょう。ワンホットエンコーディングしたときは出力されるテーブルのカラムが異なることがあります。例えばtrain_dataのEmbarkedには「C」「Q」「S」の3種類がありました。ここでtest_dataに「C」「Q」しかなかったとします。この時、ワンホットエンコーディングの処理をすると、train_dataにはEmbarkedの3列のカラムができますが、test_dataには2列しかできません。学習器にかける時は投げるテーブルのカラムを統一しなければいけないので、出力の確認はやはり必須です。pandasにはワンホットエンコーディングする時にこの問題に対処する方法がありますが、欠損値埋めを必要としたりなど処理が複雑になるため、今回は割愛して単純にワンホットエンコーディングしました。今回の出力はtrain_dataもtest_dataもカラムはSurvivedを除けば種類も順番も同じなので、どちらのテーブルにも同等の処理ができたことになります。

# アルゴリズムの選定
sklearnの公式サイトには、データセットによってどんなアルゴリズムを使うかの目安を示した[チートシート](https://scikit-learn.org/stable/tutorial/machine_learning_map/index.html)があります。以下がそのチートシートとなります。
![ml_map.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540865/43534d54-5631-86d8-8931-dfe42196d40f.png)
今回はこのチートシートに従ってアルゴリズムを選定していきましょう。

1. 50以上データがあるか
→<B>Yes</B>…train_dataだけで891レコードあるので十分です

2. 予測するのはカテゴリか
→<B>Yes</B>…今回は予測する対象が連続値ではなく離散値(カテゴリ)です

3. ラベル付きデータか
→<B>Yes</B>…train_dataの方に正解ラベル(Survived)があります。補足として、正解ラベルがないところから2つに分類する時は「<B>クラスタリング</B>」と言うタスクになり「<B>教師なし学習</B>」に分類されます。今回は「<B>こんな正解があるよ(train_data)</B>」から「<B>正解データ(教師)を元に別のデータを予測してみよう(test_data)</B>」と言う形なので「<B>教師あり学習</B>」に分類されます。

4. データ数は10万個以下か
→<B>No</B>…前述の通りです。タイタニックには10万人も乗船できません。

5. <B>LinearSVC</B>を試してみよう
6. 上手くいかなくて、テキストデータでなければ<B>k最近傍法</B>を試してみよう
7. 上手くいかなかったら<B>SVC(Support Vector Machine Classification)</B>か<B>アンサンブル分類器</B>を試してみよう

こういった流れになります。初手としてはLinearSVC、その次にk最近傍法、それでもダメだったらSVCかアンサンブル分類器を使ってみよう。と言う形になります。早速SVCをLinearSVCを実装していきましょう。

# LinearSVCのモデル
###モデルの舟形作成
モデルの作成を一気にやっていきましょう。

```python:INPUT
# モジュールのインポート
from sklearn.svm import LinearSVC
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import KFold

# 特徴量と目的変数の分割
X = treated_train_data.drop(['PassengerId','Survived'], axis=1)
y = treated_train_data['Survived']

# 標準化
sc = StandardScaler()
sc.fit(X)
X_std = sc.transform(X)

# SVCモデルのインスタンス化
clf = LinearSVC(C=1.0, class_weight='balanced', random_state=0, max_iter=10000)

# KFoldでデータを5分割し学習、resultに精度(正解率)を代入
kf = KFold(n_splits=5)
result = cross_val_score(clf, X_std, y, cv = kf, scoring = "accuracy")

# 5分割したデータのそれぞれの精度とその平均値を出力
print(result)
print(result.mean())
```


```python:OUTPUT
[0.76536313 0.78651685 0.76966292 0.75280899 0.81460674]
0.7777917268219194
```

#### モジュールのインポート
今回使用するパッケージをインポートしています。

#### 特徴量と目的変数の分割
train_dataからSurvivedを落としたもの(<B>特徴量テーブル</B>)をX、train_dataのSurvivedだけ(<B>正解ラベル</B>)をyに代入しています。

#### 標準化
「<B>標準化</B>」とはテーブルのスケールを揃える方法です。どういうことかというと「年齢カラムは最大値90で最小値0、価格カラムは最大値100,000,000で最小値1,000」といった様に、カラム間で値のスケールが異なると、<B>学習に時間がかかったり精度の低下につながったり</B>してしまいます。これを避けるために特徴量テーブルの全てのカラムについて「平均0、分散1」にする操作を加えています。
1行目は「標準化するStandardScalerをscという名前でインスタンス化したよ」
2行目は「X(特徴量テーブル)の情報をscに知らせているよ」
3行目は「その情報を元にXを標準化して新しく取得したテーブルをX_stdに代入したよ」

X_stdを見てみましょう。

```python:INPUT
X_std
```


```python:OUTPUT
array([[-1.73010796, -0.56573646,  0.43279337, ..., -0.48204268,
        -0.30756234,  0.61930636],
       [-1.72622007,  0.66386103,  0.43279337, ...,  2.0745051 ,
        -0.30756234, -1.61470971],
       [-1.72233219, -0.25833709, -0.4745452 , ..., -0.48204268,
        -0.30756234,  0.61930636],
       ...,
       [ 1.72233219, -0.1046374 ,  0.43279337, ..., -0.48204268,
        -0.30756234,  0.61930636],
       [ 1.72622007, -0.25833709, -0.4745452 , ...,  2.0745051 ,
        -0.30756234, -1.61470971],
       [ 1.73010796,  0.20276197, -0.4745452 , ..., -0.48204268,
         3.25137334, -1.61470971]])
```

numpyを覚えていますか?行列を扱うパッケージでpandasよりも表計算が早いものの視認性が低いといった特徴があります。scikit-learnは内部的にnumpyを使っているので、pandasのテーブルを投げてもnumpy配列を出力してきます。上に表示されているテーブルはnumpyの配列なので非常に見にくいです。pandasに変換しましょう。

```python:INPUT
pd.DataFrame(X_std, columns=X.columns)
```

OUTPUT
<img width="922" alt="スクリーンショット 2020-05-25 1.33.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540865/3355ea9f-313d-54c5-ffd1-4b07bdfe389f.png">
見やすくなりましたね。これを見ると全てのカラムの値が0に近い値になっていて、値のスケールが調整されたことが分かります。

#### SVCモデルのインスタンス化
今回使うLinearSVCを
C=1.0, class_weight='balanced', random_state=0, max_iter=10000
というパラメータ設定でインスタンス化しました。LinearSVCはCのパラメータが非常に重要になるのであとで調整します。

#### KFoldでデータを5分割し学習、resultに精度(正解率)を代入
「<B>KFold</B>」初登場のワードです。これはモデルの精度を正確に評価するための操作です。最初にXとyのペアをk分割(今回は5分割)します。そしてそのうちのk-1(4)つを用いてモデルを学習させ、残りの1つでそのモデルの精度を評価します。これを異なるk-1(4)つの組を使って合計k(5)回繰り返します。この時k(5)個の評価指標が得られるので、その平均値をそのモデル自体の精度とします。

![k-fold-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540865/22fccaf5-0a36-89fe-15c8-5393ef894b1b.png)
参照:(https://axa.biopapyrus.jp/machine-learning/model-evaluation/k-fold-cross-validation.html)

train_test_splitはただ単に割合に応じてデータを2分割して訓練データと評価データとしていましたが、これは分割によっては「たまたま」良い精度が出てしまったりします。学習したモデルが評価用データに対して良い精度を示したが、それは偶然で他のデータに対しては精度が低くなってしまうかもしれません。
KFoldはデータを5個に分けて5回学習してその精度を出すので、train_test_splitよりも精度への信頼性が高いです。


1行目は「KFoldを実行するインスタンスの分割数5に設定したよ」
kf = KFold(n_splits=5)

2行目は「X_stdとyをkfで分割してそれぞれclfのモデルに学習させ、その精度をaccuracy(正答率)で評価したよ」
result = cross_val_score(clf, X_std, y, cv = kf, scoring = "accuracy")

#### 5分割したデータのそれぞれの精度とその平均値を出力
これを見るとresultは5つの値が入っています。これはそれぞれのKFoldでの評価指標(正答率)です。ばらつきがあります。やはり学習するデータや評価するデータによって精度が変わることが示されています。その平均値は0.7777917268219194となっています。Kaggleでの最初の提出のスコアが0.76555だったことから、これは精度が上ったと考えられます(実際に提出してみないと分からないですが)。

ここまでで非常に大変だったと思いますが、ここまで一連の操作の結論は
「<B>LinearSVCの設定がC=1.0の時、そのモデルの精度は約0.778</B>」
となります。とりあえず精度が出ました。あとはパラメータチューニングをして、最終的なLinearSVCの精度を出しましょう。

### パラメータチューニング
LinearSVCのモデルでのC=1での精度は出ました。ここからはこのCの値を調整して精度の最も高いLinearSVCモデルを作りましょう。

```python:INPUT
# パラメータ調整用のモジュールをインポート
from sklearn.model_selection import GridSearchCV

# 調整するパラメータの詳細設定
tuned_parameters = [
    {'C': [0.01, 0.1, 1, 10, 100, 1000, 10000],
     'class_weight': ['balanced'],
     'random_state': [0],
     'max_iter': [10000]}]

# グリッドサーチの設定
clf_GS = GridSearchCV(
         LinearSVC(),  # 使用するモデル
         tuned_parameters,  #調整するパラメータのリスト
         cv=5,  # KFoldの分割数
         scoring='accuracy'  # 評価指標
)

# グリッドサーチによる精度測定&出力
clf_GS.fit(X_std, y)
print(clf_GS.best_params_)
print(clf_GS.best_score_)
```


```python:OUTPUT
{'C': 10, 'class_weight': 'balanced', 'max_iter': 10000, 'random_state': 0}
0.7811436821291821
```

これは「<B>Cが10の時に一番いい精度(約0.781)が出たよ</B>」と言っています。先ほどのC=1の時よりも精度が向上していますね。細かく解説していきます。

#### パラメータ調整用のモジュールをインポート
パラメータ調整のパッケージにはsklearnに付属した「<B>GridSearchCV</B>」と2018年12月にPreferedNetworks社がリリースした「<B>optuna</B>」があります。後者の方が高速で細かいパラメータ調整ができるものの、リリースが最近という理由でanacondaに付属していません。pip3やcondaを使えば利用はできるものの、今回は環境面から採用を見送ります。対するGridSearchCVはoptunaが出るまでパラメータチューニングのファーストチョイスでした。オライリーなど多くの書籍ではこちらの方が多く取り上げられていて、アルゴリズムも理解しやすいです。パラメータ候補のリストを投げると<B>総当たり</B>で調べ上げてくれます。

#### 調整するパラメータの詳細設定
調整するパラメータの候補を辞書型で指定します。今回はCしかチューニングしないので「'C': [0.01, 0.1, 1, 10, 100, 1000, 10000]」以外の部分は固定になっています。このリスト内の数字全てに対して学習を実行し、精度を測定します。

#### グリッドサーチの設定
以上がグリッドサーチの記法です。少し複雑です。暗記してしまいましょう。最初にモデルを指定、その次に調整するパラメータのリスト、その次KFoldの分割数、最後に評価指標を指定します。データをclf_GSに投げると勝手にKFoldして精度を評価してくれるので便利です。

#### グリッドサーチによる精度測定&出力
fit(特徴量デーブル, 正解ラベル)とすると学習が始まります。しばらくして学習が終わると、clf_GSにこの総当たりの学習で一番精度の高かったパラメータとその精度が記憶されているので出力します。

ここで初めて最終的な精度が得られました。結果は
「</B>LinearCSVのアルゴリズムではC=10のときが最も精度が高くその精度は0.781だった</B>」
となります。

# LinearCSVのモデルでtest_dataで予測値を出力
ここからは一気にいきます。

```python:INPUT
treated_test_data = treated_test_data.fillna(test_data.median(numeric_only=True))

clf = LinearSVC(C=10, class_weight='balanced', random_state=0, max_iter=10000)
clf.fit(X_std, y)

X_test = treated_test_data.drop(['PassengerId'], axis=1)

X_test_std = sc.transform(X_test)

y_test = clf.predict(X_test_std)
```

1行目「実はtrain_dataにはなかったFareの欠損がtest_dataにはあったので平均値で穴埋めしたよ」
3行目「C=10に設定し直したLinearSVCモデルをインスタンス化したよ」
4行目「train_dataで学習したよ」
6行目「test_dataから乗客識別番号を落としたよ」
8行目「test_dataを標準化したよ」
10行目「学習器にtest_dataを投げてその予測値をy_testに代入したよ」

天下り的ですがtest_dataのFareには欠損があります。train_dataにはなかったのでここは応急処置的に平均値で穴埋めしています。
さて予測結果はどうだったんでしょうか。

```python:INPUT
y_test
```


```python:OUTPUT
array([0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0,
       1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1,
       1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1,
       1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1,
       1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0,
       0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0,
       1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1,
       0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1,
       1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1,
       0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0,
       1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
       0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1,
       0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0,
       0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0,
       0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0,
       1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0,
       0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0,
       1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1,
       0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0])
```

1と0の予測値がしっかりと出力されていますね。

#出力を整形してKaggleに提出

```python:INPUT
y_pred_series = pd.Series(y_test, name='Survived')

LinearSVC_submission = pd.concat([test_data['PassengerId'], y_pred_series], axis=1)

LinearSVC_submission.to_csv('titanic/LinearSVC.csv', index=False)
```

1行目は「y_testをpandasの一列にしたよ」
3行目は「y_testのPassengerIdとy_testをくっつけたよ」
5行目は「このテーブルをcsvファイルとしてtitanicフォルダにLinearSVC.csvという名前で保存したよ」

はい。やっと予測結果が出力できました。LinearSVC_submissionを見てみましょう。

```python:INPUT
LinearSVC_submission
```

OUTPUT
<img width="193" alt="スクリーンショット 2020-05-25 4.45.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540865/0ecefd02-9e53-1d43-bf9f-d8c713ec6588.png">
いい感じのテーブルが得られました。これをKaggleに提出してみましょう！

# お疲れ様でした！
非常に大変だったと思います。ただここに書いてあることを実行できる様になれば、例えどんなテーブルデータがきても対処できるようなります。基本的にはここに書いてあることの応用です。皆さんも是非、新しいキャリアの選択肢やスキルとして「AI」に挑戦してみてください。本当にお疲れ様でした！


# 続き

以下の記事をご覧ください！
[【入門】Titanic号乗客の生死を予想しよう！(4)]()

# 紹介
[株式会社 Panta Rhei](https://www.pantarhei.co.jp)では「Everything Analysable」を標榜とし、世の中にあるあらゆる定性的な対象をAIによって定量化する事業をおこなっております。
AIに召し上がっていただく綺麗なデータを作る、データフローを整形するなどの課題や、そもそものAIの内製化等、挑戦したい試みがあればぜひご一報ください！