02-06. 사이킷런으로 수행하는 타이타닉 생존자 예측

Park Jong Hun·2021년 2월 9일
0

위키북스의 파이썬 머신러닝 완벽 가이드 책을 토대로 공부한 내용입니다.


1. kaggle


kaggle은 세계적인 ML 기반 분석 대회를 온라인상에서 주관하고 있다. 오픈된 데이터 자료를 기반으로 전 세계 데이터 분석가가 데이터 분석 실력을 경쟁하고 협업하는 가장 뛰어난 데이터 분석 오픈 포탈이다. 이 중 타이타닉 생존자 데이터는 머신러닝에 입문하는 데이터 분석가/과학자를 위한 기초 예제로 제공된다.


2. 타이타닉 탑승자 데이터


타이나닉 탑승자 데이의 feature 정보이다.

  • Passengerid : 탑승자 데이터 일련번호
  • survived : 생존 여부, 0=사망, 1=생존
  • pclass : 티켓의 선실 등급, 1=일등석, 2=이등석, 3=삼등석
  • sex : 탑승자 성별
  • name : 탑승자 이름
  • Age : 탑승자 나이
  • sibsp : 같이 탑승한 형제자매 또는 배우자 인원 수
  • parch : 같이 탑승한 부모님 또는 어린이 인원 수
  • ticket : 티켓 번호
  • fare : 요금
  • cabin : 선실 번호
  • embarked : 중간 정착 항구 C=Cherbourg, Q=Queenstown, S=Southampton
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

titanic_df = pd.read_csv('/content/drive/MyDrive/pymldg-rev/2장/titanic_train.csv')
print('\n ### train 데이터 정보 ###  \n')
print(titanic_df.info())

[output] RangeIndex는 Dataframe index의 범위를 나타낸다. 따라서 891개의 row와 12개의 cloumn으로 구성되어 있고, Age, Cabin, Embarked는 각각 714개, 204개, 889개의 not null이므로 나머지 177개, 608개, 2개의 Null값(NaN)을 가지고 있다.

사이킷런 ML 알고리즘은 Null 값을 허용하지 않으므로 Null 값을 어떻게 처리할지 결정해야 한다. DataFrame()의 fillna() 함수를 사용해서 Null 값을 평균 또는 고정 값으로 변경할 수 있다. Age의 경우는 평균, 나머지는 'N' 값으로 변경한 후, 다시 모든 cloumn에 Null 값이 없는지 확인한다.

print('초기 데이터 세트 Null 값 갯수 ',titanic_df.isnull().sum().sum())
titanic_df['Age'].fillna(titanic_df['Age'].mean(),inplace=True)
titanic_df['Cabin'].fillna('N',inplace=True)
titanic_df['Embarked'].fillna('N',inplace=True)
print('fillna() 함수 사용 후 데이터 세트 Null 값 갯수 ',titanic_df.isnull().sum().sum())

[output] 다음으로 문자열 feature인 Sex, Cabin, Embarked를 확인한다.

print(' Sex 값 분포 :\n',titanic_df['Sex'].value_counts())
print('\n Cabin 값 분포 :\n',titanic_df['Cabin'].value_counts())
print('\n Embarked 값 분포 :\n',titanic_df['Embarked'].value_counts())

[output] Sex, Embarked 값은 별로 문제가 없지만, Cabin의 경우 N이 가장 많이 있기도 하고, 나머지 값들도 정리가 제대로 되어 있지 않다. 따라서 정리를 해주어야하는데 Carbin의 경우는 선실의 등급을 나타내는 첫번째 알파벳이 중요하게 보여 앞부분만 추출하여 재정리한다.

titanic_df['Cabin'] = titanic_df['Cabin'].str[:1]
print(titanic_df['Cabin'].value_counts())

[output]


3. 데이터 분석


