본문 바로가기
데이터 분석/Kaggle

캐글 코리아 주관 - "2019 1st ML month with KaKR" kernel

by 레드홀스 2019. 2. 6.
Titanic classifier report


1. Introduction


Kaggle의 대표적인 Competition 중 하나인 Titanic에 대한 분석과정을 담은 것 입니다.

여러 Kaggler분들께서 R Program을 이용해서 작성하신 kernel 들을 참고했으며, 처음 Kaggle에 입문하시는 분들이 참고하실만한 자료가 되기를 바라면서 작성했습니다.

해당 문서를 작성하면서 참고했던 kernel, 블로그 게시물, 책들은 Chapter 7. Reference에 있습니다.




2. 준비작업


2.1 Working directory


rm(list = ls())

setwd("C:/Users/LG/Documents/Kaggle Competition/Titanic/")
getwd()
## [1] "C:/Users/LG/Documents/Kaggle Competition/Titanic"


2.2 Packages


ggplot2, dplyr와 같은 R의 대표적인 Package들 외에도 제가 분석하는데 사용한 Package들을 불러오는(장착하는) 과정입니다.

각 목적별로 분류해놨으며, 개인적으로 자주 사용하는 함수들은 패키지옆에 추가로 주석을 달아놨습니다.


# Data input, assesment : 데이터 불러들이기, 확인하는 과정 
library(readr)           # Data input with readr::read_csv()
library(descr)           # descr::CrossTable() - 범주별 빈도수, 비율 수치로 확인

# Visualization
library(VIM)             # Missing values assesment used by VIM::aggr()
library(ggplot2)         # Used in almost visualization 
library(RColorBrewer)    # plot의 color 설정 
library(scales)          # plot setting - x, y 축 설정

# Feature engineering, Data Pre-processing
# library(tidyverse)     # dplyr, ggplot2, purrr, etc... 
library(dplyr)           # Feature Engineering & Data Pre-processing 
library(purrr)           # Check missing values 
library(tidyr)           # tidyr::gather() 

# Model generation  
library(randomForest)    # For Random Forest Modeling

# Model validation : 원래는 하는게 맞지만 이번 과정에서는 생략
# library(caret)           # caret::confusionMatrix() 
# library(ROCR)            # Plotting ROC Curve


multiplot() function generation


한 화면에 여러개 plot들을 볼 때 유용한 함수가 multiplot() 입니다.

하지만 제 개인 노트북의 문제인지 함수가 작동하지 않아서 CRAN을 참고하여 여기서 multiplot() 함수를 생성해서 사용했습니다.


# Multiple plot function
#
# ggplot objects can be passed in ..., or to plotlist (as a list of ggplot objects)
# - cols:   Number of columns in layout
# - layout: A matrix specifying the layout. If present, 'cols' is ignored.
#
# If the layout is something like matrix(c(1,2,3,3), nrow=2, byrow=TRUE),
# then plot 1 will go in the upper left, 2 will go in the upper right, and
# 3 will go all the way across the bottom.
#

multiplot <- function(..., plotlist = NULL, file, cols = 1, layout = NULL) {
  library(grid)
  
  # Make a list from the ... arguments and plotlist
  plots <- c(list(...), plotlist)
  
  numPlots = length(plots)
  
  # If layout is NULL, then use 'cols' to determine layout
  if (is.null(layout)) {
    # Make the panel
    # ncol: Number of columns of plots
    # nrow: Number of rows needed, calculated from # of cols
    layout <- matrix(seq(1, cols * ceiling(numPlots/cols)),
                     ncol = cols, nrow = ceiling(numPlots/cols))
  }
  
  if (numPlots==1) {
    print(plots[[1]])
    
  } else {
    # Set up the page
    grid.newpage()
    pushViewport(viewport(layout = grid.layout(nrow(layout), ncol(layout))))
    
    # Make each plot, in the correct location
    for (i in 1:numPlots) {
      # Get the i,j matrix positions of the regions that contain this subplot
      matchidx <- as.data.frame(which(layout == i, arr.ind = TRUE))
      
      print(plots[[i]], vp = viewport(layout.pos.row = matchidx$row,
                                      layout.pos.col = matchidx$col))
    }
  }
}


2.3 Read raw data : 원본데이터 불러오기


titanic competition에서는 Model을 생성하는데 사용하는 train data와 실제 예측(추정)에 사용하는 test data가 분리되어 있습니다.

여기서는 저 2개 data들을 불러와서 하나로 묶을 것 입니다. 따로 분리되어 있는 데이터들을 하나로 묶는 이유는 모델링에 사용되는 입력변수들을 Feature engineering, Pre-processing 할 때 동일하게 작업하기 위해서 입니다.


train <- readr::read_csv("./train.csv")

test <- readr::read_csv("./test.csv")

full <- dplyr::bind_rows(train, test)


Data들을 불러오는데 read.csv() 대신 readr 패키지의 read_csv()를 사용한 이유는 read_csv()가 조금 더 빠르게 메모리에 데이터를 올려주기 때문입니다.

여기서 주의점은 문자열(Character)과 요인 변수(Factor)를 구별하지 못하고 모두 Chr속성으로 저장한다는 것입니다.

추가로 두 데이터를 full로 합칠때도 rbind()를 사용하지 않은 이유는 test 에는 Titanic competition의 종속변수(타겟변수, Y)인 Survived가 존재하지 않기 때문입니다.

따라서 두 데이터의 차원(dimension)이 맞지않아 rbind()로는 병합되지 않습니다.

하지만 dplyr::bind_rows()를 사용하면 testSurvivedNA로 처리하면서 하나로 병합해줍니다.


