简介
K-Means聚类是最广泛使用的无监督机器学习算法之一,它根据数据实例之间的相似性来形成数据的聚类:
在本指南中,我们将首先看一个简单的例子,了解K-Means算法是如何工作的,然后用Scikit-Learn实现它。然后,我们将讨论如何确定K-Means中的聚类数量(Ks),还包括距离度量、方差和K-Means的优点和缺点。
动机
想象一下下面的情况。有一天,当在附近散步时,你注意到有10家便利店,并开始想知道哪些商店是相似的--在距离上更接近。在寻找回答这个问题的方法时,你遇到了一个有趣的方法,即根据商店在地图上的坐标将它们分成几组。
例如,如果一家商店位于西5公里和北3公里处--你会给它指定(5, 3) ,并在图表中表示出来。让我们绘制这第一个点,以直观地了解发生了什么:
import matplotlib.pyplot as plt
plt.title("Store With Coordinates (5, 3)")
plt.scatter(x=5, y=3)

这只是第一个点,所以我们可以得到一个关于如何表示一个商店的想法。假设我们已经有10个坐标来收集10个商店。在将它们组织在一个numpy 数组中后,我们也可以绘制它们的位置。
import numpy as np
points = np.array([[5, 3], [10, 15], [15, 12], [24, 10], [30, 45], [85, 70], [71, 80], [60, 78], [55, 52],[80, 91]])
xs = points[:,0] # Selects all xs from the array
ys = points[:,1] # Selects all ys from the array
plt.title("10 Stores Coordinates")
plt.scatter(x=xs, y=ys)

如何手动实现K-Means算法
现在我们可以在图上看一下这10家商店,主要问题是要找到是否有办法根据距离的远近将它们分成不同的组?仅仅通过快速浏览该图,我们可能会注意到两组商店--一组是左下角的低点,另一组是右上角的点。也许,我们甚至可以把中间的那两个点区分为一个单独的组--因此创造了三个不同的组。
在这一节中,我们将详细介绍手动聚类的过程--将它们划分为给定数量的组。这样一来,我们基本上就会仔细地复习K-Means聚类算法的所有步骤。在本节结束时,你将对K-Means聚类过程中的所有步骤有一个直观和实际的了解。之后,我们将把它委托给Scikit-Learn。
确定是否有两组或三组点的最好方法是什么?一个简单的方法是简单地选择一个组的数量--例如两个--然后尝试根据这个选择对点进行分组。

