タイタニック号生存者予測

はじめに

こんにちは!
今回はKaggleチュートリアルの「タイタニック号生存者予測」を行ってみました。

簡単にKaggleとは、機械学習・データサイエンスに関わる人のコミュニティーです。Kaggleの特徴の一つにコンペがあります。
様々なコンペがあるのですが、今回行うのはその中でもチュートリアルとして常設されているものになります。

Kaggleの始め方については下記URLの記事がわかりやすかったため、参考にしてください。
https://aizine.ai/kaggle-01-0905/

それでは、考えたことや、コード等説明します。

考え方

次のような流れで考えました。
1:データの確認
2:相関を調べる
3:データを整理する
4:モデルの選択

データは以下のようになっています。

スクリーンショット 2020-11-02 15.15.52

それぞれが何を表しているかは次の通りです。
・PassengerID:乗船者のID
・Survived:0=死亡、1=生存
・Pclass:部屋のランク(1:高級、2:一般、3:安い)
・Name:名前
・ Sex: 性別
・Age: 年齢
・Sibsp: 一緒に乗船していた兄弟、配偶者の数(横のつながり)
・ Parch: 一緒に乗船していた親、子ども、孫の数(縦のつながり)
・Ticket: チケットの乗船番号
・ Fare: 乗船費用
・Cabin: 部屋番号
・Embarked: 出発した港の名前
C = Cherbourg, Q = Queenstown, S = Southampton

次に欠損値、データの重複についてみてみましょう。以下のコードで確認できます。

import pandas as pd

train_df = pd.read_csv("/kaggle/input/titanic/train.csv")
test_df = pd.read_csv("/kaggle/input/titanic/test.csv")

print(train_df.info())
print(test_df.info())
print(train_df.describe(include=["O"]))
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
#   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
0   PassengerId  891 non-null    int64  
1   Survived     891 non-null    int64  
2   Pclass       891 non-null    int64  
3   Name         891 non-null    object 
4   Sex          891 non-null    object 
5   Age          714 non-null    float64
6   SibSp        891 non-null    int64  
7   Parch        891 non-null    int64  
8   Ticket       891 non-null    object 
9   Fare         891 non-null    float64
10  Cabin        204 non-null    object 
11  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
None

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
#   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
0   PassengerId  418 non-null    int64  
1   Pclass       418 non-null    int64  
2   Name         418 non-null    object 
3   Sex          418 non-null    object 
4   Age          332 non-null    float64
5   SibSp        418 non-null    int64  
6   Parch        418 non-null    int64  
7   Ticket       418 non-null    object 
8   Fare         417 non-null    float64
9   Cabin        91 non-null     object 
10  Embarked     418 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB
None

                            Name   Sex    Ticket Cabin Embarked
count                        891   891       891   204      889
unique                       891     2       681   147        3
top     Sage, Mr. Douglas Bullen  male  CA. 2343    G6        S
freq                           1   577         7     4      644

訓練用には全部で891個のデータがあり、Age、Cabin、Embarkedに欠損があります。
テスト用ではAge、Fare、Cabinにあります。
そしてNameの部分、countとuniqueが等しいのでデータの被りがないことがわかります。

続いて、Pclass、Sex、Parch、SibSpとSurvivedの関係をみていきます。
例えばSexに関しては男性、女性と分けてSurvivedの平均を取っています。
これをそれぞれについて行いました。

import pandas as pd

train_df = pd.read_csv("/kaggle/input/titanic/train.csv")
test_df = pd.read_csv("/kaggle/input/titanic/test.csv")

pclass_pivot = train_df[["Pclass", "Survived"]].groupby(["Pclass"], as_index=False).mean().sort_values(by="Survived", ascending=False)
print(pclass_pivot)

Sex_pivot = train_df[["Sex", "Survived"]].groupby(["Sex"], as_index=False).mean().sort_values(by="Survived", ascending=False)
print(Sex_pivot)

parch_pivot = train_df[["Parch", "Survived"]].groupby(["Parch"], as_index=False).mean().sort_values(by="Survived", ascending=False)
print(parch_pivot)

