https://www.youtube.com/watch?v=LxHMfr120gU&list=PLY0OaF78qqGAxKX91WuRigHpwBU0C2SB_&index=34

1. 페널티 로지스틱회귀분석 개요

  • 로지스틱 모델의 다수의 예측변수가 포함되어 있어서 과적합이 우려될 때 페널티회귀분석을 수행하여 예측변수의 개수가 축소된 보다 간명한 모델을 만들 수 있음

    • 페널티 회귀분석의 지나치게 많은 예측변수를 갖는 로지스틱회귀모델의 페널티를 부과함

    • 따라서 그 결과로 모델의 설명력에 덜 기여하는 예측변수 회귀계수를 0으로 만들거나 혹은 0에 가깝게 축소함

      • 예측변수 회귀계수를 0으로 만드는 페널티 회귀분석 기법을 라소회귀분석

      • 예측변수 회귀계수를 0에 가깝게 축소하는 페널티 회귀분석 기법을 릿지회귀분석이라고 함

    • 라소회귀분석을 적용해 모델의 설명력에 덜 기여하는 예측변수 회귀계수를 0으로 만든다는 것은 결국 그 회귀계수를 갖는 예측변수를 모델에서 제거하는 것이기 때문에 모델의 복잡성이 줄어듦

      • 예측정확도가 비슷할 경우 일반적으로 예측변수 개수가 작은 간명한 모델이 바람직함

      • 예측변수의 개수가 작다는 것은 모델의 복잡도가 축소된다는 얘기고 그 얘기는 새 데이터에 대한 과적합 위험이 작은 보다 일반화 가능성이 높은 모델을 만들 수가 있다는 뜻이기 때문임

2. 페널티 로지스틱회귀분석 수행절차

2-1. 데이터셋 구조 확인

  • PimaIndiansDiabetes2데이터셋 구조

    • PimaIndiansDiabetes2데이터셋은 환자의 당뇨병 여부와 관련된 임상데이터 저장

    • 모두 768 개의 관측 값을 포함하고 9개 변수로 구성됨

    • 9개 변수 중 마지막 diabetes변수는

      • 예측모델에 의해 예측하고자하는 결과변수로써

      • 당뇨병 여부를 나타내는 두 개의 범주를 갖고 있음

    • 앞의 8개 변수는 예측변수임. 이 8개 예측변수로 당뇨병 여부를 예측하는 예측모델을 생성할 것임

library(mlbench)
data("PimaIndiansDiabetes2")
str(PimaIndiansDiabetes2)
## 'data.frame':    768 obs. of  9 variables:
##  $ pregnant: num  6 1 8 1 0 5 3 10 2 8 ...
##  $ glucose : num  148 85 183 89 137 116 78 115 197 125 ...
##  $ pressure: num  72 66 64 66 40 74 50 NA 70 96 ...
##  $ triceps : num  35 29 NA 23 35 NA 32 NA 45 NA ...
##  $ insulin : num  NA NA NA 94 168 NA 88 NA 543 NA ...
##  $ mass    : num  33.6 26.6 23.3 28.1 43.1 25.6 31 35.3 30.5 NA ...
##  $ pedigree: num  0.627 0.351 0.672 0.167 2.288 ...
##  $ age     : num  50 31 32 21 33 30 26 29 53 54 ...
##  $ diabetes: Factor w/ 2 levels "neg","pos": 2 1 2 1 2 1 2 1 2 2 ...

2-2. 데이터 전처리(결측값 제거) - na.omit()

  • PimaIndiansDiabetes2데이터셋은 많은 결측값이 포함돼 있어, 이를 제거하는 전처리 작업이 필요함

  • 결측값을 제외하고 PimaIndiansDiabetes3라는 새데이터셋으로 저장함

PimaIndiansDiabetes3 <- na.omit(PimaIndiansDiabetes2)
  • 이 전체 데이터를 예측모델 생성을 위한 훈련데이터검증을 위한 테스트데이터로 분할할 것임

