☰
聚类算法实战:从K-Means到DBSCAN的用户分群与异常检测指南
2026/10/14 1:15:49 网站建设 项目流程

1. 聚类算法到底在解决什么问题

1.1 从一个真实场景说起

手上有一批用户行为数据,几万条记录,每条包含消费频次、客单价、活跃天数、浏览深度等十几个字段。老板丢过来一句话:“把这些人分分类,看看能不能找出几类典型用户。”这时候你怎么办?挨个看?不现实。拍脑袋定规则?比如消费大于500算高价值、小于100算低价值?这种硬切分的问题在于,边界附近的人被生硬地劈成两类,而且阈值怎么定全凭感觉。

聚类算法就是干这个的:在没有标签的情况下,让数据自己“抱团”。它不需要你提前告诉它“这个人是高价值、那个人是低价值”,它只根据样本之间的相似度,把彼此靠近的样本归到一组,把离得远的样本分开。最终每一组叫一个“簇”,簇内的样本相似度高,簇间的样本差异大。

这件事听起来简单,但它是数据挖掘里最基础也最常被低估的一类方法。分类算法需要标注,标注成本高得离谱;聚类不需要标注,拿到数据就能跑,跑完再人工给每个簇贴标签。所以聚类的定位很清楚:探索性分析的第一把刀。

1.2 聚类和分类的本质区别

很多人刚入门的时候会把聚类和分类搞混。我用一句话说清楚:分类是“我知道有哪几类,你告诉我新来的属于哪类”;聚类是“我压根不知道有几类,你帮我看看能分成几类”。

举个例子。分类就像你手里有一本字典,来了一个生词,你查一下它属于哪个词条。聚类就像你拿到一堆没标签的照片,让你按内容相似度摆成几堆,摆完之后你再给每堆起个名字。

这个区别决定了聚类算法的评估方式和调参逻辑跟分类完全不同。分类看准确率、召回率、F1,聚类看轮廓系数、簇内平方和、Calinski-Harabasz指数。分类有明确的正确答案,聚类没有唯一正确的答案——不同的算法、不同的参数,可能得到完全不同的分法,而它们可能都有道理。

1.3 聚类能用在哪些地方

聚类不是学术玩具,它在工业界的落地场景非常多。我列几个自己实际接触过的方向:

  • 用户分群:电商、内容平台、SaaS产品用聚类把用户分成不同群体,针对每个群体做差异化运营。比如某内容平台用聚类发现有三类用户——高频短时浏览型、低频长时深度阅读型、周末集中爆发型,然后针对这三类分别调整推送策略。
  • 异常检测:正常数据会聚成若干簇,那些离所有簇都很远的点就是异常点。金融风控、设备故障预警、网络入侵检测都用这个思路。
  • 图像分割与压缩:把像素的颜色值做聚类,用簇中心代替原始像素值,实现图像压缩。经典的K-Means图像量化就是这个原理。
  • 文档归类:把大量新闻、论文、工单按主题聚类,减少人工整理成本。
  • 生物信息学:基因表达数据聚类,找出功能相似的基因群组。
  • 推荐系统冷启动:新物品没有用户行为数据时,先根据物品属性做聚类,把相似物品推荐给喜欢该簇的用户。

1.4 这篇文章适合谁看

如果你正在做数据分析、用户画像、推荐系统,或者单纯想搞清楚“无监督学习到底怎么落地”,这篇内容会对你有直接帮助。我会从算法选型讲到代码实操,从参数调优讲到踩坑记录,尽量把每个决策背后的“为什么”说清楚。不需要你有很深的数学背景,但最好写过Python,知道什么是矩阵、什么是距离。

2. 主流聚类算法选型与核心原理拆解

2.1 K-Means:最常用但不一定最好用

K-Means几乎是所有人接触的第一个聚类算法。它的逻辑极其简单:先随机选K个点作为簇中心,然后把每个样本分配给最近的中心,再重新计算每个簇的中心,反复迭代直到中心不再移动。

