pyhaya’s diary

プログラミング、特にPythonについての記事を書きます。Djangoや機械学習などホットな話題をわかりやすく説明していきたいと思います。

Kaggleのタイタニックデータの解析

Kaggleの定番データセットといえば「タイタニックの生存者予測」です。今回は生存者の予測を目指して解析を行っていきたいと思います。データの可視化について詳しい説明は前回記事で書いているのでそちらを参照してください。
Titanic: Machine Learning from Disaster | 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()

f:id:pyhaya:20181118104522p:plain

いくつかの変数があることがわかります。

  • 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)


ここまでやってきて、データの状況は下のようになっています。
f:id:pyhaya:20181118181123p:plain

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()

f:id:pyhaya:20181118192749p:plain

大きく分けて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()

f:id:pyhaya:20181118194155p:plain

数字が大きいほど生存率が小さくなるという傾向がありそうです。

アルファベットが入ったチケット

こちらは上に比べて分類が面倒です。正規表現を使ってうまくやっていきます。

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()

f:id:pyhaya:20181118194601p:plain

このように場合分けが多岐にわたるときには、漏れがないか確認する手段を持っておくことが重要です。今回の場合は、セット型を使ってチェックが可能です。

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()

f:id:pyhaya:20181122001403p:plain

TicketとSurvivedに比較的強い相関が出ました。

次回は、これらのデータを使って実際に機械学習をやってみたいと思います。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術