将数据聚类成组(第三部分)

149 阅读14分钟

如果你错过了,请查看第一第二部分。

DBSCAN:一种根据空间密度对数据进行分组的聚类算法

DBSCAN是一个缩写,代表基于密度的空间聚类的应用与噪声。对于本质上非常简单的技术来说,这是一个长得可笑的名字:

  1. 从一个data 列表中随机选择一个point 坐标。
  2. 获得该epsilon 距离内的所有邻居point
  3. 如果发现的邻居少于min_points ,则使用不同的随机点重复步骤1。否则,将point 和它的邻居归为一个集群。
  4. 在所有新发现的邻居中迭代地重复步骤2和3。所有相邻的密集点都被合并到集群中。迭代在集群停止扩展后终止。
  5. 在提取了整个聚类之后,在所有密度尚未被分析的数据点上重复步骤1-4。

DBSCAN程序可以用不到20行的代码进行编程。然而,任何基本的实现都会在我们的rocks 列表上运行得很慢。编程一个快速的实现需要一些非常细微的优化,以提高邻居的遍历速度,这已经超出了本书的范围。幸运的是,我们没有必要从头开始重建这个算法:scikit-learn提供了一个快速的DBSCAN 类,我们可以从sklearn.cluster 。让我们通过使用epsmin_samples 的参数来指定epsilonmin_points ,从而导入并初始化这个类。然后我们利用DBSCAN ,对我们的三个环进行聚类(图13)。

清单21.使用DBSCAN 对环进行聚类

 from sklearn.cluster import DBSCAN
 cluster_model = DBSCAN(eps=epsilon, min_samples=min_points) (1)
 rock_clusters = cluster_model.fit_predict(rocks) (2)
 colors = [['g', 'y', 'k'][cluster] for cluster in rock_clusters]
 plt.scatter(x_coordinates, y_coordinates, color=colors)
 plt.show()

创建一个cluster_model对象来进行密度聚类。使用eps参数传入一个0.1的epsilon值。使用 min_samples 参数,输入 min_points 的值 10。

根据密度对岩环进行聚类,并返回每个岩环的指定聚类


图13.DBSCAN聚类法准确地识别了三个不同的岩环。


DBSCAN成功地识别了这三个岩环。该算法在K-means失败的地方获得了成功。

比较DBSCAN和K-means

DBSCAN是一种对由弯曲和密集形状组成的数据进行聚类的优势算法。另外,与K-means不同,该算法在执行前不需要对聚类数量进行近似计算。此外,DBSCAN可以过滤位于空间稀疏区域的随机离群值。例如,如果我们添加了一个位于环形边界之外的离群点,DBSCAN会给它分配一个-1的聚类ID,这个负值表示这个离群点不能与数据集的其他部分聚类。

与K-means不同,拟合的DBSCAN模型不能被重新应用于全新的数据。相反,我们需要结合新旧数据,从头开始执行聚类。这是因为计算出的K-means中心可以很容易地与其他数据点进行比较。然而,额外的数据点可能会影响之前看到的数据的密度分布,这迫使DBSCAN重新计算所有的聚类。

清单22.使用DBSCAN寻找离群点

 noisy_data = rocks + [[1000, -1000]]
 clusters = DBSCAN(eps=epsilon,
                   min_samples=min_points).fit_predict(noisy_data)
 assert clusters[-1] == -1

DBSCAN技术的另一个优点是它不依赖于平均值。同时,K-means算法要求我们计算出分组点的平均坐标。正如我们在第5节所讨论的,这些平均坐标使到中心的平方距离之和最小。只有当平方距离是欧几里得的时候,最小化属性才会成立。因此,如果我们的坐标不是欧几里得的,那么平均数就没有什么用,K-means算法就不应该被应用。然而,欧几里得距离并不是衡量点之间分离度的唯一指标--存在无限的指标来定义距离。我们将在随后的小节中探讨其中的几个指标。在这个过程中,我们将学习如何将这些度量标准整合到我们的DBSCAN聚类输出中。