2.4 변수 의미 설명


각 변수들의 의미와 제가 지정한 속성들은 아래 표와 같습니다.


변수명 해석(의미) Type
PassengerID 승객을 구별하는 고유 ID number Int
Survived 승객의 생존 여부를 나타내며 생존은 1, 사망은 0 입니다. Factor
Pclass 선실의 등급으로서 1등급(1)부터 3등급(3)까지 3개 범주입니다. Ord.Factor
Name 승객의 이름 Factor
Sex 승객의 성별 Factor
Age 승객의 나이 Numeric
SibSp 각 승객과 동반하는 형제 또는 배우자의 수를 설명하는 변수이며 0부터 8까지 존재합니다. Integer
Parch 각 승객과 동반하는 부모님 또는 자녀의 수를 설명하는 변수이며 0부터 9까지 존재합니다. Integer
Ticket 승객이 탑승한 티켓에 대한 문자열 변수 Factor
Fare 승객이 지금까지 여행하면서 지불한 금액에 대한 변수 Numeric
Cabin 각 승객의 선실을 구분하는 변수이며 범주와 결측치가 너무 많습니다. Factor
Embarked 승선항, 출항지를 나타내며 C, Q, S 3개 범주이다. Factor


위 표에 써있는 내용보다 더 자세하고 명확하게 이해하고 싶으신 분들은 여기에 들어가보시기 바랍니다.

불법링크는 아니며, 나무위키에서 타이타닉호에 대한 내용을 정리해놓은 것입니다.


2.5 Change the variables type : 변수 속성 변환


본격적인 EDAFeature engineering에 앞서 몇 가지 변수 속성들을 변환하도록 하겠습니다.

그 이유는 위에서도 설명했지만 read.csv()가 아니라 readr::read_csv()로 data들을 input하면서 full에는 factor 속성의 변수들이 없기도 하고 Pclass의 1, 2, 3은 1등급, 2등급, 3등급을 나타내는 factor이기 때문입니다.


full <- full %>%
  dplyr::mutate(Survived = factor(Survived),
                Pclass   = factor(Pclass, ordered = T),
                Name     = factor(Name),
                Sex      = factor(Sex),
                Ticket   = factor(Ticket),
                Cabin    = factor(Cabin),
                Embarked = factor(Embarked))


다음은 탐색적 데이터 분석입니다.




3. 탐색적 데이터 분석 (EDA : Exploratory data analysis)


Data가 어떻게 구성이 되어 있고 그 안에 결측치(Missing value) 혹은 이상치(Outlier)가 존재하는지 등등 원시 데이터(Raw data)에 대해 탐색하고 이해하는 과정입니다.

여기서는 다양한 function들과 시각화(Visualization)을 사용할 것입니다.


3.1 수치값을 활용한 data 확인

가장 먼저 head(), summary() 등 다양한 함수들의 결과값(Output)들을 통해 data를 확인하도록 하겠습니다.


3.1.2 str()


str(full)
## Classes 'tbl_df', 'tbl' and 'data.frame':    1309 obs. of  12 variables:
##  $ PassengerId: num  1 2 3 4 5 6 7 8 9 10 ...
##  $ Survived   : Factor w/ 2 levels "0","1": 1 2 2 2 1 1 1 1 2 2 ...
##  $ Pclass     : Ord.factor w/ 3 levels "1"<"2"<"3": 3 1 3 1 3 3 1 3 3 2 ...
##  $ Name       : Factor w/ 1307 levels "Abbing, Mr. Anthony",..: 156 287 531 430 23 826 775 922 613 855 ...
##  $ Sex        : Factor w/ 2 levels "female","male": 2 1 1 1 2 2 2 2 1 1 ...
##  $ Age        : num  22 38 26 35 35 NA 54 2 27 14 ...
##  $ SibSp      : num  1 1 0 1 0 0 0 3 0 1 ...
##  $ Parch      : num  0 0 0 0 0 0 0 1 2 0 ...
##  $ Ticket     : Factor w/ 929 levels "110152","110413",..: 721 817 915 66 650 374 110 542 478 175 ...
##  $ Fare       : num  7.25 71.28 7.92 53.1 8.05 ...
##  $ Cabin      : Factor w/ 186 levels "A10","A11","A14",..: NA 107 NA 71 NA NA 164 NA NA NA ...
##  $ Embarked   : Factor w/ 3 levels "C","Q","S": 3 1 3 3 3 2 3 3 3 1 ...


Train, test data를 합쳐서 전체 관측치(레코드, 행, Row)는 총 1309개(train : 891, test : 418)이고 변수(열, Feature, variable, column)는 12개 임을 알 수 있습니다.

그 외에도 각 변수들의 속성이 무엇인지와 factor 속성인 변수들에 범주가 몇 개인지까지 알 수 있습니다.

또한 head()에서는 Age에만 존재하는 줄 알았던 결측치(NA)가 Cabin을 비롯한 다른 변수들에도 있음을 알 수 있습니다.


3.1.3 summary()


