이번 포스팅은 클러스터링 모델링을 수행하는 저의 첫 시도이며, 또한 매우 귀중한 경험이었습니다. 고객사의 데이터를 공개할 수는 없지만, 범주형 변수가 많은 데이터였습니다. 숫자 변수를 이용한 예제는 비교적 쉽게 확인할 수 있겠지만, 범주형 위주의 데이터 관련 예제는 많지가 않아서 조금 고생하였습니다.
내용을 찾다보니, 범주형 데이터를 클러스터링 하는 영역에 대해 합리적인 결과 도출이 안될수도 있다는 굉장히 생산적인 토론 내용을 접하기도 하였습니다. 변수의 갯수가 적다면 클러스터링 보다는 Cohort Analysis가 더 적합할수도 있습니다. 코호트 분석은 홈페이지 분석의 예를 드련, 특정 기간 동안 같은 특성을 가진 사용자 집단을 그룹으로 나눠서 분석하는 방법입니다.
그러나, 만약 각각 5개의 레벨을 가진 22개의 변수를 예로 든다고 하면, 어떤 변수가 있는지 확인해야 합니다. 그러면, 22x5코호트를 가지고 있을수도 있는데, 합리적이지 않을 것입니다.
참고로 전, 전체 2만건의 데이터 중 약 570개의 관측치와 약 40개의 변수로 일단 추출하였습니다. 레벨역시, 굉장히 다양하였습니다.
클러스터링 프로세스는 다음과 같은 세가지의 고유한 단계가 포함됩니다. 1. 서로 다른 매트릭스(Disimilarity Matrix)를 계산하는 것은 클러스터링에서 가장 중요한 사항입니다. 2. 클러스터링 방법 선택 3. 클러스터에 대한 평가
이번 포스팅은 입문 과정이라고 보시면 될 거 같습니다.
이 부분이 클러스터링의 중추입니다. 두개의 매트릭스를 계산할 때, 서로 다른 데이터의 집합의 점이 서로 얼마나 다르거나 멀리 떨어져 있는지를 수학적으로 표현하는 것이며, 나중에 가장 가까운 점을 함께 그룹화하거나 가장 먼 것을 분리할 수 있는데, 이것이 클러스터링의 핵심 개념입니다.
개별 데이터 포인트간의 거리에 기초합니다. 수치 데이터 포인트 사이의 거리를 상상하는 것은 꽤 쉬운 작업처럼 보이기도 합니다 (유클레디안 거리), 범주형 데이터는 명확해 보이지는 않습니다. 이 경우에 다른 매트릭스를 계산하기 위해서는 Gower 거리 방법론을 사용해야 합니다. R에서 클러스터 패키지에 Gower에 대한 설명이 있습니다. 참고하시기를 바랍니다.
library(tidyverse)
# 샘플 재사용성
set.seed(20190515)
# 변수 만들기
# ID
ids <- c(1:567) %>% factor()
# Weeks
weeks <- sample(c("월요일", "화요일", "수요일", "목요일", "금요일"),
size = 567,
replace = TRUE,
prob = c(0.3, 0.2, 0.3, 0.1, 0.1)) %>%
factor(levels = c("월요일", "화요일", "수요일", "목요일", "금요일"),
ordered = TRUE)
# city
city <- sample(c("서울", "경기", "부산", "강원", "대전"),
size = 567,
replace = TRUE,
prob = c(0.25, 0.25, 0.1, 0.3, 0.1)) %>% factor()
# departmnet
dpart <- sample(c("영업", "기획", "분석", "서비스", "개발", "경영", "디자인"),
size = 567,
replace = TRUE,
prob = c(0.25, 0.15, 0.05, 0.15, 0.1, 0.2, 0.1)) %>% factor()
# carType
carType <- sample(c("승용차", "경차", "트럭", "오토바이"),
size = 567,
replace = TRUE,
prob = c(0.4, 0.4, 0.1, 0.1))
# injured
injured <- sample(c(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
size = 567,
replace = TRUE,
prob = c(0.4, 0.2, 0.1, 0.5, 0.5, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01))
# dead
dead <- sample(c(0, 1, 2, 3),
size = 567,
replace = TRUE,
prob = c(0.7, 0.2, 0.05, 0.05))
carAccident <- data.frame(ids, weeks, city, dpart, carType, injured, dead)
6개의 변수를 만들고, 부상자 및 사망자수에 따른 fatal_level을 적용할 예정입니다.
library(knitr)
## Warning: package 'knitr' was built under R version 3.5.3
library(kableExtra)
## Warning: package 'kableExtra' was built under R version 3.5.3
carAccident %>%
head() %>%
kable() %>%
kable_styling(bootstrap_options = "striped", full_width = F)
| ids | weeks | city | dpart | carType | injured | dead |
|---|---|---|---|---|---|---|
| 1 | 수요일 | 경기 | 경영 | 승용차 | 0 | 1 |
| 2 | 목요일 | 대전 | 영업 | 승용차 | 0 | 1 |
| 3 | 수요일 | 경기 | 영업 | 승용차 | 4 | 0 |
| 4 | 수요일 | 서울 | 서비스 | 승용차 | 1 | 0 |
| 5 | 수요일 | 부산 | 경영 | 오토바이 | 4 | 0 |
| 6 | 수요일 | 강원 | 경영 | 승용차 | 0 | 0 |
carAccident <- carAccident %>%
mutate(
fatal_level = case_when(
injured == 0 & dead == 0 ~ "상해없음",
injured <= 2 & dead == 0 ~ "경상해",
injured > 2 & dead == 0 ~ "중상해",
dead > 0 ~ "치명적상해"
)
) %>%
mutate_if(is.character, as.factor)
carAccident$carAccidentType <- sample(c("car_car", "car_human", "car_only"),
size = 567,
replace = TRUE,
prob = c(0.5, 0.3, 0.2)) %>% factor()
carAccident %>%
head() %>%
kable() %>%
kable_styling(bootstrap_options = "striped", full_width = F)
| ids | weeks | city | dpart | carType | injured | dead | fatal_level | carAccidentType |
|---|---|---|---|---|---|---|---|---|
| 1 | 수요일 | 경기 | 경영 | 승용차 | 0 | 1 | 치명적상해 | car_only |
| 2 | 목요일 | 대전 | 영업 | 승용차 | 0 | 1 | 치명적상해 | car_human |
| 3 | 수요일 | 경기 | 영업 | 승용차 | 4 | 0 | 중상해 | car_car |
| 4 | 수요일 | 서울 | 서비스 | 승용차 | 1 | 0 | 경상해 | car_human |
| 5 | 수요일 | 부산 | 경영 | 오토바이 | 4 | 0 | 중상해 | car_human |
| 6 | 수요일 | 강원 | 경영 | 승용차 | 0 | 0 | 상해없음 | car_only |
glimpse(carAccident)
## Observations: 567
## Variables: 9
## $ ids <fct> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,...
## $ weeks <ord> 수요일, 목요일, 수요일, 수요일, 수요일, 수요일, 월요일, 수요일, 화요일, 월...
## $ city <fct> 경기, 대전, 경기, 서울, 부산, 강원, 대전, 강원, 부산, 대전, 강원, 강원...
## $ dpart <fct> 경영, 영업, 영업, 서비스, 경영, 경영, 개발, 경영, 분석, 영업, 경영, 디...
## $ carType <fct> 승용차, 승용차, 승용차, 승용차, 오토바이, 승용차, 트럭, 경차, 오토바이, 오...
## $ injured <dbl> 0, 0, 4, 1, 4, 0, 0, 3, 3, 3, 4, 1, 3, 4, 1, 3...
## $ dead <dbl> 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0...
## $ fatal_level <fct> 치명적상해, 치명적상해, 중상해, 경상해, 중상해, 상해없음, 치명적상해, 중상해,...
## $ carAccidentType <fct> car_only, car_human, car_car, car_human, car_h...
실무에서는 최초 데이터는 매우 지저분하게 되어 있을 가능성이 큽니다. 바로 모델링을 진행하기에 앞서서, 데이터 전처리 및 고객사에 맞는 데이터 필터링이 매우 중요합니다. 제 경우에는 전체 2만개의 데이터 중, 실제로 고객사가 필요한 데이터는 600개가 되지 않았습니다. 나머지 데이터는 사실상 큰 의미부여를 하기 힘든 데이터였던 것으로 기억합니다. 이러한 부분은 보통 고객사의 요구 수준에 맞추어 진행하게 됩니다.
library(cluster)
# 위 패키지에는 크게 daisy(), diana(), clusplot() 함수가 내장되어 있습니다.
# gower 거리를 구하기 위해 daisy() 함수를 사용합니다.
gower_distance <- daisy(carAccident[, 2:9], metric = c("gower"))
class(gower_distance)
## [1] "dissimilarity" "dist"
관측벡터들간의 거리측정 알고리즘을 여러종류가 있습니다만, 간단한게 설명하면 아래와 같습니다.
보통 클러스터링이라고 하면, K-means과 Hierarchical Clustering에 대해 접하게 될 것입니다만, 여기에서는 계층적 군집분석에 대해 다루고자 합니다. 계층적 군집분석은 개별 데이터에서 시작해서 유사한 데이터끼리 군집으로 차근차근 묶어가는 기법인 응집형 방법(Aggolomerative, Bottom-Up Method)(이하 AG)과 반대로 데이터를 하나의 군집에 속한다고 놓고, 차근차근 세부 군집으로 나누어가는 분리형 방법(Divisive, Top-Down)(이하 DI)으로 구분할 수 있습니다.
기본 개념도는 다음 그림과 같습니다.
그림-1. Aggolomerative VS. Divisive
여기서 연구자가 고민해야 하는 것은, 어떤 방법론으로 접근할까 입니다. 두개가 접근해서, 비교한 후 선택을 해도 좋습니다. 그러나, 일반적으로, AG는 작은단위의 클러스터를 찾고자 할 때 성능이 좋고, DI는 좀 더 큰 단위의 클러스터를 찾고자 할 때 성능이 좋습니다.
연결방법에 관한 논의도 충분히 가능하지만, 이번에는 간단한 개념정리로 대체합니다. 참고
# 여기에서는 일반적으로 사용되는 Complete Linkages 활용합니다.
agg_clust_c <- hclust(gower_distance, method = "complete")
plot(agg_clust_c, main = "Agglomerative, complete linkages")
divisive_clust <- diana(as.matrix(gower_distance),
diss = TRUE, keep.diss = TRUE)
plot(divisive_clust, main = "Divisive")
여기에서 각각의 클러스터링 알고리즘과 클러스터의 수를 결정해야 합니다. 다른 통계분석과 마찬가지로 클러스터링 알고리즘의 평가방법에는 한 가지 이상의 방법이 있습니다. 그런데, 여기에서 연구자의 판단이 중요합니다. 클러스터의 수는 실용적으로 만들어야 하며, 데이터가 그룹으로 나누어져 있을 때, 의미가 있습니다. 범주형 변수를 사용하면 비표준 클러스터로 끝날 수 있습니다. 그 이유는 값의 조합이 제한되어 있기 때문입니다.
우선, 클러스터가 생성이 될 때, 그룹간 거리가 분리된 그룹간의 거리는 충분히 커지는 동안에는 클러스터 내에서의 거리는 최소화됩니다. 직관적으로 쉽게 이해가 가능합니다. 포인트들 사이에서의 거리는 서로 다른 행렬 매트릭스에서 추출된 비유사성에 대한 하나의 측정방법입니다. 따라서, 클러스터링 평가는 군집과 분리에 대한 평가입니다.
크게 두가지 접근방법이 있습니다. - Elbow Method: 그룹내 유사성이 분석에 중요하다고 판단될 때 시작합니다. - Silhouette Method: 데이터 일관성의 척도로, 한 클러스터의 포인트가 이웃 클러스터의 포인터에 얼마나 가까운지 측정 합니다.
실무에서는 두가지 방법이 다른 결과를 제공할 가능성이 매우 높습니다. 즉, 서로 다른 수의 클러스터가 가장 군집이 잘되거나 또는 가장 특이하게 분리된 클러스터에 해당하기 때문에 데이터가 실제로 무엇인지 이해할 수 있는 연구자의 판단력이 중요합니다.
여기에서는 Silhouette 방법을 사용하려고 합니다. 자세한 내용은 제가 읽은 다른 글을 참조하시기를 바랍니다 (Clustering on mixed type data).
sil_width <- c(NA)
for(i in 2:8) {
pam_fit <- pam(gower_distance, diss = TRUE, k = i)
sil_width[i] <- pam_fit$silinfo$avg.width
}
plot(1:8, sil_width,
xlab = "Number of clusters",
ylab = "Silhouette Width")
lines(1:8, sil_width)
k <- 2
pam_fit <- pam(gower_distance, diss = TRUE, k)
pam_results <- carAccident %>%
mutate(cluster = pam_fit$clustering) %>%
group_by(cluster) %>%
do(the_summary = summary(.))
pam_results$the_summary
## [[1]]
## ids weeks city dpart carType
## 1 : 1 월요일:55 강원:38 개발 :18 경차 : 47
## 2 : 1 화요일:41 경기:57 경영 :57 승용차 :119
## 4 : 1 수요일:65 대전:12 기획 :37 오토바이: 14
## 5 : 1 목요일:17 부산:24 디자인:24 트럭 : 24
## 6 : 1 금요일:26 서울:73 분석 : 9
## 7 : 1 서비스:32
## (Other):198 영업 :27
## injured dead fatal_level carAccidentType
## Min. :0.000 Min. :0.0000 경상해 : 26 car_car : 46
## 1st Qu.:1.000 1st Qu.:0.0000 상해없음 : 27 car_human:111
## Median :2.000 Median :1.0000 중상해 : 28 car_only : 47
## Mean :2.181 Mean :0.9167 치명적상해:123
## 3rd Qu.:4.000 3rd Qu.:1.0000
## Max. :9.000 Max. :3.0000
##
## cluster
## Min. :1
## 1st Qu.:1
## Median :1
## Mean :1
## 3rd Qu.:1
## Max. :1
##
##
## [[2]]
## ids weeks city dpart carType
## 3 : 1 월요일:108 강원:141 개발 : 28 경차 :183
## 8 : 1 화요일: 69 경기: 81 경영 : 49 승용차 :108
## 9 : 1 수요일:105 대전: 35 기획 : 59 오토바이: 41
## 10 : 1 목요일: 34 부산: 47 디자인: 32 트럭 : 31
## 11 : 1 금요일: 47 서울: 59 분석 : 19
## 12 : 1 서비스: 63
## (Other):357 영업 :113
## injured dead fatal_level carAccidentType
## Min. : 0.00 Min. :0.0000 경상해 : 47 car_car :224
## 1st Qu.: 1.00 1st Qu.:0.0000 상해없음 : 61 car_human: 61
## Median : 3.00 Median :0.0000 중상해 :218 car_only : 78
## Mean : 2.78 Mean :0.1405 치명적상해: 37
## 3rd Qu.: 4.00 3rd Qu.:0.0000
## Max. :10.00 Max. :3.0000
##
## cluster
## Min. :2
## 1st Qu.:2
## Median :2
## Mean :2
## 3rd Qu.:2
## Max. :2
##
library(Rtsne)
## Warning: package 'Rtsne' was built under R version 3.5.3
tsne_obj <- Rtsne(gower_distance, is_distance = TRUE)
tsne_data <- tsne_obj$Y %>%
data.frame() %>%
setNames(c("X", "Y")) %>%
mutate(cluster = factor(pam_fit$clustering))
ggplot(aes(x = X, y = Y), data = tsne_data) +
geom_point(aes(color = cluster))
(계속 진행중)