3
3

More than 3 years have passed since last update.

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

Posted at

最初に投稿した記事は途中で体力が尽きて最後まで書けなかったので、リベンジです。
ちょっと分かりやすいように編集してます。

目的

タイタニック号のAgeの欠損値が多いので、全部埋めたら精度上がるんじゃないかな
と思いました。

与えられたデータ

今回インポートしたライブラリは下記のとおりです。

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こあるんですが、少ないし数値が偏ってるので最頻値で補完します。

# Embarkedの要素数をカウントする
df.Embarked.value_counts()
# 結果 
S    644
C    168
Q     77
# 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の欠損をを平均値で補完したものと比べようかと思います。
そのあとランダムフォレストにかけて、それを基準にします。

# 本当は後でごっちゃにならんように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(カッコ内同じ)でやっても全く同じ結果が出ました。
ここで注意点!!「,」と敬称の間に半角スペースが入ります
最初入れずにやって文字を認識しないのであれれ??と思ってそこでめっちゃ時間食いました。

# 半角スペース忘れずに!
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()で統計データを見てみます。

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に使う敬称を入れて、使わないものもあとで必要になるので別でとっておきます。
要素の前に「~」を入れたらそれ以外ってなるっぽいです。

# 使うデータ
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が入ってるデータを学習用、欠損データをテスト用にします。

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

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

# 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を補完したデータを元のデータと合体させます。

# ラベルとデータを合体
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も欠損なしです。
さて、ではランダムフォレストで正解率を見てみよう!!

# 【基準】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

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

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

えみちゃん、微妙に上がったよ~!(漫才王)

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3