차밍이

[파이썬 머신러닝] Kaggle 타이타닉 데이터 생존자 예측모델 RandomForest와 DecisionTree 본문

파이썬/머신러닝

[파이썬 머신러닝] Kaggle 타이타닉 데이터 생존자 예측모델 RandomForest와 DecisionTree

2020. 3. 16. 21:50
반응형

Kaggle의 타이타닉 데이터를 바탕으로 의사결정 나무(Decision tree) 모델을 만들어서 예측을 진행해보겠습니다. 그리고 랜덤포레스트 모델을 사용해서 성능을 비교해보겠습니다. 전체적으로 세세하게 데이터를 분석하고 예측하는 것이 아닌, 간단하게 데이터 전처리를 모델을 만들어 보는 것에 초점을 맞추어 실습을 진행하겠습니다.

타이타닉 데이터 출처 : [https://www.kaggle.com/c/titanic]

 

Library

import pandas as pd
import numpy as np
import re

from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn import metrics

의사결정 트리를 위한 sklearn의 라이브러리와 기타 필요한 라이브러리를 불러왔습니다.

 

EDA : 데이터 탐색

path = r"./titanic"

train = pd.read_csv(path+"/train.csv")
test = pd.read_csv(path+"/test.csv")
train.head()
train.isnull().sum()

>>>
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64

데이터를 불러온 후 기본적인 데이터의 구조를 확인합니다. 현재 데이터가 없는 결측치는 AgeCabin. Embarked로 3가지가 있습니다.

Embarked는 2개밖에 되지 않으므로 가장 많은 최빈값으로 채워버리겠습니다.

Cabin은 있는 것과 없는 것은 없는 것이므로 결측치 그대로 두는 것이 옳은 것 같다고 생각하므로 NaN이라는 문자를 넣어서 채우겠습니다.

Age는 다른 feature를 통해서 KNN으로 나이를 유추하여 값을 넣어주도록 하겟습니다.

상관관계나 다른 데이터 탐색은 넘어가도록 하겠습니다.

데이터 전처리

이름과 호칭

먼저 Name을 통해서 Mr,Ms 등의 호칭을 뽑아내어 정리해볼 예정입니다. 이름 자체는 의미가 없지만, Mr,Ms 등은 결혼 여부나 성별 등의 정보를 담고 있으며 Dr 등의 경우는 직업이나 학력을 유추해볼 수 있을 것 같습니다. 그래서 뽑아서 살리는 방향으로 두겠습니다.

# 호칭을 뽑기 위한 정규식
pat = re.compile('[\w]+, ([\w]+)')
# 이름에서 호칭을 뽑기위한 함수 정의
def find_name_title(x):
    return pat.findall(x)[0]

#이름 내용을 호칭으로 변경해버리기
train['Name'] = train['Name'].apply(find_name_title)
test['Name'] = test['Name'].apply(find_name_title)

Cabin 유무

Cabin이 있는 경우와 없는 경우를 0과 1로 구분하여 새로운 column으로 넣었습니다. 넘파이의 np.where 함수를 사용해서 조건에 따른 경우로 나누어주었습니다.

train['Cabin_yes'] = np.where(train['Cabin'].isnull()==False,1,0)
test['Cabin_yes'] = np.where(test['Cabin'].isnull()==False,1,0)
train

Cabin 종류

어떤 종류의 Cabin인지를 알기 위해서 Cabin의 앞의 문자를 뽑아옵니다.

pat_cabin = re.compile('([\w])[0-9]*')

def Cabin_word(x):
    if x is not np.nan and x is not None:
        return pat_cabin.findall(x)[0]
    else: return 'X'

train['Cabin_C'] = train['Cabin'].apply(Cabin_word)
test['Cabin_C'] = test['Cabin'].apply(Cabin_word)

One-hot Encoding

지금까지의 결과를 바탕으로 나이 데이터를 채우기 위해서 카테고리형의 문자열 데이터를 encoding 해주도록 하겠습니다. get_dummies함수를 사용해서 Encoding을 진행하였습니다.

'Name','Sex','Embarked','Cabin_C'에 해당되는 데이터는 encoding을 진행하고 'PassengerId','Ticket','Cabin'에 해당되는 데이터는 제거하였습니다. PassengerIdTicket에서는 얻을 데이터가 없어 보여서 제거하였습니다. 티켓에서 뽑을 데이터가 있을 수 있으나 시간 관계상 무시하였습니다.

train_dummy = pd.get_dummies(data=train,columns=['Name','Sex','Embarked','Cabin_C']).drop(['PassengerId','Ticket','Cabin'], axis=1)
test_dummy = pd.get_dummies(data=test,columns=['Name','Sex','Embarked','Cabin_C']).drop(['PassengerId','Ticket','Cabin'], axis=1)

나이 결측치 채우기

KNN을 사용해서 다른 feature들을 통해 나이를 예측해서 넣도록 하겠습니다.

knn = KNeighborsRegressor()
# 나이가 있는 데이터로 fit해서 모델을 생성
knn.fit(train_dummy[train_dummy['Age'].isnull()==False][train_dummy.columns.drop('Age')],
       train_dummy[train_dummy['Age'].isnull()==False]['Age'])
# 나이가 결측인 데이터를 예측
guesses = knn.predict(train_dummy[train_dummy['Age'].isnull()==True][train_dummy.columns.drop('Age')])
guesses

예측한 나잇값에 대한 결과는 아래와 같다.

array([47.8  , 31.8  , 18.4  , 32.8  , 17.6  , 29.8  , 21.584, 17.6  ,
       24.6  , 28.7  , 31.   , 33.2  , 17.6  , 29.2  , 49.4  , 38.   ,
       11.934, 29.8  , 31.   , 17.6  , 31.   , 31.   , 29.8  , 30.2  ,
       16.4  , 31.   , 47.8  , 17.6  , 20.4  , 34.8  , 26.8  , 37.   ,
       35.8  , 56.4  , 22.4  , 37.   , 28.4  , 46.8  , 28.8  , 47.8  ,
       17.6  , 35.2  , 47.8  , 29.8  , 12.6  , 26.6  , 17.1  , 19.3  ,
       34.8  , 46.4  , 47.8  , 21.5  , 40.9  , 17.6  , 33.4  , 54.6  ,
       38.   , 48.   , 17.6  , 31.4  , 26.1  , 31.   , 31.8  , 35.2  ,
       20.8  , 37.6  , 29.8  , 36.2  , 50.4  , 32.8  , 17.6  , 17.6  ,
       33.2  , 18.4  , 17.6  , 36.4  , 29.8  , 44.4  , 12.6  , 29.8  ,
       40.1  , 33.4  , 25.4  , 28.7  , 34.8  , 47.8  , 36.2  , 25.8  ,
       27.   , 31.   , 40.6  , 47.8  , 31.   , 33.4  , 44.4  , 34.8  ,
       42.2  , 33.4  , 12.6  , 27.   , 28.4  , 30.3  , 20.3  , 52.2  ,
       31.   , 31.4  , 32.8  , 29.9  , 30.8  , 29.9  , 28.2  , 41.   ,
       35.2  , 47.8  , 30.8  , 47.8  , 31.   , 24.2  , 29.9  , 17.6  ,
       27.7  , 33.6  , 31.   , 21.5  , 31.8  , 32.8  , 29.8  , 29.2  ,
       28.6  , 19.3  , 47.8  , 44.4  , 34.4  , 30.   , 29.8  , 28.4  ,
       29.8  , 17.6  , 29.8  , 34.2  , 40.6  , 33.4  , 20.3  , 29.8  ,
       17.6  , 11.934, 50.6  , 28.9  , 17.6  , 33.4  , 29.8  , 29.8  ,
       43.4  , 41.   , 46.2  , 31.4  , 32.8  , 44.3  , 44.4  , 27.8  ,
       47.8  , 37.   , 42.7  , 34.4  , 40.1  , 33.6  , 47.2  , 29.9  ,
       31.   , 43.2  , 35.2  , 34.8  , 29.9  , 37.   , 25.8  , 29.8  ,
       13.2  ])

예측된 결과를 실제 결측 된 곳에 넣어준다.

train_dummy.loc[train_dummy['Age'].isnull()==True,'Age'] = guesses
train_dummy.isnull().sum()

더 이상 결측 값이 없는 것을 확인할 수 있다.

Survived         0
Pclass           0
Age              0
SibSp            0
Parch            0
Fare             0
Cabin_yes        0
Name_Capt        0
Name_Col         0
Name_Don         0
Name_Dr          0
Name_Jonkheer    0
Name_Lady        0
Name_Major       0
Name_Master      0
Name_Miss        0
Name_Mlle        0
Name_Mme         0
Name_Mr          0
Name_Mrs         0
Name_Ms          0
Name_Rev         0
Name_Sir         0
Name_the         0
Sex_female       0
Sex_male         0
Embarked_C       0
Embarked_Q       0
Embarked_S       0
Cabin_C_A        0
Cabin_C_B        0
Cabin_C_C        0
Cabin_C_D        0
Cabin_C_E        0
Cabin_C_F        0
Cabin_C_G        0
Cabin_C_T        0
Cabin_C_X        0
dtype: int64

나이와 요금 카테고리화

남은 데이터를 확인해보자. 이제 나이와 요금을 카테고리화 하는 것이 좋을 것으로 생각된다.

train_dummy
  Survived Pclass Age SibSp Parch Fare Cabin_yes Name_Capt Name_Col Name_Don ... Embarked_S Cabin_C_A Cabin_C_B Cabin_C_C Cabin_C_D Cabin_C_E Cabin_C_F Cabin_C_G Cabin_C_T Cabin_C_X
0 0 3 22.0 1 0 7.2500 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 1
1 1 1 38.0 1 0 71.2833 1 0 0 0 ... 0 0 0 1 0 0 0 0 0 0
2 1 3 26.0 0 0 7.9250 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 1
3 1 1 35.0 1 0 53.1000 1 0 0 0 ... 1 0 0 1 0 0 0 0 0 0
4 0 3 35.0 0 0 8.0500 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
886 0 2 27.0 0 0 13.0000 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 1
887 1 1 19.0 0 0 30.0000 1 0 0 0 ... 1 0 1 0 0 0 0 0 0 0
888 0 3 13.2 1 2 23.4500 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 1
889 1 1 26.0 0 0 30.0000 1 0 0 0 ... 0 0 0 1 0 0 0 0 0 0
890 0 3 32.0 0 0 7.7500 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 1

891 rows × 38 columns

test_dummy['Age'].plot(kind='hist', bins=10)
train_dummy['Age_cat'] = pd.cut(train_dummy['Age'],10, labels=[*range(10)])
test_dummy['Age_cat'] = pd.cut(test_dummy['Age'],10, labels=[*range(10)])

 

train_dummy['Fare'].plot(kind='hist', bins=40, xlim=(10,100))

 

나이와 요금 모두 5개의 구간으로 나누었습니다.

연속형 변수를 카테고리화 하기 위해 나누는 것에는 판 다스의 cut또는 qcut함수를 사용하면 쉽게 만들 수 있습니다. 저는 qcut을 사용해서 간단하게 만들었습니다.

# train_dummy['Fare_cat'] = pd.cut(train_dummy.Fare,precision=5,bins=[0,10,30,80,120,999999] ,labels=['lowlow','low','mid','high','highhigh'])
# test_dummy['Fare_cat'] = pd.cut(test_dummy.Fare,precision=5,bins=[0,10,30,80,120,999999] ,labels=['lowlow','low','mid','high','highhigh'])


train_dummy['Fare_cat'] = pd.qcut(train_dummy.Fare,5, labels=[*range(5)])
test_dummy['Fare_cat'] = pd.qcut(test_dummy.Fare,5, labels=[*range(5)])
train_dummy['Fare_cat'].value_counts()

qcut을 하면 어느 정도 비슷하게 데이터가 들어가는 것을 알 수 있습니다.

1    184
3    180
0    179
4    176
2    172
Name: Fare_cat, dtype: int64

One-hot Encodnig 2

카테고리 화한 연속형 변수들에 대해서 다시 encoding을 진행합니다. 이전에 빠트린 Pclass도 같이 one-hot encoding 합니다.

train_final = pd.get_dummies(train_dummy, columns=['Age_cat','Fare_cat','Pclass']).drop(['Age','Fare','Pclass'],axis=1, errors='ignore')
test_final = pd.get_dummies(test_dummy, columns=['Age_cat','Fare_cat','Pclass']).drop(['Age','Fare','Pclass'],axis=1, errors='ignore')
train_final

데이터 전처리가 끝난 결과는 아래의 데이터 프레임과 같습니다. one-hot encoding의 경우 feature를 너무 늘리는 경향이 있습니다. 하지만 여기서 사용하는 데이터가 많지 않으므로 문제 되지 않을 것으로 판단했습니다.

  Survived SibSp Parch Cabin_yes Name_Capt Name_Col Name_Don Name_Dr Name_Jonkheer Name_Lady ... Age_cat_8 Age_cat_9 Fare_cat_0 Fare_cat_1 Fare_cat_2 Fare_cat_3 Fare_cat_4 Pclass_1 Pclass_2 Pclass_3
0 0 1 0 0 0 0 0 0 0 0 ... 0 0 1 0 0 0 0 0 0 1
1 1 1 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 1 1 0 0
2 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 1 0 0 0 0 0 1
3 1 1 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 1 1 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 1 0 0 0 0 0 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
886 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 1 0 0 0 1 0
887 1 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 1 0 1 0 0
888 0 1 2 0 0 0 0 0 0 0 ... 0 0 0 0 0 1 0 0 0 1
889 1 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 1 0 1 0 0
890 0 0 0 0 0 0 0 0 0 0 ... 0 0 1 0 0 0 0 0 0 1

891 rows × 53 columns

Decision Tree : 의사결정 나무 모델

X_train, X_test, y_train, y_test = train_test_split(train_final.iloc[:,1:],train_final['Survived'],test_size=0.2,random_state=42)

DT = DecisionTreeClassifier(max_depth=100)
model = DT.fit(X_train, y_train)
pred = model.predict(X_test)
accuracy_score(pred, y_test)
>> 0.8268156424581006

예측 결과는 0.82 정도의 결과를 얻을 수 있었습니다. 생각보다 높은 정확도를 보여주는 것 같습니다.

RandomForest : 랜덤 포레스트 모델

rf_clf = RandomForestClassifier()
model = rf_clf.fit(X_train, y_train)
pred = model.predict(X_test)
accuracy_score(pred, y_test)
>> 0.8435754189944135

DT에 비해서 조금 더 좋은 결과를 보여주는 것을 확인할 수 있습니다.

 

머신러닝 더 알아보기

반응형

관련된 글 보기

Comments