지리공간 데이터 (GeoSpatial data)를 처리하고 분석하는데 있어서 첫번째 관문이자 큰 도전사항 중에 하나가 지리공간 데이터 포맷이 매우 다양하다는 것입니다. 


아래에 다양한 지리공간 데이터(various GeoSpatial data foramts)의 리스트를 소개하고, 특히 이중에서 점, 선, 다각형으로 구성된 벡터 데이터 포맷의 이미지 시각화를 예시로 보였습니다. 지리공간 데이터 포맷이 상당히 많지요?


이들 지리공간 데이터 포맷별로 데이터를 DB나 R로 불러오기 (importing)할 때 사용하는 DB utility tools 이나 R의 package가 달라지다 보니 번거롭고 또 어려운 점이 있습니다.



[ 다양한 지리공간 데이터 포맷 (various GeoSpatial data formats) ]




R을 활용한 지리공간 데이터의 처리 및 분석, 시각화를 본격적으로 들어가기 전에 먼저, 이들 지리공간 데이터 포맷들 중에서 특히 벡터 데이터(Vector data)와 레스트 데이터 (Raster data) 모델에 대해서 이들이 무엇이고, 어떻게 활용이 되며, 무슨 R 패키지를 사용해서 분석할 수 있는지에 대해서 알아보겠습니다.


[ 지리공간 벡터 데이터(Vector data) vs. 레스터 데이터 (Raster data) ]






  (1) 지리공간 벡터 데이터 (Vector data)


벡터 데이터에는 KML(.kml or .kmz), GML, GeoJSON, Shapefile (.shp), WKT 등의 데이터 포맷이 있습니다.


KML (Keyhole Markup Language), GML (Geography Markup Language) 데이터 포맷은 XML 기반으로 지리공간 데이터를 저장합니다. KML은 OGC(Open Geospatil Consortium)의 공식 표준입니다. KML과 GML 데이터 포맷은 non-GIS 사용자들과 인터넷을 통해 쉽게 지리공간 데이터를 공유하는데 많이 사용됩니다.


GeoJSON 데이터 포맷은 이름에서 짐작할 수 있듯이 JSON 기반으로 간단한 지리공간 데이터와 그 외 일반 데이터를 저장합니다. GeoJSON 데이터는 인터넷으로 지리공간 & 일반 데이터를 공유하는데 역시 많이 사용됩니다.


Shapefile 데이터 포맷은 GIS (Geographic Information System) 소프트웨어를 위한 지리공간 벡터 데이터입니다. Shapefile 은 GIS 의 국제적인 제공사인 Esri(Environmental Systems Research Institute)에서 개발하고 관리하며, GIS 소프트웨어 간 상호운용성(interoperability)를 보장합니다.


WKT 데이터 포맷은 Well-Known Text 의 약자로서, 벡터 지리공간 데이터를 표현하는데 텍스트 마크업 언어(Text Markup Language)를 사용합니다. WKB (Well-Known Bianry)는 WKT와 같은 정보를 저장하는데 있어 이진(binary) 포맷을 사용해 보다 간소하고 컴퓨터가 처리하기에 편리하도록 하며, 대신 사람이 읽을 수는 없습니다.



벡터 데이터는 실제 세상을 그래픽으로 재표현(graphical representation of the real world)한 것으로서, 점, 선, 다각형(points, lines, polygons) 유형의 그래픽을 이용합니다. 벡터 데이터는 지구 표면의 객체나 특징을 일반화하여 표현하는데 사용됩니다.


벡터 데이터는 별개로 분리되고, 경계가 잘 정의되어 있어서 보통 높은 수준의 정밀도 (high level of precision) 을 가지고 있습니다. 이런 이유로 벡터 데이터는 사회 과학 (social sciences) 분야에서 많이 사용됩니다.


R 의 sf 패키지 (spatial data frame) 를 사용하여 벡터 데이터를 불러오고, 처리 및 분석, 시각화를 할 수 있습니다. (다음 포스팅에서 소개) sf 패키지는 이전의 sp 패키지, rgeos, rgdal 패키지를 모두 아우르고 있고, GEOS, GDAL, PROJ 와 R 의 interface를 제공해주어서, R로 지리공간 벡터 데이터를 다루는데 있어 매우 편리하고 강력합니다.




[ 강과 도심 지역을 나타낸 벡터 데이터(vector data)와 레스터 데이터(raster data) 비교 ]


* source: https://blog.rmotr.com/spatial-data-with-python-lets-begin-e29b5c41ead3



  (2) 지리공간 레스터 데이터 (Raster data)


레스터 데이터(Raster data)에는 ESRI Grid, GeoTIFF, JPEG 2000, NITF 등이 데이터 포맷이 있습니다.


레스터 데이터는 픽셀의 격자(grid of pixels) 로 지구의 표면을 표현합니다. 각 픽셀 안에는 색, 측정 단위 등과 같이 질문의 요소에 대한 정보를 전달하는 값이 있습니다.


레스터 데이터는 인공위성이나 항공장비에서 지구 표면을 향해 위에서 아래로 수직으로 찍은 사진으로 생각하면 이해하기가 쉽습니다.(예: NASA에서 제공하는, 인공위성에서 찍은 지구의 야간 사진 등) 이 지구표면을 수직으로 찍은 사진을 픽셀의 격자로 나누어서 각 픽셀(pixel, cell)에 지리특성정보 값을 가지고 있는 것입니다. 


레스터 데이터의 품질은 사진을 찍었던 장비의 해상도의 한계나, 활용하고자 하는 분야의 목적에 따라서 다양합니다. 레스터 데이터는 많은 환경관련 과학 분야 (environmental sciences)에 많이 사용되고 있습니다.


R의 raster 패키지를 사용하면 R에서 레스터 데이터를 처리할 수 있습니다.


위에서 각각 소개한 벡터 데이터와 레스터 데이터는 상호 간에 변환(converting from vector to raster data, from raster to vector data)이 가능하며, 하나의 분석 목적에 두 유형의 데이터 포맷이 동시에 사용되기도 합니다.


다음번 포스팅에서는 R의 spData 패키지에 내장되어 있는 지리공간 벡터 데이터 모델(Vector data model)을 가지고 sf 패키지로 시각화하는 간단한 예를 소개하겠습니다.


이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!  :-)


728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 공간데이터에 대하여 DBSCAN 알고리즘으로 밀도 기반 군집화하는 방법을 소개하였습니다.


k-means 군집분석의 경우 입력 모수(input parameter)로서 '군집의 개수 k'를 결정하는 것이 어려움이라면, DBSCAN 은 입력 모수로서 '(a) 점으로 부터의 반경 Eps (Epsilon)'와 '(b) Eps 내 최소 점의 개수 기준인 MinPts' 를 결정하는 것이 중요하고도 어려운 문제 중에 하나입니다.


그런데 무슨 연립방정식 풀듯이 이론적으로 증명된 DBSCAN의 입력모수 MinPts 와 Eps 를 구할 수 있는 공식 같은 것, 객관적인 통계량 같은 것은 없습니다. 다만 MinPts와 Eps를 결정하는데 도움을 받을 수 있는 주관적인 Heuristic method 가 있을 뿐입니다.


이번 포스팅에서는 밀도 기반 군집분석 DBSCAN 의 입력 모수 Eps, MinPts 를 결정하는 Heuristic 방법 (Determining the Parameters Eps and MinPts) 을 소개하겠습니다.


(1) MinPts 결정하는 Heuristic 방법: ln(n)

(2) Eps 결정하는 Heuristic 방법: elbow (knee) method using sorted k-dist plot





  (1) DBSCAN에서 MinPts 결정하는 Heuristic 방법


MinPts 는 한 점으로부터 반경 Eps 인 원을 그렸을 때 그 점이 코어 점 (core points), 군집이 되기 위해 Eps 안에 필요한 최소한의 점 개수를 말합니다.


MinPts 가 만약 너무 작은 수이면 잡음(noise)으로 구분되어야 할 점들 마저도 코어 점(core points)나 또는 경계점(border points)로 잘못 구분이 되어 원래 데이터셋 내의 군집 개수보다 더 많은 수의 군집이 형성될 수가 있으므로 주의가 필요합니다.


MinPts 를 결정할 때는 데이터 특성과 구조에 대해서 잘 알고 있는 업 전문가 (domain expert) 의견을 반영할 필요가 있습니다.  그런데 현실은 데이터 분석을 할 때 업 전문가가 없을 수도 있고, 있더라도 MinPts를 잘 결정할 수 없을 수도 있으므로 Heuristic 방법을 알아 둘 필요가 있습니다.


DBSCAN의 원 논문(참조 [1])에서는 2차원 데이터에 대해 실험을 해보니 MinPts 가 4개와 5개 이상 간의 k-dist plot (아래 설명 예정) 의 큰 변동이 없는 반면에 MinPts 가 점점 커질 수록 연산량(computation)이 상당히 커지므로 2차원 데이터에서는 MinPts = 4 개로 하는 것을 권장하고 있습니다.


2차원보다 많은 변수를 가지고 있는 데이터셋의 경우 MinPts = 2 * dim 을 추천하는 논문(참조 [2])도 있습니다.


데이터셋별로 데이터의 구조나 객체의 개수 n이 서로 다를 수 있으므로, 데이터셋별 객체 개수 n 특성을 감안해서 MinPts를 결정하는 Heuristic 방법으로 ln(n) 을 사용할 수 있습니다.(참조 [3]) 여기서 n 은 데이터 개수 (number of points in database) 를 말합니다.




  (2) DBSCAN에서 Eps 결정하는 Heuristic 방법

       : Elbow (Knee) method using sorted k-dist plot


DBSCAN의 원 논문(참조 [1])에서는 아래의 sorted k-dist graph 를 그린 후 Elbow method 를 사용해서 첫번째 계곡(first "valley") 지점의 점이 구분 기준점(threshold point)이 되고, 이 기준점의 왼쪽은 잡음(noist), 기준점의 오른쪽은 군집으로 구분하고, 꺽이는 부분의 k-dist 를 Eps 로 결정하는 Heuristic 방법을 소개합니다.




sorted k-dist graph 를 그리는 방법은, 먼저 MinPts 를 k개라고 했을 때, 하나의 점으로부터 k개의 가장 가까운 점들 간의 거리,k_NN (k-Nearest Neighbor)의 거리 k-dist 를 구해고, k-dist 를 내림차순으로 정렬하여, X축은 정렬된 점들 별로 Y축에는 k-dist 를 그려줍니다.

(단, R의 dbscan 패키지에서는 k-dist 가 오름차순으로 정렬이 된 sorted k-dist plot 을 그려줘서 원 논문과는 그래프의 좌우가 반대입니다.)



R의 factoextra 패키지에 내장되어 있는 multishapes 데이터셋을 가지고 R의 dbscan 패키지를 사용해서 최적의 Eps 모수 값을 결정해보겠습니다.


예제 데이터셋 multishapes 은 아래의 산점도처럼 크기가 다른 원 고리형 2개, 선형 2개, 원형 1개의 5개 군집과 잡음들로 구성이 되어있습니다.



## factoextra for visualizing clusters
if(!require(devtools)) install.packages("devtools")
devtools::install_github("kassambara/factoextra")

library(factoextra)
data("multishapes", package = "factoextra")

## multishapes dataset
str(multishapes)
# 'data.frame':    1100 obs. of  3 variables:
#   $ x    : num  -0.804 0.853 0.927 -0.753 0.707 ...
# $ y    : num  -0.853 0.368 -0.275 -0.512 0.811 ...
# $ shape: num  1 1 1 1 1 1 1 1 1 1 ...


## scatter plot

df <- multishapes[, 1:2]
plot(df, main="multishapes dataset")




다음으로 위에서 설명했던 sorted k-dist plot 을 R의 dbscan 패키지의 kNNdistplot(data, k) 함수를 사용해서 k=5로 지정하고 그려보겠습니다. (k-dist 의 오름차순 기준으로 정렬이 되어서 원 논문과는 반대 모양임.)


sorted k-dist plot 에서 꺽이는 팔꿈치(elbow) 부분의 점이 threshold point 가 되겠습니다. 아래의 예처럼 비교적 눈에 띄게 구분이 되는 경우도 있고, 어디를 꺽인 팔꿈치 (혹은 무릎 knee) 부분인지 콕 집기가 애매한 데이터셋도 있습니다. (객관적이라기 보다는 다분히 주관적입니다.)


이 기준점을 기준으로 왼쪽까지는 군집(clusters)에 속하는 점들이 되겠고, 오른쪽 부터는 잡음 점(noise points)로 간주합니다. 그리고 이 기준점(threshold point)의 k-NN distance 를 최적의 Eps로 결정하면 됩니다. 이렇게 하면 k-dist(p) 와 같거나 작은 값을 가지는 점들은 모두 코어 점(core points)가 됩니다.


아래 예에서는 꺽이는 팔꿈치 부분의 점의 5-NN distance (k-dist(p)) 가 0.15 이므로 Eps 를 0.15로 하면 되겠네요.  (k 를 4 ~ 7 까지 바꾸어 가면서 sorted k-dist plot 을 그려보니 k가 커질수록 elbow 지점의 Eps 가 조금씩 커지기는 하는데요, 큰 차이는 없네요.)



## Determining the optimal Eps value using k-dist plot & elbow method

library(dbscan)


dbscan::kNNdistplot(df, k=5)
abline(h = 0.15, lty = 2)





위에서 sorted k-dist plot 의 elbow mothod 로 구한 최적의 Eps = 0.15 값을 사용하고  MinPts = 5 로 입력 모수를 결정해서 DBSCAN 알고리즘 군집화 및 시각화를 해보겠습니다.



## DBSCAN clustering
set.seed(1004)
db <- dbscan::dbscan(df, eps = 0.15, minPts = 5)

## or using fpc package
# library(fpc)
# db <- fpc::dbscan(df, eps = 0.15, MinPts = 5)


