【後編】KaggleのKarnel上でtitanic問題を解いてみた ~モデル作成~


[前回](https://qiita.com/takuya66520126/items/983adb0c7a64755ff40a)の続き。今回は実際に予測モデルの構築、kaggleへのサブミットまでを行う。


#動作環境
・Kaggle kernel上


#準備

##ライブラリのインポート

```py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
import plotly.offline as py
import plotly.graph_objs as go
import cufflinks as cf

py.init_notebook_mode(connected=True)
# デフォルトでPlotlyがオンラインモードになっているので、オフラインモードへと変更

#cf.go_offline()# 恒久的にデフォルトをオフラインモードに
```

##データセットの読み込み
```py
train=pd.read_csv("../input/train.csv")
test=pd.read_csv("../input/test.csv")
data=[train,test]
```

#前処理
##どう前処理するか

・欠損値を何らかの形で埋める
・不必要と思しきデータを排除する
・連続した数字データをカテゴライズ
(例えば、0~99までの数字を含むデータをそのまま扱うと100クラスに区別されたことになり、大雑把に多い、少ないを把握できない)
・文字データを数字におきかえる（分類器は数字しか受け付けない）
分類器につっこむ前に上記の処理が必要...

##具体的には....
欠損値を数えた結果、欠損しているのはAge, Cabin, Embarked。
●Cabinは891人中687人のデータが欠損しているので、あんまり使えなさそう
●Embarkedが欠損しているのは高々２名分なので、これはそれほど影響はなさそう。
●Name・・・生死には関係ないと思われる。ただし、Mr.やMrs.などの敬称は人物の属性を表すので関係あるかも。
●PassengerId・・・通し番号なので、関係が薄いと思われる。（断言はできないが）今回は排除。
●ticket・・・分類器には数字化したデータしか用いることができないことを考えると、規則性がわからない文字列に頼るのはあまり得策ではなさそう。

##使用する分類器

今回は、ランダムフォレストによって、予測モデルを構築する。

そのため、ゴールとしてはrandom forest classifierに突っ込むことだが、分類器は基本的には<b>数字のデータ</b>にしておかなければいけない。
今回のデータセットの意味を考えると、氏名が生死に影響するとは考えにくい。
しかし、"Name"にはすべて、"Mr"や"Mrs"といった敬称が含まれており、これはその人の属性を表すものとして、結果に影響を与えそうである。（性別や年齢のセグメントで敬称が変わるため）
そこで、"Name"から全員分の敬称を抜き出すことを考える。敬称は多岐に渡るようであるから、いくつかのパターンに分類する。

##名前は敬称だけ抜き取る
```py
import re
def get_title(name):
    title_search = re.search(' ([A-Za-z]+)\.', name)
    if title_search:
        return title_search.group(1)
    return ""

#"Title"という列を作り、その中に全員分の敬称を収録する
for df in [train,test]:
    df['Title'] = df['Name'].apply(get_title)
```

```py
train.info() #確認
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/7e69aa59-e508-b375-d8fc-07d2384df5b7.png)


```py
train.Title
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/56e8208d-4137-9255-dd3c-ed3d40914c97.png)

```py
#trainに対して
train.Title.value_counts()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/349f8263-10cb-ceff-547d-3d3129d1bec4.png)

```py
#testに対して
test.Title.value_counts()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/4b06a2b9-2732-c040-62ea-2c5f157b204f.png)

```py
#Mr, Miss, Others(その他)におきかえ
for df in data:
    df['Title'] = df['Title'].replace(['Lady', 'Countess','Capt', 'Col','Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Others')
    df['Title'] = df['Title'].replace(["Mlle","Ms"],"Miss")
    df['Title'] = df['Title'].replace("Mme","Mrs")
```


```py
train.Title.value_counts()
#改めて、どの敬称がいくつずつ含まれているのかを調べる
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/09fed877-a880-6740-a922-3933dc4e8a07.png)


##欠損値を埋める

```py
train.isnull().sum()
#改めてtrain、testそれぞれについて各列における欠損値の個数を確認
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/ebe08557-ba70-222d-0c80-561016b7ad37.png)

```py
test.isnull().sum()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/b6aa8496-a114-93a2-5f3e-872bbd5e4841.png)


欠損しているのは
●trainについて・・・Embarked、Age、Cabinの３種類
●testについて・・・Fare、Age、Cabinの３種類

###まずはtrainのembarkedから埋めていく。
欠けているのは高々二人分なので、一番乗ってきた人が多い港で補完する。

```py
train.Embarked.value_counts()
#train.Embarkedの値とその個数を一覧にして表示する
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/bebf7699-b045-6640-56ea-0ce55b0d9687.png)