sibsp_pivot = train_df[["SibSp", "Survived"]].groupby(['SibSp'], as_index=False).mean().sort_values(by='Survived', ascending=False)
print(sibsp_pivot)
   Pclass  Survived        Sex  Survived
0       1  0.629630   0  female  0.742038
1       2  0.472826   1    male  0.188908
2       3  0.242363

  Parch  Survived       SibSp  Survived
3      3  0.600000    1      1  0.535885
1      1  0.550847    2      2  0.464286
2      2  0.500000    0      0  0.345395
0      0  0.343658    3      3  0.250000
5      5  0.200000    4      4  0.1666675      
4      4  0.000000    5      5  0.000000
6      6  0.000000    6      8  0.000000

これを見るとPclass、Sexは明らかに関係がありそうです。
またParch、SibSpははっきりとは掴めません。
Ageについてもみてみましょう。

import matplotlib.pyplot as plt
import pandas as pd

train_df = pd.read_csv("/kaggle/input/titanic/train.csv")
test_df = pd.read_csv("/kaggle/input/titanic/test.csv")

Age_se = train_df["Age"].dropna()

train_df["Age"].hist(bins=8)
plt.show()

train_df["Age"].hist(by=train_df["Survived"], bins=10)
plt.show()

ダウンロード (1)

ダウンロード (2)

グラフで表してみました。全体の年齢層は20-30代が最も多くなっています。Survivedで分けたときは、だいたい15歳以下の生存率が高く、15-30歳ぐらいの方が多く死亡し、20-40歳ぐらいの方が多く生き残っているように読み取れます。

Fareについてもみてみましょう。

import pandas as pd
import matplotlib.pyplot as plt

train_df = pd.read_csv("/kaggle/input/titanic/train.csv")
test_df = pd.read_csv("/kaggle/input/titanic/test.csv")

pivot = train_df[["Survived", "Fare"]].groupby(["Survived"], as_index=False).mean()

plt.bar(pivot["Survived"], pivot["Fare"])
plt.xlabel('Survived')
plt.ylabel('Fare')

plt.show()

ダウンロード (6)

生き残っている人のが高い料金を払っていることがわかります。

最後にEmbarkedについて。

import pandas as pd
import matplotlib.pyplot as plt

train_df = pd.read_csv("/kaggle/input/titanic/train.csv")
test_df = pd.read_csv("/kaggle/input/titanic/test.csv")

pivot = train_df[["Survived", "Embarked"]].groupby(["Embarked"], as_index=False).mean()

plt.bar(pivot["Embarked"], pivot["Survived"])
plt.show()

ダウンロード (8)

Cherbourgから乗船している人の生存率が高いようです。

これより各項目と生死についての関係性をまとめると、
・Pclass 関係あり
・Sex 関係あり
・Age 関係あり
・Fare 関係あり
・Embarked 関係あり
・Parch/SubSp 不明
となります。チケット番号や欠損の多かった部屋番号は取り除くことにします。
また、名前はMr.やMrs.等の敬称の部分のみ活用したいと思います。

コード編

これが今回書いたコードになります。#1、#2のように部分ごとに番号を振ってあります。

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split


train_df = pd.read_csv("/kaggle/input/titanic/train.csv")
test_df = pd.read_csv("/kaggle/input/titanic/test.csv")

#1
train_df = train_df.drop(["Ticket", "Cabin"], axis=1)
test_df = test_df.drop(["Ticket", "Cabin"], axis=1)
combine = [train_df, test_df]

#2
for dataset in combine:
   dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
   
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Capt": 5, "Lady": 6, "Countess": 7, "Col": 8, "Don": 9, "Dr": 10, "Major": 11, "Rev": 12, "Sir": 13, "Jonkheer": 14, "Mlle": 15, "Ms": 16, "Mme": 17}
for dataset in combine:
   dataset["Title"] = dataset["Title"].map(title_mapping)
   dataset['Title'] = dataset['Title'].fillna(0)
   