比方说,我们已经决定有两组我们的商店(点)。现在,我们需要找到一种方法来了解哪些点属于哪个组。这可以通过选择一个点代表第1组和一个点代表第2组来实现。这些点将在测量所有其他点到每个组的距离时被用作参考。
这样一来,比如说点(5, 3) 最终属于第1组,而点(79, 60) 属于第2组。当试图将一个新的点(6, 3) 分配到组中时,我们需要测量它与这两个点的距离。在点(6, 3) 更接近于(5, 3) 的情况下,因此它属于该点所代表的组--组1。这样,我们可以很容易地将所有的点归入相应的组。
在这个例子中,除了确定组*(群*)的数量,我们还选择了一些点作为每个组的新点的距离参考。
这就是了解我们的商店之间的相似性的一般想法。让我们把它付诸实践 - 我们可以先随机选择两个参考点。第一组的参考点将是(5, 3) ,第二组的参考点将是(10, 15) 。我们可以通过[0] 和[1] 索引选择我们的numpy 数组中的两个点,并将它们存储在g1 (第1组)和g2 (第2组)变量中。
g1 = points[0]
g2 = points[1]
这样做之后,我们需要计算所有其他点到这些参考点的距离。这就提出了一个重要问题--如何测量这个距离。我们基本上可以使用任何距离测量,但是,为了本指南的目的,让我们使用欧氏距离_。
知道欧氏距离测量法是基于毕达哥拉斯定理的,可能会有帮助。
c^2 = a^2 + b^2
当适应于平面内的点时--(a1, b1) 和(a2, b2) ,前面的公式变成了。
c^2 = (a2-a1)^2 + (b2-b1)^2
距离将是c 的平方根,所以我们也可以把公式写成。
euclidean_{dist} = \sqrt[2][(a2 - a1)^2 + (b2 - b1) ^2)]
注意:你也可以将欧氏距离公式泛化为多维的点。例如,在三维空间中,点有三个坐标--我们的公式以如下方式反映出来:
euclidean_{dist} = \sqrt[2][(a2 - a1)^2 + (b2 - b1) ^2 + (c2 - c1) ^2)]
无论我们操作的空间有多少维,都遵循同样的原则。
到目前为止,我们已经选好了代表组的点,并且知道如何计算距离。现在,让我们把距离和组放在一起,把我们收集的每一个存储点分配给一个组。
为了更直观地了解这一点,我们将声明三个列表。第一个是存储第一组的点 -points_in_g1 。第二个存储第二组的点 -points_in_g2 ,最后一个 -group ,将这些点标记为1 (属于第一组)或2 (属于第二组):
points_in_g1 = []
points_in_g2 = []
group = []
我们现在可以遍历我们的点,并计算它们与我们每一组参考点之间的欧几里得距离。每个点将更接近两个组中的一个--根据哪个组最接近,我们将把每个点分配到相应的列表中,同时也将1 或2 添加到group 列表中:
for p in points:
x1, y1 = p[0], p[1]
euclidean_distance_g1 = np.sqrt((g1[0] - x1)**2 + (g1[1] - y1)**2)
euclidean_distance_g2 = np.sqrt((g2[0] - x1)**2 + (g2[1] - y1)**2)
if euclidean_distance_g1 < euclidean_distance_g2:
points_in_g1.append(p)
group.append('1')
else:
points_in_g2.append(p)
group.append('2')
让我们看一下这个迭代的结果,看看发生了什么:
print(f'points_in_g1:{points_in_g1}\n \
\npoints_in_g2:{points_in_g2}\n \
\ngroup:{group}')
其中的结果是:
points_in_g1:[array([5, 3])]
points_in_g2:[array([10, 15]), array([15, 12]),
array([24, 10]), array([30, 45]),
array([85, 70]), array([71, 80]),
array([60, 78]), array([55, 52]),
array([80, 91])]
group:[1, 2, 2, 2, 2, 2, 2, 2, 2, 2]
我们也可以用Seaborn的scatterplot() ,把group 作为hue 的参数,绘制出聚类结果,根据分配的组别用不同的颜色表示:
import seaborn as sns
sns.scatterplot(x=points[:, 0], y=points[:, 1], hue=group)

