codestates / codestates/ds-blog
[이재우] Kaggle 타이타닉 데이터 생존여부 예측 Challenge_첫번째 이야기
- Dominant language
- No language data
- Stars
- 2
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Description
# 타이타닉

아마 모두가 타이타닉호 침몰 사건에 대해 알고 있을 것이다. 혹은, 이 이야기를 바탕으로한 타이타닉 영화가 더 친숙할 수 있겠다. 타이타닉 영화를 처음봤을 때 그 순간을 잊지못하며, 영화 주인공인 잭과 로즈가 주던 감정들은 로봇이라 불리는 나에게도 확 다가왔었다. 잠시 사랑얘기는 접어두고, 타이타닉호는 그 당시 가장 안전한 배로 유명했는데, 침몰하여 더 큰 이슈가 되었는지도 모르겠다. 1912년 4월 15일, 타이타닉호는 잭과 로즈를 태우고 운항중 빙산에 충돌하여 침몰하게 되었다. 영화에서 나온 상황처럼 구명보트는 모든 사람들을 구조하기에 충분하지 않았고, 그 결과로 2224명중 1502명이 사망했다. 어쩌면 생존자는 천운을 받은 사람이라 생각될 수 있을 정도로 많은 사망자가 발생한 사건이었다. 정말 천운에 운명이 정해진걸까? 정말 잭은 죽을 수 밖에 없던 운명인걸까?
아마 데이터를 한 번이라도 공부 해본 사람은 타이타닉 데이터를 모를 수 없을 것이다. 타이타닉 데이터는 당시 타이타닉호 승객들의 정보를 담고 있다. 여담으로, 잭과 로즈의 정보는 없었다. 이 데이터를 통해서, 천운이아닌 생존여부에 영향을 줄 수 있는 요인들을 파악해 생존여부를 예측해보려고 한다. 사실, 이 예측은 Kaggle에서 진행되었으며, 이미 수 많은 사람들이 도전 하였고 심지어는 예측결과가 100%인 모델을 만들기도 했다. 예측결과가 실력을 나타내는 지표라고 말 할 수 없지만, 내가 가진 실력은 얼마인지 평가해 볼 수 있지 않을까? 그럼 같이 시작해보자.
# 타이타닉 데이터
앞에서 말했듯이, 타이타닉 데이터는 당시 타이타닉호의 승객들의 정보를 담고 있다. 다음 표를 통해 데이터를 직접 보자.
```py
#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를 통해 예측결과를 확인하게 된다. 그러면 나머지 특성들에 대해 좀 더 알아보자.
## 데이터 설명
* PassengerId : 승객 고유번호
* Survived : 생존여부
* 0 : 아니요
* 1 : 네
* Pclass : 티켓 등급
* 1 : 1등급
* 2 : 2등급
* 3 : 3등급
* Name : 이름
* Sex : 성별
* Age : 나이
* SibSp : 타이타닉호 밖의 # of Sibling + Spouse
* Sibling = brother, sister, stepbrother, stepsister
* Spouse = husband, wife (mistresses and fiancés were ignored)
* Parch : 타이타닉호 밖의 # of parent + child
* Parent = mother, father
* Child = daughter, son, stepdaughter, stepson
* Some children travelled only with a nanny, therefore parch=0 for them.
* Ticket : 티켓 고유번호
* Fare : 운임
* Cabin : 객실 번호
* embarked : 승선항
* C = Cherbourg
* Q = Queenstown
* S = Southampton
## 데이터
간단한 데이터지만, 여러가지 활용해 볼 수 있는 다양한 특성들이 있다. 이제 생존여부에 관련이 있는 특징들이 있나 찾아봐야하는데 그전에 지금 우리가 가진 데이터가 잘 구성되어 있는지, 결측치는 없는지 세세히 확인 할 필요가 있다. 그렇지 않으면, 예측 모델 자체가 의미가 없어진다.
```py
train.info()
```
```
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
```
```py
test.info()
```
```
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등급의 차이가 얼마나 차이나는지 애매하기 때문에 범주형 데이터로 바꿔주자.
```py
# Pclass는 범주형 데이터로 형변화
train['Pclass'] = train.Pclass.replace({1:'1st', 2:'2nd', 3:'3rd'}).astype('object')
```
2. `Embarked`는 승선항이 어디인지 알려주는 특성인데, 2개의 결측치가 존재한다. 결측치를 최빈값인 S로 설정하고, 편의를 위해 약자를 Full Name으로 바꿔주자.

