LoginSignup
7
3

More than 3 years have passed since last update.

【Python】新卒入社して2ヶ月経ったのでKaggleのTitanicにチャレンジする②【決定木】

Last updated at Posted at 2019-06-11

概要

新卒3ヶ月目になりました(自己紹介)。Python歴も2ヶ月目に突入です。

前回の記事→【Python】新卒入社して2ヶ月経ったのでKaggleのTitanicにチャレンジする【KNN】
の続きです。

前回は「新卒2ヶ月目にしてTitanicスコアカンストする自分,最強では…?」という内容の記事になる予定でしたが,別にカンストしたりしなかったので,今回も機械学習とPythonのTitanic生存予測にチャレンジして,研鑽に努めていこうと思います。

それではやっていきます。

今回のやること一覧

1.データ概観,前処理,EDA
2.決定木による予測モデル構築
3.テストセットの生存予測,結果

環境

  • Windows10
  • Anaconda
  • Python 3.6.5
  • JupyterNotebook

1.データ概観,前処理,EDA

まずは使うデータの内容を確認していきます。前回に引き続き,Kaggleが提供してくれる以下12列,891行のデータを使います。

使用データ:train.csv(全12列,891行)

カラム 内容 説明
PassengerId 搭乗者番号
Survival 生死 0なら死亡,1なら生存
Pclass チケットの等級 1 = 1st,2 = 2nd,3 = 3rd
Name 名前 「First name , 敬称 . Last name(旧姓)」のフォーマット
Sex 性別 maleかfemale
Age 年齢 1歳未満の場合は少数表記,推定した年齢なら「~.5」の表記
SibSp 同乗してた兄弟姉妹,配偶者の人数
Parch 同乗してた両親,子供の人数
Ticket チケット番号
Fare 運賃
Cabin 客室番号
Embarked 搭乗した港 C = Cherbourg,Q = Queenstown,S = Southampton

train.csvを「df_train_raw」でDataFrame化して,行数や欠損数と,データの先頭5行を確認します。

データ概観
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

df_train_raw = pd.read_csv('C:/~hoge~/train.csv')
display(df_train_raw.info(), df_train_raw.head())

キャプチャ1.PNG
全体は891行,欠損値が含まれているのはAge,Cabin,Embarkedです。この辺りも前回と同じです。
欠損値の多いCabinを「あったかなかったか」の0,1にしておきましょう。

CabinのNullを補完→Cabin有:1,Cabin無:0にする
df_train_raw['Cabin'].fillna(0, inplace=True)
df_train_raw['Cabin'] = df_train_raw['Cabin'].where(df_train_raw['Cabin'] == 0, 1)

さて,次は2番目に欠損値の多いAgeです。
前回は欠損していた場合には搭乗者全体の平均値(29.67)を入れていましたが,他のカラムを上手く使って,その人の年代をもう少し正確に予測できるといいですよね。

今回は,前回使わなかったNameを使って,Ageの欠損値を補完していきましょう。

Nameは,各行「First name , 敬称 . Last name(旧姓)」という,カンマとピリオドで区切られたフォーマットになっているので,まずは名前を分割して,それぞれのFirst name,敬称,Last nameの入った新しいカラムを追加したData Frameを作りましょう。

各行のNameを分割→3カラムを追加した新しいDataFrameの作成
df_train_name1 = df_train_raw['Name'].str.split(',', expand=True).rename(columns={0:'First', 1:'Other'})
df_train_name2 = df_train_name1['Other'].str.split('.', expand=True).rename(columns=({0:'Title', 1:'Last', 2:'Other'}))
df_train_names = pd.concat([df_train_name1, df_train_name2], axis=1)[['First', 'Title', 'Last']]

df_train_name = pd.concat([df_train_raw, df_train_names], axis=1)
display(df_train_name.head())

新しいData Frame「df_train_name」はこんな感じです。
キャプチャ2.PNG

今回注目するのは名前の中央にあったTitleです。
Titleが,各搭乗者の敬称です。日本人にはあまり馴染みのないものかもしれません。
今回train.csvに入っていた搭乗者の敬称全17種類はこんな感じになっていました(和訳,説明はGoogle調べ,間違ってたらすいません)

搭乗者の敬称一覧
敬称 和訳 正式名,説明
Mr 男性
Mrs 既婚の女性
Miss 未婚の女性
Master 少年,青年男性
Don ドン スぺイン貴族
Rev 聖職者 Reverend
Dr 医者 Doctor
Mme 既婚女性 Madame
Ms 女性
Major 少佐
Lady 貴族の夫人
Sir 勲爵士
Mlle 未婚の女性 Mademoiselle
Col 大佐 Colonel
Capt 船長 Captain
the Countess 女伯爵
Jonkheer ヨンクヘール オランダ貴族