可以清楚地看到,只有我们的第一个点被分配到了第一组,而其他的点都被分配到了第二组。 这个结果与我们一开始设想的不同。考虑到我们的结果和我们最初的预期之间的差异--有什么办法可以改变这种情况吗?看来是有的!
一种方法是重复这个过程,选择不同的点作为各组的参考。这将改变我们的结果,希望能与我们一开始的设想更加一致。这第二次,我们可以不象以前那样随机选择,而是通过获得所有已经分组的点的平均值来选择它们。这样一来,那些新的点就可以被定位在相应组的中间。
例如,如果第二组只有点(10, 15),(30, 45) 。新的中心点将是(10 + 30)/2 和(15+45)/2 - 这等于(20, 30) 。
由于我们把结果放在列表中,我们可以先把它们转换成numpy 数组,选择它们的x,y,然后得到平均值:
g1_center = [np.array(points_in_g1)[:, 0].mean(), np.array(points_in_g1)[:, 1].mean()]
g2_center = [np.array(points_in_g2)[:, 0].mean(), np.array(points_in_g2)[:, 1].mean()]
g1_center, g2_center
建议:尽量使用numpy 和NumPy数组。它们经过优化,性能更好,并简化了许多线性代数操作。每当你试图解决一些线性代数问题时,你一定要看一下numpy 文档,检查是否有任何numpy 方法来解决你的问题。机会是有的!
为了帮助我们用新的中心点重复这个过程,让我们把以前的代码转化为一个函数,执行它,看看点的分组方式是否有变化。
def assigns_points_to_two_groups(g1_center, g2_center):
points_in_g1 = []
points_in_g2 = []
group = []
for p in points:
x1, y1 = p[0], p[1]
euclidean_distance_g1 = np.sqrt((g1_center[0] - x1)**2 + (g1_center[1] - y1)**2)
euclidean_distance_g2 = np.sqrt((g2_center[0] - x1)**2 + (g2_center[1] - y1)**2)
if euclidean_distance_g1 < euclidean_distance_g2:
points_in_g1.append(p)
group.append(1)
else:
points_in_g2.append(p)
group.append(2)
return points_in_g1, points_in_g2, group
注意:如果你注意到你一直在重复相同的代码,你应该把这些代码包装成一个单独的函数。将代码组织成函数被认为是一种最佳做法,特别是因为它们便于测试。与没有任何函数的完整代码相比,测试孤立的一段代码更容易。
让我们调用该函数并将其结果存储在points_in_g1,points_in_g2, 和group 变量中:
points_in_g1, points_in_g2, group = assigns_points_to_two_groups(g1_center, g2_center)
points_in_g1, points_in_g2, group
同时用彩色的点绘制散点图,以直观地看到组的划分:
sns.scatterplot(x=points[:, 0], y=points[:, 1], hue=group)

看起来我们的点的聚类情况越来越好。但仍然有两个点在图的中间,当考虑到它们与两组的接近程度时,可以被分配到任何一组。到目前为止,我们开发的算法将这两个点分配到第二组。
这意味着我们也许可以再重复一次这个过程,取X和Y的平均值,创建两个新的中心点 ***(centerroids)***到我们的组中,并根据距离重新分配它们。
让我们也创建一个函数来更新中心点。现在整个过程可以简化为对该函数的多次调用:
def updates_centroids(points_in_g1, points_in_g2):
g1_center = np.array(points_in_g1)[:, 0].mean(), np.array(points_in_g1)[:, 1].mean()
g2_center = np.array(points_in_g2)[:, 0].mean(), np.array(points_in_g2)[:, 1].mean()
return g1_center, g2_center
g1_center, g2_center = updates_centroids(points_in_g1, points_in_g2)
points_in_g1, points_in_g2, group = assigns_points_to_two_groups(g1_center, g2_center)
sns.scatterplot(x=points[:, 0], y=points[:, 1], hue=group)

请注意,在第三次迭代之后,每一个点现在都属于不同的群组。看起来结果越来越好--让我们再做一次。现在进入我们方法的第四次迭代:
g1_center, g2_center = updates_centroids(points_in_g1, points_in_g2)
points_in_g1, points_in_g2, group = assigns_points_to_two_groups(g1_center, g2_center)
sns.scatterplot(x=points[:, 0], y=points[:, 1], hue=group)