```py
train.Embarked=train.Embarked.fillna("S")
#"S"が最も多かったので、欠損値に"S"を代入
```

```py
train.Embarked.value_counts() #確認する
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/a3660b9a-3323-59a0-7c7a-6843db38e8b2.png)


###Ageを補完する。（train,test両方) 
一番荒っぽいが手っ取り早い方法は、 全ての年齢の欠損値を「年齢のデータが残っている乗客の平均年齢」で埋めてしまうことである。 しかし、これでは若干雑すぎるので「敬称ごとの平均で埋める」という方法を採用する。 敬称の区別は年齢の情報を含んだものであると考えられるため、有効だと思われる。

敬称別での年齢の平均をとって、欠損値を埋めよう。 for文を用いることで、train、test両方に対して一気に処理をしてしまおう。

```py
for df in data:
    for title in train.Title.unique():
        df.loc[(df.Age.isnull())&(df.Title==title),"Age"] = df.loc[df.Title==title,'Age'].mean()
#unique()は重複しない値をリストにして返すpandasの関数
```

```py
#参考
df.loc[df.Title=='Mr','Age'].mean()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/08713d10-b3cb-02fa-6c99-3bc6eb4db37e.png)

```py
#参考unique関数
train.Title.unique()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/8425dfe5-a211-d630-d926-b30bcbb37885.png)

###testのFareが１箇所欠けているので、これを埋める
高々１箇所なので、testの他のFare列の中央値を入れてしまえば十分。

```py
#testのFareを埋める
test.Fare=test.Fare.fillna(test.Fare.median())
```

###残るはCabinの情報だが・・・
Cabinは欠損値の方が多いため、今回はCabinの情報は使えないものと判断し、あとで丸ごと削除することとする。

これでCabin以外の欠損値はすべて補完できたはずである。
念のため確認を行う。

```py
train.isnull().sum()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/edc941fc-e30d-0885-d574-f2446d02f8ed.png)


```py
test.isnull().sum()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/d66d86c8-3c8e-23b8-7fa9-4f85542462c8.png)

##データのカテゴリ化

バラバラの値をとるデータをカテゴリ化する。
運賃の額(Fare)と、年齢(Age)のデータはそれぞれバラバラの値をとっているが、分類器にデータを投入することを考えると、これらのデータをカテゴリ化しておく必要がある。
分類器は数字のみ受け付けるため、カテゴリ名には数字を割り当てることとするが、分類器は<b>float型(浮動小数)には対応しないため、必ずint型</b>に整形しておく必要があることに注意。

```py
for df in data:#train,testともに適用

    # "Age"を５クラスにわけ、"Age_band"列を新たに作り、クラスの値を代入
    df.loc[ df['Age'] <= 22, 'Age_band']     = 0
    df.loc[(df['Age'] > 22) & (df['Age'] <= 30), 'Age_band'] = 1
    df.loc[(df['Age'] > 30) & (df['Age'] <= 37), 'Age_band'] = 2
    df.loc[(df['Age'] > 37) & (df['Age'] <= 59), 'Age_band'] = 3
    df.loc[ df['Age'] > 59, 'Age_band'] = 4
    df.Age_band = df.Age_band.astype(int)
```

```py
'''
for df in data:
    
    # "Fare"を４クラスにわけ、"Fare_band"列を新たに作り、クラスの値を代入
    df.loc[ df['Fare'] <= 8, 'Fare_band']  = 0.
    df.loc[(df['Fare'] > 8) & (df['Fare'] <= 15), 'Fare_band'] = 1
    df.loc[(df['Fare'] > 15) & (df['Fare'] <= 31), 'Fare_band']   = 2
    df.loc[ df['Fare'] > 31, 'Fare_band']     = 3
    df.Fare_band = df.Fare_band.astype(int)
'''
#上の方法でもよいがここではqcutを用いた手法を用いる
for df in data:
    df['Fare_band']=pd.qcut(df.Fare,4,labels=range(4))