## Plot DBSCAN results
library("factoextra")
fviz_cluster(db, df, stand = FALSE, frame = FALSE, geom = "point")





이외에도 OPTICS: Ordering Points To Identify the Clustering Structure 알고리즘을 이용하는 방법, MinPts 와 Eps 를 변화시켜가면서 잡음의 비율(percentage of noises)의 민감도 분석 (sensitivity analysis) 을 하는 방법도 있으나 이번 포스팅에서는 자세히 설명하지는 않겠습니다.


이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!  :-)


[Reference]

* [1] Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise", KDD-96

* [2] Erich Schubert, Jorg Sander, Martin Ester, Hans Peter Kriegel, Xiaowei Xu, 2017, "DBSCAN Revisited: Why and How You Should (Still) Use DBSCAN

* [3] Chossing eps and minpts for DBSCAN (R)? : https://stackoverflow.com/questions/12893492/choosing-eps-and-minpts-for-dbscan-r

* [4] DBSCAN in R : http://www.sthda.com/english/wiki/wiki.php?id_contents=7940




728x90
반응형
Posted by Rfriend
,

이번 DBSCAN(Density-Based Spatial Clustering of Applications with Noise) 알고리즘에 대한 포스팅은 Martin Ester, el.al, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise"[1]  논문을 참조하여 작성하였습니다.

 

군집분석은 공간 데이터(spatial data)의 그룹, 구조, 구성요소 등을 식별 (class identification)하는 과업에 활용될 수 있습니다. 하지만 대용량의 공간 데이터에 대한 군집화는 다음의 3가지 요건을 충족시킬 수 있어야 합니다.

 

(1) 대용량 데이터를 다룰 때 적당한 입력 모수에 대한 선험적 지식이 종종 알려져있지 않으므로, 입력 모수(input parameter)를 결정하기 위해 필요한 업 지식은 최소화되어야 하고 (minimal requirements of domain knowledge),

(2) 공간데이터의 형태는 구형, 옆으로 퍼진 형태, 선형, 가늘고 긴 형태 등 다양할 수 있으므로, 임의의 형태의 군집 탐색 가능 (discovery of clusters with arbitrary shape)해야 하고,

(3) 단지 수천개의 객체를 가진 작은 데이터셋 뿐만이 아니라, 대용량 공간데이터에 대해서도 효율적으로 군집화 연산이 가능해야 함.

 

이번에 소개하는 DBSCAN 알고리즘은 위의 3가지 대용량 공간 데이터에 대한 군집화 조건을 모두 만족합니다. DBSCAN 알고리즘은 이론과 현실 문제 적용에서의 우수성을 인정받아서 2014년에 ACM SIGKDD 데이터마이닝 컨퍼런스에서 상을 받기도 했습니다.

 

아래의 시각화는 여러가지 모양과 형태의 데이터셋들에 대해 다양한 군집분석 알고리즘을 적용하여 그 결과를 비교한 것입니다. 아래 10개의 군집화 알고리즘 중에서 DBSCAN의 군집화 결과가 데이터셋의 형태에 상관없이 매우 좋은 군집화 결과를 보여주고 있습니다. DBSCAN은 군집에 속하지 않는 잡음(noise), 이상치(outlier) 도 탐지를 할 수 있어서 anomaly detection 에도 쓸 수가 있습니다.

 

* 출처: https://scikit-learn.org/stable/auto_examples/cluster/plot_cluster_comparison.html#sphx-glr-auto-examples-cluster-plot-cluster-comparison-py

 

 

자, 이제 DBSCAN 에 대한 본문으로 들어갈 볼까요?

 

(1) DBSCAN 알고리즘 정의

(2) k-means vs. DBSCAN 군집화 알고리즘 비교

(3) R을 이용한 DBSCAN 군집화 (예시)

 

 

 

  (1) DBSCAN 알고리즘 정의

 

DBSCAN 알고리즘을 정의하는데 필요한 기본 용어들을 먼저 살펴보겠습니다.

 

(정의 1) 반경 Eps 내 이웃점 (Eps-neighborhood of a point)

: 공간 데이터베이스에 속한 점들 중에서 두 점 p와 q의 거리가 반경 Eps() 이내인 점

 

 

* D : 데이터베이스(Database), 데이터셋

* dist(p, q) :점 p와 q 의 거리(distance)

 

이때, 분석가가 입력해줘야 하는 모수로서,

* Eps (Epsilon, 엡실론): 점 p 로 부터의 반경

* MinPts : 최소 기준 점 개수 (minimum number of points)

 

DBSCAN 알고리즘은 밀도 기반(Density-Based)라고 했는데요, 이때 데이터 점의 밀도(density)는 하나의 점으로 부터 반경 Eps 이내에 점이 몇 개나 있는지로 측정합니다.

 

 

(정의 2) 코어 점(core points), 경계 점(border points)

 

* 코어 점 (core points) : 군집 내 점 (points inside of the cluster), 한 점으로 부터 반경이 Eps 인 원을 그렸을 때 그 원 안에 이웃점(Eps-neighborhood of a point)MinPts 이상의 점이 있는 점.

* 경계 점 (border points) : 군집의 경계에 있는 점 (points on the border of the cluster)

 

* 출처: [1] Martin Ester, el.al

 

 

(정의 3) 직접적으로 밀도(기반)-도달가능한 (directly density-reachable)

 

점 p가 점 q 반경 Eps 이내에 있는 이웃점에 속하고, 점 q의 반경 Eps 이내 이웃점의 개수가 MinPts 이상일 때 점 p는 점 q 로 부터 직접적으로 밀도(기반)-도달가능하다고 합니다.

 

(a)

(b)

(core point condition)

 

위의 figure 2 에서 점 p는 점 q로 부터 직접적으로 밀도(기반)-도달가능합니다. 하지만 점 q는 점 p로 부터 직접적으로 밀도(기반)-도달가능하지 않습니다. 왜냐하면 점 p는 코어 점(core point)의 조건을 충족시키지 못하기 때문입니다.

 

 

(정의 4) 밀도(기반)-도달가능한 (density-reachable)

 

만약 연쇄적인 점들

들이 있고, 점

이 점

로 부터 직접적으로 밀도(기반)-도달가능하다면(directly density-reachable), 점 p는 점 q로 부터 반경 Eps 내 MinPts 조건 하에 밀도(기반)-도달가능(density-reachable)하다고 합니다.

 

(정의 3)의 '직접적으로 밀도(기반)-도달가능'은 대칭적(symmetric)인 반면에, (정의 4)의 '밀도(기반)-도달가능'은 비대칭적(asymmetric)입니다.

 

 

(정의 5) 밀도(기반)-연결된 (density-connected)

 

만약 두 점 p와 q가 모두 어떤 점 o 로 부터 반경 Eps 내 MinPts 조건 하에 밀도(기반)-도달가능(density-reachable)하다면 점 p는 점 q와 반경 Eps 내 최소 점 개수 MinPts 조건 하에 밀도(기반)-연결되었다고 합니다. 

(다르게 말하면, p 가 o의 친구이고, q도 o의 친구이면, p와 q는 친구 o를 통해 서로 연결되었다고 보면 됩니다.)

 

 

(정의 6) 군집 (cluster)

 

드디어 이제 DBSCAN 알고리즘이 군집(cluster)를 어떻게 정의하는지 말해볼 때가 왔군요. 위의 정의(1)~(5)까지의 용어와 개념을 사용하여 군집을 정의해보면, "군집은 밀도(기반)-도달가능한 최대치의 밀도(기반)-연결된 점들의 집합이다 (A cluster is defined to be a set of density-connected points which is maximal with respect to density-reachablility.)" 라고 할 수 있겠습니다.

 

 

* 출처: [1] Martin Ester, el.al

 

D를 공간 점들의 데이터베이스(데이터셋) 라고 하고, C 를 군집(Cluster) 라고 했을 때 군집 C는 반경 Eps와 최소 점 개수 MinPts 조건이 주어졌을 때, 아래의 (a) 최대의 밀도(기반)-도달가능 조건과 (b) 연결성 조건을 만족하는 D의 비어있지 않은 부분집합이라고 할 수 있습니다.

 

(a) Maximality wrt. Density-reachability

 

(b) Connectivity

 

 

(정의 7) (잡음, noise)

 

잡음 점은 군집에 속하지 못하는 점. 즉, 코어 점도 아니고 경계 점도 아닌 점을 말합니다.

 

 

분석의 목적이 군집화라면 잡음점을 무시하거나 제거하면 되구요, 만약 분석의 목적이 군집화가 아니라 anomaly detection, outlier detection 이라면 잡음 점(noises)들이 주요 관심사가 되겠습니다.

 

 

DBSCAN 알고리즘의 군집화 절차는 아래와 같이 정리할 수 있습니다.

 

입력 모수(input parameters)로서, 점으로 부터의 (a) 반경 Eps와 (b) Eps 반경 내 최소 점 개수 기준인 MinPts 조건이 주어졌을 때,

 

(1) 공간 데이터셋으로부터 초기값(seed)으로서 코어 점(core points)의 조건을 만족하는 임의의 점을 선택합니다.

(2) 초기값으로 부터 밀도(기반)-도달가능한 점들을 뽑아서 코어 점(core points)과 경계 점(border point)을 구분하고, 이에 속하지 않은 점들을 잡음(noises)으로 구분합니다. 

(3) 반경 Eps 인 원 주위에 있는 코어 점들을 서로 연결합니다.

(4) 연결된 코어 점들을 하나의 군집으로 정의합니다.

(5) 모든 경계점들을 어느 하나의 군집으로 할당합니다. (만약 경계점 중에 여러 군집에 걸쳐있는 경우는 반복 과정에서 먼저 할당된 군집으로 할당함.)

 

 

왼쪽의 그림은 Wikipedia에 소개된 내용인데요, 원 논문의 그림보다 좀더 이해하기 쉬울거 같아서 한번 더 소개합니다.

 

점으로 부터의 반경이 Eps 이고 MinPts = 4 라고 했을 때,

 

점 A를 포함해서 가운데의 빨간점 6개는 코어 점(core points) 입니다.

 

그리고 코어점은 아니지만 코어점과 연결 가능한 노란색 점 B, C 는 경계점(border points) 입니다.

 

그리고 코어 점도 아니고 경계 점도 아닌 파란색 점 N 은 잡음(noise) 점이 되겠습니다.

 

군집화의 과정은 각 점들을 순회하면서 재귀적(recursive)으로 도달가능, 연결가능을 평가하면서 진행이 됩니다.

 

 

 

 

 

 

  (2) k-means Clustering vs. DBSCAN 군집화 알고리즘 비교

 

k-means 와 DBSCAN 군집화 알고리즘을 아래의 표에 비교해서 정리해보았습니다. 유사성(혹은 비유사성 거리) 기반의 k-means 대비, 밀도 기반의 DBSCAN 의 경우 분석가가 미리 군집의 개수 (k)를 입력해주지 않아도 되고, 잡음/이상치에도 견고하며, 계산 복잡도도 상대적으로 작습니다. 게다가 군집으로 찾아낼 수 있는 모양도 구형, 원형, 길게 늘어선 형태, 선형 등 임의의 모양에 대해서 비교적 잘 군집화를 하고, 잡음/이상치도 별도로 구분을 해낼 수 있습니다. 이래저래 DBSCAN이 k-means 대비 우수한 점이 많습니다.

 

 

 

 

  (3) R을 이용한 DBSCAN 군집화 (예시)

 

DBSCAN 분석을 위한 R코드는 참조 [3] 사이트의 코드를 거의 그대로 사용하였습니다.

 

예제로 사용할 데이터셋으로 중앙이 비어있는 원형, 선형, 구형 등 다양한 형태의 군집과 잡음으로 구성된, factoextra 패키지에 내장되어 있는 multishapes 데이터셋을 사용하겠습니다.

 



## factoextra for visualizing clusters
if(!require(devtools)) install.packages("devtools")
devtools::install_github("kassambara/factoextra")

library(factoextra)
data("multishapes", package = "factoextra")

## multishapes dataset
str(multishapes)
# 'data.frame':    1100 obs. of  3 variables:
#   $ x    : num  -0.804 0.853 0.927 -0.753 0.707 ...
# $ y    : num  -0.853 0.368 -0.275 -0.512 0.811 ...
# $ shape: num  1 1 1 1 1 1 1 1 1 1 ...

plot(multishapes$x, multishapes$y)



 

 

 

k-means clustering 과 DBSCAN clustering 알고리즘을 비교해보기 위해, 위의 multishapes 데이터셋에 대해 군집의 개수 k=5로 해서 k-means 군집화를 해보겠습니다.

 

k-means 군집화의 경우, (1) 상단에 위치한 중앙이 비어있는 원형 군집 2개를 제대로 구분하지 못하고 있고, (2) 좌측 하단에 위치한 선형 2개 군집도 제대로 구분하지 못하고 있으며, (3) 우측 하단의 물방울 형태 군집도 잡음/이상치가 섞여서 군집화가 되었습니다. 전혀 만족스럽지 않은 군집화 결과네요.

 



##-- k-means clustering plot
df <- multishapes[, 1:2]
set.seed(1004)
km.res <- kmeans(df, centers=5, nstart = 25)
fviz_cluster(km.res, df, frame = FALSE, geom = "point")




 

 

이번에는 DBSCAN 알고리즘으로 입력 모수로서 Eps = 0.15, MinPts = 5 로 하여 multishapes 데이터셋에 대해 군집화를 해보겠습니다.

 

R의 fpc 패키지나 dbscan 패키지를 사용하여 DBSCAN 알고리즘으로 군집화를 할 수 있습니다. 아래 예시에서는 fpc 패키지를 사용하였으며, 패키지명::함수명() 형태로서 fpc::dbscan(data, eps, MinPts) 으로 패키지 이름을 명시적으로 입력해주었습니다.

 