它的优势是快。时间复杂度是O(n×K×t),n是样本数,K是簇数,t是迭代次数。几万条数据、十几个特征,几秒钟就跑完了。但它的局限也很明显:

  • 必须提前指定K。你不知道数据该分几类,它也不知道,你得自己试。
  • 对初始中心敏感。随机初始化可能陷入局部最优,所以实际用的时候要设n_init多次重启取最优。
  • 只能发现球形簇。如果数据分布是长条形、环形、月牙形,K-Means会强行切成球,结果很糟糕。
  • 对异常值敏感。一个极端离群点会把簇中心拽偏,影响整个簇的划分。

我个人的经验是:K-Means适合做第一轮快速探索。先跑一下看看大概能分几类,再用其他算法验证。

2.2 DBSCAN:基于密度的另一种思路

DBSCAN不关心簇是不是球形,它关心的是“密度”。核心概念就两个参数:eps(邻域半径)和min_samples(邻域内最少样本数)。如果一个点的邻域内样本数超过min_samples,它就是核心点;核心点之间如果互相在邻域内,就连成一片,形成一个簇。

它的好处是:

  • 不需要指定簇数。算法自己决定能形成几个簇。
  • 能发现任意形状的簇。环形、S形、不规则形状都能识别。
  • 能识别噪声点。不属于任何簇的点被标记为-1,天然适合异常检测。

但DBSCAN的调参比K-Means更麻烦。eps稍微变一点,结果可能天差地别。而且它在高维数据上表现不好,因为高维空间中距离度量会失效——所有点之间的距离都差不多远,密度概念就模糊了。

2.3 层次聚类:从小到大的合并过程

层次聚类有两种方向:自底向上(凝聚型)和自顶向下(分裂型)。实际用得多的是凝聚型:一开始每个点自成一簇,然后每次找最近的两个簇合并,直到所有点合并成一个大簇。

它的输出是一棵“树”,叫树状图。你可以根据业务需要,在树的某个高度切一刀,得到对应数量的簇。这个特性很适合探索性分析——你不需要提前决定K,先看树状图,再决定切在哪里。

代价是计算复杂度高,O(n²)甚至O(n³),数据量大了跑不动。所以层次聚类一般用在几千条以内的小数据集上。

2.4 高斯混合模型:软聚类的代表

前面三种都是“硬聚类”——每个样本只属于一个簇。高斯混合模型(GMM)是“软聚类”——每个样本以不同概率属于各个簇。

GMM假设数据是由若干个高斯分布混合生成的,每个高斯分布对应一个簇。它输出的是每个样本属于每个簇的概率,而不是一个硬标签。这在某些场景下非常有用,比如一个用户可能同时具有“价格敏感”和“品质追求”两种特征,硬聚类会强行把他分到一边,GMM可以给出60%对40%的概率分布。

但GMM的计算量比K-Means大,而且对初始值敏感,容易收敛到局部最优。

2.5 算法选型对照表

算法需要指定簇数簇形状对异常值数据规模适用场景
K-Means是球形敏感大快速探索、用户分群
DBSCAN否任意不敏感中异常检测、不规则簇
层次聚类否(可切树)任意较敏感小探索性分析、小数据集
GMM是椭圆形较敏感中软聚类、概率输出

选型的时候先问自己三个问题:数据量多大?簇大概是什么形状?要不要处理异常值?答案基本能帮你锁定一两个候选算法。

3. 完整实操流程:从原始数据到聚类结果

3.1 数据准备与预处理

我拿一份模拟的用户行为数据来演示。假设有5000条用户记录,字段包括:日均使用时长、周活跃天数、月消费金额、功能使用种类数、社交互动次数。

第一步永远是检查数据质量。缺失值怎么处理?数值型字段用中位数填充还是均值填充?我的习惯是中位数,因为中位数对异常值更稳健。类别型字段要么做独热编码,要么直接剔除。