첫 번째로 어떤 유형의 승객들이 생존 확률이 가장 높았는지 확인해 보면 가장 우선순위가 되는 구조 대상은 여성과 아이들, 노약자이다. 그 다음이 부자나 유명인일 것으로 추정된다. 그리고 삼등석에 탄 많은 가난한 사람들은 구조되지 못했을 수 있다. 따라서 우선 성별에 따른 생존자 수를 비교한다.

titanic_df.groupby(['Sex','Survived'])['Survived'].count()

[output] 비교해보면 탑승객이 남자가 여자보다 대략 200명 정도 더 많이 탑승했고, 여자는 314명 중 233명으로 약 74.2% 생존했지만, 남자는 577명 중 109명으로 약 18.8%만 생존하였다.

import seaborn as sns
sns.barplot(x='Sex', y = 'Survived', data=titanic_df)

[output] seaborn 패키지를 이용하면 시각화하여 비교할 수 있다.

그 다음으로 부자와 가난한 사람 간의 생존 확률을 비교해본다. 부를 측정하기 위해 객실 등급에 따른 생존 확룰을 확인해본다.

sns.barplot(x='Pclass', y='Survived', hue='Sex', data=titanic_df)

[output] 여성의 경우 일등석과 이등석의 생존 확률 차이가 크진 않지만, 삼등석의 경우엔 상대적으로 많이 떨어진 것을 알 수 있다. 남성의 경우엔 일등석의 생존 확률이 이등석, 삼등석보다 훨씬 높은 것을 볼 수 있다.

마지막으로 나이에 따른 생존 확률을 알아보는데, 나이의 경우엔 값의 종류가 많기 때문에 0~5세는 Baby, 6~12세는 Child, 13~18세는 Teenager, 19~25세는 Student, 26~35세는 Young Adult, 36~60세는 Adult, 61세 이상은 Elderly로 분류하고, -1 이하의 오류 값은 Unknown으로 분류한다.

# 입력 age에 따라 구분값을 반환하는 함수 설정. DataFrame의 apply lambda식에 사용. 
def get_category(age):
    cat = ''
    if age <= -1: cat = 'Unknown'
    elif age <= 5: cat = 'Baby'
    elif age <= 12: cat = 'Child'
    elif age <= 18: cat = 'Teenager'
    elif age <= 25: cat = 'Student'
    elif age <= 35: cat = 'Young Adult'
    elif age <= 60: cat = 'Adult'
    else : cat = 'Elderly'
    return cat

# 막대그래프의 크기 figure를 더 크게 설정 
plt.figure(figsize=(10,6))

#X축의 값을 순차적으로 표시하기 위한 설정 
group_names = ['Unknown', 'Baby', 'Child', 'Teenager', 'Student', 'Young Adult', 'Adult', 'Elderly']

# lambda 식에 위에서 생성한 get_category( ) 함수를 반환값으로 지정. 
# get_category(X)는 입력값으로 'Age' 컬럼값을 받아서 해당하는 cat 반환
titanic_df['Age_cat'] = titanic_df['Age'].apply(lambda x : get_category(x))
sns.barplot(x='Age_cat', y = 'Survived', hue='Sex', data=titanic_df, order=group_names)
titanic_df.drop('Age_cat', axis=1, inplace=True)

[output] 여자 child를 제외한 대부분 생존 확률이 높게 나오는 것을 볼 수 있다. 특히 여자 Elderly는 매우 높은 생존 확률을 보인다. 따라서 Sex, Age, PClass 등이 생존을 좌우하는 중요한 feature인 것을 어느 정도 확인할 수 있다.


4. 데이터 전처리


학습에 사용될 문자열 카테고리 feature를 숫자형 카테고리 feature로 변환해야한다. encoding은 사이킷런의 LabelEncoder 클래스를 이용하여 label encoding을 적용한다. LabelEncoder 객체는 카테고리 값의 유형 수에 따라 0~(카테고리 유형수-1) 까지의 숫자값으로 변환한다. 여러 column을 encode_features() 함수를 생성해 한 번에 변환한다.

from sklearn import preprocessing