아래 표(참조 [4])는 프로그래밍 언어별 DBSCAN 을 할 수 있는 패키지들을 비교한 것입니다.  R 의 dbscan 패키지가 지원하는 기능 면에서는 가장 강력하네요. R fpc 패키지는 dbscan 에만 특화된 simple 한 패키지이구요.

 

 



##-- DBSCAN using fpc or dbscan package
install.packages("fpc")
library(fpc)


## Compute DBSCAN using fpc package
set.seed(1004)
db <- fpc::dbscan(df, eps = 0.15, MinPts = 5)



## or
# install.packages("dbscan")
# library(dbscan)

# dbscan(data, eps, MinPts = 5, scale = FALSE,
#        method = c("hybrid", "raw", "dist"))


 

 

마지막으로 factoextra 패키지의 fviz_cluster() 함수로 DBSCAN 군집화 결과를 시각화해보았습니다.

 

DBSCAN 군집화 결과를 보면, (1) 상단에 위치한 중앙이 비어있는 원형 군집 2개를 잘 구분하였고, (2) 좌측 하단에 위치한 선형 2개 군집도 제대로 구분하였으며, (3) 우측 하단의 물방울 형태 군집도 잘 군집화하였을 뿐만 아니라, (4) 잡음/이상치도 잘 구분하였습니다. 매우 만족스러운 군집화 결과네요!
 


## Plot DBSCAN results
library("factoextra")
fviz_cluster(db, df, stand = FALSE, frame = FALSE, geom = "point")

## or
#plot(db, df, main = "DBSCAN", frame = FALSE)






 

 

DBSCAN 군집화 결과 객체에서 cluster 속성에 각 관측치별 속한 군집 정보가 들어있습니다. cluster '0'은 잡음/이상치를 의미합니다. 아래 예제에서는 무작위로 50개 관측치를 샘플링해서 그 관측치들이 무슨 군집으로 할당이 되었는지 프린트해보았습니다.

 



## Print DBSCAN
print(db)
# dbscan Pts=1100 MinPts=5 eps=0.15
# 0   1   2   3  4  5
# border 31  24   1   5  7  1
# seed    0 386 404  99 92 50
# total  31 410 405 104 99 51



names(db)
# [1] "cluster" "eps"     "MinPts"  "isseed"



# Cluster membership. Noise/outlier observations are coded as 0
# A random subset is shown
db$cluster[sample(1:1089, 50)]
# [1] 2 2 1 1 0 2 4 1 2 1 4 1 2 2 1 4 2 3 2 1 1 5 2 1 5 1 2 1 3 1 1 4 2 3 2 1 2 1 1 3 2 1 2 1 2 1 1 1 1 4


 

 

각 클러스터별로 요약통계량 (가령, 평균)을 계산해서 profiling 해보면 클러스터의 특징을 파악하는데 많은 도움이 될 것입니다. 아래 코드는 dplyr 패키지의 group_by() 와 summarise() 함수를 사용해서 각 DBSCAN Cluster 별로 x, y 변수의 평균을 구해본 것입니다. 

 

# summary statistics by Cluster membership
library(dplyr)
df %>% 
  group_by(db$cluster) %>% 
  summarise(x_mean = mean(x, rm=TRUE), 
            y_mean = mean(y, rm=TRUE))

# `db$cluster`   x_mean  y_mean
# <dbl>    <dbl>   <dbl>
#   1            0  0.0604  -1.10  
# 2            1  0.00419 -0.0394
# 3            2  0.00275 -0.0119
# 4            3 -0.754   -2.01  
# 5            4 -0.669   -2.99  
# 6            5  0.991   -2.50

 

 

[Reference]

[Reference]

* Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise", KDD-96



출처: https://rfriend.tistory.com/588 [R, Python 분석과 프로그래밍의 친구 (by R Friend)]

[Reference]

* Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise", KDD-96



출처: https://rfriend.tistory.com/588 [R, Python 분석과 프로그래밍의 친구 (by R Friend)]

[Reference]

* Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise", KDD-96



출처: https://rfriend.tistory.com/588 [R, Python 분석과 프로그래밍의 친구 (by R Friend)]

[Reference]

* Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise", KDD-96



출처: https://rfriend.tistory.com/588 [R, Python 분석과 프로그래밍의 친구 (by R Friend)]

[Reference]

* Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise", KDD-96



출처: https://rfriend.tistory.com/588 [R, Python 분석과 프로그래밍의 친구 (by R Friend)]

[Reference]

* Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise", KDD-96



출처: https://rfriend.tistory.com/588 [R, Python 분석과 프로그래밍의 친구 (by R Friend)]

[Reference]

* Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise", KDD-96



출처: https://rfriend.tistory.com/588 [R, Python 분석과 프로그래밍의 친구 (by R Friend)]

* [1] Martin Ester, Hans-Peter Kriegel, Jorg Sander, Xiaowei Xu, 1996, "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise"

(pdf download: https://www.aaai.org/Papers/KDD/1996/KDD96-037.pdf )

* [2] DBSCAN at Wikipedia: https://en.wikipedia.org/wiki/DBSCAN

* [3] DBSCAN in R : http://www.sthda.com/english/wiki/wiki.php?id_contents=7940

* [4] R dbscan package: https://cran.r-project.org/web/packages/dbscan/vignettes/dbscan.pdf

 

다음번 포스팅에서는 DBSCAN 알고리즘의 입력 모수인 Eps, MinPts 를 결정하는 휴리스틱 방법을 소개하겠습니다.

 

이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요! :-)

 

728x90
반응형
Posted by Rfriend
,
지난번 포스팅에서는 분할적 군집분석(Partitioning clustering)에서 군집의 개수 k를 결정하는 3가지 방법 (계층적 군집분석 덴드로그램, Elbow method, Silhouettes method)을 소개하였습니다.

군집분석 결과를 현장에 적용하려면 각 군집의 특성을 이해하고, 군집별로 차별화된 대응 전략을 도출할 수 있어야만 합니다. 이번 포스팅에서는 군집분석 결과를 해석하는 3가지 방법을 소개하겠습니다.

(1) 군집별 변수별 중심 좌표 (Center Points by Clusters)
(2) 군집별 변수별 평행 좌표 그림 (Parallel Coordinates Plot by Clusters)
(3) (차원축소 후) 군집별 산점도 (Scatter Plot by Clusters using Reduced Dimension)



  (0) 샘플 데이터 준비 : USArrests {datasets}


예제로 사용할 샘플 데이터는 R datasets 패키지에 내장되어있는 USArrests 데이터셋을 사용하겠습니다. 미국 내 50개 주별 범죄율 관련된 살인(murder), 폭행(assault), 도시인구(UrbanPop), 강간(rape) 의 4개 변수 통계 데이터가 들어있습니다.

군집분석은 변수별 척도에 민감하므로 scale() 함수를 사용해서 표준화(standardization)를 해주었습니다.


## importing USArrests dataset from {datasets}
data(USArrests)

dim(USArrests)
# [1] 50  4

head(USArrests)
# Murder Assault UrbanPop Rape
# Alabama      13.2     236       58 21.2
# Alaska       10.0     263       48 44.5
# Arizona       8.1     294       80 31.0
# Arkansas      8.8     190       50 19.5
# California    9.0     276       91 40.6
# Colorado      7.9     204       78 38.7


## standardization (scaling)
USArrests_scaled <- data.frame(scale(USArrests))

head(USArrests_scaled)
# Murder   Assault   UrbanPop         Rape
# Alabama    1.24256408 0.7828393 -0.5209066 -0.003416473
# Alaska     0.50786248 1.1068225 -1.2117642  2.484202941
# Arizona    0.07163341 1.4788032  0.9989801  1.042878388
# Arkansas   0.23234938 0.2308680 -1.0735927 -0.184916602
# California 0.27826823 1.2628144  1.7589234  2.067820292
# Colorado   0.02571456 0.3988593  0.8608085  1.864967207





  (1) 군집별 변수별 중심 좌표 (Center Points by Clusters)


표준화한 USArrests 데이터셋에 대해 k-means 의 군집개수 k=4 로 해서 군집분석을 하고, 4개의 군집에 대해 해석(interpretation, profiling)을 해보겠습니다.


군집분석 결과를 해석하는 가장 쉽고 또 직관적인 방법은 각 군집별로 군집화에 사용했던 변수들의 중심 좌표를 살펴보는 것입니다.


R 의 군집분석 결과에는 군집별 중심좌표로서 군집별 변수별 평균('Cluster means') 정보가 군집분석 결과 객체의 "centers" attributes에 계산되어 저장되어 있습니다.



## -- (1) Centers per Clusters
## K-means clustering
set.seed(1004)
kmeans_k4 <- kmeans(USArrests_scaled, centers = 4)


## attributes of k-means clustering results

names(kmeans_k4)
# [1] "cluster"      "centers"      "totss"        "withinss"     "tot.withinss"
# [6] "betweenss"    "size"         "iter"         "ifault"



## center points per variables

kmeans_k4$centers
# Murder    Assault   UrbanPop        Rape
# 1 -0.9615407 -1.1066010 -0.9301069 -0.96676331
# 2  0.6950701  1.0394414  0.7226370  1.27693964
# 3  1.4118898  0.8743346 -0.8145211  0.01927104
# 4 -0.4894375 -0.3826001  0.5758298 -0.26165379




그런데 kmeans_k4$centers 로 조회한 4개 각 군집의 변수별 중심 좌표 (평균) 은 여기서는 표준화한 후의 데이터에 대한 변수별 평균입니다. 표준화하기 전의 원래 데이터의 군집별 변수별 중심 좌표 (평균)을 알고 싶으면, 아래의 예시처럼 원래의 데이터에 군집화 결과 변수를 추가해주고(미국 주 이름별로 이미 정렬이 된 상태이기 때문에 merge가 아니라 그냥 칼럼을 추가해주었습니다.), 각 군집별 변수별 평균을 구해주면 됩니다.


## add 'cluster' to the original dataset

USArrests$cluster <- kmeans_k4$cluster
head(USArrests)

# Murder Assault UrbanPop Rape cluster
# Alabama      13.2     236       58 21.2       3
# Alaska       10.0     263       48 44.5       2
# Arizona       8.1     294       80 31.0       2
# Arkansas      8.8     190       50 19.5       3
# California    9.0     276       91 40.6       2
# Colorado      7.9     204       78 38.7       2


library(dplyr)
USArrests %>%
  group_by(cluster) %>%
  summarise(murder = mean(Murder),
            assault = mean(Assault),
            urbanpop = mean(UrbanPop),
            rape = mean(Rape))


# # A tibble: 4 x 5
# cluster murder assault urbanpop  rape
# <fct>    <dbl>   <dbl>    <dbl> <dbl>
#   1 1         3.6     78.5     52.1  12.2
# 2 2        10.8    257.      76    33.2
# 3 3        13.9    244.      53.8  21.4
# 4 4         5.66   139.      73.9  18.8





  (2) 군집별 변수별 평행 좌표 그림 (Parallel Coordinates Plot by Clusters)


다변량 데이터셋으로 군집분석을 했을 때 다수 변수들에 대해 관측치 개개의 값들이 군집별로 어떻게 다른지를 보려면 평행 좌표 그림을 사용할 수 있습니다.


GGally 패키지의 ggparcoord() 함수를 이용해서 ggplot2 기반의 군집별 변수별 평행좌표그림 (parallel coordinates plot)을 그려보겠습니다.


표준화하기 전의 원본 데이터를 사용하고, scale = "std" 를 해주면 알아서 각 변수들을 표준화해줍니다.

(혹은 표준화한 데이터를 바로 사용해도 됩니다).


단, 관측치 개수가 너무 많으면 하나의 평행좌표그림에 모든 관측치를 표현할 경우 서로 겹쳐보여서 군집 특성을 파악하는 것이 힘들 수도 있습니다.


ggplotly 패키지의 ggplotly() 함수를 사용하면 interactive plot 을 그려서 군집별로 하이라이트해서 좀더 수월하게 그래프를 볼 수도 있습니다. (저는 R 4.02 버전을 쓰고 있는데요, 이 버전에서는 ggplotly 가 지원이 안된다고 나오네요 -_-;)



## Parallel Coordinates Plot
install.packages("GGally")
library(GGally)

USArrests$cluster <- as.factor(USArrests$cluster)

p <- ggparcoord(data = USArrests,
                columns = c(1:4),
                groupColumn = "cluster",
                scale = "std") +
  labs(x = "Violent Crime Rates by US State",
       y = "value in scaled",
       title = "Parallel Coordinates Plot by Cluster")

p




## Interactive plot using ggplotly

install.packages("ggplotly")
library(ggplotly)
ggplotly(p)

 




  (3) (차원축소 후) 군집별 산점도 (Scatter Plot by Clusters using Reduced Dimension)


다변량 데이터셋으로 분할적 군집분석(partitioning clustering)한 결과를 2차원의 평면에 군집별로 색깔과 모양을 달리해서 산점도로 표현을 할 수 있다면 군집별 특성을 이해하는데 도움이 될 것입니다.


다변량 변수를 2차원으로 차원축소하는데 요인분석(factor analysis), 주성분분석(principal component analysis, PCA) 등을 사용합니다.


k-means 군집분석한 결과를 R의 Factoextra 패키지의 fviz_cluster() 함수를 사용하면 주성분분석(PCA) 으로 다변량을 2차원으로 축소해주고, 이를 군집별로 색깔과 모양을 달리하여 R ggplot2 기반의 세련된 시각화를 해줍니다.


아래 예의 경우 Dim1 (62%), Dim2 (24.7%) 라고 되어있는데요, 이는 전체 분산 중 가로 축 Dim1 이 62%를 설명하고, 세로 축 Dim2 가 24.7% 를 차지한다는 뜻입니다. USArrests 데이터셋의 경우 '살인(murder)', '폭행(assault)', '강간(rape)'의 3개 변수가 "범죄"와 관련이 있어서 Dim1에 의해 많이 설명이 되고, 나머지 변수인 '도시인구(UrbanPop)'가 Dim2에 의해 설명이 되겠네요.

