kaggleのTitanic問題をといてみる

kaggleでチュートリアルがわりに使われているTitanicの問題を解いてみて実際に行われている分析の流れを把握できるようにしたいと思います。 kaggleでは個人の解答が公開、議論されているので普段分析をしない人でも学習にはちょうど良さそうな気がします。

まずはデータの読み込み

import pandas as pd
from pandas import Series,DataFrame

# numpy, matplotlib, seaborn
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('whitegrid')
%matplotlib inline

# machine learning
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC, LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB

# get titanic & test csv files as a DataFrame
titanic_df = pd.read_csv("train.csv")
test_df    = pd.read_csv("test.csv")

# preview the data

それから読み込んだ情報を確認してみます。

titanic_df.head()

f:id:steavevaivai:20171123191113p:plain

titanic_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    891 non-null int64
Survived       891 non-null int64
Pclass         891 non-null int64
Name           891 non-null object
Sex            891 non-null object
Age            714 non-null float64
SibSp          891 non-null int64
Parch          891 non-null int64
Ticket         891 non-null object
Fare           891 non-null float64
Cabin          204 non-null object
Embarked       889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB

Titanicの問題では学習データのcsvを読み込んで生き残った人(survived=1)を学習し、テストデータに対して分類を行うものとなっています。csvを読み込んだままの情報だと欠損があったり、不要な項目があったり、そのままでは分析に利用できない項目がありますので、実際に生存に影響のある項目だけが残すようにしていき機械学習して分類となります。

モデルの学習

とりあえず生存に明らかに必要のな誘うな項目は削除します。

titanic_df = titanic_df.drop(['PassengerId','Name','Ticket'], axis=1)
test_df    = test_df.drop(['Name','Ticket'], axis=1)

欠損が多い項目は削除しておきます。

titanic_df.drop("Cabin",axis=1,inplace=True)
test_df.drop("Cabin",axis=1,inplace=True)

Embarkedの項目を処理する

Embarkedの項目を分析で扱いようにします。まず各項目がどんな値を取っているのか確認してみます。

titanic_df[["Embarked", "Survived"]].groupby(['Embarked'],as_index=False).mean()

f:id:steavevaivai:20171123191043p:plain
C,Q,Sの3つの値取ることが確認できました。次に、titanic_df.info()実行時にEmbarkedは2項目欠損があったのでとりあえずSで埋めときます。

titanic_df["Embarked"] = titanic_df["Embarked"].fillna("S")

それからEmbarkedの項目が生存に影響しているかグラフ表示してみます。

sns.factorplot('Embarked','Survived', data=titanic_df,size=4,aspect=3)

f:id:steavevaivai:20171123191048p:plain
Sに比べてCとQの方が生存確率は高いかもしれないといった感じなのでしょうか。S,C,Qのぞれぞれの累計値、生存確率も出してみます。

fig, (axis1,axis2,axis3) = plt.subplots(1,3,figsize=(15,5))
sns.countplot(x='Embarked', data=titanic_df, ax=axis1)
sns.countplot(x='Survived', hue="Embarked", data=titanic_df, order=[1,0], ax=axis2)
embark_perc = titanic_df[["Embarked", "Survived"]].groupby(['Embarked'],as_index=False).mean()
sns.barplot(x='Embarked', y='Survived', data=embark_perc,order=['S','C','Q'],ax=axis3)

f:id:steavevaivai:20171123191038p:plain

C,Qかどうかを判断するための項目を分析用にデータフレームに追加します。

embark_dummies_titanic  = pd.get_dummies(titanic_df['Embarked'])
embark_dummies_titanic.drop(['S'], axis=1, inplace=True)

embark_dummies_test  = pd.get_dummies(test_df['Embarked'])
embark_dummies_test.drop(['S'], axis=1, inplace=True)

titanic_df = titanic_df.join(embark_dummies_titanic)
test_df    = test_df.join(embark_dummies_test)
titanic_df.drop(['Embarked'], axis=1,inplace=True)
test_df.drop(['Embarked'], axis=1,inplace=True)
titanic_df.head()

f:id:steavevaivai:20171123191108p:plain

Fare(運賃)の項目を処理する

欠損は中央値で埋めときます。

test_df["Fare"].fillna(test_df["Fare"].median(), inplace=True)

それからデータがFloat64になっていたのでintに変換しておきます。でグラフ表示します。