基于非欧几里得距离的聚类

假设我们正在访问曼哈顿,希望知道从帝国大厦到哥伦布广场的步行距离。帝国大厦位于第34街和第五大道的交叉口。同时,哥伦布圆环位于第57街和第八大道的交叉口。曼哈顿的街道和大道总是相互垂直的。这让我们可以把曼哈顿表现为一个二维坐标系,街道位于X轴上,大道位于Y轴上。在这种表示方式下,帝国大厦位于坐标(34,5),哥伦布广场位于坐标(57,8)。我们可以很容易地计算出这两个坐标点之间的直线欧几里得距离。然而,这个最终的长度将是无法通行的,因为高耸的钢铁建筑占据了每个城市街区所勾勒的区域。一个更正确的解决方案仅限于穿过构成城市网格的垂直人行道的路径。这样的路线要求我们在第五大道和第三大道之间走三个街区,然后在第34街和第57街之间走23个街区,总距离为26个街区。曼哈顿的平均街区长度为0.17英里,因此我们可以估计步行距离为4.42英里。让我们用一个广义的manhattan_distance 函数来直接计算这个步行距离。

清单 23.计算曼哈顿距离

 def manhattan_distance(point_a, point_b):
     num_blocks = np.sum(np.absolute(point_a - point_b))
     return 0.17 * num_blocks
  
 x = np.array([34, 5])
 y = np.array([57, 8])
 distance = manhattan_distance(x, y) (1)
  
 print(f"Manhattan distance is {distance} miles")

我们也可以通过从 scipy.spatial.distance 中导入 cityblock,然后运行 .17 * cityblock(x, y) 来生成这个输出。

 Manhattan distance is 4.42 miles

现在,假设我们希望对两个以上的曼哈顿地点进行聚类。我们将假设每个聚类的点都在其他三个聚类点的一英里步行范围内。这个假设让我们使用scikit-learn的DBSCAN 类来应用DBSCAN聚类。在DBSCAN的初始化过程中,我们把eps 设为1,把min_samples 设为3。此外,我们将metric= manhattan_distance 传递给初始化方法。metric 参数将欧氏距离换成了我们自定义的距离度量,因此聚类距离正确反映了城市中基于网格的约束。下面的代码对曼哈顿坐标进行了聚类,并将它们和它们的聚类名称一起绘制在网格上(图14)。

清单24.使用曼哈顿距离进行聚类

 points = [[35, 5], [33, 6], [37, 4], [40, 7], [45, 5]]
 clusters = DBSCAN(eps=1, min_samples=3,
                   metric=manhattan_distance).fit_predict(points) 

manhattan_distance 函数通过度量参数传入 DBSCAN。

离群值用x形标记绘制。

网格法显示矩形网格,我们在上面计算曼哈顿距离。

 Point at index 0 is in cluster 0
 Point at index 1 is in cluster 0
 Point at index 2 is in cluster 0
 Point at index 3 is an outlier
 Point at index 4 is an outlier

图14.矩形网格中的五个点已经用曼哈顿距离进行了聚类。网格左下角的三个点属于一个集群。剩下的两个点是离群点,用x标记。


前三个位置属于一个集群,其余的点是离群值。我们能不能用K-means算法检测出这个聚类?也许可以。毕竟,我们的曼哈顿区块坐标可以被平均化,使它们与K-means的实现相兼容。如果我们把曼哈顿距离换成一个不同的度量,而平均坐标不那么容易得到,会怎么样呢?让我们定义一个具有以下属性的非线性距离度量:如果两点的所有元素都是负数,则相距0个单位;如果两点的所有元素都是非负数,则相距2个单位;否则相距10个单位。鉴于这种荒谬的距离度量,我们能计算出任何两个任意点的平均值吗?我们不能,所以K-means不能被应用。该算法的一个弱点是,它依赖于平均距离的存在。与K-means不同,DBSCAN算法并不要求我们的距离函数是线性可分的。因此,我们可以使用我们可笑的距离度量轻松地运行DBSCAN聚类。

