본문 바로가기

Data Science : Project/개인 프로젝트

캐글 타이타닉 생존자 예측

반응형

 

캐글 연습은 타이타닉으로 시작!

 

타이타닉은 데이터분석 수업을 들으면서 많이 접한 데이터이긴 한데, 나 혼자서 처음부터 끝까지 제대로 해 본적은 없었다.

그리고 결측치 처리에서 단순히 평균으로 대체하는 것으로 배우고 지나갔는데, 좀 더 꼼꼼하게 처리하면 분석 결과가 더 좋아지지 않을까 생각해서 결측치 처리 방식을 고민하고 시도해보고 싶었다.

 

R과 파이썬 모두 진행했고, 그 중 submission 결과가 더 좋은 R코드를 기반으로 설명했다.

진행하면서 생기는 이슈에 대해서는 R과 파이썬 모두 설명했다. 생각보다 기초적인 이슈가 많이 발생하네.

 

 

 


데이터 설명

survival: 생존 여부. 0은 사망, 1은 생존.

pclass: 좌석 등급. 1등급이 제일 높다.

sex: 성별.

age: 나이.

sibsp: 타이타닉 호에 함께 탑승한 형제/자매 수. siblings+spouses.

parch: 타이타닉 호에 함께 탑승한 부모/자식 수. parents+children.

ticket: 티켓 번호.

fare: 승객이 지불한 요금.

cabin: 객실 번호.

embarked : 승선한 장소.

 

 

 


데이터 전처리

titanic_test$Survived <- c(NA)
titanic_raw <- rbind(titanic_train, titanic_test)

train, test data를 합쳐서 Survived 중에 NA값이 있고, 이는 전부 test data임을 합치기 전에 확인하였다.

 

전처리를 위해 생존여부에 따라 각 컬럼을 시각화하였다.

 

 

1. Pclass

titanic_raw$Pclass <- as.factor(titanic_raw$Pclass)
qplot(Survived, data = titanic_raw, fill = Pclass)

범례를 확인했을 때, Pclass의 결측치는 없다.

1등석 사람들은 생존자 비율이 높지만, 3등석은 사망자 비율이 높은 것을 알 수 있다.

 

 

2. Sex

qplot(Survived, data = titanic_raw, fill = Sex)

범례를 확인했을 때, Sex의 결측치는 없다.

여성에 비해 남성의 사망자 비율이 높은 것을 알 수 있다.

 

 

3. Sibsp, Parch

titanic_raw$family <- titanic_raw$SibSp + titanic_raw$Parch
titanic_raw$family <- ifelse(titanic_raw$family == 0, 0, 1)
qplot(Survived, data = titanic_raw, fill = as.factor(family))

분석에 용이하게 하기 위해 Sibsp와 Parch 컬럼을 합쳐 Family 컬럼으로 만들었고, 가족이 함께 탑승했냐 아니냐로 나누어 0과 1로 요약하였다.

범례를 확인했을 때, Family의 결측치는 없다.

가족이 없는 경우의 사망자 비율이 생존자 비율보다 높은 것을 알 수 있다.

 

 

4. Fare

boxplot(titanic_raw$Fare)$stats

Fare의 분포를 확인했을 때 100 이상 값들은 이상치로 분류되고, 특이한 경우라고 판단되어 Fare를 10 단위로 나눠서 분석에 용이하게 변경, 100 이상은 10으로 통일하였다.

titanic_raw$Fare_group <- titanic_raw$Fare%/%10
titanic_raw$Fare_group <- ifelse(titanic_raw$Fare_group > 10, 10, titanic_raw$Fare_group)
titanic_raw$Fare_group <- as.factor(titanic_raw$Fare_group)

이를 시각화한 결과는 다음과 같다.

qplot(Survived, data = titanic_raw, fill = Fare_group)

 

 

5. Embarked

qplot(Survived, data = titanic_raw, fill = Embarked)

범례를 확인하니 C, Q, S 이외에 값이 존재한다.

S가 C, Q에 비해 높은 비율을 차지해서 결측치를 S로 처리하였다. 이때 C=2, Q=3, S=4이다.

