7 minute read

머신러닝 - 캐글 타이타닉 문제 해결하기

캐글 타이타닉
위 링크는 캐글 데이터 분석 경진대회의 타이타닉호 탑승객의 생존여부를 예측하는 과제이다.


목차

  1. 데이터 불러오기
  2. 데이터 시각화
  3. 데이터 전처리
  4. 모델 학습과 선택
  5. 제출

데이터 불러오기

링크에서 데이터를 받아와야한다. 데이터는 이미 훈련셋과 테스트셋으로 나뉘어져 있으며 테스트 셋에는 라벨이 되는 생존여부에 대한 데이터가 없다.

데이터의 특성은 이와 같다.

Variable Definition Key
Survival 생존여부 0 = No, 1 = Yes
Pclass 탑승권 등급 1 = 1st, 2 = 2nd, 3 = 3th
Sex 성별 male, female
Age 나이  
SibSp 형제 또는 배우자 동승여부  
Parch 부모 또는 자녀 동승여부  
ticket 티켓 번호  
fare 지불 요금  
Cabin 객실 번호  
Embarked 탑승 항구 C = Cherbourg, Q = Queenstown, S = Southampton

데이터 분석을 위한 numpy, pandas 라이브러리를 불러온 후 https://github.com/ageron/data/raw/main/titanic.tgz 에서 파일을 받아 read_csv 메서드를 통해 가져온다.

  • train_df : 훈련셋
  • test_df : 테스트 셋
from pathlib import Path
import pandas as pd
import tarfile
import urllib.request

def load_titanic_data():
    tarball_path = Path("datasets/titanic.tgz")
    if not tarball_path.is_file():
        Path("datasets").mkdir(parents=True, exist_ok=True)
        url = "https://github.com/ageron/data/raw/main/titanic.tgz"
        urllib.request.urlretrieve(url, tarball_path)
        with tarfile.open(tarball_path) as titanic_tarball:
            titanic_tarball.extractall(path="datasets")
    return [pd.read_csv(Path("datasets/titanic") / filename)
            for filename in ("train.csv", "test.csv")]

train_df, test_df = load_titanic_data()
train_df.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

데이터 시각화

import matplotlib.pyplot as plt
import seaborn as sns

# 데이터 시각화 설정
plt.rc('font', size=14)
plt.rc('axes', labelsize=14, titlesize=14)
plt.rc('legend', fontsize=14)
plt.rc('xtick', labelsize=10)
plt.rc('ytick', labelsize=10)
# 생존율 시각화
f, ax = plt.subplots(1,2, figsize=(10,5))
train_df['Survived'].value_counts().plot.pie(explode=[0, 0.1], autopct='%1.1f%%', ax=ax[0], shadow=True)
ax[0].set_title('Survived')
ax[0].set_ylabel('')
sns.countplot('Survived', data=train_df, ax=ax[1])
ax[1].set_title('Survived')
plt.show

png

타이타닉호에 탑승한 승객의 생존율은 61.6% 이다.

# 특성별 생존 여부를 piechart로 시각화하기 위한 함수

def pie_chart(feature):
    feature_ratio = train_df[feature].value_counts(sort=False)
    feature_size = feature_ratio.size
    feature_index = feature_ratio.index
    survived = train_df[train_df['Survived'] == 1][feature].value_counts()
    dead = train_df[train_df['Survived'] == 0][feature].value_counts()

    plt.plot(aspect='auto')
    plt.pie(feature_ratio, labels=feature_index, autopct='%1.1f%%')
    plt.title(feature + '\'s ratio in total')
    plt.show()

    for i, idx in enumerate(feature_index):
        plt.subplot(1, feature_size + 1, i + 1, aspect='equal')
        plt.pie([survived[idx], dead[idx]], labels=['Survived', 'Dead'], autopct='%1.1f%%')
        plt.title(str(idx) + '\'s ratio')
    plt.show()
# 성별 별 생존 비율
pie_chart('Sex')

png

png

탑승객 중 남자는 64.8%, 여성은 35.2%

  • 남자의 생존율 : 18.9%
  • 여성의 생존율 : 74.2%

여성의 경우가 생존율이 높았음을 알 수 있다.

# 객실 등급 Pclass 데이터
pie_chart('Pclass')
pd.crosstab([train_df['Sex'], train_df['Survived']],
            train_df['Pclass'],
            margins=True).style.background_gradient(cmap='summer_r')

png

png

  Pclass 1 2 3 All
Sex Survived        
female 0 3 6 72 81
1 91 70 72 233
male 0 77 91 300 468
1 45 17 47 109
All 216 184 491 891