第二步是处理异常值。这里有个坑:聚类本身可以用来检测异常值,但如果你在预处理阶段就把异常值删了,聚类就检测不到了。所以我的做法是:先保留异常值跑一遍聚类,看看哪些点被标记为噪声,再决定是删除还是单独分析。

第三步是标准化。这是最关键的一步。日均使用时长可能是0到300分钟,月消费金额可能是0到50000元,量纲差了几百倍。如果不标准化,消费金额会主导距离计算,使用时长几乎不起作用。标准化的方法用Z-Score最稳妥:

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X)

注意:标准化之后,每个特征的均值为0、标准差为1。如果你用Min-Max归一化,对异常值会更敏感,因为最大值和最小值会被极端值拉偏。

3.2 K-Means实操:确定最佳簇数

K-Means最大的痛点就是K怎么定。我用两个方法交叉验证:肘部法和轮廓系数。

肘部法是看簇内平方和(inertia)随K变化的曲线。K增大,inertia必然下降,但下降速度会在某个点突然变缓,那个拐点就是候选K。

from sklearn.cluster import KMeans import matplotlib.pyplot as plt inertias = [] K_range = range(2, 11) for k in K_range: kmeans = KMeans(n_clusters=k, n_init=10, random_state=42) kmeans.fit(X_scaled) inertias.append(kmeans.inertia_) plt.plot(K_range, inertias, 'bo-') plt.xlabel('K') plt.ylabel('Inertia') plt.show()

轮廓系数则是综合考量簇内紧密度和簇间分离度,取值在-1到1之间,越接近1越好。

from sklearn.metrics import silhouette_score sil_scores = [] for k in K_range: kmeans = KMeans(n_clusters=k, n_init=10, random_state=42) labels = kmeans.fit_predict(X_scaled) sil_scores.append(silhouette_score(X_scaled, labels))

两个方法的结果不一定完全一致。我的经验是:如果肘部法和轮廓系数指向不同的K,优先选轮廓系数高的,但也要考虑业务可解释性。比如轮廓系数说K=7最好,但K=7分出来的簇在业务上很难解释,那不如选K=4,虽然指标略低但每个簇都有明确的业务含义。

3.3 DBSCAN实操:k-距离曲线定eps

DBSCAN的eps参数可以用k-距离曲线来辅助确定。具体做法是:对每个点计算它到第k个最近邻的距离,然后把这些距离从小到大排序画出来。曲线拐弯的地方对应的距离值,就是比较合适的eps。

from sklearn.neighbors import NearestNeighbors import numpy as np k = 5 # 对应min_samples nbrs = NearestNeighbors(n_neighbors=k).fit(X_scaled) distances, indices = nbrs.kneighbors(X_scaled) distances = np.sort(distances[:, k-1], axis=0) plt.plot(distances) plt.xlabel('Points sorted by distance') plt.ylabel(f'{k}-th nearest neighbor distance') plt.show()

min_samples一般设成特征数加1,或者特征数的两倍。特征多的时候可以适当调大。

from sklearn.cluster import DBSCAN dbscan = DBSCAN(eps=0.8, min_samples=6) labels = dbscan.fit_predict(X_scaled)

跑完之后统计一下每个簇的样本数和噪声点比例。如果噪声点超过30%,说明eps太小或者min_samples太大;如果只有一个簇,说明eps太大。

3.4 聚类结果的可视化与解读

高维数据没法直接画散点图,我一般用两种方式降维可视化:PCA和t-SNE。

PCA适合看整体分布,速度快,但它是线性降维,复杂结构可能被压扁。t-SNE适合看局部结构,能把高维空间中相近的点在低维空间中也拉近,但计算慢,而且不同参数下结果差异大。

from sklearn.decomposition import PCA from sklearn.manifold import TSNE pca = PCA(n_components=2) X_pca = pca.fit_transform(X_scaled) tsne = TSNE(n_components=2, perplexity=30, random_state=42) X_tsne = tsne.fit_transform(X_scaled)

