library(tidyverse)
library(magrittr)
library(clustMixType) #which has kproto clustering
dat <- mtcars %>% as_data_frame()
dat[, c(2, 8:11)] %<>% mutate_all(as.factor) # categorical variables
knitr::kable(head(dat))
| mpg | cyl | disp | hp | drat | wt | qsec | vs | am | gear | carb |
|---|---|---|---|---|---|---|---|---|---|---|
| 21.0 | 6 | 160 | 110 | 3.90 | 2.620 | 16.46 | 0 | 1 | 4 | 4 |
| 21.0 | 6 | 160 | 110 | 3.90 | 2.875 | 17.02 | 0 | 1 | 4 | 4 |
| 22.8 | 4 | 108 | 93 | 3.85 | 2.320 | 18.61 | 1 | 1 | 4 | 1 |
| 21.4 | 6 | 258 | 110 | 3.08 | 3.215 | 19.44 | 1 | 0 | 3 | 1 |
| 18.7 | 8 | 360 | 175 | 3.15 | 3.440 | 17.02 | 0 | 0 | 3 | 2 |
| 18.1 | 6 | 225 | 105 | 2.76 | 3.460 | 20.22 | 1 | 0 | 3 | 1 |
# for standardization
scale_2sd <- function(x) {
return((x - mean(x))/(2 * sd(x)))
}
dat_std <- dat %>% mutate_if(is.numeric, scale_2sd) #standardization for numerical variable only
knitr::kable(dat_std %>% head())
| mpg | cyl | disp | hp | drat | wt | qsec | vs | am | gear | carb |
|---|---|---|---|---|---|---|---|---|---|---|
| 0.0754424 | 6 | -0.2853099 | -0.2675464 | 0.2837568 | -0.3051998 | -0.3885826 | 0 | 1 | 4 | 4 |
| 0.0754424 | 6 | -0.2853099 | -0.2675464 | 0.2837568 | -0.1748926 | -0.2318904 | 0 | 1 | 4 | 4 |
| 0.2247717 | 4 | -0.4950910 | -0.3915202 | 0.2369998 | -0.4585023 | 0.2130034 | 1 | 1 | 4 | 1 |
| 0.1086267 | 6 | 0.1100468 | -0.2675464 | -0.4830588 | -0.0011498 | 0.4452436 | 1 | 0 | 3 | 1 |
| -0.1153673 | 8 | 0.5215406 | 0.2064711 | -0.4175989 | 0.1138271 | -0.2318904 | 0 | 0 | 3 | 2 |
| -0.1651437 | 6 | -0.0230835 | -0.3040093 | -0.7823039 | 0.1240473 | 0.6634934 | 1 | 0 | 3 | 1 |
dat_std_coding <- model.matrix(~. - 1, dat_std) # one-hot coding
knitr::kable(dat_std_coding %>% head())
| mpg | cyl4 | cyl6 | cyl8 | disp | hp | drat | wt | qsec | vs1 | am1 | gear4 | gear5 | carb2 | carb3 | carb4 | carb6 | carb8 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.0754424 | 0 | 1 | 0 | -0.2853099 | -0.2675464 | 0.2837568 | -0.3051998 | -0.3885826 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
| 0.0754424 | 0 | 1 | 0 | -0.2853099 | -0.2675464 | 0.2837568 | -0.1748926 | -0.2318904 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
| 0.2247717 | 1 | 0 | 0 | -0.4950910 | -0.3915202 | 0.2369998 | -0.4585023 | 0.2130034 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0.1086267 | 0 | 1 | 0 | 0.1100468 | -0.2675464 | -0.4830588 | -0.0011498 | 0.4452436 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| -0.1153673 | 0 | 0 | 1 | 0.5215406 | 0.2064711 | -0.4175989 | 0.1138271 | -0.2318904 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| -0.1651437 | 0 | 1 | 0 | -0.0230835 | -0.3040093 | -0.7823039 | 0.1240473 | 0.6634934 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
# k-means (with arbitrary k=3)
set.seed(12)
km <- kmeans(dat_std_coding, centers = 3, iter.max = 200)
# k-proto type from the package
set.seed(12)
kp <- kproto(dat_std %>% as.data.frame(), k = 3)
## Estimated lambda: 0.4196721
km$centers
## mpg cyl4 cyl6 cyl8 disp hp drat wt
## 1 -0.02885107 0 1 0 -0.1912542 -0.1779519 -0.01014461 -0.05115559
## 2 0.54530181 1 0 0 -0.5066437 -0.4670978 0.44358094 -0.47601596
## 3 -0.41402588 0 0 1 0.4937043 0.4559814 -0.34345558 0.39959034
## qsec vs1 am1 gear4 gear5 carb2 carb3
## 1 0.03592528 0.5714286 0.4285714 0.5714286 0.1428571 0.0000000 0.0000000
## 2 0.36053824 0.9090909 0.7272727 0.7272727 0.1818182 0.5454545 0.0000000
## 3 -0.30124268 0.0000000 0.1428571 0.0000000 0.1428571 0.2857143 0.2142857
## carb4 carb6 carb8
## 1 0.5714286 0.1428571 0.00000000
## 2 0.0000000 0.0000000 0.00000000
## 3 0.4285714 0.0000000 0.07142857
kp$centers
## mpg cyl disp hp drat wt
## 3 0.04972459 6 -0.2486789 -0.2602538 0.04155533 -0.07907855
## 26 0.66238957 4 -0.5531339 -0.4726501 0.54910310 -0.60043490
## 29 -0.41402588 8 0.4937043 0.4559814 -0.34345558 0.39959034
## qsec vs am gear carb
## 3 0.2871524 1 0 4 4
## 26 0.1682342 1 1 4 1
## 29 -0.3012427 0 0 3 4
mpg의 경우를 보면 km 과 kp의 center값이 나름 유사함을 알 수 있습니다. 또한 cyl값을 보아도, km에서는 cyl4,6,8이 center 1,2,3에서 각각 cyl6, cyl4,cyl8로 나타났는데 이는 kp에서 cyl의 center가 6,4,8로 나온것과 일치함을 알 수 있습니다.
km의 vs1을 살펴보면 cetner 1과 2에서는 vs1값이 0.5 보다 크므로 vs1, center 3에서는 vs1값이 0.5 보다 작으므로 vs0으로 표시되고, 이는 kp에서 1,1,0으로 center가 나타난 것과 동일합니다.
plot(1:32, km$cluster, main = "black=kmeans, blue=kproto")
points(1:32, kp$cluster, pch = 4, col = "blue")
각각의 cetner들을 그래프로 나타내보면 몇 가지 빼고는 두 알고리듬의 center가 일치하는 것을 볼 수 있습니다. 사실 이 데이터셋이 클러스터링을 하기에 좋은 데이터도 아니고, seed를 다르게 해줄때마다 center와 cluster가 계속 바뀌는 것을 볼 수있습니다. non-identified solution이고 좋은 데이터셋과 좋은 해결 방법이 아닙니다만, 단순히 여기서는 k-proto를 쓰는 것과 k-means를 쓰는것이 원리로는 특별히 다를 것이 없음을 보여주기 위함입니다.
kproto를 쓴 package (clustMixType) 을 보면 total distance를 다음과 같이 정의합니다.
\[d(x,y)=d_{euclid}(x,y)+\lambda d_{simple matching}(x,y)\]
여기서 simple matching이라는 것은 categorical variable이 일치하면 distance=0, 불일치하면 distance=1을 주는 것인데, k-means에서 one-hot-coding을 하게되면 사실은 이것과 똑같은 결과를 낫습니다. 여기서 \(\lambda\)를 주는 것은 (논문을 읽지 않아서 확실치 않습니다만) categorical var. 과 numerical var. 사이의 절대적인 거리의 차가 차이가 크게 나는 것을 보정해주는 것 같습니다. one-hot-coding에서는 distance가 아무리 커봐야 1이지만, numerical variable은 그보다 더 클수 있죠.
이 예시에서 저는 numeircal variable을 정규화(scale)해서 그 문제를 해결하려고 하였습니다.
Gaussian mixture 모델을 probability 모델로 이해하면 k-means와 그 아류작들을 이해하는데 도움이 되지 않을 까 싶습니다. 결국에는 각 center가 있고, 그 center의 중심에서 원들을 그릴때, 각 데이터 포인트가 얼마나 그 중심에 가까이 들어올 것인가를 확률로 나타내어 가장 확률이 큰 center로 k를 부여하는 알고리듬이기 때문입니다.