(* 주성분분석 참조 : https://rfriend.tistory.com/61 )



## fviz_silhouette:Visualize Silhouette Information from Clustering
install.packages("factoextra")
library(factoextra)


# Visualize kmeans clustering
fviz_cluster(kmeans_k4, USArrests_scaled, ellipse.type = "norm")+
  theme_minimal()




[ R factoextra package Reference ]

* https://rpkgs.datanovia.com/factoextra/index.html

* https://cran.r-project.org/web/packages/factoextra/factoextra.pdf



혹은 다변량 데이터셋에서 군집을 구분해서 잘 설명해줄 수 있는 대표적인 변수 2개만 선택해서 ggplot2로 직접 2차원 산점도를 그릴 수도 있습니다.

이번 USArrests 데이터셋의 경우 '살인(murder)', '폭행(assault)', '강간(rape)'의 3개 변수가 "범죄"와 관련이 있으므로 이중에서 '살인(murder)' 변수를 하나 뽑고, 나머지 변수인 '도시인구(UrbanPop)'를 나머지 하나로 선택해서 2차원 산점도를 그려보겠습니다.


## Scatter plot by Murder and UrbanPop by clusters
library(ggplot2)
library(repr)
options(repr.plot.width=12, repr.plot.height=12)


set.seed(1004)
km_4 <- kmeans(USArrests_scaled, centers = 4)


cluster_num <- as.factor(km_4$cluster)
city_name <- rownames(USArrests_scaled)

ggplot(data=USArrests_scaled, aes(x=Murder, y=UrbanPop, colour=cluster_num)) +
  geom_point(shape=19, size=4) +
  ggtitle("Scatter Plot of USArrests") +
  theme(plot.title = element_text(size = 20, face = "bold")) +
  annotate("text", x = USArrests_scaled$Murder, y = USArrests_scaled$UrbanPop+0.1,
           label=city_name, size = 5, color="gray") +
  annotate("point", x = km_4$centers[1,1], y = km_4$centers[1,3], size = 6, color = "black") +
  annotate("point", x = km_4$centers[2,1], y = km_4$centers[2,3], size = 6, color = "black") +
  annotate("point", x = km_4$centers[3,1], y = km_4$centers[3,3], size = 6, color = "black") +
  annotate("point", x = km_4$centers[4,1], y = km_4$centers[4,3], size = 6, color = "black") +
  annotate("text", x = km_4$centers[1,1], y = km_4$centers[1,3]+0.2, label="cluster 1", size = 8) +
  annotate("text", x = km_4$centers[2,1], y = km_4$centers[2,3]+0.2, label="cluster 2", size = 8) +
  annotate("text", x = km_4$centers[3,1], y = km_4$centers[3,3]+0.2, label="cluster 3", size = 8) +
  annotate("text", x = km_4$centers[4,1], y = km_4$centers[4,3]+0.2, label="cluster 4", size = 8)




이상으로 위의 (1) 군집별 중심 좌표 (평균), (2) 군집별 다변량 변수별 평행 좌표 그림, (3) (차원 축소 후) 군집별 산점도를 통해 군집 1 ~ 4 번까지의 특성을 파악해 본 결과,


* 군집 1 : 범죄(살인, 폭행, 강간)도 낮고, 도시인구수도 낮은 도시군 (예: Vermont, Wisconsin 등)

* 군집 2 : 범죄도 높고, 도시인구수도 높은 도시군 (예: New York, Flolida 등)

* 군집 3 : 범죄는 높고, 도시인구수는 낮은 도시군 (예: Mississippi, South Carolina 등)

* 군집 4 : 범죄는 낮고, 도시인구수는 높은 도시군 (예: Utah, Massachusetts 등)


으로 해석할 수 있겠네요.

이번 포스팅이 많은 도움이 되었기를 바랍니다.
행복한 데이터 과학자 되세요. :-)


728x90
반응형
Posted by Rfriend
,

군집분석 중에서 k-means clustering, k-median clustering, k-medoid clustering, fuzzy k-means clustering, Gaussian mixture model clustering 등의 알고리즘은 군집의 개수 k를 미리 정해주어야 합니다.


그런데 군집분석이 정답 Y가 없는 상태에서 데이터 구조/군집을 탐색하고 마이닝하는 비지도학습(unsupervised learning)이다 보니 최적의 군집의 개수 k를 결정하고 군집 결과를 평가하는 것이 다분히 주관적인 편입니다. 해당 업과 데이터에 대해서 잘 아는 SME(Subject Matter Expert)의 지식과 경험, 그리고 활용 목적을 고려한 주관적인 의견을 반영하여 최종결정하는 것이 필요하고 중요합니다.


단, 이때 주관적인 경험과 지식에만 의존하는 것이 아니라, 군집의 개수 k를 선택하는데 있어 계량적인 데이터 분석 수치와 시각화의 도움을 받을 수 있다면 좀더 쉽고 또 보다 데이터 구조와 특성을 잘 반영한 의사결정을 할 수 있을 것입니다. (물론, 이마저도 정답이 없다보니 분석가의 주관적인 평가와 판단이 들어가게 되고, 명확한 판단을 내리기가 애매한 경우도 생길 수 있습니다.)


이번 포스팅에서는 군집의 개수 k 를 미리 정해주어야 하는 알고리즘에서 군집의 개수 k를 결정하고 선택하는데 도움을 받을 수 있는 3가지 방법(Determining the number of clusters)대해서 알아보겠습니다.


(1) 계층적 군집분석의 덴드로그램 시각화를 이용한 군집의 개수 결정
     (Hierarchical Clustering: Dendrogram)

(2) 팔꿈치 방법을 이용한 군집의 개수 결정 (The Elbow Method)

(3) 실루엣 방법을 이용한 군집의 개수 결정 (The Silhouette Method)






  0. 샘플 데이터 USArrests {datasets} 불러오기 및 전처리


예제로 사용할 데이터는 R의 datasets 패키지에 내장되어 있는, 미국 주별 폭력적인 범죄 비율("Violent Crime Rates by US State)의 통계를 포함한 USArrests 데이터셋입니다. 이 데이터셋은 1973년 미국의 50개 주 별로 10만 명당 살인(Murder), 폭행(Assault), 강간(Rape) 체포 건수와 도시 인구 비율 (Percent Urban Population)의 네 개의 변수로 구성되어 있습니다.


## importing USArrests dataset from {datasets}
data(USArrests)

dim(USArrests)
# [1] 50  4

head(USArrests)
# Murder Assault UrbanPop Rape
# Alabama      13.2     236       58 21.2
# Alaska       10.0     263       48 44.5
# Arizona       8.1     294       80 31.0
# Arkansas      8.8     190       50 19.5
# California    9.0     276       91 40.6
# Colorado      7.9     204       78 38.7




군집분석을 할 때 변수별 척도가 다를 경우 큰 척도의 변수 값에 군집의 결과가 휘둘릴 수 있으므로, 먼저 scale() 함수를 이용해서 표준화(standardization)를 해주었습니다.



## standardization (scaling)
USArrests_scaled <- data.frame(scale(USArrests))

head(USArrests_scaled)
# Murder   Assault   UrbanPop         Rape
# Alabama    1.24256408 0.7828393 -0.5209066 -0.003416473
# Alaska     0.50786248 1.1068225 -1.2117642  2.484202941
# Arizona    0.07163341 1.4788032  0.9989801  1.042878388
# Arkansas   0.23234938 0.2308680 -1.0735927 -0.184916602
# California 0.27826823 1.2628144  1.7589234  2.067820292
# Colorado   0.02571456 0.3988593  0.8608085  1.864967207

 



다음으로, 표준화한 데이터에 대해서 50개의 각 주별 쌍의 유사도 행렬 (proximity matrix)로서 유클리드 거리 행렬 (Euclidean distance matrix) 을 dist(method = "euclidean") 함수를 사용해서 계산하였습니다. 자, 이제 군집분석을 위한 데이터셋 준비가 다 되었네요.



# calculating the euclidean diatance matrix
dist_eucl <- dist(USArrests_scaled, method = "euclidean")

dist_eucl
# Alabama    Alaska   Arizona  Arkansas California  Colorado Connecticut
# Alaska         2.7037541                                                               
# Arizona        2.2935197 2.7006429                                                     
# Arkansas       1.2898102 2.8260386 2.7177583                                           
# California     3.2631104 3.0125415 1.3104842 3.7636409                                 
# Colorado       2.6510673 2.3265187 1.3650307 2.8310512  1.2876185                      
# Connecticut    3.2152975 4.7399125 3.2628575 2.6076395  4.0663898 3.3279920            
# Delaware       2.0192927 3.6213633 1.9093696 1.8003239  3.0737852 2.5547456   1.7568475
# Florida        2.2981353 2.9967642 1.7493928 3.3721968  2.0250039 2.4458600   4.4700701
# Georgia        1.1314351 2.8194388 2.7871963 2.2117614  3.3780585 2.8649105   3.9738227
# Hawaii         3.3885300 4.5301340 3.2621208 2.9723097  3.6589083 2.8233524   1.3843291
# Idaho          2.9146623 4.0580555 3.5210071 1.7687255  4.4879436 3.4767685   1.6354214
# Illinois       1.8734993 3.2670626 1.0825512 2.4626424  1.9117469 1.7898322   2.7400560

-- 이하 생략함 --

 




  (1) 계층적 군집분석의 덴드로그램 시각화를 이용한 군집의 개수 결정
     (Hierarchical Clustering: Dendrogram)


계층적 군집분석(hierarchical clustering)에는 군집과 군집 간의 거리를 측정하는 다양한 연결법(linkage method)으로서, 단일연결법, 완전연결법, 평균연결법, 중심연결법, Ward연결법 등이 있습니다. 그중에서 Ward 연결법(Ward Linkage Method)에 의한 계층적 군집분석을 해보고, 이 결과를 덴드로그램(Dendrogram)으로 시각화를 해보겠습니다.


덴드로그램을 시각화 한 후에 왼쪽의 높이(Height) 축을 기준으로, 위에서 아래로 높이를 이동하면 군집의 개수가 1개, 2개, 4개, 6개, 8개, ..., 50개, 이런 식으로 변하게 됩니다.


이때 군집 간 높이의 차이가 큰 군집의 개수를 선택하면 군집 내 응집력은 높고, 군집간 이질성이 큰 적절한 군집을 구할 수 있습니다. 아래의 예의 경우 군집의 개수가 2개 또는 4개가 적당해 보이네요.


다만, 이 방법은 (a) 데이터 개수가 많을 경우 (가령, 수십만개, 수백만개) 군집화 시간이 오래걸리고, 덴드로그램으로 시각화가 거의 불가능할 수 있습니다. (b) 통계량을 산출하는게 아니고 덴드로그램 시각화 결과를 보고 분석가의 판단에 의지하다보니 다분히 주관적입니다.



set.seed(1004) # for reproducibility
hc_ward <- hclust(dist_eucl, method="ward.D")

# 덴드로그램
plot(hc_ward)





  (2) 팔꿈치 방법을 이용한 군집의 개수 결정 (The Elbow Method)


다음으로는 비계층적 군집분석(nonhierarchical clustering, partitioning clustering) 중에서 k-means 군집분석(또는 k-median, k-medoids 등)의 k를 다르게 바꾸어가면서 군집화를 했을 때, 군집의 수 k별 "군집 내 군집과 개체 간 거리 제곱합의 총합 (tot.withinss: Total within-cluster sum of squares)" 의 그래프가 팔꿈치 모양으로 꺽이는 지점의 k 를 최적 군집의 개수를 선택하는 방법이 팔꿈치 방법(The Elbow Method) 입니다.


군집 Gi의 중심 측도가 평균인 경우,


Total Within-Cluster Sum of Squares

tot.withinss =



아래의 USArrests 데이터셋 예의 경우 군집의 개수가 4개일 때 Total within-cluster sum of squares 가 팔꿈치 모양으로 꺽이고, 군집 k가 5개 이상일 때부터는 tot.withinss의 변화가 매우 작으므로, 군집의 개수를 4개로 하는 것이 가장 좋아보이네요. (분석가의 주관적인 요소가 들어가긴 합니다. ^^;)



# find the optimal number of clusters using Total within-cluster sum of squares
tot_withinss <- c()

for (i in 1:20){
  set.seed(1004) # for reproducibility
  kmeans_cluster <- kmeans(USArrests_scaled, centers = i, iter.max = 1000)
  tot_withinss[i] <- kmeans_cluster$tot.withinss
}

plot(c(1:20), tot_withinss, type="b",
     main="Optimal number of clusters",
     xlab="Number of clusters",
     ylab="Total within-cluster sum of squares")






또는, 군집의 개수 k 별로 전체 분산(Total Variance) 중에서 그룹 간 분산(Between-Group Variance)의 비율로 계산(F-test 라고도 함)하는 "설명된 분산의 비율(the Percentage of Variance Explained)"을 계산하여 시각화한 후에, 팔꿈치 모양으로 꺽이는 지점의 군집의 개수 k를 선택하는 방법을 사용할 수 도 있습니다. (이 경우 위의 그룹 내 분산(Within-cluster Sum of Squares)을 사용했을 때와는 그래프 모양이 반대가 됩니다.)





## the percentage of variance explained as a function of the number of clusters
## Percentage of variance explained is the ratio of the between-group variance to the total variance,
## also known as an F-test.
r2 <- c()

for (i in 1:20){
  set.seed(1004) # for reproducibility
  kmeans_cluster <- kmeans(USArrests_scaled, centers = i, iter.max = 1000)
  r2[i] <- kmeans_cluster$betweenss / kmeans_cluster$totss
}

plot(c(1:20), r2, type="b",
     main="The Elbow Method - Percentage of Variance Explained",
     xlab="Number of clusters",
     ylab="Percentage of Variance Explained")







  (3) 실루엣 방법을 이용한 군집의 개수 결정 (The Silhouette Method)


Peter J. Rousseeuw 는 "Silhouettes: a grapical aid to the interpretation and validation of cluster analysis" (1987) 라는 논문에서 분할적 군집분석(partitioning clustering)의 결과를 시각화하여 해석하고 평가할 수 있는 지표로서 실루엣(Silhouette)을 제안하였습니다.


실루엣을 계산하기 위해서 먼저 아래의 용어를 정의합니다. (아래의 논문 예시 그림 참조)


군집 A가 객체 i와 다른 객체들을 포함하고 있을 때,


a(i) = 군집 A 내의 객체 i 와 군집 A 내의 다른 객체들 간의 평균 비유사성 (즉, 거리 (distance))

        (average dissimilarity of i to all other objects of A)


d(i, C) = 군집 A 내의 객체 i와 다른 군집 C에 속한 모든 객체와의 평균 비유사성 (즉, 거리)

        (average dissimilarity of i to all objects of C)


b(i) = d(i, C) 의 최소값 =



* source: Reter J. Rousseeuw (1987)



실루엣은 아래 공식을 보면 알 수 있는 것처럼, 밀집성(tightness)와 분리성(separation)의 비교에 기반한 지표로서, 실루엣은 어떤 객체가 군집 안에 잘 놓여 있는지, 그리고 어떤 객체는 단지 군집들 사이의 어딘가에 놓여있는지를 보여줍니다.




전체 군집분석의 실루엣들을 하나의 그래프에 모아서 보여줌으로써, 데이터 구성과 형상의 개요(overview of the data configuration)군집들의 상대적인 질(the relative quality of the clusters)을 평가할 수 있습니다.


평균 실루엣 폭(the average silhouette width)은 군집화 결과에 대한 타당성(validity)을 평가하고, 또 적절한 군집 개수를 선택(to select an 'appropritate' number of clusters)하는데 활용할 수 있습니다.


실루엣은 -1 <= s(i) <= 1 사이의 값을 가집니다. 위 공식에 의하면 "실루엣 값이 1에 가까우면 다른 군집과의 거리보다 동일 군집 내 객체 간 거리가 가깝다는 뜻이므로 객체 i가 잘 군집화가 되었다는 의미"입니다. (이와 반대로 실루엣 값이 -1에 가까우면 객체 i의 군집화가 잘못되었다는 뜻입니다.)


만약 대부분의 객체가 높은 실루엣 값을 가진다면 군집의 구성이 적절하게 잘 되었다고 평가할 수 있습니다. 반면 많은 객체들이 낮거나 혹은 음(-)의 실루엣 값을 가진다면 군집 구성이 너무 많거나 또는 너무 적은 수의 군집을 가졌기 때문이라고 판단할 수 있습니다.

(아래 예에서는 군집4 에 정렬된 객체들의 뒷부분에 낮은 실루엣 값, 또는 음의 실루엣 값을 가진 객체들이 여러개 보이네요.)



## fviz_silhouette: Visualize Silhouette Information from Clustering
install.packages("factoextra")
library(factoextra)


# K-means clustering
set.seed(1004)
km.res <- kmeans(USArrests_scaled, centers = 4)

# Visualize silhouhette information
library("cluster")
sil <- silhouette(km.res$cluster, dist(USArrests_scaled))
fviz_silhouette(sil)





이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요. :-)


