📌 이항 로지스틱 회귀분석(Binomial Logistic Regression Analysis)
ln(p / (1-p)) = B0 + B1x1 + B2x2 + ... + Bmxm
- x : 예측변수
- B : 회귀계수
- m : 예측변수의 개수
📌 로그 오즈, 오즈비
이동 통신 회사의 고객 이탈 데이터, 고객의 통화 특성과 이탈 여부
> library(modeldata)
> data("mlc_churn")
> str(mlc_churn)
Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 5000 obs. of 20 variables:
$ state : Factor w/ 51 levels "AK","AL","AR",..: 17 36 32 36 37 2 20 25 19 50 ...
$ account_length : int 128 107 137 84 75 118 121 147 117 141 ...
$ area_code : Factor w/ 3 levels "area_code_408",..: 2 2 2 1 2 3 3 2 1 2 ...
$ international_plan : Factor w/ 2 levels "no","yes": 1 1 1 2 2 2 1 2 1 2 ...
$ voice_mail_plan : Factor w/ 2 levels "no","yes": 2 2 1 1 1 1 2 1 1 2 ...
$ number_vmail_messages : int 25 26 0 0 0 0 24 0 0 37 ...
$ total_day_minutes : num 265 162 243 299 167 ...
$ total_day_calls : int 110 123 114 71 113 98 88 79 97 84 ...
$ total_day_charge : num 45.1 27.5 41.4 50.9 28.3 ...
$ total_eve_minutes : num 197.4 195.5 121.2 61.9 148.3 ...
$ total_eve_calls : int 99 103 110 88 122 101 108 94 80 111 ...
$ total_eve_charge : num 16.78 16.62 10.3 5.26 12.61 ...
$ total_night_minutes : num 245 254 163 197 187 ...
$ total_night_calls : int 91 103 104 89 121 118 118 96 90 97 ...
$ total_night_charge : num 11.01 11.45 7.32 8.86 8.41 ...
$ total_intl_minutes : num 10 13.7 12.2 6.6 10.1 6.3 7.5 7.1 8.7 11.2 ...
$ total_intl_calls : int 3 3 5 7 3 6 7 6 4 5 ...
$ total_intl_charge : num 2.7 3.7 3.29 1.78 2.73 1.7 2.03 1.92 2.35 3.02 ...
$ number_customer_service_calls: int 1 1 0 2 3 0 3 0 1 0 ...
$ churn : Factor w/ 2 levels "yes","no": 2 2 2 2 2 2 2 2 2 2 ...
> churn <- mlc_churn
두 변수를 예측변수에서 제외시킨다.
> churn <- churn[, -c(1, 3)]
- 고객 이탈 : 사건 미발 (1)
- 고객 미이탈 : 사건 발생 (2)
관심 있는 사건은 고객 이탈
이기 때문에 고객 이탈이 사건 발생으로 해석되도록 고객 미이탈(1)
, 고객 이탈(2)
로 변환한다.
> churn$churn <- factor(ifelse(churn$churn=='no', 1, 2),
+ levels=c(1, 2),
+ labels=c('no', 'yes'))
$ churn : Factor w/ 2 levels "no","yes": 1 1 1 1 1 1 1 1 1 1 ...
> churn.train <- churn[1:3333, ]
> churn.test <- churn[3334:5000, ]
훈련 데이터와 테스트 데이터가 적절히 분할 되어 있는지 확인한다.
이탈 고객 비율 계산 후 비교한다.
> table(churn.train$churn)
no yes
2850 483
> prop.table(table(churn.train$churn)) # 이탈 고객 14.5%
no yes
0.8550855 0.1449145
> table(churn.test$churn)
no yes
1443 224
> prop.table(table(churn.test$churn)) # 이탈 고객 13.4%
no yes
0.8656269 0.1343731
이탈 고객 비율이 크게 다르지 않으므로 적절히 분할 되었다.
glm(종속변수 ~ 독립변수, data, family=이항분포 로짓)
> churn.logit <- glm(churn ~ ., data=churn.train, family=binomial(link='logit'))
> summary(churn.logit)
Call:
glm(formula = churn ~ ., family = binomial(link = "logit"), data = churn.train)
Deviance Residuals:
Min 1Q Median 3Q Max
-2.1532 -0.5132 -0.3402 -0.1953 3.2528
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -8.6515638 0.7243142 -11.944 < 2e-16 ***
account_length 0.0008458 0.0013912 0.608 0.543199
international_planyes 2.0427543 0.1454974 14.040 < 2e-16 ***
voice_mail_planyes -2.0250146 0.5740840 -3.527 0.000420 ***
number_vmail_messages 0.0358803 0.0180108 1.992 0.046355 *
total_day_minutes -0.2441993 3.2742224 -0.075 0.940547
total_day_calls 0.0031962 0.0027612 1.158 0.247048
total_day_charge 1.5127081 19.2601862 0.079 0.937398
total_eve_minutes 0.8186945 1.6357258 0.501 0.616717
total_eve_calls 0.0010579 0.0027826 0.380 0.703817
total_eve_charge -9.5463678 19.2437266 -0.496 0.619840
total_night_minutes -0.1238287 0.8764906 -0.141 0.887650
total_night_calls 0.0006993 0.0028419 0.246 0.805628
total_night_charge 2.8338084 19.4769043 0.145 0.884319
total_intl_minutes -4.3377914 5.3009719 -0.818 0.413185
total_intl_calls -0.0929680 0.0250603 -3.710 0.000207 ***
total_intl_charge 16.3900316 19.6323938 0.835 0.403804
number_customer_service_calls 0.5135638 0.0392678 13.079 < 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: 2758.3 on 3332 degrees of freedom
Residual deviance: 2158.7 on 3315 degrees of freedom
AIC: 2194.7
Number of Fisher Scoring iterations: 6
international_plan, voice_mail_plan가 범주형 변수이므로 자동으로 더미변수로 자동 변환되었다. 더미변수는 no, yes 중 첫 번째 no를 기준 범주로 한다.
1) 오즈비 계산
> exp(coef(churn.logit))
(Intercept) account_length international_planyes voice_mail_planyes number_vmail_messages
1.748532e-04 1.000846e+00 7.711821e+00 1.319919e-01 1.036532e+00
total_day_minutes total_day_calls total_day_charge total_eve_minutes total_eve_calls
7.833315e-01 1.003201e+00 4.539006e+00 2.267538e+00 1.001058e+00
total_eve_charge total_night_minutes total_night_calls total_night_charge total_intl_minutes
7.146035e-05 8.835312e-01 1.000700e+00 1.701012e+01 1.306535e-02
total_intl_calls total_intl_charge number_customer_service_calls
9.112226e-01 1.312503e+07 1.671236e+00
>
2) 모델의 통계적 유의성 검정
📝 이탈도는 카이스퀘어 분포를 따른다.
자유도의 차이만큼 충분한 크기의 이탈도 차이가 있는지를 검정한다.
> pchisq(q=2758.3-2158.7, df=3332-3315, lower.tail=FALSE)
[1] 1.731898e-116
> pchisq(q=churn.logit$null.deviance - churn.logit$deviance,
+ df=churn.logit$df.null - churn.logit$df.residual,
+ lower.tail=FALSE)
[1] 1.757917e-116
p-value=0에 가까운 값이므로 이탈도의 차이는 통계적으로 유의미한 차이이다. 위 두 코드는 방식만 다를 뿐 위와 같은 결과값이 나타난다.
> churn.logit.pred <- predict(churn.logit, newdata=churn.test, type='response')
> head(churn.logit.pred)
3334 3335 3336 3337 3338 3339
0.07236813 0.05774332 0.22650409 0.15289153 0.07078500 0.05880824
> churn.logit.pred <- factor(churn.logit.pred > 0.5,
+ levels=c(FALSE, TRUE),
+ labels=c('no', 'yes'))
> head(churn.logit.pred)
3334 3335 3336 3337 3338 3339
no no no no no no
Levels: no yes
> table(churn.logit.pred)
churn.logit.pred
no yes
1595 72
72명이 이탈 고객, 1595명이 미이탈 고객으로 분류되었다.
> table(churn.test$churn, churn.logit.pred,
+ dnn=c('Actual', 'Predicted'))
Predicted
Actual no yes
no 1414 29
yes 181 43
> mean(churn.test$churn==churn.logit.pred) # 0.87
[1] 0.8740252
87.4%
📌 단계별 로지스틱 회귀모델
> churn.logit2 <- step(churn.logit)
> summary(churn.logit2)
Call:
glm(formula = churn ~ international_plan + voice_mail_plan +
number_vmail_messages + total_day_charge + total_eve_minutes +
total_night_charge + total_intl_calls + total_intl_charge +
number_customer_service_calls, family = binomial(link = "logit"),
data = churn.train)
Deviance Residuals:
Min 1Q Median 3Q Max
-2.1204 -0.5133 -0.3375 -0.1969 3.2421
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -8.067161 0.515870 -15.638 < 2e-16 ***
international_planyes 2.040338 0.145243 14.048 < 2e-16 ***
voice_mail_planyes -2.003234 0.572352 -3.500 0.000465 ***
number_vmail_messages 0.035262 0.017964 1.963 0.049654 *
total_day_charge 0.076589 0.006371 12.022 < 2e-16 ***
total_eve_minutes 0.007182 0.001142 6.290 3.17e-10 ***
total_night_charge 0.082547 0.024653 3.348 0.000813 ***
total_intl_calls -0.092176 0.024988 -3.689 0.000225 ***
total_intl_charge 0.326138 0.075453 4.322 1.54e-05 ***
number_customer_service_calls 0.512256 0.039141 13.087 < 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: 2758.3 on 3332 degrees of freedom
Residual deviance: 2161.6 on 3323 degrees of freedom
AIC: 2181.6
Number of Fisher Scoring iterations: 6
1) 오즈비 계산
> exp(coef(churn.logit2))
(Intercept) international_planyes voice_mail_planyes number_vmail_messages total_day_charge
0.0003136727 7.6932116779 0.1348982797 1.0358905790 1.0795984333
total_eve_minutes total_night_charge total_intl_calls total_intl_charge number_customer_service_calls
1.0072083250 1.0860502564 0.9119442135 1.3856065987 1.6690530946
2) 모델의 통계적 유의성 검정
> pchisq(q=2758.293-2161.624, df=3332-3323, lower.tail=FALSE)
[1] 1.086172e-122
> pchisq(q=churn.logit2$null.deviance - churn.logit2$deviance,
+ df=churn.logit2$df.null - churn.logit2$df.residual,
+ lower.tail=FALSE)
[1] 1.086153e-122
p-value=0에 가까운 값이므로 이탈도의 차이는 통계적으로 유의미한 차이이다.
3) 고객 이탈 확률 예측
> churn.logit2.pred <- predict(churn.logit2, newdata=churn.test, type='response')
> head(churn.logit2.pred)
3334 3335 3336 3337 3338 3339
no no no no no no
Levels: no yes
4) 이탈 / 미이탈 분류
> churn.logit2.pred <- factor(churn.logit2.pred > 0.5,
+ levels=c(FALSE, TRUE),
+ labels=c('no', 'yes'))
> head(churn.logit2.pred)
3334 3335 3336 3337 3338 3339
no no no no no no
Levels: no yes
> table(churn.logit2.pred)
churn.logit2.pred
no yes
1590 77
77명이 이탈 고객, 1590명이 미이탈 고객으로 분류되었다.
5) 혼동행렬
> table(churn.test$churn, churn.logit2.pred,
+ dnn=c('Actual', 'Predicted'))
Predicted
Actual no yes
no 1408 35
yes 182 42
6) 예측 정확도
> mean(churn.test$churn==churn.logit2.pred)
[1] 0.869826
0.87%
때로는 관심 있는 특정 예측변수가 사건 발생 확률에 미치는 영향 살펴보는 것도 좋다.
예측변수 변화 수준에 따라 사건 발생 확률이 어떻게 달라지는가?
예측변수가 결과변수 확률에 미치는 영향
- 관심 있는 예측변수 값들이 포함된 데이터 셋 생성
- 값들을 변화시켜가며 어떻게 변화되는지 추정
ex) 고객의 서비스 센터 전화 횟수가 고객 이탈 확률에 미치는 영향에 관심이 있다.
서비스 센터 전화 횟수
> table(churn.test$number_customer_service_calls)
0 1 2 3 4 5 6 7
326 605 368 236 86 30 12 4
> testdata <- data.frame(number_customer_service_calls=c(0:7),
+ international_plan='no',
+ voice_mail_plan='no',
+ number_vmail_messages=mean(churn.test$number_vmail_messages),
+ total_day_charge=mean(churn.test$total_day_charge),
+ total_eve_minutes=mean(churn.test$total_eve_minutes),
+ total_night_charge=mean(churn.test$total_night_charge),
+ total_intl_calls=mean(churn.test$total_intl_calls),
+ total_intl_charge=mean(churn.test$total_intl_charge))
> head(testdata)
number_customer_service_calls international_plan voice_mail_plan number_vmail_messages total_day_charge total_eve_minutes total_night_charge
1 0 no no 7.067786 30.82434 199.9492 8.974559
2 1 no no 7.067786 30.82434 199.9492 8.974559
3 2 no no 7.067786 30.82434 199.9492 8.974559
4 3 no no 7.067786 30.82434 199.9492 8.974559
5 4 no no 7.067786 30.82434 199.9492 8.974559
6 5 no no 7.067786 30.82434 199.9492 8.974559
total_intl_calls total_intl_charge
1 4.346731 2.784421
2 4.346731 2.784421
3 4.346731 2.784421
4 4.346731 2.784421
5 4.346731 2.784421
6 4.346731 2.784421
> testdata$prob <- predict(churn.logit2,
+ newdata=testdata,
+ type='response')
> testdata[c('number_customer_service_calls', 'prob')] # 고객 이탈 확률
number_customer_service_calls prob
1 0 0.05881547
2 1 0.09444949
3 2 0.14827168
4 3 0.22513913
5 4 0.32657688
6 5 0.44733309
7 6 0.57463916
8 7 0.69276138
다른 예측변수들이 일정하다는 가정하에서의 예측확률 변화를 나타낸다.
📌 과산포
📝 과산포 확인하는 방법
> deviance(churn.logit2)/df.residual(churn.logit2)
[1] 0.6505038
0.65로 1보다 작기 때문에 과산포의 위험은 없다.
과산포 여부는 통계적으로 검정도 가능하다.
> fit.origin <- glm(churn ~ international_plan
+ + voice_mail_plan + number_vmail_messages
+ + total_day_charge + total_eve_minutes
+ + total_night_charge + total_intl_calls + total_intl_charge
+ + number_customer_service_calls,
+ family=binomial(),
+ data=churn.train)
> fit.overdis <- glm(churn ~ international_plan
+ + voice_mail_plan + number_vmail_messages
+ + total_day_charge + total_eve_minutes
+ + total_night_charge + total_intl_calls + total_intl_charge
+ + number_customer_service_calls,
+ family=quasibinomial(),
+ data=churn.train)
> pchisq(summary(fit.overdis)$dispersion * fit.origin$df.residual,
+ fit.origin$df.residual, lower.tail=FALSE)
[1] 0.08385493
귀무가설 : 과산포 비율이 1이다.
p-value=0.084이므로 귀무가설을 기각할 수 없다.
즉, 과산포의 가능성은 작다.