def encode_features(dataDF):
    features = ['Cabin', 'Sex', 'Embarked']
    for feature in features:
        le = preprocessing.LabelEncoder()
        le = le.fit(dataDF[feature])
        dataDF[feature] = le.transform(dataDF[feature])
    return dataDF

titanic_df = encode_features(titanic_df)
titanic_df.head()

[output] 지금까지의 feature를 가공한 내용을 정리하여 함수로 만들어 재사용을 쉽게 할 수 있도록 한다. 데이터 전처리 전체 과정을 호출하는 함수는 transform_features()이며, 내부 함수로 Null 처리, formatting, encoding을 구성한다.

from sklearn.preprocessing import LabelEncoder

# Null 처리 함수
def fillna(df):
    df['Age'].fillna(df['Age'].mean(),inplace=True)
    df['Cabin'].fillna('N',inplace=True)
    df['Embarked'].fillna('N',inplace=True)
    df['Fare'].fillna(0,inplace=True)
    return df

# 머신러닝 알고리즘에 불필요한 속성 제거
def drop_features(df):
    df.drop(['PassengerId','Name','Ticket'],axis=1,inplace=True)
    return df

# 레이블 인코딩 수행. 
def format_features(df):
    df['Cabin'] = df['Cabin'].str[:1]
    features = ['Cabin','Sex','Embarked']
    for feature in features:
        le = LabelEncoder()
        le = le.fit(df[feature])
        df[feature] = le.transform(df[feature])
    return df

# 앞에서 설정한 Data Preprocessing 함수 호출
def transform_features(df):
    df = fillna(df)
    df = drop_features(df)
    df = format_features(df)
    return df

원본 데이터를 다시 불러 label data인 Survived 속성만 별도로 분리해 클래스 결정값 dataset으로 만들고 Survived 속성은 drop 한다. 그리고 전처리 함수를 이용해 가공한 후 train_test_split() API를 이용해 전체 데이터의 20%를 테스트 dataset로 추출한다.

# 원본 데이터를 재로딩 하고, feature데이터 셋과 Label 데이터 셋 추출. 
titanic_df = pd.read_csv('/content/drive/MyDrive/pymldg-rev/2장/titanic_train.csv')
y_titanic_df = titanic_df['Survived']
X_titanic_df= titanic_df.drop('Survived',axis=1)

X_titanic_df = transform_features(X_titanic_df)

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test=train_test_split(X_titanic_df, y_titanic_df, \
                                                  test_size=0.2, random_state=11)

5. 모델 학습


ML 알고리즘인 결정 트리, 랜덤 포레스트, 로지스틱 회귀를 이용해 타이타닉 생존자를 예측해 보겠다. 사이킷런은 결정 트리는 DecisionTreeClassifier, 랜덤 포레스트는 RandomForestClassifier, 로지스틱 회귀는 LogisticRegression 클래스를 제공한다. 학습/테스트 dataset을 기반으로 fit(학습), predict(예측)하고, 성능 평가는 정확도로 accuracy_score() API를 사용할 것이다.

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

# 결정트리, Random Forest, 로지스틱 회귀를 위한 사이킷런 Classifier 클래스 생성
dt_clf = DecisionTreeClassifier(random_state=11)
rf_clf = RandomForestClassifier(random_state=11)
lr_clf = LogisticRegression()

# DecisionTreeClassifier 학습/예측/평가
dt_clf.fit(X_train , y_train)
dt_pred = dt_clf.predict(X_test)
print('DecisionTreeClassifier 정확도: {0:.4f}'.format(accuracy_score(y_test, dt_pred)))

# RandomForestClassifier 학습/예측/평가
rf_clf.fit(X_train , y_train)
rf_pred = rf_clf.predict(X_test)
print('RandomForestClassifier 정확도:{0:.4f}'.format(accuracy_score(y_test, rf_pred)))

# LogisticRegression 학습/예측/평가
lr_clf.fit(X_train , y_train)
lr_pred = lr_clf.predict(X_test)
print('LogisticRegression 정확도: {0:.4f}'.format(accuracy_score(y_test, lr_pred)))