titanic_raw$Embarked <- ifelse(titanic_raw$Embarked == "", "4", titanic_raw$Embarked)
titanic_raw$Embarked <- as.factor(titanic_raw$Embarked)

qplot(Survived, data = titanic_raw, fill = Embarked)

 

1차적으로 컬럼 정리를 해줬다.

분석에 사용하지 않는 Name, Ticket, Cabin, 그리고 Family로 합쳐서 필요없어진 SibSp, Parch를 제거하였다.

raw <- dplyr::select(titanic_raw, -Name, -SibSp, -Parch, -Ticket, -Cabin)

 

 

6. Age

qplot(Age, data = titanic_raw)

boxplot(titanic_raw$Age)$stats

Age의 분포를 확인하면 위와 같고, 시각화한 결과로는 이상치는 없었으나 결측치는 확인할 수 없었다.

Age_NA <- is.na(raw$Age)
raw_na <- filter(raw, Age_NA)
raw_not_na <- filter(raw, !Age_NA)

결측치를 해결하는 방법으로 공통점을 찾는 방법을 이용했다.

다른 컬럼의 값이 모두 같다면 Age의 값도 같을 확률이 높지 않을까 생각했다. 다른 모든 컬럼이 같은 경우가 있고, Fare 이외의 모든 컬럼이 같은 경우가 있어 따로 처리해주었다.

check <- c()
for(i in (1:263)){ # train:177 test:86
  for(j in (1:1046)){ # train:714 test:332
    if ((raw_na$Pclass[i] == raw_not_na$Pclass[j])
        & (raw_na$Sex[i] == raw_not_na$Sex[j])
        & (raw_na$Embarked[i] == raw_not_na$Embarked[j])
        & (raw_na$family[i] == raw_not_na$family[j])
        & (raw_na$Fare_group[i] == raw_not_na$Fare_group[j])){
      check <- c(check, j)
      break
    }
    else{
      if(j==1046) check <- c(check, 0)
    }
  }
}
raw_na$has_same <- check
check2 <- c()
for(i in (1:263)){
  for(j in (1:1046)){
    if ((raw_na$Pclass[i] == raw_not_na$Pclass[j])
        & (raw_na$Sex[i] == raw_not_na$Sex[j])
        & (raw_na$Embarked[i] == raw_not_na$Embarked[j])
        & (raw_na$family[i] == raw_not_na$family[j])){
      check2 <- c(check2, j)
      break
    }
    else{
      if(j==1046) check2 <- c(check2, 0)
    }
  }
}
raw_na$notsame_fare <- check2
for(i in (1:263)){
  raw_na$Age[i] <- ifelse(raw_na$has_same[i] != 0, raw_not_na$Age[raw_na$has_same[i]], raw_not_na$Age[raw_na$notsame_fare[i]])
}

raw_na <- dplyr::select(raw_na, -has_same, -notsame_fare)

raw <- rbind(raw_na, raw_not_na)

 

나이의 분포를 확인하기 위해 Age_group 컬럼을 추가적으로 생성하였다.

raw$Age_group <- ifelse(raw$Age < 20, 0,
                                ifelse(raw$Age < 30, 1,
                                       ifelse(raw$Age < 40, 2,
                                              ifelse(raw$Age < 65, 3, 4))))
raw$Age_group <- as.factor(raw$Age_group)
qplot(Survived, data = raw, fill = Age_group)

어린이와 장년의 경우 사망자와 생존자의 비율이 거의 비슷하고, 청년의 경우 사망자의 비율이 더 높다.

 

 

최종 컬럼 정리를 진행하였다.

시각화에 사용하기 위해 만든 Age_group과, Fare_group을 생성했기 때문에 필요없어진 Fare 컬럼을 제거하였다.

전처리를 위해 train과 test data를 병합했었는데, 다시 나누기 용이하게 데이터를 PassengerId 기준으로 정렬하였다.

정렬 과정에서 생긴 rownames를 제거하였다.

titanic_data <- dplyr::select(raw, -Age_group, -Fare)
titanic_data <- titanic_data[c(order(titanic_data$PassengerId)),]
rownames(titanic_data) <- NULL

 

 

 


모델 구현 및 선택

모델을 구현하기 전, 전처리를 위해 합쳐놓았던 데이터를 다시 train과 test data로 나누었다.

