codestates / ds-blog

blog sharing for data science bootcamp course
2 stars 4 forks source link

[이재우] Kaggle 타이타닉 데이터 생존여부 예측 Challenge_두번째 이야기 #192

Open jingwoo4710 opened 3 years ago

jingwoo4710 commented 3 years ago

첫번째 도전

Kaggle 타이타닉 데이터 생존 여부 예측 Challenge_첫번째 이야기는 캐글의 한 challenge인 타이타닉에 탑승했던 사람들의 생존 여부를 예측해보는 challenge이다. 이 전에는, Logistic Regression Model을 활용하여, 예측모델을 설정하고 생존 여부 예측 결과를 확인해보았다. 0.77점으로 생각보다 낮은 점수에 실망하였었다. 하지만 오늘은 다를 것이다! 먼저, 점수를 개선할 방법으로는 다음과 같다.

  1. 데이터의 이해 부족 : 좋은 예측을 기대하기 위해선, 먼저 좋은 데이터를 학습시켜주어야 한다. 좀 더 데이터 feature들을 공부해보고, 놓칠 수 있었던 정보는 무엇이 있나? 더 심도 있게 파헤쳐 보자.
  2. 예측 모델 : 당시에는 Logistic Regression Model밖에 사용할 수가 없었다. 다른 모델을 알지 못했기 때문이다. 하지만 이번 글에서의 다른 예측모델인 Random Forest 분류 모델을 활용해볼 것이다.

데이터 설명

타이타닉 데이터의 각 feature들의 설명이다. 지난번과는 달리, 각 feature들을 시각화, 상관관계등을 포함해 자세히 알아보자.

데이터

#Data Import
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

submission = test[['PassengerId']]

train.head()
  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
2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

header()를 통해서 5번째 열까지 데이터를 같이 보자. 우선, Train data set은 891개의 sample과 앞의 데이터 설명과 같이 12개의 특성을 가진다. 마찬가지로, Test data set은 418개의 sample과 target을 제외한 11개의 특성을 가진다. 여기서 우리의 target은 Survived이다. Pclass, Sex, Embarked는 범주형 데이터이고, Age, SibSp, Parch, Fare는 숫자 형 데이터이다.

데이터 전처리

1. Target

우리의 target, label은 Survived이며, 그 비율을 확인해보자.

fig , ax = plt.subplots(figsize=(6,4))
sns.countplot(x='Survived', data=train)
plt.title("Count of Survival")
plt.show()

다운로드

위의 분포를 보면, train 데이터에서 약 62% 정도 사망했으며, 38% 생존하였음을 알 수 있다. 따라서 타이타닉 데이터셋을 imbalance 하다고 말할 수 있다.

2. 숫자형 데이터

숫자 형 데이터로는 Age, SibSp, Parch, Fare가 존재한다. 숫자 형 데이터는 Target 값과의 선형 상관 관계를 corr()을 통해서 한눈에 알 수 있다.

corr_df=train[['Age','SibSp','Parch','Fare','Survived']]  
cor= corr_df.corr(method='pearson')
print(cor)
               Age     SibSp     Parch      Fare  Survived
Age       1.000000 -0.308247 -0.189119  0.096067 -0.077221
SibSp    -0.308247  1.000000  0.414838  0.159651 -0.035322
Parch    -0.189119  0.414838  1.000000  0.216225  0.081629
Fare      0.096067  0.159651  0.216225  1.000000  0.257307
Survived -0.077221 -0.035322  0.081629  0.257307  1.000000

목푯값인 Survived와 강력한 선형관계를 보여주는 숫자 형 feature는 없어 보이며, 그나마 Fare정도는 목푯값과 선형관계가 있음을 알 수 있다. 좀 더 직관적으로 확인하기 위해, 그래프를 통해 확인 해보자.

fig , ax = plt.subplots( figsize = (6,6 ))
cmap = sns.diverging_palette( 220 , 10 , as_cmap = True )
sns.heatmap(
    cor, cmap = cmap, square = True, cbar = False, cbar_kws = { 'shrink' : 1 }, 
  annot = True, annot_kws = { 'fontsize' : 14 }
)
plt.yticks(rotation = 0)
plt.xticks(rotation = 0) 

다운로드 (1)