[Reference]

* Determining the number of clusters in a data set
   : https://en.wikipedia.org/wiki/Determining_the_number_of_clusters_in_a_data_set

* Silhouette (Clustering): https://en.wikipedia.org/wiki/Silhouette_(clustering)

* Peter J. Rousseeuw, Journal of Computational and Applied Mathematics 20 (1987), "Silhouettes: a Graphical Aid to the Interpretation and Validation of Cluster Analysis"



728x90
반응형
Posted by Rfriend
,

그동안 연속형(continuous), 이분형(binary), 서열형(ordinal), 명목형(nominal) 변수별 다양한 유사성(Similarity) 측정, 혹은 비유사성(Dis-similarity)으로서 거리(Distance) 측정 방법을 여러개의 포스팅에 각각 나누어서 연재하였습니다.


그러면, 만약 여러개의 변수가 있는 데이터셋에 연속형, 이분형, 서열형, 명목형 변수가 혼합되어 섞여있다면 어떻게 유사성, 혹은 비유사성을 측정해야 할까요?


이처럼 여러 데이터 유형의 변수가 섞여있으면, 각 유형의 변수별로 유사성(혹은 비유사성, 거리)을 평가한 다음에, 이를 합하거나 평균을 내주면 됩니다.


이때 각 유형 변수별 유사성 척도는 특정 유형의 변수의 영향력이 동일하게 작용하도록 [0, 1] 사이의 값을 사용합니다. 이전 포스팅에서 살펴보았듯이 이분형(binary), 명목형(nominal), 서열형(ordinal) 변수의 유사성 (혹은 비유사성 거리) 값은 0 ~ 1 사이의 값을 가집니다.


반면, 연속형 변수의 거리 (가령, 맨하탄 거리, 유클리드 등)의 값은 [0, 1] 사이의 값이 아니므로, 각 연속형 변수의 범위(range = Xmax - Xmin)로 거리를 나누어 줌으로써 연속형 변수의 정규화된 거리가 [0, 1] 사이의 값으로 변환을 해준 후에 다른 범주형 변수의 거리와 더해주거나 평균을 내주면 됩니다.


아래의 예에서는 "이분형(binary) 변수"인 성별, "연속형(continuous) 변수"인 연령과 용돈, "명목형(nominal) 변수"인 직업, "서열형(ordinal) 변수"인 직업만족도의 5개 변수에 대해 관측치 1, 2, 3의 3명 간 비유사성인 거리(distance)를 계산해보겠습니다.




먼저, 성별은 이분형(이진형, binary) 변수이므로 두 관측치의 값이 같으면 거리는 '0', 두 관측치의 값이 다르면 거리는 '1'이 됩니다. (만약 유사성을 평가하는 거라면 반대로 두 관측치의 값이    같으면 유사성은 '1', 두 관측치의 값이 다르면 유사성은 '0'이 됩니다.)


위 예에서 dist_gender(x1, x2) 는 관측치 ID 1번과 ID 2번 관측치의 성별(gender) 이분형 변수에 대한 거리를 계산한 것으로서, ID 1번은 "남성"이고 ID 2번은 "여성"으로서 서로 다르므로 거리는 '1'이 됩니다. 반면, dist_gender(x1, x3) 는 관측치 ID 1번과 ID 3번 모두 "남성"으로서 서로 같으므로 거리는 '0'이 됩니다.


다음으로 연령(age)과 용돈(allowance)은 연속형(continuous) 변수로서, 두 관측치 간 거리를 계산했을 때의 값이 [0, 1] 사이의 값으로 정규화될 수 있도록 연령의 범위(range), 용돈의 범위로 맨하탄 거리(두 값 차이의 절대값)를 나누어주었습니다.


직업(job) 변수는 명목형(nominal) 변수이므로, 두 관측치의 값이 서로 같으면 거리는 '0', 서로 다르면 거리는 '1'이 됩니다. (유사성은 이와 반대임). 따라서 관측치 ID 1, ID 2, ID 3번이 모두 서로 간에 직업이 다르므로 관측치 쌍별 직업의 거리는 모두 '1'이 됩니다.


다음으로, 직업 만족도(job satisfaction)는 서열형(ordinal) 변수로서, 정규화한 순위(normalized rank)를 사용해서 [0, 1] 사이의 거리 값을 계산해줍니다.


마지막으로, 위의 혼합형(연속형, 이분형, 명목형, 서열형) 변수 5개(p=5)에 대해 두 관측치 간 각 변수별 거리를 계산한 값들을 평균하여 모든 변수를 종합하는 평균 거리를 계산하면,




입니다.


관측치 (ID 1번, ID 3번)이 평균 거리가 0.27 로서 (ID 1번, ID 2번)의 거리 0.54, (ID 2번, ID 3번)의 거리 0.62 대비 상대적으로 거리가 짧으므로 서로 유사하다고 평가할 수 있겠네요.


위의 이론적인 부분들을 R의 사용자 정의 함수로 표현해보면 아래와 같습니다.



# =======================================
# Distance measure for mixed variables
# : continuous, binary, ordinal, nominal
# =======================================


## 3 objects with variables of gender, age, allowance, job, and job_satisfaction
id <- c(1, 2, 3)
gender <- c('M', 'F', 'M')
age <- c(41, 24, 43)
allowance <- c(350000, 400000, 320000)
job <- c('consultant', 'designer', 'officier')
job_satisfaction <- c(4, 5, 3)


## range of continuous variable (age, allowance)

age_range <- 50
allowance_range <- 400000


## max rank of ordinal variable (job satisfaction)

max_rank <- 5

## Distance for Binary variable
dist_binary <- function(xi, xj){
  d = 0
  if (xi != xj){d = 1}
  return(d)
}

## Distance for Continuous variable
dist_continuous <- function(xi, xj, x_rng){
  d = abs(xi - xj)/x_rng
  return(d)
}

## Distance for Nominal variable
dist_nominal <- function(xi, xj){
  d = 0
  if (xi != xj){d = 1}
  return(d)
}

## Distance for Ordinal variable
dist_ordinal <- function(xi, xj, max_rank){
  d = abs(xi-xj)/(max_rank-1)
  return(d)
}

## Average of Distance for Mixed variables
dist_avg <- function(xi_id, xj_id, age_range, allowance_range, max_rank){
  d_gender = dist_binary(gender[xi_id], gender[xj_id])
  d_age = dist_continuous(age[xi_id], age[xj_id], age_range)
  d_allowance = dist_continuous(allowance[xi_id], allowance[xj_id], allowance_range)
  d_job = dist_nominal(job[xi_id], job[xj_id])
  d_job_satisfaction = dist_ordinal(job_satisfaction[xi_id], job_satisfaction[xj_id], max_rank)
  d_all <- c(d_gender, d_age, d_allowance, d_job, d_job_satisfaction)
  d_avg = mean(d_all)
 
  # print for detailed information
  print(paste0("**==== Distance of ID", xi_id, " and ID", xj_id, " ====**"))
  print("-----------------------------------------")
  print(paste0("Distance of Gender (Binary):", d_gender))
  print(paste0("Distance of Age (Continuous):", d_age))
  print(paste0("Distance of Allowance (Continuous):", d_allowance))
  print(paste0("Distance of Job(Nominal):", d_job))
  print(paste0("Distance of Job Satisfaction(Ordinal):", d_job_satisfaction))
  print("-----------------------------------------")
  print(paste0("Average Distance of ID", xi_id, " and ID", xj_id, ": ", d_avg))
  print("-----------------------------------------")
 
  return(d_avg)
}

## Distance b/w ID 1 and ID 2
dist_avg(1, 2, age_range, allowance_range, max_rank)
# [1] "**==== Distance of ID1 and ID2 ====**"
# [1] "-----------------------------------------"
# [1] "Distance of Gender (Binary):1"
# [1] "Distance of Age (Continuous):0.34"
# [1] "Distance of Allowance (Continuous):0.125"
# [1] "Distance of Job(Nominal):1"
# [1] "Distance of Job Satisfaction(Ordinal):0.25"
# [1] "-----------------------------------------"
# [1] "Average Distance of ID1 and ID2: 0.543"
# [1] "-----------------------------------------"
# [1] 0.543


## Distance b/w ID 1 and ID 3
dist_avg(1, 3, age_range, allowance_range, max_rank)
# [1] "**==== Distance of ID1 and ID3 ====**"
# [1] "-----------------------------------------"
# [1] "Distance of Gender (Binary):0"
# [1] "Distance of Age (Continuous):0.04"
# [1] "Distance of Allowance (Continuous):0.075"
# [1] "Distance of Job(Nominal):1"
# [1] "Distance of Job Satisfaction(Ordinal):0.25"
# [1] "-----------------------------------------"
# [1] "Average Distance of ID1 and ID3: 0.273"
# [1] "-----------------------------------------"
# [1] 0.273


## Distance b/w ID 2 and ID 3
dist_avg(2, 3, age_range, allowance_range, max_rank)
# [1] "**==== Distance of ID2 and ID3 ====**"
# [1] "-----------------------------------------"
# [1] "Distance of Gender (Binary):1"
# [1] "Distance of Age (Continuous):0.38"
# [1] "Distance of Allowance (Continuous):0.2"
# [1] "Distance of Job(Nominal):1"
# [1] "Distance of Job Satisfaction(Ordinal):0.5"
# [1] "-----------------------------------------"
# [1] "Average Distance of ID2 and ID3: 0.616"
# [1] "-----------------------------------------"
# [1] 0.616

 



이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요! :-)



728x90
반응형
Posted by Rfriend
,

군집분석 연재하다가 다른 주제 포스팅하느라 잠시 옆길로 샌다는게... 시간이 참 많이 흘러서... 정말 오랜만에 군집분석(Clustering) 관련 포스팅하네요. ^^;