2-3. 데이터 전처리(훈련/테스트데이터분할) - createDataPartition()

  • 여기서는 caret패키지 createDataPartition()로 훈련데이터와 테스트데이터를 7대 3의 비율로 분할함

  • createDataPartition() 인수설명

    • y: 데이터셋에 포함되어 있는 결과변수를 지정

    • p: 훈련데이터의 비율 지정. 훈련/테스트데이터 비율을 7대 3으로 하기 때문에 0.7을 지정함

    • list: 훈련데이터에 대응되는 데이터인덱스가 리스트가 아닌 행렬 구조로 출력되도록 FALSE 지정

library(caret)
set.seed(123)
train <- createDataPartition(y=PimaIndiansDiabetes3$diabetes, 
                             p=0.7, list=FALSE)
head(train, 10)
##       Resample1
##  [1,]         1
##  [2,]         2
##  [3,]         3
##  [4,]         4
##  [5,]         5
##  [6,]         6
##  [7,]         7
##  [8,]         9
##  [9,]        11
## [10,]        13
  • 이 인덱스로 훈련/테스트데이터로 분화를 수행함

  • 훈련데이터를 제외한 나머지 데이터는 테스트데이터로 저장함

diabetes.train <- PimaIndiansDiabetes3[train,]
diabetes.test <- PimaIndiansDiabetes3[-train,]
  • 전체 데이터셋으로부터 훈련/테스트데이터가 적절히 잘 분할됐는지 평가하기 위해 두 데이터셋에 포함된 당뇨병 환자 비율을 계산해 보겠음

  • 먼저 훈련데이터에 포함된 당뇨병 환자수와 비율 계산함

table(diabetes.train$diabetes)
## 
## neg pos 
## 184  91
sum(table(diabetes.train$diabetes))
## [1] 275
prop.table(table(diabetes.train$diabetes))
## 
##       neg       pos 
## 0.6690909 0.3309091
  • 결과해석

    • 훈련데이터에는 모두 275개 관측값이 있고 그중 당뇨병 환자는 91명임

    • 비율을 계산하면 당뇨병 환자비율은 약 33% 정도됨

  • 테스트데이터에 대해서도 계산을 해보겠음

table(diabetes.test$diabetes)
## 
## neg pos 
##  78  39
sum(table(diabetes.test$diabetes))
## [1] 117
prop.table(table(diabetes.test$diabetes))
## 
##       neg       pos 
## 0.6666667 0.3333333
  • 결과해석

    • 테스트 데이터에는 117개 관측값이 있고 그중 당뇨병 환자는 39명임

    • 비율로는 약 33% 정도임

    • 훈련 데이터와 테스트데이터 모두 당뇨병 환자를 33%를 포함하고 있음

  • 훈련 데이터와 테스트 데이터에 포함되어 있는 결과 변수의 당뇨병 환자 비율이 모두 동일하기 때문에 전체 데이터셋으로부터 “모델생성을 위한 훈련데이터와 테스트를 위한 테스트데이터가 잘 분할됐다”고 판단할 수 있음

  • createDataPartition()이 이처럼 훈련/테스트데이터셋에 포함된 당뇨병 환자 비율이 동일하도록 작업함

    • createDataPartition()는 전체 데이터셋을 훈련/테스트데이터셋으로 분할할 때 전체 데이터셋에 포함된 결과변수의 범주비율이 훈련/테스트 데이터셋에서도 동일하게 유지되도록 생성함

    • 그래서 훈련/테스트데이터셋에 포함된 당뇨병 환자 비율 33%는 전체 데이터셋의 당뇨병 환자 비율이기도 함