这第四次我们得到了与前一次相同的结果。因此,看来我们的点不会再改变组别了,我们的结果已经达到了某种稳定性--它已经到了一个不可改变的状态,或者说已经收敛。除此之外,我们的结果与我们设想的2组完全相同。我们也可以看看这个达成的划分是否有意义。
让我们快速回顾一下到目前为止我们所做的事情。我们已经将我们的10家商店在地理上分为两部分--那些在西南下层地区的商店和其他在东北地区的商店。除了我们已有的数据外,收集更多的数据--收入,每天的顾客数量,以及更多的数据,可能会很有趣。这样,我们可以进行更丰富的分析,并可能产生更有趣的结果。
当一个已经建立的品牌想要挑选一个地区开一家新店时,可以进行这样的聚类研究。在这种情况下,除了位置之外,还有很多变量需要考虑。
这一切与K-Means算法有什么关系?
在跟随这些步骤时,你可能会想知道它们与K-Means算法有什么关系。到目前为止,我们所进行的过程就是K-Means算法。简而言之,我们已经确定了组/群的数量,随机选择初始点,并在每次迭代中更新中心点,直到集群收敛。我们基本上是通过手工进行整个算法--仔细进行每一步。
K-Means中的K来自于在开始迭代过程之前需要设置的群组数量。在我们的案例中,K=2。这一特点有时被看作是 否定考虑到还有其他的聚类方法,比如层次聚类,不需要事先设定固定的聚类数量。
由于其对平均值的使用,K-means也变得对异常值和极端值很敏感--它们增强了变异性,使我们的中心点更难发挥其作用。因此,在使用K-means算法进行聚类之前,要有意识地进行极端值和离群值分析。
另外,注意我们的点是以直线部分分割的,在创建聚类时没有曲线。这也可能是K-Means算法的一个缺点。
注意:当你需要它更灵活地适应椭圆和其他形状时,可以尝试使用广义的K-均值高斯混合模型。这个模型可以适应椭圆的分割集群。
K-Means也有很多 优点!它在大数据集上表现良好,如果你使用某些类型的分层聚类算法,大数据集会变得难以处理。它还能保证收敛性,并能很容易地泛化和适应。除此之外,它可能是最常用的聚类算法。
现在我们已经复习了K-Means算法中执行的所有步骤,并了解了它的所有优点和缺点,最后我们可以使用Scikit-Learn库实现K-Means。
如何使用Scikit-Learn实现K-Means算法
为了仔细检查我们的结果,让我们再做一次这个过程,但现在使用3行代码,sklearn :
from sklearn.cluster import KMeans
# The random_state needs to be the same number to get reproducible results
kmeans = KMeans(n_clusters=2, random_state=42)
kmeans.fit(points)
kmeans.labels_
在这里,标签与我们之前的组别相同。让我们快速绘制结果:
sns.scatterplot(x = points[:,0], y = points[:,1], hue=kmeans.labels_)