清单25:使用一个可笑的距离度量进行聚类

 def ridiculous_measure(point_a, point_b):
     is_negative_a = np.array(point_a) < 0 

返回一个布尔数组,如果 point_a[i] < 0,则 is_negative_a[i] 为 True

point_a 和 point_b 的所有元素都是负的。

负数元素存在,但不是所有元素都是负数。

所有元素都是非负的。

 [-1, -1] falls in cluster 0
 [-10, -10] falls in cluster 0
 [-1000, -13435] falls in cluster 0
 [3, 5] is an outlier
 [5, -7] is an outlier

用我们的ridiculous_measure 度量标准运行DBSCAN,会导致负数坐标被聚类为一个单一的组。所有其他的坐标都被当作离群值处理。这些结果在概念上并不实用,但在度量选择方面的灵活性却非常值得赞赏。我们在公制的选择上并没有受到限制!我们可以设定一个新的公制。例如,我们可以设置公制来计算基于地球曲率的遍历距离。这样的度量对于地理区域的聚类特别有用。

DBSCAN聚类方法

  • dbscan_model = DBSCAN(eps=epsilon, min_samples=min_points)- 创建一个DBSCAN模型,按密度聚类。一个密集的点被定义为在距离epsilon ,至少有min_points 个邻居。这些邻居被认为是与该点属于同一个聚类。
  • clusters = dbscan_model.fit_predict(data)- 使用一个初始化的DBSCAN 对象对输入的数据执行DBSCAN。clusters 数组包含集群ID。data[i] 的聚类ID等于clusters[i] 。未聚类的离群点被分配一个ID为-1。
  • clusters = DBSCAN(eps=epsilon, min_samples=min_points).fit_predict(data)- 在一行代码中执行DBSCAN,并返回产生的聚类。
  • dbscan_model = DBSCAN(eps=epsilon, min_samples=min_points, metric=metric_function)- 创建一个DBSCAN模型,其中距离指标由一个自定义指标函数定义。metric_function 距离度量不需要是欧几里得的。

DBSCAN确实有某些缺点。该算法的目的是检测具有类似点密度分布的集群。然而,现实世界的数据在密度上是不同的。例如,曼哈顿的比萨店比加州橙县的比萨店分布得更密集。因此,我们在选择密度参数时可能会遇到困难,无法让我们对这两个地方的商店进行分组。这突出了该算法的另一个限制。DBSCAN要求epsmin_samples 参数有意义的值。特别是,改变eps 的输入将大大影响聚类的质量。不幸的是,没有一个可靠的程序来估计适当的eps 。虽然文献中偶尔会提到某些启发式方法,但它们的好处是微乎其微的。大多数时候,我们必须依靠我们对问题的直觉层面的理解来给两个DBSCAN参数分配实际的输入。例如,如果我们要对一组地理位置进行聚类,我们的epsmin_samples 值将取决于这些地点是分布在整个地球上还是被限制在一个地理区域内。在每种情况下,我们对密度和距离的理解都会有所不同。一般来说,如果我们是对分布在地球上的随机城市进行聚类,我们可以将min_sampleseps 参数分别设置为等于三个城市和250英里。这就假设每个聚类的城市与其他至少三个聚类的城市相距250英里以内。对于一个更加区域性的位置分布,需要一个更低的eps 值。

使用Pandas分析聚类

到目前为止,我们一直将数据输入和聚类输出分开。例如,在我们的岩环分析中,输入数据在rocks 列表中,聚类输出在rock_clusters 阵列中。追踪坐标和聚类都需要我们在输入列表和输出数组之间映射索引。因此,如果我们希望提取聚类0中的所有岩石,我们必须获得rocks[i] 的所有实例,其中rock_clusters[i] == 0 。这种索引分析是曲折的。我们可以通过将坐标和集群合并到一个Pandas表中来更直观地分析集群的岩石。