```

###少し寄り道
新しい列Title,Age_band,Fare_bandを作ったことで
新たな切り口でデータを視覚化することが可能になった。
ここで、Title、Age_band、Fare_bandを切り口としていくつかグラフを作成する。

敬称(Title)と支払った運賃(Fare)の関係を調べて視覚化してみよう。

```py
sns.swarmplot("Title","Fare",data=train)
#敬称(Title)と支払った運賃(Fare)の関係
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/f520bc7d-75ff-5c8c-9886-d5701e23eb72.png)

運賃のクラス(Fare_band)と性別(Sex)で生存率がどう変わるかを調べよう。

```py
sns.factorplot("Fare_band","Survived",data=train,hue="Sex")
#運賃のクラス(Fare_band)・性別(Sex)と生存率の関係
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/b35c9021-f425-3eac-85b6-872a804dc7a0.png)

##不要と思しきデータの削除

・PassengerId・・・形式が扱いにくい（クラス分類しにくい）上に、あまり生死と関係がなさそう
・Name・・・敬称を集めたTitle列を作ったので用済み
・Ticket・・・規則性が見出せず扱いに困る
・Cabin・・・欠損値が多すぎるので不採用
・Age・・・Age_bandを作ったので用済み
・Fare・・・Fare_bandを作ったので用済み

```py
drop_columns = ['PassengerId', 'Name', 'Ticket', 'Cabin','Age','Fare']
# 上の6つのcolumn名をリストに。
```

```py
train.info()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/c7b824ab-48cd-e8a6-ac11-c527751a4cc9.png)


```py
train = train.drop(drop_columns, axis = 1)
test  = test.drop(drop_columns, axis = 1)
'''
for df in data:
    df=df.drop(drop_columns,axis=1)
'''
```
##データをすべて数字になおす
いま文字列の形で記述されているのは
・Sex列
・Title列
・Embarked列
である。いずれもカテゴリであるので、ラベルとして数字を割り当ててあげればよい。
（例)　Mr:0, Miss:1, Mrs:2　・・・など

```py
data=[train,test]#for文でdropした場合には不要

for df in data:
    # 性別を数字でおきかえ
    df.loc[df['Sex']=="female", "Sex"]=0
    df.loc[df['Sex']=='male','Sex']=1
    
    # 敬称を数字で置き換え
    df.loc[df['Title']=='Mr', 'Title']=0
    df.loc[df['Title']=='Miss', 'Title']=1
    df.loc[df['Title']=='Mrs', 'Title']=2
    df.loc[df['Title']=='Master', 'Title']=3
    df.loc[df['Title']=='Others', 'Title']=4
    
    # 乗船した港３種類を数字でおきかえ
    df.loc[df['Embarked']=='S', 'Embarked']=0
    df.loc[df['Embarked']=='C', 'Embarked']=1
    df.loc[df['Embarked']=='Q', 'Embarked']=2
```

```py
train #データの確認
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/47b492a1-8387-f080-c825-5a1b1cb9d31e.png)


#データを分類器につっこむ

```py
from sklearn.model_selection import train_test_split
```

```py
train.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/5e8623ba-cb30-f0fd-32cc-592f8fa77998.png)

```py
test.head()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/bdeb79a1-8a3e-f249-fb21-fd1e8fcb03cd.png)

testのデータには答え（"Survived"）がついておらず、これを用いて学習器の良し悪しを確かめる術がないので、
trainの一部のデータを分類器の検証用に用いることとする。


train→tr_train、tr_test　　に分割
（tr_trainデータ数）：（tr_testのデータ数）＝7:3
となるように指定する。

```py
tr_train,tr_test=train_test_split(train, test_size=0.3)
#tr_train,tr_testに振り分けられるデータはランダムに決まる
```

```py
print(tr_train.info())
print(tr_test.info())
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/cdda1a17-f10b-1ecd-c0b0-0be004521b95.png)


tr_train_Xにはtr_trainの"Survived"列【以外】のデータを、
tr_train_Yにはtr_trainの"Survived"列のみを、
tr_test_Xにはtr_testの"Survived"列【以外】のデータを、
tr_test_Yにはtr_testの"Survived"列のみを納める。

```py
tr_train_X = tr_train[train.columns[1:]]
tr_train_Y = tr_train[train.columns[0]]
tr_test_X = tr_test[train.columns[1:]]
tr_test_Y = tr_test[train.columns[0]]
```

決定木(DecisionTreeClassifier)
決定木のモデルを設定。

```py
from sklearn.tree import DecisionTreeClassifier
model=DecisionTreeClassifier()
```

