😎

【教師あり学習・分類】タイタニックデータで生存者分析をやってみた

2022/11/10に公開

はじめに

はい、今回は有名なタイタニックデータで教師あり学習の分類を行っていきます!
タイタニックの乗客が生存したかどうか判定してもらいましょう!

初心者でも実装できて、評価指標などを考察してみるのを目標にします!
※僕自身も初心者のため、間違っている部分があればコメントにてご指摘いただければ、幸いです。

今回の流れは
データ読み込み => 加工 => モデル作成 => 推論
で行っていきます!

環境

  • colab

準備

今回使うのは有名なデータセットでタイタニックデータです。
まずはデータを読み込みましょう!ってことで、あらかじめgoogle driveに用意していたcsvを読み込みます。

# google driveのマウント
from google.colab import drive
drive.mount('/content/drive')

#!/usr/bin/env python

# 必要なモジュールをインポート
import csv
import pandas as pd

CSV_PATH = '/content/drive/MyDrive/titanic.csv'
# csvを読み込んでデータフレームにする。
titanic_dataframe = pd.read_csv(CSV_PATH, sep=",")

データがない方はkaggleやsignateのコンペサイトからダウンロードできるみたいです!
それでは、データフレームの中身を見てみましょう。

titanic_dataframe.info()
# ---------------------------------------
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 1309 entries, 0 to 1308
# Data columns (total 14 columns):
#  #   Column     Non-Null Count  Dtype  
# ---  ------     --------------  -----  
#  0   pclass     1309 non-null   int64  
#  1   survived   1309 non-null   int64  
#  2   name       1309 non-null   object 
#  3   sex        1309 non-null   object 
#  4   age        1046 non-null   float64
#  5   sibsp      1309 non-null   int64  
#  6   parch      1309 non-null   int64  
#  7   ticket     1309 non-null   object 
#  8   fare       1308 non-null   float64
#  9   cabin      295 non-null    object 
#  10  embarked   1307 non-null   object 
#  11  boat       486 non-null    object 
#  12  body       121 non-null    float64
#  13  home.dest  745 non-null    object 
# dtypes: float64(3), int64(4), object(7)
# memory usage: 143.3+ KB
# ---------------------------------------

pclass:客室のクラス(1,2,3の順に高級クラス)
survived:生存の有無(生存:1、死亡:0)
name:氏名
sex:性別
age:年齢
sibsp:乗船していた兄弟、配偶者の数
parch:乗船していた両親、子供の数
ticket:チケットナンバー
fare:運賃
cabin:船室
embarked:乗船した港(S=Southampton, C=Cherbourg, Q=Queenstown)
boat:生存した際の救命ボート番号
body:死亡した人のナンバー
home.dest:出港地&到着地

データ加工

全てのデータを使えるわけではないので、データを精査していきます!
nameやticketは生存したかどうかに関係なさそうな列(カラム)の為、削除します。
cabinは客室のmapなどと照らし合わせると使えそうなデータですが、今回は面倒なので削除します。
boatやbodyは生存した人や死亡した人などのみにあるデータなので、不要と判断し削除します。
home.destは生存率にあまり関係がないと思い、削除します。
また、性別は文字列なので0,1に置き換え、embarkedはone-hotエンコーディングで処理します。

  • one-hotエンコーディングとは
    まずデータにはカテゴリカルデータと呼ばれる分類や区別を行うためのデータがあります。
    血液型や性別などですね。これは基本的に数値ではないので、0、1で表現するダミー変数にしないと、機械学習では扱いづらいデータとなります。
    それを上手く行ってくれるのがone-hotエンコーディングです。

今回の性別もone-hotエンコーディングで行けましたね、、、

# データ加工
# 使わないカラム
NO_USE_COLUMN = ['name', 'ticket', 'cabin', 'boat', 'body', 'home.dest']
replaced_dataframe = titanic_dataframe.drop(columns=NO_USE_COLUMN)
# 男性を0、女性を1に変換
replaced_dataframe = replaced_dataframe.replace({'male': 0, 'female': 1})
# one-hotエンコーディング(文字列を数値に変換)
replaced_dataframe = pd.get_dummies(replaced_dataframe, dummy_na=False)

更にレコードに欠損値があるとモデル作成で躓くので欠損値の有無を確認します。

# 欠損値の確認
titanic_dataframe.isnull().sum()
# -------------------------------
# pclass          0
# survived        0
# sex             0
# age           263
# sibsp           0
# parch           0
# fare            1
# embarked        2
# dtype: int64
# -------------------------------

年齢が結構欠損していますね。今回は1309レコードあるので263レコード無くなってもあまり支障は無いと判断して欠損値を削除します。

