Kaggleのタイタニックデータの解析
Kaggleの定番データセットといえば「タイタニックの生存者予測」です。今回は生存者の予測を目指して解析を行っていきたいと思います。データの可視化について詳しい説明は前回記事で書いているのでそちらを参照してください。
Titanic: Machine Learning from Disaster | Kaggle
解析環境
- Kaggleのカーネル
データの全体像の把握
データ解析をする際に最も重要なことはもちろんデータを理解することです。まずは大雑把にデータの全体像を見てみましょう。
import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import os import warnings warnings.filterwarnings('ignore') print(os.listdir("../input")) # -> ['train.csv', 'gender_submission.csv', 'test.csv']
train = pd.read_csv('../input/train.csv') test = pd.read_csv('../input/test.csv') train.head()
いくつかの変数があることがわかります。
- PassengerId : 乗客ID
- Survived : 生存ラベル(1: 生存 0:死亡)
- Pclass : 身分(1が最高)
- Name : 乗客氏名
- Sex : 性別
- Age : 年齢
- SibSp : 乗客中の親戚の人数
- Parch : 乗客中の親もしくは子供の人数
- Ticket : チケット番号
- Fare : 乗船料
- Cabin : 客室番号
- Embarked : どこの港で乗船したか
変数の吟味
これらの変数の中で、中心となるのはもちろん'Survived'です。これにほかの変数が影響を与えうるか考えてみます。
'PassengerId', 'Ticket'
'PassengerId'はただの通し番号なので、生存には関係ない気がします。ただ、これが乗客の身分や部屋の位置に関係する可能性は一度調べる必要がありそうです。これは'Ticket'にも言えます。
'Pclass', 'Cabin', 'Fare'
'Pclass'に関しては、漠然と、身分の高い人は安全なところに客室があるのかなと思うので、関係ありそうです。これも後で解析しましょう。同様に'Cabin'と'Fare'も調べてみます。
'Sex', 'Age'
'Sex'や'Age'に関しては、欧米ではレディーファーストの精神が強いので女性や子供が優先的に救助されていた可能性があります。
'Name'
'Name'は生存とは全く関係なさそうに見えます。ただ、上の表を見てみると、乗客の名前には最初に'Mr'や'Mrs'などがついていることがわかります。これはその人の社会的な地位や状態を表しているといえるので、ここの情報だけは使える気がします。
'SibSp', 'Parch'
これらは親戚関係のパラメータですが、正直、これらのパラメータが'Survived'に効いてくるストーリーは思いつきませんでした。相関係数だけ調べて、早々に落としてもいいかもしれません。
'Embarked'
乗船地です。これはその土地の発展状況によって乗客層の経済力を示している可能性があります。
カテゴリ変数を数値へ変換していく
'Name'を変換する
'Name'の変換は少し手間です。まずは正規表現を使って敬称を取り出し、'Title'という名前の新しいカラムを作ります。
train['Title'] = train['Name'].str.extract(r'([A-Za-z]+)\.', expand=False) test['Title'] = test['Name'].str.extract(r'([A-Za-z]+)\.', expand=False)
数値に置き換える
では、'Sex', 'Embarked', 'Title'を数値に変換していきます。これにはsklearn.preprocessing.LabelEncoder
を使います。
from sklearn.preprocessing import LabelEncoder for col in ['Sex', 'Title']: le = LabelEncoder() le.fit(train[col]) train[col] = le.transform(train[col]) le.fit(test[col]) test[col] = le.transform(test[col]) train['Embarked'] = train['Embarked'].map({'S' : 1, 'C' : 2, 'Q' : 3}) test['Embarked'] = train['Embarked'].map({'S' : 1, 'C' : 2, 'Q' : 3})
多分'Embarked'はnull valueがあってうまくいかなかったので普通にmap
を使って置き換えました。
'Name'を落とす
もう名前情報はいらなくなったので落としてしまいましょう。
train.drop('Name', axis=1, inplace=True) test.drop('Name', axis=1, inplace=True)
ここまでやってきて、データの状況は下のようになっています。
Ticketを処理する
現時点ではまだ、'Ticket'が数値量になっていません。しかし、どのように数値に変換してよいのかはっきりしないので、もう少し詳しくこの変数について知ることにします。
いくつかTicketを眺めてみると、数字だけからなるチケット番号と、アルファベットが混ざるものの2つがあることがわかります。まずはこれから分離してみます。
number_ticket = train[train['Ticket'].str.match('\d+')] num_alpha_ticket = train[train['Ticket'].str.match('[A-Z]+.+')]
数字だけのチケット
数字だけのチケットnumber_ticket
についてまず見てみましょう。数字がどんなふうに分布しているか見るために、簡単なプロットをしてみます。
number_ticket['Ticket'] = number_ticket['Ticket'].apply(lambda x: int(x)) number_ticket.sort_values('Ticket', inplace=True) plt.figure() plt.plot(number_ticket['Ticket'], '-o') plt.show()
大きく分けて2つのグループに分かれている様子が見て取れます。ylim
をいじって拡大してみると、チケット番号は以下の5通りに分けられることがわかります。
- 10000以下
i*1e+4
より大きく(i+1)*1e+4
以下 (i=1,2,3)- 300000以上
このグループ分けをしたときに、生存率に差が出るか見てみましょう。
x = [1, 2, 3, 4, 5] lowest_num_ticket = number_ticket[number_ticket['Ticket'] <= 100000] sec_lowest_num_ticket = number_ticket[(number_ticket['Ticket'] > 100000) & (number_ticket['Ticket'] < 200000)] thir_lowest_num_ticket = number_ticket[(number_ticket['Ticket'] > 200000) & (number_ticket['Ticket'] < 300000)] four_lowest_num_ticket = number_ticket[(number_ticket['Ticket'] > 300000) & (number_ticket['Ticket'] < 400000)] high_num_ticket = number_ticket[number_ticket['Ticket'] > 3000000] y = [lowest_num_ticket['Survived'].mean(), sec_lowest_num_ticket['Survived'].mean(), thir_lowest_num_ticket['Survived'].mean(), four_lowest_num_ticket['Survived'].mean(), high_num_ticket['Survived'].mean() ] plt.figure() plt.bar(x, y) plt.xlabel('ticket number') plt.ylabel('Survived') plt.show()
数字が大きいほど生存率が小さくなるという傾向がありそうです。
アルファベットが入ったチケット
こちらは上に比べて分類が面倒です。正規表現を使ってうまくやっていきます。
A_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('A.+')] CA_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('C\.*A\.*.+')] PC_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('PC.+')] PP_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('PP.+')] SOTON_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('SOTON.+')] STON_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('STON.+')] LINE_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('LINE.*')] FC_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('F\.C\.(C\.)*.+')] W_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('W.+')] C_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('C.+')] SC_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('S(\.)*C.+')] SO_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('S(\.)*O.+')] other_ticket = num_alpha_ticket[ num_alpha_ticket['Ticket'].str.match( '(Fa)*(P/PP)*(S\.P)*(S\.*W)*.+' ) ] x = [i for i in range(1, 14)] y = [A_ticket['Survived'].mean(), CA_ticket['Survived'].mean(), PC_ticket['Survived'].mean() ,PP_ticket['Survived'].mean(), SOTON_ticket['Survived'].mean(), STON_ticket['Survived'].mean() ,LINE_ticket['Survived'].mean(), FC_ticket['Survived'].mean(), W_ticket['Survived'].mean() ,C_ticket['Survived'].mean(), SC_ticket['Survived'].mean(), SO_ticket['Survived'].mean() ,other_ticket['Survived'].mean() ] plt.figure() plt.bar(x, y) plt.ylabel('survived') plt.show()
このように場合分けが多岐にわたるときには、漏れがないか確認する手段を持っておくことが重要です。今回の場合は、セット型を使ってチェックが可能です。
new_set = set(A_ticket['Ticket']) | set(CA_ticket['Ticket']) |\ set(PC_ticket['Ticket']) | set(PP_ticket['Ticket']) |\ set(SOTON_ticket['Ticket']) | set(STON_ticket['Ticket']) |\ set(LINE_ticket['Ticket']) | set(FC_ticket['Ticket']) |\ set(W_ticket['Ticket']) | set(C_ticket['Ticket']) |\ set(SC_ticket['Ticket']) | set(SO_ticket['Ticket']) |\ set(other_ticket['Ticket']) set(num_alpha_ticket['Ticket']) - new_set
これで計算結果が空のセットになることを確認します。
グラフを見ると、チケットによって生存率には大きな差が出ていることがわかります。
チケットのラベリング
この結果をもとにして、チケットのラベリングを行います。
number_ticket.loc[number_ticket['Ticket'] <= 100000, 'Ticket'] = 14 number_ticket.loc[(number_ticket['Ticket'] > 100000) & (number_ticket['Ticket'] <= 200000), 'Ticket'] = 15 number_ticket.loc[(number_ticket['Ticket'] > 200000) & (number_ticket['Ticket'] <= 300000), 'Ticket'] = 13 number_ticket.loc[(number_ticket['Ticket'] > 300000) & (number_ticket['Ticket'] <= 400000), 'Ticket'] = 5 number_ticket.loc[number_ticket['Ticket'] > 3000000, 'Ticket'] = 6 num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('A.+'), 'Ticket'] = "1" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('C\.*A\.*.+'), 'Ticket'] = "8" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('PC.+'), 'Ticket'] = "16" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('PP.+'), 'Ticket'] = "18" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('SOTON.+'), 'Ticket'] = "3" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('STON.+'), 'Ticket'] = "11" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('LINE.*'), 'Ticket'] = "7" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('F\.C\.(C\.)*.+'), 'Ticket'] = "17" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('W.+'), 'Ticket'] = "4" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('C.+'), 'Ticket'] = "9" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('S(\.)*C.+'), 'Ticket'] = "12" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('S(\.)*O.+'), 'Ticket'] = "2" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('[^\d](Fa)*(P/PP)*(S\.P)*(S\.*W)*.+'), 'Ticket'] = "10" num_alpha_ticket['Ticket'] = num_alpha_ticket['Ticket'].apply(lambda x: int(x)) train = pd.concat([number_ticket, num_alpha_ticket])
ここまでの処理の結果
ここまでの処理で、各変数の相関係数がどのように変化したか見てみます。
plt.figure(figsize=(10, 8)) sns.heatmap(train.corr(), annot=True, cmap='Reds') plt.show()
TicketとSurvivedに比較的強い相関が出ました。
次回は、これらのデータを使って実際に機械学習をやってみたいと思います。
- 作者: 門脇大輔,阪田隆司,保坂桂佑,平松雄司
- 出版社/メーカー: 技術評論社
- 発売日: 2019/10/09
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る