codestates / ds-blog

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

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

Open jingwoo4710 opened 3 years ago

jingwoo4710 commented 3 years ago

타이타닉

아마 모두가 타이타닉호 침몰 사건에 대해 알고 있을 것이다. 혹은, 이 이야기를 바탕으로한 타이타닉 영화가 더 친숙할 수 있겠다. 타이타닉 영화를 처음봤을 때 그 순간을 잊지못하며, 영화 주인공인 잭과 로즈가 주던 감정들은 로봇이라 불리는 나에게도 확 다가왔었다. 잠시 사랑얘기는 접어두고, 타이타닉호는 그 당시 가장 안전한 배로 유명했는데, 침몰하여 더 큰 이슈가 되었는지도 모르겠다. 1912년 4월 15일, 타이타닉호는 잭과 로즈를 태우고 운항중 빙산에 충돌하여 침몰하게 되었다. 영화에서 나온 상황처럼 구명보트는 모든 사람들을 구조하기에 충분하지 않았고, 그 결과로 2224명중 1502명이 사망했다. 어쩌면 생존자는 천운을 받은 사람이라 생각될 수 있을 정도로 많은 사망자가 발생한 사건이었다. 정말 천운에 운명이 정해진걸까? 정말 잭은 죽을 수 밖에 없던 운명인걸까?

아마 데이터를 한 번이라도 공부 해본 사람은 타이타닉 데이터를 모를 수 없을 것이다. 타이타닉 데이터는 당시 타이타닉호 승객들의 정보를 담고 있다. 여담으로, 잭과 로즈의 정보는 없었다. 이 데이터를 통해서, 천운이아닌 생존여부에 영향을 줄 수 있는 요인들을 파악해 생존여부를 예측해보려고 한다. 사실, 이 예측은 Kaggle에서 진행되었으며, 이미 수 많은 사람들이 도전 하였고 심지어는 예측결과가 100%인 모델을 만들기도 했다. 예측결과가 실력을 나타내는 지표라고 말 할 수 없지만, 내가 가진 실력은 얼마인지 평가해 볼 수 있지 않을까? 그럼 같이 시작해보자.

타이타닉 데이터

앞에서 말했듯이, 타이타닉 데이터는 당시 타이타닉호의 승객들의 정보를 담고 있다. 다음 표를 통해 데이터를 직접 보자.

#Data Import
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
submission = test[['PassengerId']]
  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

먼저 train data, test data로 구분되며, 총 1309명의 정보가, 12개의 특성으로 나뉘어 담겨 있으다. 그 중에 Survived를 통해서 생존여부를 확인을 할 수 있고, 나의 예측 Target이 된다. train data를 통해서 예측 모델을 훈련하여, test data를 통해 예측결과를 확인하게 된다. 그러면 나머지 특성들에 대해 좀 더 알아보자.

데이터 설명

데이터

간단한 데이터지만, 여러가지 활용해 볼 수 있는 다양한 특성들이 있다. 이제 생존여부에 관련이 있는 특징들이 있나 찾아봐야하는데 그전에 지금 우리가 가진 데이터가 잘 구성되어 있는지, 결측치는 없는지 세세히 확인 할 필요가 있다. 그렇지 않으면, 예측 모델 자체가 의미가 없어진다.

train.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
test.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  418 non-null    int64  
 1   Pclass       418 non-null    int64  
 2   Name         418 non-null    object 
 3   Sex          418 non-null    object 
 4   Age          332 non-null    float64
 5   SibSp        418 non-null    int64  
 6   Parch        418 non-null    int64  
 7   Ticket       418 non-null    object 
 8   Fare         417 non-null    float64
 9   Cabin        91 non-null     object 
 10  Embarked     418 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB
  1. Pclass는 티켓의 등급을 의미하는데 위의 표를 보면 숫자형 데이터이다. 물론 티켓의 등급간의 차이가 의미가 있을 수 있지만, 1등급의 차이가 얼마나 차이나는지 애매하기 때문에 범주형 데이터로 바꿔주자.
    # Pclass는 범주형 데이터로 형변화
    train['Pclass'] = train.Pclass.replace({1:'1st', 2:'2nd', 3:'3rd'}).astype('object')
  2. Embarked는 승선항이 어디인지 알려주는 특성인데, 2개의 결측치가 존재한다. 결측치를 최빈값인 S로 설정하고, 편의를 위해 약자를 Full Name으로 바꿔주자.