Feature들 사이에는 서로 큰 상관관계를 확인할 수 없다. 그중에서 SibSpParch의 선형관계는 0.414로 상대적으로 다른 feature들에 비해 높음을 알 수 있다. 위에서도 확인한 것과 같이 Survived와 가장 큰 선형관계를 보여주는 숫자 형 feature는 Fare이다.

3. 범주형 데이터

범주 형의 데이터를 가진 feature로는 Pclass, Sex, Embarked이며, target과의 관계를 숫자 형 데이터처럼 corr()을 통해 확인하기 힘드니, 시각화를 통해 확인해보자.

3.1 Sex

# Survived vs Sex
plt.figure(figsize = (10,5))
plt.subplot(1, 2, 1)
b = sns.countplot(x = 'Survived',hue = 'Sex', data = train);
b.set_xlabel("Survived",fontsize = 15)
b.set_ylabel("Count",fontsize = 15)
b.legend(fontsize = 14)

# Survival Probability
plt.subplot(1, 2, 2)
g = sns.barplot(x = "Sex", y = "Survived", data = train)
g = g.set_ylabel("Survival Probability")
plt.xticks(rotation=0)

다운로드 (2)

위의 왼쪽 그래프는 남자와 여자의 사망자 수와 생존자 수를 확인할 수 있다. 한눈에 봐도 알 수 있듯이, 남자와 비교하면 여자가 훨씬 생존을 많이 했음을 알 수 있다. 그리고 오른쪽 그래프는 성별에 따라 생존확률을 나타내주는데, 여자의 생존확률이 훨씬 높았음을 알 수 있다.

3.2 Embarked

Embarked는 승선항을 의미하며,

Survival Probability

plt.subplot(1, 2, 2) g = sns.barplot(x = "Embarked", y = "Survived", data = train) g = g.set_ylabel("Survival Probability") plt.xticks(rotation=0)

