저자 책 웹페이지: https://dataninja.me/ipds-kr/
https://goo.gl/hmyTre 혹은 https://archive.ics.uci.edu/ml/datasets.html 에서 고차원 분류분석 데이터를 찾아서 로지스틱 분류분석을 실행하고, 결과를 슬라이드 10여 장 내외로 요약하라.
UCI 보다는 캐글에 있는 다음 자료를 분석해 보자. https://www.kaggle.com/ludobenistant/hr-analytics
일단은 필수패키지인 tidyverse, 그리고 머신러닝을 위한 몇가지 패키지를 로드하자. (로딩 메시지를 감추기 위해 suppressMessages() 명령을 사용.)
# install.packages("tidyverse")
suppressMessages(library(tidyverse))
# install.packages(c("ROCR", "MASS", "glmnet", "randomForest", "gbm", "rpart", "boot"))
suppressMessages(library(gridExtra))
suppressMessages(library(ROCR))
suppressMessages(library(MASS))
suppressMessages(library(glmnet))
## Warning: package 'glmnet' was built under R version 3.4.2
suppressMessages(library(randomForest))
suppressMessages(library(gbm))
suppressMessages(library(rpart))
suppressMessages(library(boot))
책에서 기술한대로 이항 오차 함수, 그리고 panel.cor 함수를 정의하자:
binomial_deviance <- function(y_obs, yhat){
epsilon = 0.0001
yhat = ifelse(yhat < epsilon, epsilon, yhat)
yhat = ifelse(yhat > 1-epsilon, 1-epsilon, yhat)
a = ifelse(y_obs==0, 0, y_obs * log(y_obs/yhat))
b = ifelse(y_obs==1, 0, (1-y_obs) * log((1-y_obs)/(1-yhat)))
return(2*sum(a + b))
}
# exmaple(pairs) 에서 따옴
panel.cor <- function(x, y, digits = 2, prefix = "", cex.cor, ...){
usr <- par("usr"); on.exit(par(usr))
par(usr = c(0, 1, 0, 1))
r <- abs(cor(x, y))
txt <- format(c(r, 0.123456789), digits = digits)[1]
txt <- paste0(prefix, txt)
if(missing(cex.cor)) cex.cor <- 0.8/strwidth(txt)
text(0.5, 0.5, txt, cex = cex.cor * r)
}
자료를 human-resources-analytics.zip 파일로 다운받은 후 다음처럼 R에 읽어들인다:
df <- read_csv("human-resources-analytics.zip")
## Parsed with column specification:
## cols(
## satisfaction_level = col_double(),
## last_evaluation = col_double(),
## number_project = col_integer(),
## average_montly_hours = col_integer(),
## time_spend_company = col_integer(),
## Work_accident = col_integer(),
## left = col_integer(),
## promotion_last_5years = col_integer(),
## sales = col_character(),
## salary = col_character()
## )
glimpse(df)
## Observations: 14,999
## Variables: 10
## $ satisfaction_level <dbl> 0.38, 0.80, 0.11, 0.72, 0.37, 0.41, 0.10...
## $ last_evaluation <dbl> 0.53, 0.86, 0.88, 0.87, 0.52, 0.50, 0.77...
## $ number_project <int> 2, 5, 7, 5, 2, 2, 6, 5, 5, 2, 2, 6, 4, 2...
## $ average_montly_hours <int> 157, 262, 272, 223, 159, 153, 247, 259, ...
## $ time_spend_company <int> 3, 6, 4, 5, 3, 3, 4, 5, 5, 3, 3, 4, 5, 3...
## $ Work_accident <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...
## $ left <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1...
## $ promotion_last_5years <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...
## $ sales <chr> "sales", "sales", "sales", "sales", "sal...
## $ salary <chr> "low", "medium", "medium", "low", "low",...
분석의 목적은 다른 변수를 이용하여 left 여부를 예측하는 것이다.
각 변수들의 요약통계량을 살펴보자:
summary(df)
## satisfaction_level last_evaluation number_project average_montly_hours
## Min. :0.0900 Min. :0.3600 Min. :2.000 Min. : 96.0
## 1st Qu.:0.4400 1st Qu.:0.5600 1st Qu.:3.000 1st Qu.:156.0
## Median :0.6400 Median :0.7200 Median :4.000 Median :200.0
## Mean :0.6128 Mean :0.7161 Mean :3.803 Mean :201.1
## 3rd Qu.:0.8200 3rd Qu.:0.8700 3rd Qu.:5.000 3rd Qu.:245.0
## Max. :1.0000 Max. :1.0000 Max. :7.000 Max. :310.0
## time_spend_company Work_accident left
## Min. : 2.000 Min. :0.0000 Min. :0.0000
## 1st Qu.: 3.000 1st Qu.:0.0000 1st Qu.:0.0000
## Median : 3.000 Median :0.0000 Median :0.0000
## Mean : 3.498 Mean :0.1446 Mean :0.2381
## 3rd Qu.: 4.000 3rd Qu.:0.0000 3rd Qu.:0.0000
## Max. :10.000 Max. :1.0000 Max. :1.0000
## promotion_last_5years sales salary
## Min. :0.00000 Length:14999 Length:14999
## 1st Qu.:0.00000 Class :character Class :character
## Median :0.00000 Mode :character Mode :character
## Mean :0.02127
## 3rd Qu.:0.00000
## Max. :1.00000
범주형 변수들의 도수분포는 다음과 같다:
table(df$left)
##
## 0 1
## 11428 3571
table(df$sales)
##
## accounting hr IT management marketing product_mng
## 767 739 1227 630 858 902
## RandD sales support technical
## 787 4140 2229 2720
table(df$salary)
##
## high low medium
## 1237 7316 6446
수량형 변수들간의 관계는 산점도 행렬로 살펴볼 수 있다:
set.seed(2017)
df %>%
dplyr::select(-sales, -salary) %>%
sample_n(500) %>%
pairs(lower.panel=function(x,y){ points(x,y); abline(0, 1, col='red')},
upper.panel = panel.cor)
(
select 함수가 MASS 라이브러리에 재정의 된 관계로 dplyr::select()로 표기했다. ) 반응변수와 큰 상관관계가 있는 변수는 satisfaction_level 임을 알 수 있고, 설명변수중 past_evaluation, number_projects, average_monthly_hours 간에 비교적 높은 상관관계가 있음을 알 수 있다.
(혹시 시간이 걸리더라도 좀 더 고급진 산점도행렬을 얻고자 한다면 다음처럼 GGally::ggparis() 함수를 사용하자)
# install.packages("GGally")
# suppressMessages(library(GGally))
# set.seed(2017); df %>% sample_n(1000) %>% GGally::ggpairs()
모형행렬은 반응변수 left 를 제외한 모든 변수들을 model.matrix() 에 입력해주면 얻을 수 있다:
x <- model.matrix( ~ . - left, data=df)
glimpse(x)
## num [1:14999, 1:19] 1 1 1 1 1 1 1 1 1 1 ...
## - attr(*, "dimnames")=List of 2
## ..$ : chr [1:14999] "1" "2" "3" "4" ...
## ..$ : chr [1:19] "(Intercept)" "satisfaction_level" "last_evaluation" "number_project" ...
## - attr(*, "assign")= int [1:19] 0 1 2 3 4 5 6 7 8 8 ...
## - attr(*, "contrasts")=List of 2
## ..$ sales : chr "contr.treatment"
## ..$ salary: chr "contr.treatment"
colnames(x)
## [1] "(Intercept)" "satisfaction_level"
## [3] "last_evaluation" "number_project"
## [5] "average_montly_hours" "time_spend_company"
## [7] "Work_accident" "promotion_last_5years"
## [9] "saleshr" "salesIT"
## [11] "salesmanagement" "salesmarketing"
## [13] "salesproduct_mng" "salesRandD"
## [15] "salessales" "salessupport"
## [17] "salestechnical" "salarylow"
## [19] "salarymedium"
dim(x)
## [1] 14999 19
모형의 차원은 \(p=19\) 임을 알 수 있다.
원 데이터를 6:4:4 비율로 훈련, 검증, 테스트셋으로 나누도록 하자. (재현 가능성을 위해 set.seed()를 사용했다.)
set.seed(2017)
n <- nrow(df)
idx <- 1:n
training_idx <- sample(idx, n * .60)
idx <- setdiff(idx, training_idx)
validate_idx = sample(idx, n * .20)
test_idx <- setdiff(idx, validate_idx)
length(training_idx)
## [1] 8999
length(validate_idx)
## [1] 2999
length(test_idx)
## [1] 3001
training <- df[training_idx,]
validation <- df[validate_idx,]
test <- df[test_idx,]
df_glm_full <- glm(left ~ ., data=training, family=binomial)
summary(df_glm_full)
##
## Call:
## glm(formula = left ~ ., family = binomial, data = training)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.1949 -0.6781 -0.4119 -0.1144 3.0619
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.3207933 0.2458962 -5.371 7.82e-08 ***
## satisfaction_level -4.0265898 0.1250099 -32.210 < 2e-16 ***
## last_evaluation 0.7373843 0.1911870 3.857 0.000115 ***
## number_project -0.3409404 0.0274920 -12.401 < 2e-16 ***
## average_montly_hours 0.0047920 0.0006639 7.217 5.30e-13 ***
## time_spend_company 0.2535567 0.0198581 12.768 < 2e-16 ***
## Work_accident -1.4459747 0.1117518 -12.939 < 2e-16 ***
## promotion_last_5years -1.5679693 0.3529061 -4.443 8.87e-06 ***
## saleshr 0.0462927 0.1712222 0.270 0.786879
## salesIT -0.1770899 0.1544876 -1.146 0.251669
## salesmanagement -0.6100011 0.2096773 -2.909 0.003623 **
## salesmarketing -0.0031379 0.1678177 -0.019 0.985082
## salesproduct_mng -0.2147629 0.1636240 -1.313 0.189338
## salesRandD -0.6806671 0.1858707 -3.662 0.000250 ***
## salessales -0.0982475 0.1306772 -0.752 0.452151
## salessupport -0.0494077 0.1395616 -0.354 0.723324
## salestechnical 0.0328907 0.1357538 0.242 0.808562
## salarylow 1.8403821 0.1628753 11.299 < 2e-16 ***
## salarymedium 1.3799875 0.1639047 8.419 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 9921.2 on 8998 degrees of freedom
## Residual deviance: 7845.3 on 8980 degrees of freedom
## AIC: 7883.3
##
## Number of Fisher Scoring iterations: 5
로지스틱 모형의 예측 정확도 지표는 다음처럼 계산하고 시각화할 수 있다:
y_obs <- validation$left
yhat_lm <- predict(df_glm_full, newdata=validation, type='response')
ggplot(data.frame(y_obs, yhat_lm),
aes(yhat_lm, fill=factor(y_obs))) +
geom_density(alpha=.5)
binomial_deviance(y_obs, yhat_lm)
## [1] 2528.212
pred_lm <- prediction(yhat_lm, y_obs)
perf_lm <- performance(pred_lm, measure = "tpr", x.measure = "fpr")
plot(perf_lm, col='black', main="ROC Curve for GLM")
performance(pred_lm, "auc")@y.values[[1]]
## [1] 0.8334041
xx <- model.matrix(left ~ .-1, df)
x <- xx[training_idx, ]
y <- training$left
df_cvfit <- cv.glmnet(x, y, family = "binomial")
plot(df_cvfit)
y_obs <- validation$left
yhat_glmnet <- predict(df_cvfit, s="lambda.1se", newx=xx[validate_idx,], type='response')
yhat_glmnet <- yhat_glmnet[,1] # change to a vectro from [n*1] matrix
binomial_deviance(y_obs, yhat_glmnet)
## [1] 2560.567
pred_glmnet <- prediction(yhat_glmnet, y_obs)
perf_glmnet <- performance(pred_glmnet, measure="tpr", x.measure="fpr")
performance(pred_glmnet, "auc")@y.values[[1]]
## [1] 0.8284201
나무모형을 적합하는 rpart::rpart() 함수를 적용할 때 주의할 사항은 수량형 반응변수 left 를 인자로 변환해주어서 회귀 나무모형이 아니라 분류분석 나무모형을 적합하는 것이다.
df_tr <- rpart(as.factor(left) ~ ., data = training)
df_tr
## n= 8999
##
## node), split, n, loss, yval, (yprob)
## * denotes terminal node
##
## 1) root 8999 2161 0 (0.75986221 0.24013779)
## 2) satisfaction_level>=0.465 6448 635 0 (0.90151985 0.09848015)
## 4) time_spend_company< 4.5 5247 77 0 (0.98532495 0.01467505) *
## 5) time_spend_company>=4.5 1201 558 0 (0.53538718 0.46461282)
## 10) last_evaluation< 0.815 482 23 0 (0.95228216 0.04771784) *
## 11) last_evaluation>=0.815 719 184 1 (0.25591099 0.74408901)
## 22) average_montly_hours< 216.5 122 9 0 (0.92622951 0.07377049) *
## 23) average_montly_hours>=216.5 597 71 1 (0.11892797 0.88107203)
## 46) satisfaction_level< 0.715 43 6 0 (0.86046512 0.13953488) *
## 47) satisfaction_level>=0.715 554 34 1 (0.06137184 0.93862816) *
## 3) satisfaction_level< 0.465 2551 1025 1 (0.40180321 0.59819679)
## 6) number_project>=2.5 1482 582 0 (0.60728745 0.39271255)
## 12) satisfaction_level>=0.115 966 66 0 (0.93167702 0.06832298) *
## 13) satisfaction_level< 0.115 516 0 1 (0.00000000 1.00000000) *
## 7) number_project< 2.5 1069 125 1 (0.11693171 0.88306829)
## 14) last_evaluation>=0.575 84 7 0 (0.91666667 0.08333333) *
## 15) last_evaluation< 0.575 985 48 1 (0.04873096 0.95126904) *
# printcp(df_tr)
# summary(df_tr)
opar <- par(mfrow = c(1,1), xpd = NA)
plot(df_tr)
text(df_tr, use.n = TRUE)
par(opar)
나무모형의 출력 결과를 살펴보면 어떤 변수들의 조합이 직원의 이직율을 높이는 지 알수 있다. 그림에서 가장 “성공”(이직)의 비율이 높은 잎(leaf)는 가장 오른쪽의 잎, 즉:
3) satisfaction_level< 0.465 2551 1025 1 (0.40180321 0.59819679)7) number_project< 2.5 1069 125 1 (0.11693171 0.88306829)15) last_evaluation< 0.575 985 48 1 (0.04873096 0.95126904) *만족도가 낮고, 일한 프로젝트가 적고, 마지막 업무평가가 좋지 않은 집단임을 알 수 있다.
yhat_tr <- predict(df_tr, validation)[, "1"]
binomial_deviance(y_obs, yhat_tr)
## [1] 827.8547
pred_tr <- prediction(yhat_tr, y_obs)
perf_tr <- performance(pred_tr, measure = "tpr", x.measure = "fpr")
performance(pred_tr, "auc")@y.values[[1]]
## [1] 0.9667003
randomForest() 함수를 적용할 때 주의할 사항은:
left 를 인자로 변환해주어서 회귀모형이 아닌 분류분석이 실행되도록 한다.sales, salary 도 인자형으로 바꿔줘야 한다.set.seed(2017)
df_rf <- randomForest(as.factor(left) ~ ., training %>%
mutate(salary=as.factor(salary),
sales=as.factor(sales)))
df_rf
##
## Call:
## randomForest(formula = as.factor(left) ~ ., data = training %>% mutate(salary = as.factor(salary), sales = as.factor(sales)))
## Type of random forest: classification
## Number of trees: 500
## No. of variables tried at each split: 3
##
## OOB estimate of error rate: 0.98%
## Confusion matrix:
## 0 1 class.error
## 0 6827 11 0.001608658
## 1 77 2084 0.035631652
랜덤포레스트 모형의 오류 감소 추세 그래프는 다음과 같다:
plot(df_rf)
각 변수들의 모형에의 기여도는 다음과 같다:
varImpPlot(df_rf)
예측을 실행할 때는, 훈련셋과 마찬가지의 as.factor 변환을 같은 변수에 적용해야 한다:
yhat_rf <- predict(df_rf,
newdata=validation %>%
mutate(salary=as.factor(salary), sales=as.factor(sales)),
type='prob')[,'1']
binomial_deviance(y_obs, yhat_rf)
## [1] 371.0111
pred_rf <- prediction(yhat_rf, y_obs)
perf_rf <- performance(pred_rf, measure="tpr", x.measure="fpr")
performance(pred_tr, "auc")@y.values[[1]]
## [1] 0.9667003
(결과 생략) 관심있는 독자는 다음 코드로 부스팅 모형을 실행할 수 있다. 앞서 랜덤포레스트 모형과 마찬가지로, 문자형 변수를 인자형 변수로 먼저 변환해 주어야 한다.
set.seed(2017)
df_gbm <- gbm(left ~ ., data=training %>%
mutate(salary=as.factor(salary), sales=as.factor(sales)),
distribution="bernoulli",
n.trees=1000, cv.folds=3, verbose=TRUE)
(best_iter <- gbm.perf(df_gbm, method="cv"))
yhat_gbm <- predict(df_gbm, n.trees=best_iter,
newdata=validation %>%
mutate(salary=as.factor(salary), sales=as.factor(sales)),
type='response')
binomial_deviance(y_obs, yhat_gbm)
pred_gbm <- prediction(yhat_gbm, y_obs)
perf_gbm <- performance(pred_gbm, measure="tpr", x.measure="fpr")
performance(pred_gbm, "auc")@y.values[[1]]
다음과 같은 시각화로 각 예측모형들의 예측확률들의 관계를 알 수 있다:
pairs(data.frame(y_obs=y_obs,
yhat_lm=yhat_lm,
yhat_glmnet=c(yhat_glmnet),
yhat_tr=yhat_tr,
yhat_rf=yhat_rf),
lower.panel=function(x,y){ points(x,y); abline(0, 1, col='red')},
upper.panel = panel.cor)
로지스틱 모형과 glmnet 모형은 매우 유사한 결과를 주는 것을 알 수 있다. 그리고, 나무 모형과 랜덤포레스트 모형도 상관관계가 높다. 반응변수의 관측치와 가장 상관관계가 높은, 즉 예측력이 높은 방법은 랜덤포레스트이다.
테스트셋을 이용해 일반화 능력을 계산해보자:
y_obs_test <- test$left
yhat_rf_test <- predict(df_rf,
newdata=test %>%
mutate(salary=as.factor(salary), sales=as.factor(sales)),
type='prob')[,'1']
binomial_deviance(y_obs_test, yhat_rf_test)
## [1] 403.2456
pred_rf_test <- prediction(yhat_rf_test, y_obs_test)
performance(pred_rf_test, "auc")@y.values[[1]]
## [1] 0.9909491
마지막으로 ROC 커브를 통해 네 예측방법을 비교해보자.
plot(perf_lm, col='black', main="ROC Curve")
plot(perf_glmnet, add=TRUE, col='blue')
plot(perf_tr, add=TRUE, col='red')
plot(perf_rf, add=TRUE, col='cyan')
legend('bottomright', inset=.1,
legend=c("GLM", "glmnet", "Tree", "RF"),
col=c('black', 'blue', 'red', 'cyan'), lty=1, lwd=2)
자료 자체가 가상의 (synthetic), 시뮬레이트 된 자료이므로 비교적 간단한 모형인 나무모형으로도 무척 높은 예측력을 얻을 수 있었다. 변수 해석에 관해서는 나무모형 결과를 참조하라.