# Embarked 결측치를 최빈값으로
mode = train.Embarked.mode()[0]

train['Embarked'] = train.Embarked.replace(np.NaN, mode)

# 승선항을 Full name으로 변경
train['Embarked'] = train.Embarked.replace({'C' : 'Cherbourg', 
                                          'Q' : 'Queenstown',
                                          'S' : 'Southampton'})
  1. SibSp, Parch는 위의 설명을 보면 타이타닉호에 같이 탑승하지 않은 가족들의 수를 의미하기 때문에, 두 feature를 합쳐서 하나의 가족 수를 나타내는 family라는 특징을 새로 정의하자.
# SibSP 와 Parch를 합하여 총 가족수를 계산
train['family'] = train.SibSp + train.Parch
  1. Age의 경우 전체 891명의 데이터 중 714명의 데이터를 담고 있다. 즉, 결측치가 존재한다는 뜻이다. 결측치를 어떻게 다루면 좋을지 생각해보자.

    탑승객들의 나이분포를 보면 대부분의 사람들이 20 ~ 40대에 분포하고 있음을 알 수 있다. 가장 일반적인 평균으로 결측치를 채워넣어도 될 것 같다는 생각이 든다.

    # 나이 결측치를 평균으로
    train['Age'] = train.Age.replace(np.NaN, train.Age.mean())
  2. Cabin은 객실 번호를 나타내는데, 결측치의 비율이 엄청 높음을 알 수 있다. 따라서 Cabin은 의미가 없다 생각하여 삭제하자. 또, 승객들의 고유값인 PassengerId, Name, Ticket은 생존여부와 관련이 없어서 삭제한다. 마지막으로 SibSpParch를 사용하여 새로운 family를 생성하였기 때문에 삭제하자.
# Train
train.drop(columns=['PassengerId', 'Name', 'SibSp', 'Parch', 'Cabin','Ticket'], inplace = True)
  1. 이 모든과정을, test data에도 적용하자.
# Test data에도 적용
# Pclass는 범주형 데이터로 형변화
test['Pclass'] = test.Pclass.replace({1:'1st', 2:'2nd', 3:'3rd'}).astype('object')

# SibSP 와 Parch를 합하여 총 가족수를 계산
test['family'] = test.SibSp + test.Parch

# 승선항을 Full name으로 변경
test['Embarked'] = test.Embarked.replace({'C' : 'Cherbourg', 
                                          'Q' : 'Queenstown',
                                          'S' : 'Southampton'})

# 나이 결측치를 평균으로
test['Age'] = test.Age.replace(np.NaN, test.Age.mean())

# Train
test.drop(columns=['PassengerId', 'Name', 'SibSp', 'Parch', 'Cabin','Ticket'], inplace = True)
  1. Test data에서는 Fare의 단 하나의 결측치가 존재한다. 따라서, Age와 같이 평균으로 대치하자.
    # Fare 결측치 평균
    test['Fare'] = test.Fare.replace(np.NaN, test.Fare.mean())

    이를 통해서, 간단한 작업들을 통해 최종 데이터를 아래와 같이 얻을 수 있었다.

  Survived Pclass Sex Age Fare Embarked family
0 3rd male 22.0 7.2500 Southampton 1
1 1st female 38.0 71.2833 Cherbourg 1
1 3rd female 26.0 7.9250 Southampton 0
1 1st female 35.0 53.1000 Southampton 1
0 3rd male 35.0 8.0500 Southampton 0

주어진 데이터를 통해서, family와 같은 특성들을 만들기도 했고, 불 필요한 특성들을 삭제해 내가 원하는 특징만을 남겼다.

예측 모델

나는 예측 모델을 Machine Learning 중의 지도 학습의 분류의 한 방법인 Logistic Regression Model을 활용하였다.

  1. 모델링을 통해 컴퓨터 학습을 위해서 데이터의 범주형 데이터를 숫자형으로 바꿔줄 필요가 있다. 그 방법으로 OneHotEncoding의 방법을 선택하였다. 판다스에서 제공하는 pd.get_dummies 메서드를 활용하였다.
# 범주형 열
catlist = list(train.select_dtypes(include=['object']).columns)