train_df = train_df.drop(['Name', 'PassengerId'], axis=1)
test_df = test_df.drop(['Name'], axis=1)
combine = [train_df, test_df]

#3
for dataset in combine:
   dataset['Sex'] = dataset['Sex'].map( {'female': 1, 'male': 0} ).astype(int)

#4   
guess_ages = np.zeros((2,3))
for dataset in combine:
   for i in range(0, 2):
       for j in range(0, 3):
           guess_df = dataset[(dataset['Sex'] == i) & (dataset['Pclass'] == j+1)]['Age'].dropna()
           age_guess = guess_df.median()
           guess_ages[i,j] = int( age_guess/0.5 + 0.5 ) * 0.5
   for i in range(0, 2):
       for j in range(0, 3):
           dataset.loc[ (dataset.Age.isnull()) & (dataset.Sex == i) & (dataset.Pclass == j+1), 'Age'] = guess_ages[i,j]
   dataset['Age'] = dataset['Age'].astype(int)
   
for dataset in combine:    
   dataset.loc[dataset['Age'] <= 10, 'Age'] = 0
   dataset.loc[(dataset['Age'] > 10) & (dataset['Age'] <= 20), 'Age'] = 1
   dataset.loc[(dataset['Age'] > 20) & (dataset['Age'] <= 30), 'Age'] = 2
   dataset.loc[(dataset['Age'] > 30) & (dataset['Age'] <= 40), 'Age'] = 3
   dataset.loc[(dataset['Age'] > 40) & (dataset['Age'] <= 50), 'Age'] = 4
   dataset.loc[(dataset['Age'] > 50) & (dataset['Age'] <= 60), 'Age'] = 5
   dataset.loc[(dataset['Age'] > 60) & (dataset['Age'] <= 70), 'Age'] = 6
   dataset.loc[(dataset['Age'] > 70) & (dataset['Age'] <= 80), 'Age'] = 7
   dataset.loc[(dataset['Age'] > 80) & (dataset['Age'] <= 90), 'Age'] = 8

#5
freq_port = train_df.Embarked.dropna().mode()[0]
for dataset in combine:
   dataset['Embarked'] = dataset['Embarked'].fillna(freq_port)

for dataset in combine:
   dataset['Embarked'] = dataset['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)

#6
test_df['Fare'].fillna(test_df['Fare'].dropna().median(), inplace=True)

for dataset in combine:
   dataset.loc[ dataset['Fare'] <= 7.55, 'Fare'] = 0
   dataset.loc[(dataset['Fare'] > 7.55) & (dataset['Fare'] <= 7.854), 'Fare'] = 1
   dataset.loc[(dataset['Fare'] > 7.854) & (dataset['Fare'] <= 8.05), 'Fare']   = 2
   dataset.loc[(dataset['Fare'] > 8.05) & (dataset['Fare'] <= 10.5), 'Fare'] = 3
   dataset.loc[(dataset['Fare'] > 10.5) & (dataset['Fare'] <= 14.454), 'Fare']   = 4
   dataset.loc[(dataset['Fare'] > 14.454) & (dataset['Fare'] <= 21.679), 'Fare'] = 5
   dataset.loc[(dataset['Fare'] > 21.679) & (dataset['Fare'] <= 27.0), 'Fare']   = 6
   dataset.loc[(dataset['Fare'] > 27.0) & (dataset['Fare'] <= 39.688), 'Fare'] = 7
   dataset.loc[(dataset['Fare'] > 39.688) & (dataset['Fare'] <= 77.958), 'Fare']   = 8
   dataset.loc[(dataset['Fare'] > 77.958) & (dataset['Fare'] <= 512.329), 'Fare'] = 9
   dataset['Fare'] = dataset['Fare'].astype(int)

#7
X_train = train_df.drop("Survived", axis=1)
Y_train = train_df["Survived"]
X_test = test_df.drop("PassengerId", axis=1)

random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, Y_train)

random_pred = random_forest.predict(X_test)
acc_random_forest = round(random_forest.score(X_train, Y_train)* 100, 2)

print(acc_random_forest)