2-4. 예측변수 행렬 / 결과변수 벡터 생성 및 교차검증 객체생성 - model.matrix(), cv.glmnet()

  • 훈련/테스트데이터셋이 준비됐으므로 페널티 회귀분석으로 예측모델 생성 후 모델성능을 평가할 것임

    • 페널티 회귀분석에서는 예측오차를 최소화하는 최적의 \(\lambda\)를 먼저 결정해야 함. \(\lambda\)는 페널티 크기를 지정해 줌

    • glmnet패키지 cv.glmnet() 교차검증을 통해 최적의 \(\lambda\)를 산출함

  • cv.glmnet() 인수설명

    • 1st: 예측변수 행렬 지정

    • 2nd: 결과변수 벡터 지정

    • family: 결과변수 확률분포 지정. 여기서는 현재 현재 이항 로지스틱회귀분석을 수행하기 때문에 “binomial” 지정

    • \(\alpha\): 라소회귀분석을 바탕으로 한 페널티 회귀분석을 수행할 예정이기 때문에 1을 지정함. 0을 지정하면 릿지회귀분석을 수행함

  • cv.glmnet()은 포뮬러 형식의 모델 설정을 지원하지않아 예측변수/결과변수를 각각 별도 지정해야 함

    • 예측변수는 행렬 형식, 결과변수는 벡터 형식으로 지정돼야 함

    • 예측변수로 숫자만 취할 수 있어 데이터셋에 범주형 변수가 있으면 사전에 더미변수로 변환해야 함

    • model.matrix()로 이러한 데이터 전처리 작업을 쉽게 수행할 수 있음

  • model.matrix() 인수설명

    • 1st: 포뮬러 형식으로 결과변수와 예측변수 간 관계를 지정함

    • 2nd: 사용할 데이터셋을 지정함

    • model.matrix()는

      • 모델에 투입할 예측변수 행렬을 생성하고

      • 범주형 변수를 더미변수로 자동으로 변환해 줌

x <- model.matrix(diabetes ~ ., diabetes.train)
head(x)
##    (Intercept) pregnant glucose pressure triceps insulin mass pedigree age
## 4            1        1      89       66      23      94 28.1    0.167  21
## 5            1        0     137       40      35     168 43.1    2.288  33
## 7            1        3      78       50      32      88 31.0    0.248  26
## 9            1        2     197       70      45     543 30.5    0.158  53
## 14           1        1     189       60      23     846 30.1    0.398  59
## 15           1        5     166       72      19     175 25.8    0.587  51
  • 첫 번째 열이 불필요하여 첫 번째 열(Intercept)을 제거하는 추가작업을 수행하겠음

  • 결과변수 벡터를 별도로 생성해 y변수에 저장

    • 결과변수는 범주형 변수이기 때문에 숫자 형식으로 변환함

    • 사건발생과 미발생을 1과 0으로 코딩되도록 범주형 변수를 변환해 y 변수에 저장함

x <- model.matrix(diabetes ~ ., diabetes.train)[,-1]
y <- ifelse(diabetes.train$diabetes=="pos", 1, 0)
  • 이렇게 생성한 예측변수 행렬과 결과변수 벡터를 cv.glmnet()에 x인수와 y인수로 지정
library(glmnet)
set.seed(123)
diabetes.cv <- cv.glmnet(x=x, y=y, family="binomial")

2-5. 교차검정객체 원소(lambda.min 및 lambda.1se)의 \(\lambda\)값 및 회귀계수 확인

  • 이렇게 생성한 교차검정 객체에는 많은 정보가 포함돼 있음. 그 중 지금 찾고자 하는 예측오차를 가장 작게 만드는 \(\lambda\)는 교차검정 객체의 lambda.min원소에서 확인할 수 있음
diabetes.cv$lambda.min
## [1] 0.01087884
  • 그런데 페널티 회귀분석을 수행하는 중요한 목표 중 하나는 일정 수준의 예측정확도를 보장하면서 가능하면 복잡하지 않은 간명한 모델을 구축하는 것임
  • cv.glmnet()는 이를 위해 교차검정 객체의 lambda.1se원소에 또 다른 \(\lambda\)를 제공함