下面的代码创建了一个有三列的Pandas表。X,Y, 和Cluster 。表中的每一行都是位于rocks[i] 的岩石的x坐标、y坐标和集群。

清单26.在表格中存储簇状坐标

 import pandas as pd
 x_coordinates, y_coordinates = np.array(rocks).T
 df = pd.DataFrame({'X': x_coordinates, 'Y': y_coordinates,
                    'Cluster': rock_clusters})

我们的Pandas表让我们可以很容易地访问任何集群中的岩石。让我们使用第8节中描述的技术来绘制属于集群0的岩石(图15)。

清单27.使用Pandas绘制单个簇的图

 df_cluster = df[df.Cluster == 0] 

只选择群集列等于0的那些行

绘制所选行的X和Y列。注意,我们也通过运行 df_cluster.plot.scatter(x='X', y='Y') 来执行散点图。


图15.属于聚类0的岩石


Pandas允许我们获得一个包含任何单一集群元素的表格。另外,我们可能想获得多个表,其中每个表都映射到一个簇的ID。在Pandas中,这可以通过调用df.groupby('Cluster') 来实现。groupby 方法将创建三个表:每个集群一个。它将返回群集ID和表之间的映射的可迭代的数据。让我们使用groupby 方法来迭代我们的三个聚类。随后我们将绘制集群1和集群2中的岩石,但不绘制集群0中的岩石(图16)。

调用df.groupby('Cluster') ,返回的不仅仅是一个可迭代的对象:它返回一个DataFrameGroupBy 对象,该对象提供了额外的集群过滤和分析方法。

清单28.使用Pandas对聚类进行迭代

 for cluster_id, df_cluster in df.groupby('Cluster'): 

由df.groupby('Cluster')返回的可迭代的每个元素都是一个元组。元组的第一个元素是由df.Cluster获得的集群ID。第二个元素是一个由所有 df.Cluster 等于集群 ID 的行组成的表。


图16.属于聚类1和2的岩石


Pandasgroupby 方法可以让我们反复检查不同的聚类。这在我们的案例研究3的分析中可能被证明是有用的。

总结

  • K-means算法通过搜索K 中心点对输入的数据进行聚类。这些中心点代表了所发现的数据组的平均坐标。K-means是通过选择K个随机中心点来初始化的。然后,每个数据点根据其最近的中心点进行聚类,中心点被反复地重新计算,直到它们收敛在稳定的位置。
  • K-means保证会收敛到一个解决方案。然而,该解决方案可能不是最优的。
  • K-means需要欧几里得距离来区分各点。该算法不打算对非欧几里得坐标进行聚类。
  • 在执行K-means聚类后,我们可以计算出结果的惯性。惯性等于每个数据点与其最近的中心之间的平方距离之和。
  • K值的范围内绘制惯性图,会生成一个肘部图。肘形图中的肘部分量应该向下指向一个合理的K值。利用肘形图,我们可以启发式地选择一个有意义的K值输入给K-means。
  • DBSCAN算法基于密度对数据进行聚类。密度是用epsilonmin_points 参数来定义的。如果一个点位于min_point 邻居的epsilon 距离之内,那么这个点就处于空间的密集区。密集区域内的每一个点的邻居也会在该空间内聚类。DBSCAN迭代地扩展空间密集区的边界,直到检测到一个完整的集群。
  • 在非密集区域的点不会被DBSCAN算法所聚类。它们被当作离群值处理。
  • DBSCAN是一种对由弯曲和密集形状组成的数据进行聚类的有利算法。
  • DBSCAN可以使用任意的、非欧几里得的距离进行聚类。
  • 在选择适当的epsilonmin_points 参数方面,没有可靠的启发式方法。然而,如果我们希望对全球城市进行聚类,我们可以将这两个参数分别设置为250英里和三个城市。
  • 将聚类的数据存储在Pandas表中,可以让我们直观地用groupby 方法对聚类进行迭代。