【備忘録】タイタニック号のデータを完璧に前処理したい　～Age篇～

[最初に投稿した記事](https://qiita.com/hamco/items/ae0de188adfa240832fb)は途中で体力が尽きて最後まで書けなかったので、リベンジです。
ちょっと分かりやすいように編集してます。
## 目的
タイタニック号のAgeの欠損値が多いので、全部埋めたら精度上がるんじゃないかな
と思いました。

## 与えられたデータ
今回インポートしたライブラリは下記のとおりです。

```python
import pandas as pd
from sklearn.model_selection import train_test_split as tts
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.ensemble import RandomForestRegressor as RFR
```
いつも長い名前は自分勝手に省略してしまうので、途中でわけわからんのが出てきたら
そういうことなんだなと思ってください。

元のデータをDataFrameに入れるとこんな感じです。

| PassengerId |Survived |Pclass |Name |Sex |Age |SibSp |Parch |Ticket |Fare |Cabin |Embarked|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1 |0 |3 |Braund, Mr. Owen Harris |male |22.0 |1 |0 |A/5 21171 |7.2500 |NaN |S|
|... |... |... |... |... |... |... |... |... |... |... |... |
|891 |0 |3 |Dooley, Mr. Patrick |male |32.0 |0 |0 |370376 |7.7500 |NaN |Q|

ちょっと書くのめんどくさいんで最初と最後だけ。
皆が知ってるタイタニック号の学習用データです。
df.info()で情報を見ます。

```
df.info()
# 結果
<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
```
全データは12×891、Age,Cabin,Embarkedで欠損があるのと、
Name,Sex,Ticket,Cabin,EmbarkedはObject（文字列）なのでこのままでは使えません。

## 【前処理1】SexとEmbarked
たぶん全員やってるであろう処理です。
SexとEmbarkedはObjectですが、要素が2つ、ないしは3つしかないので
簡単な数字に置き換えます。
また、Embarkedは欠損が2こあるんですが、少ないし数値が偏ってるので最頻値で補完します。

```python
# Embarkedの要素数をカウントする
df.Embarked.value_counts()
# 結果 
S    644
C    168
Q     77
```

```python
# Sex を数値化
df['Sex'] = df['Sex'].map({'male':0, 'female':1})
# Embarked を数値化
df['Embarked'] = df['Embarked'].map({'S':0, 'C':1, 'Q':2})
# Embarked はとりあえず一番多いS(0)で補完
df['Embarked'] = df['Embarked'].fillna(0)
```

## 【基準】Ageを平均値補完
今回はAgeの欠損をを平均値で補完したものと比べようかと思います。
そのあとランダムフォレストにかけて、それを基準にします。

```python
# 本当は後でごっちゃにならんようにdf.copy()で別のDFつくってやってます。
# Age は欠損値を埋めるため、とりあえずを平均値で補完 
df['Age'] = df['Age'].fillna(df['Age'].mean())

# ObjectのままのName,Ticket,Cabinはとりあえずなしでデータを作成
df_data = df[['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']]
df_label = df[['Survived']]

(train_data, test_data, train_label, test_label) = tts(df_data, df_label, test_size=0.3, random_state=0)
# train_test_split=ttsとしてます

clf = RFC()　# RandomForestClassifier=RFCとしてます
clf.fit(train_data, train_label)
clf.score(test_data, test_label)
```

今回データの前処理だけのビフォーアフターが見たいので
パラメータとかはdefaultのままです。

結果は・・・
>0.8134328358208955

ちょっと基準が高かったです。。
これ以上高みを目指せるのでしょうか。。。

## 【前処理2】Ageの特徴量観察
年齢で生死が分かれるかというとそうじゃないと信じたいですが
とりあえず何かのデータで大体の年齢がわかったら嬉しいなと思いました。

##### まず既知のAgeとの相関関係を見てみる
dropnaでAgeの欠損値を抜いたものを、df.corr()で相関係数を見てみる。

| | PassengerId |Survived |Pclass |Sex |Age |SibSp |Parch |Fare |Embarked|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|Age |0.033207 |-0.069809 |-0.331339 |-0.084153 |1.000000 |-0.232625 |-0.179191 |0.091566 |0.007461|

こう見るとPclass,SibSp,Parchが相関高そう。
Pclassはやっぱ年齢を重ねるにつれてグレードアップしてるのか、負の相関です。
SibSpとParchは大家族になると子供が多くなるのでそういう関係かな。
Fareがそんな相関高くないのは、家族ごとに合算されてたりするんでしょうか。

##### Nameとの関係性は？
この段階でまだ文字列データなのがName,Ticket,Cabinです。
この中でNameにはMrとかMissとかの敬称が入っています。
英語の授業で未婚の女性はMiss、既婚の女性はMrsって習ったんで
もしかしたらこれを分析すればある程度年齢と関係してくるのでは？？と思いました。

## 【前処理3-1】Nameから敬称を取り出す
Nameは、「Braund, Mr. Owen Harris」のように、「, 」「.」で挟まれていますので
この挟まれているところだけ取り出して、「Honorific（敬称）」という列名で保存します。

文字列を取り出す場合、今回apply()でやりましたが、map(カッコ内同じ)でやっても全く同じ結果が出ました。
ここで注意点！！**「,」と敬称の間に半角スペースが入ります**。
最初入れずにやって文字を認識しないのであれれ？？と思ってそこでめっちゃ時間食いました。

```python
# 半角スペース忘れずに！
df['Honorific'] = df['Name'].apply(lambda x: x.split(', ')[1].split('.')[0])
df['Honorific'].value_counts()
# 結果
Mr              517
Miss            182
Mrs             125
Master           40
Dr                7
Rev               6
Mlle              2
Col               2
Major             2
Lady              1
Ms                1
the Countess      1
Don               1
Mme               1
Jonkheer          1
Sir               1
Capt              1
Name: Honorific, dtype: int64
```

一人だけの敬称もあるようです。
更にdescribe()で統計データを見てみます。

```python
df.groupby('Honorific').describe()['Age']
```
結果はこんな感じです。

|Honorific|count 	|mean 	|std 	|min 	|25% 	|50% 	|75% 	|max|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|Capt 	|1.0 	|70.000000 	|NaN 	|70.00 	|70.000 	|70.0 	|70.00 	|70.0|
|Col 	|2.0 	|58.000000 	|2.828427 |56.00 |57.000 	|58.0 	|59.00 	|60.0|
|Don 	|1.0 	|40.000000 	|NaN 	|40.00 	|40.000 	|40.0 	|40.00 	|40.0|
|Dr 	|6.0 	|42.000000 	|12.016655 |23.00 |35.000 	|46.5 	|49.75 	|54.0|
|Jonkheer |1.0 	|38.000000 	|NaN 	|38.00 	|38.000 	|38.0 	|38.00 	|38.0|
|Lady 	|1.0 	|48.000000 	|NaN 	|48.00 	|48.000 	|48.0 	|48.00 	|48.0|
|Major 	|2.0 	|48.500000 	|4.949747 |45.00 |46.750 	|48.5 	|50.25 	|52.0|
|Master 	|36.0 	|4.574167 |3.619872 |0.42 	|1.000 	|3.5 	|8.00 	|12.0|
|Miss 	|146.0 	|21.773973 |12.990292 |0.75 	|14.125 	|21.0 	|30.00 	|63.0|
|Mlle 	|2.0 	|24.000000 |0.000000 |24.00 	|24.000 	|24.0 	|24.00 	|24.0|
|Mme 	|1.0 	|24.000000 |NaN 	|24.00 	|24.000 	|24.0 	|24.00 	|24.0|
|Mr 	|398.0 	|32.368090 |12.708793 	|11.00 	|23.000 	|30.0 	|39.00|80.0|
|Mrs 	|108.0 	|35.898148 |11.433628 	|14.00 	|27.750 	|35.0 	|44.00 	|63.0|
|Ms 	|1.0 	|28.000000 |NaN 	|28.00 	|28.000 	|28.0 	|28.00 	|28.0
|Rev 	|6.0 	|43.166667 |13.136463 	|27.00 	|31.500 	|46.5 	|53.25 	|57.0|
|Sir 	|1.0 	|49.000000 |NaN 	|49.00 	|49.000 	|49.0 	|49.00 	|49.0|
|the Countess 	|1.0 	|33.000000 |NaN 	|33.00 	|33.000 |33.0 	|33.00 	|33.0|

Masterはどうやら小さい男の子につけるみたいです。
でも12歳でMasterの子もいれば11歳でMrの子もいるんですね。。
あとMrsは既婚の女性らしいですが、最年少は14歳です。・・・本当に？？
あとstd(標準偏差)は一人だとNaNになります。そりゃそっか。

ここでデータとして使えるようにすべての敬称を数値化します。
ただ、全部数値化するのは面倒です。

上の表を見ると、1人だけの敬称は皆年齢が入ってるので今回は無視したいと思います。
df_nameに使う敬称を入れて、使わないものもあとで必要になるので別でとっておきます。
要素の前に「~」を入れたらそれ以外ってなるっぽいです。

```python
# 使うデータ
df_name = df[df['Honorific'].isin(['Mr', 'Miss', 'Mrs', 'Master', 'Dr', 'Rev', 'Major', 'Mlle', 'Col'])]
# 使わないデータ
df_unneed_name = df[~df['Honorific'].isin(['Mr', 'Miss', 'Mrs', 'Master', 'Dr', 'Rev', 'Major', 'Mlle', 'Col'])]
# Honorificを数値化
df_name['Honorific'] = df_name['Honorific'].map({'Mr':0, 'Miss':1, 'Mrs':2, 'Master':3, 'Dr':4, 'Rev':5, 'Major':6, 'Mlle':7, 'Col':8})
# 結果
0    517
1    182
2    125
3     40
4      7
5      6
8      2
7      2
6      2
Name: Honorific, dtype: int64
```

##　【前処理3-2】Ageを他の特徴量で予想
よし、やっと準備が整ったので早速予測させよう。
Ageが入ってるデータを学習用、欠損データをテスト用にします。

```python
df_Agefill = df_name.dropna(subset=['Age'])
df_Agefill_data = df_Agefill[['Pclass', 'Sex', 'SibSp', 'Parch', 'Fare', 'Embarked','Honorific']]    # 学習用データ
df_Agefill_label = df_Agefill[['Age']]    # 学習用ラベル

df_Agenull = df_name[df_name['Age'].isnull()]
df_Agenull_data = df_Agenull[['Pclass', 'Sex', 'SibSp', 'Parch', 'Fare', 'Embarked','Honorific']]  # テスト用データ
df_Agenull_label = df_Agenull[['Age']]  # テスト用ラベル
```

ちなみにdf_Agefill.corr()で学習用データの相関係数を見てみると、
AgeとHonorificの相関係数は
>-0.095726

でした。
あんなに頑張ったのに・・・、
いや負けない！
飽くまで線形だから敬称の数値化した数字と合ってないだけかも！

```python
# RandomForestRegressor を RFR としてます。
clf = RFR()
clf.fit(df_Agefill_data, df_Agefill_label)

# 答えをラベルに格納（代入）
age_answer = clf.predict(df_Agenull_data)
df_Agenull_label['Age'] = age_answer
df_Agenull_label

#結果
        Age
5   	37.987776
17  	31.422079
19  	26.808000
26  	32.879936
28  	20.253988
... 	...
859 	24.929030
863 	15.495167
868 	25.716969
878 	27.344498
888 	7.838333
```

Ageはクラス分類とは異なるので回帰分析ツールのRandomForestRegressorを使います。
浮動小数点ではありますが、なんかいい感じじゃないでしょうか？？
ただこれ答えがどこにもないんで合ってるかわかんないんですけどね。。

## 【結果】Ageを補完したものをランダムフォレストにかけてみる
Ageを補完したデータを元のデータと合体させます。

```python
# ラベルとデータを合体
df_Agenull['Age'] = df_Agenull_label
# 元々Ageが入ってたデータ、新しく入れたデータ、そして敬称で省いたデータを合体
df_Age = pd.concat([df_Agefill, df_Agenull, df_unneed_name])
# このままだとindexがバラバラなので元に戻す
df_Age = df_Age.sort_index()
df_Age.info()

# 結果
<class 'pandas.core.frame.DataFrame'>
Int64Index: 891 entries, 0 to 890
Data columns (total 13 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    int64  
 5   Age          891 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     891 non-null    float64
 12  Honorific    891 non-null    object 
dtypes: float64(3), int64(6), object(4)
memory usage: 97.5+ KB
```

新たにHonorificがついた以外は元に戻りました。
Ageも欠損なしです。
さて、ではランダムフォレストで正解率を見てみよう！！

```python
# 【基準】Ageを平均値補完 の処理と全く同じ
df_data = df_Age[['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']]
df_label = df_Age[['Survived']]

(train_data, test_data, train_label, test_label) = tts(df_data, df_label, test_size=0.3, random_state=0)

clf = RFC()
clf.fit(train_data, train_label)
clf.score(test_data, test_label)
```

結果は・・・・・・

>0.832089552238806

めっちゃ微妙！！
でもちょっと上がった・・・かな？？

この頑張りに見合うほどの上昇率じゃなかったけど
まぁまぁ、ちょっと上がっただけでも嬉しい！（笑

えみちゃん、微妙に上がったよ～！（漫才王）