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를 부여하는 알고리듬이기 때문입니다.