AI 공부 저장소

[파이썬 머신러닝 완벽가이드] Chap02-5. 타이타닉 생존자 예측 본문

Artificial Intelligence/[파이썬 머신러닝 완벽가이드]

[파이썬 머신러닝 완벽가이드] Chap02-5. 타이타닉 생존자 예측

aiclaudev 2022. 1. 10. 19:42

 

 

 

- 본 글은 파이썬 머신러닝 완벽 가이드 (권철민 지음)을 공부하며 내용을 추가한 정리글입니다. 개인 공부를 위해 만들었으며, 만약 문제가 발생할 시 글을 비공개로 전환함을 알립니다.

 

 

 

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

#데이터 불러오기
titanic_df = pd.read_csv("C:/Users/82102/Desktop/sw/PythonMLGuide/titanic_train.csv")
titanic_df.head(3)

 

데이터를 불러오는 것은 이미 익숙해졌으리라 생각합니다. 간단히 위와 같은 DataFrame을 확인해볼 수 있네요!

 

이제 각 컬럼의 속성에 대해 파악해봅시다.

 

#컬럼 속성 확인하기
print('\n ### 학습 데이터 정보 ###\n')
titanic_df.info()

 

총 12개의 Column을 확인할 수 있습니다.

각 컬럼에 대해 Non-Null Count와 Dtype을 확인해볼 수 있네요. 

Age, Cabin, Embarked 컬럼에 대해 Non-Null Count가 891이 아닌 것이 보이시죠? 이는 해당 컬럼에 Null값, 즉 결측치가 있다는 것입니다. 사이킷런 머신러닝 알고리즘은 Null 값을 허용하지 않으므로 Null 값을 어떻게 처리할지 결정해야 합니다. 평균 값으로 대체하거나(수치형 데이터에 대해), 다른 값으로 대체하거나, 삭제해주어야 하죠.

 

여기서는 DataFrame의 fillna( )함수를 통해 결측치를 없애봅시다.

# 결측치 없애기
titanic_df['Age'].fillna(titanic_df['Age'].mean(), inplace = True) # Age의 평균값으로 결측치 대체
titanic_df['Cabin'].fillna('N', inplace = True) # 'N'으로 대체
titanic_df['Embarked'].fillna('N', inplace = True) # 'N'값으로 대체
print('데이터 세트 Null값 개수 : ', titanic_df.isnull().sum().sum())

 

결측치를 모두 없앤 것을 확인할 수 있습니다. 참고로, 데이터 프레임 전체의 결측치 개수를 확인해보기 위해titanic_df.isnull( ).sum( ).sum( )을 print해보는데요, sum( )을 한번만 사용하면 각 컬럼에서 Null이 몇개인지 나옵니다. 따라서, 이에 대해서 또 sum( )을 해주면 모든 컬럼에 대한 결측치가 나오겠죠!

 

현재 남아있는 문자열 피처는 Sex, Cabin, Embarked입니다. 먼저 이 피처들의 값 분류를 살펴보죠.

# 값 분류 확인하기
print('Sex 값 분포 : \n', titanic_df['Sex'].value_counts())
print('\n')
print('Cabin 값 분포 : \n', titanic_df['Cabin'].value_counts())
print('\n')
print('Embarked 값 분포 : \n', titanic_df['Embarked'].value_counts())

Sex와 Embarked 컬럼은 별 문제가 없어보이나, Cabin(선실)의 경우 속성값이 제대로 정리되어있지 않은 것 같습니다. 예를 들어, 'C23 C25 C27'로 이루어진 행이 4건이 되죠. Cabin의 경우 선실의 등급을 나타내는 맨 앞 알파벳이 중요해보이네요. 이처럼 선실의 등급은 경제적인 위치(부자 / 가난)에 따른 생존률 예측에 큰 도움이 될 수 있습니다. 따라서, Cabin의 경우 맨 앞 알파벳만 추출해봅시다.

 

titanic_df['Cabin'] = titanic_df['Cabin'].str[:1] # str은 글자를 추출하기위한 메소드
titanic_df['Cabin'].head(3)

 

맨 앞 알파벳만이 추출된 것을 확인할 수 있네요!

 

이제 본격적으로 머신러닝 알고리즘을 적용해 예측을 수행하기 이전, 데이터를 탐색하고 시각화해봅시다. 즉, 어떤 특성을 가진 사람이 생존률이 높고 낮은지를 따져보자는 것이죠. 가장 먼저 성별에 따른 생존자 수를 확인해봅시다!