submission = pd.DataFrame({
       "PassengerId": test_df["PassengerId"],
       "Survived": random_pred
   })

submission.to_csv('/kaggle/working/my_submission.csv', index=False)

#1
TicketとCabinをデータの中から取り除きます。
操作がしやすいようcombineとしてまとめておきます。

train_df = train_df.drop(["Ticket", "Cabin"], axis=1)
test_df = test_df.drop(["Ticket", "Cabin"], axis=1)
combine = [train_df, test_df]

#2
名前の中から敬称の部分を取り出し、Titleという欄を新しく作り敬称を入れていきます。その後それぞれの敬称に対し番号を振っていきます。
最後にNameとPassengerIDを取り除きます。

for dataset in combine:
   dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
   
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Capt": 5, "Lady": 6, "Countess": 7, "Col": 8, "Don": 9, "Dr": 10, "Major": 11, "Rev": 12, "Sir": 13, "Jonkheer": 14, "Mlle": 15, "Ms": 16, "Mme": 17}
for dataset in combine:
   dataset["Title"] = dataset["Title"].map(title_mapping)
   dataset['Title'] = dataset['Title'].fillna(0)
   
train_df = train_df.drop(['Name', 'PassengerId'], axis=1)
test_df = test_df.drop(['Name'], axis=1)
combine = [train_df, test_df]

#3
Sexに番号を振ります。

for dataset in combine:
   dataset['Sex'] = dataset['Sex'].map( {'female': 1, 'male': 0} ).astype(int)

#4
Ageの欠損値を埋めていきます。
今回は同じ港から乗船し同性の人の年齢の中間値で埋めました。
そして、年代ごとに番号を振りました。

guess_ages = np.zeros((2,3))
for dataset in combine:
   for i in range(0, 2):
       for j in range(0, 3):
           guess_df = dataset[(dataset['Sex'] == i) & (dataset['Pclass'] == j+1)]['Age'].dropna()
           age_guess = guess_df.median()
           guess_ages[i,j] = int( age_guess/0.5 + 0.5 ) * 0.5
   for i in range(0, 2):
       for j in range(0, 3):
           dataset.loc[ (dataset.Age.isnull()) & (dataset.Sex == i) & (dataset.Pclass == j+1), 'Age'] = guess_ages[i,j]
   dataset['Age'] = dataset['Age'].astype(int)
   
for dataset in combine:    
   dataset.loc[ dataset['Age'] <= 10, 'Age'] = 0
   dataset.loc[(dataset['Age'] > 10) & (dataset['Age'] <= 20), 'Age'] = 1
   dataset.loc[(dataset['Age'] > 20) & (dataset['Age'] <= 30), 'Age'] = 2
   dataset.loc[(dataset['Age'] > 30) & (dataset['Age'] <= 40), 'Age'] = 3
   dataset.loc[(dataset['Age'] > 40) & (dataset['Age'] <= 50), 'Age'] = 4
   dataset.loc[(dataset['Age'] > 50) & (dataset['Age'] <= 60), 'Age'] = 5
   dataset.loc[(dataset['Age'] > 60) & (dataset['Age'] <= 70), 'Age'] = 6
   dataset.loc[(dataset['Age'] > 70) & (dataset['Age'] <= 80), 'Age'] = 7
   dataset.loc[(dataset['Age'] > 80) & (dataset['Age'] <= 90), 'Age'] = 8

#5
Embarkedの欠損値は一番多くの人が乗った港を入れました。
そしてそれぞれの港に番号を振ります。

freq_port = train_df.Embarked.dropna().mode()[0]
for dataset in combine:
   dataset['Embarked'] = dataset['Embarked'].fillna(freq_port)