titanic_df['Fare'] = titanic_df['Fare'].astype(int)
test_df['Fare']    = test_df['Fare'].astype(int)
titanic_df['Fare'].plot(kind='hist', figsize=(15,3),bins=100, xlim=(0,50))

f:id:steavevaivai:20171123191059p:plain
生存者の運賃の平均と中央値を確認してみます。

fare_not_survived = titanic_df["Fare"][titanic_df["Survived"] == 0]
fare_survived     = titanic_df["Fare"][titanic_df["Survived"] == 1]

avgerage_fare = DataFrame([fare_not_survived.mean(), fare_survived.mean()])
std_fare      = DataFrame([fare_not_survived.std(), fare_survived.std()])
avgerage_fare.index.names = std_fare.index.names = ["Survived"]
avgerage_fare.plot(yerr=std_fare,kind='bar',legend=False)
std_fare.plot(yerr=std_fare,kind='bar',legend=False)

f:id:steavevaivai:20171123191104p:plain 運賃も生存に影響があったということでデータフレームに残しておきます。

年齢、性別を処理する

まずは年齢のデータを確認する
欠損値はSurvive毎で平均 ± 標準偏差の範囲の乱数を設定します。

average_age_titanic   = titanic_df["Age"].mean()
std_age_titanic       = titanic_df["Age"].std()
count_nan_age_titanic = titanic_df["Age"].isnull().sum()

average_age_test   = test_df["Age"].mean()
std_age_test       = test_df["Age"].std()
count_nan_age_test = test_df["Age"].isnull().sum()

rand_1 = np.random.randint(average_age_titanic - std_age_titanic, average_age_titanic + std_age_titanic, size = count_nan_age_titanic)
rand_2 = np.random.randint(average_age_test - std_age_test, average_age_test + std_age_test, size = count_nan_age_test)
titanic_df["Age"][np.isnan(titanic_df["Age"])] = rand_1
test_df["Age"][np.isnan(test_df["Age"])] = rand_2

Ageがfloat型だったのでint型に変換します。

titanic_df['Age'] = titanic_df['Age'].astype(int)
test_df['Age']    = test_df['Age'].astype(int)

それからグラフ表示します。

fig, (axis1,axis2) = plt.subplots(1,2,figsize=(15,4))
axis1.set_title('Original Age values - Titanic')
axis2.set_title('New Age values - Titanic')
titanic_df['Age'].dropna().astype(int).hist(bins=70, ax=axis1)
titanic_df['Age'].hist(bins=70, ax=axis2)

f:id:steavevaivai:20171125051734p:plain
年齢別の生存率を表示します。

facet = sns.FacetGrid(titanic_df, hue="Survived",aspect=4)
facet.map(sns.kdeplot,'Age',shade= True)
facet.set(xlim=(0, titanic_df['Age'].max()))
facet.add_legend()

fig, axis1 = plt.subplots(1,1,figsize=(18,4))
average_age = titanic_df[["Age", "Survived"]].groupby(['Age'],as_index=False).mean()
sns.barplot(x='Age', y='Survived', data=average_age)

f:id:steavevaivai:20171125051831p:plain
子供の方が生存率が高いことがわかります。

次に性別も合わせて子供か、成人男性か、成人女性かで分けてSurviveに相関関係があるかみてみます。

def get_person(passenger):
    age,sex = passenger
    return 'child' if age < 16 else sex
titanic_df['Person'] = titanic_df[['Age','Sex']].apply(get_person,axis=1)
test_df['Person']    = test_df[['Age','Sex']].apply(get_person,axis=1)
person_dummies_titanic  = pd.get_dummies(titanic_df['Person'])
person_dummies_titanic.columns = ['Child','Female','Male']
person_dummies_test  = pd.get_dummies(test_df['Person'])
person_dummies_test.columns = ['Child','Female','Male']

グラフで表示すると子供と女性が助かっていることがわかるのでデータフレームに残します。

fig, (axis1,axis2) = plt.subplots(1,2,figsize=(10,5))
sns.countplot(x='Person', data=titanic_df, ax=axis1)
person_perc = titanic_df[["Person", "Survived"]].groupby(['Person'],as_index=False).mean()
sns.barplot(x='Person', y='Survived', data=person_perc, ax=axis2, order=['male','female','child'])

f:id:steavevaivai:20171123201332p:plain