# 성별에 따른 생존자 수 확인하기
titanic_df.groupby(['Sex', 'Survived'])['Survived'].count()

 

먼저, 'Sex'와 'Survived' 두개의 컬럼으로 groupby하였습니다. 즉, Sex로 먼저 그룹을 나눈 뒤 Survived로 다시 그룹을 세분화한 것이죠. 그리고 Survived 컬럼에 대해서만 count( )를 진행하였습니다. 이제 결과를 해석해보죠!

여자는 314명 중 233명으로 약 74.2%가 생존했지만, 남자의 경우에는 577명 중 468명이 죽고 109명만 살아남아 약 18.8%가 생존했네요. 이를 시본(Seaborn) 패키지를 통해 시각화해봅시다! 이에 앞서, 시본에 대해 알아봅시다.

시본은 맷플롯립에 기반하고 있지만 더욱 세련된 비주얼, 쉬운 API, 판다스 DataFrame과의 연동 등으로 애용되는 시각화 패키지입니다. X축에 'Sex'컬럼, Y축에 'Survived'컬럼 그리고 이들 데이터를 가져올 데이터로 DataFrame 객체명을 다음과 같이 입력하고 barplot( ) 함수를 호출하면 가로 막대 차트를 쉽게 그릴 수 있습니다.

 

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

시본을 사용하여 그래프로 시각화하니 결과를 해석하기가 더욱 쉽네요. 자동으로 남/녀에 대한 생존율 그래프를 그려준 모습입니다! 

 

그렇다면, 부자와 가난한 사람간의 생존률 차이는 어떨까요? 아까 말씀드린 것 처럼 부를 측정할 수 있는 속성으로 적당한 것은 객실 등급(Pclass)입니다. 따라서, 객실 등급과 성별을 함께 고려하여 생존률을 분석해봅시다.

sns.barplot(x = 'Pclass', y = 'Survived', hue = 'Sex', data = titanic_df) # hue는 x를 더욱 세부적으로 나누기 위한 특성

hue라는 속성을 이용하여 객실 등급으로 먼저 나눈 후, 성별로 세분화한 것을 확인할 수 있네요.

위 그래프에서 여성이 남성에 비해, 그리고 높은 등급의 객실이 낮은 등급의 객실보다 생존률이 더욱 높은 것을 확인할 수 있네요.

 

이번에는 나이(Age)에 따른 생존 확률을 알아보겠습니다. 하지만 나이는 숫자형 데이터이고 수의 종류(?)가 굉장히 많기에 바로 그래프를 그리기에는 무리가 있습니다. 따라서, 범위별로 구분해 카테고리 값을 할당하기로 하죠. 

0~5세는 Baby, 6~12세는 Child, 13~18세는 Teenager, 19~25세는 Student, 26~35세는 Young Adult, 36~60세는 Adult, 61세 이상은 Elderly로 분류하고, -1 이하의 오류값은 Unknown으로 분류합시다.

# 카테고리값 할당을 위한 함수
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 Child'
    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)

위 그래프의 결과는 스스로 분석해 봅시다!

 

지금까지 Sex, Age, PClass 등이 생존을 좌우하는 중요한 피처인 것을 확인했습니다. 이제 ML을 이용하여 모델을 만들어보죠.

 

사이킷런에서 제공하는 ML은 문자열 데이터를 지원하지 않는다고 한 것 기억나시죠? 따라서, 문자열 데이터를 LabelEncoding 하여 숫자 코드값으로 바꾸어 봅시다.

from sklearn import preprocessing

# 여러 칼컴을 레이블 인코딩 하기
def encode_features(dataDF) :
    features = ['Cabin', 'Sex', 'Embarked']
    for feature in features :
        le = preprocessing.LabelEncoder()
        le.fit(dataDF[feature])
        dataDF[feature] = le.transform(dataDF[feature])
    
    return dataDF

titanic_df = encode_features(titanic_df)
titanic_df.head()

Sex, Cabin, Embarked 컬럼이 숫자형으로 바뀐 것을 확인할 수 있네요.

 

지금까지 피처를 가공한 내역을 정리하고 이를 함수로 쉽게 재사용할 수 있도록 만들어보죠!

데이터의 전처리를 전체적으로 호출하는 함수는 transform_features( )이며, 결측치 처리, 포매팅, 인코딩 등을 종합적으로 수행하도록 해보죠.

 

# 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 = preprocessing.LabelEncoder()
        le.fit(df[feature])
        df[feature] = le.transform(df[feature])
    
    return df