이전 포스팅에서는 연속형 변수에 대한 (비)유사성 측정 척도(https://rfriend.tistory.com/199)로서 맨하탄 거리(Manhattan distance), 유클리드 거리(Euclid distance), 표준화 거리(Standardized distance), 마할라노비스 거리(Mahalanobis distance) 등에 대해서 소개를 하였습니다.


그리고 범주형 자료에 대한 유사성 척도로서 자카드 거리, 코사인 거리, 편집거리 등에 대해서도 다루었는데요, 중복이 있긴 하지만 한번더 종합적으로 정리를 해보고, 연속형과 범주형 자료가 섞여있는 혼합 자료에 대한 유사성 측정하는 방법과 예도 소개할 겸 해서 이번 포스팅을 써봅니다.


이번 포스팅에서는 범주형 데이터(Categorical Data)로서 이분형 (binary), 서열형 (ordinal), 명목형 (nominal) 데이터에 대한 유사성 측정 척도를 소개하고, 다음번 포스팅에서는 연속형과 범주형 혼합 자료의 유사성 측정 방법에 대해 소개하겠습니다.


일반적으로 비유사성(Dis-similarity) 척도로서 거리(Distance)를 많이 사용하며, 거리가 짧을 수록 유사하고, 거리가 멀 수록 유사하지 않다고 평가합니다. 유사성(Similarity)은 비유사성(Dis-similarity) 척도인 거리의 역수 (=1-거리) 로 계산합니다. (본 포스팅에서는 유사성 척도, 비유사성 척도/ 거리가 수시로 번갈아가면서 사용을 하는데요, 혼돈하지 마시기 바랍니다.)


(1) 이분형 (이진형) 변수의 유사성 측정 (Similarity measures for Binary variable)

(2) 서열형 변수의 유사성 측정 (Similarity measures for Ordinal variable)

(3) 명목형 변수의 유사성 측정 (Similarity measures for Nominal variable)



(1) 이분형 (이진형) 변수유사성 측정 (Similarity measures for Binary variable)


범주형 자료 중에서 클래스로 두 개의 값 (보통 '0'과 '1', [0, 1] with 1 = identity) 만을 가지는 자료를 이분형 (이진형) 변수 (Binary variable) 라고 합니다. 


이분형 (이진형) 자료 변수에 대한 유사성 척도 (또는 비유사성 척도, 거리)로 Hamming distance (Simple matching), Jacard Co-efficient (Asymmetric binary attributes), Russel-Rao, Kulczynski-2, Ochiai measure, Dice similarity, Tanimoto similarity 등 여러가지 측정 척도가 있습니다. 이중에서 이번 포스팅에서는 Hamming distance, Jacard Co-efficient 에 대해서 자세히 설명하겠습니다.



(1-1) Hamming distance (Simple matching)


정보이론에서 Hamming distance 는 같은 길이의 두 문자열(two strings of equal length)을 대상으로 대응하는 기호가 서로 다른 위치의 개수를 말합니다. 다른말로 하면, 하나의 문자열을 다른 문자열과 동일하게 하기 위해 변경시켜야만 하는 최소한의 문자열 대체 회수를 의미합니다.


Hamming distance는 두개의 순열 사이의 편집거리(edit distance)를 측정하는 여러가지 문자열 측정 지표 중에서 하나입니다. Hamming distance는 미국의 수학자인 Richard Hamming의 이름을 따서 지어진 이름입니다.


Hamming distance function단순 매칭 계수 (Simple Matching Coefficient) 라고도 하며, 값이 다른 경우의 비율을 의미합니다.


관측치의 k번째 변수가 이진형 자료일 때, 단순 매칭 방법은 두 변수의 값이 같으면 1, 다르면 0으로 표현합니다.



따라서 이 변수가 0과 1로 치환되어 있다면 단순매칭에 의한 유사성은 아래처럼 표현할 수 있습니다.



p개의 이분형(이진형) 변수를 가지는 두 개의 관측치의 값을 비교하여 집계한 분할표(contingency table)를 작성하면 아래의 표와 같이 표현할 수 있습니다.



이 분할표에 대해서 해밍 거리(Hamming distance), 또는 단순 매칭 계수 (Simple matching coefficient) 는 서로 다른 값을 가지는 변수 개수의 비율이므로 다음의 식으로 표현할 수 있습니다. (거리는 dist() 또는 d() 로 표기합니다.)



반대로, 단순 매칭에 의한 유사성은 서로 같은 값을 가지는 변수 개수의 비율이므로 위의 식에서 분자가 바뀐 아래의 식으로 표현할 수 있습니다. (유사성은 sim() 으로 표기합니다.)





(1-2) Jacard Co-efficient (Asymmetric binary attributes)


자카드 거리는 어느 하나의 상태가 다른 상태보다 더 중요하거나 더 값진 비대칭(asymmetric) 인 경우에 사용합니다. 편의상 상태 '1'을 더 중요하거나 값진 상태 (일반적으로 드물게 발생하고 희소한 상태) 를 나타낼 때 사용합니다.


위의 해밍 거리 (또는 단순 매칭 계수)에서는 두 관측치 Xi, Xj의 분할표에서 a, b, c, d 의 모든 경우를 포함하였다면, Jacard Co-efficient 는 두 관측치 Xi, Xj 가 모두 '0'인 'd'의 경우는 무시하고 나머지 a, b, c 만을 가지고(비대칭적으로) 비유사성인 거리나 혹은 유사성을 측정하게 됩니다.





(자카드 거리 계수, 자카드 유사성 척도에서는 분모에서 모두가 '0'인 d 는 무시되어 빠져있습니다.)




위의 해밍 거리(Hamming distance)와 자카드 계수 (Jacard coefficient) 를 3명의 사람에 대해 증상을 진단한 6개의 변수를 가진 간단한 예를 들어서 계산을 하여 비교해보겠습니다.




위의 이분형 변수 자료를 Feaver, Cough 증상이 Y(Yes)인 경우는 1, N(No)인 경우는 0으로 치환하고, Test-1 ~ Test-4에서 P(Positive)인 경우는 1, N(Negative)인 경우는 0으로 치환하면 아래와 같은 자료를 얻습니다. 


그리고 이 자료를 사용하여 Lee와 Jung, Lee와 Hong, Jung과 Hong 별 쌍의 분할표(contingency table)를 작성합니다.


마지막으로 위의 (1)과 (2)에서 소개한 해밍 거리(Hamming Distance), 자카드 계수(Jacard Co-efficient) 공식에 분할표의 값을 대입해주면 됩니다.




거리가 작을 수록 더 관측치 간에 두 유사하다고 평가할 수 있으므로, Hamming distance와 Jacard Co-efficient 모두 가장 작은 Lee와 Jung의 두 명의 환자가 다른 두 쌍의 환자들 보다 더 유사하다고 판단할 수 있겠네요.


물론, 이번 예제의 경우 증상이 있고(Y)/없고(N), 테스트 결과의 양성(P)/음성(N)을 다룬, 변수의 값의 중요성이 다른 비대칭적(asymmetric)인 자료에 해당하므로 Jacard Co-efficient 를 사용하는게 더 적합하겠네요.




  (2) 서열형 변수의 유사성 측정 (Similarity measures for Ordinal variable)


서열형 척도(Ordinal scale)는 1 < 2 < 3 < 4 < 5 처럼 "순서(Order)가 있는" 범주형 자료의 척도를 말합니다. 예를 들면, 설문조사를 할 때 '1 (매우 불만족) < 2 (다소 불만족) < 3 (보통) < 4 (다소 만족) < 5 (매우 만족)' 과 같이 만족도를 조사하는 문항의 척도라든지, '-2 (매우 동의하지 않음), -1 (동의하지 않음), 0 (무관심), 1 (동의함), 2 (매우 동의함)' 과 같은 비교 지수, 또는 우선순위 rank,  배열 순서 등에서 서열형 척도를 사용합니다.


서열형 변수의 유사성을 측정하는 방법에는 Normalized Rank Transformation, Spearman Distance, Footrule Distance, Kendall Distance, Cayley Distance, Hamming Distance, Ulam Distance, Chebyshev/ Maximum Distance, Minkowski Distance 등 여러가지가 있습니다.


이중에서 이번 포스팅에서는 이해하기에 직관적이고 계산이 간편해서 많이 사용되는 Normalized Rank Transformation 방법을 소개하겠습니다.


등급이나 순위 등의 질적 변수(qualitative variable)인 서열형 변수는 정규화(normalization)를 통해서 양적 변수(quantititive variable)로 변환할 수 있습니다. 일단 정규화를 통해 서열형 변수를 양적변수로 변환을 하고 나면, 양적변수의 거리 계산 방법을 사용하는 원리입니다.


서열형 척도로 관측된 두 관측치의 거리를 계산하기 위해 다음의 두 단계를 거쳐서 변환을 하고, 이어서 연속형 변수에서 사용하는 거리 계산(가령, 유클리드 거리)을 하면 됩니다.



(a) 순서형 값(ordinal value)을 순위(rank)로 변환 (이때 순위(rank) r = 1 to R)

(b) 순위(rank)를 [0, 1] 사이의 값을 가지도록 정규화 (normalization)

    

(c) 정규화된 서열형 변수 X 에 대하여 연속형 변수 거리 계산 함수 적용하여 거리 계산

  p 개의 서열형 변수가 있다고 했을 때,


  • 유클리드 거리 (Euclidean distance)


  • 맨해턴 거리 (Manhattan distance)



서열형 변수의 유사성(similarity)비유사성 척도인 거리의 역수 개념이므로 1 에서 거리를 빼주어서 계산합니다.




매장 별 고객만족도를 -2 (매우 불만족) < -1 (불만족) < 0 (보통) < 1 (만족) < 2 (매우 만족) 의 5단계 서열형 척도로 설문조사한 자료가 있다고 해보겠습니다. 아래의 예처럼 Shop A, B, C 의 3개 매장에 대해서 설문문항 S1, S2, S3 의 3개 질문항목을 조사했다고 했을 때 각 매장별 거리를 구해보겠습니다.







  (3) 명목형 변수의 유사성 측정 (Similarity measures for Nominal variable)


명목형 변수는 (서열형 변수와는 다르게) "순서(Order)가 없는" 범주형 자료를 말합니다.


명목형 변수의 유사성은 두 변수의 값이 같으면 1, 다르면 0으로 평가합니다.




따라서 p개의 명목형 변수가 있다고 했을 때, 두 관측치에서 m개의 변수 값이 서로 같다면 거리와 유사성은 아래와 같습니다. (즉, Hamming distance 와 동일)






다음번 포스팅에서는 혼합형 변수의 유사성 (혹은 비유사성 거리)을 평가하는 방법과 R 로 유사성 혹은 거리를 계산하는 방법/ 코드 (https://rfriend.tistory.com/584)를 소개하겠습니다.



[Reference]

* Hamming distance: https://en.wikipedia.org/wiki/Hamming_distance

* Jacard Coefficient similarity
 : https://t4tutorials.com/jaccard-coefficient-similarity-measure-for-asymmetric-binary-variables/

* Normalized rank for ordinal data
 : https://people.revoledu.com/kardi/tutorial/Similarity/Normalized-Rank.html

* 전치혁 저, "데이터마이닝 기법과 응용", 한나래



이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요! :-)



728x90
반응형
Posted by Rfriend
,
지난번 포스팅에서는 PostgreSQL, Greenplum database에서 SQL과 Apache MADlib을 활용해서 대용량의 연속형 데이터에 대한 In-DB 상관관계 분석(Correlation Analysis in Database)에 대해서 알아보았습니다.

이번 포스팅에서는 상관관계 분석에서 한발 더 나아가서, 설명변수(독립변수) X와 목표변수(종속변수) Y 간의 선형/인과관계를 모델링하는 선형 회귀모형(Linear Regression)을 훈련(train)하고 예측(predict)하는 방법을 소개하겠습니다.

PostgreSQL, Greenplum database에서 대용량 데이터에 대해
(1) SQL로 선형 회귀모형 적합하고 모수 확인하기
(2) SQL로 그룹별로 선형 회귀모형 적합하고 예측하기
(3) Apache MADlib으로 다중 선형 회귀모형 적합하기
(4) Apache MADlib으로 그룹별로 다중 선형 회귀모형 적합하고 예측하기




먼저, 예제로 사용할 데이터로 4개의 연속형 데이터('sepal_length', 'sepal_width', 'petal_length', 'petal_width')와 1개의 범주형 데이터('class_name')를 가진 iris 데이터셋으로 테이블을 만들어보겠습니다.



-- Iris data table
DROP TABLE IF EXISTS iris;
CREATE TABLE iris (id INT, sepal_length FLOAT, sepal_width FLOAT,
                    petal_length FLOAT, petal_width FLOAT,
                   class_name text);
INSERT INTO iris VALUES
(1,5.1,3.5,1.4,0.2,'Iris-setosa'),
(2,4.9,3.0,1.4,0.2,'Iris-setosa'),
(3,4.7,3.2,1.3,0.2,'Iris-setosa'),
(4,4.6,3.1,1.5,0.2,'Iris-setosa'),
(5,5.0,3.6,1.4,0.2,'Iris-setosa'),
(6,5.4,3.9,1.7,0.4,'Iris-setosa'),
(7,4.6,3.4,1.4,0.3,'Iris-setosa'),
(8,5.0,3.4,1.5,0.2,'Iris-setosa'),
(9,4.4,2.9,1.4,0.2,'Iris-setosa'),
(10,4.9,3.1,1.5,0.1,'Iris-setosa'),
(11,7.0,3.2,4.7,1.4,'Iris-versicolor'),
(12,6.4,3.2,4.5,1.5,'Iris-versicolor'),
(13,6.9,3.1,4.9,1.5,'Iris-versicolor'),
(14,5.5,2.3,4.0,1.3,'Iris-versicolor'),
(15,6.5,2.8,4.6,1.5,'Iris-versicolor'),
(16,5.7,2.8,4.5,1.3,'Iris-versicolor'),
(17,6.3,3.3,4.7,1.6,'Iris-versicolor'),
(18,4.9,2.4,3.3,1.0,'Iris-versicolor'),
(19,6.6,2.9,4.6,1.3,'Iris-versicolor'),
(20,5.2,2.7,3.9,1.4,'Iris-versicolor'),
(21,6.3,3.3,6.0,2.5,'Iris-virginica'),
(22,5.8,2.7,5.1,1.9,'Iris-virginica'),
(23,7.1,3.0,5.9,2.1,'Iris-virginica'),
(24,6.3,2.9,5.6,1.8,'Iris-virginica'),
(25,6.5,3.0,5.8,2.2,'Iris-virginica'),
(26,7.6,3.0,6.6,2.1,'Iris-virginica'),
(27,4.9,2.5,4.5,1.7,'Iris-virginica'),
(28,7.3,2.9,6.3,1.8,'Iris-virginica'),
(29,6.7,2.5,5.8,1.8,'Iris-virginica'),
(30,7.2,3.6,6.1,2.5,'Iris-virginica');

SELECT * FROM iris ORDER BY id LIMIT 5;





  (1) SQL로 선형 회귀모형 적합하고 모수 확인하기


PostgreSQL 에서 설명변수(독립변수) X 1개와 목표변수(종속변수) Y 와의 선형 회귀모형을 적합할 수 있습니다. 대신에 하나의 함수로 한번에 선형 회귀모형을 적합하는 것은 아니구요, REGR_SLOPE(Y, X) 함수로 기울기(slope)를 구하고, REGR_INTERCEPT(Y, X)로 Y절편을 구할 수 있습니다.



-- Python으로 산점도와 선형회귀선을 겹쳐서 그래보면 아래와 같습니다.



## Scatter Plot using Python seaborn package


import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['figure.figsize'] = [12, 8]


iris = sns.load_dataset('iris')


sns.regplot(x=iris['petal_length'],
            y=iris['petal_width'],
            fit_reg=True)

plt.title('Scatter Plot with Regression Line', fontsize=16)
plt.show()

 




REGR_COUNT(Y, X)는 관측치의 개수, REGR_AVGY(Y, X) 는 Y의 평균 값, REGR_AVGX(Y, X) 는 X의 평균 값을 구해줍니다.


그리고 REGR_R2(Y, X)는 적합된 선형회귀모형의 설명력을 보여주는 결정계수(coefficient of determination)를 구해줍니다.



----------------------------
-- (1) PostgreSQL functions
----------------------------
-- Training a Regression using PostgreSQL regr_slope(Y, X), regr_intercept(Y, X) function
DROP TABLE IF EXISTS iris_regr_postgres;
CREATE TABLE iris_regr_postgres AS (
SELECT
    'petal_width' AS y_var_nm
    , 'petal_length' AS x_var_nm
    , REGR_SLOPE(petal_width, petal_length)
    , REGR_INTERCEPT(petal_width, petal_length)
    , REGR_R2(petal_width, petal_length)
    , REGR_AVGY(petal_width, petal_length)
    , REGR_AVGX(petal_width, petal_length)
    , REGR_COUNT(petal_width, petal_length)
FROM  iris
);



SELECT * FROM iris_regr_postgres;







  (2) SQL로 그룹별로 선형 회귀모형 적합하고 예측하기


다음으로 'class_name' 범주('iris_setosa', 'iris_versicolor', 'iris_virginica') 그룹별로 1개 설명변수 'petal_length'와 종속변수 'petal_width'의 관계를 모형화하는 선형 회귀모형을 적합해보겠습니다.


위의 (1)번 SQL query에 SELECT 문에 그룹 칼럼('class_name')을 넣어주고, FROM 절 다음에 GROUP BY 그룹 칼럼('class_name') 을 넣어주면 됩니다. 모델 3개가 잘 적합되었습니다.



-- Regression by Groups
DROP TABLE IF EXISTS iris_regr_grp_postgres;
CREATE TABLE iris_regr_grp_postgres AS (
SELECT
    class_name AS group_nm
    , 'petal_width' AS y_var_nm
    , 'petal_length' AS x_var_nm
    , REGR_SLOPE(petal_width, petal_length)
    , REGR_INTERCEPT(petal_width, petal_length)
    , REGR_R2(petal_width, petal_length)
    , REGR_AVGY(petal_width, petal_length)
    , REGR_AVGX(petal_width, petal_length)
    , REGR_COUNT(petal_width, petal_length)
FROM  iris
GROUP BY class_name
ORDER BY class_name
);



SELECT * FROM iris_regr_grp_postgres ORDER BY group_nm;





이제 위에서 적합한 class_name별 3개 모델(기울기 slope, Y절편 intercept)의 모수를 활용해서 아래의 수식을 사용해서 예측을 해보겠습니다.



-- Prediction
SELECT
    iris.class_name
    , iris.id
    , iris.petal_width AS y_petal_width
    , (iris.petal_length * m.regr_slope + m.regr_intercept) AS pred_petal_width
FROM iris, iris_regr_grp_postgres m
WHERE iris.class_name = m.group_nm
ORDER BY id;







  (3) Apache MADlib으로 다중 선형 회귀모형(Multiple Linear Regression) 적합하기


SQL 기반의 오픈소스 Apache MADlib의 madlib.linregr_train() 함수를 사용하여 PostgreSQL, Greenplum database에서 여러개의 설명변수를 사용하는 다중 선형회귀모형 (multiple linear regression)을 적합할 수 있습니다.


위의 (1)번 PostgreSQL 의 기울기, Y절편 함수에서는 설명변수 X로 1개의 칼럼만을 사용하는 한계가 있었습니다. 그리고 기울기와 Y절편, R^2 등을 구하기 위해 개별 함수를 사용해야 하는 불편함이 있었습니다.


반면에, MADlib의 madlib.linregr_train() 함수는 source table, output table, dependent variable, ARRAY[1, independent variables] 의 순서대로 칼럼 이름을 넣어주면 됩니다. 그러면 회귀계수(coef), 결정계수(r2), 설명변수별 표준화오차(std_err)와 t통계량(t_stats), p값(p_values), condition_no, 관측치 개수(num_rows_processed), 결측치 개수(num_missing_rows_skipped), 분산공분산(variance_covariance) 의 결과를 반환합니다.



----------------
-- (2) MADlib
----------------
-- Multivariate Regression using MADlib
DROP TABLE IF EXISTS iris_regr, iris_regr_summary;
SELECT madlib.linregr_train(
    'iris'              -- source table
    , 'iris_regr'      -- output table
    , 'petal_width' -- dependent variable
    , 'ARRAY[1, petal_length, sepal_length]' -- independent variables
);

SELECT * FROM iris_regr;





위의 선형 회귀모형 적합 결과를 좀더 보기 좋도록 UNNEST() 를 사용해서 설명변수별로 구분해서 풀어서 제시해보겠습니다.



SELECT
    UNNEST(ARRAY['intercept', 'petal_length', 'sepal_length']) AS var_nm
    , UNNEST(coef) AS coef
    , UNNEST(std_err) AS std_err
    , UNNEST(t_stats) AS t_stats
    , UNNEST(p_values) AS p_values
FROM iris_regr;






  (4) Apache MADlib으로 그룹별로 다중 선형 회귀모형 적합하고 예측하기


이번에는 class_name 범주의 그룹별로 다중 선형회귀모형을 적합해 보겠습니다.


madlib.linregr_tarin() 함수의 5번째 인자에 Grouping Column으로서 'class_name' 을 넣어주면 됩니다.



-- Multiple Regression by Group using MADlib
DROP TABLE IF EXISTS iris_regr_grp, iris_regr_grp_summary;
SELECT madlib.linregr_train(
    'iris'          -- source table
    , 'iris_regr_grp'   -- output table
    , 'petal_width' -- dependent variable
    , 'ARRAY[1, petal_length, sepal_length]' -- indepent variables
    , 'class_name'  -- grouping column
);

--SELECT * FROM iris_regr_grp;

SELECT
    class_name
    , UNNEST(ARRAY['intercept', 'petal_length', 'sepal_length']) AS var_nm
    , UNNEST(coef) AS coef
    , UNNEST(std_err) AS std_err
    , UNNEST(t_stats) AS t_stats
    , UNNEST(p_values) AS p_values
FROM iris_regr_grp;





위에서 3개 범주 그룹별로 적합한 모델을 사용해서 madlib.linregr_predict() 함수로 예측을 해보겠습니다. 이때 WHERE 조건절에 input dataset의 class_name 범주와 모델 테이블의 class_name 이 같아야 한다는 조건을 추가해줍니다.



-- Prediction
SELECT iris.*,
       madlib.linregr_predict( m.coef,
                               ARRAY[1,petal_length,sepal_length]
                             ) AS predict
FROM iris, iris_regr_grp m
WHERE  iris.class_name = m.class_name
ORDER BY id
LIMIT 10;





[Reference]
* PostgreSQL 9.4: https://www.postgresql.org/docs/9.4/functions-aggregate.html
* Apache MADlib : https://madlib.apache.org/docs/latest/group__grp__linreg.html


이번 포스팅이 많은 도움이 되었기를 바랍니다.
행복한 데이터 과학자 되세요! :-)