summary(full)
##   PassengerId   Survived   Pclass                                Name     
##  Min.   :   1   0   :549   1:323   Connolly, Miss. Kate            :   2  
##  1st Qu.: 328   1   :342   2:277   Kelly, Mr. James                :   2  
##  Median : 655   NA's:418   3:709   Abbing, Mr. Anthony             :   1  
##  Mean   : 655                      Abbott, Master. Eugene Joseph   :   1  
##  3rd Qu.: 982                      Abbott, Mr. Rossmore Edward     :   1  
##  Max.   :1309                      Abbott, Mrs. Stanton (Rosa Hunt):   1  
##                                    (Other)                         :1301  
##      Sex           Age            SibSp            Parch      
##  female:466   Min.   : 0.17   Min.   :0.0000   Min.   :0.000  
##  male  :843   1st Qu.:21.00   1st Qu.:0.0000   1st Qu.:0.000  
##               Median :28.00   Median :0.0000   Median :0.000  
##               Mean   :29.88   Mean   :0.4989   Mean   :0.385  
##               3rd Qu.:39.00   3rd Qu.:1.0000   3rd Qu.:0.000  
##               Max.   :80.00   Max.   :8.0000   Max.   :9.000  
##               NA's   :263                                     
##       Ticket          Fare                     Cabin      Embarked  
##  CA. 2343:  11   Min.   :  0.000   C23 C25 C27    :   6   C   :270  
##  1601    :   8   1st Qu.:  7.896   B57 B59 B63 B66:   5   Q   :123  
##  CA 2144 :   8   Median : 14.454   G6             :   5   S   :914  
##  3101295 :   7   Mean   : 33.295   B96 B98        :   4   NA's:  2  
##  347077  :   7   3rd Qu.: 31.275   C22 C26        :   4             
##  347082  :   7   Max.   :512.329   (Other)        : 271             
##  (Other) :1261   NA's   :1         NA's           :1014


summary()는 data에 대한 많은 정보를 제공해줍니다.

수량형(Integer, Numeric) 변수들의 대푯값들, 범주형(Factor) 변수들의 범주가 몇 개인지 그리고 각 범주에 속하는 관측치의 갯수들까지 모두 수치값들로 보여줍니다.

여기서 확인하고 넘어갈 것들은 다음과 같습니다.

  1. Survived : 이번 competition의 타겟 변수이며 418개의 결측치는 Test data 때문입니다.

  2. Pclass : 1등급, 2등급, 3등급으로 범주가 세 개인데 3등급 승객이 가장 많습니다.

  3. Name : 이름이 비슷한 사람들이 있습니다. 따라서 혼자 탄 승객도 있지만 가족들과 같이 탑승한 승객도 있음을 알 수 있습니다.

  4. Sex : 남성이 여성보다 2배 가까이 더 많습니다.

  5. Age : 0.17부터 80세까지 있는데 17을 잘못 기입한 이상치 인건지 확인이 필요해보이며 263개의 결측치가 존재합니다.

  6. SibSp : 0부터 8까지 있는데 3분위수가 1이므로 부부 혹은 형제와 함께 Titanic호에 탑승했음을 알 수 있습니다.

  7. Parch : 0부터 9까지 있지만 3분위수가 0인 것을 통해서 부모, 자녀들과 함께 탄 승객들이 거의 없음을 알 수 있습니다.

SibSpParch는 모두 가족관계를 나타내는 변수들입니다. 이를 통해서 가족 중 누가 탔는지는 모르지만 동승한 인원이 총 몇 명인지 구하고, 그것을 바탕으로 가족의 규모를 나타내는 FamilySized 라는 범주형 파생변수를 만들 것 입니다.

  1. Ticket : 3.1.2 str()의 결과와 같이보면 완전히 동일한 티켓을 가진 승객들도 있고, 티켓이 일정 부분만 겹치는 승객들도 있고 아예 다른 티켓을 가진 승객이 있음을 알 수 있습니다. 이것을 이용해서 ticket.size라는 파생변수를 만들 계획입니다.

  2. Fare : 0부터 512까지 있고 1개의 결측치가 있습니다. 3분위수가 31.275이며 최댓값이 512인게 신경쓰입니다.

  3. Cabin : 총 12개 Feature들 중에서 가장 많은(1014개) 결측치를 갖고 있습니다. 배의 구역을 나타내는 변수인데 활용할 방법이 없다면 버려야 될 것 같습니다.

  4. Embarked : 총 3개 범주로 구성이 되어있고 S가 가장 많으며 2개의 결측치가 있습니다.

데이터에 대한 기본적인 탐색을 하실 때는 summary()str() 그 외에 다양한 함수들의 output들도 같이 비교해가면서 보시길 바랍니다.


3.2 Missing values


위에서 언급했던 결측치가 존재하는 변수는 무엇이고 또 얼마나 존재하는지 확인해보는 과정입니다.

dplyr, ggplot2, VIM 패키지들을 이용해서 수치적으로 확인하면서 동시에 시각적으로도 확인해보려고 합니다.

제가 실행한 모든 코드들을 사용하실 필요는 없으며, 독자분께서 읽으시면서 필요하시거나 좋다고 생각하는 부분들만 사용하셔도 됩니다.


3.2.1 VIM packages


VIM::aggr(full, prop = FALSE, combined = TRUE, numbers = TRUE,
          sortVars = TRUE, sortCombs = TRUE)

## 
##  Variables sorted by number of missings: 
##     Variable Count
##        Cabin  1014
##     Survived   418
##          Age   263
##     Embarked     2
##         Fare     1
##  PassengerId     0
##       Pclass     0
##         Name     0
##          Sex     0
##        SibSp     0
##        Parch     0
##       Ticket     0


3.2.2 tidyverse packages


VIM 패키지를 이용해서 한 번에 결측치를 확인하는 것 외에 tidyverse에 존재하는 다양한 패키지들을 이용하여 결측치를 확인하는 방법들입니다.

먼저 dplyr로 각 변수별 결측치 비율을 구하는 것 입니다.


full %>%
  dplyr::summarize_all(funs(sum(is.na(.))/n()))