新しく作ったデータフレームをもとに,Titleごとの人数,生存率,平均年齢をまとめたData Frameを作成して,どんな感じになってるか見てみましょう。

Titleごとの人数,生存率,平均年齢のDataFrameを作成
## Titleごとの人数,生存率を算出,Data Frame作成
title_list = []
survived_par_list = []
for titles in df_train_name['Title']:
    if titles not in title_list:
        title_list.append(titles)
        survived_par_list.append('{:.3%}'.format(df_train_name.where(df_train_name['Title'] == titles)['Survived'].sum() / df_train_name.where(df_train_name['Title'] == titles)['Survived'].count()))        
    else:
        pass
df_title_survived_count = pd.merge(pd.DataFrame(df_train_name['Title'].value_counts()).reset_index(),
                                   pd.DataFrame(survived_par_list, title_list).reset_index(), on='index')

## Titleごとの平均年齢を算出,Data Frame作成
title_list = []
age_mean_list = []
for titles in df_train_name['Title']:
    if titles not in title_list:
        title_list.append(titles)
        age_mean_list.append('{:.3f}'.format(df_train_name.where(df_train_name['Title'] == titles)['Age'].mean()))        
    else:
        pass

## 作成した2つのData Frameを結合
df_title = pd.merge(df_title_survived_count, pd.DataFrame(age_mean_list, title_list).reset_index(), on='index').rename(columns=({'index':'Title', 'Title':'count', '0_x':'survive', '0_y':'age_mean'}))

display(df_title)

こんな感じです。
キャプチャ3.PNG
後半のTitleは人数が少ないので,生存率等あまり参考にはなりませんが,上から4種類の平均年齢が分かったのはとてもありがたいですね。

これが分かれば,例えばAgeの欠損した「Master」の人に,全体平均の29歳は入れるべきでなさそうだ,など,欠損値の補完をよりその人の属性に近づいたものに出来ます。

今回は,Ageの欠損している各行には,それぞれのTitleの平均値を入れていくことにしましょう。

Ageの欠損値を補完したDataFrameの作成
df_null_in = pd.merge(df_train_name, df_title[['Title', 'age_mean']], on='Title')

for indexes, values in enumerate(df_null_in['Age']):
    if values != values:
        df_null_in.iloc[indexes, 5] = df_null_in.iloc[indexes, 15]
    else:
        pass

df_null_in.head()

こうなります。
キャプチャ4.PNG
Ageの欠損がなくなりました。5行だけ出したData Frameの,上から3行目が欠損していた行ですね。ちゃんと「Mr」の平均値が入っているのが分かります。

最後に,Embarkedの欠損は2件だけなのでひとまず除外して,モデル構築に必要なカラムだけのData Frameにしましょう。前回同様,家族人数のカラムも作成していきます。