replaced_dataframe = replaced_dataframe.dropna(axis=0).reset_index(drop=True)

他にも中央値や平均で埋める方法もあるようなのですが、よくわかってないので、次回挑戦します!
他の削除理由としては、年齢を中央値で埋めると263人が、中央値での年齢になって変かなって思った次第です。

モデル作成

さて、地味な作業は終わり。これからやっと機械学習っぽいステップに入ります!楽しみですね!

モデル作成に最低限必要な要素として
「目的変数」「説明変数」「トレーニングデータとテストデータ」の3つがあります。

目的変数

目的変数とは教師あり学習をさせる上で機械に与える答えとなります。つまりゴールですね。

# 目的変数は生存したかどうかを学習したいので、survivedになります。
TARGET = 'survived'
Y = replaced_dataframe[TARGET]

説明変数

説明変数とは目的変数を求めるために使う要素であって、何かの原因となっている変数のことです。

# 説明変数は、その他のカラムですね。
X = replaced_dataframe.drop(columns=TARGET)

トレーニングデータとテストデータ

機械学習で学習に使うデータをトレーニングデータです。これで学習させた結果がモデルと呼ばれます。テストデータはモデルの性能を判定するために使用するデータです。基本的には一つのデータを切り分けてこれら二つのデータにします。kaggleやsignateなどのデータサイエンスコンペのサイトでは既に別々に用意されていたりします。
今回はデータをシャッフルしてトレーニングデータ : テストデータ = 7 : 3で切り分けてみます。

from sklearn.model_selection import train_test_split

# トレーニングデータおよびテストデータ分割
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.3, shuffle=True, random_state=3)

さて3つの要素の説明が終わったので、ついに機械学習です!!

モデル学習

今回はsurvivedが0か1かを予想したいので、
説明変数から二値の目的変数が起こる確率を予想できる「ロジスティック回帰」のアルゴリズムを使ってみます。
アルゴリズムという言葉を使えるようになる日が来るなんて、、、(感動)

ロジスティック回帰

from sklearn.linear_model import LogisticRegression

# ロジスティック回帰のインスタンス作成
model_logi = LogisticRegression()

# モデル学習
model_logi.fit(X_train, Y_train)

・・・ん??
終わり?
え?
はい。どうやらこれで学習が終わったようです。あっけなかったですね(笑)

推論

気を取り直して推論を行っていきます。
先ほど切り分けたテストデータを使って生存したかどうかを推論させます。

# 生存確率の推論
proba_logi = model_logi.predict_proba(X_test)[:, 1] 
prediction_logi = model_logi.predict(X_test)
# 結果データ作成
result_logi = pd.DataFrame()
result_logi["実データ"] = Y_test
result_logi["生存推論"] = prediction_logi
result_logi["生存推論確率"] = proba_logi
# 小数点設定
pd.options.display.precision=2
# 昇順に並び替え
result_logi = result_logi.sort_index()
# とりあえず、10件のみ出力
result_logi.head(10)
index 実データ 生存推論 生存推論確率
3 0 0 0.43
4 0 1 0.91
5 1 0 0.26
7 0 0 0.31
10 0 0 0.48
16 1 1 0.92
27 1 1 0.88
28 1 0 0.41
30 1 0 0.49
31 1 1 0.90

ほほう。。。
index4は高確率で生きていると機械は判定が、実際は亡くなっているようですね。
逆にindex28や30は亡くなっていると判定しているが、実際は生きていたようです。

仮にこれが実際の事故現場の救助する判断材料として使われていたら、28や30の人は亡くなっていると判定されて救助打ち切り、、、なんて恐ろしいことになってしまいますね。

ちなみにindexが飛び飛びなのはデータを切り分ける際にシャッフルしたからです!

次に今回の推論結果の混同行列を出してみましょう!
混同行列とは予想とその予想の答えが正解かどうかの組み分けを行ったものです。

from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
# 混合行列
cm_logi = confusion_matrix(y_true=Y_test, y_pred=prediction_logi)

tn_logi, fp_logi, fn_logi, tp_logi = cm_logi.flatten()


なんのこっちゃ?って感じですよね。これらの要素を言葉で表すと

  • True Positive
    生存していると予想して、実際は生存していた数: 94
  • False Positive
    生存していると予想して、実際は死亡していた数: 28
  • False Negative
    死亡していると予想して、実際は生存していた数: 40
  • True Negative
    死亡していると予想して、実際は死亡していた数: 152

こんな感じです。
予想側の要素(Positiveか、Negativeか)が実際はどうだったのか(Trueだったのか、Falseだったのか)と読むと予想の値と実際の値の区別がつきそうですね。