结果图与上一节中的图相同。
注意:只看我们如何使用Scikit-Learn执行K-Means算法,可能会给你一个印象,即这是一个没有问题的算法,你不需要太担心。仅仅3行代码就完成了我们在上一节中讨论的K-Means算法的所有步骤,而且是一步一步地完成的。但是。 魔鬼在细节中在这种情况下!如果你不了解所有的步骤和算法的局限性,你很可能会面临K-Means算法给你带来你意想不到的结果的情况。
使用Scikit-Learn,你也可以通过设置init='k-means++' 参数来初始化K-Means,以便更快地收敛。从广义上讲,K-Means++仍然按照均匀分布随机选择k个初始集群中心。然后,从剩余的数据点中选择每一个后续的聚类中心,不是仅仅通过计算距离度量--而是通过使用概率。使用概率可以加快算法的速度,而且在处理非常大的数据集时很有帮助。
肘部方法--选择最佳组数
到目前为止,一切都很好!我们已经根据点和中心点之间的欧几里得距离对10家商店进行了分组。但是,图中间那两个有点难聚类的点怎么办?它们不能也形成一个单独的组吗?我们选择K=2组实际上是个错误吗?也许我们实际上有K=3组?我们甚至可以有三个以上的组而不被察觉。
这里所问的问题是 如何确定K-Means中的组数(K)?.为了回答这个问题,我们需要了解对于不同的K值是否会有一个 "更好的 "聚类。
天真的方法是通过对不同K值的点进行聚类,所以,对于K=2,K=3,K=4,以此类推:
for number_of_clusters in range(1, 11):
kmeans = KMeans(n_clusters = number_of_clusters, random_state = 42)
kmeans.fit(points)
但是,仅仅对不同的K进行聚类并不足以了解我们是否选择了理想的K值。我们需要一种方法来评估我们所选择的每个K的聚类质量。
手动计算集群内平方和(WCSS)。
这里是一个理想的地方,可以引入一个衡量我们的聚类点彼此之间的接近程度。它基本上描述了我们在一个集群内有多少差异。这个度量被称为集群内平方和,或 WCSS的简称。WCSS越小,说明我们的点越接近,因此我们有一个更完善的集群。WCSS公式可以用于任何数量的聚类。
WCSS = sum(Pi_1 - Centroid_1)^2 + \cdots + \sum(Pi_n - Centroid_n) ^2
注意:在本指南中,我们使用欧氏距离来获得中心点,但也可以使用其他距离测量方法,如曼哈顿。
现在我们可以假设我们已经选择了两个集群,并尝试实现WCSS,以更好地了解什么是WCSS以及如何使用它。正如公式所说,我们需要将所有集群点和中心点之间的平方差值相加。因此,如果我们第一组的第一个点是(5, 3) ,第一组的最后一个中心点(收敛后)是(16.8, 17.0) ,那么WCSS将是。
WCSS = sum((5,3) - (16.8, 17.0))^2
WCSS = sum((5-16.8) + (3-17.0))^2
WCSS = sum((-11.8) + (-14.0))^2
WCSS = sum((-25.8))^2
WCSS = 335.24
这个例子说明了我们是如何计算集群中一个点的WCSS的。但是集群通常包含不止一个点,我们在计算WCSS时需要考虑到所有的点。我们将通过定义一个函数来做到这一点,该函数接收一个点簇和中心点,并返回平方之和:
def sum_of_squares(cluster, centroid):
squares = []
for p in cluster:
squares.append((p - centroid)**2)
ss = np.array(squares).sum()
return ss
现在我们可以得到每个簇的平方之和:
g1 = sum_of_squares(points_in_g1, g1_center)
g2 = sum_of_squares(points_in_g2, g2_center)
然后将这些结果相加,得到总的WCSS:
g1 + g2
这样的结果是:
2964.3999999999996
因此,在我们的例子中,当K等于2时,总的WCSS是2964.39。现在,我们可以切换K,并计算出所有K的WCSS。这样,我们就可以深入了解我们应该选择什么样的K来使我们的聚类表现最好。
使用Scikit-Learn计算WCSS
幸运的是,我们不需要手动计算每个K的WCS。在对给定的簇进行K-Means聚类后,我们可以通过使用inertia_ 属性获得其WCS。现在,我们可以回到我们的K-Meansfor 循环,用它来转换集群的数量,并列出相应的WCSS值:
wcss = []
for number_of_clusters in range(1, 11):
kmeans = KMeans(n_clusters = number_of_clusters, random_state = 42)
kmeans.fit(points)
wcss.append(kmeans.inertia_)
wcss
请注意,列表中的第二个值与我们之前计算的K=2的值完全相同:
[18272.9, # For k=1
2964.3999999999996, # For k=2
1198.75, # For k=3
861.75,
570.5,
337.5,
175.83333333333334,
79.5,
17.0,
0.0]
为了直观地看到这些结果,让我们把K和WCSS值一起绘制出来:
ks = [1, 2, 3, 4, 5 , 6 , 7 , 8, 9, 10]
plt.plot(ks, wcss)

当x = 2 ,线上有一个中断,是一个低点,而当x = 3 ,则是一个更低的中断。请注意,这让我们想起了一个肘部的形状。通过将K和WCS一起绘制出来,我们是用肘部方法来选择K的数量。而选择的K正好是最低的肘点,所以,在我们的例子中,它应该是3 ,而不是2 。
ks = [1, 2, 3, 4, 5 , 6 , 7 , 8, 9, 10]
plt.plot(ks, wcss);
plt.axvline(3, linestyle='--', color='r')