## # A tibble: 1 x 12
##   PassengerId Survived Pclass  Name   Sex   Age SibSp Parch Ticket    Fare
##         <dbl>    <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>   <dbl>
## 1           0    0.319      0     0     0 0.201     0     0      0 7.64e-4
## # ... with 2 more variables: Cabin <dbl>, Embarked <dbl>


이렇게 변수들에 존재하는 결측치의 비율을 확인하는 방법도 있지만, 시각자료들을 이용해서도 확인할 수 있습니다.

아래의 2개 Bar plot들을 봐주시기 바랍니다.


# 각 feature의 결측치 비율 계산 -> Data Frame 속성 but 1행 12열 구조로 되어있음.
missing_values <- full %>%
  dplyr::summarize_all(funs(sum(is.na(.))/n()))

# 위에서 구한 missing_values를 12X2 data frame 으로 생성 
missing_values <- tidyr::gather(missing_values,
                                key = "feature", value = "missing_pct")

# missing_values를 이용한 시각화 
missing_values %>% 
  # Aesthetic setting : missing_pct 내림차순으로 정렬  
  ggplot(aes(x = reorder(feature, missing_pct), y = missing_pct)) +
  # Bar plot 
  geom_bar(stat = "identity", fill = "red") +
  # Title generation 
  ggtitle("Rate of missing values in each features") +
  # Title detail setting 
  theme(plot.title = element_text(face = "bold",    # 글씨체 
                                  hjust = 0.5,      # Horizon(가로비율) = 0.5
                                  size = 15, color = "darkblue")) +
  # x, y axis label setting 
  labs(x = "Feature names", y = "Rate") + 
  # Plot의 x, y축 변환 
  coord_flip()


위 막대 그래프를 보시면 모든 feature들에 대해 결측치 비율을 확인 할 수 있습니다.

하지만 실질적으로 우리가 궁금한 것은 결측치가 존재하는 변수는 무엇인지와 그 안에 얼마나 되는 결측치가 존재하느냐입니다.

때문에 purrr 패키지를 이용해서 결측치 비율을 계산한 후, 하나라도 존재하는 변수만 추출한 뒤에 시각화 해보았습니다.


# 변수별 결측치 비율 계산
miss_pct <- purrr::map_dbl(full, function(x){round((sum(is.na(x))/length(x)) * 100, 1) })

# 결측치 비율이 0%보다 큰 변수들만 선택
miss_pct <- miss_pct[miss_pct > 0]

# Data Frame 생성 
data.frame(miss = miss_pct, var = names(miss_pct), row.names = NULL) %>%
  # Aesthetic setting : miss 내림차순으로 정렬 
  ggplot(aes(x = reorder(var, miss), y = miss)) + 
  # Bar plot 
  geom_bar(stat = 'identity', fill = 'red') +
  # Plot title setting 
  ggtitle("Rate of missing values") + 
  # Title detail setting 
  theme(plot.title = element_text(face = "bold",    # 글씨체 
                                  hjust = 0.5,      # Horizon(가로비율) = 0.5
                                  size = 15, color = "darkblue")) +
  # x, y axis label setting 
  labs(x = 'Feature names', y = 'Rate of missing values') +
  # Plot의 x, y축 변환 
  coord_flip()


이를 통해 총 12개 변수들 중에서 4개 변수에만 결측치가 존재하며(Survivedtest data 때문이므로 제외) Cabin, Age, Embarked, Fare 순으로 결측치가 많음을 알 수 있습니다.




이제는 시각화를 통해서 feature를 분석, 탐색하는 과정입니다.

3.3 Age


age.p1 <- full %>% 
  ggplot(aes(Age)) + 
  # 히스토그램 그리기, 설정
  geom_histogram(breaks = seq(0, 80, by = 1), # 간격 설정 
                 col    = "red",              # 막대 경계선 색깔 
                 fill   = "green",            # 막대 내부 색깔 
                 alpha  = .5) +               # 막대 투명도 = 50% 
  # Plot title
  ggtitle("All Titanic passengers age hitogram") +
  theme(plot.title = element_text(face = "bold",    # 글씨체 
                                  hjust = 0.5,      # Horizon(가로비율) = 0.5
                                  size = 15, color = "darkblue"))

age.p2 <- full %>% 
  # test data set의 Survived == NA 인 값들 제외 
  filter(!is.na(Survived)) %>% 
  ggplot(aes(Age, fill = Survived)) + 
  geom_density(alpha = .5) +
  ggtitle("Titanic passengers age density plot") + 
  theme(plot.title = element_text(face = "bold", hjust = 0.5,
                                  size = 15, color = "darkblue"))

# multiplot layout 형식 지정
multi.layout = matrix(c(1, 1, 2, 2), 2, 2, byrow = T)

# 위에서 생성한 2개의 그래프 한 화면에 출력 
multiplot(age.p1, age.p2, layout = multi.layout)


3.4 Pclass


Pclass에 해당하는 탑승객의 빈도수를 시각화 해보겠습니다.

dplyr패키지를 활용해서 Pclass별로 집단화(그룹핑) 시킨 후 범주별 빈도수를 나타내는 Data Frame을 생성한 뒤에 ggplot으로 시각화 했습니다.