# 앞 3가지 함수를 모두 통함
def transform_features(df) :
    df = fillna(df)
    df = drop_features(df)
    df = format_features(df)
    return df

 

데이터를 전처리하는 transform_features( )함수를 만들었으니 다시 데이터를 로딩한 후 데이터 전처리 함수를 적용하고, ML을 시작해봅시다.

 

# 데이터를 재로딩하고, 피처 데이터 세트와 레이블 데이터 세트 추출
titanic_df = pd.read_csv("C:/Users/82102/Desktop/sw/PythonMLGuide/titanic_train.csv")
y_titanic_df = titanic_df['Survived']
X_titanic_df = titanic_df.drop('Survived', axis = 1, inplace = False)

X_titanic_df = transform_features(X_titanic_df)

 

내려받은 데이터 세트를 기반으로해서 train_test_split( )을 이용해 학습 데이터와 테스트 데이터 세트를 추출하겠습니다. 테스트 데이터 세트 크기는 전체의 20%입니다.

 

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)

 

ML 알고리즘인 결정트리, 랜덤 포레스트, 로지스틱 회귀를 이용해서 타이타닉 생존자를 예측해봅시다. (로지스틱 회귀는 이름은 회귀지만 매우 강력한 분류 알고리즘입니다.) 사이킷런은 결정 트리를 위해서 DecisionTreeClassifier, 랜덤 포레스트를 위해 RandomForestClassifier, 로지스틱 회귀를 위해 LogisticRegression 클래스를 제공합니다.

 

이제 train_test_split( )으로 분리한 학습 데이터와 테스트 데이터를 기반으로 모델을 학습(fit)하고 예측(predict) 해봅시다. 예측 성능 평가는 정확도로 하며 이를 위해 accuracy_score( )를 사용하죠!

 

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

# 각 모델에 대한 Classifier 클래스 생성
dt_clf = DecisionTreeClassifier(random_state = 11)
rf_clf = RandomForestClassifier(random_state = 11)
lr_clf = LogisticRegression(random_state = 11)

# DecisionTreeClassfier 학습/예측평가
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)))

3개의 알고리즘 중 LogisticRegression이 타 알고리즘에 비해 높은 정확도를 나타내고 있습니다. 아직 최적화 작업을 수행하지 않았고, 데이터 양도 충분하지 않기에 LogistiocRegression이 가장 좋은 모델이라고 할 수는 없습니다. 

 

다음으로는 교차 검증으로 결정 트리 모델을 좀 더 평가해봅시다! model_selection 패키지의 KFold, cross_val_score( ), GridSearchCV 클래스를 모두 사용해보죠!

from sklearn.model_selection import KFold

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] # values를 통해 df를 ndarray로 변환
        y_train, y_test = y_titanic_df.values[train_index], y_titanic_df.values[test_index]
        
        # Classifier 학습/예측/평가
        clf.fit(X_train, y_train)
        clf_pred = clf.predict(X_test)
        accuracy = accuracy_score(y_test, clf_pred)
        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_fold 호출
exec_kfold(dt_clf, folds = 5)

평균 정확도는 0.7823정도 되는 것을 확인할 수 있네요.

 

이번엔 cross_val_score( ) API를 이용해봅시다!

from sklearn.model_selection import cross_val_score

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

왜 KFold를 이용했을 때의 accuracy와 cross_val_score를 이용했을 때의 accuracy가 다를까요? 

정답은, 모델이 Classifier이기에 cross_val_score은 KFold가 아닌 Statified KFold를 수행하기 때문입니다. 

 

마지막으로, GridSearchCV를 이용하여 하이퍼 파라미터의 최적값을 구해봅시다.

 

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))

최적화된 하이퍼 파라미터인 max_depth = 3, min_samples_leaf = 5, min_samples_split = 2로 학습시킨 뒤 예측 정확도가 약 87.15%로 향상되었네요. 일반적으로 하이퍼 파라미터를 튜닝하더라도 이정도 수준으로 증가하기는 매우 어렵다고 합니다. 테스트용 데이터 세트가 작기 때문에 수치상으로 예측 성능이 많이 증가한 것 처럼 보인다고 책에선 설명하네요.

 

 

 

- 본 글은 파이썬 머신러닝 완벽 가이드 (권철민 지음)을 공부하며 내용을 추가한 정리글입니다. 개인 공부를 위해 만들었으며, 만약 문제가 발생할 시 글을 비공개로 전환함을 알립니다.