予測のためのDataFrameの作成
df_train = df_null_in[['PassengerId', 'Survived', 'Pclass', 'First', 'Title', 
 'Last', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Cabin', 'Embarked']].dropna()

## 家族人数の列FamilyはSibSp,Parchに1(自分)を足して算出
df_train['Family'] = df_train['SibSp'] + df_train['Parch'] + 1

df_train.info()

キャプチャ5.PNG
全部で14列,889行です。

それでは,このData Frameを使って,生存予測のためのモデルを作っていきましょう。

2.決定木による予測モデル構築

モデル構築をする前に,前回調べた各変数とSurvivedの関係をおさらいします。

Pclass:Pclassが高いほど生存率が高い
Cabin:Cabinがあるグループはないグループよりも生存率が高い
Sex:女性は,男性よりも生存率が高い
Family:家族(複数人)で搭乗したグループは,一人で搭乗したグループより生存率が高い
Fare:運賃を7.5より少なく払っているグループは死亡者の数が生存者を上回っている
Embarked:EmbarkedがCのグループでは,生存者の数が死亡者を上回っている
Age:0歳代~10歳代のグループでは,生存者の数が死亡者を上回っている

こんな感じでした。
前回同様,モデル構築に使う変数は上記の7つです。

そして今回は,機械学習アルゴリズムの1つである決定木(Decision Tree)を使います。
決定木によるモデル構築に関しては,今回もPythonではじめる機械学習Python機械学習プログラミングを参考にしました。

決定木はその名の通り,いくつもの枝を持つ木のようなモデルを構築することで,データを分類していくモデルです。

それではやっていきましょう。

決定木を用いて予測モデルの構築
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

df_train['Sex'] = df_train['Sex'].where(df_train['Sex'] == 'male', 1)
df_train['Sex'] = df_train['Sex'].where(df_train['Sex'] == 1, 0)

df_train.loc[:, 'Sex'] = df_train.loc[:, 'Sex'].astype(np.int64)
df_train.loc[:, 'Cabin'] = df_train.loc[:, 'Cabin'].astype(np.int64)
df_train.loc[:, 'Age'] = df_train.loc[:, 'Age'].astype(np.float64)

df_train_x = df_train[['Pclass', 'Age', 'Sex', 'Cabin', 'Fare', 'Embarked', 'Family']]
df_train_y = df_train['Survived']
df_train_category_x = pd.get_dummies(df_train_x)

X_train, X_test, y_train, y_test = train_test_split(df_train_category_x, df_train_y, random_state=1)

tree = DecisionTreeClassifier(random_state=1)
tree.fit(X_train, y_train)

tree_trainscore = round(tree.score(X_train, y_train), 5)
tree_testscore = round(tree.score(X_test, y_test), 5)

display(tree_trainscore, tree_testscore)

結果は…
キャプチャ6.PNG

上がtrainデータ,下がtestデータに対してのスコアです。trainに対しての精度は高いですが,testの精度が低くなっています。

実際に,分類の為にどのようなモデルを構築したのかを確認してみましょう。

決定木の可視化
## 必要に応じてpip install
from pydotplus import graph_from_dot_data
from  sklearn.tree import export_graphviz

features = ['Pclass', 'Sex', 'Age', 'Fare', 'Cabin', 'Family', 'Embarked_C', 'Embarked_Q', 'Embarked_S']
dot_data = export_graphviz(tree, filled=True, rounded=True, class_names=['0', '1'], feature_names=features, out_file=None)
graph = graph_from_dot_data(dot_data)
graph.write_png('tree.png')

tree.png

とんでもないことになっていますね。

trainデータを出来るだけ正確に分類できるようにと,多くの枝に別れた,かなり複雑なモデルになっているのが分かります。
しかし,この内の多くは,testデータの分類には必要ない枝かもしれません。
trainデータとtestデータのスコアの乖離は,どうやらこれが原因で生じるようです。

trainデータにだけ過剰に適合するのではなく,それ以外のデータにも上手く適合させられるような,汎化性能の高いモデルを構築しなくては,testデータに対する正確な予測はできないので,このままではいけませんね。

今回は,木の深さを変えてモデルを作っていきましょう。

深さを1から10までの間で試して,trainとtest両データに対するスコアがどう変動するか確認していきます。
ついでに,両スコアがどのくらい離れているのかもわかるようにしましょう。

depthを1から10までで試してモデルの構築,スコアの変動の可視化
X_train, X_test, y_train, y_test = train_test_split(df_train_category_x, df_train_y, random_state=1)

train_score_list = []
test_score_list = []
kairi_list = []
for depth in range(1, 10):
    tree = DecisionTreeClassifier(max_depth=depth, random_state=1)
    tree.fit(X_train, y_train)

    train_score_list.append(round(tree.score(X_train, y_train), 5))
    test_score_list.append(round(tree.score(X_test, y_test), 5))
    kairi_list.append(round(tree.score(X_train, y_train), 5) - round(tree.score(X_test, y_test), 5))

ax = plt.axes(ylabel='スコア', xlabel='木の深さ')
ax.set_title('木の深さごとのtrain,testデータスコア')
plt.plot(range(1, 10), train_score_list, label='train_score')
plt.plot(range(1, 10), test_score_list, label='test_score')
ax.legend()
display(test_score_list.index(max(test_score_list)), max(test_score_list), pd.Series(kairi_list))

結果は…
キャプチャ7.PNG
こんな感じでした。深さが8の時,testスコアが最大で0.80になっています。

しかし,深さ8の時も,先ほどと同様にtrainとtestの乖離が大きく,これではやはり過剰適合に対する不安が残ります。かといって次にスコアが高いのは深さが1の場合,これは,木を見ると分かるのですが,性別だけで生死を分類している2択ですのでちょっと…

というわけで,今回はtestスコアも高く,trainスコアとの乖離も比較的小さい深さ3でやってみることにします。

モデルも決まったので,早速test.csvの予測をしていきましょう。

3.テストセットの生存予測,結果

まずはtrain.csv同様,データの確認から始めます。

データ概観
df_test_raw = pd.read_csv('C:/~hoge~/test.csv')
display(df_test_raw.info(), df_test_raw.head())

全体は418行,欠損値が含まれているのはAge,Cabin,Fareでした。
Ageの欠損はtrainデータ同様にTitleごとの平均値を,Fareの欠損には全体の平均値を入れるようにしましょう。

まずはtrain同様,Cabinの変換とTitleの確認を行います。

データ前処理:Cabin変換,Title確認
df_test_raw['Cabin'].fillna(0, inplace=True)
df_test_raw['Cabin'] = df_test_raw['Cabin'].where(df_test_raw['Cabin'] == 0, 1)

df_test_name1 = df_test_raw['Name'].str.split(',', expand=True).rename(columns={0:'First', 1:'Other'})
df_test_name2 = df_test_name1['Other'].str.split('.', expand=True).rename(columns=({0:'Title', 1:'Last', 2:'Other'}))
df_test_names = pd.concat([df_test_name1, df_test_name2], axis=1)[['First', 'Title', 'Last']]

display(df_test_names.head(), df_test_names['Title'].unique())

キャプチャ9.PNG
うまく分割できました。testの方は,敬称の種類が少ないですね。
ちなみに新しい敬称「Dona」は,Donの女性形だそうです。

Titleごとの人数と平均年齢もみてみましょう。

Titleごとの人数,平均年齢の算出
df_test_name = pd.concat([df_test_raw, df_test_names], axis=1)

title_list = []
age_mean_list = []
for titles in df_test_name['Title']:
    if titles not in title_list:
        title_list.append(titles)
        age_mean_list.append('{:.3f}'.format(df_test_name.where(df_test_name['Title'] == titles)['Age'].mean()))        
    else:
        pass

df_title = pd.merge(pd.DataFrame(df_test_name['Title'].value_counts()).reset_index(),
                                   pd.DataFrame(age_mean_list, title_list).reset_index(), on='index').rename(columns=({'index':'Title', 'Title':'count', 0:'age_mean'}))
display(df_title)

キャプチャ10.PNG
TitleがMsの人は,1件だけで欠損してますね。仕方ないのでここにはtrainの方の平均年齢を入れることにしましょう。
AgeとFareの欠損値を処理して,Familyも追加したData Frame「df_test」を作成します。

予測のためのDataFrameの作成
dftest_null_in = pd.merge(df_test_name, df_title[['Title', 'age_mean']], on='Title')
for indexes, values in enumerate(dftest_null_in['Age']):
    if values != values:
        dftest_null_in.iloc[indexes, 4] = dftest_null_in.iloc[indexes, 14]
    else:
        pass
dftest_null_in['Age'].replace('nan', df_null_in.query('Title == " Ms"').loc[:, 'age_mean'].item(), inplace=True)

df_test = dftest_null_in[['PassengerId', 'Pclass', 'First', 'Title', 'Last', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Cabin', 'Embarked']]
df_test['Family'] = df_test['SibSp'] + df_test['Parch'] + 1
df_test['Fare'] = df_test['Fare'].fillna(df_test['Fare'].mean())

df_test = df_test.sort_values('PassengerId')
df_test.info()

キャプチャ11.PNG

これで準備完了です。

それでは,Survivedの予測をして,Kaggleに提出するCSVファイルを作成していきます。

決定木を用いたテストデータの生存予測,submitファイル作成
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

df_test['Sex'] = df_test['Sex'].where(df_test['Sex'] == 'male', 1)
df_test['Sex'] = df_test['Sex'].where(df_test['Sex'] == 1, 0)

df_test.loc[:, 'Sex'] = df_test.loc[:, 'Sex'].astype(np.int64)
df_test.loc[:, 'Cabin'] = df_test.loc[:, 'Cabin'].astype(np.int64)
df_test.loc[:, 'Age'] = df_test.loc[:, 'Age'].astype(np.float64)

df_test_x = df_test[['Pclass', 'Age', 'Sex', 'Cabin', 'Family', 'Fare', 'Embarked']]
df_test_category_x = pd.get_dummies(df_test_x)

tree = DecisionTreeClassifier(max_depth=3, random_state=1)
tree.fit(X_train, y_train)
decision_tree_predict = tree.predict(df_test_category_x)

decision_tree_predict = pd.Series(decision_tree_predict)
submit = pd.concat([df_test_raw['PassengerId'], decision_tree_predict], axis=1).rename(columns={0:'Survived'})

submit.to_csv('gender_submission.csv')

出来ました。

気になるスコアは...
qiita_score.PNG

0.779でした。ちょっと上がってます(前回のKNNでは0.76でした)。
小数点以下の変動に一喜一憂するのは少し悔しいですが,やっぱりスコアが上がってると嬉しいですね。

しかし,そこまでガッツリ上がらなかったのは,やはり過剰適合と適合不足のバランスがうまく取れなかったせいでしょうか。
決定木ではこれが限界のようです…

まとめ

今回は,前回に引き続き機械学習,Pythonの練習として,決定木を用いてTitanic生存予測にチャレンジしました。
決定木はデータに対して構築したモデルを可視化しやすい一方で,学習したデータに過剰に適合しやすく,汎化性能が低くなりがちなので,そのバランスを取って上手くテストデータにも使えるようなモデルを構築するのが難しいです。

次回は,今回用いた決定木のデメリットを補う,ランダムフォレストを使ってやっていこうと思います。

参考サイト,文献

https://www.kaggle.com/c/titanic
Pythonではじめる機械学習
Python機械学習プログラミング

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3