person_dummies_titanic.drop(['Male'], axis=1, inplace=True)
person_dummies_test.drop(['Male'], axis=1, inplace=True)
titanic_df = titanic_df.join(person_dummies_titanic)
test_df    = test_df.join(person_dummies_test)
titanic_df.drop(['Sex'],axis=1,inplace=True)
test_df.drop(['Sex'],axis=1,inplace=True)
titanic_df.drop(['Person'],axis=1,inplace=True)
test_df.drop(['Person'],axis=1,inplace=True)           

SibSp,Parchを処理する

SibSp, Parchは同伴者を表しているようで以下のように家族ずれかどうかを判断するように変換します。

titanic_df['Family'] =  titanic_df["Parch"] + titanic_df["SibSp"]
titanic_df['Family'].loc[titanic_df['Family'] > 0] = 1
titanic_df['Family'].loc[titanic_df['Family'] == 0] = 0

test_df['Family'] =  test_df["Parch"] + test_df["SibSp"]
test_df['Family'].loc[test_df['Family'] > 0] = 1
test_df['Family'].loc[test_df['Family'] == 0] = 0

Familyの項目に変換したので使わなくなったSibSp, Parchは削除します。

titanic_df = titanic_df.drop(['SibSp','Parch'], axis=1)
test_df    = test_df.drop(['SibSp','Parch'], axis=1)

それからグラフ表示したら家族ずれの方が生存確率がたかそうなのがわかるので残しておきます。

fig, (axis1,axis2) = plt.subplots(1,2,sharex=True,figsize=(10,5))

# sns.factorplot('Family',data=titanic_df,kind='count',ax=axis1)
sns.countplot(x='Family', data=titanic_df, order=[1,0], ax=axis1)

# average of survived for those who had/didn't have any family member
family_perc = titanic_df[["Family", "Survived"]].groupby(['Family'],as_index=False).mean()
sns.barplot(x='Family', y='Survived', data=family_perc, order=[1,0], ax=axis2)

axis1.set_xticklabels(["With Family","Alone"], rotation=0)

f:id:steavevaivai:20171123201307p:plain

Pclassを処理する

表示してみる

sns.factorplot('Pclass','Survived',order=[1,2,3], data=titanic_df,size=5)
pclass_dummies_titanic  = pd.get_dummies(titanic_df['Pclass'])
pclass_dummies_titanic.columns = ['Class_1','Class_2','Class_3']

pclass_dummies_test  = pd.get_dummies(test_df['Pclass'])
pclass_dummies_test.columns = ['Class_1','Class_2','Class_3']

titanic_df.drop(['Pclass'],axis=1,inplace=True)
test_df.drop(['Pclass'],axis=1,inplace=True)

f:id:steavevaivai:20171125052137p:plain
pclassは1,2の場合に生存率高いことがわかったのでpclass1,2かどうか判定した結果をデータフレームに付与します。

pclass_dummies_test.drop(['Class_3'], axis=1, inplace=True)
pclass_dummies_titanic.drop(['Class_3'], axis=1, inplace=True)

titanic_df = titanic_df.join(pclass_dummies_titanic)
test_df    = test_df.join(pclass_dummies_test)

最終的にモデルはこのようになりました。

titanic_df.head()

f:id:steavevaivai:20171125055040p:plain

機械学習

このように分析に影響のある項目だけが残るようにデータフレームを操作するのですが、最終的には機械学習で分類を行います。scikit-learnなら簡単に使うことができるので良いと思います。ロジスティック回帰、ランダムフォレストであれば以下のようになります。

# ロジスティック回帰
X_train = titanic_df.drop("Survived",axis=1)
Y_train = titanic_df["Survived"]

logreg = LogisticRegression()
logreg.fit(X_train, Y_train)
Y_pred = logreg.predict(X_test)
logreg.score(X_train, Y_train)
0.8058361391694725

# ランダムフォレスト
random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, Y_train)
Y_pred = random_forest.predict(X_test)
random_forest.score(X_train, Y_train)
0.9640852974186308

今回はランダムフォレストの方がうまく分類できているようです。事前に意味のある情報に分類をしていたのでその場合はランダムフォレストの精度がよくなるのでしょうか。 kaggleのkernelをみたら実際の分析手順を追えるけど欠損の扱いや目的変数に対して影響があるのかを判断して説明できるようになるにはちゃんと勉強した方がよさそうな気がします。