# OneHotEncoding
train_encoded = pd.get_dummies(train, prefix=catlist, drop_first=True )
test_encoded = pd.get_dummies(test, prefix=catlist, drop_first=True )

위의 최종데이터가 get_dummies를 통과하고 나면, 다음과 같이 모두 수치형인 데이터로 변환하게 된다. 각 범주형 데이터에 해당하는 value들로 열을 생성하여, 그 value에 해당하면 1 해당하지 않으면 0을 생성함을 알 수 있다.

  Survived Age Fare family Pclass_2nd Pclass_3rd Sex_male Embarked_Queenstown Embarked_Southampton
0 22.0 7.2500 1 0 1 1 0 1
1 38.0 71.2833 1 0 0 0 0 0
1 26.0 7.9250 0 0 1 0 0 1
1 35.0 53.1000 1 0 0 0 0 1
0 35.0 8.0500 0 0 1 1 0 1
  1. 다음의 과정으로는 몇개의 Feature를 사용할 것인가 이다. 여기서 사용한 방법은 SelectKBest를 통하여, 필요한 feature의 갯수를 확인하자. SelectKBest를 사용하기 위해서는 Hyper Parameter인 k를 우리가 직접 정해줘야 하는데 이를 최적화 하는 방법을 다룬 StackOverFlow를 참고하여 다음 최적화 과정을 실행하였다.
    
    # Feature Selection
    from sklearn.feature_selection import SelectKBest
    from sklearn.model_selection import GridSearchCV
    from sklearn.tree import DecisionTreeClassifier
    from sklearn.naive_bayes import GaussianNB
    from sklearn.pipeline import Pipeline

cols = train_encoded.columns.tolist() labels = 'Survived' features = [col for col in cols if col not in labels]

X_train = train_encoded[features] y_train = train_encoded[labels]

set your configuration options