diabetes.cv$lambda.1se
## [1] 0.05805712
  • 교차검정 객체의 lambda.1se원소에 제시된 \(\lambda\)는 최소예측오차 한계의 표준편차 범위 내의 정확도를 보이는 모델 중 가장 예측변수 개수가 작은, 간명한 모델을 생성함
  • 따라서 lambda.1se의 \(\lambda\)를 사용하면 어느 정도 예측정확도를 희생할 지 모르지만 일반화 가능성은 높아짐. 즉, 과적합의 위험은 줄어든 모델을 생성할 수 있음
  • lambda.min의 \(\lambda\)와 lambda.1se의 \(\lambda\)를 사용하여 생성한 회귀모델의 회귀계수 추출
coef(diabetes.cv, diabetes.cv$lambda.min)
## 9 x 1 sparse Matrix of class "dgCMatrix"
##                      s1
## (Intercept) -9.23133474
## pregnant     0.01006916
## glucose      0.03446189
## pressure     .         
## triceps      0.02678380
## insulin      0.00147175
## mass         0.04309304
## pedigree     1.14386810
## age          0.03039017
coef(diabetes.cv, diabetes.cv$lambda.1se)
## 9 x 1 sparse Matrix of class "dgCMatrix"
##                        s1
## (Intercept) -5.6548824026
## pregnant     .           
## glucose      0.0276723503
## pressure     .           
## triceps      0.0137868666
## insulin      0.0001604243
## mass         0.0120567460
## pedigree     0.3234881011
## age          0.0137023590
  • 비교결과

    • lambda.min의 \(\lambda\)를 사용한 경우, 7개의 예측변수를 갖는 회귀모델이 구축됐음

    • lambda.1se의 \(\lambda\)를 사용한 경우, 그보다 작은 6개의 예측변수를 갖는 회귀모델이 생성돼 모델의 복잡성이 다소 줄어들었음

2-6. 교차검정객체 원소(lambda.min 및 lambda.1se)의 \(\lambda\)값에 따른 예측모델 생성

2-6-1. 예측오차를 최소화한 \(\lambda\)를 사용한 모델(lambda.min 사용모델)의 예측정확도 계산

  • glmnet() 인수

    • 예측모델은 glmnet()으로 생성. 조금 전 사용한 cv.glmnet()와 대부분의 인수를 공유함

    • x, y: 각각 예측변수 행렬과 결과변수 벡터를 지정함

    • family: 결과변수에는 확률분포인 binomial 지정

    • alpha: 1을 지정해 라소회귀분석 수행

    • lambda: 교차검증 객체에서 추출한 예측오차를 최소화하는 \(\lambda\)인 lambda.min원소에 있는 값을 지정

diabetes.gnet1 <- glmnet(x=x, y=y,
                         family="binomial",
                         alpha=1,
                         lambda=diabetes.cv$lambda.min)
  • 이어서 model.matrix()로 테스트 데이터셋에 포함된 예측변수 행렬 생성
diabetes.test.x <- model.matrix(diabetes ~ ., diabetes.test)[,-1]
  • 이어서 predict()로 테스트 데이터에 대한 예측확률 계산

  • predict() 인수설명

    • 1st: 예측모델을 지정함

    • newx: 테스트 데이터로부터 생성한 예측변수 행렬을 지정함

    • type: response를 지정해 확률로 결과값이 출력되도록 함

diabetes.pred1 <- predict(diabetes.gnet1, 
                          newx=diabetes.test.x, 
                          type="response")
  • 이어서 예측확률 0.5를 기준으로 당뇨병 여부를 판정함

    • 예측활률이 0.5점 보다 크면 당뇨병 환자로, 그렇지 않으면 정상으로 판정함
diabetes.pred1 <- ifelse(diabetes.pred1 > 0.5, "pos", "neg")
  • table()로 실제 판정결과와 예측모델에 의한 예측결과 비교
table(diabetes.test$diabetes, diabetes.pred1, 
      dnn=c("Actual", "Predicted"))
##       Predicted
## Actual neg pos
##    neg  69   9
##    pos  20  19
  • 결과해석: 대각선에 있는 수치는 실제 판정결과와 예측모델의 예측결과가 일치하는 케이스 갯수임

  • 예측정확도 계산

