캐글 포켓몬 데이터가 공개되어 721종류 포켓몬에 대한 데이터와 포켓몬 유형에 대한 정보가 담겨있다.
각 포켓몬에 대한 데이터 원본은 http://pokemondb.net/pokedex에서 확인한다. 전통적인 군집분석에 대해서는 포켓몬 군집분석을 참조한다.
기계학습을 통해 예측모형을 개발할 경우 데이터를 가져와서 전처리 작업을 통한 데이터 정제작업과는 별개로, 데이터 과학자가 데이터와 친숙해지는 과정이 필요하다. 이런 과정을 통해서 새로운 “아하~” 통찰, 인사이트(Insight)를 발견해 낼 수도 있고, 데이터에 더 친숙해지게 된다. 추가로 새로운 피쳐공학(Feature Engineering)을 통해 새로운 변수 및 유의미한 관측점을 추려낼 수 있다.
특히, 차원축소 기법과 함께 군집분석(cluster analysis)이 많이 활용되는 다변량 분석기법 중 하나이다. 군집분석은 관측점(observation)을 유위미한 군집으로 묶어내는데 군집은 공통된 특징(feature)을 공유하게 된다.
군집분석을 위한 데이터를 준비한 후에 전처리 작업을 거쳐서 자료형에 맞는 유사성(Similarity) 측도를 정의해야 한다. 유사성에 기반한 거리가 준비되면 군집 알고리즘을 실행해서 최적의 군집알고리즘을 찾아내게 되면 이를 바탕으로 프로파일링 작업을 수행해서 마무리 한다.
즉, 군집분석에 필요한 데이터를 가져온 후 다양한 군집분석 모형 탐색과정을 거쳐 최적의 군집 알고리즘을 선정하고, 더불어 적절한 군집갯수 \(k\)도 선정한다. 선정된 군집갯수와 군집 알고리즘에 맞춰 데이터에 적합시켜 군집에 데이터를 접목하여 군집특성을 요약하는 절차를 거친다.
군집분석은 크게 계층적 군집분석과 k-평균 군집분석으로 나뉜다. 두가지 군집분석 알고리즘 모두 장단점이 있다.
계층적 군집 알고리즘 | k-평균 군집 알고리즘 | |
---|---|---|
거리 측도 | 거의 모든 거리측도 | 유클리드 거리 |
군집 안정성 | 안정적임 | 유동적임 |
군집수 평가 | 수목도, 실루엣, 팔꿈치 | 실루엣, 팔꿈치 |
계산 복잡도 | 상대적으로 높음 | 상대적으로 낮음 |
포켓몬 데이터를 캐글에서 다운로드 받아 불러온다. 변수명을 변경하고 범주형 데이터에 대해서 범주를 재조정하고, 군집분석에 적합하게 척도(scale)를 각 변수별로 동일하게 재조정하는 전처리 작업을 수행한다.
# 0. 환경설정 --------------------------------------------
library(tidyverse)
library(janitor)
library(factoextra)
library(FactoMineR)
library(clustertend)
library(NbClust)
library(clValid)
library(mclust)
library(pheatmap)
library(ggthemes)
library(extrafont)
library(gridExtra)
library(ggmosaic)
loadfonts()
# 1. 포켓몬 데이터 가져오기 ------------------------------
pkmon_dat <- read_csv("data/Pokemon.csv", col_type = cols()) %>%
dplyr::filter(Name != "Shuckle") %>%
as.data.frame()
pkmon_df <- pkmon_dat %>%
mutate(legend = ifelse(Legendary == "True", "전설", "일반")) %>%
dplyr::select(legend, attack = Attack, defense = Defense, sp_attack = `Sp. Atk`, sp_defense =`Sp. Def`, speed = Speed)
# 2. 데이터 전처리 ---------------------------------------
## 2.1. 척도조정
pkmon_scaled_df <- pkmon_df %>%
select(-legend) %>%
mutate_if(is.integer, scale)
row.names(pkmon_scaled_df) <- pkmon_dat$Name
DT::datatable(pkmon_scaled_df)
범주형 변수도 가변수(dummy variable)를 생성하고 쟈카드 거리(Jaccard distance)를 사용해서 거리를 생성해 낼 수 있다.
library(caret)
pkmon_cat_df <- pkmon_dat %>% tbl_df %>%
clean_names() %>%
select(generation, legendary) %>%
mutate(generation = factor(generation))
pkmon_dummy_df <- predict(dummyVars(~ ., data = pkmon_cat_df, fullRank = TRUE), pkmon_cat_df)
dist(t(pkmon_dummy_df), method="binary")
generation.2 generation.3 generation.4 generation.5
generation.3 1.0000000
generation.4 1.0000000 1.0000000
generation.5 1.0000000 1.0000000 1.0000000
generation.6 1.0000000 1.0000000 1.0000000 1.0000000
legendaryTrue 0.9696970 0.9130435 0.9248555 0.9302326
generation.6
generation.3
generation.4
generation.5
generation.6
legendaryTrue 0.9424460
비지도 학습의 대표적인 알고리즘이 계층적 군집(Hierachical Clustering) 알고리즘과 k-평균(k-means) 알고리즘이다. 계층적 군집(Hierachical Clustering) 알고리즘은 dist()
함수로 변수간 거리를 미리 계산하고 나서 hclust
함수에 넣어 수목도(dendrogram)을 통해 적절한 군집갯수를 확정한다.
높이를 기준으로 7.5를 자를 경우 군집을 3개까지 확보가 가능하다. 물론 cutree
함수를 통해 군집 갯수를 지정해 놓으면 자동으로 특정 갯수(예를 들어, 5개)만큼 선정한다.
계층적 군집법은 다양한 연결방법(linkage)을 제시하고 있어 상황에 맞춰 적절한 연결방식을 선택한다. centroid
는 역전 현상이 나타나 권장되고 있지 않다.
# 3. 비지도 학습 : 계층적 군집(Hierachical Clustering) -----------------------------------------
pkmon_dist <- dist(pkmon_scaled_df)
pkmon_hclust <- hclust(pkmon_dist)
plot(pkmon_hclust)
abline(h=8.5, col="red")
# 높이를 기준으로 군집화
#cutree(pkmon_hclust, h = 220)
# 군집갯수를 기준으로 군집화
#cutree(pkmon_hclust, k = 5)
## 3.1. 다양한 계층적 군집 연결방법 -----------------------------------------------------
pkmon_hclust_complete <- hclust(pkmon_dist, method = "complete")
pkmon_hclust_average <- hclust(pkmon_dist, method = "average")
pkmon_hclust_single <- hclust(pkmon_dist, method = "single")
pkmon_hclust_cut <- cutree(pkmon_hclust_complete, k = 3)
table(pkmon_hclust_cut)
pkmon_hclust_cut
1 2 3
305 359 135
비지도 학습 k-평균법은 전체 군내 제곱합(total within cluster sum of squares)를 최소화하는 방식이 선택되는데 이를 위해서 x축에는 군집갯수, y축에는 전체 군내 제곱합(WSS)를 쭉 시각화해서 팔꿈치처럼 전체 군내 제곱합이 변곡점을 지나 감소하는 지점을 최적 군집갯수로 선정한다.
# 3. 비지도 학습 : k-평균(k-means) -----------------------------------------
# 3.1. 비지도 학습 k-평균 알고리즘 시각화 --------------------------------
par(mfrow = c(2, 3))
for(i in 1:6) {
pkmon_km_out <- kmeans(pkmon_scaled_df, i, nstart=1, iter.max = 50)
plot(pkmon_scaled_df[1:2], col = pkmon_km_out$cluster,
main = paste0("Total Within Sum of Squares: ", round(pkmon_km_out$tot.withinss, 0)),
xlab = "공격력", ylab = "방어력")
}
## 3.2. k-평균 군집갯수 선정 --------------------------------
# 포켓몬 데이터 군집내 전체 제곱합 초기화: 0
pkmon_wss <- 0
# 군집을 1개부터 15개까지 증가
for (i in 1:15) {
pkmon_km_out <- kmeans(pkmon_scaled_df, i, nstart=20, iter.max = 50)
pkmon_wss[i] <- pkmon_km_out$tot.withinss
}
par(mfrow=c(1,2))
par(family = "NanumGothic")
# 군집내 전체 제곱합과 군집갯수 시각화
plot(1:15, pkmon_wss, type = "b",
xlab = "군집(Cluster) 갯수",
ylab = "군집내 전체 제곱합(Within groups sum of squares)")
## 3.3. k-평균 군집화 --------------------------------
k <- 3
pkmon_km_out <- kmeans(pkmon_scaled_df, centers = k, nstart = 20, iter.max = 50)
# 방어와 속도 기준으로 군집 시각화
plot(pkmon_scaled_df[, c("defense", "speed")],
col = pkmon_km_out$cluster,
main = paste("k-평균법을 활용한 포켓몬 군집화:", k, "개 군집"),
xlab = "방어력", ylab = "속도")
주성분분석을 위해 차원축소를 한 후에 계층적 군집분석과 k-평균 군집분석을 수행하고 이를 table
함수를 통해 성능을 비교한다.
# 4. 군집분석 성능비교 ------------------------------
pkmon_pca <- prcomp(pkmon_scaled_df)
pkmon_pca_df <- pkmon_pca$x[,c(1:4)]
pkmon_pca_dist <- dist(pkmon_pca_df)
# 계층적 군집분석 vs. K-평균 군집
pkmon_hclust <- hclust(pkmon_pca_dist, method = "complete")
pkmon_km <- kmeans(pkmon_pca_df, 3, nstart=20, iter.max = 50)
# 3개 군집으로 분류
pkmon_hclust_clusters <- cutree(pkmon_hclust, k=3)
# 군집비교
table(pkmon_km$cluster, pkmon_hclust_clusters)
존재론적인 질문이 될 수 있으나, 군집은 과연 존재하는가? 이러한 철학적인 질문에 대해서 hopkins 통계량, fviz_dist
함수의 시각적인 방법을 통해서 확인할 수 있다. 물론, 질문자체가 철학적이기에 이외에도 다양한 방법론이 존재한다. 예를 들어, CCC를 들 수 있다.
# 1. 포켓몬 데이터 가져오기 ------------------------------
# 2. 군집이 있는가? clustering tendancy ------------------
## 2.1. hopkins 통계량 : H = 0.5 기준
pkmon_scaled_df %>%
clustertend::hopkins(., nrow(pkmon_scaled_df) -1)
$H
[1] 0.2164373
군집이 존재한다고 판단이 되면, 다음으로 드는 의문점은 몇개의 군집이 적절한가라는 문제로 귀결된다. 적절한 군집 식별을 위해서 팔꿈치(elbow) 방법, 실루엣 방법, 갭 방법 등이 개발되었다. 이를 시각화해서 최적의 군집 갯수를 선정한다.
NbClust
팩키지 포함된 다양한 군집방법을 모두 시도해보고 나서 최적의 성능을 보이는 군집갯수를 선정하는 것도 가능하다.
# 3. 군집이 존재한다면 몇개일까? ------------------
## 3.1. 팔꿈치 방법
elbow_g <- pkmon_scaled_df %>%
fviz_nbclust(., kmeans, method = "wss") +
theme_few(base_family = "NanumGothic") +
geom_vline(xintercept = 2, linetype = 2) +
scale_y_continuous(labels = scales::comma ) +
labs(x="군집갯수(k)", y="전체 군집내 제곱합", title="최적 군집갯수", subtitle = "팔꿈치 방법(Elbow method)")
## 3.2. 실루엣 방법
silhouette_g <- pkmon_scaled_df %>%
fviz_nbclust(., kmeans, method = "silhouette") +
theme_few(base_family = "NanumGothic") +
geom_vline(xintercept = 2, linetype = 2) +
scale_y_continuous(labels = scales::comma ) +
labs(x="군집갯수(k)", y="평균 실루엣 폭",
title="최적 군집갯수", subtitle = "실루엣 방법(silhouette method)")
## 3.3. 갭 방법
gap_g <- pkmon_scaled_df %>%
fviz_nbclust(., kmeans, nstart = 25, method = "gap_stat", nboot = 50) +
theme_few(base_family = "NanumGothic") +
geom_vline(xintercept = 2, linetype = 2) +
scale_y_continuous(labels = scales::comma ) +
labs(x="군집갯수(k)", y="갭 통계량(k)",
title="최적 군집갯수", subtitle = "갭 방법(gap method)")
grid.arrange(elbow_g, silhouette_g, gap_g, nrow=3)
## 3.4. NbClust 방법
pkmon_scaled_df %>%
NbClust(., distance = "euclidean", min.nc = 2, max.nc = 10, method = "kmeans") %>%
fviz_nbclust() +
scale_y_continuous(labels = scales::comma ) +
labs(x="군집갯수(k)", y="추천 빈도수",
title="최적 군집갯수", subtitle = "NbClust 방법")
*** : The Hubert index is a graphical method of determining the number of clusters.
In the plot of Hubert index, we seek a significant knee that corresponds to a
significant increase of the value of the measure i.e the significant peak in Hubert
index second differences plot.
*** : The D index is a graphical method of determining the number of clusters.
In the plot of D index, we seek a significant knee (the significant peak in Dindex
second differences plot) that corresponds to a significant increase of the value of
the measure.
*******************************************************************
* Among all indices:
* 11 proposed 2 as the best number of clusters
* 8 proposed 3 as the best number of clusters
* 2 proposed 8 as the best number of clusters
* 2 proposed 10 as the best number of clusters
***** Conclusion *****
* According to the majority rule, the best number of clusters is 2
*******************************************************************
Among all indices:
===================
* 2 proposed 0 as the best number of clusters
* 1 proposed 1 as the best number of clusters
* 11 proposed 2 as the best number of clusters
* 8 proposed 3 as the best number of clusters
* 2 proposed 8 as the best number of clusters
* 2 proposed 10 as the best number of clusters
Conclusion
=========================
* According to the majority rule, the best number of clusters is 2 .
clValid
팩키지는 최적 군집분석 알고리즘과 군집갯수를 선택하도록 개발되었다. 대상 군집 알고리즘(clmethods
)을 쭉 나열하고, 군집갯수(nClust
)도 지정하고 나서, 검증(validation
)을 위한 조건도 지정하면 해당 데이터에 대한 최적 군집 알고리즘과 군집갯수를 제시하여 준다.
# 4. 최적 군집 알고리즘 선택 ------------------
clmethods <- c("hierarchical", "kmeans", "diana", "fanny", "model", "sota", "pam", "clara", "agnes")
clust_algo <- pkmon_scaled_df %>%
sample_frac(0.5) %>%
as.matrix %>%
clValid(., nClust = 2:7,
clMethods = clmethods, validation = "internal")
summary(clust_algo)
Clustering Methods:
hierarchical kmeans diana fanny model sota pam clara agnes
Cluster sizes:
2 3 4 5 6 7
Validation Measures:
2 3 4 5 6 7
hierarchical Connectivity 5.9690 23.1746 23.4286 27.9115 78.2175 84.1159
Dunn 0.1989 0.1489 0.1489 0.1489 0.0959 0.1092
Silhouette 0.4013 0.3207 0.2855 0.2125 0.2367 0.2169
kmeans Connectivity 78.0071 123.0944 152.6683 157.4448 172.6917 207.4095
Dunn 0.0317 0.0726 0.0671 0.0491 0.0758 0.0636
Silhouette 0.2965 0.2689 0.2515 0.2417 0.2262 0.1916
diana Connectivity 96.3552 107.3444 149.6655 196.5766 239.0496 274.4230
Dunn 0.0534 0.0572 0.0731 0.0496 0.0524 0.0540
Silhouette 0.2933 0.2770 0.2370 0.2031 0.1492 0.1492
fanny Connectivity 72.0250 NA NA NA NA NA
Dunn 0.0490 NA NA NA NA NA
Silhouette 0.2945 NA NA NA NA NA
model Connectivity 238.5508 199.7754 275.8369 400.3933 331.1250 423.2274
Dunn 0.0318 0.0374 0.0175 0.0267 0.0230 0.0287
Silhouette 0.1167 0.1673 0.0807 0.0361 0.1039 -0.0071
sota Connectivity 81.7258 142.9468 156.4853 185.9992 201.7143 209.3365
Dunn 0.0317 0.0387 0.0387 0.0412 0.0412 0.0412
Silhouette 0.2966 0.2659 0.2488 0.2064 0.2021 0.1941
pam Connectivity 61.9456 123.4163 150.0821 186.9901 214.6357 260.0198
Dunn 0.0446 0.0575 0.0584 0.0555 0.0432 0.0394
Silhouette 0.2781 0.2198 0.2265 0.2185 0.2039 0.1703
clara Connectivity 60.7242 131.5472 191.7774 200.2841 274.2960 264.1591
Dunn 0.0508 0.0494 0.0477 0.0270 0.0162 0.0340
Silhouette 0.2766 0.2324 0.2151 0.2166 0.1652 0.1776
agnes Connectivity 5.9690 23.1746 23.4286 27.9115 78.2175 84.1159
Dunn 0.1989 0.1489 0.1489 0.1489 0.0959 0.1092
Silhouette 0.4013 0.3207 0.2855 0.2125 0.2367 0.2169
Optimal Scores:
Score Method Clusters
Connectivity 5.9690 hierarchical 2
Dunn 0.1989 hierarchical 2
Silhouette 0.4013 hierarchical 2
최적 군집 알고리즘과 군집갯수가 선정되면 “군집분석 알고리즘 적합”하는 과정을 거치게 된다. 군집 2개를 갖는 계층적 군집분석 알고리즘이 선정된 군집분석 알고리즘이기 때문에, 이를 포켓몬 데이터 적합시킨다. 30개 포켓몬 표본을 뽑아 수목도(dendogram)도 그려보고, fviz_cluster
함수로 시각화도 실행해 본다.
# 1. 포켓몬 데이터 가져오기 ------------------------------
# pkmon_scaled_df <- readRDS("data_preprocessed/pkmon_scaled_df.rds")
# 2. 최적 군집 알고리즘 적합: 계층적 군집 ------------------
pkmon_smpl_df <- pkmon_scaled_df %>%
sample_n(30)
pkmon_hclust <- pkmon_smpl_df %>%
get_dist(method = "euclidean") %>%
hclust(method = "ward.D2")
# 3. 군집분석 시각화 ------------------
## 3.1. 수목도(dendogram)
fviz_dend(pkmon_hclust, k = 2,
cex = 0.5,
k_colors = c("#2E9FDF", "#FC4E07"),
color_labels_by_k = TRUE,
rect = TRUE) +
theme_void(base_family="NanumGothic") +
labs(title="포켓몬 캐릭터 군집분석", subtitle="계층적 군집분석 알고리즘 - 군집 2개", y="")
## 3.2. 군집 시각화
pkmon_grp <- cutree(pkmon_hclust, k = 2)
fviz_cluster(list(data = pkmon_smpl_df, cluster = pkmon_grp),
palette = c("#2E9FDF", "#FC4E07"),
ellipse.type = "convex",
repel = TRUE,
show.clust.cent = FALSE, ggtheme = theme_minimal(base_family = "NanumGothic")) +
labs(title="포켓몬 군집 그래프", subtitle ="계층적 군집분석 알고리즘 - 군집 2개")
군집분석을 관측점 관점과 변수관점으로 열지도(heatmap)를 생성하여 면밀하게 살펴본다. 예쁜 열지도(pheatmap
)를 통해 정적 열지도도 그려보고, d3heatmap
을 통해 인터랙티브 열지도도 그려본다.
군집 알고리즘을 통해 해당 관측점에 대한 군집에 대한 배정이 완료되었다면, 이제는 해당 군집별 특성을 요약하는 작업을 수행한다.
군집 2개를 갖는 계층적 군집분석 알고리즘 적합시킨 데이터를 원본 pkmon_df
데이터프레임과 병합한다. 병합된 pkmon_full_df
데이터프레임에 대해 군집특성요약을 위해 추가로 필요한 데이터 정제작업도 수행한다.
군집 2개를 갖는 계층적 군집분석 알고리즘 적합시킨 데이터를 원본 pkmon_df
데이터프레임과 병합한다. 병합된 pkmon_full_df
데이터프레임에 대해 군집특성요약을 위해 추가로 필요한 데이터 정제작업도 수행한다.
# 4. 군집자체 분석 ------------------
## 4.1. 군집데이터 준비
pkmon_full_hclust <- pkmon_scaled_df %>%
get_dist(method = "euclidean") %>%
hclust(method = "ward.D2")
pkmon_full_grp <- cutree(pkmon_full_hclust, k = 2) %>% as_data_frame()
pkmon_full_df <- bind_cols(pkmon_df, pkmon_full_grp) %>%
select(group=value, everything())
# 5. 데이터 내보내기 ------------------
# saveRDS(pkmon_full_df, "data_preprocessed/pkmon_full_df.rds")
병렬그래프(parallel plot)를 통해 군집을 잘 식별하는 변수가 어떤 것인지 활용한다.
# 2. 포켓몬 군집 해석 ------------------
## 2.1. 전체적인 능력값
pkmon_full_df %>%
mutate(ID = 1:n()) %>%
gather(key, value, -group, -legend, -ID) %>%
ggplot(aes(x = key, y = value, color= as.factor(group), group = ID)) +
geom_path(alpha = 0.5, lineend = 'round', linejoin = 'round') +
geom_point(size=1, shape=21, colour="grey50") +
scale_fill_manual(values=c("red","blue")) +
theme_bw(base_family = "NanumGothic") +
labs(x="", y="능력치", color="군집", title="포켓몬 캐릭터 군집 평행그림")
군집은 태생이 범주형인데, 범주형 변수가 포함된 경우 군집과 교차하여 군집별 특성도 함께 살펴본다. 이를 위해서 모자이크 그래프를 활용한다.
## 2.2. 범주형 변수에 대한 군집
pkmon_full_df %>%
mutate(group = factor(group),
legend = factor(legend)) %>%
ggplot() +
geom_mosaic(aes(x = product(legend, group), fill=product(group)), na.rm=TRUE, divider=mosaic("v")) +
theme_minimal(base_family = "NanumGothic") +
labs(x="전설여부", title='포켓몬 전설, 군집') +
theme(legend.position = "right")
연속형 변수와 군집을 밀도 그래프와 교차하여 각 군집별 특성도 각 변수별로 시각화하여 이해한다.
## 2.3. 단변량 비교
pkmon_full_df %>%
mutate(group = factor(group),
legend = factor(legend)) %>%
ggplot(aes(attack, colour = group)) +
geom_density() +
theme_minimal(base_family = "NanumGothic") +
labs(x="공격", y="분포밀도", title='포켓몬 공격력', color="군집") +
theme(legend.position = "right")
density_univariate <- function(df, var, var_name) {
df %>%
mutate(group = factor(group),
legend = factor(legend)) %>%
ggplot(aes_string(var)) +
geom_density(aes(colour = group)) +
theme_minimal(base_family = "NanumGothic") +
labs(x="공격", y="분포밀도", title=var_name, color="군집") +
theme(legend.position = "none")
}
attack_g <- density_univariate(pkmon_full_df, "attack", "공격력")
defense_g <- density_univariate(pkmon_full_df, "defense", "방어력")
sp_attack_g <- density_univariate(pkmon_full_df, "sp_attack", "특수공격력")
sp_defense_g <- density_univariate(pkmon_full_df, "sp_defense", "특수방어력")
speed_g <- density_univariate(pkmon_full_df, "speed", "속도")
grid.arrange(attack_g, defense_g, sp_attack_g, sp_defense_g,speed_g, nrow=2)