我们可以再次运行K-Means聚类算法,看看我们的数据在三个聚类中会是什么样子。
kmeans = KMeans(n_clusters=3, random_state=42)
kmeans.fit(points)
sns.scatterplot(x = points[:,0], y = points[:,1], hue=kmeans.labels_)
我们已经对两个聚类感到满意了,但是根据肘部方法,三个聚类会更适合我们的数据。在这种情况下,我们将有三种商店而不是两种。在使用肘部方法之前,我们考虑了西南和东北的商店集群,现在我们在中心也有商店。也许这可能是开设另一家商店的好地点,因为它在附近的竞争会比较少。
其他集群质量衡量标准
在评估集群质量时,还有其他一些措施可以使用:
- 剪影分数--不仅分析集群内各点之间的距离,也分析集群本身的距离。
- 聚类之间的平方和(BCSS)- 度量的补充,WCSS
- 平方误差之和 (SSE)
- 最大半径--测量一个点到其中心点的最大距离
- 平均半径--从一个点到其中心点的最大距离之和除以聚类的数量。
建议进行实验,了解它们中的每一个,因为根据问题的不同,一些替代方法可能比最广泛使用的指标*(WCSS和Silhouette Score)*更适用。
最后,与许多数据科学算法一样,我们希望减少每个集群内部的差异,并使不同集群之间的差异最大化。因此,我们有更多的定义和可分离的聚类。
在另一个数据集上应用K-Means
让我们在另一个数据集上使用我们学到的东西。这一次,我们将尝试找到类似葡萄酒的群体。
注:你可以在这里下载该数据集。
我们首先导入pandas ,将wine-clustering CSV*(Comma-Separated Values)*文件读成Dataframe 结构。
import pandas as pd
df = pd.read_csv('wine-clustering.csv')
加载后,让我们用head() ,偷看一下前五条记录的数据:
df.head()
这样的结果是:
Alcohol Malic_Acid Ash Ash_Alcanity Magnesium Total_Phenols Flavanoids Nonflavanoid_Phenols Proanthocyanins Color_Intensity Hue OD280 Proline
0 14.23 1.71 2.43 15.6 127 2.80 3.06 0.28 2.29 5.64 1.04 3.92 1065
1 13.20 1.78 2.14 11.2 100 2.65 2.76 0.26 1.28 4.38 1.05 3.40 1050
2 13.16 2.36 2.67 18.6 101 2.80 3.24 0.30 2.81 5.68 1.03 3.17 1185
3 14.37 1.95 2.50 16.8 113 3.85 3.49 0.24 2.18 7.80 0.86 3.45 1480
4 13.24 2.59 2.87 21.0 118 2.80 2.69 0.39 1.82 4.32 1.04 2.93 735
我们有许多关于葡萄酒中存在的物质的测量结果。在这里,我们也不需要对分类列进行转换,因为所有的分类都是数字的。现在,我们来看看用describe() 方法进行的描述性统计。
df.describe().T # T is for transposing the table
描述表:
count mean std min 25% 50% 75% max
Alcohol 178.0 13.000618 0.811827 11.03 12.3625 13.050 13.6775 14.83
Malic_Acid 178.0 2.336348 1.117146 0.74 1.6025 1.865 3.0825 5.80
Ash 178.0 2.366517 0.274344 1.36 2.2100 2.360 2.5575 3.23
Ash_Alcanity 178.0 19.494944 3.339564 10.60 17.2000 19.500 21.5000 30.00
Magnesium 178.0 99.741573 14.282484 70.00 88.0000 98.000 107.0000 162.00
Total_Phenols 178.0 2.295112 0.625851 0.98 1.7425 2.355 2.8000 3.88
Flavanoids 178.0 2.029270 0.998859 0.34 1.2050 2.135 2.8750 5.08
Nonflavanoid_Phenols 178.0 0.361854 0.124453 0.13 0.2700 0.340 0.4375 0.66
Proanthocyanins 178.0 1.590899 0.572359 0.41 1.2500 1.555 1.9500 3.58
Color_Intensity 178.0 5.058090 2.318286 1.28 3.2200 4.690 6.2000 13.00
Hue 178.0 0.957449 0.228572 0.48 0.7825 0.965 1.1200 1.71
OD280 178.0 2.611685 0.709990 1.27 1.9375 2.780 3.1700 4.00
Proline 178.0 746.893258 314.907474 278.00 500.500 673.500 985.0000 1680.00
通过观察该表可以看出,数据中存在一些差异性--对于一些列,如Alchool ,比较多,而对于其他列,如Malic_Acid ,比较少。现在我们可以检查在我们的数据集中是否有任何null ,或NaN 的值。
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 178 entries, 0 to 177
Data columns (total 13 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Alcohol 178 non-null float64
1 Malic_Acid 178 non-null float64
2 Ash 178 non-null float64
3 Ash_Alcanity 178 non-null float64
4 Magnesium 178 non-null int64
5 Total_Phenols 178 non-null float64
6 Flavanoids 178 non-null float64
7 Nonflavanoid_Phenols 178 non-null float64
8 Proanthocyanins 178 non-null float64
9 Color_Intensity 178 non-null float64
10 Hue 178 non-null float64
11 OD280 178 non-null float64
12 Proline 178 non-null int64
dtypes: float64(11), int64(2)
memory usage: 18.2 KB
考虑到数据集中没有空值,所以不需要丢弃或输入数据。我们可以使用Seabornpairplot() 来查看数据分布,并检查数据集是否形成了对列,而这些对列对于聚类来说是有趣的。
sns.pairplot(df)

通过观察配对图,有两列似乎对聚类有希望--Alcohol 和OD280 (这是一种确定葡萄酒中蛋白质浓度的方法)。在结合其中两列的图上,似乎有3个明显的聚类。
还有其他一些列似乎也有关联性。最明显的是Alcohol 和Total_Phenols ,以及Alcohol 和Flavanoids 。它们有很大的线性关系,可以在配对图中观察到。
由于我们的重点是用K-Means聚类,让我们选择一对列,例如Alcohol 和OD280 ,并测试这个数据集的肘部方法。
注意:当使用更多的数据集列时,将需要在3个维度上进行绘图,或者将数据还原为主成分(使用PCA)。这是一种有效的,也是比较常见的方法,只是要确保根据主成分的解释程度来选择主成分,并记住在减少数据维度时,会有一些信息损失--所以图是真实数据的近似值,而不是它的真实情况。
让我们把散点图的这两列设置为它的轴线,来仔细看看我们想要划分的点。
sns.scatterplot(data=df, x='OD280', y='Alcohol')

现在我们可以定义我们的列并使用肘部方法来确定群组的数量。我们还将用kmeans++ 来启动算法,只是为了确保它更快地收敛。
values = df[['OD280', 'Alcohol']]
wcss_wine = []
for i in range(1, 11):
kmeans = KMeans(n_clusters = i, init = 'k-means++', random_state = 42)
kmeans.fit(values)
wcss_wine.append(kmeans.inertia_)
我们已经计算了WCSS,所以我们可以绘制结果。
clusters_wine = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
plt.plot(clusters_wine, wcss_wine)
plt.axvline(3, linestyle='--', color='r')

根据肘部方法,我们在这里应该有3个聚类。最后一步,让我们把我们的点聚成3个集群,并把这些集群用颜色标识出来。
kmeans_wine = KMeans(n_clusters=3, random_state=42)
kmeans_wine.fit(values)
sns.scatterplot(x = values['OD280'], y = values['Alcohol'], hue=kmeans_wine.labels_)

我们可以在图中看到群组0 ,1 ,和2 。根据我们的分析,第0组的葡萄酒蛋白质含量较高,酒精含量较低;第1组的葡萄酒酒精含量较高,蛋白质含量较低;第2组的葡萄酒中既有高蛋白质,又有高酒精。
这是一个非常有趣的数据集,我鼓励你通过归一化和PCA后的数据聚类来进一步分析,也可以通过解释结果和寻找新的联系。
结论
K-Means聚类是一种简单但非常有效的用于数据聚类的无监督机器学习算法。它根据数据点之间的欧几里得距离对数据进行聚类。K-Means聚类算法在文本文件、图像、视频等的分组方面有很多用途。