
你有没有遇到过这种情况给一堆数据做聚类K-Means跑出来永远是一坨圆滚滚的簇遇到像月亮形状的点群就彻底傻眼或者数据集里藏着几个极端的异常点最后被强行拽进了某个簇里导致整体中心偏移。我在刚开始接触机器学习时用K-Means处理一个用户行为数据集就踩了这个坑换了好几种预处理方式都没法把环形的分布切出来。后来查到密度聚类方法DBSCAN才算把问题解决。这篇文章就围绕DBSCAN聚类算法展开把理论、图解、Python代码实现一次说明白适合正在学机器学习的入门者、准备面试的算法岗同学以及实际业务里需要做客户分群或异常检测的工程师参考。DBSCAN的全称是Density-Based Spatial Clustering of Applications with Noise翻译过来就是“具有噪声的基于密度的空间聚类方法”。和K-Means这种基于距离中心的划分方式完全不同DBSCAN是让簇按照样本分布的紧密程度自己“长出来”不需要事先指定簇的数量还能顺手把离群点识别出来。光是这三点就足够让它在很多场景里比K-Means更实用。1. 聚类问题与DBSCAN的设计动机1.1 聚类任务到底在解决什么问题聚类是无监督学习里最基础也最常用的一类任务。所谓无监督就是训练数据没有标签我们不知道每个样本属于哪个类别只知道每个样本有一堆特征。聚类的目标就是根据样本之间的相似性把相似的样本归到同一组不同的样本分开。这个思想用一句话说就是“物以类聚人以群分”。这里的关键在于“相似”怎么定义。最常见的定义是欧氏距离两个点离得越近越相似。但不是所有场景都适合用欧氏距离直接衡量比如经纬度坐标、用户浏览序列、图像特征向量距离含义各不相同。聚类算法做的事情是在给定相似度定义后找出数据中存在的自然分组结构。可能你接触过的第一个聚类算法就是K-Means。它的思路很直观随机初始化K个中心点然后迭代地把每个样本分给离它最近的中心再重新计算每个组内样本的均值作为新中心反复循环直到收敛。这个方法简单、快但背后有一个隐藏假设每个簇大致是凸的、形状接近球形而且所有簇的样本数量不能差太远。一旦数据分布不满足这两个条件K-Means的效果就会打折扣。1.2 K-Means的三大短板簇形状、离群点、K值依赖我在实际项目中总结过K-Means的三大痛点。第一是簇形状受限。K-Means用均值作为中心每个簇在特征空间里被划分成以中心为原点的Voronoi区域边界都是直线最终形成的簇也就是凸多边形。遇到月牙形、环形、S形这种非凸分布K-Means硬要切几刀切出来的结果很离谱。第二是离群点太容易被带偏。K-Means在迭代时需要计算簇内样本的均值一个离群点距离其他样本十万八千里但因为它是某个簇的一员中心就会被拽过去。哪怕你提前做了归一化也无法完全避免这种影响。更麻烦的是K-Means会强制把所有样本都分到某个簇里噪声点没有单独的标记这在做异常检测时很不友好。第三是K值必须提前指定。很多初学者以为K值是经验值其实它本身是最难估计的超参数。用肘部法则或者轮廓系数去选K本质上是在“猜”数据里有多少簇。对于没有先验信息的业务场景这个猜测往往不靠谱而且K-Means对初始化很敏感跑几次可能结果都不一样。这些短板叠加起来催生了对一种更灵活的聚类方法的需求。1.3 DBSCAN的破局思路让密度决定一切DBSCAN的设计理念和K-Means完全不同。它不去假设簇的形状也不要求预先指定簇的数量而是把簇定义为“密度相连的点的最大集合”。什么意思呢在一张二维散点图里如果一个区域的点密度明显高于周围那么这个高密度区域很可能就是一个簇。DBSCAN要做的事情就是找到这些高密度区域并把它们连成一片。这个思路来源于一个很朴素的生活经验一群人聚在一起聊天圈内的人之间距离近圈外的人稀疏。当你站在人群外面观察很容易看出哪一群人是聚在一起的即使人群排成一条长龙你依然能看出这条龙是一个整体。K-Means做不到这一点因为它只会画圆DBSCAN却能通过“密度可达”把长条形的簇完整地串联起来。这种基于密度的聚类方式还有一个天然的好处不需要把所有样本都硬塞进某个簇。样本周围如果太稀疏它就直接被标记为噪声点。于是聚类和异常检测被统一到了一个框架里这在很多业务场景中非常有用。2. DBSCAN核心原理与图解2.1 两个让新手最头疼的参数eps与minPtsDBSCAN有两个关键参数。第一个是eps也叫邻域半径它规定了“距我多远算邻居”。第二个是minPts也叫最小样本数它规定了“一个区域里至少要有多少个点才算得上密集”。这两个参数共同定义了什么是“核心点”。如果一个点自身的eps半径范围内邻居数量大于等于minPts那么它就是核心点。比如minPts设为4某个点周围半径0.5的圆里包含了6个点那这个点就是核心点。可以这样类比想象一只公鸡站在空地上它打鸣时能听到的半径就是eps。如果它打鸣之后周围有minPts只公鸡响应那它就是鸡群里的“头头”核心点。EPS过大整个场地里的公鸡都能听见所有公鸡会被归成一个巨大群体eps过小每只公鸡都孤立谁也听不见谁最终全是噪声。选择eps和minPts其实就是找到合适的“听觉半径”和“响应人数阈值”。参数选择没有万能公式但有一个经典经验先用K-距离图确定eps区间并把minPts设置为特征维度数加1或维度数的两倍。后面在代码部分会实测展示这个方法。2.2 三种点的分类核心点、边界点与噪声点确定核心点之后DBSCAN把样本分成三类。第一类是核心点前面说了它的邻域内样本数不少于minPts。第二类是边界点它本身不是核心点但它落在某个核心点的邻域内。第三类是噪声点它既不是核心点也不落在任何核心点邻域内。用前面的例子继续延伸能带头呼应的公鸡是核心点自己不打鸣但站在核心点身边的小鸡是边界点远处独自溜达听不见任何打鸣声的公鸡就是噪声点。边界点很有意思它属于某个簇但因为自己的邻域不够密不能继续向外扩张簇的边界噪声点则什么都不属于。这三类点的区分非常直观。想象一个二维平面一团密集的点聚在一起边缘有一些稀疏的点。密集区域内部的点基本是核心点边缘上那些稀疏的点中离核心点足够近的成了边界点离所有核心点都远的就是噪声。从视觉效果上看簇就是由一堆核心点加上围绕它们的边界点组成的实体噪声则像背景里的尘埃。2.3 密度直达、密度可达与密度相连要把“密度”变成一种“连接”关系DBSCAN引入了几个概念。密度直达是指核心点A的邻域内包含另一个点B那么从A到B是密度直达的。注意两个点可能互为直达也可能只存在单向关系因为B不一定是核心点。密度可达则要传递一层。如果存在一个点链P1到P2密度直达P2到P3密度直达……直到Pk到Q密度直达那么从P1到Q就是密度可达的。这等于说你可以通过一连串核心点“跳”到目标点。在这个链条里中间经过的点都必须有足够密度允许最后一个点是边界点。密度相连是更宽松的关系。如果存在一个点O使得点A和点B都从O密度可达那么A和B是密度相连的。即使A和B之间没有直接或者间接的“单行道”关系只要它们都能从同一个核心出发到达它们就属于同一个簇。DBSCAN把“由密度可达关系导出的最大密度相连样本集合”定义为一个簇。理解这三个概念是理解算法的钥匙。密度直达是最小的连接单位密度可达是连成线密度相连是连成片。簇内部任何两个点都能通过密度相连建立联系而不同簇之间没有密度相连的点。2.4 算法执行流程一步步看DBSCAN怎么找簇把概念串起来DBSCAN的执行过程并不复杂。先给出算法伪代码级的流程再解释每一步在做什么。遍历数据集中的所有点找到一个还没有被访问过的点P。求P的eps邻域内所有点。如果邻域内点的数量小于minPts暂时把P标记为噪声进入下一个点。如果数量大于等于minPts新建一个簇C把P作为种子点加入并标记为已访问。取出种子点邻域内的所有点逐个遍历如果该点未被访问过标记为已访问并检查它的邻域如果它也是核心点就把它的邻域点加入种子集合无论它是不是核心点只要它不属于任何已有簇就把它加入簇C。当种子集合为空簇C生成完成继续处理剩余未访问点。这里的第5步是扩张的关键。它很像BFS广度优先搜索从种子点出发不断向外扩展。为什么最后一步很重要因为边界点邻域内的点数不够没法继续向外扩展自然就成了簇的边缘。而噪声点因为从来没能加入任何一个簇最终保持为噪声标签。在实现时为了避免重复计算邻域通常用一个布尔数组记录每个点是否已被访问。一个点如果已经被访问就不会再被当作新的种子。这个细节能大幅节省时间。2.5 eps和minPts取值大小对结果的影响同样的数据集参数设置不同聚类结果会天差地别。eps太小邻域覆盖范围窄核心点变少很多本应连在一起的簇被切断大量点被误判成噪声。eps太大邻域覆盖范围广不同簇可能被桥接在一起最终合并成一个巨型簇噪声也几乎消失。minPts也一样。minPts太小比如设为1那每个点自己都能成为核心点几乎每个点都会自成一簇或导致簇迅速扩散噪声很少minPts太大满足核心条件的点变少簇会被压缩噪声变多。比较常见的经验是当数据维度越高越需要更大的minPts来稳定密度估计。在实际调参过程中我建议先固定minPts再分析k-距离图确定eps随后根据聚类结果微调。后面会给出完整的Python实现。3. Python实现DBSCAN直接可跑的代码3.1 环境准备与模拟数据生成要用Python跑DBSCAN最方便的是scikit-learn库。建议使用Anaconda环境或者直接执行pip install scikit-learn matplotlib pandas安装依赖。为了演示不同形状数据的聚类效果我用make_blobs生成几团高斯分布的簇再用make_moons生成两个交错的月牙形分布。这两个数据集分别对应DBSCAN擅长的场景和K-Means不太擅长的场景。import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs, make_moons from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler # 生成两个环形/月牙形簇更容易看出DBSCAN的优势 X_moons, _ make_moons(n_samples300, noise0.05, random_state42) # 生成三团高斯分布的数据集附带少量噪声点 X_blobs, _ make_blobs(n_samples300, centers3, cluster_std0.6, random_state42)运行这段代码后X是一个二维数组每一行是一个样本的横纵坐标。可视化后能看到月牙形数据特别考验聚类算法对非凸簇的识别能力。3.2 用K-距离图确定eps的实操方法直接拍脑袋设置eps很难一个常用的办法是画出k-距离图。方法是计算每个样本与它最近的第k个邻居的距离k恰好等于minPts然后把这些距离从小到大排序画成曲线。曲线在某个位置会出现明显的拐点这个拐点对应的距离值就可以作为eps的参考。代码实现如下from sklearn.neighbors import NearestNeighbors def k_distance_plot(X, k4): nbrs NearestNeighbors(n_neighborsk).fit(X) distances, _ nbrs.kneighbors(X) # 取每个点和第k个邻居的距离 k_dist distances[:, -1] k_dist.sort() plt.plot(np.arange(len(k_dist)), k_dist) plt.xlabel(Points sorted by distance) plt.ylabel(k-th nearest neighbor distance) plt.show() return k_dist k_distance_plot(X_moons, k4)如果曲线是在0.3左右出现明显陡增的拐点那么eps就可以从0.3附近开始尝试。这种方法的直觉是密度高的地方点到最近第k个邻居的距离都很小拐点之后距离急剧增大说明进入了稀疏区域。把eps设为拐点处的距离就能把密集区域和稀疏区域分开。需要说明的是k距离图只提供一个起始参考真正好的参数还要结合业务和聚类评价指标做微调。我在实际使用中通常先看拐点然后以拐点为中心按0.1的步长来回尝试。3.3 使用sklearn的DBSCAN进行聚类参数确定之后调用DBSCAN非常简洁。核心代码如下# 对月牙形数据聚类 db_moons DBSCAN(eps0.25, min_samples4).fit(X_moons) labels_moons db_moons.labels_ # 对高斯团数据聚类 db_blobs DBSCAN(eps0.5, min_samples4).fit(X_blobs) labels_blobs db_blobs.labels_这里的labels_是一个一维数组数值从-1到簇数量减1。-1表示噪声点0、1、2等表示不同簇的编号。没有多余的属性核心结果就这一个。为了直观我写一个绘制散点图的函数把不同簇的点用不同颜色显示噪声点单独用灰色标记。def plot_clusters(X, labels, title): plt.figure(figsize(8, 6)) unique_labels np.unique(labels) colors plt.cm.Set1(np.linspace(0, 1, len(unique_labels))) for label, color in zip(unique_labels, colors): if label -1: color (0.5, 0.5, 0.5) mask labels label plt.scatter(X[mask, 0], X[mask, 1], c[color], s30, labelfcluster {label}) plt.title(title) plt.xlabel(Feature 1) plt.ylabel(Feature 2) plt.legend() plt.show() plot_clusters(X_moons, labels_moons, DBSCAN on moons dataset) plot_clusters(X_blobs, labels_blobs, DBSCAN on blobs dataset)如果在月牙数据上跑K-MeansKMeans会把两个月牙从中间切断产生互相交错的错误分簇而DBSCAN能顺着密度连接把月牙完整保留下来还能把稀疏区域的噪声标成-1。3.4 手动实现DBSCAN核心逻辑虽然可以直接用sklearn但为了深入理解算法我建议你至少手写一版。手写代码其实不多核心就是用NearestNeighbors预先算好每个点的邻居索引然后BFS扩展簇。以下是参考实现def custom_dbscan(X, eps, min_samples): n_samples X.shape[0] labels np.full(n_samples, -1) visited np.zeros(n_samples, dtypebool) cluster_id 0 # 预计算每个点的邻居 nbrs NearestNeighbors(radiuseps).fit(X) neighbors_list nbrs.radius_neighbors(X, radiuseps, sort_resultsTrue)[1] def expand_cluster(p, neighbors, cluster_id): labels[p] cluster_id queue list(neighbors) while queue: q queue.pop() if not visited[q]: visited[q] True q_neighbors neighbors_list[q] if len(q_neighbors) min_samples: for r in q_neighbors: if not visited[r]: queue.append(r) if labels[q] -1: labels[q] cluster_id for i in range(n_samples): if visited[i]: continue visited[i] True neighbors neighbors_list[i] if len(neighbors) min_samples: labels[i] -1 else: expand_cluster(i, neighbors, cluster_id) cluster_id 1 return labels labels_custom custom_dbscan(X_moons, eps0.25, min_samples4)这个实现有几个容易踩的坑。第一是queue.pop()使用栈而非队列其实不影响结果因为所有邻居都会被处理但如果想更贴近原始BFS可以改成pop(0)不过效率低。第二是radius_neighbors返回的是ndarray如果不加sort_resultsTrue邻居顺序不一定稳定。第三是边界点会重复进入簇好在通过labels[q] -1判断可以有效避免重复分配。手写实现的输出和sklearn版本在大多数情况下一致但细节差异在于sklearn对边界点和噪声点的处理做了优化比如它只在把所有核心点归簇后再分配边界点。如果只想在业务里快速用直接用sklearn就好想加深理解手写一遍非常有价值。3.5 如何评价聚类结果聚类没有真实标签时最常用的评价指标之一是轮廓系数。它同时衡量了簇内紧密度和簇间分离度取值范围在-1到1之间越高说明聚类效果越好。from sklearn.metrics import silhouette_score score_moons silhouette_score(X_moons, labels_moons) print(fSilhouette Score for moons: {score_moons:.3f})轮廓系数的计算公式不复杂但对新手来说理解它比背公式更重要。每个样本的轮廓系数等于(b-a)/max(a,b)其中a是该样本与同簇其他样本的平均距离b是该样本与最近的其他簇中样本的平均距离。如果b远大于a说明这个样本离自己的簇很近离别的簇很远轮廓系数接近1聚类效果好。需要留意的是轮廓系数在簇形状复杂时会有偏差。比如DBSCAN聚出的月牙形簇轮廓系数可能也不低但K-Means划分出的簇虽然轮廓系数看起来还行实际上却不符合真实分布。所以评价聚类不能只看一个分数最好把聚类结果可视化出来用眼睛检查一下是否符合业务直觉。4. DBSCAN与其他聚类算法的对比4.1 与K-Means的核心差异K-Means和DBSCAN很常见地被放在一起对比因为两者差异足够大。K-Means需要指定KDBSCAN不需要K-Means假设簇是凸形DBSCAN能处理任意形状K-Means把每个样本都分配进簇DBSCAN能把稀疏点标记为噪声。从算法复杂度看K-Means接近线性DBSCAN在不做索引优化时是O(n²)在做半径查询时如果用KD-Tree或Ball-Tree可以把部分情况降到O(n log n)。因此在大规模高维数据上DBSCAN会明显比K-Means慢。如果你只需快速对一组球形结构明显的样本做分组K-Means完全够用但如果业务里存在大量噪声或者簇的形状不规则DBSCAN更合适。我个人的经验是预处理和业务目标决定算法选择而不是算法本身的好坏。4.2 与层次聚类、GMM等其他算法的对比层次聚类通过不断合并或者分裂来构建树状结构不需要提前指定簇数但时间复杂度通常也是O(n²)以上而且对离群点敏感难以处理大规模数据。DBSCAN对离群点有天然鲁棒性。高斯混合模型GMM是软聚类方法它假设每个簇服从一个高斯分布适合处理簇之间有重叠的情况输出的是样本属于每个簇的概率。但GMM依然假设簇是高斯形状遇到月牙形数据同样无能为力。DBSCAN不需要这种概率假设更适合分布不规则的数据。在业务中如果数据适合用多个高斯分布描述比如语音信号、金融收益率分布GMM更强如果只想找到任意形状的密集区域DBSCAN更直接。两者并不冲突可以先用DBSCAN探索数据结构再用更适合的算法细分。4.3 DBSCAN的典型应用场景DBSCAN最常见的应用之一是地理空间聚类。比如外卖配送里把用户下单坐标按密度聚类就可以找出高需求区域帮助规划配送路线。DBSCAN可以把零散的下单点标为噪声避免异常订单影响区域划分。另一个高频场景是异常检测。比如在交易数据中正常交易往往聚集在常见模式周围而欺诈交易孤立在稀疏区域DBSCAN直接把这些稀疏点标为噪声天然就是一个异常检测器。在图像分割中DBSCAN也能派上用场。把每个像素视为特征空间中的一个点用像素的颜色特征聚类可以把图像中颜色相近的紧密区域分割出来。这种方法在某些简单场景下比阈值分割更灵活。5. 实战案例客户分群与异常点识别5.1 业务场景与数据准备假设我们有一份电商客户数据包含两个特征最近一次购买距离现在的天数recency和累计消费金额monetary。我们希望把客户分成几组并找出可能需要特殊关注的异常客户。数据本身量纲不同天数可能是0到100的整数金额可能是几十到几万如果直接算欧氏距离金额会压制天数的影响。所以第一步必须标准化。# 模拟数据 rng np.random.RandomState(42) n 500 recency rng.gamma(shape2, scale10, sizen) monetary rng.lognormal(mean5, sigma1.2, sizen) X_cust np.column_stack([recency, monetary]) # 添加一些异常点 X_cust[:10] [[200, 100000], [180, 80000], [220, 120000], [170, 70000], [210, 90000], [190, 130000], [160, 60000], [230, 110000], [240, 95000], [150, 85000]] scaler StandardScaler() X_cust_scaled scaler.fit_transform(X_cust)标准化的意义在于把两个特征缩放到同一量纲让距离计算更有意义。实际业务里还要考虑是否使用对数变换来处理长尾分布比如金额特别大时取log往往更合理。5.2 确定参数并执行聚类先画k-距离图minPts取维度数加1即3。观察拐点大约在0.8左右于是我们先设置eps0.8min_samples3。db_cust DBSCAN(eps0.8, min_samples3).fit(X_cust_scaled) labels_cust db_cust.labels_ unique, counts np.unique(labels_cust, return_countsTrue) for label, count in zip(unique, counts): print(fCluster {label}: {count} samples)运行后会看到类似输出噪声点可能有10个左右其余被分成3到4个簇。每个簇的中心、边界和包含的样本数量能直接反映不同客户群体的特征。5.3 结果解读与业务动作假设聚类结果把300个客户分成了簇0、簇1、簇2产生13个噪声点。可以进一步计算每个簇的原始特征均值import pandas as pd df_cust pd.DataFrame(X_cust, columns[recency, monetary]) df_cust[cluster] labels_cust df_cust.groupby(cluster).agg({recency: mean, monetary: median, cluster: count})如果簇0的recency很低、monetary很高那很可能是高价值活跃客户簇1的recency很高、monetary很低则是流失风险客户噪声点和边界点可能是数据异常或者极端大客户。针对这些分组运营可以制定不同的触达策略比如给流失风险客户发优惠券给高价值客户做专属服务。5.4 调参心得与实际坑点实战里最容易犯的错是直接拿原始特征跑DBSCAN。不标准化金额特征会把距离拉爆eps几乎没法设。另一个坑是盲目追求所有点都被分簇。有些业务认为噪声多是坏事于是不断调大eps最后所有点都被塞进一个巨大的簇完全失去了分组意义。噪声点本身可能就是有价值的信息比如异常交易、极端VIP客户不能简单忽略。当发现簇数量不符合预期时我的习惯是先用tsne或PCA把数据降到二维可视化再用肉眼观察密度分布。因为DBSCAN的结果高度依赖密度定义可视化能帮你快速判断参数是否合理。如果数据本身很紧密eps稍微变化就会导致合并建议先把特标准化再用k距离图。6. 常见问题与排查技巧实录6.1 到底怎么调eps和minPts很多新人问我DBSCAN有没有“默认参数”严格来说没有但可以给一个实用起点minPts取2乘以数据维度eps通过k距离图取拐点值。以二维数据为例minPts4是常见选择数据维度超过10时minPts至少取20因为高维空间里密度估计很不稳定。如果聚类结果里噪声太多说明eps太小或者minPts太大如果所有样本都被划进一个簇说明eps太大。可以先固定minPts把eps从0.1开始以0.05步长递增记录簇数量和噪声比例。画出“eps-簇数量曲线”后找一个相对平稳的区间。这个过程比只靠肉眼调参可重复得多。6.2 为什么DBSCAN在高维数据上表现不佳DBSCAN依赖距离阈值来判断密度而高维空间有个著名的“维度灾难”问题样本之间距离迅速趋于相等距离判定失去区分度。分辨率下降邻域里到底该包含哪些点变得模糊eps非常难选。应对方法主要有三种。第一先做降维比如PCA到10维以内再聚类第二改用更适合高维的距离度量比如余弦相似度配合合适的半径阈值第三换用OPTICS算法它可以看作DBSCAN的改进版不需要显式指定eps能把不同密度层次的结构也识别出来。6.3 参数量大、数据量大时如何加速DBSCAN最耗时的部分是邻域查询。直接用Python双重循环计算所有点对距离在几万样本上就慢得不能忍。解决办法是把距离矩阵计算改成KD-Treesklearn里的算法参数algorithmkd_tree就能起到这个作用。如果数据维度不高KD-Tree速度提升非常明显。如果数据量到百万级建议先采样一部分数据调好参数再全量预测。不过要注意sklearn的DBSCAN并不支持用fit后直接transform新样本因为聚类结果是基于整个数据集的密度分布得到的。对新增样本通常做法是重新跑一次聚类或者利用fit好的模型做邻居查询后把新样本分配到最近的核心点所在簇。我自己更倾向于定期重跑全量因为流式数据会造成密度漂移固定模型会越用越偏。6.4 聚类结果不稳定怎么办DBSCAN本身是确定性的只要参数和样本顺序不变每次运行结果一致。如果发现结果不稳定多半是特征标准化方式或者eps取到了临界值。临界参数会导致微小扰动就改变某个点的核心判定进而影响整个簇的扩张。解决方法是把临界点识别出来。看labels里是否有样本仅靠微小距离变化就会换簇。可以用np.where(labels -1)统计噪声点如果噪声点的数量刚好落在参数变化边缘不妨在最终聚类前先通过人工审核方式确认这些样本。业务上如果不希望噪声点出现太多可以微调eps到拐点略大的一侧得到更稳定但略粗糙的分组。6.5 常用Debug清单为了让你少走弯路我整理了一份DBSCAN排查清单是否做了标准化或适当的特征变换k距离图中的拐点是否明显如果不明显说明数据本身密度差异不大DBSCAN可能不适合。簇的样本数是否符合业务预期噪声点比例是否可解释是否存在某个簇跨越了大片区域把高密度区域和低密度区域连在了一起如果是eps可能过大。是否尝试了不同距离度量欧氏、曼哈顿、余弦代码中是否设置了algorithmkd_tree数据量大时性能是否可接受用这份清单逐项检查大部分DBSCAN的异常结果都能定位到问题所在。回到最初那个让我放弃K-Means的项目。当时我需要从一组用户行为日志中找出高活跃时段和异常登录行为数据分布非常不规律K-Means怎么跑都得不到清晰的分组。换成DBSCAN后我把购买间隔和登录次数缩放标准化靠k距离图确定了eps很快就把用户分成了高频稳定使用、低频流失倾向、夜间异常访问等几个簇那些被标为噪声的点最后验证下来大部分是机器行为。在那之后凡是要做无监督分群我都会先跑一版DBSCAN当作探索工具看看数据里有没有K-Means看不到的形态。如果你也在为不规则簇和异常点发愁不妨按这篇文章里的代码跑一遍把参数调参的耐心放在数据理解和可视化上大概率能打开新思路。