Survived의 NA 여부로 train과 test data를 나누었고, Survived와 다른 컬럼을 다시 한 번 나누었다.

check_NA <- is.na(titanic_data$Survived)
train_data <- filter(titanic_data, !check_NA)
train_num <- train_data[,1]
train_data <- train_data[,-1]
test_data <- filter(titanic_data, check_NA)
test_num <- test_data[,1]
test_data <- test_data[,-1]

 

분석 방법은 아래 다섯가지를 선택하였고, 이 중 결과가 가장 좋은 모델을 최종 선택했다.

  • Logistic Regression
  • Random Forest
  • kNN
  • Naive Bayesian Classifier
  • SVM

 

 

1. Logistic Regression

model_logistic <- glm(Survived ~ ., data = train_data, family = "binomial")
summary(model_logistic)

 

p-value에 의해 Pclass, Sex, Age, Embarked4, Fare_group4가 통계적으로 유의한 것을 알 수 있다.

Estimate로 주요한 내용을 확인하면,

  • Pclass가 3인 사람은 그렇지 않은 사람에 비해 생존 확률의 Log가 2.37만큼 감소하게 된다. 이는 Pclass가 2일 때보다 확실히 생존확률이 낮음을 알 수 있다.
  • Sex가 male인 사람은 그렇지 않은 사람(=female)에 비해 생존 확률의 Log가 2.57만큼 감소하게 된다.
  • Embarked4(=S, Southampton)인 사람은 그렇지 않은 사람에 비해 생존 확률의 Log가 0.66만큼 감소하게 된다.

 

 

ANOVA로 변수 추가를 통해 모델의 성능이 얼마나 나아졌는지 확인해보자.

anova(model_logistic, test="Chisq")

Null일 때 Resid. Dev의 값은 1186.66이고, 이 값과의 차이를 통해 변수가 추가되었을 때 모델의 성능이 얼마나 추가되었는지 확인할 수 있다.

Pclass, Sex, Age가 추가되었을 때 Resid. Dev의 값이 800 이상을 유지하는 것을 확인할 수 있다.

 

 

로지스틱 회귀분석에서는 Mcfadden R2으로 모델 fit을 확인할 수 있다.

-> psc1 패키지의 pR2를 사용한다!

install.packages("pscl") # pR2를 사용하려고(모델해석)
library(pscl)
pR2(model_logistic)

r2CU는 R2값이다. R2값이 0.5인 것으로 보아 모델이 train data의 분산의 약 50% 정도를 설명한다고 할 수 있다.

 

 

2. Random Forest

install.packages("randomForest")
install.packages("caret", dependencies = TRUE) # e1071 오류 생겨서 dependencies 추가함

library(MASS)
library(randomForest)
library(caret)


set.seed(10)
train_data$Survived <- as.factor(train_data$Survived)
model_rf = randomForest(Survived ~ Pclass + Sex + Age + Embarked + family + Fare_group, data = train_data, mtry = floor(sqrt(7)), ntree = 1000, proximity = T)
model_rf

table(train_data$Survived, predict(model_rf)) #(row,col)
importance(model_rf)

pred_rf <- predict(model_rf)
summary(pred_rf)
train_rf <- train_data$Survived
summary(train_rf)
caret::confusionMatrix(pred_rf, train_rf)

 

 

3. kNN

## knn은 실행전에 데이터값이 전부 숫자여야 한다
knn_train <- train_data
knn_test <- test_data
## level만 바꾸면 데이터는 저절로 바뀐다
levels(knn_train$Sex) <- c(1,2) # 1:female, 2:male
levels(knn_train$Embarked) <- c(2,3,4) #2:C 3:Q 4:S
levels(knn_test$Sex) <- c(1,2)
levels(knn_test$Embarked) <- c(2,3,4)
knn_valid <- knn_train[713:891,]
knn_train <- knn_train[1:712,]

knn_train_x <- knn_train[,-1]
knn_train_y <- knn_train[,1]
knn_valid_x <- knn_valid[,-1]
knn_valid_y <- knn_valid[,1]
library(class)