```py
# 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'})
```
3. `SibSp`, `Parch`는 위의 설명을 보면 타이타닉호에 같이 탑승하지 않은 가족들의 수를 의미하기 때문에, 두 feature를 합쳐서 하나의 가족 수를 나타내는 `family`라는 특징을 새로 정의하자.
```py
# SibSP 와 Parch를 합하여 총 가족수를 계산
train['family'] = train.SibSp + train.Parch
```
4. `Age`의 경우 전체 891명의 데이터 중 714명의 데이터를 담고 있다. 즉, 결측치가 존재한다는 뜻이다. 결측치를 어떻게 다루면 좋을지 생각해보자.

탑승객들의 나이분포를 보면 대부분의 사람들이 20 ~ 40대에 분포하고 있음을 알 수 있다. 가장 일반적인 평균으로 결측치를 채워넣어도 될 것 같다는 생각이 든다.
```py
# 나이 결측치를 평균으로
train['Age'] = train.Age.replace(np.NaN, train.Age.mean())
```
5. `Cabin`은 객실 번호를 나타내는데, 결측치의 비율이 엄청 높음을 알 수 있다. 따라서 `Cabin`은 의미가 없다 생각하여 삭제하자. 또, 승객들의 고유값인 `PassengerId`, `Name`, `Ticket`은 생존여부와 관련이 없어서 삭제한다. 마지막으로 `SibSp`와 `Parch`를 사용하여 새로운 `family`를 생성하였기 때문에 삭제하자.
```py
# Train
train.drop(columns=['PassengerId', 'Name', 'SibSp', 'Parch', 'Cabin','Ticket'], inplace = True)
```
6. 이 모든과정을, test data에도 적용하자.
```py
# 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)
```
7. Test data에서는 `Fare`의 단 하나의 결측치가 존재한다. 따라서, `Age`와 같이 평균으로 대치하자.
```py
# 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` 메서드를 활용하였다.
```py
# 범주형 열
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
2. 다음의 과정으로는 몇개의 Feature를 사용할 것인가 이다. 여기서 사용한 방법은 `SelectKBest`를 통하여, 필요한 feature의 갯수를 확인하자.
`SelectKBest`를 사용하기 위해서는 Hyper Parameter인 k를 우리가 직접 정해줘야 하는데 이를 최적화 하는 방법을 다룬 [StackOverFlow](https://stackoverflow.com/questions/48675487/optimizing-number-of-optimum-features)를 참고하여 다음 최적화 과정을 실행하였다.
```py
# 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.best_params_)
```
>{'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]])
4. Scaling을 통하여 위와 같은 Model에 학습시키기전 최종데이터이다. 지금까지 범주형 데이터를 숫자형 데이터로, 몇개의 특성을 선택 할지, 데이터 값들의 단위까지 통일 시켜주어서 좀 더 학습에 도움이 되도록 하였다. 앞 서 선택한 **Logistic Regression** 모델을 설정해보고, 학습을 통해 예측까지 진행해보자.
```py
# 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들의 상관관계가 높다면 지금까지의 결과가 무의미 해질 수 있다. 그렇기 때문에, 꼭 필요한 과정이라 생각이 된다.
5. 다중공산성을 잘 나타내어주는 **Variance Inflation Factor**(VIF)를 통해서 확인해보자. 만약 **VIF**의 값이 10 보다 높은 feature들이 존재한다면, 제외를 하든 변형을 해주든 예측을 하기전에 feature 선택을 다시 해야 할 필요가 있다.
```py
# 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 이하이기 때문에, 다중공산성 문제가 없음을 확인하고 나의 모델링이 잘 되었음을 확인했다. 마지막으로, 캐글에 제출을 위한 데이터를 만들자.
```py
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
# 제출 결과

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