[output] 3개의 알고리즘 중 LogisticRegression이 가장 높은 정확도를 보인다. 하지만 최적화 작업을 수행하지도 않았고, 데이터의 양도 충분하지 않기 때문에 어떤 알고리즘이 가장 성능이 좋다고 말할 수는 없다.

결정 트리 모델로 교차 검증을 해보겠다. 교차 검증을 위해 사이킷런의 model_selection 패키지의 KFold 클래스, cross_val_score(), GridSearchCV 클래스를 5개의 fold로 설정하여 사용해 볼 것이다.

from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score

def exec_kfold(clf, folds=5):
    # 폴드 세트를 5개인 KFold객체를 생성, 폴드 수만큼 예측결과 저장을 위한  리스트 객체 생성.
    kfold = KFold(n_splits=folds)
    scores = []

    # KFold 교차 검증 수행. 
    for iter_count , (train_index, test_index) in enumerate(kfold.split(X_titanic_df)):
        # X_titanic_df 데이터에서 교차 검증별로 학습과 검증 데이터를 가리키는 index 생성
        X_train, X_test = X_titanic_df.values[train_index], X_titanic_df.values[test_index]
        y_train, y_test = y_titanic_df.values[train_index], y_titanic_df.values[test_index]

        # Classifier 학습, 예측, 정확도 계산 
        clf.fit(X_train, y_train) 
        predictions = clf.predict(X_test)
        accuracy = accuracy_score(y_test, predictions)
        scores.append(accuracy)
        print("교차 검증 {0} 정확도: {1:.4f}".format(iter_count, accuracy))     

    # 5개 fold에서의 평균 정확도 계산. 
    mean_score = np.mean(scores)
    print("평균 정확도: {0:.4f}".format(mean_score)) 
# exec_kfold 호출
print('\n ### KFold 클래스 ###  \n')
exec_kfold(dt_clf , folds=5) 

print('\n ### StratifiedKFold 클래스 ###  \n')
scores = cross_val_score(dt_clf, X_titanic_df , y_titanic_df , cv=5)
for iter_count,accuracy in enumerate(scores):
    print("교차 검증 {0} 정확도: {1:.4f}".format(iter_count, accuracy))
print("평균 정확도: {0:.4f}".format(np.mean(scores)))

[output] 마지막으로 GridSearchCV를 이용해 DecesionTreeClassifier의 최적의 hyper parameter를 찾고 예측 성능을 측정해 볼 것이다. hyper parameter는 max_depth, min_samples_split, min_samples_leaf를 변경하면서 성능을 측정한다.

from sklearn.model_selection import GridSearchCV

parameters = {'max_depth':[2,3,5,10],
             'min_samples_split':[2,3,5], 'min_samples_leaf':[1,5,8]}

grid_dclf = GridSearchCV(dt_clf , param_grid=parameters , scoring='accuracy' , cv=5)
grid_dclf.fit(X_train , y_train)

print('GridSearchCV 최적 하이퍼 파라미터 :',grid_dclf.best_params_)
print('GridSearchCV 최고 정확도: {0:.4f}'.format(grid_dclf.best_score_))
best_dclf = grid_dclf.best_estimator_

# GridSearchCV의 최적 하이퍼 파라미터로 학습된 Estimator로 예측 및 평가 수행. 
dpredictions = best_dclf.predict(X_test)
accuracy = accuracy_score(y_test , dpredictions)
print('테스트 세트에서의 DecisionTreeClassifier 정확도 : {0:.4f}'.format(accuracy))

[output] hyper parameter를 튜닝하여 튜닝 전보다 예측 정확도가 약 8% 이상 증가하였다. 하지만 일반적으로 hyper parameter를 튜닝하여도 이 정도로 성능이 오르진 않고, 지금은 테스트용 dataset이 작아서 수치상으로 예측 성능이 많이 증가한 것처럼 보이는 것이다.

profile
NLP, AI, LLM, MLops

0개의 댓글