mean(diabetes.test$diabetes==diabetes.pred1)
## [1] 0.7521368
  • 결과해석: 예측 정확도는 75.2%로 선출되었음

2-6-2. 간명도를 높인 모델(lambda.1se 사용모델)의 예측정확도 계산

  • 앞서와 동일한 방식으로 계산하고 \(\lambda\)값으로, lambda.min이 아닌 lambda.1se에 포함된 \(\lambda\)값 지정
diabetes.gnet2 <- glmnet(x=x, y=y,
                         family="binomial",
                         alpha=1,
                         lambda=diabetes.cv$lambda.1se)

# 이어서 model.matrix()로 테스트 데이터셋에 포함된 예측변수 행렬 생성
diabetes.test.x <- model.matrix(diabetes ~ ., diabetes.test)[,-1]

# 이어서 predict()로 테스트 데이터에 대한 예측확률 계산
diabetes.pred2 <- predict(diabetes.gnet2, 
                          newx=diabetes.test.x, 
                          type="response")

# 이어서 예측확률 0.5를 기준으로 당뇨병 여부를 판정함
diabetes.pred2 <- ifelse(diabetes.pred2 > 0.5, "pos", "neg")

# table()로 실제 판정결과와 예측모델에 의한 예측결과 비교
table(diabetes.test$diabetes, diabetes.pred2, 
      dnn=c("Actual", "Predicted"))
##       Predicted
## Actual neg pos
##    neg  71   7
##    pos  22  17
# 예측정확도 계산
mean(diabetes.test$diabetes==diabetes.pred2) # 예측 정확도는 75.2%로 선출되었음
## [1] 0.7521368
  • 결과해석

    • 간명도를 높인 예측모델의 경우 예측정확도는 75.2%로 계산됨

    • 우연히도 앞서 예측오차를 최소화하는 \(\lambda\)를 이용한 예측정확도와 동일한 값이 산출됨

2-7. 모든 예측변수가 포함된 일반적인 이항 로지스틱회귀분석 수행 후 앞선 페널티 회귀분석 결과와 비교

  • glm()로 예측모델 생성
diabetes.logit <- glm(diabetes ~ ., 
                      data=diabetes.train, 
                      family=binomial(link="logit"))
  • predict()로 예측확률 계산
diabetes.logit.pred <- predict(diabetes.logit, 
                               newdata=diabetes.test,
                               type="response")
  • 예측확률을 당뇨병 환자 여부를 나타내는 두 개의 범주로 변환
diabetes.logit.pred <- ifelse(diabetes.logit.pred > 0.5, "pos", "neg")
  • table()로 혼동행렬 생성
table(diabetes.test$diabetes, diabetes.logit.pred, 
      dnn=c("Actual", "Predicted"))
##       Predicted
## Actual neg pos
##    neg  66  12
##    pos  20  19
  • 끝으로 예측정확도 계산
mean(diabetes.test$diabetes==diabetes.logit.pred)
## [1] 0.7264957
  • 결과해석

    • 예측정확도는 72.6%로 산출되었음

    • 예측모델의 모든 예측변수를 투입했음에도 불구하고 앞서 생성한 2개의 페널티 로지스틱회귀모델의 예측정확도에 비해서 더 낮게 계산됨

    • 따라서 여기에서는

      • 모델이 간명도 관점에서 뿐만 아니라

      • 모델의 예측정확도 관점에서도

      • 일반적인 이항 로지스틱회귀분석을 이용한 예측모델보다 페널티 회귀분석을 이용한 예측모델이 더 우수하다고 판단내릴 수 있음

    • 앞서 생성한 두 개의 페널티 로지스틱회귀모델의 예측정확도가 모두 동일했음

    • 따라서 투입된 예측변수의 개수를 고려할 때 lambda.1se에 포함된 \(\lambda\)를 이용한 페널티 로지스틱회귀모델이 3개의 모델을 가운데는 일반화 관점에서 가장 우수한 모델이라고 얘기할 수 있음