탑승권 등급 1등급 24.2% , 2등급 20.7% , 3등급 55.1%로 구성되어 있다.

  • 1등급의 생존율: 63.0%
  • 2등급의 생존율: 47.3%
  • 3등급의 생존율: 24.2%

탑승권 등급이 높을 수록 생존율이 높았음을 알 수 있다.

# 항구의 위치와 연관성
pie_chart("Embarked")
f, ax = plt.subplots(2, 2, figsize=(15,10))
sns.countplot('Embarked', data=train_df, ax=ax[0,0])
ax[0,0].set_title('No. Of Passengers Boarded')
sns.countplot('Embarked', hue='Sex', data=train_df, ax=ax[0,1])
ax[0,1].set_title('Male-Female Split for Embarked')
sns.countplot('Embarked', hue='Survived', data=train_df, ax=ax[1,0])
ax[1,0].set_title('Embarked vs Survived')
sns.countplot('Embarked', hue='Pclass', data=train_df, ax=ax[1,1])
ax[1,1].set_title('Embarked vs Pclass')

plt.show()

png

png

png

  • 항구별 탑승자 비율 : 절반 이상의 사람이 S항구를 통해 입항한 것을 볼 수 있음

  • 항구별 탑승자 성별 : S항구를 통해 들어온 사람들의 성별은 남성이 많은 수를 차지함

  • 항구별 생존율: 많은 사람이 탄 만큼 S항구의 생존율이 낮음, C항구의 생존률이 높음

  • 항구별 탑승권 등급 : S항구와 Q항구는 3등급객실의 비율이 높으며, C는 1등급객실의 비율이 높음. C항구가 부유한 계층이 사는 지역임을 유추할 수 있음.

# 특성별 생존 여부를 barchart로 시각화하기 위한 함수
def bar_chart(feature):
    survived = train_df[train_df["Survived"] == 1][feature].value_counts()
    dead = train_df[train_df["Survived"] == 0][feature].value_counts()
    df = pd.DataFrame([survived, dead])
    df.index = ["Survived", "Dead"]
    df.plot(kind='bar', stacked=True, figsize=(10,5))
# SibSp 시각화
sibsp_ratio = train_df["SibSp"].value_counts()
plt.pie(sibsp_ratio, labels=sibsp_ratio.index, autopct='%1.1f%%')
plt.title('SibSp\'s ratio')
plt.show()
survived= train_df[train_df['Survived'] == 1]["SibSp"].value_counts()
dead = train_df[train_df['Survived'] == 0]["SibSp"].value_counts()

for i, index in enumerate(survived.index):
    plt.subplot(1, survived.size+1, i+1, aspect='equal')
    plt.pie([survived[index], dead[index]], labels=["Survived", "Dead"], autopct='%1.1f%%')
    plt.title(str(index) + '\'s ratio')
plt.show()
bar_chart("SibSp")
train_df["SibSp"].value_counts()

png

png

0    608
1    209
2     28
4     18
3     16
8      7
5      5
Name: SibSp, dtype: int64

png

형제 또는 배우자가 동승하지 않은 경우는 68.2%, 동승한 경우는 31.8%이다.

동승자의 수가 1명 또는 2명인 경우가 생존율이 제일 높았다.

# Parch 시각화
parch_ratio = train_df["Parch"].value_counts()
plt.pie(parch_ratio, labels=parch_ratio.index, autopct='%1.1f%%')
plt.title('Parch\'s ratio')
plt.show()
survived= train_df[train_df['Survived'] == 1]["Parch"].value_counts()
dead = train_df[train_df['Survived'] == 0]["Parch"].value_counts()

for i, index in enumerate(survived.index):
    plt.subplot(1, survived.size+1, i+1, aspect='equal')
    plt.pie([survived[index], dead[index]], labels=["Survived", "Dead"], autopct='%1.1f%%')
    plt.title(str(index) + '\'s ratio')
plt.show()
bar_chart("Parch")
train_df["Parch"].value_counts()

png

png

0    678
1    118
2     80
5      5
3      5
4      4
6      1
Name: Parch, dtype: int64

png

부모 또는 자녀가 동승하지 않은 경우는 76.1%이며, 동승한 경우는 23.9%이다.

동승자가 없는 탑승객보다 1명 ~ 3명까지 동승한 탑승객의 생존율이 높았다.

데이터를 시각화해본 결과로 알아본 데이터 특성간 유의한 정보는 남자보단 여자, 부유한 계층일 수록 생존율이 높은 것이다.

이를 데이터 전처리 하는데 사용하도록 한다.