画图的时候用簇标签上色,噪声点用灰色或黑色单独标出来。如果某个簇在图上散得很开,说明这个簇内部异质性高,可能需要拆分;如果两个簇在图上混在一起,说明它们可能应该合并。

3.5 给簇贴标签:从数字到业务含义

聚类跑完只是第一步,真正有价值的是给每个簇赋予业务含义。做法是:对每个簇,计算各个特征的平均值,然后跟全局平均值对比,看哪些特征显著偏高或偏低。

import pandas as pd df['cluster'] = labels cluster_profile = df.groupby('cluster').mean() global_mean = df.mean() diff = cluster_profile - global_mean print(diff)

比如某个簇在“月消费金额”上比全局均值高2个标准差,在“活跃天数”上也偏高,那这个簇可以命名为“高价值活跃用户”。另一个簇在“使用时长”上偏低、“功能使用种类数”也偏低,可以叫“轻度尝鲜用户”。

这个过程需要结合业务知识反复推敲。我踩过的坑是:有一次聚类分出一个簇,各项指标都很平庸,怎么都找不到业务解释。后来发现这个簇其实是“数据缺失填充后形成的伪簇”——那些缺失值被中位数填充的用户,特征向量都差不多,自然聚到了一起。所以预处理阶段的缺失值处理策略,会直接影响聚类结果的可解释性。

4. 常见问题与排查技巧实录

4.1 聚类结果每次跑都不一样

这是新手最常遇到的问题。原因通常是随机初始化。K-Means默认会随机选初始中心,不同随机种子得到不同结果。

解决办法有两个:一是设random_state固定随机种子,保证可复现;二是增大n_init,让算法多跑几次取最优。sklearn从1.4版本开始,n_init的默认值从10改成了'auto',建议手动设成10或20。

kmeans = KMeans(n_clusters=4, n_init=20, random_state=42)

如果设了随机种子结果还是不稳定,那可能是数据本身就没有明显的簇结构。这时候强行聚类就是在噪声里找规律,换算法也救不了。

4.2 某个簇特别大,其他簇特别小

这种情况通常有两种原因:一是数据本身分布不均匀,确实存在一个主导群体;二是算法参数不合适,把该分开的簇合并了。

如果是K-Means,可以尝试增大K,看看大簇能不能拆开。如果是DBSCAN,可能是eps太大,把相邻的簇连成一片了,适当调小eps试试。

还有一种可能是异常值干扰。少数极端值把簇中心拉偏,导致大量正常点被归到同一个簇。这时候可以先剔除异常值再聚类,或者改用DBSCAN这种对异常值不敏感的算法。

4.3 高维数据聚类效果很差

维度灾难是聚类绕不开的问题。当特征维度超过20维,距离度量就开始失效——所有点之间的距离都趋近于相同,密度概念也模糊了。

应对策略有三个:

  • 降维:用PCA降到10维以内再聚类。但要注意,PCA是无监督降维,它保留的是方差最大的方向,不一定是对聚类最有用的方向。
  • 特征选择:根据业务知识手动筛选特征,去掉冗余和噪声特征。
  • 改用适合高维的算法:比如谱聚类,或者先用自编码器压缩特征再聚类。

我自己的习惯是:特征数超过15个就先做一轮特征筛选,把相关性高于0.9的特征去掉冗余,把方差极小的特征直接剔除。

4.4 怎么判断聚类结果有没有意义

这个问题没有标准答案,但有几个实用的检验角度:

  • 业务可解释性:每个簇能不能用一两句话描述清楚?如果说不清楚,要么是簇数不对,要么是特征选得不好。
  • 稳定性:换一个随机种子、换一个算法,结果是否大致相似?如果差异巨大,说明数据结构本身不清晰。
  • 外部验证:如果有额外的标签信息(比如用户是否流失),可以看聚类结果和这些标签是否有相关性。注意,聚类是无监督的,不能用标签来调参,但可以用来事后验证。
  • 轮廓系数:虽然不能全信,但低于0.2基本说明簇结构很弱。

4.5 常见问题速查表