このモデルにtr_train_Xのデータを学習させ、tr_train_Yのデータを分類し判別率を算出

```py
model.fit(tr_train_X,tr_train_Y)
predict= model.predict(tr_test_X)
```

判別率をsklearn.metricsを用いて見てみる

```py
from sklearn import metrics
print('判別率：',metrics.accuracy_score(predict, tr_test_Y))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/7042eb82-653e-3373-30ae-92d6292adc02.png)

ランダムフォレスト(RandomForestClassifier)
ランダムフォレストのモデルを設定。とりあえず、n_estimatorは100としておく。

```py
from sklearn.ensemble import RandomForestClassifier
model=RandomForestClassifier(n_estimators=100)
```

このモデルにtr_train_Xのデータを学習させ、tr_train_Yのデータを分類し判別率を算出


```py
model.fit(tr_train_X,tr_train_Y)
predict= model.predict(tr_test_X)
```

##判別率をsklearn.metricsで確認

```py
from sklearn import metrics
print('判別率：',metrics.accuracy_score(predict, tr_test_Y))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/6d3a37ab-0610-386b-d3c1-a09f3c634793.png)

##K-分割交差検証

```py
from sklearn.model_selection import KFold, cross_val_score, cross_val_predict
kf=KFold(n_splits=5, random_state=30, shuffle=True)

x=train[train.columns[1:]]
y=train["Survived"]
cv_result = cross_val_score(model, x, y, cv = kf)
print(cv_result)
print("平均精度：{}".format(cv_result.mean()))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/1b5cf34e-ceb5-d393-745e-477b87cec6fe.png)

##グリッドサーチ
ハイパーパラメータチューニングの最も基本的な手法

```py
from sklearn.model_selection import GridSearchCV
param={'n_estimators':range(100,1000,100),"max_depth":range(1, 10, 1)}#100から1000の100区切り
GS_rf=GridSearchCV(estimator=RandomForestClassifier(random_state=0),param_grid=param,verbose=True,cv=5)
GS_rf.fit(x,y)
print(GS_rf.best_score_)
print(GS_rf.best_estimator_)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/51ef98c4-de6b-574c-d1b7-86cf33ccf7f0.png)

ランダムフォレストのパラメタチューニングの結果、試した条件の中では、
決定木の数：200
が最適で、このとき
精度：約81%
の分類器が得られることがわかった。

補足：GridSearchCVはパラメタチューニングを行う際、引数cvをn(整数)と設定すれば
どのパラメタの組み合わせが「最適」であるかを求めるために、裏でデータをn分割して交差検証を行ってくれる。

```py
test
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/6d1fd2e4-77ec-26aa-ecdc-7539e5082cad.png)

これまではtrainデータセットの一部を精度予測のテスト用に用いていたが、最後はtrainデータセット全てで学習を行う。
前準備として、
train_Xにはtrainの"Survived"列【以外】のデータを、
train_Yにはtrainの"Survived"列のみを、格納する。


```py
train_X=train[train.columns[1:]]
train_Y=train[train.columns[0]]
```

test用のデータセット(test.csv)をこのモデルに従って分類。
また、分類結果を提出する形式にまとめる(csv形式)

```py
model=RandomForestClassifier(max_depth=4, n_estimators=200)
model.fit(train_X,train_Y)
```

```py
test_prediction = model.predict(test)

passenger_id = np.arange(892,1310)
test = pd.DataFrame( { 'PassengerId': passenger_id , 'Survived': test_prediction } )
test.shape
test.head()
test.to_csv( 'titanic_forsubmisson.csv' , index = False )
```

```py
pd.Series(model.feature_importances_, index=tr_train_X.columns).plot.bar()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/15ddc3d3-bc4b-f088-87f2-3cf87e30ae21.png)

```py
test_prediction
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/7a838ea2-1802-8c52-8333-27b4f2bd890a.png)

```py
# PassengerIdを取得
PassengerId = np.array(test["PassengerId"]).astype(int)

# my_prediction(予測データ）とPassengerIdをデータフレームへ落とし込む
my_solution = pd.DataFrame(test_prediction, PassengerId, columns = ["Survived"])

# my_tree_one.csvとして書き出し
my_solution.to_csv("my_tree_one.csv", index_label = ["PassengerId"])
```


#最終結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374781/0ce8ed2e-9309-c868-ba8c-a33f0590fc93.png)
2019/6/27時点