full %>% 
  # dplyr::group_by(), summarize() 를 이용해서 Pclass 빈도수 구하기
  group_by(Pclass) %>% 
  summarize(N = n()) %>% 
  # Aesthetic setting 
  ggplot(aes(Pclass, N)) +
  geom_col() +
  # Pclass 빈도수 plot에 출력 
  geom_text(aes(label = N),        # Plot의 y에 해당하는 N(빈도수)를 매핑
            size = 5,              # 글씨 크기 
            vjust = 1.2,           # vertical(가로) 위치 설정 
            color = "#FFFFFF") +   # 글씨 색깔 : 흰색
  # Plot title 
  ggtitle("Number of each Pclass's passengers") + 
  # Title setting 
  theme(plot.title = element_text(face = "bold", hjust = 0.5, size = 15)) +
  # x, y axis name change  
  labs(x = "Pclass", y = "Count")


3등급 객실에 탑승한 승객이 가장 많음을 알 수 있습니다.

객실의 등급와 생존율이 연관있는지는 Chapter5에서 다루도록 하겠습니다.


3.5 Fare


탑승객이 지불한 금액을 나타내는 Fare 변수에 대한 시각화 입니다.

히스토그램과 상자그림 두 개를 이용했습니다.

# Histogram 
Fare.p1 <- full %>%
  ggplot(aes(Fare)) + 
  geom_histogram(col    = "yellow",
                 fill   = "blue", 
                 alpha  = .5) +
  ggtitle("Histogram of passengers Fare") +
  theme(plot.title = element_text(face = "bold", hjust = 0.5, size = 15))

# Boxplot 
Fare.p2 <- full %>%
  filter(!is.na(Survived)) %>% 
  ggplot(aes(Survived, Fare)) + 
  # 관측치를 회색점으로 찍되, 중복되는 부분은 퍼지게 그려줍니다.
  geom_jitter(col = "gray") + 
  # 상자그림 : 투명도 50% 
  geom_boxplot(alpha = .5) + 
  ggtitle("Boxplot of passengers Fare") +
  theme(plot.title = element_text(face = "bold", hjust = 0.5, size = 15))

# multiplot layout 형식 지정
multi.layout = matrix(c(1, 1, 2, 2), 2, 2)

# 위에서 생성한 2개의 그래프 한 화면에 출력 
multiplot(Fare.p1, Fare.p2, layout = multi.layout)


생존자들이 사망한 승객들보다 Fare가 더 높지만 그렇게 큰 차이는 없음을 알 수 있습니다.


3.6 Sex

남, 여 간에 생존율의 차이가 있을까요? 아래의 plot들을 보시기 바랍니다.


sex.p1 <- full %>% 
  dplyr::group_by(Sex) %>% 
  summarize(N = n()) %>% 
  ggplot(aes(Sex, N)) +
  geom_col() +
  geom_text(aes(label = N), size = 5, vjust = 1.2, color = "#FFFFFF") + 
  ggtitle("Bar plot of Sex") +
  labs(x = "Sex", y = "Count")
  
sex.p2 <- full[1:891, ] %>% 
  ggplot(aes(Sex, fill = Survived)) +
  geom_bar(position = "fill") + 
  scale_fill_brewer(palette = "Set1") +
  scale_y_continuous(labels = percent) +
  ggtitle("Survival Rate by Sex") + 
  labs(x = "Sex", y = "Rate")

multi.layout = matrix(rep(c(1, 2), times = 2), 2, 2, byrow = T)

multiplot(sex.p1, sex.p2, layout = multi.layout)

mosaicplot(Survived ~ Sex,
           data = full[1:891, ], col = TRUE,
           main = "Survival rate by passengers gender")


그래프를 해석해보면, 남성이 여성보다 훨씬 많은 반면에 생존율은 여성 탑승객이 높음을 알 수 있습니다.


4. Feature engineering & Data Pre-processing


Chapter 3 EDA의 내용들을 바탕으로 결측치(Missing value, NA)를 채우고 동시에 파생변수를 생성하는 과정입니다.


4.1 Age -> Age.Group


full <- full %>%
  # 결측치(NA)를 먼저 채우는데 결측치를 제외한 값들의 평균으로 채움.
  mutate(Age = ifelse(is.na(Age), mean(full$Age, na.rm = TRUE), Age),
         # Age 값에 따라 범주형 파생 변수 Age.Group 를 생성
         Age.Group = case_when(Age < 13             ~ "Age.0012",
                               Age >= 13 & Age < 18 ~ "Age.1317",
                               Age >= 18 & Age < 60 ~ "Age.1859",
                               Age >= 60            ~ "Age.60inf"),
         # Chr 속성을 Factor로 변환 
         Age.Group = factor(Age.Group))


4.3 SibSp & Parch -> FamilySized


full <- full %>% 
  # SibSp, Parch와 1(본인)을 더해서 FamilySize라는 파생변수를 먼저 생성  
  mutate(FamilySize = .$SibSp + .$Parch + 1,
         # FamilySize 의 값에 따라서 범주형 파생 변수 FamilySized 를 생성 
         FamilySized = dplyr::case_when(FamilySize == 1 ~ "Single",
                                        FamilySize >= 2 & FamilySize < 5 ~ "Small",
                                        FamilySize >= 5 ~ "Big"),
         # Chr 속성인 FamilySized를 factor로 변환하고
         # 집단 규모 크기에 따라 levels를 새로 지정
         FamilySized = factor(FamilySized, levels = c("Single", "Small", "Big")))


SibSp, Parch를 이용해서 FamilySized를 만들었습니다.

이렇게 두 개의 변수를 하나로 줄이면 모델이 더욱 단순해지는 장점이 있습니다.

비슷한 사용 방법으로는 키와 체중을 합쳐서 BMI 지수로 만드는 것 입니다.


4.4 Name & Sex -> title


Chapter 3.6 Sex의 결과를 봤을 때 여성의 생존율이 남성보다 높은 것을 확인했었습니다.