![다운로드 (3)](https://user-images.githubusercontent.com/70493869/96974329-0c232c80-1554-11eb-99d4-674e5879a2c0.png)

생존확률이 승선 항에 따라 엄청난 차이를 보여주진 않지만, 다른 항들에 비해 Cherbourg 에서 배를 타신 탑승객들의 생존확률이 다소 높음을 확인할 수 있다.

### 3.3 Pclass
`Pclass`는 표의 등급을 의미하며 1등급이 제일 높은 등급의 표고 3등급이 제일 낮은 등급의 표다.
```py
# Survived vs Pclass
plt.figure(figsize = (10,5))
plt.subplot(1, 2, 1)
b = sns.countplot(x = 'Survived',hue = 'Pclass', data = train);
b.set_xlabel("Pclass",fontsize = 15)
b.set_ylabel("Count",fontsize = 15)
b.legend(fontsize = 14)

# Survival Probability
plt.subplot(1, 2, 2)
g = sns.barplot(x = "Pclass", y = "Survived", data = train)
g = g.set_ylabel("Survival Probability")
plt.xticks(rotation=0)

다운로드 (4)

앞에서 숫자 형 데이터에서 Fare가 생존과 양의 상관관계를 보여주었다. 아무래도 표 등급과 Fare는 관계가 있으므로 위와 같은 결과를 확인할 수 있지 않을까 생각한다. 높은 등급의 표을 가진 사람일수록 더 높은 확률로 생존할 수 있음을 확인할 수 있다.

4. 결측치 처리

첫 번째 도전에서도 알 수 있었듯이, train과 test에서 결측치 존재했음을 알 수 있었다.

print(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
print(test.isnull().sum())
PassengerId      0
Pclass           0
Name             0
Sex              0
Age             86
SibSp            0
Parch            0
Ticket           0
Fare             1
Cabin          327
Embarked         0
dtype: int64

4.1 Embarked

첫 번째 도전에서와같이 승선항의 경우, train data set에서의 최빈값인 S값으로 대치하자.

train['Embarked']=train['Embarked'].fillna("S")

4.2 Others

다른 모든 결측치들은 나중에 sklearn.SimpleInputerstrategy을 median, 또는 mean으로 최적화된 결과로 한 번에 결측치을 채우려고 한다.

5. Feature Engineering

첫 번째에서 feature expansion을 했던 부분을 가져오고, 추가로 몇몇 작업을 해보자.

5.1 Cabin

위에서 알 수 있듯이, 객실 번호에 대한 정보가 가장 많은 결측치를 보유하고 있다. 그래서, 중간값으로 결측치를 하기에는 무리가 있어, 결측치를 객실이 없던 승객들, 정보가 있는 사람은 객실이 존재했던 사람들 2가지로 분류하자.

# make binary for Cabin
train['Cabin']=train['Cabin'].apply(lambda x: 0 if x==np.NaN else 1)
test['Cabin']=test['Cabin'].apply(lambda x: 0 if x==np.NaN else 1)

5.2 SibSp, Parch

첫 번째의 도전과 같게, 두 feature의 합을 통해서 총 가족 구성원 수라는 새로운 feature를 만들자.

# make Family
train['Family']=train.apply(lambda x: x['SibSp']+x['Parch'], axis=1)
test['Family']=test.apply(lambda x: x['SibSp']+x['Parch'], axis=1)

5.3 Name

첫 번째의 도전에서는 이름을 완전히 배제하였었는데, 이름에서 뽑아낼 수 있는 정보들이 있는지 확인해보자.

       'Braund, Mr. Owen Harris',
       'Cumings, Mrs. John Bradley (Florence Briggs Thayer)',
       'Heikkinen, Miss. Laina',
       'Futrelle, Mrs. Jacques Heath (Lily May Peel)',
       'Allen, Mr. William Henry', 'Moran, Mr. James',
       'McCarthy, Mr. Timothy J', 'Palsson, Master. Gosta Leonard',
       'Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)',
       'Nasser, Mrs. Nicholas (Adele Achem)',
       'Sandstrom, Miss. Marguerite Rut', 'Bonnell, Miss. Elizabeth',

위를 보면 이름의 형식을 대략 파악할 수 있는데, 두 번째 단어가 위의 예시를 통해서는 *Mr., Mrs., Miss.등을 확인할 수 있는데 모든 이름의 양식을 보면 Cap, Dr등 직업을 알 수 있다. 따라서 Name에서 Title**부분만 사용해보자.

import re
# Name 추출 함수
def get_title(name):
    title_search = re.search(' ([A-Za-z]+)\.', name)
    if title_search:
        return title_search.group(1)
    return ""

# 적용
train['Name']=train['Name'].apply(get_title)
test['Name']=test['Name'].apply(get_title)

위를 적용한 결과는 다음과 같다.

train['Name'].unique()

array(['Mr', 'Mrs', 'Miss', 'Master', 'Don', 'Rev', 'Dr', 'Mme', 'Ms', 'Major', 'Lady', 'Sir', 'Mlle', 'Col', 'Capt', 'Countess', 'Jonkheer'], dtype=object) 따라서 새로운 feature인 Title을 만들었다.

이 작업을 마지막으로 feature engineering을 마친다. 다음 단계로, 모델을 설정해보자.

6. 모델

앞서 언급했듯이, 이번에 사용할 모델은 RandomForestClassifer이다. Tree based 모델에서 Ensemble 종류의 하나이며, weak learner로 여러 개의 decision tree로 구성되어있다.

from sklearn.pipeline import make_pipeline
from category_encoders import OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RandomizedSearchCV
# model
pipe = make_pipeline(
    OrdinalEncoder(),
    SimpleImputer(),
    RandomForestClassifier(random_state = 1, n_jobs = -1)
)

dist = {
    'simpleimputer__strategy':['mean', 'median'],
    'randomforestclassifier__n_estimators': [200, 500],
    'randomforestclassifier__max_features': ['auto', 'sqrt', 'log2'],
    'randomforestclassifier__max_depth' : [4,5,6,7,8],
    'randomforestclassifier__criterion' :['gini', 'entropy']
}

# Optimizer
clf = RandomizedSearchCV(
    pipe,
    param_distributions=dist,
    n_iter=20, 
    cv=3, 
    scoring='accuracy',  
    verbose=1,
    n_jobs=-1
)

# Optimization result
clf.fit(X_train, y_train)

RandomizedSearchCV를 통해서 RandomForestClassifier를 통해서 hyper parameter를 최적화해보았다. hyper parameter로는 SimpleImputer에서의 strategy, RandomForestClassifier에서는 n_estimators, max_features, max_depth, criterion을 최적화하였다. 그 결과는 다음과 같다.

clf.best_params_
{'randomforestclassifier__criterion': 'gini',
 'randomforestclassifier__max_depth': 6,
 'randomforestclassifier__max_features': 'log2',
 'randomforestclassifier__n_estimators': 500,
 'simpleimputer__strategy': 'mean'}

이를 통해 훈련 정확도를 확인해보자!

print('정확도 : ', clf.score(X_train, y_train))

정확도 : 0.8619528619528619 첫 번째 시도보다 좋은 징조이다. Test 데이터에 적용한 후 캐글에 제출 내용을 확인해보자.

6. 제출

스크린샷 2020-10-24 오후 2 47 00

흠 생각보다 높지 않은 점수이다. 분명 첫 번째 도전보다는 오른 점수이지만, 다른 사람들에 비해 많이 부족했다. 다른 하이퍼 파라미터들을 수정했어야 했나. 아니면, imbalanced 한 부분을 좀 더 신경 써서 다루었어야 했나 생각이 든다.

7. Imbalanced Data

RandomForestClassifier에서 imbalanced data를 다루는 방법의 하나는 class_weight를 조정하는 것이다. 먼저, 0과 1의 비율을 확인해보자. 그리고 iteration을 50회 올려 좀 더 많은 옵션을 고려하여 최적화를 진행해보자.

y_train.value_counts()

0 549 1 342 Name: Survived, dtype: int64 0의 횟수가 약 0.6배 많다. 따라서 class_weight의 옵션을 다음과 같이, {None, 'balanced', {0:1.6, 1:2}}로 주고 최적화를 해보자.

from sklearn.pipeline import make_pipeline
from category_encoders import OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RandomizedSearchCV

pipe = make_pipeline(
    OrdinalEncoder(),
    SimpleImputer(),
    RandomForestClassifier(random_state = 1, n_jobs = -1)
)

dist = {
    'simpleimputer__strategy':['mean', 'median'],
    'randomforestclassifier__n_estimators': [200, 500],
    'randomforestclassifier__max_features': ['auto', 'sqrt', 'log2'],
    'randomforestclassifier__max_depth' : [4,5,6,7,8],
    'randomforestclassifier__criterion' :['gini', 'entropy'],
    'randomforestclassifier__class_weight' :[ None, 'balanced', {0:1.6 , 1: 2}]
}

# Optimizer
clf = RandomizedSearchCV(
    pipe,
    param_distributions=dist,
    n_iter=50, 
    cv=3, 
    scoring='accuracy',  
    verbose=1,
    n_jobs=-1
)

# Optimization result
clf.fit(X_train, y_train)
{'randomforestclassifier__class_weight': None,
 'randomforestclassifier__criterion': 'gini',
 'randomforestclassifier__max_depth': 7,
 'randomforestclassifier__max_features': 'sqrt',
 'randomforestclassifier__n_estimators': 500,
 'simpleimputer__strategy': 'mean'}

데이터의 balance가 문제가 아닌 듯하다. 최적화에서 None으로 나온 것을 확인해볼 수 있다. 그래도, 총 150개의 옵션을 보도록 했더니 조금의 성능 상승이 있었다. 이 모델을 통해서 캐글에 제출을 해보자.

print('정확도 : ', clf.score(X_train, y_train))

정확도 : 0.9079685746352413

y_pred = clf.predict(X_test)
#submission
submission['Survived'] = y_pred

submission.to_csv('submission_titanic.csv', index= False)
스크린샷 2020-10-25 오후 12 14 33

역시 성능 상승에 역할이 있음을 알 수 있었다. 결론은 더욱더 세밀하게 hyper parameter를 조절하게 되면 성능을 조금씩 올릴 수 있었는데, 내가 목표했던 바는 엄청난 성능의 상승을 기대했었는데 feature engineering이 부족했거나 데이터의 이해를 올리면 성능의 엄청난 상승을 기대할 순 없는 것일까? 내가 놓치고 있는 부분은 무엇일까? 생각한다. 다른 순위귄의 높은 성능을 보여주는 모델을 만든 사람은 대체 무엇을 더 알고 있는것일까? 지금은 한 발짝 물러서지만 그 끝은 다를것이다!

Maiven commented 3 years ago

재우님, 코멘트 드리겠습니다.

한 주도 고생하셨습니다.