param_grid = [{ 'classify': [DecisionTreeClassifier()], #first option use DT 'kbest__k': range(1, 9), #range of n in SelectKBest(n)

#classifier's specific configs
'classify__criterion': ('gini', 'entropy'), 
'classify__min_samples_split': range(2,10),
'classify__min_samples_leaf': range(1,10)

}, { 'classify': [GaussianNB()], #second option use NB 'kbest__k': range(1, 9), #range of n in SelectKBest(n) }]

pipe = Pipeline(steps=[("kbest", SelectKBest()), ("classify", DecisionTreeClassifier())]) #I put DT as default, but eventually the program will ignore this when you use GridSearchCV.

Here the might of GridSearchCV working, this may takes time especially if you have more than one classifiers to be evaluated

grid = GridSearchCV(pipe, param_grid=param_grid, cv=3, scoring='f1') grid.fit(X_train, y_train)

Find your best params if you want to use optimal setting later without running the grid search again (by commenting all these grid search lines)

print(grid.bestparams)

>{'classify': DecisionTreeClassifier(ccp_alpha=0.0, class_weight=None, criterion='entropy',
                       max_depth=None, max_features=None, max_leaf_nodes=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=7, min_samples_split=9,
                       min_weight_fraction_leaf=0.0, presort='deprecated',
                       random_state=None, splitter='best'), 'classify__criterion': 'entropy', 'classify__min_samples_leaf': 7, 'classify__min_samples_split': 9, 'kbest__k': 8}

Feature 수가 8개로 많지 않아, 의미가 있나 싶지만 혹시나 하는 마음에 적용을 해봤으나 역시 모든 feature를 사용하는것이 좋다는것을 다시 한번 확인할 수 있었다. 만약 정말 많은 feature수를 상대해야 한다면, 꼭 필요한 과정이지 않을까 생각이 된다.

3. 위의 데이터 설명을 통해 예측할 수 있겠지만 각 특성들은 서로 다른 단위를 가지고 있다. 예를들어, `family`는 명을 단위로 할 것이고, `Fare`는 돈의 단위 일 것이다. 이를 해결하기 위해서 `StandardSclaer`를 통해서 평균이 0, 표준편차가 1인 값들로 단위를 통일 시켜주자.

```py
# Scale
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train, y_train)
X_test_scaled = scaler.transform(X_test)

array([[-0.5924806 , -0.50244517, 0.05915988, ..., 0.73769513, -0.30756234, 0.61583843], [ 0.63878901, 0.78684529, 0.05915988, ..., -1.35557354, -0.30756234, -1.62380254], [-0.2846632 , -0.48885426, -0.56097483, ..., -1.35557354, -0.30756234, 0.61583843], ..., [ 0. , -0.17626324, 1.29942929, ..., -1.35557354, -0.30756234, 0.61583843], [-0.2846632 , -0.04438104, -0.56097483, ..., 0.73769513, -0.30756234, -1.62380254], [ 0.17706291, -0.49237783, -0.56097483, ..., 0.73769513, 3.25137334, -1.62380254]])

  1. Scaling을 통하여 위와 같은 Model에 학습시키기전 최종데이터이다. 지금까지 범주형 데이터를 숫자형 데이터로, 몇개의 특성을 선택 할지, 데이터 값들의 단위까지 통일 시켜주어서 좀 더 학습에 도움이 되도록 하였다. 앞 서 선택한 Logistic Regression 모델을 설정해보고, 학습을 통해 예측까지 진행해보자.
# Logistic Model
from sklearn.linear_model import LogisticRegressionCV

logistic = LogisticRegressionCV(cv = 5, random_state=1)

logistic.fit(X_train_scaled, y_train)

y_pred = logistic.predict(X_test_scaled)

array([0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0])

Logistic Regression 모델을 정의한 후, train 데이터를 통해서 학습 시키고 test 데이터의 예측값 까지 확인 해보았다. 마지막으로 결과를 확인하기 전에, 다중공산성을 확인해보자. 이는 Feature들 간의 상관관계를 확인해보는 단계이다. 만약, feature들의 상관관계가 높다면 지금까지의 결과가 무의미 해질 수 있다. 그렇기 때문에, 꼭 필요한 과정이라 생각이 된다.

  1. 다중공산성을 잘 나타내어주는 Variance Inflation Factor(VIF)를 통해서 확인해보자. 만약 VIF의 값이 10 보다 높은 feature들이 존재한다면, 제외를 하든 변형을 해주든 예측을 하기전에 feature 선택을 다시 해야 할 필요가 있다.
# VIF check
df_X_train_selected = pd.DataFrame(X_train_scaled, columns = features)

from statsmodels.stats.outliers_influence import variance_inflation_factor

# the independent variables set 
X = df_X_train_selected

# VIF dataframe 
vif_data = pd.DataFrame() 
vif_data["feature"] = X.columns 

# calculating VIF for each feature 
vif_data["VIF"] = [variance_inflation_factor(X.values, i) 
                          for i in range(len(X.columns))] 
           feature       VIF

0 Age 1.218971 1 Fare 1.760508 2 family 1.217117 3 Pclass_2nd 2.076647 4 Pclass_3rd 2.757946 5 Sex_male 1.109791 6 Embarked_Queenstown 1.490315 7 Embarked_Southampton 1.499550

VIF의 값이 모두 10 이하이기 때문에, 다중공산성 문제가 없음을 확인하고 나의 모델링이 잘 되었음을 확인했다. 마지막으로, 캐글에 제출을 위한 데이터를 만들자.

submission['Survived'] = y_pred

submission
  PassengerId Survived
892 0
893 0
894 0
895 0
896 1
... ...
1305 0
1306 1
1307 0
1308 0
1309 0

제출 결과

스크린샷 2020-10-16 오후 5 14 07

나는 약 77%의 점수를 확인할 수 있었다. 내가 아는 모든 방법을 총동원했는데, 생각보다 낮은 점수라 실망스럽다. 하지만, 앞으로 배울 모델들이 배운 모델들 보다 훨씬 많기에, 미래의 나는 몇점일까 궁금해지기도 한다. 전혀 알지 못하는 Kaggle이라는 세상에, 한 발자국 내딛었다는 의미를 두고 앞으로의 남은 공부를 더 열심히 하는 동기가 되었으면 한다.

seungtaemoon commented 3 years ago

타이타닉 kaggle에 도전하셨군요!

재우님은 언제나 한발 앞서 계신 것 같습니다. 저도 시간을 내서 한번 도전해봐야 겠네요.

수고하셨습니다~!

Maiven commented 3 years ago

안녕하세요, 이번에 처음으로 인사드리는 코드스테이츠 조대희입니다. 블로그에 대한 코멘트드립니다.

고생하셨습니다. 감사합니다.