KaggleのTitanicで生データを確認する（kaggle⑥）

#はじめに
初めて<b>Kaggle（カグル）</b>のコンペに参加してみたお話です。
[前回の「KaggleのTitanicでscikit-learnの全モデルを試す」](https://qiita.com/sudominoru/items/4c1d48093bbd3500dafb)では、scikit-learnのモデルに対して「交差検証」を行うことでスコアを少し上げることができました。
今回は、本来なら最初にやるべきだった「生データを確認する」ことをやってみたいと思います。


#目次
1.生データを確認する意義
2.結果
3.生データを確認する
4.学習する
5.全コード
6.まとめ

履歴

#1.生データを確認する意義

[会社を変える分析の力](https://www.amazon.co.jp/dp/4062882183)という本を読みました。
本の内容の１つに、「データを分析する前に生データを確認しましょう」と書かれています。
生データを見ないと異常値も発見できない。
データ分析を始める前に、まずは生データをビジュアル化して異常値がないか視覚的に見る。
そういった習慣を身につけてくださいとのことです。
生データを確認し、異常値の確認やデータの利用方法など再度確認してみましょう。

#2.結果

結果から言うと、入力データを精査することで、さらにスコアが少しあがり「0.80382」になりました。
上位9%（2020/1/7現在）という結果です。
提出までの流れを見ていきたいと思います。

#3.生データを確認する

いくつかの生データを確認してみましょう。

### Fare(運賃)
pclass(チケットクラス)ごとに運賃を散布図にしてみます。
以下のようになりました。

![20200109_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/ca62ad59-e882-59b2-adef-176d419f933d.png)

横軸がpclassです。
「1」の運賃が高い傾向にあります。チケットクラスとしては、1 > 2 > 3 の順でグレードが良くなるようです。
散布図から運賃「0」がそれぞれのpclassにあることが分かります。
生データを見てみましょう。運賃（Fare）の昇順でソートします。

|PassengerId|Survived|Pclass|Sex|Age|SibSp|Parch|Ticket|Fare|Cabin|Embarked|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--
|180|0|3|male|36|0|0|LINE|0||S|
|264|0|1|male|40|0|0|112059|0|B94|S|
|272|1|3|male|25|0|0|LINE|0||S|
|278|0|2|male||0|0|239853|0||S|
|303|0|3|male|19|0|0|LINE|0||S|
|414|0|2|male||0|0|239853|0||S|
|467|0|2|male||0|0|239853|0||S|
|482|0|2|male||0|0|239854|0||S|
|598|0|3|male|49|0|0|LINE|0||S|
|634|0|1|male||0|0|112052|0||S|
|675|0|2|male||0|0|239856|0||S|
|733|0|2|male||0|0|239855|0||S|
|807|0|1|male|39|0|0|112050|0|A36|S|
|816|0|1|male||0|0|112058|0|B102|S|
|823|0|1|male|38|0|0|19972|0||S|
|379|0|3|male|20|0|0|2648|4.0125||C|
|873|0|1|male|33|0|0|695|5|B51 B53 B55|S|
|327|0|3|male|61|0|0|345364|6.2375||S|
|844|0|3|male|34.5|0|0|2683|6.4375||C|

Fareの昇順にするとFare「0」でPClassが1、2、3が存在しています。
Fare「0」は無料ではなく「運賃不明」の意味が近そうです。
Fare「0」は学習データから除外しましょう。
Fare「0」を除外して再度散布図を作成すると以下になります。

![20200109_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/f58ddec5-75c8-f642-d51b-c99fd5489b03.png)

少し見やすくなりました。pclass「1」の小さい点も気になります。
上の表をみると、pclass「1」でFare「5」のデータがあります。
これも異常値の可能性があるため、除外しましょう。

![20200109_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/b4fff035-02ca-fa5e-1a24-8de4ce59ba2c.png)

pclassごとに運賃の幅がある程度定まった散布図になりました。

### Ticket(チケット番号)
チケット番号は名義尺度です。
チケット番号の昇順に並べてみます。

|PassengerId|Survived|Pclass|Sex|Age|SibSp|Parch|Ticket|Fare|Cabin|Embarked|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|258|1|1|female|30|0|0|110152|86.5|B77|S|
|505|1|1|female|16|0|0|110152|86.5|B79|S|
|760|1|1|female|33|0|0|110152|86.5|B77|S|
|263|0|1|male|52|1|1|110413|79.65|E67|S|
|559|1|1|female|39|1|1|110413|79.65|E67|S|
|586|1|1|female|18|0|2|110413|79.65|E68|S|
|111|0|1|male|47|0|0|110465|52|C110|S|
|476|0|1|male||0|0|110465|52|A14|S|
|431|1|1|male|28|0|0|110564|26.55|C52|S|
|367|1|1|female|60|1|0|110813|75.25|D37|C|

チケット番号を見ると、数字のみであったり、文字と数字の組み合わせだったり、規則性は読み取れません。
同じチケット番号の人がいることも分かります。
氏名を見ると同じチケット番号の人は、同じ姓であることが多いです。家族だと思われます。
また、同じチケット番号の人の Survived と見比べてみると、同じチケット番号だと Survived が同じ傾向にあります。
チケット番号でラベル付けする方針で検討します。
以下のイメージです。

|PassengerId|Survived|Ticket|Ticket（ラベル）|
|:--|:--|:--|:--|
|505|1|110152|チケットA|
|258|1|110152|チケットA|
|760|1|110152|チケットA|
|586|1|110413|チケットB|
|559|1|110413|チケットB|
|263|0|110413|チケットB|
|111|0|110465|チケットC|
|476|0|110465|チケットC|
|431|1|110564|NaN|
|367|1|110813|NaN|

同じチケット番号をグループ化したいため、重複がないチケットは「NaN」にします。
チケットA、Bをそのまま数字化してもよいのですが、ラベル付けであることを明記するため、One-Hot エンコーディングを行います。
以下のようなイメージです。
ソースコードは後で記載しますが、 pandas.get_dummies で One-Hot エンコーディングできます。


|PassengerId|Survived|チケットA|チケットB|チケットC|
|:--|:--|:--|:--|:--|
|505|1|1|0|0|
|258|1|1|0|0|
|760|1|1|0|0|
|586|1|0|1|0|
|559|1|0|1|0|
|263|0|0|1|0|
|111|0|0|0|1|
|476|0|0|0|1|
|431|1|0|0|0|
|367|1|0|0|0|

### sibsp(兄弟/配偶者の数) / parch(親/子供の数)
sibsp と parch は[以前](https://qiita.com/sudominoru/items/840e87cc77de29f10ca2)もグラフ化していましたが、再度グラフ化してみましょう。
![20200119_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/f959fee1-4c32-d6dc-8fe2-f8158e94c4a9.png)
![20200119_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/e9240d8d-6c11-0b12-296c-e61e2f41ea39.png)
相関係数では有意な差はありませんでしたが、sibsp、parchともに、グラフをみると以下が分かります。
・sibsp、parchが 0 の場合、Survived は 0 の方が多い（倍くらい）
・sibsp、parchが 1 または 2の場合、Survived の 0 と 1 は同じくらい
・sibsp、parchが 3以上は母数が少ない

前回は、相関係数が小さいため学習データから除外しましたが、0、1、2 はラベルデータとして利用できそうです。
sibsp と parch が3以下のデータのみ抽出して相関係数を確認すると以下になりました。

```python
# SibSpが3未満の場合のクラメール連関係数を確認する
df_SibSp = df[df['SibSp'] < 3]
cramersV(df_SibSp['Survived'], df_SibSp['SibSp'])

# SurvivedとSibSp(3未満)のクロス集計表を表示する
cross_sibsp = pandas.crosstab(df_SibSp['Survived'], df_SibSp['SibSp'])
cross_sibsp

cross_sibsp.T.plot(kind='bar', stacked=False, width=0.8)
plt.show()
```

```python
0.16260950922794606
```

![20200120_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/90dfc68a-f672-8994-a2ba-16f031593b81.png)

相関係数は 0.16 で「弱い相関あり」になりました。
省略しますが、Parch も同様の結果になります。
そこで、Ticketと同様、SibSp と Parch も One-Hot エンコーディングしてみましょう。
以下のようなイメージです。

|PassengerId|Survived|SibSp_1|SibSp_2|SibSp_3|SibSp_4|SibSp_5|SibSp_8|
|:--|:--|:--|:--|:--|:--|:--|:--|
|505|1|0|0|0|0|0|0|
|258|1|0|0|0|0|0|0|
|760|1|0|0|0|0|0|0|
|586|1|0|0|0|0|0|0|
|559|1|1|0|0|0|0|0|
|263|0|1|0|0|0|0|0|
|111|0|0|0|0|0|0|0|
|476|0|0|0|0|0|0|0|
|431|1|0|0|0|0|0|0|
|367|1|1|0|0|0|0|0|

### Cabin(客室番号)

Cabinを確認してみましょう。
検証データ（train.csv）約900件中、Cabin200件程度です。
Cabinは名義尺度です。
先頭1文字を同じグループとみなしてグループ化してみると以下になりました。

![20200120_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/fea3bb29-2437-9258-781b-60cf20b3a017.png)

それぞれ、Survived「1」が多い結果になっています。
先頭1文字のラベルデータは有用そうです。
Cabinも 先頭1文字を One-Hot エンコーディングしてみましょう。
以下のようなイメージです。

|PassengerId|Survived|Cabin_A|Cabin_B|Cabin_C|Cabin_D|Cabin_E|Cabin_F|Cabin_G|Cabin_T|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|505|1|0|1|0|0|0|0|0|0|
|258|1|0|1|0|0|0|0|0|0|
|760|1|0|1|0|0|0|0|0|0|
|586|1|0|0|0|0|1|0|0|0|
|559|1|0|0|0|0|1|0|0|0|
|263|0|0|0|0|0|1|0|0|0|
|111|0|0|0|1|0|0|0|0|0|
|476|0|1|0|0|0|0|0|0|0|
|431|1|0|0|1|0|0|0|0|0|
|367|1|0|0|0|1|0|0|0|0|

#4.学習する

ここまでの状況をもとに学習してみましょう。
入力データは以下です。

|No|項目名|項目名(日本語)|変換方式|
|:--|:--|:--|:--|
|1|Pclass|チケットクラス|標準化|
|2|Sex|性別|数値化|
|3|SibSp|兄弟/配偶者|one-hotエンコーディング|
|4|Parch|親/子供|one-hotエンコーディング|
|5|Ticket|チケット番号|one-hotエンコーディング|
|6|Fare|運賃|標準化|
|7|Cabin|客室番号|先頭1文字をone-hotエンコーディング|


[kaggle⑤](https://qiita.com/sudominoru/items/4c1d48093bbd3500dafb)の全モデルを試す、さらに、[kaggle④](https://qiita.com/sudominoru/items/1c21cf4afaf67fda3fee)のグリッドサーチでモデルとパラメータを試したところ、以下になりました。

```python
GradientBoostingClassifier(criterion='friedman_mse', init=None,
                           learning_rate=0.1, loss='exponential', max_depth=6,
                           max_features=None, max_leaf_nodes=None,
                           min_impurity_decrease=0.0, min_impurity_split=None,
                           min_samples_leaf=1, min_samples_split=2,
                           min_weight_fraction_leaf=0.0, n_estimators=100,
                           n_iter_no_change=None, presort='auto',
                           random_state=1, subsample=1.0, tol=0.0001,
                           validation_fraction=0.1, verbose=0,
                           warm_start=False)
```


#5.全コード
全コードは以下です。
ただし、実際に学習させると「Cabin」を含めた場合にスコアが上がらなかったため、最終的にCabinは除外しました。


```python
import numpy
import pandas 
import matplotlib.pyplot as plt

######################################
# クラメールの連関係数
# Cramer's coefficient of association
# 0.5  >= : 非常に強い相関（Very strong correlation） 
# 0.25 >= : 強い相関（strong correlation） 
# 0.1  >= : やや弱い相関（Slightly weak correlation） 
# 0.1　< :  相関なし（No correlation） 
######################################
def cramersV(x, y):
    """
    Calc Cramer's V.

    Parameters
    ----------
    x : {numpy.ndarray, pandas.Series}
    y : {numpy.ndarray, pandas.Series}
    """
    table = numpy.array(pandas.crosstab(x, y)).astype(numpy.float32)
    n = table.sum()
    colsum = table.sum(axis=0)
    rowsum = table.sum(axis=1)
    expect = numpy.outer(rowsum, colsum) / n
    chisq = numpy.sum((table - expect) ** 2 / expect)
    return numpy.sqrt(chisq / (n * (numpy.min(table.shape) - 1)))

######################################
# 相関比
# Correlation ratio
# 0.5  >= : 非常に強い相関（Very strong correlation） 
# 0.25 >= : 強い相関（strong correlation） 
# 0.1  >= : やや弱い相関（Slightly weak correlation） 
# 0.1　< :  相関なし（No correlation） 
######################################
def CorrelationV(x, y):
    """
    Calc Correlation ratio 

    Parameters
    ----------
    x : nominal scale {numpy.ndarray, pandas.Series}
    y : ratio   scale {numpy.ndarray, pandas.Series}
    """
    variation = ((y - y.mean()) ** 2).sum()
    inter_class = sum([((y[x == i] - y[x == i].mean()) ** 2).sum() for i in numpy.unique(x)])
    correlation_ratio = inter_class / variation
    return 1 - correlation_ratio

# train.csvを読み込む
# Load train.csv
df = pandas.read_csv('/kaggle/input/titanic/train.csv')

# test.csvを読み込む
# Load test.csv
df_test = pandas.read_csv('/kaggle/input/titanic/test.csv')

# 'PassengerId'を抽出する(結果と結合するため)
# Extract 'PassengerId'(To combine with the result)
df_test_index = df_test[['PassengerId']]

df_all = pandas.concat([df, df_test], sort=False)

##############################
# データ前処理
# 必要な項目を抽出する
# Data preprocessing 
# Extract necessary items
##############################
df = df[['Survived', 'Pclass', 'Sex', 'SibSp', 'Parch', 'Ticket', 'Fare']]
df_test = df_test[['Pclass', 'Sex', 'SibSp', 'Parch', 'Ticket', 'Fare']]

##############################
# Fare と pclassの散布図をプロットする
# Draw scatter plot of Fare and pclass
##############################
plt.scatter(df['Pclass'], df['Fare'])
plt.xticks(numpy.linspace(1, 3, 3))
plt.ylim(0, 300)
plt.show()

##############################
# Fare 0 を除外する
# Exclude Fare 0
##############################
df = df[df['Fare'] != 0].reset_index(drop=True)

##############################
# Fare と pclassの散布図をプロットする
# Draw scatter plot of Fare and pclass
##############################
plt.scatter(df['Pclass'], df['Fare'])
plt.xticks(numpy.linspace(1, 3, 3))
#plt.xlim(1, 3)
plt.ylim(0, 300)
plt.show()

##############################
# Fare 0 を除外する
# Exclude Fare 0
##############################
df = df[df['Fare'] != 5].reset_index(drop=True)

##############################
# Fare と pclassの散布図をプロットする
# Draw scatter plot of Fare and pclass
##############################
plt.scatter(df['Pclass'], df['Fare'])
plt.xticks(numpy.linspace(1, 3, 3))
plt.ylim(0, 300)
plt.show()

##############################
# SurvivedとAgeのクロス集計表を表示する
# Display Survived and Age crosstabulation table
##############################
cross_age = pandas.crosstab(df_all['Survived'], round(df_all['Age'],-1))
cross_age

cross_age.T.plot(kind='bar', stacked=False, width=0.8)
plt.show()

##############################
# SurvivedとSibSpのクロス集計表を表示する
# Display Survived and SibSp crosstabulation table
##############################
cross_sibsp = pandas.crosstab(df['Survived'], df['SibSp'])
cross_sibsp

cross_sibsp.T.plot(kind='bar', stacked=False, width=0.8)
plt.show()

# SibSpが3未満の場合のクラメール連関係数を確認する
# Check Cramer's coefficient of association when SibSp is less than 3
df_SibSp = df[df['SibSp'] < 3]
cramersV(df_SibSp['Survived'], df_SibSp['SibSp'])

##############################
# SurvivedとSibSp(3未満)のクロス集計表を表示する
# Display a crosstabulation of Survived and SibSp (less than 3)
##############################
cross_sibsp = pandas.crosstab(df_SibSp['Survived'], df_SibSp['SibSp'])
cross_sibsp

cross_sibsp.T.plot(kind='bar', stacked=False, width=0.8)
plt.show()

##############################
# SurvivedとParchのクロス集計表を表示する
# Display Survived and Parch crosstabulation table
##############################
cross_parch = pandas.crosstab(df['Survived'], df['Parch'])
cross_parch

cross_parch.T.plot(kind='bar', stacked=False, width=0.8)
plt.show()

# Parchが3未満の場合のクラメール連関係数を確認する
# Check Cramer's coefficient of association when Parch is less than 3
df_Parch = df[df['Parch'] < 3]
cramersV(df_Parch['Survived'], df_Parch['Parch'])

##############################
# SurvivedとParch(3未満)のクロス集計表を表示する
# Display a crosstabulation of Survived and Parch (less than 3)
##############################
cross_parch = pandas.crosstab(df_Parch['Survived'], df_Parch['Parch'])
cross_parch
cross_parch = pandas.crosstab(df_Parch['Survived'], df_Parch['Parch'])
cross_parch

cross_parch.T.plot(kind='bar', stacked=False, width=0.8)
plt.show()

from sklearn.preprocessing import LabelEncoder
##############################
# データ前処理
# ラベル（名称）を数値化する
# Data preprocessing
# Digitize labels
##############################
##############################
# Sex
##############################
encoder_sex = LabelEncoder()
df['Sex'] = encoder_sex.fit_transform(df['Sex'].values)
df_test['Sex'] = encoder_sex.transform(df_test['Sex'].values)

##############################
# データ前処理
# One-Hot エンコーディング
# Data preprocessing
# One-Hot Encoding
##############################
##############################
# SibSp
##############################
SibSp_values = df_all['SibSp'].value_counts()
SibSp_values = pandas.Series(SibSp_values.index, name='SibSp')
categories = set(SibSp_values.tolist())
df['SibSp'] = pandas.Categorical(df['SibSp'], categories=categories)
df_test['SibSp'] = pandas.Categorical(df_test['SibSp'], categories=categories)

df = pandas.get_dummies(df, columns=['SibSp'])
df_test = pandas.get_dummies(df_test, columns=['SibSp'])

##############################
# Parch
##############################
Parch_values = df_all['Parch'].value_counts()
Parch_values = pandas.Series(Parch_values.index, name='Parch')
categories = set(Parch_values.tolist())
df['Parch'] = pandas.Categorical(df['Parch'], categories=categories)
df_test['Parch'] = pandas.Categorical(df_test['Parch'], categories=categories)

df = pandas.get_dummies(df, columns=['Parch'])
df_test = pandas.get_dummies(df_test, columns=['Parch'])

##############################
# Ticket
##############################
ticket_values = df_all['Ticket'].value_counts()
ticket_values = ticket_values[ticket_values > 1]
ticket_values = pandas.Series(ticket_values.index, name='Ticket')
categories = set(ticket_values.tolist())
df['Ticket'] = pandas.Categorical(df['Ticket'], categories=categories)
df_test['Ticket'] = pandas.Categorical(df_test['Ticket'], categories=categories)

df = pandas.get_dummies(df, columns=['Ticket'])
df_test = pandas.get_dummies(df_test, columns=['Ticket'])

##############################
# データ前処理
# 数値を標準化する
# Data preprocessing
# Standardize numbers
##############################
from sklearn.preprocessing import StandardScaler

# 標準化
# Standardize numbers
standard = StandardScaler()
df_std = pandas.DataFrame(standard.fit_transform(df[['Pclass', 'Fare']]), columns=['Pclass', 'Fare'])
df['Pclass'] = df_std['Pclass']
df['Fare'] = df_std['Fare']

df_test_std = pandas.DataFrame(standard.transform(df_test[['Pclass', 'Fare']]), columns=['Pclass', 'Fare'])
df_test['Pclass'] = df_test_std['Pclass']
df_test['Fare'] = df_test_std['Fare']

##############################
# データ前処理
# 欠損値を処理する
# Data preprocessing
# Fill or remove missing values
##############################
df_test = df_test.fillna({'Fare':0})

# トレーニングデータを準備する
# Prepare training data
x_train = df.drop(columns='Survived').values
y_train = df[['Survived']].values
# y_train の次元を削除
# Delete y_train dimension
y_train = numpy.ravel(y_train)

##############################
# モデルを構築する
# Build the model
# GradientBoostingClassifier
##############################
from sklearn.ensemble import GradientBoostingClassifier
model = GradientBoostingClassifier(random_state=1,loss='exponential', learning_rate=0.1, max_depth=6)

import os
if(os.path.exists('./result.csv')):
    os.remove('./result.csv')

##############################
# 学習
# Trainig
##############################
model.fit(x_train, y_train)

##############################
# 結果を予想する
# Predict results
##############################
x_test = df_test.values
y_test = model.predict(x_test)

# PassengerId のDataFrameと結果を結合する
# Combine the data frame of PassengerId and the result
df_output = pandas.concat([df_test_index, pandas.DataFrame(y_test, columns=['Survived'])], axis=1)

# result.csvをカレントディレクトリに書き込む
# Write result.csv to the current directory
df_output.to_csv('result.csv', index=False)

```

これを提出するとスコアが「0.80382」になりました。


#6.まとめ

スコアが0.8を超え、上位10％以内に入ることができました。
最終的に利用した入力データは以下です。

|No|項目名|項目名(日本語)|変換方式|
|:--|:--|:--|:--|
|1|Pclass|チケットクラス|標準化|
|2|Sex|性別|数値化|
|3|SibSp|兄弟/配偶者|one-hotエンコーディング|
|4|Parch|親/子供|one-hotエンコーディング|
|5|Ticket|チケット番号|one-hotエンコーディング|
|6|Fare|運賃|標準化|


今回までは、scikit-learn で学習を進めていました。
機械学習は他のフレームワークもあるので、別のフレームワークも利用してみましょう。
[次回](https://qiita.com/sudominoru/items/386d79d2deab9f76334e)はkerasを使って学習してみたいと思います。

#履歴
2020/01/29 初版公開
2020/02/03 誤字修正
2020/02/15 次回のリンク追加