따라서 Name에서 “성별과 관련된 이름만을 추출해서 범주화 시키면 쓸모있지 않을까?” 라는 생각이듭니다.

먼저 full data에서 Name 이라는 열벡터만 추출 한 뒤에 title로 저장합니다.


# 우선 Name 열벡터만 추출해서 title 벡터에 저장 
title <- full$Name

# 정규표현식과 gsub()을 이용해서 성별과 관련성이 높은 이름만 추출해서 title 벡터로 저장 
title <- gsub("^.*, (.*?)\\..*$", "\\1", title)

# 위에서 저장한 title 벡터를 다시 full 에 저장하되, title 파생변수로 저장 
full$title <- title


그 다음 고유한(Unique한) title들에는 어떤 것들이 있는지 확인해봅니다.


unique(full$title)
##  [1] "Mr"           "Mrs"          "Miss"         "Master"      
##  [5] "Don"          "Rev"          "Dr"           "Mme"         
##  [9] "Ms"           "Major"        "Lady"         "Sir"         
## [13] "Mlle"         "Col"          "Capt"         "the Countess"
## [17] "Jonkheer"     "Dona"


총 18개의 범주가 있음을 알 수 있습니다.

title이라는 파생변수를 그대로 사용할 경우 모델의(특히 Tree based model) 복잡도가 상당히 높아지기 때문에 범주를 줄여줘야합니다.

그 전에 descr패키지를 이용해서 각 범주별 빈도수와 비율을 확인해보겠습니다.


# 범주별 빈도수, 비율 확인 
descr::CrossTable(full$title)
##    Cell Contents 
## |-------------------------|
## |                       N | 
## |           N / Row Total | 
## |-------------------------|
## 
## |         Capt |          Col |          Don |         Dona |
## |--------------|--------------|--------------|--------------|
## |            1 |            4 |            1 |            1 |
## |        0.001 |        0.003 |        0.001 |        0.001 |
## |--------------|--------------|--------------|--------------|
## 
## |           Dr |     Jonkheer |         Lady |        Major |
## |--------------|--------------|--------------|--------------|
## |            8 |            1 |            1 |            2 |
## |        0.006 |        0.001 |        0.001 |        0.002 |
## |--------------|--------------|--------------|--------------|
## 
## |       Master |         Miss |         Mlle |          Mme |
## |--------------|--------------|--------------|--------------|
## |           61 |          260 |            2 |            1 |
## |        0.047 |        0.199 |        0.002 |        0.001 |
## |--------------|--------------|--------------|--------------|
## 
## |           Mr |          Mrs |           Ms |          Rev |
## |--------------|--------------|--------------|--------------|
## |          757 |          197 |            2 |            8 |
## |        0.578 |        0.150 |        0.002 |        0.006 |
## |--------------|--------------|--------------|--------------|
## 
## |          Sir | the Countess |
## |--------------|--------------|
## |            1 |            1 |
## |        0.001 |        0.001 |
## |--------------|--------------|


18개나 되는 범주들의 빈도수와 비율이 너무 제각각입니다.

따라서 이것들을 총 5개 범주로 줄이도록 하겠습니다.


# 5개 범주로 단순화 시키는 작업 
full <- full %>%
  # "%in%" 대신 "=="을 사용하게되면 Recyling Rule 때문에 원하는대로 되지 않습니다.
  mutate(title = ifelse(title %in% c("Mlle", "Ms", "Lady", "Dona"), "Miss", title),
         title = ifelse(title == "Mme", "Mrs", title),
         title = ifelse(title %in% c("Capt", "Col", "Major", "Dr", "Rev", "Don",
                                     "Sir", "the Countess", "Jonkheer"), "Officer", title),
         title = factor(title))

# 파생변수 생성 후 각 범주별 빈도수, 비율 확인 
descr::CrossTable(full$title)
##    Cell Contents 
## |-------------------------|
## |                       N | 
## |           N / Row Total | 
## |-------------------------|
## 
## |  Master |    Miss |      Mr |     Mrs | Officer |
## |---------|---------|---------|---------|---------|
## |      61 |     266 |     757 |     198 |      27 |
## |   0.047 |   0.203 |   0.578 |   0.151 |   0.021 |
## |---------|---------|---------|---------|---------|


4.5 Ticket -> ticket.size


Chapter 3.1.3 Summary()에서도 봤지만 승객은 (train, test 합쳐서 모두) 1309명입니다. 그런데 모든 승객의 티켓이 다 다르지 않습니다.

아래 summary()unique()의 결과를 보시기 바랍니다.


# 고유한(unique한) 범주의 갯수만 파악하려고 length()를 사용했습니다.
length(unique(full$Ticket))
## [1] 929
# 모두 출력하면 너무 지저분해서 10개만 출력했습니다.
head(summary(full$Ticket), 10)
##     CA. 2343         1601      CA 2144      3101295       347077 
##           11            8            8            7            7 
##       347082     PC 17608 S.O.C. 14879       113781        19950 
##            7            7            7            6            6


결측치가 없는 feature인데 왜 고유한 티켓이 929개 일까요?

심지어 티켓이 CA. 2343으로 완전히 같은 인원이 11명이나 됩니다.

어떤 승객들인지 확인해보겠습니다.


full %>% 
  # 티켓이 일치하는 11명의 승객들만 필터링 
  filter(Ticket == "CA. 2343") %>% 
  # 모든 변수에 대해 확인할 필요는 없으므로 아래 변수들만 보려고 합니다.
  select(Pclass, Name, Age, FamilySized)