问题现象可能原因排查方向解决办法
每次结果不同随机初始化检查random_state固定种子,增大n_init
簇大小极不均匀参数不当或异常值看簇内样本数分布调K/eps,剔除异常值
高维效果差维度灾难看特征数降维或特征选择
轮廓系数低数据结构不明显看降维可视化换算法或重新选特征
噪声点过多eps太小看k-距离曲线调大eps或减小min_samples
只有一个簇eps太大看k-距离曲线调小eps

4.6 几个我踩过的坑

第一个坑:忘了标准化。有一次跑K-Means,消费金额字段没做标准化,结果聚类完全被这个字段主导,其他字段形同虚设。分出来的簇就是按消费金额从高到低切的几段,毫无洞察价值。从那以后我养成了习惯:只要特征量纲不同,第一步永远是标准化。

第二个坑:用聚类结果直接做决策。聚类是探索性工具,不是决策工具。它告诉你“数据可以这样分”,但不告诉你“应该这样分”。我见过有人把聚类结果直接当成用户分层标准,结果业务方一看就说“这个分层跟我们实际认知完全不符”。正确做法是:聚类结果作为参考,结合业务经验调整后再落地。

第三个坑:忽略业务周期。用户行为数据是有周期性的。工作日和周末的行为模式不同,月初和月末的消费行为也不同。如果拿一个时间截面的数据做聚类,得到的可能是“工作日用户”和“周末用户”的划分,而不是真正的用户类型划分。解决办法是:要么用多个时间截面的数据做特征,要么在特征工程阶段就把周期性因素剥离掉。

第四个坑:簇数太多导致过度解读。有一次我跑出来8个簇,每个簇都硬编了一个业务故事。后来换了一批数据重新跑,发现其中几个簇根本不稳定,换一批样本就消失了。教训是:簇数宁少勿多,3到6个簇通常是最容易落地和解释的。超过6个簇就要警惕是不是在拟合噪声。

5. 聚类效果的评估与迭代优化

5.1 内部指标:没有标签时怎么评估

内部指标只依赖数据本身和聚类结果,不需要外部标签。常用的有三个:

  • 轮廓系数:综合考虑簇内距离和簇间距离,取值-1到1。大于0.5说明簇结构清晰,0.2到0.5说明结构一般,低于0.2说明结构很弱。
  • Calinski-Harabasz指数:簇间离散度与簇内离散度的比值,越大越好。计算比轮廓系数快,适合大数据集。
  • Davies-Bouldin指数:簇内距离与簇间距离的比值,越小越好。
from sklearn.metrics import calinski_harabasz_score, davies_bouldin_score ch_score = calinski_harabasz_score(X_scaled, labels) db_score = davies_bouldin_score(X_scaled, labels)

这三个指标各有侧重,我的习惯是至少看两个,如果两个指标结论一致,那基本可以放心;如果矛盾,就需要结合业务判断。

5.2 外部指标:有标签时的验证

如果你手头恰好有一些标签(比如用户是否流失、是否复购),可以用外部指标来验证聚类结果和这些标签的一致性。常用的有调整兰德指数(ARI)和归一化互信息(NMI)。

但要注意:外部指标只能用来验证,不能用来调参。如果你根据ARI来选K,那就等于在用标签信息做监督学习,聚类就失去了无监督的意义。

5.3 迭代优化的三个方向

聚类不是跑一次就完事的,通常需要迭代两到三轮。优化的方向有三个:

方向一:调整特征。聚类效果不好,十有八九是特征的问题。试试增加有区分度的特征,去掉噪声特征,或者对偏态分布的特征做对数变换。

方向二:调整算法参数。K-Means调K和n_init,DBSCAN调eps和min_samples,GMM调协方差类型。每次只调一个参数,观察指标变化。

方向三:换算法。如果K-Means跑出来轮廓系数一直很低,试试DBSCAN或谱聚类。不同算法对数据结构的假设不同,换一个可能豁然开朗。

5.4 一个完整的迭代案例

