聚类分析是探索性数据分析的一种形式,在这种分析中,观测数据被分成具有共同特征的不同组。
聚类分析(也称为分类)的目的是构造群(或类或群),同时确保以下性质:在一个群中观测值必须尽可能相似,而属于不同群的观测值必须尽可能不同。
主要有两种分类:
第一种方法通常在预先确定类的数量时使用,而第二种方法通常用于未知数量的类,并帮助确定最佳数量。这两种方法在下面通过演算和R程序中的应用进行了说明。注意,对于层次聚类,本文只介绍了升序分类。
聚类算法利用距离将观测数据分成不同的组。因此,在深入介绍这两种分类方法之前,将介绍如何计算点之间距离的演算。
存在一个数据集,包含点 a = ( 0 , 0 ) ′ , b = ( 1 , 0 ) ′ a = (0,0)', b=(1,0)' a=(0,0)′,b=(1,0)′ 和 c = ( 5 , 5 ) ′ c=(5,5)' c=(5,5)′. 计算点间欧式距离矩阵(matrix of Euclidean distances)。
# We create the points in R
a <- c(0, 0)
b <- c(1, 0)
c <- c(5, 5)
X <- rbind(a, b, c) # a, b and c are combined per row
colnames(X) <- c("x", "y") # rename columns
X # display the points
OUTPUT:
## x y
## a 0 0
## b 1 0
## c 5 5
根据勾股定理(Pythagorean formula),我们知道 ( x a , y a ) (x_a, y_a) (xa,ya)和 ( x b , y b ) (x_b, y_b) (xb,yb)之间的距离在 R 2 \mathbb{R}^2 R2中是 ( x a − x b ) 2 + ( y a − y b ) 2 \sqrt{(x_a - x_b)^2 + (y_a - y_b)^2} (xa−xb)2+(ya−yb)2. 此外, b = ( 1 , 0 ) ′ b=(1,0)' b=(1,0)′ 和 c = ( 5 , 5 ) ′ c=(5,5)' c=(5,5)′ 的距离如下:
( x b − x c ) 2 + ( y b − y c ) 2 = ( 1 − 5 ) 2 + ( 0 − 5 ) 2 = 6.403124 \sqrt{(x_b - x_c)^2 + (y_b - y_c)^2} = \sqrt{(1-5)^2 + (0-5)^2}\\ = 6.403124 (xb−xc)2+(yb−yc)2=(1−5)2+(0−5)2=6.403124
在R中,我们可以用 dist() 函数来计算点之间的距离:
# The distance is found using the dist() function:
distance <- dist(X, method = "euclidean")
distance # display the distance matrix
OUTPUT:
## a b
## b 1.000000
## c 7.071068 6.403124
需要注意的是,method = “euclidean” 不是强制性的,因为欧式方法是默认方法。
dist() 函数生成的距离矩阵给出了不同点之间的距离。点b和c之间的欧几里德距离是6.403124,这与我们上面通过勾股定理得到的结果相对应。
注:如果两个变量的单位不同,在计算欧式距离时,一个变量的权重可能比另一个更大。在这种情况下,最好缩放数据。缩放数据允许获得独立于其单位的变量,这可以通过 scale() 函数来实现。
该方法的目的是将n个观测值划分为k个聚类,其中每个观测值属于最接近平均值的聚类,作为聚类的原型。
K-means算法的思想很简单,简单来讲就是对于给定的样本集,按照样本之间的距离大小,将样本集划分为K个簇。让簇内的点尽量紧密的连在一起,而让簇间的距离尽量的大,两个对象之间的距离越近,相似性越高。聚类的结果就是使类内部的同质性高,而类之间的异质性高。- - - Kmeans聚类介绍及R语言实现(黑_太狼de数据)。
k-means算法就是将 n 个数据点进行聚类分析,得到 k 个聚类,使得每个数据点到聚类中心的距离最小。而实际上,这个问题往往是NP-hard的,以此有许多启发式的方法求解,从而避开局部最小值。值得注意的是,k-means算法往往容易和k-nearest neighbor classifier(k-NN)[下篇讲解] 算法混淆。后者是有监督学习的分类(回归)算法,主要是用来判定数据点属于哪个类别中心的。
下图的数据库包含了1979年26个欧洲国家不同行业的就业人口百分比。它包含10个变量:
# Import data
Eurojobs <- read.csv(
file = "data/Eurojobs.csv",
sep = ",", dec = ".", header = TRUE
)
head(Eurojobs) # head() is used to display only the first 6 observations
OUTPUT:
## Country Agr Min Man PS Con SI Fin SPS TC
## 1 Belgium 3.3 0.9 27.6 0.9 8.2 19.1 6.2 26.6 7.2
## 2 Denmark 9.2 0.1 21.8 0.6 8.3 14.6 6.5 32.2 7.1
## 3 France 10.8 0.8 27.5 0.9 8.9 16.8 6.0 22.6 5.7
## 4 W. Germany 6.7 1.3 35.8 0.9 7.3 14.4 5.0 22.3 6.1
## 5 Ireland 23.2 1.0 20.7 1.3 7.5 16.8 2.8 20.8 6.1
## 6 Italy 15.9 0.6 27.6 0.5 10.0 18.1 1.6 20.1 5.7
可以看到,在第一个变量Country之前有一个编号。为了更清楚起见,用国家取代这个编号。为此,需要添加参数行名称在导入函数中为1读取.csv()指定第一列对应于行名称:
Eurojobs <- read.csv(
file = "data/Eurojobs.csv",
sep = ",", dec = ".", header = TRUE, row.names = 1
)
Eurojobs # displays dataset
OUTPUT:
## Agr Min Man PS Con SI Fin SPS TC
## Belgium 3.3 0.9 27.6 0.9 8.2 19.1 6.2 26.6 7.2
## Denmark 9.2 0.1 21.8 0.6 8.3 14.6 6.5 32.2 7.1
## France 10.8 0.8 27.5 0.9 8.9 16.8 6.0 22.6 5.7
## W. Germany 6.7 1.3 35.8 0.9 7.3 14.4 5.0 22.3 6.1
## Ireland 23.2 1.0 20.7 1.3 7.5 16.8 2.8 20.8 6.1
## Italy 15.9 0.6 27.6 0.5 10.0 18.1 1.6 20.1 5.7
## Luxembourg 7.7 3.1 30.8 0.8 9.2 18.5 4.6 19.2 6.2
## Netherlands 6.3 0.1 22.5 1.0 9.9 18.0 6.8 28.5 6.8
## United Kingdom 2.7 1.4 30.2 1.4 6.9 16.9 5.7 28.3 6.4
## Austria 12.7 1.1 30.2 1.4 9.0 16.8 4.9 16.8 7.0
## Finland 13.0 0.4 25.9 1.3 7.4 14.7 5.5 24.3 7.6
## Greece 41.4 0.6 17.6 0.6 8.1 11.5 2.4 11.0 6.7
## Norway 9.0 0.5 22.4 0.8 8.6 16.9 4.7 27.6 9.4
## Portugal 27.8 0.3 24.5 0.6 8.4 13.3 2.7 16.7 5.7
## Spain 22.9 0.8 28.5 0.7 11.5 9.7 8.5 11.8 5.5
## Sweden 6.1 0.4 25.9 0.8 7.2 14.4 6.0 32.4 6.8
## Switzerland 7.7 0.2 37.8 0.8 9.5 17.5 5.3 15.4 5.7
## Turkey 66.8 0.7 7.9 0.1 2.8 5.2 1.1 11.9 3.2
## Bulgaria 23.6 1.9 32.3 0.6 7.9 8.0 0.7 18.2 6.7
## Czechoslovakia 16.5 2.9 35.5 1.2 8.7 9.2 0.9 17.9 7.0
## E. Germany 4.2 2.9 41.2 1.3 7.6 11.2 1.2 22.1 8.4
## Hungary 21.7 3.1 29.6 1.9 8.2 9.4 0.9 17.2 8.0
## Poland 31.1 2.5 25.7 0.9 8.4 7.5 0.9 16.1 6.9
## Rumania 34.7 2.1 30.1 0.6 8.7 5.9 1.3 11.7 5.0
## USSR 23.7 1.4 25.8 0.6 9.2 6.1 0.5 23.6 9.3
## Yugoslavia 48.7 1.5 16.8 1.1 4.9 6.4 11.3 5.3 4.0
dim(Eurojobs) # displays the number of rows and columns
OUTPUT:
## [1] 26 9
我们现在有了一个由26个观察值和9个连续定量变量组成的“干净”数据集,我们可以以此为基础进行分类。注意,在这种情况下,没有必要对数据进行标准化,因为它们都是用同一单位(百分比)表示的。否则,我们通过scale()函数标准化数据。
所谓的k-means集群是通过 kmeans() 函数完成的,参数中心对应于所需集群的数量。在下面的例子中,我们用2个类和3个类来应用分类。
model <- kmeans(Eurojobs, centers = 2)
# displays the class determined by
# the model for all observations:
print(model$cluster)
OUTPUT:
## Belgium Denmark France W. Germany Ireland
## 1 1 1 1 2
## Italy Luxembourg Netherlands United Kingdom Austria
## 1 1 1 1 1
## Finland Greece Norway Portugal Spain
## 1 2 1 2 2
## Sweden Switzerland Turkey Bulgaria Czechoslovakia
## 1 1 2 2 1
## E. Germany Hungary Poland Rumania USSR
## 1 2 2 2 2
## Yugoslavia
## 2
参数centers=2用于设置预先确定的簇数。在此文章中,簇的数目是任意确定的。应该根据分析的上下文和目标,或者根据本文中介绍的方法来确定集群的数量.调用 print(model$cluster),此输出指定每个国家所属的组(即1或2)。
每个观测值的聚类可以作为列直接存储在数据集中:
Eurojobs_cluster <- data.frame(Eurojobs,
cluster = as.factor(model$cluster)
)
head(Eurojobs_cluster)
OUTPUT:
## Agr Min Man PS Con SI Fin SPS TC cluster
## Belgium 3.3 0.9 27.6 0.9 8.2 19.1 6.2 26.6 7.2 1
## Denmark 9.2 0.1 21.8 0.6 8.3 14.6 6.5 32.2 7.1 1
## France 10.8 0.8 27.5 0.9 8.9 16.8 6.0 22.6 5.7 1
## W. Germany 6.7 1.3 35.8 0.9 7.3 14.4 5.0 22.3 6.1 1
## Ireland 23.2 1.0 20.7 1.3 7.5 16.8 2.8 20.8 6.1 2
## Italy 15.9 0.6 27.6 0.5 10.0 18.1 1.6 20.1 5.7 1
通过使用以下公式计算分区“解释”的TSS百分比:
BSS TSS × 100 % \dfrac{\operatorname{BSS}}{\operatorname{TSS}} \times 100\% TSSBSS×100%
其中BSS和TSS分别代表Stand for Between Sum of Squares 与 Total Sum of Squares。百分比越高,得分(以及质量)就越好,它意味着BSS很大和/或TSS很小。
# BSS and TSS are extracted from the model and stored
(BSS <- model$betweenss)
OUTPUT:
## [1] 4823.535
(TSS <- model$totss)
OUTPUT:
## [1] 9299.59
# We calculate the quality of the partition
BSS / TSS * 100
OUTPUT:
## [1] 51.86826
这个间隔比例是51.86826%,通常这个数字没有特定的含义。只有在和其他分区(具有相同数量的集群)的质量相比时,才能够反映出它的重要性。
k-means算法使用一组随机的初始点来达到最终的分类。由于初始中心是随机选择的,同一个命令kmeans(Eurojobs,centers=2)每次运行时可能会给出不同的结果,因此分区带来的结果会略有不同。kmeans()函数中的nstart参数允许使用不同的初始中心多次运行该算法,以便获得一个可能更好的分区:
model2 <- kmeans(Eurojobs, centers = 2, nstart = 10)
100 * model2$betweenss / model2$totss
OUTPUT:
## [1] 54.2503
根据最初的随机选择,这个新分区与第一个分区相比是否更好。在我们的例子中,当质量提高到54.2503%时,分区会更好。
关于k-means经常被引用的一个主要限制是结果的稳定性。由于初始中心是随机选择的,运行相同的命令可能会产生不同的结果。在kmeans()函数中添加nstart参数将限制此问题,因为它将生成多个不同的初始化,并采用最优化的初始化,从而提高分类的稳定性。
我们现在使用3个集群来执行k-均值分类并计算其质量:
model3 <- kmeans(Eurojobs, centers = 3)
BSS3 <- model3$betweenss
TSS3 <- model3$totss
BSS3 / TSS3 * 100
OUTPUT:
## [1] 74.59455
可以看出,分为三组可以获得更高的解释百分比和更高的质量。因此我们可以猜测:有了更多的类,分区就会更精细,BSS的贡献也会更高。另一方面,“模型”将更加复杂,需要更多的类。在k=n(每个观测值都是一个单例类)的极端情况下,会有BSS=TSS,但是分区也失去了意义。
为了找到k-means的最佳聚类数,建议根据以下条件选择:
根据数据中有特定数量的组(较为主观).
以下四种方法:
1, 肘部法则(Elbow Method method), 使用簇内平方和。
我们知道k-means是以最小化样本与质点平方误差作为目标函数,将每个簇的质点与簇内样本点的平方距离误差和称为畸变程度(distortions),那么,对于一个簇,它的畸变程度越低,代表簇内成员越紧密,畸变程度越高,代表簇内结构越松散。 畸变程度会随着类别的增加而降低,但对于有一定区分度的数据,在达到某个临界点时畸变程度会得到极大改善,之后缓慢下降,这个临界点就可以考虑为聚类性能较好的点。
2, 平均轮廓法(Average silhouette method).
s ( i ) = b ( i ) − a ( i ) m a x ( a ( i ) , b ( i ) , i f ∣ C i ∣ > 1 s(i) = \frac{b(i) - a(i)}{max(a(i),b(i)} , if |C_i| > 1 s(i)=max(a(i),b(i)b(i)−a(i),if∣Ci∣>1
a(i)是测量组内的相似度,b(i)是测量组间的相似度,s(i)范围从-1到1,值越大说明组内吻合越高,组间距离越远——也就是说,轮廓系数值越大,聚类效果越好.
3, 缺口统计法(Gap statistic method).
之前我们提到了通过找“肘点”来找到最佳聚类数,肘点的选择并不是那么清晰,因此 R. Tibshirani, G. Walther, and T. Hastie (Standford University, 2001)提出了Gap Statistic方法,定义的Gap值为:
G a p ( k ) = 1 B ∑ b = 1 B l o g ( W k b ∗ ) − l o g ( W k ) Gap(k) = \frac{1}{B} \sum\limits_{b=1}^B log(W_{kb}^*) - log(W_k) Gap(k)=B1b=1∑Blog(Wkb∗)−log(Wk)
4, Nbclust包. 见RDocumentation.
下面我将用这四种方法用R来实现:
1, 肘部法则.
# load required packages
library(factoextra)
library(NbClust)
# Elbow method
fviz_nbclust(Eurojobs, kmeans, method = "wss") +
geom_vline(xintercept = 4, linetype = 2) + # add line for better visualisation
labs(subtitle = "Elbow method") # add subtitle
图中膝部的位置通常被认为是适当数量的簇的一个指标,因为这意味着添加另一个簇并不能更好地改善分区。因此这个方法建议4个聚类。肘部法则有时候很难得到明确的结果。
2,平均轮廓法
平均轮廓法测量聚类的质量,并确定每个点在其簇中的位置。
平均轮廓法建议2个簇。
3,缺口统计法
最佳的聚类数是使差距统计最大化的聚类数。这种方法只建议1个聚类(因此这是一个无用的聚类)。
因此,这三种方法不一定会导致相同的结果。在这里,所有3种方法都建议不同数量的集群。
4, NbClust()
nbclust_out <- NbClust(
data = Eurojobs,
distance = "euclidean",
min.nc = 2, # minimum number of clusters
max.nc = 5, # maximum number of clusters
method = "kmeans"
) # one of: "ward.D", "ward.D2", "single", "complete", "average", "mcquitty", "median", "centroid", "kmeans"
OUTPUT:
*** : 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:
* 6 proposed 2 as the best number of clusters
* 15 proposed 3 as the best number of clusters
* 2 proposed 5 as the best number of clusters
***** Conclusion *****
* According to the majority rule, the best number of clusters is 3
*******************************************************************
程序的建议是3个聚类。我们来进一步验证:
# create a dataframe of the optimal number of clusters
nbclust_plot <- data.frame(clusters = nbclust_out$Best.nc[1, ])
# select only indices which select between 2 and 5 clusters
nbclust_plot <- subset(nbclust_plot, clusters >= 2 & clusters <= 5)
# create plot
ggplot(nbclust_plot) +
aes(x = clusters) +
geom_histogram(bins = 30L, fill = "#0c4c8a") +
labs(x = "Number of clusters", y = "Frequency among all indices", title = "Optimal number of clusters") +
theme_minimal()
通常还可以使用 fviz_cluster() 功能,主成分分析是用来表示二维平面中的变量。根据平均轮廓法的定义,我们将数据分为两个组。
library(factoextra)
km_res <- kmeans(Eurojobs, centers = 2, nstart = 20)
fviz_cluster(km_res, Eurojobs, ellipse.type = "norm")
我将对下图所示的点用k-means算法,k=2,以i=5和i=6为初始中心。计算刚刚找到的分区的质量,然后用R来验证答案。
假设变量具有相同的单位,因此不需要缩放数据。
第一步:以下是6个点的坐标:
最初的中心是:第一个点: 5 (9 , 7); 第二个点: 6 (6 , 8)
第二步:用勾股定理逐点计算距离矩阵。a点和b点之间的距离是通过以下公式得出的:
( x a − x b ) 2 + ( y a − y b ) 2 \sqrt{(x_a - x_b)^2 + (y_a - y_b)^2} (xa−xb)2+(ya−yb)2
由此可见,我们可以得到以下的距离矩阵(round(dist(X), 2)):
## 1 2 3 4 5
## 2 3.61
## 3 5.10 2.24
## 4 7.28 5.66 3.61
## 5 4.47 5.39 7.62 10.82
## 6 5.10 3.61 5.66 9.22 3.16
第三步。根据第二步计算的距离矩阵,我们可以将每个点放在最接近的组中,并计算出中心的坐标。
我们首先将每个点放在最接近的组中:
另外,计算每个点与点5和点6之间的距离就足够了。例如,当我们将每个点与初始中心(点5和6)进行比较时,不需要计算点1和点2之间的距离。
然后,我们通过取坐标x和y的平均值来计算两组中心的坐标:
第4步。我们通过检查每个点是否在最近的簇(Cluster)中来确保分配是最优的。由于勾股定理,一个点和一个簇中心之间的距离再次被计算出来。因此,我们有:
未完待续