Intro

이번 포스팅은 클러스터링 모델링을 수행하는 저의 첫 시도이며, 또한 매우 귀중한 경험이었습니다. 고객사의 데이터를 공개할 수는 없지만, 범주형 변수가 많은 데이터였습니다. 숫자 변수를 이용한 예제는 비교적 쉽게 확인할 수 있겠지만, 범주형 위주의 데이터 관련 예제는 많지가 않아서 조금 고생하였습니다.

내용을 찾다보니, 범주형 데이터를 클러스터링 하는 영역에 대해 합리적인 결과 도출이 안될수도 있다는 굉장히 생산적인 토론 내용을 접하기도 하였습니다. 변수의 갯수가 적다면 클러스터링 보다는 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

fatal_level & carAccidentType 변수 추가하기

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
  • 위 데이터 전체 관측치는 567개이며, 변수의 개수는 9개입니다. 각각의 데이터에 대한 성격은 아래에서 확인하시기를 바랍니다.
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개가 되지 않았습니다. 나머지 데이터는 사실상 큰 의미부여를 하기 힘든 데이터였던 것으로 기억합니다. 이러한 부분은 보통 고객사의 요구 수준에 맞추어 진행하게 됩니다.

Dissimilarity Matrix

library(cluster)
# 위 패키지에는 크게 daisy(), diana(), clusplot() 함수가 내장되어 있습니다. 
# gower 거리를 구하기 위해 daisy() 함수를 사용합니다. 
gower_distance <- daisy(carAccident[, 2:9], metric = c("gower"))
class(gower_distance)
## [1] "dissimilarity" "dist"

관측벡터들간의 거리측정 알고리즘을 여러종류가 있습니다만, 간단한게 설명하면 아래와 같습니다.

  • Euclidean : 두 점 사이의 거리를 구할 때 가장 많이 쓰는 방식으로, 식은 다음과 같습니다.
  • Manhattan : 두 점 사이의 절대적 거리를 이용한 거리 계산 방식으로 다음과 같습니다.
  • Maximum : 두 점 사이의 거리가 좌표 차원에서의 가장 큰 벡터공간에서 정의됩니다.
  • Gower : 양적변수가 포함되어 있을때도 사용할 수 있는 방법으로, 우선 선택된 변수들을 [0,1]사이의 값으로 표준화 시킨 후, 모든 변수들간의 거리를 가중평균하여 합한 값을 사용합니다.

클러스터링 알고리즘(Clustering Algorithms)

보통 클러스터링이라고 하면, K-meansHierarchical Clustering에 대해 접하게 될 것입니다만, 여기에서는 계층적 군집분석에 대해 다루고자 합니다. 계층적 군집분석은 개별 데이터에서 시작해서 유사한 데이터끼리 군집으로 차근차근 묶어가는 기법인 응집형 방법(Aggolomerative, Bottom-Up Method)(이하 AG)과 반대로 데이터를 하나의 군집에 속한다고 놓고, 차근차근 세부 군집으로 나누어가는 분리형 방법(Divisive, Top-Down)(이하 DI)으로 구분할 수 있습니다.

기본 개념도는 다음 그림과 같습니다.

그림-1. Aggolomerative VS. Divisive

그림-1. Aggolomerative VS. Divisive

여기서 연구자가 고민해야 하는 것은, 어떤 방법론으로 접근할까 입니다. 두개가 접근해서, 비교한 후 선택을 해도 좋습니다. 그러나, 일반적으로, AG는 작은단위의 클러스터를 찾고자 할 때 성능이 좋고, DI는 좀 더 큰 단위의 클러스터를 찾고자 할 때 성능이 좋습니다.

연결방법에 관한 논의도 충분히 가능하지만, 이번에는 간단한 개념정리로 대체합니다. 참고

  • Complete : 최장연결법으로, 두 군집간의 최장 거리를 군집간 거리로 정의합니다.
  • Single : 최단연결법으로, 두 군집간의 최단 거리를 군집간 거리로 정의합니다.
  • Ward.D : Ward가 제안한 방법으로, 군집간의 거리보다는 군집내의 편차제곱합에 근거를 두고 군집을 병합하는 방법입니다. 군집을 병합하는 과정에서 생기는 정보의 손실이 최소가 되도록 정의합니다.
  • Ward.D2 : Ward.D 방법에 표준화 수치를 사용한 것으로 절대값 대신 거듭제곱값을 사용합니다.
  • Average : 평균연결법으로 각 군집에 속한 모든 개체들간의 거리의 평균으로 정의합니다.
  • Mcquitty : 산술평균을 이용한 가중 쌍그룹 방법 (Weighted Pair Group Method with Arithmetic Means; WPGMA) 으로, 가장 가까운 두 군집이 합쳐져 하나의 그룹을 형성한 후, 다른 군집과의 거리는 산술평균으로 구합니다.
  • Median : 중앙연결법으로, 군집간의 거리를 군집의 모든 샘플의 중앙값으로 정의하는 것입니다.
  • Centroid : 중심연결법으로, 두 군집간의 거리가 두 군집의 중심간 거리로 정의됩니다. 여기서, s,t 는 각 군집의 중심점을 나타냅니다.

Aggolomerative Hierachical Clustering 알고리즘

# 여기에서는 일반적으로 사용되는 Complete Linkages 활용합니다. 
agg_clust_c <- hclust(gower_distance, method = "complete")
plot(agg_clust_c, main = "Agglomerative, complete linkages")

Divisive Hierachical Clustering 알고리즘

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).

Visualisation

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)

Summary

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))

(계속 진행중)