ここから更に「評価指標」というものを算出していきます。

  • 正答率
    予想に対してどのぐらい合っていたか
  • 適合率
    生存していると予想して、実際に生存していた割合
  • 再現率
    実際に生存していて、生存していると予想した割合
  • f1スコア
    適合率と再現率のトレードオフに対してそのバランスを見る値

求め方はそれぞれ次の通りになります。

正答率 = \frac{TP + TN}{TP + FP + FN + TN}

適合率 = \frac{TP}{TP + FP}

再現率 = \frac{TP}{TP + FN}

f1スコア = \frac{2 × 適合率 × 再現率}{適合率 + 再現率}

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

print('正答率: ',  round(accuracy_score(y_true=Y_test, y_pred=prediction_logi),2))
print('適合率: ', round(precision_score(y_true=Y_test, y_pred=prediction_logi),2))
print('再現率: ',    round(recall_score(y_true=Y_test, y_pred=prediction_logi),2))
print('f1スコア: ',  round(f1_score(y_true=Y_test, y_pred=prediction_logi),2))

正答率: 0.78
適合率: 0.77
再現率: 0.7
f1スコア: 0.73

なるほどって感じですね。
先ほど話した「実際の事故現場の救助する判断材料」として使われている場合は、「死亡していると予想して、実際は生きていた数」つまり、FNが多いのはマズイですね。
よって評価指標で言うと、再現率を上げることが重要なのでは無いでしょうか?

意外とモデル学習は簡単なので、違うアルゴリズムも使ってみましょう!

モデル作成&推論ver2

次は「ランダムフォレスト」というアルゴリズムを使ってみます。
ランダムフォレストは決定木をたくさん集めて多数決を取り、全体として一つの分類器にすることです。
決定木とは、分類(や回帰)のルールをツリーで表現するものです。
流れは先ほどと同じなのでコードをずらずらっと書いていきます!

from sklearn.ensemble import RandomForestClassifier

# ランダムフォレストの学習モデルを作成
model_rf = RandomForestClassifier()

# 学習モデルにテストデータを与えて学習させる
model_rf.fit(X_train, Y_train)

# 生存確率の推論
proba_rf = model_rf.predict_proba(X_test)[:, 1] 
prediction_rf = model_rf.predict(X_test)
# 結果データ作成
result_rf = pd.DataFrame()
result_rf["実データ"] = Y_test
result_rf["生存推論"] = prediction_rf
result_rf["生存推論確率"] = proba_rf
# 昇順に並び替え
result_rf = result_rf.sort_index()
# 出力
result_rf.head(10)
index 実データ 生存推論 生存推論確率
3 0 1 0.68
4 0 1 0.86
5 1 0 0.16
7 0 0 0.11
10 0 1 0.59
16 1 1 0.9
27 1 1 1.0
28 1 0 0.43
30 1 1 0.63
31 1 1 0.99
# 混合行列
cm_rf = confusion_matrix(y_true=Y_test, y_pred=prediction_rf)
tn_rf, fp_rf, fn_rf, tp_rf = cm_rf.flatten()

# 正解率
print('正解率: ',  round(accuracy_score(y_true=Y_test, y_pred=prediction_rf),2))
# 適合率
print('適合率: ', round(precision_score(y_true=Y_test, y_pred=prediction_rf),2))
# 再現率
print('recall: ',    round(recall_score(y_true=Y_test, y_pred=prediction_rf),2))
# f1スコア
print('f1 score: ',  round(f1_score(y_true=Y_test, y_pred=prediction_rf),2))

正解率: 0.75
適合率: 0.73
再現率: 0.66
f1スコア: 0.69

ロジスティック回帰と比較すると
正答率: -0.03
適合率: -0.04
再現率: -0.04
f1スコア:-0.04
と精度は落ちています。再現率も下がっているので、今回はロジスティック回帰の方が合っているようです。
アルゴリズムによって変わってくるということは色々なアルゴリズムを知り、使ってみることが大事なのですね。

まとめ

今回はロジスティック回帰とランダムフォレストを使ってタイタニックデータの分析を行ってみました。
個人的には混合行列の見方やそれらの数値が何を表しているのかが分かりづらかったです。きちんと覚えて、復習もしないとすぐにわからなくなりそうです、、、

実装観点

モデル作成は実装するだけならかなり簡単で、前処理のデータ加工の方が時間がかかり、大変だと感じました。
また今回はモデル作成model_logi = LogisticRegression()の時に()内でパラメータの設定を行わなかったのですが、そこを深掘りして、色々設定すると結果も変わってくるのかもしれません。

ビジネス観点

欠損しているデータをどのように加工するのかを考えたり、推論後の評価指標をどのように扱うのかが大事なのだと感じました。

Discussion