聚类分析这话题,我这两年带过不少做数据分析的朋友,也处理过形形色色的业务数据。很多人一上来就问“聚类该选K-means还是层次聚类”,但我更想说,在动手跑代码之前,先想明白“为什么要聚类”可能更重要。聚类分析及R实现,这个标题本身我理解就是两件事:一是把聚类这套方法论吃透,二是真正能在R里把代码跑通、把业务问题解掉。这篇文章我尽量把我踩过的坑、验证过的代码和调整参数的思路都整理出来,希望能让不同基础的朋友都有收获。
先从最简单的认知框架说起。聚类属于无监督学习,跟分类不一样——分类是你已经知道有几类、每类长什么样,拿已知标签去训练模型;聚类更像是给你一筐混杂的积木,没有人告诉你该分成几堆,你要自己根据形状和颜色把它们归拢成几组。商业场景里最典型的例子就是客户分群:用户画像里没有“高价值”“潜力型”“沉默型”这样的列,但用聚类跑一遍,往往能挖出几条很清晰的人群特征。
R做聚类最大的好处在于生态完整。从基础包里的hclust、kmeans,到专门做密度聚类的dbscan包,再到可视化的factoextra、ggplot2,几乎每个环节都有成熟工具,不需要自己造轮子。但同时也要注意,工具多不代表活就简单,聚类的每个环节——变量处理、距离计算、聚类方法、类别数确定——都有取舍和陷阱。这篇文章我会把整个过程拆开讲,并用一个完整的模拟案例串起来。
1. 聚类分析的核心思路:先把“物以类聚”翻译成数学问题
1.1 聚类的本质:从“相似”到“距离”
要理解聚类,先得理解“相似”。业务上我们说两个客户很像,可能是年龄相近、消费习惯相似、活跃时段重叠。但计算机不能理解“像不像”,它只认数字。于是“相似度”就转化成了“距离”——两个样本在特征空间里离得越近,说明它们越相似。
这里就带出了聚类分析最关键的一个前置决策:选什么特征、特征怎么处理。我见过太多人拿到数据直接丢进聚类函数里跑,结果出来的簇完全没有业务含义。为什么?因为原始特征往往量纲不同、分布形态差异巨大,甚至有些特征本身信息冗余。比如同时放“年龄”和“收入”,收入数值成千上万,年龄只有几十,距离计算时收入几乎主导了整个结果,年龄这个变量就白放了。
所以在进入距离计算和聚类算法之前,必须做两步处理:标准化和特征筛选。标准化最常用的是z-score,也就是把每个变量减去均值除以标准差,让所有变量都落到均值0、标准差1的尺度上。R里scale()函数一行就搞定。特征筛选则要看具体情况,如果特征太多且相关性强,可以先跑个主成分分析做降维,或者用业务逻辑手动挑。聚类不怕变量少,怕变量杂。
1.2 距离度量怎么选:连续、离散、混合数据各有讲究
把数据准备好了,接下来算距离。最广为人知的是欧氏距离,也就是直线距离。但用它之前要确认数据是连续型、且各维度经过标准化处理。如果是二元属性(比如是否活跃、是否购买过),jaccard距离通常更合适;如果数据包含分类变量,比如客户所在区域、行业类型,可以考虑Gower距离,它在daisy函数里有现成实现。
我自己的经验是:能算清楚业务含义的度量,比理论上最优的度量更重要。比如做用户分群如果只用了连续消费变量,欧氏距离完全够用且解释起来容易。加进来几个分类变量后,Gower距离就显现出优势。dist()函数默认欧氏距离,daisy()支持混合数据,两个都在R包里,按需选择,别一上来就凭感觉。
1.3 标准化处理:容易被忽略却决定聚类结果的第一步
标准化这一步值得多说几句,因为它太容易被忽略,而影响又是决定性的。假如你有两个变量:月消费金额和月登录次数。前者数值在几百到几万,后者在0到几十。不标准化直接算欧氏距离,消费金额的差值会完全淹没登录次数的差值,聚类结果几乎只反映消费单一维度的分层。
R里用scale()函数处理,默认就是z-score标准化。有个小提醒:标准化应该在筛选出最终参与聚类的特征之后做,而不是一开始就把全量数据标准化。因为你可能中间要删除掉一个离群值明显、方差异常的特征,提前标准化等于白做。
另外一个坑是:标准化的对象应该是“连续数值变量”,如果是0/1这种天然同尺度的变量,不标准化问题不大。但为了一致性和省事,我习惯连续变量一律标准化,分类变量单独处理,最后合并距离矩阵。
2. 三种主流聚类方法:原理、适用场景与R实现路径
2.1 K-means:最快上手,但别被它的简单骗了
K-means是几乎所有人接触聚类的第一站。它的目标函数很直白——把n个样本分到k个簇中,让每个样本到其所属簇中心的距离平方和(SSE,簇内误差平方和)最小。算法流程也不复杂:先随机初始化k个中心点,分配样本到最近中心点所属的簇,重新计算每个簇的中心(均值),再重新分配,一直迭代到中心点不再变化。
R里调用kmeans()函数非常简洁,核心代码就一行:
km_result <- kmeans(scale_data, centers = 4, nstart = 25)这里面两个参数要留神。centers就是你要指定分成几类,这个值的确定本身就是聚类分析里最麻烦的问题之一,我后面的章节会专门讲。nstart是随机初始中心的次数,因为kmeans对初始中心敏感,不同初始中心可能收敛到不同局部最优解。R默认是1,但我强烈建议至少设成25,让算法多跑几轮挑一个SSE最小的结果,这样结果更稳定。
虽然上手快,K-means的局限性也要认清。它对球形分布的数据表现良好,但对细长条、半月形这种形状的簇几乎无能为力,因为它本质上是按“到中心的距离”分配样本,天然偏好凸形簇。另外它对离群点敏感,一个极端大值可能导致簇中心被拉偏。所以数据清洗阶段最好做离群点检测,或者用修剪版的K-medoids(PAM)来缓解。
适用场景上,K-means特别适合数据量大、维度适中、簇形状比较规整、主要目的是客户分群或产品分层的场景。比如几万条用户交易记录,标准化后跑kmeans,得到几类明确的消费人群,这是K-means最典型的用武之地。
2.2 层次聚类:不预设类别数,从树状图里找答案
层次聚类跟K-means完全不同。它不需要你一开始就定好分几类,而是通过不断合并(凝聚式)或分裂(分裂式)样本,生成一棵完整的层次树,再根据树状图来决定切割位置。
R里层次聚类两步走,先算距离矩阵,再调用hclust():
dist_matrix <- dist(scale_data, method = "euclidean") hc_result <- hclust(dist_matrix, method = "ward.D2") plot(hc_result) # 画树状图看结构hclust()里的method参数有很多选项,ward.D2、complete、average、single等。它们的区别在于簇与簇之间的距离怎么定义。最直观的complete(最长距离法)是用两个簇中距离最远的点作为簇间距,single(最短距离法)正好相反,取最近的点。ward.D2比较特殊,它是按合并后簇内离差平方和的增量最小来选择合并对象,倾向于生成大小更均衡的簇,是我在业务里最常用的方法。
层次聚类的最大优势是信息量大,一张树状图能看全局:哪些样本先抱团、哪些簇关系近、大致分几类比较自然。缺点是计算复杂度高,几千个样本跑起来还能接受,上到几万样本hclust就非常吃力了。所以在数据量大的时候,我通常先用K-means粗分,再对每个簇内部做层次聚类,把这个组合方案分享给不少朋友试过,反馈都不错。
树状图里确定类别数有一个实用技巧:观察合并距离的跳跃。hclust结果里有一个“合并高度”,如果某次合并的高度突然跳得很高,说明之前是一个个簇自然生长,而这次合并强行把两个差异很大的簇拉到一起,这个位置往往就是合理的切割点。R里用abline()可以画水平切割线辅助判断:
plot(hc_result) rect.hclust(hc_result, k = 4, border = 2:5)2.3 DBSCAN:专治各种不规则簇形,但参数调试要耐心
如果数据里有大量噪声点、簇的形状又不是规规矩矩的圆形,DBSCAN往往比K-means更稳。它的核心思想是“密度相连”:在一个点的eps半径范围内,如果邻居数达到minPts阈值,这个点就视为核心点,所有密度相连的核心点及其邻域扩张成一个簇。
R里使用dbscan包:
library(dbscan) db_result <- dbscan(scale_data, eps = 1.5, minPts = 5)DBSCAN不需要预先指定类别数,还能自动把离散点标记为噪声(结果中聚类ID为0)。这在处理带有大量异常值的空间数据、轨迹数据、或者用户行为日志时非常实用。
但DBSCAN的两个参数eps和minPts调起来比较费心。eps太小会导致簇分裂严重、大量样本被判为噪声,eps太大又会把本不该合并的簇粘在一起。一个经典的经验法则是:先设minPts = 2 * 数据维度,然后画出k-距离图,找到曲线拐点处对应的距离作为eps的参考值。R里有kNNdistplot()函数可以直接画:
library(dbscan) kNNdistplot(scale_data, k = 5) abline(h = 1.5, col = "red")画出来后看红线切在哪个位置密度开始骤降,那个高度对应的就是比较合理的eps。这个方法不完美,但至少让参数选择从“瞎猜”变成了“有据可依”。
3. 实操全流程:从数据准备到聚类结果解读
3.1 场景设定:某电商平台的会员消费分群
理论讲了这么多,最终还是要落到一个具体案例上。我用一个模拟场景来说明:假设你拿到某电商平台2000名会员的数据,字段包括年龄、月均消费金额、月均购买次数、最近一次购买距今天数(类似RFM模型里的F和R)。目标是根据这些行为特征做客户分群,为后续精细化运营提供分组依据。
模拟数据在R里可以用随机数生成,方便复现。真实业务里数据可能又乱又脏,但逻辑完全一致。我先整理数据、看一眼缺失值和分布,然后做标准化,再进入聚类。我给这个模拟场景选K-means作为主方法,原因很实在:簇形状大概率是近似球形的、数据量2000规模跑起来很快、而且结果好解释——每个簇都能用几个变量的均值概括出一类人群。
3.2 数据准备:缺失值处理、离群点识别与标准化
数据准备我一般分三步:清洗、去离群、标准化。
清洗主要是处理缺失值。跑聚类前缺失值必须处理掉,因为大多数距离计算函数遇到缺失值会直接报错或返回NA。常用做法有三种:删除缺失样本、用均值或中位数填充、用模型预测填充。对于会员数据这种通常缺失率不高的场景,删除或中位数填充就够了。如果缺失率超过20%,我会先怀疑这个变量是否可靠,并考虑直接丢弃。
离群点识别同样重要。我习惯先用箱线图或DBSCAN跑一遍粗筛,看有哪些样本在多个变量上都极端偏离。比如月消费金额高达平均值的几十倍,这类样本单独成簇还好,最怕它带偏K-means的簇中心。R里可以用boxplot()快速可视化,也可以用dbscan()先把噪声点标记出来再剔除。
标准化我用scale()完成,这一点不再重复。需要补充的是,标准化后一定要检查每个变量的均值和标准差是否真的变成了0和1。看似多此一举,但能避免一些因为数据框类型不同而产生的隐性错误。
3.3 确定k值:肘部法则与轮廓系数的交叉验证
K-means最让人头疼的就是k怎么定。我常用的方法有两个,交叉验证着看。
肘部法则的出处是“随着k增大,SSE单调下降,但下降速度会在某个点突然变缓,转折点像手肘”。R里写个循环:
sse <- c() for (i in 1:10) { km <- kmeans(scale_data, centers = i, nstart = 25) sse[i] <- km$tot.withinss } plot(1:10, sse, type = "b", xlab = "聚类数k", ylab = "簇内误差平方和")运行之后看拐点在哪里。很多时候拐点不会特别清晰,比如曲线是平滑下滑的,这时肘部法则就不够用了。
轮廓系数则是另一个角度:计算每个样本与其自身簇内其他样本的平均距离(凝聚度),再计算该样本与最近的其他簇所有样本的平均距离(分离度),两者做差除以较大值。结果范围在-1到1,越接近1表示聚类效果越好,接近0说明样本在两簇边界上,负数则表示可能分错了簇。R里用cluster包的silhouette()函数计算,遍历k值画图:
library(cluster) avg_sil <- c() for (i in 2:10) { km <- kmeans(scale_data, centers = i, nstart = 25) sil <- silhouette(km$cluster, dist(scale_data)) avg_sil[i] <- mean(sil[, 3]) } plot(2:10, avg_sil[2:10], type = "b", xlab = "聚类数k", ylab = "平均轮廓系数")一般选平均轮廓系数最高点对应的k,但也要结合业务可解释性。有时候统计上最优的k是5,但分出来某一类只有几十个人,业务上没法单独运营,这时我宁可选择k为4,把那一小群并进相邻的簇。聚类永远是为业务服务的,不是让业务来迁就算法。在我这个模拟案例里,肘部图和轮廓系数的结果都指向k=4附近,于是确定用4个簇。
3.4 运行K-means:关键参数设置与稳定策略
k确定之后跑正式聚类。代码很简单,但参数设置上有讲究。nstart至少25次起步,我习惯直接设50。中心初始化算法默认是Hartigan-Wong,R里这个实现效果还不错,一般不用改。最大迭代次数iter.max默认10有时不够,尤其数据量大、特征多的时候,调到100更保险。
另一个值得注意的点是随机种子。kmeans本身有随机性,即使nstart很大,也只是让结果稳定的概率更高,不保证绝对一样。为了保证结果可复现,我习惯在跑之前用set.seed()固定随机种子。这不是耍小聪明,而是团队协作和项目管理里必须养成的习惯——不然别人跑你脚本,每次聚类结果都不同,后续分析怎么对得上?
set.seed(42) km_final <- kmeans(scale_data, centers = 4, nstart = 50, iter.max = 100)跑完后看几个核心输出:cluster属性是每个样本的分组编号,centers是每个簇的中心坐标(注意是标准化后的值),size是每簇样本数,tot.withinss是簇内总平方和。我每次都会先看size,如果某个簇只有几个人或者占了全量70%以上,基本可以判定聚类不理想,需要回头检查标准化、是否含有离群点、k值是否合理。
3.5 结果可视化:二维投影的色彩分组
聚类结果验证环节,可视化是必须的。高维数据不能直接画图,我通常用主成分分析或t-SNE降维到二维,再用散点图着色展示聚类效果。
R里先用prcomp()做PCA,取前两个主成分,再用ggplot2绘图:
pca_res <- prcomp(scale_data, scale. = TRUE) pca_coord <- as.data.frame(pca_res$x[, 1:2]) pca_coord$cluster <- as.factor(km_final$cluster) library(ggplot2) ggplot(pca_coord, aes(x = PC1, y = PC2, color = cluster)) + geom_point(alpha = 0.6, size = 1.5) + labs(title = "K-means聚类结果可视化 (PCA降维)")画完之后重点看两个事:一是簇之间是否分得开,如果两个簇重叠严重,说明特征本身区分度不够或者k选得不对;二是簇的分布是否符合直觉,比如有没有某个簇被单独甩在角落,这类孤立簇往往值得单拎出来看业务含义。
pairs()函数画两两特征散点图也是一个好用的辅助工具,虽然图多信息杂,但能看出哪些变量对区分簇贡献大。聚类跟分类不同,没有正确标签告诉你分得对不对,只能靠可视化加业务语义去交叉验证。
3.6 解读聚类结果:每个簇的中心特征与业务画像
聚类跑出来了,代码不是终点,解读才是价值所在。我习惯先把每个簇中心还原成原始尺度,这样业务同事能直接看懂,然后逐簇总结画像。
还原中心方法很简单,就是拿标准化后的中心乘以原始标准差再加重建过程中的均值偏移,最省事的方式是用scale()结合属性:
cluster_centers <- km_final$centers # 计算原始尺度中心 centers_raw <- scale(cluster_centers, center = FALSE, scale = 1 / attr(scale_data, "scaled:scale")) centers_raw <- sweep(centers_raw, 2, attr(scale_data, "scaled:center"), FUN = "+")更简单的做法是直接按簇分组,用aggregate或者dplyr的group_by计算原始变量均值:
library(dplyr) data_with_cluster <- cbind(data, cluster = km_final$cluster) data_with_cluster %>% group_by(cluster) %>% summarise( avg_age = mean(age), avg_amount = mean(monthly_amount), avg_freq = mean(purchase_freq), avg_recency = mean(recency_days), count = n() )模拟案例跑出来大致是这样:簇1是高频高金额的“高价值活跃客”,占比约20%;簇2是低频高金额的“理性大额客”,像买贵价电子产品,平时不常来;簇3是高频低金额的“低价囤货客”,购买频繁但客单价低;簇4是低频低金额且最近购买间隔很长的“沉睡客”。这四个画像一出来,运营策略基本就清晰了——簇4要唤醒,簇1要维护和放大,簇2要促复购,簇3要提客单。
这一步是整个聚类分析里最体现业务判断力的地方。同样的四类画像,不同行业的人看到的运营启示完全不同。技术只是把数据切成块,真正读懂每块的含义才是分析的灵魂。
4. 常见问题与排查技巧实录
4.1 聚类结果不稳定:随机性的来源与对策
我在实操中遇到过不少朋友跑同一份数据,每次聚类结果都不一样,或者自己跑两次结果差别很大。这种不稳定主要来源于K-means的随机初始化和数据顺序。
对策就是我前面提到的set.seed加nstart加大。set.seed固定随机数种子,确保同样的数据每次跑出来的初始中心一致;nstart让算法从多个随机起点各跑一遍,最终选最优的,这样即使不同种子,结果也不会差太多。如果数据量在百万级,K-means跑起来本身就慢,可以考虑用Mini-batch K-means,R里可以用flexclust包,牺牲一点精度换速度,结果稳定性也好一些。
层次聚类没有随机性,结果完全确定,所以对稳定性有硬性要求的场景,我也推荐用层次聚类交叉验证K-means的结论。
4.2 不同量纲导致的聚类偏移:用户画像全乱套
这是新手最容易踩的坑。举一个我对接过的真实场景:某朋友做客户分群,数据里有“历史累计消费”和“最近30天消费次数”两个变量,前者数值范围是几千到几十万,后者是0到10,结果聚类切出来的簇几乎只按“历史累计消费”分层,“最近30天消费次数”完全没能参与区分——这等于你本来想从两个维度看客户,结果一个维度废掉了。
处理方式就一句话:进入聚类前一定要标准化。如果因为业务原因不想动某些变量(比如0/1标志位),至少保证连续数值变量全部标准化。混合类型可以用Gower距离,这个我在距离度量那节提到过,daisy()函数可以直接处理。
4.3 聚类结果难解释:降维、样本量与特征选择的纠偏
有时候聚类跑完了,簇的边界在统计上很清晰,但业务上一句话说不出来这个簇是“什么样的人”。这时候问题通常出在三个地方。
一是特征选择出了问题,可能放进了噪声特征,比如跟业务目标完全无关的ID列、时间戳等。ID列这种是绝对禁用的,它每个样本都是唯一值,聚类会被它搅得面目全非。二是样本量太少或者特征维度太高,高维空间里所有距离都趋于相近,聚类会失去意义。这时候先做特征筛选或PCA降维,再聚类。三是有离散分布的长尾变量,比如少数用户贡献了绝大多数消费金额,这种变量不处理的话会形成“1%用户单独成簇”的畸形结果,可以考虑先取对数或者做分箱处理。
4.4 树状图的阅读指南:别把高度轴当摆设
层次聚类得到树状图后,很多新手只会看图觉得“嗯挺好看的”,然后就不知道下一步干什么了。这里有几个阅读要点。
看高度轴:合并高度越高,说明合并的两个簇差异越大。如果有一条横跨很高位置的合并线,这往往是划分大类的关键位置。看分支长度:分支越长表示该分支的样本越异质。看簇大小:如果某个簇在树状图里如同孤岛一样悬挂,说明这个簇在特征上与其他样本差异非常大。R里rect.hclust()可以画出矩形框标注切割后几个簇,再配合cutree()提取具体每个样本的类别ID:
cluster_labels <- cutree(hc_result, k = 4)cutree函数有h参数按高度切和k参数按类别数切两种用法,业务场景我一般用k,因为是需要确定类别数的,而不是反过来。
4.5 一个容易被忽略的问题:数据顺序影响DBSCAN结果
DBSCAN本身不涉及随机初始化,逻辑上应该是确定的,但如果你发现两次结果不一样,先别怀疑算法,看看数据是否做了shuffle。dbscan包实现里,如果数据排序变化,某些边界点的归属可能变化,尤其当两个簇距离很近时。解决办法是固定数据读取顺序,或者设置好随机种子之后再做任何涉及随机采样的预处理。这类细节问题排查起来很磨人,但知道了也就一句话的事。
5. 聚类分析的方法选择与调参心得
5.1 K-means、层次、DBSCAN选型速查
很多朋友问我到底哪种聚类方法好,我的回答是:没有绝对好坏,只有适不适合。我把选型思路整理成一张表:
| 场景特征 | 推荐方法 | 原因 |
|---|---|---|
| 数据量大(万级以上)、簇近似球形 | K-means | 计算高效,结果稳定 |
| 不知道分几类、想看全局结构 | 层次聚类 | 树状图提供完整层次信息 |
| 簇形状不规则、数据含大量噪声 | DBSCAN | 不预设簇数,自动识别噪声点 |
| 混合类型数据(数值+分类) | 层次聚类+Gower距离 | 距离度量更贴合数据结构 |
| 数据量大且簇形状复杂 | 各方法均不理想 | 先做特征工程,或改用其他方法 |
这个表不是教条,而是帮助你快速锁定方向。实际业务里往往需要多个方法交叉验证:比如用K-means得到一个分群,再用层次聚类跑一遍看有没有相同结论,这种双保险策略能很大程度减少“聚类结果只是算法巧合”的风险。
5.2 不同算法组合使用的实战技巧:K-means粗分+层次细看
我个人的一大心得是聚类方法组合使用比单打独斗效果好得多。以大样本数据为例:先跑K-means粗分成4-5个簇,再从每个簇里随机抽取一些样本做层次聚类,把子簇结构看细。这样既解决了层次聚类在大样本下计算慢的问题,又保留了看层次细节的优势。
还有一种常用组合是先用DBSCAN做离群点检测,然后对剔除了噪声的数据跑K-means正式聚类。DBSCAN标记出来的噪声点在业务上常常就是异常刷单、测试账号等特殊群体,剔除后K-means的结果会干净很多。这个方法我在多个项目里验证过,效果稳定比单一K-means好得多。
5.3 聚类分析送给大家的经验法则:三思而后行
最后分享几个纯经验层面的建议。
第一,聚类不是流水线式的按钮操作,每一步都要带着业务问题去想。“我到底想分出什么样的群体”“分出来之后运营上能怎么用”,这两个问题如果一开始没想清楚,结果大概率会失效。
第二,不要盲目追求轮廓系数最高。统计指标只是参考,业务可解释性永远是第一位的。我见过一个案例,轮廓系数最高时分的5类里有两类业务上完全无法解读,反而是略低一点的4类方案非常贴合运营场景。这就是死磕指标和真正做事的区别。
第三,聚类需要对结果保持怀疑的态度。同一个业务问题,换一种距离度量、换一种聚类方法,结果可能完全不同。多做几次尝试、多交叉验证,得到的结论才经得住推敲。
第四,保存聚类的全部过程——包括随机种子、标准化参数、特征版本,重现分析与持续迭代都需要这些记录。分析结果本身有时候反而是最不重要的。
做数据分析这几年,我最大的感受是聚类分析从来不是一个纯粹的统计学题目,它更像是统计学和业务洞察之间的一座桥。算法负责用数学方式寻找数据中的结构,而分析者的职责是理解这些结构背后的业务逻辑。对于刚开始接触的人来说,从K-means学起是最稳妥的路径,因为它简单快速、覆盖面广;等把整个流程跑熟了,再从层次聚类和DBSCAN里感受不同方法的特色。这套思路和代码,希望能帮你少走一些我走过的弯路。