我拿之前做过的一个用户分群项目举例。第一轮用K-Means,K=4,轮廓系数0.32,分出来的簇业务方觉得太粗。第二轮增加了一个“近30天互动次数”特征,K调到5,轮廓系数升到0.41,但有一个簇只有几十个样本,明显是异常值形成的。第三轮先剔除异常值再聚类,K=4,轮廓系数0.45,四个簇的业务含义都很清晰,最终落地。

整个过程花了两天,跑了十几轮。聚类就是这样,没有一步到位的,都是在反复试错中逼近一个可用的结果。

6. 工程落地中的性能优化与规模化

6.1 大数据量下的K-Means优化

K-Means在几万条数据上跑没问题,但到了百万级、千万级,标准实现就扛不住了。这时候可以用MiniBatch K-Means,它每次只用一小批样本更新簇中心,速度快很多,代价是精度略降。

from sklearn.cluster import MiniBatchKMeans mbk = MiniBatchKMeans(n_clusters=5, batch_size=1000, n_init=10, random_state=42) labels = mbk.fit_predict(X_scaled)

实测下来,百万级数据用MiniBatch K-Means比标准K-Means快5到10倍,轮廓系数只低0.02到0.05,完全可接受。

6.2 用PCA预降维加速

如果特征维度很高,先做PCA降维再聚类,既能加速又能去噪。一般保留85%到95%的方差即可。

pca = PCA(n_components=0.9) X_pca = pca.fit_transform(X_scaled)

注意:PCA之后的新特征是原始特征的线性组合,可解释性会下降。如果业务方要求每个簇必须能用原始特征解释,那就不能做PCA,只能做特征选择。

6.3 增量聚类:新数据来了怎么办

实际业务中数据是不断新增的,不可能每次来新数据都全量重跑。有两种策略:

  • 定期全量重跑:比如每周跑一次全量聚类,更新簇中心。新数据用最近的簇中心做分配。
  • 在线更新:用MiniBatch K-Means的partial_fit方法增量更新簇中心。
mbk = MiniBatchKMeans(n_clusters=5, batch_size=1000) mbk.partial_fit(X_first_batch) mbk.partial_fit(X_new_batch)

增量聚类的风险是簇中心会随时间漂移,跑久了可能偏离初始的业务含义。所以建议每隔一段时间做一次全量重跑,校准簇中心。

6.4 聚类结果的持久化与复用

跑好的聚类模型要保存下来,新数据来了直接预测所属簇,不用重新训练。

import joblib joblib.dump(kmeans, 'kmeans_model.pkl') joblib.dump(scaler, 'scaler.pkl') # 新数据预测 kmeans_loaded = joblib.load('kmeans_model.pkl') scaler_loaded = joblib.load('scaler.pkl') new_labels = kmeans_loaded.predict(scaler_loaded.transform(X_new))

注意:保存模型的时候一定要把标准化器一起保存。新数据必须用同样的标准化参数处理,否则距离计算会完全错位。

7. 几个实战中的经验体会

聚类算法本身不难,难的是把它用对地方、用出价值。我做了这么多项目,最大的体会是:聚类是手段,不是目的。业务方要的不是几个簇的编号,而是“这几类人分别是谁、他们想要什么、我们该对他们做什么”。

所以每次做聚类之前,我都会先问业务方三个问题:你打算用这个结果做什么决策?你期望分几类?每类大概是什么样?这三个问题能帮你省掉大量无效迭代。

另外,不要迷信指标。轮廓系数0.5不一定比0.4好,如果0.4对应的分法业务上更合理,那就选0.4。指标是参考,业务价值才是最终标准。

最后分享一个小技巧:聚类结果出来之后,先别急着汇报,自己拿几个典型样本看看。从每个簇里随机抽几条原始数据,看看它们是不是真的相似。这一步能帮你发现很多指标看不出来的问题,比如某个簇其实混进了两种完全不同的样本,只是它们在标准化后的空间里碰巧离得近。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询