728x90
반응형
Posted by Rfriend
,
지난번 포스팅에서는 PostgreSQL, Greenplum database에서 SQL, MADlib 을 사용하여 연수형 데이터의 집계/ 요약통계량(aggregation/ summary statistics)을 구하는 방법(https://rfriend.tistory.com/580)을 소개하였습니다.

이번 포스팅에서는 PostgreSQL, Greenplum DB에서 SQL, Apache MADlib 을 사용하여 여러개의 연속형 변수들 간의 상관관계를 구하는 방법을 소개하겠습니다.

(1) SQL 로 두개 연속형 변수간 상관계수 구하기
(2) SQL 로 다수의 연속형 변수간 상관계수 행렬(Correlation Matrix) 구하기
(3) MADlib으로 다수의 연속형 변수간 상관계수 행렬 구하기
(4) MADlib으로 다수의 연속형 변수간 그룹별로 상관계수 행렬(Correlation Matrix by Group) 구하기



예제로 사용하도록 연속형 데이터 4개 칼럼과 범주형 데이터 1개를 가지고 iris 샘플 데이터를 테이블로 만들어보겠습니다.



-- Iris data table
DROP TABLE IF EXISTS iris;
CREATE TABLE iris (id INT, sepal_length FLOAT, sepal_width FLOAT,
                    petal_length FLOAT, petal_width FLOAT,
                   class_name TEXT);
INSERT INTO iris VALUES
(1,5.1,3.5,1.4,0.2,'Iris-setosa'),
(2,4.9,3.0,1.4,0.2,'Iris-setosa'),
(3,4.7,3.2,1.3,0.2,'Iris-setosa'),
(4,4.6,3.1,1.5,0.2,'Iris-setosa'),
(5,5.0,3.6,1.4,0.2,'Iris-setosa'),
(6,5.4,3.9,1.7,0.4,'Iris-setosa'),
(7,4.6,3.4,1.4,0.3,'Iris-setosa'),
(8,5.0,3.4,1.5,0.2,'Iris-setosa'),
(9,4.4,2.9,1.4,0.2,'Iris-setosa'),
(10,4.9,3.1,1.5,0.1,'Iris-setosa'),
(11,7.0,3.2,4.7,1.4,'Iris-versicolor'),
(12,6.4,3.2,4.5,1.5,'Iris-versicolor'),
(13,6.9,3.1,4.9,1.5,'Iris-versicolor'),
(14,5.5,2.3,4.0,1.3,'Iris-versicolor'),
(15,6.5,2.8,4.6,1.5,'Iris-versicolor'),
(16,5.7,2.8,4.5,1.3,'Iris-versicolor'),
(17,6.3,3.3,4.7,1.6,'Iris-versicolor'),
(18,4.9,2.4,3.3,1.0,'Iris-versicolor'),
(19,6.6,2.9,4.6,1.3,'Iris-versicolor'),
(20,5.2,2.7,3.9,1.4,'Iris-versicolor'),
(21,6.3,3.3,6.0,2.5,'Iris-virginica'),
(22,5.8,2.7,5.1,1.9,'Iris-virginica'),
(23,7.1,3.0,5.9,2.1,'Iris-virginica'),
(24,6.3,2.9,5.6,1.8,'Iris-virginica'),
(25,6.5,3.0,5.8,2.2,'Iris-virginica'),
(26,7.6,3.0,6.6,2.1,'Iris-virginica'),
(27,4.9,2.5,4.5,1.7,'Iris-virginica'),
(28,7.3,2.9,6.3,1.8,'Iris-virginica'),
(29,6.7,2.5,5.8,1.8,'Iris-virginica'),
(30,7.2,3.6,6.1,2.5,'Iris-virginica');



SELECT * FROM iris ORDER BY id LIMIT 5;






  (1) SQL 로 두개 연속형 변수간 상관계수 구하기


공분산(Covariance)은 두 변량이 각각의 평균으로부터 변화하는 방향 및 양에 대한 기대값입니다. PostgreSQL, Greenplum DB에서 모집단의 공분산(Population Covariance)는 COVAR_POP(Y, X) 함수를 사용하며, 샘플 공분산(Sample Covariance)은 COVAR_SAMP(Y, X) 함수를 사용합니다.

(* 공분산, 상관계수 참고: https://rfriend.tistory.com/126)


피어슨 상관계수(Pearson's Correlation Coefficient)는 공분산을 표준편차로 나누어서 표준화해준 값으로서, -1~1 사이의 값을 가집니다. 1에 가까울수록 두 변수간에 양의 선형관계가 있고, -1에 가까울수록 음의 선형관계가 있으며, 0이면 선형관계가 없다고 해석합니다. PostgreSQL, Greenplum DB에서는 CORR(Y, X) 함수를 사용합니다.



----- b/w Y and X
-- Population Covariance using SQL: covar_pop(Y, X)
SELECT COVAR_POP(sepal_length, sepal_width) FROM iris;


-- Sample Covariance using SQL: covar_samp(Y, X)
SELECT COVAR_SAMP(sepal_width, sepal_length) FROM iris;


-- Pearson's Correlation Coefficients using SQL: corr(Y, X)
SELECT CORR(sepal_width, sepal_length) FROM iris;





  (2) SQL 로 다수의 연속형 변수간 상관계수 행렬(Correlation Matrix) 구하기


먼저, 이해를 돕기 위해서 2단계로 나누어서 과정을 설명하자면요, 긴 형태(long format)로 두 연속형 변수 간의 상관계수를 구해보겠습니다. (두번째 단계에서 Pivoting 하는데 사용하는, sub query 의 내용임)


------ Correlation Coefficients among multiple variables
-- Correlation Matrix

-- (a) Calculating pairwise correlation using corr(Y, X) function

SELECT
    1 AS column_position
    , 'sepal_length' AS row
    , 'sepal_width' AS col
    , CORR(sepal_length, sepal_width) AS corr_coef
FROM iris
UNION
SELECT

    1 AS column_position
    , 'sepal_length' AS row
    , 'petal_length' AS col
    , CORR(sepal_length, petal_length) AS corr_coef
FROM iris
UNION
SELECT

    1 AS column_position
    , 'sepal_length' AS row
    , 'petal_width' AS col
    , CORR(sepal_length, petal_width) AS corr_coef
FROM iris
UNION
SELECT

    2 AS column_position
    , 'sepal_width' AS row
    , 'petal_length' AS col
    , CORR(sepal_width, petal_length) AS corr_coef
FROM iris
UNION
SELECT

    2 AS column_position
    , 'sepal_width' AS row
    , 'petal_width' AS col
    , CORR(sepal_width, petal_width) AS corr_coef
FROM iris
UNION
SELECT

    3 AS column_position
    , 'petal_length' AS row
    , 'petal_width' AS col
    , CORR(petal_length, petal_width) AS corr_coef
FROM iris;




위의 SQL query를 sub query 로 넣고, manual 하게 테이블을 Pivot 하여 우리가 원하는 상관계수 행렬 (correlation matrix)를 만들어보겠습니다. 대각행렬에서 우상단에만 상관계수를 나타내고 좌하단에는 0으로 채워넣기를 하였습니다.



-- (b) Pivoting the table to get a correlation matrix

SELECT
    column_position
    , row
    , SUM(CASE WHEN col='sepal_length' THEN corr_coef ELSE 0 END) AS sepal_length
    , SUM(CASE WHEN col='sepal_width' THEN corr_coef ELSE 0 END) AS sepal_width
    , SUM(CASE WHEN col='petal_length' THEN corr_coef ELSE 0 END) AS petal_length
    , SUM(CASE WHEN col='petal_width' THEN corr_coef ELSE 0 END) AS petal_width
FROM (
    SELECT
        1 AS column_position
        , 'sepal_length' AS row
        , 'sepal_width' AS col
        , CORR(sepal_length, sepal_width) AS corr_coef
    FROM iris
    UNION
    SELECT

        1 AS column_position
        , 'sepal_length' AS row
        , 'petal_length' AS col
        , CORR(sepal_length, petal_length) AS corr_coef
    FROM iris
    UNION
    SELECT

        1 AS column_position
        , 'sepal_length' AS row
        , 'petal_width' AS col
        , CORR(sepal_length, petal_width) AS corr_coef
    FROM iris
    UNION
    SELECT

        2 AS column_position
        , 'sepal_width' AS row
        , 'petal_length' AS col
        , CORR(sepal_width, petal_length) AS corr_coef
    FROM iris
    UNION
    SELECT

        2 AS column_position
        , 'sepal_width' AS row
        , 'petal_width' AS col
        , CORR(sepal_width, petal_width) AS corr_coef
    FROM iris
    UNION
    SELECT
        3 AS column_position
        , 'petal_length' AS row
        , 'petal_width' AS col
        , CORR(petal_length, petal_width) AS corr_coef
    FROM iris
) a
GROUP BY column_position, row
ORDER BY column_position;




원하는 상관계수행렬을 구하기는 했는데요, SQL query가 엄청 길어지고 또 복잡해졌습니다. 분석의 대상이 되는 연속형 데이터 칼럼 수가 늘어날수록 query가 더 길어지고 복잡해지는 구조여서 더 좋은 방법이 없을까 고민하게 됩니다. 짜잔~! 이럴때 PostgreSQL, Greenplum DB에서 쉽고 편하고 또 빠르게 분석하는데 쓸 수 있는 SQL 기반의 오픈소스 분석툴인 Apache MADlib이 있습니다!




  (3) MADlib으로 다수의 연속형 변수간 상관계수 행렬 구하기


위의 (2)번에서 pure SQL로 했던 상관계수 행렬 구하기를 Apache MADlib의 madlib.correlation() 함수를 사용하면 아래처럼 단 3줄이면 끝납니다! 비교할 수 없을 정도로 SQL query가 간소해졌습니다!



----- Correlation Matrix using MADlib
DROP TABLE IF EXISTS iris_corr_output, iris_corr_output_summary;
SELECT madlib.correlation( 'iris',             -- source table
                           'iris_corr_output',       -- output table
                           'sepal_length, sepal_width, petal_length, petal_width' -- target columns
                         );


/*                         
SELECT madlib.correlation( 'iris',        -- source table
                           'iris_corr_output',  -- output table
                           '*'                     -- for all columns
                         );                        
*/
                       


SELECT * FROM iris_corr_output ORDER BY column_position;






  (4) MADlib으로 다수의 연속형 변수간 그룹별로 상관계수 행렬구하기

       (Correlation Matrix by Group using Apache MADlib)


PostgreSQL, Greenplum DB에서 Apache MADlib을 사용했을 때 코드가 간소해지는점 말고 또 좋은 점 중의 하나는 그룹별 연산을 하기에도 굉장히 편리하다는 점입니다.


아래 예제는 class_name ('iris-setosa', 'iris-versicolor', 'iris-virginica') 의 그룹별로 상관계수 행렬을 구하기(correlation matrix by groups using MADlib) 입니다. 위의 (3)번 예제어서 'grouping column' 위치에 "class_name" 칼럼 이름만 추가하면 끝입니다. MADlib이 이처럼 "simple & powerful" 합니다. (만약 (2)번 방법으로 SQL로 일일이 group by 넣어서 query 짠다고 상상을 해보면 끔찍하지 않나요? ^^;)



-- Correlation Matrix by Groups using MADlib
DROP TABLE IF EXISTS iris_corr_grp_output, iris_corr_grp_output_summary;
SELECT madlib.correlation( 'iris'             -- source table
                           , 'iris_corr_grp_output' -- output table
                           , '*' -- target columns
                           , False -- verbose
                           , 'class_name' -- grouping column
                         );
                         


SELECT * FROM iris_corr_grp_output ORDER BY class_name, column_position;




[Reference]
* PostgreSQL 9.4: https://www.postgresql.org/docs/9.4/functions-aggregate.html
* Apache MADlib: https://madlib.apache.org/docs/v1.0/group__grp__correlation.html

이번 포스팅이 많은 도움이 되었기를 바랍니다 .
행복한 데이터 과학자 되세요!  :-)




728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 PostgreSQL, Greenplum database에서 SQL, MADlib 함수, PL/R, PL/Python을 사용해서 연속형 데이터에 대한 요약통계량을 구하는 방법을 소개하겠습니다.  무척 쉬운 내용이므로 쉬어가는 코너 정도로 가볍게 생각해주시면 좋겠습니다. ^^


PostgreSQL, Greenplum database 에서 연속형 데이터에 대해 그룹별로, 

(1) SQL 로 요약통계량 구하기

(2) Apache MADlib 으로 요약통계량 구하기





참고로, 이번 포스팅에서는 PostgreSQL 9.4, Greenplum 6.10.1 버전을 사용하였으며, PostgreSQL 9.4 버전보다 낮은 버전을 사용하면 최빈값(mode), 사분위부(percentile) 구하는 함수를 사용할 수 없습니다. 


먼저, 예제로 사용하기 위해 '나이'의 연속형 데이터와 '성별'의 범주형 데이터 (그룹)를 가진 간단한 테이블을 만들어보겠습니다. 결측값(missing value)도 성별 그룹별로 몇 개 넣어봤습니다. 



DROP TABLE IF EXISTS cust;

CREATE TABLE cust (id INT, age INT, gender TEXT);

INSERT INTO cust VALUES

(1,NULL,'M'),

(2,NULL,'M'),

(3,25,'M'),

(4,28,'M'),

(5,27,'M'),

(6,25,'M'),

(7,26,'M'),

(8,29,'M'),

(9,25,'M'),

(10,27,'M'),

(11,NULL,'F'),

(12,23,'F'),

(13,25,'F'),

(14,23,'F'),

(15,24,'F'),

(16,26,'F'),

(17,23,'F'),

(18,24,'F'),

(19,22,'F'),

(20,23,'F');

 




 (1) SQL로 연속형 데이터의 그룹별 요약통계량 구하기


함수가 굳이 설명을 안해도 될 정도로 간단하므로 길게 설명하지는 않겠습니다. 


표준편차 STDDEV() 와 분산 VARIANCE() 함수는 표본표준편차(sample standard deviation), 표본분산(sample variance) 를 계산해줍니다. 만약 모표준편차(population standard deviation), 모분산(population variance)를 구하고 싶으면 STDDEV_POP(), VAR_POP() 함수를 사용하면 됩니다. 


PostgreSQL 9.4 버전 이상부터 최빈값(MODE), 백분위수(Percentile) 함수가 생겨서 정렬한 후에 집계하는 기능이 매우 편리해졌습니다. (MODE(), PERCENTILE_DISC() 함수를 사용하지 않고 pure SQL로 최빈값과 백분위수를 구하려면 query 가 꽤 길어집니다.)



SELECT

    gender AS group_by_value

    , 'age' AS target_column

    , COUNT(*) AS row_count

    , COUNT(DISTINCT age) AS distinct_values

    , AVG(age)

    , VARIANCE(age)

    , STDDEV(age)

    , MIN(age)

    , PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY age) AS first_quartile

    , MEDIAN(age)

    , PERCENTILE_DISC(0.75) WITHIN GROUP (ORDER BY age) AS third_quartile

    , MAX(age)

    , MODE() WITHIN GROUP (ORDER BY age) -- over PostgreSQL 9.4

FROM cust

WHERE age IS NOT NULL

GROUP BY gender

ORDER BY gender;





성별 그룹별로 연령(age) 칼럼의 결측값 개수를 구해보겠습니다. 

결측값 개수는 WHERE age IS NULL 로 조건절을 주고 COUNT(*)로 행의 개수를 세어주면 됩니다. 



SELECT 

    gender

    , COUNT(*) AS missing_count

FROM cust

WHERE age IS NULL

GROUP BY gender

ORDER BY gender;


Out[5]:
gendermissing_count
F1
M2





위의 집계/ 요약통계량과 결측값 개수를 하나의 조회 결과로 보려면 아래처럼 Join 을 해주면 됩니다.



WITH summary_tbl AS (
    SELECT
        gender AS group_by_value
        , 'age' AS target_column
        , COUNT(*) AS row_count
        , COUNT(DISTINCT age) AS distinct_values
        , AVG(age)
        , VARIANCE(age)
        , STDDEV(age)
        , MIN(age)
        , PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY age) AS first_quartile
        , MEDIAN(age)
        , PERCENTILE_DISC(0.75) WITHIN GROUP (ORDER BY age) AS third_quartile
        , MAX(age)
        , MODE() WITHIN GROUP (ORDER BY age)
    FROM cust
    WHERE age IS NOT NULL
    GROUP BY gender
    ORDER BY gender
), missing_tbl AS (
    SELECT
        gender AS group_by_value
        , COUNT(*) AS missing_count
    FROM cust
    WHERE age IS NULL
    GROUP BY gender
)
SELECT a.*, b.missing_count
FROM summary_tbl a LEFT JOIN missing_tbl b USING(group_by_value)
;

 




  (2) Apache MADlib으로 연속형 데이터의 그룹별 요약통계량 구하기