for dataset in combine:
   dataset['Embarked'] = dataset['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)

#6
テストデータにFareの欠損値があるので中間値で埋めました。
その後、価格帯で分類し番号を振っています。
分類の数値に関しては次のコードで出てきた数値を使っています。

train_df['FareBand'] = pd.qcut(train_df['Fare'],10)
train_df[['FareBand', 'Survived']].groupby(['FareBand'], as_index=False).mean().sort_values(by='FareBand', ascending=True)​
test_df['Fare'].fillna(test_df['Fare'].dropna().median(), inplace=True)

for dataset in combine:
   dataset.loc[ dataset['Fare'] <= 7.55, 'Fare'] = 0
   dataset.loc[(dataset['Fare'] > 7.55) & (dataset['Fare'] <= 7.854), 'Fare'] = 1
   dataset.loc[(dataset['Fare'] > 7.854) & (dataset['Fare'] <= 8.05), 'Fare']   = 2
   dataset.loc[(dataset['Fare'] > 8.05) & (dataset['Fare'] <= 10.5), 'Fare'] = 3
   dataset.loc[(dataset['Fare'] > 10.5) & (dataset['Fare'] <= 14.454), 'Fare']   = 4
   dataset.loc[(dataset['Fare'] > 14.454) & (dataset['Fare'] <= 21.679), 'Fare'] = 5
   dataset.loc[(dataset['Fare'] > 21.679) & (dataset['Fare'] <= 27.0), 'Fare']   = 6
   dataset.loc[(dataset['Fare'] > 27.0) & (dataset['Fare'] <= 39.688), 'Fare'] = 7
   dataset.loc[(dataset['Fare'] > 39.688) & (dataset['Fare'] <= 77.958), 'Fare']   = 8
   dataset.loc[(dataset['Fare'] > 77.958) & (dataset['Fare'] <= 512.329), 'Fare'] = 9
   dataset['Fare'] = dataset['Fare'].astype(int)

ここまでで、データの中身はこのようになっています。

   Survived  Pclass  Sex  Age  SibSp  Parch  Fare  Embarked  Title
0         0       3    0    2      1      0     0         0      1
1         1       1    1    3      1      0     8         1      3
2         1       3    1    2      0      0     2         0      2
3         1       1    1    3      1      0     8         0      3
4         0       3    0    3      0      0     2         0      1
5         0       3    0    2      0      0     3         2      1
6         0       1    0    5      0      0     8         0      1
7         0       3    0    0      3      1     5         0      4
8         1       3    1    2      0      2     4         0      3
9         1       2    1    1      1      0     7         1      3

#7
最後にモデルを用いて予測を行っていきます。
なぜRandomForestを使っているかというと、様々なモデルで検証してみた結果これが一番いい数値を出したからです。
参考までに↓

                     Model  Score
3            Random Forest  91.36
5            Decision Tree  91.36
1                      KNN  86.42
4               Linear SVC  80.25
2      Logistic Regression  78.77
7               perceptron  77.67
0  Support Vector Machines  75.76
6                 gaussian  73.06
8                      sgd  48.37

はじめに訓練用データのラベル作りをしています。
そしてランダムフォレストを用いて予測を行い、CSVファイルに保存しています。

X_train = train_df.drop("Survived", axis=1)
Y_train = train_df["Survived"]
X_test = test_df.drop("PassengerId", axis=1)

random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, Y_train)

random_pred = random_forest.predict(X_test)
acc_random_forest = round(random_forest.score(X_train, Y_train)* 100, 2)

print(acc_random_forest)

submission = pd.DataFrame({
       "PassengerId": test_df["PassengerId"],
       "Survived": random_pred
   })

submission.to_csv('/kaggle/working/my_submission.csv', index=False)

提出した結果がこちら!

スクリーンショット 2020-11-02 15.02.41

スコアは76.315%でした。順位はおそらく14374位。
全体で17000人ぐらいなのでまだまだでした。

改善点

・今回は家族・親戚の部分をそのまま使っていますが、例えばそれらをまとめて一つの特微量にする
・敬称をまとめてみる(Mr/Miss/Mrsの3つなど)
・年齢、価格帯の分け方を変えてみる。
・欠損値の埋め方を変える。
・Age、Fare、Embarkedをまとめて一つの特微量にする

などまだまだできることはたくさんあると思います。
今度挑戦してみたいと思います。


この記事が気に入ったらサポートをしてみませんか?