데이터 전처리

# 결측치 확인
print(train_df.isnull().sum())
print()
print(test_df.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

PassengerId      0
Pclass           0
Name             0
Sex              0
Age             86
SibSp            0
Parch            0
Ticket           0
Fare             1
Cabin          327
Embarked         0
dtype: int64

훈련셋에는 나이와 카빈, 항구 값에 누락치가 있고, 테스트 셋에는 나이, Fare, 카빈에 누락치가 있다.

train_df['Name'].head()
0                              Braund, Mr. Owen Harris
1    Cumings, Mrs. John Bradley (Florence Briggs Th...
2                               Heikkinen, Miss. Laina
3         Futrelle, Mrs. Jacques Heath (Lily May Peel)
4                             Allen, Mr. William Henry
Name: Name, dtype: object

전처리 과정

  1. Name 특성에서 승객들 이름 앞에 title을 새로운 특성으로 만들어 사용한다.

  2. Embarked 특성은 결측치를 최빈값으로 채운다.

  3. Age 특성은 결측치는 평균으로 대체한 후 연속 데이터를 범주 데이터로 변환한다.

  4. Fare 특성는 Pclass와 연관이 있음으로, Fare 특성이 결측된 값을 Pclass 평균값으로 넣어 후, 범주 데이터로 변환한다.

  5. 나머지 PassengerId, Name, Ticket, Cabin 특성은 drop한다.

  6. 범주형 데이터를 OneHotEncoding을 통해 정수로 변환한다.


1.

Name 특성을 보면 이름과 성 사에 Mr. Mrs. Dr. 등의 타이틀이 붙는다. 이 타이틀을 통해 성별 또는 신분, 직업을 예측할 수 있음으로 Title이라는 새로운 특성을 만들어 사용한다.

# 1
def title_pipeline(titanic):
    titanic['Title'] = titanic.Name.str.extract(' ([A-Za-z]+)\.')
    titanic['Title'] = titanic['Title'].replace(['Capt', 'Col', 'Countess', 'Don','Dona', 'Dr', 'Jonkheer',
                                                'Lady','Major', 'Rev', 'Sir'], 'Other')
    titanic['Title'] = titanic['Title'].replace('Mlle', 'Miss')
    titanic['Title'] = titanic['Title'].replace('Mme', 'Mrs')
    titanic['Title'] = titanic['Title'].replace('Ms', 'Miss')
    titanic['Title'] = titanic['Title'].astype(str)
    return titanic

([A-Za-z]+)\.' 는 정규표현식으로 공백으로 시작하고, .으로 끝나는 문자열을 추출할 때 사용한다.

title_test = train_df.copy()
title_test = title_pipeline(title_test)
print(title_test["Title"].value_counts())
print(title_test[['Title', 'Survived']].groupby(['Title'], as_index=False).mean())
Mr        517
Miss      185
Mrs       126
Master     40
Other      23
Name: Title, dtype: int64
    Title  Survived
0  Master  0.575000
1    Miss  0.702703
2      Mr  0.156673
3     Mrs  0.793651
4   Other  0.347826

2.

# 2
def embarked_pipeline(titanic):
    titanic['Embarked'] = titanic['Embarked'].fillna('S')
    titanic['Embarked'] = titanic['Embarked'].astype(str)
    return titanic

3.

# 5
def age_pipeline(titanic):
    titanic['Age'].fillna(titanic['Age'].mean(), inplace=True)
    titanic.loc[ titanic['Age'] <= 16, 'Age'] = 0
    titanic.loc[ (titanic['Age'] > 16) & (titanic['Age'] <= 32), 'Age'] = 1
    titanic.loc[ (titanic['Age'] > 32) & (titanic['Age'] <= 48), 'Age'] = 2
    titanic.loc[ (titanic['Age'] > 48) & (titanic['Age'] <= 64), 'Age'] = 3
    titanic.loc[ titanic['Age'] > 64, 'Age'] = 4
    titanic['Age'] = titanic['Age'].map({ 0: 'Child', 1: 'Young', 2: 'Middle', 3: 'Prime', 4: 'Old'}).astype(str)
    return titanic

4.

# 4
def fare_pipeline(titanic):
    titanic['Fare'] = titanic['Fare'].fillna(13.675)
    titanic.loc[ titanic['Fare'] <= 7.854, 'Fare'] = 0
    titanic.loc[ (titanic['Fare'] > 7.854) & (titanic['Fare'] <= 10.5), 'Fare'] = 1
    titanic.loc[ (titanic['Fare'] > 10.5) & (titanic['Fare'] <= 21.679), 'Fare'] = 2
    titanic.loc[ (titanic['Fare'] > 21.679) & (titanic['Fare'] <= 39.688), 'Fare'] = 3
    titanic.loc[ titanic['Fare'] > 39.688, 'Fare'] = 4
    titanic['Fare'] = titanic['Fare'].astype(int)
    return titanic

5.

# 5
def drop_pipeline(titanic):
    features_drop = ["PassengerId","Name", "Ticket", "Cabin"]
    titanic_prepared = titanic.drop(features_drop, axis=1)
    return titanic_prepared

6.

# 6.
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import OneHotEncoder, FunctionTransformer, StandardScaler

def cat_pipeline():
    return ColumnTransformer([("cat", OneHotEncoder(handle_unknown='ignore'), make_column_selector(dtype_include=object))],
                                         remainder = 'passthrough')

전처리 파이프라인

# 전처리 파이프라인
from sklearn.pipeline import Pipeline
from sklearn import set_config
set_config(display='diagram')

preprocessing = Pipeline([("title_preprocessing", FunctionTransformer(title_pipeline)),
                         ("embarked_preprocessing", FunctionTransformer(embarked_pipeline)),
                         ("age_preprocessing", FunctionTransformer(age_pipeline)),
                         ("fare_preprocessing", FunctionTransformer(fare_pipeline)),
                         ("drop_preprocessing", FunctionTransformer(drop_pipeline)),
                         ("cat_preprocessing", cat_pipeline())])

훈련셋 전처리하기

titanic_label : 타이타닉 탑승객의 생존여부 정보
titanic : 전처리할 특성

titanic_prepare = train_df.copy()
titanic_label = titanic_prepare['Survived'].copy()
titanic = titanic_prepare.drop("Survived", axis=1)
titanic = preprocessing.fit_transform(titanic)
print(titanic)
print(titanic.shape)
[[0. 1. 0. ... 1. 0. 0.]
 [1. 0. 0. ... 1. 0. 4.]
 [1. 0. 0. ... 0. 0. 1.]
 ...
 [1. 0. 0. ... 1. 2. 3.]
 [0. 1. 0. ... 0. 0. 3.]
 [0. 1. 0. ... 0. 0. 0.]]
(891, 19)

테스트셋 전처리하기

test_set = test_df.copy()
test_set = preprocessing.fit_transform(test_set)
test_set.shape
(418, 19)

모델 학습과 훈련

모델 학습과 평가에 필요한 pipeline을 만들어 훈련된 결과를 평가한다.

평가예정 모델

  • LogisticRegression 선형회귀모델
  • SVC SVC 모델
  • KNeighborsClassifier
  • RandomForestClassifier 랜덤포레스트 모델
  • GaussianNB

평가 : 교차검증

from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import cross_val_score
from sklearn.utils import shuffle

# 데이터 셔플
titanic, titanic_label = shuffle(titanic, titanic_label, random_state=42)

# 모델 학습과 평가에 대한 pipeline
def train_and_test(model):
    model.fit(titanic, titanic_label)
    prediction = model.predict(test_set)

    accuracy = cross_val_score(model, titanic, titanic_label, cv=10)
    accuracy = accuracy.mean()
    print(f"Accuracy : {accuracy} %")
    return prediction
log_pred = train_and_test(LogisticRegression())
svm_pred = train_and_test(SVC())
knn_pred_4 = train_and_test(KNeighborsClassifier(n_neighbors = 4))
rf_pred = train_and_test(RandomForestClassifier(n_estimators=100))
nb_pred = train_and_test(GaussianNB())
Accuracy : 0.8215480649188514 %
Accuracy : 0.8316354556803995 %
Accuracy : 0.8181772784019975 %
Accuracy : 0.8102996254681647 %
Accuracy : 0.7789013732833958 %

제출

교차검증을 통해 83.16%를 보인 SVC 모델을 채택해서 제출한다.

제출은 캐글 사이트에서 Submit Predictions에서 제출할 수 있으며, 캐글 노트북에서 작성할 경우 예측한 파일 이름을 submission.csv으로 해야한다.

submission = pd.DataFrame({
    "PassengerId": test_df["PassengerId"],
    "Survived": svm_pred
})

submission.to_csv('submission.csv', index=False)

kaggle_titanic_predict.jpg

채점 결과 교차검증에서 정확도 보다 낮은 77.99%가 나왔다.

개선 해야할 점

GridSearchCV 또는 RandomizedSearchCV 를 통해 더 많은 모델과 하이퍼파라미터 설정을 비교하면 좋아질 것 같다.

Leave a comment