Apache MADlib의 madlib.summary() 함수를 사용하면 단 몇 줄의 코드만으로 위의 (1)번에서 SQL 집계 함수를 사용해서 길게 짠 코드를 대신해서 매우 깔끔하고 간단하게 구할 수 있습니다. 


아래는 (1)번의 결과를 얻기위해 성별(gender) 연령(age) 칼럼의 집계/요약데이터를 구하는 madlib.summary() 함수 예시입니다. 


Target columns 위치에는 1 개 이상의 분석을 원하는 연속형 데이터 칼럼을 추가로 넣어주기만 하면 되므로 (1) 번의 pure SQL 대비 훨씬 편리한 측면이 있습니다! 


그리고 그룹별로 구분해서 집계/요약하고 싶으면 Grouping columns 위치에 기준 칼럼 이름을 넣어주기만 하면 되므로 역시 (1)번의 pure SQL 대비 훨씬 편리합니다!



DROP TABLE IF EXISTS cust_summary;

SELECT madlib.summary('cust'     -- Source table

                      ,'cust_summary'   -- Output table

                      , 'age'                -- Target columns

                      , 'gender'            -- Grouping columns

);






madlib.summary() 함수의 결과 테이블에서 조회할 수 있는 집계/요약통계량 칼럼 리스트는 아래와 같습니다. 



SELECT column_name

FROM INFORMATION_SCHEMA.COLUMNS

    WHERE TABLE_SCHEMA = 'public'

        AND TABLE_NAME    = 'cust_summary'

    ORDER BY ORDINAL_POSITION;

Out[7]:
column_name
group_by
group_by_value
target_column
column_number
data_type
row_count
distinct_values
missing_values
blank_values
fraction_missing
fraction_blank
positive_values
negative_values
zero_values
mean
variance
confidence_interval
min
max
first_quartile
median
third_quartile
most_frequent_values
mfv_frequencies

 



[Reference]

* PostgreSQL aggregate functions: https://www.postgresql.org/docs/9.4/functions-aggregate.html

* Apache MADlib summary function: https://madlib.apache.org/docs/v1.11/group__grp__summary.html



다음번 포스팅에서는 PostgreSQL, Greenplum에서 SQL과 Apache MADlib을 이용하여 상관계수, 상관계수 행렬을 구하는 방법(https://rfriend.tistory.com/581)을 소개하겠습니다.


이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!



728x90
반응형
Posted by Rfriend
,