## # A tibble: 11 x 4
##    Pclass Name                                    Age FamilySized
##    <ord>  <fct>                                 <dbl> <fct>      
##  1 3      Sage, Master. Thomas Henry             29.9 Big        
##  2 3      Sage, Miss. Constance Gladys           29.9 Big        
##  3 3      Sage, Mr. Frederick                    29.9 Big        
##  4 3      Sage, Mr. George John Jr               29.9 Big        
##  5 3      Sage, Miss. Stella Anna                29.9 Big        
##  6 3      Sage, Mr. Douglas Bullen               29.9 Big        
##  7 3      "Sage, Miss. Dorothy Edith \"Dolly\""  29.9 Big        
##  8 3      Sage, Miss. Ada                        29.9 Big        
##  9 3      Sage, Mr. John George                  29.9 Big        
## 10 3      Sage, Master. William Henry            14.5 Big        
## 11 3      Sage, Mrs. John (Annie Bullen)         29.9 Big


위 11명의 승객들은 모두 같은 가족, 형제들인 것을 알 수 있습니다.

이렇게 티켓이 완전히 동일한 승객들이 있는 반면에 일부분만 일치하는 승객들도 존재합니다.

이런 티켓의 고유한 넘버(글자수) 갯수를 나타내는 ticket.unique 파생변수를 만들고

ticket.unique를 바탕으로 3개 범주를 갖는 ticket.size 파생변수를 만들어 봅시다.


# 우선 ticket.unique가 모두 0이라고 저장함
ticket.unique <- rep(0, nrow(full))

# Ticket Feature에서 고유한 것들만 추출해서 tickets 벡터에 저장 
tickets <- unique(full$Ticket)

# 반복문을 중첩 활용해서 티켓이 같은 승객들만 추출 후, 각 티켓들의 길이(문자 갯수)를 추출해서 저장한다.
for (i in 1:length(tickets)) {
  current.ticket <- tickets[i]
  party.indexes <- which(full$Ticket == current.ticket)
    # For loop 중첩 
    for (k in 1:length(party.indexes)) {
    ticket.unique[party.indexes[k]] <- length(party.indexes)
    }
  }

# 위에서 계산한 ticket.unique 을 파생변수로 저장 
full$ticket.unique <- ticket.unique

# ticket.unique에 따라 세가지 범주로 나눠서 ticket.size 변수 생성 
full <- full %>% 
  mutate(ticket.size = case_when(ticket.unique == 1 ~ 'Single',
                                 ticket.unique < 5 & ticket.unique >= 2 ~ "Small",
                                 ticket.unique >= 5 ~ "Big"),
         ticket.size = factor(ticket.size,
                              levels = c("Single", "Small", "Big")))


4.6 Embarked


결측치(Missing value, NA)가 2개 있던 feature 입니다. Embarked의 경우 3개 범주 중에서 가장 최빈값인 S로 치환하도록 합니다.


full$Embarked <- replace(full$Embarked,               # 치환할 Data$feature 지정
                         which(is.na(full$Embarked)), # 결측치들만 찾기
                         'S')                         # 치환할 값 지정 


4.7 Fare


Fare의 경우 결측치가 유일하게 1개 있었습니다.

위에서 본 히스토그램(Chapter 3.5 Fare)을 바탕으로 결측치를 0으로 치환해줍니다.


full$Fare <- replace(full$Fare, which(is.na(full$Fare)), 0)


여기까지 하시면 데이터 전처리가 모두 끝난 것입니다.

다음은 지금까지 만든 파생변수들을 탐색하면서 모델 생성에 사용할 변수들을 선택하는 과정입니다.

즉, Feature selection 이라고 보시면 됩니다.




5. Relationship to target feature Survived & Feature selection


본격적인 시각화에 앞서, 여기서는 각 변수들이 생존율과 얼마나 연관성이 높은지를 보는것이 목적이기 때문에 full data 전체를 사용하지 않고, 생존과 사망여부를 알 수 있는 train data set만 사용하였습니다.

그리고 위에서 사용한 plot들이 그대로 중복된 경우도 있으니 참고하시기 바랍니다.


5.0 Data set split

먼저 아래 코드를 이용해서 전처리가 끝난 full data를 train, test로 분할합니다.

# feature selection 전이므로 우선은 모든 변수들을 선택합니다.
train <- full[1:891, ]

test <- full[892:1309, ]


5.1 Pclass


train %>% 
  ggplot(aes(Pclass, fill = Survived)) + 
  geom_bar(position = "fill") +
  # plot 테마 설정 : 조금 더 선명한 색깔로 변환해준다.
  scale_fill_brewer(palette = "Set1") +
  # Y axis setting 
  scale_y_continuous(labels = percent) +
  # x, y 축 이름과 plot의 main title, sub title 설정 
  labs(x = "Pclass", y = "Rate",
       title = "Bar plot", subtitle = "How many people survived in each Pclass?")


5.2 Sex


Chapter 3.6 Sex와 동일합니다.


mosaicplot(Survived ~ Sex,
           data = train, col = TRUE,
           main = "Survival rate by passengers gender")


5.3 Embarked


train %>% 
  ggplot(aes(Embarked, fill = Survived)) +
  geom_bar(position = "fill") + 
  scale_fill_brewer(palette = "Set1") +
  scale_y_continuous(labels = percent) +
  labs(x = "Embarked", y = "Rate",
       title = "Bar plot", subtitle = "How many people survived in each Embarked?")


5.4 FamilySized


