Kaggle에 있는 타이타닉 분석을 해보려 한다. 분석의 목표는 train데이터로 모델을 만들어 test 데이터의 Survived(생존유무) 컬럼을 만들기 위함이다. 각의 모델에 대한 기술적인 설명은 본인의 블로그에 명시해 두려고 한다.
- 기술 블로그: 류동균의 R 공부방입니다.
타이타닉을 분석하기에 앞서 약간의 도메인지식이 분석에 도움을 준다. 도메인지식은 아주 간단하게 다음고 같다. 타이타닉이 침몰한 날은 1912년이다. 당시에는 “Lady First”라는 개념이 사람들에게 전체적으로 있었으며, 그런이유인지 가라앉은 당시에 승무원들은 어린아이와 여자부터 우선적으로 구조를 했다고 한다. 타이타닉호에는 부자, 일반인, 가난한 이민자 등 여러부류의 사람이 있었으며 객실은 1등급, 2등급, 3등급이 있었다고 한다. 가난한 이민자는 주로 3등급 객실에서 머물렀다고 한다.
모델을 만들기에 앞서 전반적인 데이터를 확인하기 위해 탐색적 자료 분석(EDA)가 필요하다. 컬럼의 개수는 몇개인지 어떠한 컬럼이 있는지 또한 컬럼별 분포라던가 데이터가 어떠한 유형으로 이루어져있는지를 확인하기위해 매우 중요하다고 할 수 있다. 또한 필요한 컬럼과 필요하지 않는 컬럼을 나누는 중요한 작업이라고 할 수 있다. 그럼 탐색적 자료 분석을 시작해보자.
library(dplyr) # data handling
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(ggplot2) # data visualization
library(randomForest) # Random Forest
## randomForest 4.6-14
## Type rfNews() to see new features/changes/bug fixes.
##
## Attaching package: 'randomForest'
## The following object is masked from 'package:ggplot2':
##
## margin
## The following object is masked from 'package:dplyr':
##
## combine
library(rpart) # Decision Tree
library(rpart.plot) # data visualization
# train data
train_set <- read.csv("train.csv")
# test data
test_set <- read.csv("test.csv")
# 제출양식
gender_submission <- read.csv('gender_submission.csv')
우선 이해를 돕기위해 컬럼이 의미하는 바를 간단하게 알아볼 필요가 있다. 컬럼의 의미들은 다음과 같다.
PassengerId : 탑승객의 ID(인덱스와 같은 개념)
Survived : 생존유무(0은 사망 1은 생존)
Pclass : 객실의 등급
Name : 이름
Sex :성별
SibSp : 동승한 형제 혹은 배우자의 수
Parch : 동승한 자녀 혹은 부모의 수
Ticket : 티켓번호
Fare : 요금
Cabin : 선실
Embarked : 탑승지 (C = Cherbourg, Q = Queenstown, S = Southampton)
str()을 실행했을때 데이터의 자료형과 대략적으로 값이 어떻게 들어가 있는지를 확인할 수 있다. summary()를 실행했을때 각 컬럼별 분포, NA값 유무/수, Factor, 최댓값, 최솟값 등 여러 정보들을 알 수 있었다.
str(train_set)
## 'data.frame': 891 obs. of 12 variables:
## $ PassengerId: int 1 2 3 4 5 6 7 8 9 10 ...
## $ Survived : int 0 1 1 1 0 0 0 0 1 1 ...
## $ Pclass : int 3 1 3 1 3 3 1 3 3 2 ...
## $ Name : Factor w/ 891 levels "Abbing, Mr. Anthony",..: 109 191 358 277 16 559 520 629 417 581 ...
## $ 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 : int 1 1 0 1 0 0 0 3 0 1 ...
## $ Parch : int 0 0 0 0 0 0 0 1 2 0 ...
## $ Ticket : Factor w/ 681 levels "110152","110413",..: 524 597 670 50 473 276 86 396 345 133 ...
## $ Fare : num 7.25 71.28 7.92 53.1 8.05 ...
## $ Cabin : Factor w/ 148 levels "","A10","A14",..: 1 83 1 57 1 1 131 1 1 1 ...
## $ Embarked : Factor w/ 4 levels "","C","Q","S": 4 2 4 4 4 3 4 4 4 2 ...
str(test_set)
## 'data.frame': 418 obs. of 11 variables:
## $ PassengerId: int 892 893 894 895 896 897 898 899 900 901 ...
## $ Pclass : int 3 3 2 3 3 3 3 2 3 3 ...
## $ Name : Factor w/ 418 levels "Abbott, Master. Eugene Joseph",..: 210 409 273 414 182 370 85 58 5 104 ...
## $ Sex : Factor w/ 2 levels "female","male": 2 1 2 2 1 2 1 2 1 2 ...
## $ Age : num 34.5 47 62 27 22 14 30 26 18 21 ...
## $ SibSp : int 0 1 0 0 1 0 0 1 0 2 ...
## $ Parch : int 0 0 0 0 1 0 0 1 0 0 ...
## $ Ticket : Factor w/ 363 levels "110469","110489",..: 153 222 74 148 139 262 159 85 101 270 ...
## $ Fare : num 7.83 7 9.69 8.66 12.29 ...
## $ Cabin : Factor w/ 77 levels "","A11","A18",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ Embarked : Factor w/ 3 levels "C","Q","S": 2 3 2 3 3 3 2 3 1 3 ...
summary(train_set)
## PassengerId Survived Pclass
## Min. : 1.0 Min. :0.0000 Min. :1.000
## 1st Qu.:223.5 1st Qu.:0.0000 1st Qu.:2.000
## Median :446.0 Median :0.0000 Median :3.000
## Mean :446.0 Mean :0.3838 Mean :2.309
## 3rd Qu.:668.5 3rd Qu.:1.0000 3rd Qu.:3.000
## Max. :891.0 Max. :1.0000 Max. :3.000
##
## Name Sex Age
## Abbing, Mr. Anthony : 1 female:314 Min. : 0.42
## Abbott, Mr. Rossmore Edward : 1 male :577 1st Qu.:20.12
## Abbott, Mrs. Stanton (Rosa Hunt) : 1 Median :28.00
## Abelson, Mr. Samuel : 1 Mean :29.70
## Abelson, Mrs. Samuel (Hannah Wizosky): 1 3rd Qu.:38.00
## Adahl, Mr. Mauritz Nils Martin : 1 Max. :80.00
## (Other) :885 NA's :177
## SibSp Parch Ticket Fare
## Min. :0.000 Min. :0.0000 1601 : 7 Min. : 0.00
## 1st Qu.:0.000 1st Qu.:0.0000 347082 : 7 1st Qu.: 7.91
## Median :0.000 Median :0.0000 CA. 2343: 7 Median : 14.45
## Mean :0.523 Mean :0.3816 3101295 : 6 Mean : 32.20
## 3rd Qu.:1.000 3rd Qu.:0.0000 347088 : 6 3rd Qu.: 31.00
## Max. :8.000 Max. :6.0000 CA 2144 : 6 Max. :512.33
## (Other) :852
## Cabin Embarked
## :687 : 2
## B96 B98 : 4 C:168
## C23 C25 C27: 4 Q: 77
## G6 : 4 S:644
## C22 C26 : 3
## D : 3
## (Other) :186
데이터를 파악했을때 자료형과 전박적인 데이터의 분포를 알 수 있었다. 이제 데이터 분석을 하기위해 자료형을 알맞게 변환시켜 주고 시각화를 해보자.
데이터를 보았을때 Pclass의 경우 자료형이 int로 되어있는데 이는 1, 2, 3 3가지의 객실등급으로 이루어져 있이때문에 factor로 바꿔주는 것이 좋다고 판단되었다. Name, Ticket, Cabin의 경우 Factor일 필요가 없기때문에 character로 바꿔 주었다.
# 자료형 변환
train_set$Pclass <- train_set$Pclass %>%
as.factor()
train_set$Name <- train_set$Name %>%
as.character()
train_set$Ticket <- train_set$Ticket %>%
as.character()
train_set$Cabin <- train_set$Cabin %>%
as.character()
# train_set$Survived <- train_set$Survived %>% as.factor()
test_set$Pclass <- test_set$Pclass %>%
as.factor()
test_set$Name <- test_set$Name %>%
as.character()
test_set$Ticket <- test_set$Ticket %>%
as.character()
test_set$Cabin <- test_set$Cabin %>%
as.character()
# 데이터 확인
str(train_set)
## 'data.frame': 891 obs. of 12 variables:
## $ PassengerId: int 1 2 3 4 5 6 7 8 9 10 ...
## $ Survived : int 0 1 1 1 0 0 0 0 1 1 ...
## $ Pclass : Factor w/ 3 levels "1","2","3": 3 1 3 1 3 3 1 3 3 2 ...
## $ Name : chr "Braund, Mr. Owen Harris" "Cumings, Mrs. John Bradley (Florence Briggs Thayer)" "Heikkinen, Miss. Laina" "Futrelle, Mrs. Jacques Heath (Lily May Peel)" ...
## $ 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 : int 1 1 0 1 0 0 0 3 0 1 ...
## $ Parch : int 0 0 0 0 0 0 0 1 2 0 ...
## $ Ticket : chr "A/5 21171" "PC 17599" "STON/O2. 3101282" "113803" ...
## $ Fare : num 7.25 71.28 7.92 53.1 8.05 ...
## $ Cabin : chr "" "C85" "" "C123" ...
## $ Embarked : Factor w/ 4 levels "","C","Q","S": 4 2 4 4 4 3 4 4 4 2 ...
str(test_set)
## 'data.frame': 418 obs. of 11 variables:
## $ PassengerId: int 892 893 894 895 896 897 898 899 900 901 ...
## $ Pclass : Factor w/ 3 levels "1","2","3": 3 3 2 3 3 3 3 2 3 3 ...
## $ Name : chr "Kelly, Mr. James" "Wilkes, Mrs. James (Ellen Needs)" "Myles, Mr. Thomas Francis" "Wirz, Mr. Albert" ...
## $ Sex : Factor w/ 2 levels "female","male": 2 1 2 2 1 2 1 2 1 2 ...
## $ Age : num 34.5 47 62 27 22 14 30 26 18 21 ...
## $ SibSp : int 0 1 0 0 1 0 0 1 0 2 ...
## $ Parch : int 0 0 0 0 1 0 0 1 0 0 ...
## $ Ticket : chr "330911" "363272" "240276" "315154" ...
## $ Fare : num 7.83 7 9.69 8.66 12.29 ...
## $ Cabin : chr "" "" "" "" ...
## $ Embarked : Factor w/ 3 levels "C","Q","S": 2 3 2 3 3 3 2 3 1 3 ...
train 데이터의 컬럼별 시각화를 통해 여러가지 정보들을 알아보자. 시각화한 그래프로 부터 얻은 주요한 정보는 도메인지식에서 설명한 것과 같이 여성이 남성보다 매우높은 생존율을 보였고, Pclass의 등급이 높을수록 높은 생존율을 보여 모델을 만들때 유의미한 데이터라고 생각됬다. Embraked라는 컬럼의 경우 탑승지를 의미하는 컬럼인데 S라는 factor가 생존과 사망의 비율이 꽤 많은 차이가 있었지만 탑승지가 생존과 사망에 영향을 주기에는 어렵다고 판단되어 제외하도록했다.
# install.packages("gridExtra")
# library(gridExtra)
ggplot(train_set, aes(x = Survived, fill=Sex))+geom_bar()+
ggtitle("Suvival by Sex")
ggplot(train_set, aes(x = Survived, fill=Pclass))+geom_bar()+
ggtitle("Suvival by Pclass")
ggplot(train_set, aes(x = Survived, fill=factor(SibSp)))+geom_bar()+
ggtitle("Suvival by SibSp")
ggplot(train_set, aes(x = Survived, fill=factor(Parch)))+geom_bar()+
ggtitle("Suvival by Parch")
ggplot(train_set, aes(x = Survived, fill=factor(Embarked)))+geom_bar()+
ggtitle("Suvival by Embarked")
ggplot(train_set, aes(x = Survived, fill=factor(Age)))+geom_bar()+
ggtitle("Suvival by Age")
# grid.arrange(plot1, plot2,plot3,plot4,plot5,plot6, nrow=3, ncol=2)
도메인지식에서 알 수 있드시 어린아이를 먼저 구출했다고 한다. 그런데 2)에서 데이터를 시각화 했을때 조금 알아보기 어려운 부분이 있다. 그렇기때문에 승객들의 나이를 9세이하, 10대, 20대 …… 80대 까지 그룹으로 구분지어 그 데이터를 확인해보도록 하자.
# age_df 생성
age_df <- data.frame()
k <- 0
for (i in 0:8){
col_name <- paste(k,"~",k+9)
recent_df <- filter(train_set, Age >= k & Age <= k+9)
recent_survived_ratio <- sum(recent_df$Survived) / nrow(recent_df)
survived_df <- data.frame(age = c(col_name),
survived_ratio = c(recent_survived_ratio),
number_of_age = c(nrow(recent_df)))
age_df <- rbind(age_df, survived_df)
k <- k+10
}
# 데이터 시각화
# 나이대 별 생존률 ggplot 점 + 선
ggplot(age_df, aes(x = age, y = survived_ratio, group = 1)) +
geom_point(stat = "identity") +
geom_line(stat = "identity")
# 나이대 별 인구수 ggplot 막대그래프
ggplot(age_df, aes(x = age, y = number_of_age)) + geom_bar(stat="identity")
Titanic train_set 데이터의 Age 컬럼에는 NA값이 177개가 존재한다. 모델을 생성하기 위해서는 이러한 NA 결측치를 제거하거나 특정한 값으로 채워 해주어야한다. 이것을 판단하는 기준은 NA값이 비율이 얼마나되는지 NA값이 있는 컬럼의 중요도가 얼마나 높은지를 생각해야하고 또한 NA값을 다른값으로 채울때는 중간값, 평균값, 특정함수로 나온 값 등 어떠한 값으로 채워 줄 것인지를 판단해야 한다. train_set에는 891개의 row가 있다. 그중에서 Age의 NA값을 가진 컬럼의 수는 177개이다. 만약 NA값의 수가 700개라고 가정하고 Age가 데이터 분석을 하는대에 있어서 아주 중요하다고 해보자. 그러면 어떻게해야하는가? 결과부터 말하자면 데이터 분석을 포기해야한다. NA값에 다른값을 임의로 채워넣는 순간 그것은 raw data가 아닌 예측된 데이터이기 때문이다. 그러한 관점으로 titanic 데이터 분석을 해보자. 사실 뒤에나올 Random Forest에서 컬럼의 importance를 시각화하여 보여줄 예정인데 자료에따르면 Age는 아주연관이 없는것은 아니지만 아주 크게 중요한 데이터는 아니다. 이번 분석에서는 NA값의 비율이 약 20% 정도로 아주 큰 비율은 아니다. 원래는 여성의 경우만 Name 컬럼에 있는 “Ms”, “Mrs”,“Miss”와 같이 기혼인 여성과 미혼인 여성의 차이와 동승한 자녀/부모 의 유무로 값을 대치해주려 했지만 Parch컬럼에 동승한 자녀/부모 의 수가 합산이 되어 자녀인지 부모인지 식별이 불가능한 관계로 Age는 NA값은 NA값을 제외한 전체의 평균Age로 대체하기로 했다.
# train/test set NA index
train_index_NA <- which(is.na(train_set$Age))
test_index_NA <- which(is.na(test_set$Age))
# NA값을 전체나이의 평균값으로 대치
train_set[train_index_NA,]$Age <- mean(train_set$Age, na.rm = T)
test_set[test_index_NA,]$Age <- mean(test_set$Age, na.rm = T)
# NA값 확인
sum(is.na(train_set$Age))
## [1] 0
sum(is.na(test_set$Age))
## [1] 0
데이터에 대한 구조파악, 자료형 변환을 마쳤으니 이제 train_set데이터로 test_set을 분류할 모델을 만들 차례이다. 모델은 Random Forest, Decision Tree로 만들어보도록 하자. 그전에 Survived컬럼의 자료형을 Factor로 바꾸어보자.
train_set$Survived <- train_set$Survived %>% as.factor()
Dicision Tree란 나무에 가지를 치듯이 여러개의 데이터를 노드에서 가지를 쳐나가 분류를 하는 기법이다. 자세한 설명은 기술블로그를 참조하길 바란다.
# Decision Tree model
rpart_m <- rpart(Survived ~ Sex + Age + Pclass, data = train_set)
# Decision Tree visualization
prp(rpart_m, type = 4, extra = 2, digits = 3)
# predict test_set
rpart_p <-predict(rpart_m, newdata = test_set, type = "class")
Random Forest란 앙상블 기법 중 하나로 여러가지 의사결정 나무를 집합하여 숲을 이룬 것과 비슷하다고 볼 수 있다. 자세한 내용은 기술블로그를 참조하길 바란다. Random Forest 모델을 만들때 중요도 라는 것을 varImpPlot을 통해 시각화하여 확인 할 수 있는데, 이는 각 컬럼이 분류에 대한 의사결정을 하는대에 얼마나 중요한지를 의미한다. 2) 데이터시각화, 도메인지식 에서도 알 수 있드시 Sex가 가장 높은 중요도를 보여주었다.
# Random Forest model
rf_m <- randomForest(Survived ~ Sex + Age + Pclass , data = train_set)
# importance
rf_info <- randomForest(Survived ~ Sex + Age + Pclass , data = train_set, importance = T)
importance(rf_info)
## 0 1 MeanDecreaseAccuracy MeanDecreaseGini
## Sex 48.68271 60.53798 56.66827 103.79682
## Age 16.06754 14.58783 21.24966 20.95221
## Pclass 20.11466 24.12838 24.35693 37.25917
varImpPlot(rf_info)
# predict test_set
rf_p <- predict(rf_m, newdata = test_set, type = "class")
이제 생성한 모델을 Submit 양식에 맞추어 dataframe으로 만들어 결과를 제출해보자. Submit양식은 처음에 받은 gender_submission.csv에서 확인 할 수 있다. 컬럼은 PassengerID, Survived 두가지이다. 제출한 결과 score는 다음과 같다.
Decision Tree : 0.73684
Random Forest : 0.75119
두 모델 모두 70%대로 비슷한 신뢰도를 보였으며 Random Forest 모델이 조금더 높았다.
# Decision Tree Submit Data
titatic_rpart <- cbind(PassengerID = test_set$PassengerId, Survived = rpart_p %>%
as.character() %>%
as.numeric()) %>% as.data.frame()
write.csv(titatic_rpart, file = 'titanic_rpart_sumbit.csv', row.names = F)
# Random Forest Submit Data
titatic_rf <- cbind(PassengerID = test_set$PassengerId, Survived = rf_p %>%
as.character() %>%
as.numeric()) %>% as.data.frame()
write.csv(titatic_rf, file = 'titanic_rf_sumbit.csv', row.names = F)
이렇게 Kaggle에서 ’타이타닉 생존자 분석’이라는 주제로 데이터 분석을 해보았다. 신뢰도는 70%대로 아주 낮은 수치는 아니지만 조금은 아쉬운 수치이다. 실제 데이터를 가지고 시도해보는 흥미로운 작업이였고 기회가 된다면 공부를 좀더 해서 몇년뒤에 다시 시도해보아 현재의 나와 미래의 내가 얼마나 달라졌을지 Test해보는 시간도 가져보고 싶다.