set.seed(1000)
knn_1 <- knn(train = knn_train_x, test = knn_valid_x, cl = knn_train_y, k = 1)
knn_accuracy_1 <- sum(knn_1 == knn_valid_y) / length(knn_valid_y)
knn_accuracy_1 ## 0.7374

# 1~100까지의 k로 knn 적용 + 분류정확도 계산하기
knn_accuracy_k <- NULL
for(kk in c(1:100)){
  set.seed(1000)
  knn_k <- knn(train = knn_train_x, test = knn_valid_x, cl = knn_train_y, k = kk)
  knn_accuracy_k <- c(knn_accuracy_k, sum(knn_k == knn_valid_y) / length(knn_valid_y))
}
# 분류정확도 데이터생성
knn_valid_k <- data.frame(k=c(1:100), accuracy = knn_accuracy_k)
# 분류정확도 그래프
plot(formula = accuracy ~ k, data = knn_valid_k, type = "o", pch = 20, main = "validation - optimal k")
# 그래프에 몇 k인지 라벨링
with(knn_valid_k, text(accuracy ~ k, labels = rownames(knn_valid_k), pos = 1, cex = 0.7))

min(knn_valid_k[knn_valid_k$accuracy %in% max(knn_accuracy_k), "k"]) # 9

knn_accuracy_k

 

 

4. Naive Bayesian Classifier

library(e1071)
library(caret)

nb_model <- naiveBayes(Survived ~ ., data = train_data)
nb_model
nbpred <- predict(nb_model, train_data, type="class")
length(nbpred)
length(train_data$Survived)
caret::confusionMatrix(nbpred, train_data$Survived)

 

 

5. SVM

library(e1071)

svm_model <- svm(Survived ~ ., data = train_data)
svm_model

table(predict(svm_model, train_data), train_data$Survived)

 

 

 

랜덤포레스트의 결과가 제일 좋아서 이 결과를 submission.csv로 저장하였다.

 

 

 


제출

완성한 파일을 제출하여 얼마나 정확하게 예측하였는지 확인해보자.

https://www.kaggle.com/c/titanic

 

해당 주소로 들어가서 본인의 계정으로 kaggle에 로그인한다.

그 뒤에 Submit Predictions를 클릭.

 

 

 

Step1에서 생성한 결과파일을 화살표 부분에 드래그해넣고

Step2에서 파일에 대한 설명을 넣고(옵션)

Make Submission 하면 내가 맞춘 비율은 얼마인지, 상위 몇 %인지 나온다.

 

여기서 겪은 시행착오!

1) Column was not expected

컬럼명이 다르게 들어가 있어서 그렇다! 문제에서 제시하는 컬럼명은 PassengerId, Survived 이다.

    저장과정에서 컬럼명이 바뀌었다면 csv 파일에서 이를 수정해줘야 한다.

2) 에러는 안 났는데 Score가 0이다

→ 아무리 못 맞춰도 0일수가 있나? 이는 파이썬으로 진행할 때 생긴 문제인데 결과가 숫자형이 아니라서 그렇다!

    결과 컬럼을 astype(int) 처리해줘야 한다!

 

 

 

나의 score는 0.765.

위의 코드를 돌려보면 너무 train data에 overfitting 되는 경향이 있는데 그렇더라도 다섯가지 방법을 모두 kaggle 사이트에 시도해보았을 때 이 결과가 제일 좋은 결과였다.

 

 

 

+) 사담

도대체 score가 1인 사람들은 어떻게 한 걸까? 하고 그 사람의 코드를 뜯어보고 실행해도 오히려 나보다 낮은 결과가 나오곤 했다.

뭐가 잘못됐을까? 하고 찾아보다가 어떤 사람의 리뷰에 '이게 정답 csv 파일이고, 이걸 업로드해' 라고 되어있었다.

이게 뭐야? 했는데 밑의 댓글들까지 읽어보니 이게 오래된 Competition이라면 생기는 문제고, 1은 대부분 이런 사람들이라고 말하고 있었다.

그렇구나! 1을 목표로 하는건 당연히 안되는 일이구나! 그렇지 어떻게 100%를 맞추겠어! 라고 생각하면서 맘이 한결 편해진 순간이었다... 그런데 진짜로 1을 만들어내는 사람이 있을까?

 

 

 

 

 

반응형