train %>% 
  ggplot(aes(FamilySized, fill = Survived)) + 
  geom_bar(position = "fill") + 
  scale_fill_brewer(palette = "Set1") + 
  scale_y_continuous(labels = percent) +
  labs(x = "FamilySized", y = "Rate",
       title = "Bar plot", subtitle = "Survival rate by FamilySized")


동승한 인원수에 따라 생존율에 차이가 있고 FamilySizedSurvived는 비선형 관계임을 알 수 있습니다.

5.5 Age.Group


train %>% 
  ggplot(aes(Age.Group, fill = Survived)) + 
  geom_bar(position = "fill") + 
  scale_fill_brewer(palette = "Set1") + 
  scale_y_continuous(labels = percent) +
  labs(x = "Age group", y = "Rate",
       title = "Bar plot", subtitle = "Survival rate by Age group")


5.6 title


train %>% 
  ggplot(aes(title, fill = Survived)) + 
  geom_bar(position = "fill") + 
  scale_fill_brewer(palette = "Set1") + 
  scale_y_continuous(labels = percent) +
  labs(x = "title", y = "Rate",
       title = "Bar plot", subtitle = "Survival rate by passengers title")


5.7 ticket.size


train %>% 
  ggplot(aes(ticket.size, fill = Survived)) + 
  geom_bar(position = "fill") + 
  scale_fill_brewer(palette = "Set1") + 
  scale_y_continuous(labels = percent) +
  labs(x = "ticket.size", y = "Rate",
       title = "Bar plot", subtitle = "Survival rate by ticket.size")


5.8 Description of actual used features


지금까지 생성한 파생변수들이 모두 유용함을 알았으니 실제로 사용할 변수들만 선택해서 저장하도록 합니다.

아래 표는 실제 선택한 변수들에 대한 간단한 설명들입니다.

변수명 Type 설명
Survived factor Target feature, 생존 == 1, 사망 == 0
Sex factor 성별, male or female
Pclass factor 선실 등급, 1등급(1), 2등급(2), 3등급(3)
Embarked factor 승선항, 사우샘프턴(S), 셸부르(C), 퀸즈타운(Q)
FamilySized factor 가족의 규모, SibSpParch를 이용해서 만든 파생변수, 범주는 3개
Age.Group factor 연령대, Age를 이용해서 만든 파생변수, 범주는 4개
title factor 이름의 일부분, Name을 이용해서 만든 파생변수, 범주는 5개
ticket.size factor 티켓의 고유한 부분의 길이, ticket을 이용해서 만든 파생변수, 범주는 3개


# Id number 제외하고 실제로 사용할 7개 입력변수와 1개의 타겟변수를 선택, 저장 
train <- train %>% 
  select("Pclass", "Sex", "Embarked", "FamilySized",
         "Age.Group", "title", "ticket.size", "Survived")

# Submit을 위해서 Id 열벡터 추출해서 ID에 저장 
ID <- test$PassengerId

# Id와 Survived를 제외한 나머지 6개 변수들을 선택, 저장 
test <- test %>% 
  select("Pclass", "Sex", "Embarked", "FamilySized",
         "Age.Group", "title", "ticket.size")




6. Machine learning model generation


본격적으로 train data set을 이용해서 기계학습 모델을 생성할 차례입니다.

원래는 train, validation, test data set들을 먼저 만들고 다양한 모델들을 생성 한 후에 교차검증(CV, Cross Validation)를 거쳐서 최종 모델을 선택하는게 맞지만 여기서는 그런 과정들을 생략하고 RandomForest 만을 생성한 뒤에 test data를 예측(추정) 해보고 competitionSubmit할 data를 만드는 것 까지 해보겠습니다.


6.1 Random Forest model generation


titanic.rf <- randomForest(Survived ~ ., data = train, importance = T, ntree = 2000)


6.2 Feature importance check


importance(titanic.rf)
##                     0        1 MeanDecreaseAccuracy MeanDecreaseGini
## Pclass      48.165493 53.72632             65.97967        36.304460
## Sex         52.035670 36.23593             55.90245        55.458197
## Embarked    -7.223648 36.52360             26.19142         9.289104
## FamilySized 31.452820 31.02720             48.10491        17.722879
## Age.Group   16.424753 25.63863             29.49344        10.232071
## title       49.022273 42.33142             57.01137        74.670542
## ticket.size 41.955244 37.08541             60.88671        23.274933
varImpPlot(titanic.rf)

6.3 Predict test data and create submit data


# Prediction 
pred.rf <- predict(object = titanic.rf, newdata = test, type = "class")

# Data frame generation 
submit <- data.frame(PassengerID = ID, Survived = pred.rf)

# Write the submit data frame to file : setwd()로 지정해놓은 폴더에 csv로 생성됩니다.
write.csv(submit, file = './titanic_submit.csv', row.names = F)


여기까지가 R을 이용한 Titanic 데이터 분석입니다.

긴 글 읽어주셔서 감사하고 많은 피드백과 조언들 부탁드립니다.




7. Reference : 참고문헌

  1. ‘Megan Risdal’ titanic kernel

  2. ‘Heads or Tails’ titanic kernel

  3. Hitesh palamada - Head Start for Data Scientist

  4. 나무위키 - RMS 타이타닉

  5. R을 활용한 데이터 과학(인사이트), Garrett Grolemund, Hadley Wickham 지음, 김설기, 최혜민 옮김, 978-89-6626-235-9 : tidyverse를 이해하기 가장 좋은 책

  6. tidyr::gather() - Software Carpentry

  7. 머신러닝 탐구생활(비제이퍼블릭), 정권우 지음, 979-11-86697-69-6 : Data 분석 노트(표) 작성하는데 많은 도움을 받았습니다.