聚类分析原理与R语言实战:K-means、层次聚类、DBSCAN全解析
2026/9/15 17:49:37 网站建设 项目流程

最近把聚类分析这套东西重新系统地过了一遍,从算法原理到R语言实现,再到实际业务场景里的应用,踩了不少坑,也理清了很多以前模棱两可的概念。这篇是数据分析学习总结笔记的第2篇,专门讲聚类分析以及它在R语言里的落地方式。如果你正在学数据分析,或者工作中需要做用户分群、商品归类、异常检测这类事情,这篇文章应该能帮你省不少时间。文里的代码都是可以直接复制运行的水平,环境用RStudio就行,数据我会用内置数据集和模拟数据来讲,不需要额外找数据源。

1. 先搞清楚聚类分析到底是干嘛的——它和分类根本是两回事

很多人刚接触聚类时,最容易混淆的就是"聚类"和"分类"。我当时学的时候也绕了一阵子,后来发现只要抓住一个核心区别就通透了:分类是监督学习,聚类是无监督学习。

什么意思?分类是你手里已经有一批打好了标签的数据,比如一万条用户记录,里面已经标好了哪些是高价值用户、哪些是普通用户、哪些是流失风险用户,你要做的是训练一个模型,让它在面对新用户时能自动归入其中某一类。整个过程是"有标准答案"的。

聚类则完全反过来。你手里只有一堆数据,没有标签、没有答案,甚至到底该分成几类你都不知道。你要做的,是让算法根据数据本身的特征分布,把"长得像"的样本自动归到一起,让"不太像"的样本分开。聚类结果好不好,也没有一个绝对的对错,更多是看它在业务上能不能解释得通。

这个区别决定了聚类分析在很多场景里是"探索性"的工具,它的核心价值不是预测,而是帮我们发现数据内在的结构。

举几个实际例子你就有感觉了:

  • 用户分群:电商平台把用户按消费金额、购买频次、浏览时长等特征分组,找出"高价值活跃用户""价格敏感型用户""沉睡用户"等不同群体,然后针对每个群体做不同的运营策略。这是聚类在商业数据分析里最常见的应用,也是问得最多的场景。
  • 商品归类:零售企业根据商品的销售曲线、价格区间、库存周转率等特征,把几千个SKU自动归成几大类,辅助采购和定价决策。
  • 异常检测:通过聚类找出远离所有簇中心的数据点,这些点往往是刷单行为、设备故障、欺诈交易等异常情况。
  • 文本主题归纳:把一堆新闻文档按词频特征聚合,自动发现几个热点话题簇,这是文本挖掘里常用的冷启动手段。

在这些场景里,你都没有"标准答案",只能靠数据的几何特征说话。所以在正式动手写代码之前,我强烈建议你先想明白一个问题:你手里的数据,分完组之后是要给谁看、拿来做什么决策?这会直接决定你后面选什么算法、分几类、怎么解读结果。

2. 常用聚类算法对比:K-means、层次聚类、DBSCAN到底该怎么选

R语言里能实现聚类分析的包和函数非常多,但万变不离其宗,常用的就三大流派:基于划分的K-means、基于层次的hclust、基于密度的DBSCAN。我一个个说清楚它们的原理、优点和致命短板。

2.1 K-means:最经典,但也最容易栽跟头

K-means的核心思想非常朴素。先指定一个K值,也就是你想把数据分成几簇,然后算法会随机选K个点作为初始中心,接着反复执行两步:第一步,把每个样本分配到离它最近的中心点所在的簇;第二步,重新计算每个簇的中心(也就是该簇所有点的均值),再重复第一步。直到中心点不再移动,或者迭代次数用尽,聚类就完成了。

我当年第一次学这个算法时,总觉得"就这么简单?"实际上它确实原理简单,但有两个前提条件经常被忽略:

第一,K值必须由你事先指定。算法自己不会告诉你该分成几类,你给3就出3簇,给5就出5簇。K值选得好不好,直接决定聚类结果的业务价值。后面第6章我会专门讲怎么选K,这里先留个钩子。

第二,K-means对初始中心点非常敏感。不同的随机初始点,可能收敛到不同的局部最优解。这也是为什么R语言的kmeans函数里有个nstart参数,让你从多个随机起点中挑一个最优结果——这个细节真的会决定聚类结果的可复现性,实战中特别重要。

K-means的适用场景是:数据量较大(几万到几十万条都没问题)、样本特征维度适中、类别大致呈球形分布、簇与簇之间区分明显。它最大的短板是对离群点非常敏感,几个异常值就能把簇中心拉偏,还有就是它对非球形簇(比如月牙形、环形)效果极差。

2.2 层次聚类:不需要预先指定K,但计算量感人

层次聚类是另一条完全不同的路线。它不要求你先定K值,而是递归地把数据合并或分裂,最终生成一棵树状结构,你随时可以从这棵树里"切"出你想要的分类数。

具体来说有两种方向:

  • 自底向上(凝聚式):一开始每个样本单独成一簇,然后每次找距离最近的两个簇合并,不断重复直到最后只剩一簇。
  • 自顶向下(分裂式):从所有样本一个整体开始,不断把最不相似的簇拆开,直到每个样本单独成簇。R语言里hclust函数默认实现的是凝聚式。

层次聚类的最大优点就是不需要猜K值,你聚类完之后看树状图(dendrogram),眼睛看到该切的地方切一刀就行,非常直观。而且它不限于球形簇,能处理很多不规则形状。

但代价是计算复杂度高。我拿3万条用户数据跑过一次hclust,跑了差不多两分钟还没出来,而且内存占用非常大。所以如果你数据量超过两三万条,我建议谨慎使用层次聚类,或者先对数据进行抽样再去聚类。另外,层次聚类一旦某个合并或分裂步骤做了错误决定,后面是没有回头路可走的,这跟K-means迭代优化的机制不一样,所以它对距离计算方式比较敏感。

2.3 DBSCAN:密度聚类,专治各种不规则形状

DBSCAN的思路跟前面两种完全不同。它不看距离中心,而是看密度。核心就两个参数:邻域半径eps和最小样本数minPts。以此为基础,把样本分成三类:核心点(邻域内样本数超过minPts)、边界点(落在核心点邻域内但不是核心点)、噪声点(既不满足核心点也不在核心点邻域内的点)。

DBSCAN最厉害的地方有三个:

  • 不需要指定簇的数量,它自己会找
  • 能识别任意形状的簇,月牙形、环形、凹形都行
  • 天然能发现离群点,被标为噪声点的就是

它的缺点是参数不好调,尤其是eps。设大了,本来该分开的簇会被连到一起;设小了,一个簇会被拆成好几个碎块。我自己的经验是,eps选多少没有一个通用办法,基本靠看K距离图去做判断,这部分在R里没有特别现成的函数,经常要自己动手画。另外,如果数据里各个簇的密度差异非常大,DBSCAN的表现也不会太好,因为一个全局eps没法同时适配不同密度的区域。

2.4 怎么选:从业务需求倒推

学了三个算法以后肯定有人会问:那我到底用哪个?我个人的选择逻辑是:

判断条件推荐算法
数据量大(>3万),簇近似球形,有业务先验知道大概分几类K-means
数据量小(<1万),想直观看到层次关系,不确定分几类层次聚类
簇形状不规则,数据里有大量噪声/离群点DBSCAN
快速跑通一个基线结果,给业务方看个大概K-means(K取3~5)

这里多说一句,这三种算法不是互相排斥的,我经常会在一个项目里配合用:先用层次聚类在小样本上摸清大概分几类合适,再用K-means在全量数据上跑正式结果,最后用DBSCAN去筛查那些离群点。组合拳打下来,效果往往比单用任何一个都稳。

距离度量这一点也值得提。不管是K-means还是层次聚类,前提都是能算出"样本之间的距离"。最常用的是欧氏距离,直接算特征空间里的直线距离,适合连续型数值特征;曼哈顿距离则是各维度绝对差之和,适合某些特殊的业务场景;余弦相似度更适合文本和高维稀疏向量,R语言里用cosine函数或者在proxy包里指定method="cosine"。

3. R语言环境准备:从安装到数据预处理,这些坑一定要提前避开

说是学习笔记,但代码能不能跑通才是硬道理。先把环境搞定,再做数据预处理,不然直接上手聚类函数,很容易出现报错,而且报错信息还看不懂。

3.1 R和RStudio的安装

如果你用的是Windows,直接去R语言官网下载安装包安装即可,这个没什么好说的。需要提醒的是R语言官网默认给的是二进制安装包,下载速度有时候比较慢,国内可以找镜像站点,一般高校或云厂商都有提供。

如果你用的是macOS,这里有一个特别容易踩的坑:下载了R安装包之后,双击安装完,打开R控制台,输入install.packages("factoextra"),极有可能会报错说找不到某个编译环境。这是因为R里不少包(比如factoextra依赖的类)需要从源码编译,而macOS上默认没有装gfortran编译器,就会卡在安装这一步。解决办法是去编译器官网单独下载gfortran,装好后再重启RStudio。我当时在这个问题上卡了快一个小时,才明白R报错里的"compiled from source"是什么意思。

RStudio是R的IDE,本质上就是给R套了个好用的编辑器和可视化管理界面,日常写脚本、看变量、画图都离不开它。装好R之后再去RStudio官网下载一个对应系统的安装包,装完打开RStudio,它会自动识别你系统里的R。

装完以后,在RStudio的控制台里执行以下命令,把接下来要用的包一次性装齐:

install.packages("cluster") # 轮廓系数等聚类评估 install.packages("factoextra") # 聚类可视化,太好用了 install.packages("dplyr") # 数据操作 install.packages("ggplot2") # 绘图 install.packages("readr") # 数据读取

3.2 数据读入和基础清洗

装包完成只是万里长征第一步。真正做聚类分析时,你会发现80%的时间都花在数据清洗与预处理上,真正跑聚类就那一行函数的事。

先读数据。R语言里读csv最顺手的是readr包的read_csv,速度比基础函数read.csv快不少,而且不会把字符串默认转成因子,省去很多麻烦:

library(readr) # 假设你有一个用户行为数据文件 data <- read_csv("user_behavior.csv") # 快速看一下数据结构 str(data) # 每一列的类型和样例 summary(data) # 数值列的最小值、最大值、均值、缺失值情况 head(data) # 前几行数据长什么样

数据读进来以后,第一件事不是直接聚类,而是检查数据质量。我会按顺序查这几样:

检查缺失值。R语言里缺失值用NA表示,kmeans函数遇到NA会直接报错或者返回的结果缺失那一行,所以必须处理。处理方式无非两种:删掉含缺失值的行,或者用均值/中位数填充。哪种好要看缺失比例,如果缺失行占比低于5%,直接删掉就行;如果比较高,建议用median(data$列名, na.rm=TRUE)这种带na.rm参数的方式计算中位数去填,因为均值容易受离群点影响。

检查量纲差异。这一步是最关键的,但也是新手最容易忽略的。假如你的数据里有一列叫"消费金额",范围是0到50000,另一列叫"购买次数",范围是1到50,如果你直接拿这两列去算欧氏距离,消费金额这一列会完全主导距离计算,购买次数那列基本不起作用。解决办法就是标准化,R语言里一行代码:

# 只对数值列做标准化 data_scaled <- scale(data[, c("amount", "frequency", "avg_order_value")]) # 标准化后每一列均值接近0,标准差为1 # 可以验证一下 colMeans(data_scaled) # 接近0 apply(data_scaled, 2, sd) # 接近1

scale函数做的事情很简单:对每一列,先减去这列的均值,再除以标准差。标准化之后,所有特征都在同一个尺度上,才不至于某一个维度单独称王。

检查离群点。K-means对离群点敏感,所以在跑聚类前,我一般会用箱线图或者直接看各个数值列的分位数,把明显异常的值找出来。不是说一定要把离群点删掉,而是你要心里有数,知道它们在哪些位置,后面聚类时它们很可能单独成一簇,或者把簇中心拉偏。判断离群点可以用:

# 看各列的分位数,判断是否存在极端值 summary(data[, c("amount", "frequency")]) # 或者画箱线图 boxplot(data$amount, main="消费金额分布")

如果是明显的录入错误(比如消费金额为负),直接处理掉;如果是真实存在的少数高价值用户,我建议先保留,等聚类结果出来再看它们落到了哪个簇,这样反而能帮你发现一些"高价值特殊群组"。

4. K-means聚类的R语言实现:完整代码与参数调优

环境装好、数据洗干净之后,就可以正式开始聚类了。这一章我用一套完整的流程把K-means跑通,然后把nstart、迭代次数这些容易被忽视的参数讲透。

4.1 先用内置数据集跑一个最小可运行版本

R语言里有一个非常经典的内置数据集iris(鸢尾花数据),150条样本、4个特征、3个品种,做聚类演示再合适不过。我们用这个数据跑一个最小版本的K-means:

# 加载iris数据 data(iris) # 只取4个数值特征列,去掉品种标签列 iris_features <- iris[, 1:4] # 数据标准化 iris_scaled <- scale(iris_features) # 指定聚类数为3,设置随机种子保证结果可复现 set.seed(123) km_iris <- kmeans(iris_scaled, centers = 3, nstart = 25) # 查看聚类结果的基本信息 km_iris

运行完以后,kmeans对象里包含几个关键的字段,我逐个解释一下:

  • km_iris$cluster:每个样本被分到哪个簇(1到3之间的一个整数)
  • km_iris$centers:每个簇的中心点坐标,标准化后的值
  • km_iris$size:每个簇的样本数量
  • km_iris$tot.withinss:簇内平方和,衡量样本离自己簇中心的距离总和,这个值越小说明簇内越紧凑
  • km_iris$betweenss:簇间平方和,衡量不同簇中心之间的分离程度,越大说明簇之间分得越开

4.2 给聚类结果画图:把簇画出来才算完事

跑完聚类不画图,等于白做。因为聚类的最终目的是给人看的,一张清晰的散点图比任何数据表格都有说服力。这里我强烈推荐factoextra包的fviz_cluster函数,它会把高维数据用PCA降到二维,然后把每个簇的分布画出来:

library(factoextra) # 可视化聚类结果 fviz_cluster(km_iris, data = iris_scaled, palette = "Set2", ellipse.type = "norm", ggtheme = theme_minimal())

这张图出来以后,你能立刻看出来三簇分得干不干净。如果簇之间有大量重叠,说明样本特征区分度不够,或者K值选得不太合适;如果几个簇干净利落地分开,那这个聚类结果就很有说服力。

如果想用ggplot2自己画二维散点图(适合只有两个特征或者想自定义样式的时候),代码也很简单:

library(ggplot2) # 将聚类结果和原始数据合并成一个数据框 plot_data <- as.data.frame(iris_scaled) plot_data$cluster <- as.factor(km_iris$cluster) ggplot(plot_data, aes(x = Sepal.Length, y = Sepal.Width, color = cluster)) + geom_point(size = 2, alpha = 0.7) + labs(title = "K-means聚类结果(K=3)") + theme_minimal()

4.3 nstart参数为什么重要:一个真实案例

很多人第一次用kmeans函数时,都是直接写kmeans(data, centers=3),根本不加nstart。后来他发现同一个数据,每次跑出来的结果居然不一样,就开始怀疑代码有bug。这不是bug,而是K-means的初始中心点是随机的,不同起点收敛到的解不同。

kmeans函数里的nstart参数的意思是:让我从nstart个不同的随机初始中心点出发,分别跑K-means,最后选出簇内平方和最小的那个作为最终结果。nstart设成25或50是常见的做法,多花一点点时间,换来的是稳定得多、好得多的聚类效果。

另外再强调一次set.seed(123)。这是为了固定随机数种子。也就是说,你在自己的电脑上设置了种子,跟我这边跑的随机序列完全一样,最终结果也完全一样。写教程、做复现、跟别人对结果,这个种子一定要设置,不然你换了台电脑或换了个时间跑,结果就会变。

4.4 K-means的可视化优化:簇中心和距离

聚类图除了散点之外,把簇中心画上去会更有信息量。我们可以手动计算并画出来:

# 看看簇中心长什么样 km_iris$centers # 对于二维特征,可以直接把中心点添加到ggplot图里 center_df <- as.data.frame(km_iris$centers) center_df$cluster <- rownames(center_df) ggplot(plot_data, aes(x = Sepal.Length, y = Sepal.Width, color = cluster)) + geom_point(size = 2, alpha = 0.5) + geom_point(data = center_df, aes(x = Sepal.Length, y = Sepal.Width, color = cluster), shape = 8, size = 5, stroke = 2) + labs(title = "K-means聚类结果及簇中心位置") + theme_minimal()

画出来之后就能很直观地看到,三个菱形点基本落在对应簇的中央位置。簇中心的含义不仅是几何中心,还能反过来指导业务:比如用户分群后,算出每个簇的消费金额均值、活跃天数均值,这个均值向量就是该簇用户的"画像",你可以据此给不同群组起名字并制定策略。

4.5 K-means实战里最容易踩的三个坑

第一个坑,数据没标准化就直接聚类。前面讲过量纲问题,这里再强调一次。如果数据里有一列是"消费金额"(几百到几万),另一列是"点击次数"(个位数),没标准化时聚类结果基本只看消费金额。很多初学者跑完聚类发现簇和簇之间没法解释,一查就是栽在这。

第二个坑,K值瞎猜。有人直接拍脑袋定K=3,结果业务方怎么看怎么不合理。K值没有一个公式可以直接算出来,但后面第6章我会讲怎么用肘部法则和轮廓系数系统性地去选,而不是靠运气。

第三个坑,不设随机种子。同一个脚本跑两次结果不一样,你自己都解释不清,跟别人协作时更是一团乱麻。记住一条纪律:凡是涉及随机过程的代码,第一行就是set.seed。

5. 层次聚类的R语言实现:树状图解读与实操细节

层次聚类虽然在处理大数据量时吃力,但你架不住它直观。尤其是面对小样本探索性分析的时候,画一张树状图,谁跟谁近、谁跟谁远,一眼就能看明白。这一章把hclust的完整使用流程讲清楚。

5.1 用hclust实现层次聚类:最短代码

还是用iris数据,只需要三行核心代码:

# 1. 计算样本之间的距离矩阵 dist_matrix <- dist(iris_scaled, method = "euclidean") # 2. 用Ward方法做层次聚类 hc <- hclust(dist_matrix, method = "ward.D2") # 3. 画树状图 plot(hc, labels = FALSE, main = "层次聚类树状图")

这里有几个关键的细节需要单独解释。

dist函数计算的是任意两个样本之间的距离,得到的dist_matrix是一个下三角矩阵,存储了所有样本两两之间的欧氏距离。method参数还可以换"manhattan"(曼哈顿距离)、"maximum"(切比雪夫距离)等,缺省是欧氏距离。

hclust的method参数有很多种选择,最常用的是"ward.D2",它的原理是:合并两个簇时,让合并后簇内离差平方和的增加量最小。用Ward方法得到的聚类,簇与簇之间边界通常很清晰,形状也比较紧凑,适合大多数业务场景。其他还有"complete"(最长距离法)、"average"(平均距离法)等,"complete"对离群点更稳健,"average"则介于两者之间。我的实践经验是:拿不准的时候,默认用ward.D2基本不会出大错。

5.2 树状图怎么看,以及怎么从树上"砍"出簇

树状图里,最底部的每个小分支代表一个样本,往上逐渐合并成大分支,合并点的高度代表这两个簇之间的距离远近,高度越低,说明它们越相似。

那么怎么决定分几类呢?你的眼睛就是最好的工具。找一根横线去切树状图,这根横线从上往下移动,碰到几根竖线,就代表分了几类。在实际操作里,一般是看树状图里"合并高度落差比较大的那条缝",在缝的地方横着切一刀,就是比较自然的分类方式。

代码层面,用cutree函数实现这个"切"的动作:

# 把树切成3个簇 hc_clusters <- cutree(hc, k = 3) # 查看前几个样本被分到了哪个簇 head(hc_clusters) # 对比一下真实的iris品种标签 table(hc_clusters, iris$Species)

table输出就是一个小矩阵:行是层次聚类分出来的3个簇,列是真实的3个品种。你就能看到聚类结果和真实标签大概有多少重合,这是评估聚类效果时很直接的手段。

5.3 树状图的美化与标签优化

直接plot出来的树状图默认带样本标签,如果样本很多,标签会密密麻麻挤成一团。实际做报告时我一般会去掉标签,同时用rect.hclust函数在图上直接框出各个簇:

plot(hc, labels = FALSE, main = "层次聚类结果(切为3簇)", xlab = "样本编号", ylab = "高度") rect.hclust(hc, k = 3, border = c("#E7298A", "#66A61E", "#E6AB02"))

加了边框之后,这张图拿去给业务方看,他们一眼就能看懂"系统把样本分成了三大块,每块内部比较相似,块和块之间差异比较大"。

5.4 层次聚类选K的经验:先看树状图,再验证

在不知道K值该取多少时,层次聚类最大的价值就是帮你"侦察地形"。用层次聚类在小样本(比如随机抽5000条)上画出树状图,看看自然的分簇形态,然后拿着这个K值去全量数据上跑K-means,两个算法互相验证,得到的结论会更扎实。

我自己有个习惯:先用hclust对抽样数据做一次快速聚类,画完树状图基本就对"分几类"有数了。如果树状图里从第2个分支开始高度落差就很大,说明分2类极好;如果落差出现在第4、第5个分支,那分4类或5类更有业务意义。这种"视觉探索先行"的思路,比直接对着K值范围跑一大堆实验要高效得多。

6. 聚类数K的选择与聚类质量评估:肘部法则、轮廓系数和业务解释性的三角验证

聚类分析最纠结的问题永远是:到底该分几类?K值取少了,信息丢失严重;取多了,簇之间差异不明显,业务方看了头大。这一章我讲三种成体系的方法,你配合着用,基本能锁定一个合理的K值。

6.1 肘部法则:画一条弯折曲线,找那个"拐点"

肘部法则的原理很简单。K值越大,簇内样本离自己簇中心越近,簇内平方和(WSS)理论上会一直下降。但问题是,当K超过某个值后,再增加K带来的"簇内紧凑度提升"幅度会急剧变小。把K值和对应的WSS画成折线图,曲线会出现一个像手肘一样的拐点,这个拐点就是相对合理的K值。

在R里手写这个逻辑其实很清晰:

# 尝试K从1到10,分别计算总簇内平方和 wss <- numeric(10) for (k in 1:10) { set.seed(123) km <- kmeans(iris_scaled, centers = k, nstart = 25) wss[k] <- km$tot.withinss } # 画肘部法则图 plot(1:10, wss, type = "b", xlab = "聚类数K", ylab = "总簇内平方和(WSS)", main = "肘部法则:WSS随K值的变化")

画出来以后你会发现,WSS曲线从K=1到K=3下降得非常陡,过了K=3之后曲线就开始变得平缓。这个"在3附近拐弯"的信号,就是在告诉你:分3类可能就够了,再往下分收益不大。

需要说明的是,肘部法则的"拐点"经常是不明显的,尤其在真实业务数据里曲线可能是平滑下降的,这个时候就要结合其他指标来综合判断了。

6.2 轮廓系数:量化每个样本分得准不准

轮廓系数解决的遗憾是:WSS只能看整体,看不出每个样本的分类可靠性。它的计算方法是,对每个样本i:

  • 计算它到所在簇其他所有样本的平均距离,记作a(i),代表"簇内不相似度"
  • 计算它到最近的另一个簇所有样本的平均距离,记作b(i),代表"与最近邻簇的不相似度"
  • 轮廓系数s(i) = (b(i) - a(i)) / max(a(i), b(i))

如果s(i)接近1,说明样本i离自己的簇很近、离其他簇很远,分类很可靠;接近0,说明样本i在簇边界上,模棱两可;接近-1,说明它可能被分错了簇。

把所有样本的轮廓系数取平均,就得到整体轮廓系数。R语言里用cluster包的silhouette函数:

library(cluster) # 计算当前聚类结果的轮廓系数 sil <- silhouette(km_iris$cluster, dist(iris_scaled)) # 提取平均轮廓系数 mean(sil[, 3])

在K值选择上,这组代码会非常有用:

# 尝试K从2到8,计算每个K对应的平均轮廓系数 avg_sil <- numeric(7) for (k in 2:8) { set.seed(123) km <- kmeans(iris_scaled, centers = k, nstart = 25) sil <- silhouette(km$cluster, dist(iris_scaled)) avg_sil[k-1] <- mean(sil[, 3]) } # 看哪个K的轮廓系数最高 names(avg_sil) <- 2:8 avg_sil

平均轮廓系数的取值范围是-1到1,通常在业务数据里能到0.3以上就已经算不错了,超过0.5属于结构明显。如果某个K值对应的平均轮廓系数明显高于其他K,这就是一个非常有说服力的选择依据。

6.3 聚类结果的业务解释性:所有指标最终服务于决策

有一次我给一个电商项目做用户分群,肘部法则说K=4合适,轮廓系数说K=3最高,最后我们却选了K=5。为什么?因为K=5的时候,每个簇都能对应一个清晰的业务标签:高价值活跃用户、价格敏感型用户、沉睡用户、新用户、流失风险用户。这5个群组分别对应不同的运营动作,领导看了直点头。

反过来,如果某个K值虽然统计指标很好,但分出来的簇根本没法用一句业务语言概括,那这个聚类结果就是"统计上漂亮、业务上没用"。

所以我的建议是:先用肘部法则缩小K的范围,再用轮廓系数判断候选K里哪个在数据结构上更自然,最后套进业务场景里验证能不能解释得通。这三个维度合在一起,比任何单一指标都可靠。

还值得多提一句的是,fviz_cluster函数配合轮廓系数一起看,效果很好。fviz里可以直接画轮廓系数的棒棒糖图:

fviz_silhouette(sil)

每根柱子的高度代表样本的轮廓系数,柱子颜色代表它属于哪个簇。如果某个簇里大多数样本的柱子都很矮甚至成负数,说明这个簇可能分得不合理,考虑调整K值或特征选择。

7. 实战案例:电商用户消费数据聚类(R语言全流程演示)

学了这么多,我们来一个综合案例,模拟一次完整的电商用户分群。这个案例会串起前面所有步骤:数据构造、预处理、K值选择、聚类执行、结果可视化、业务解读。

7.1 构造模拟数据:三个特征,三个隐藏群组

先读取构造好的数据。这里我直接模拟生成一份电商用户消费行为数据,包含1000个用户,每个用户有3个特征:月均消费金额(amount)、月均购买次数(frequency)、平均客单价(avg_order_value)。为了让聚类效果更明显,我让数据来自三个不同的"真实群组":

set.seed(42) # 模拟用户数据 n <- 1000 group1 <- data.frame( amount = rnorm(n/3, mean = 3000, sd = 400), frequency = rnorm(n/3, mean = 15, sd = 3), avg_order_value = rnorm(n/3, mean = 200, sd = 30) ) group2 <- data.frame( amount = rnorm(n/3, mean = 800, sd = 150), frequency = rnorm(n/3, mean = 5, sd = 1.5), avg_order_value = rnorm(n/3, mean = 160, sd = 25) ) group3 <- data.frame( amount = rnorm(n/3, mean = 1500, sd = 250), frequency = rnorm(n/3, mean = 3, sd = 1), avg_order_value = rnorm(n/3, mean = 500, sd = 60) ) # 合并并打乱顺序 data <- rbind(group1, group2, group3) data <- data[sample(1:n), ] rownames(data) <- NULL head(data)

这份模拟数据的设计很刻意:第一群组是"高频中客单"的活跃用户,第二群组是"低频低客单"的价格敏感型用户,第三群组是"低频高客单"的奢侈品型用户。三个群组的真实边界并不完全清晰,还带一定噪声,正好用来检验聚类算法能不能把它们大致找回来。

7.2 标准流程:预处理、选K、跑聚类

按前面讲的标准流程走一遍:

# 1. 标准化 data_scaled <- scale(data) # 2. 用肘部法则初步判断K值范围 wss <- numeric(10) for (k in 1:10) { set.seed(123) km <- kmeans(data_scaled, centers = k, nstart = 25) wss[k] <- km$tot.withinss } plot(1:10, wss, type = "b", xlab = "K", ylab = "WSS") # 3. 用轮廓系数进一步验证K=3 avg_sil <- numeric(8) for (k in 2:9) { set.seed(123) km <- kmeans(data_scaled, centers = k, nstart = 25) sil <- silhouette(km$cluster, dist(data_scaled)) avg_sil[k-1] <- mean(sil[, 3]) } names(avg_sil) <- 2:9 avg_sil

运行完这组代码,你大概率会看到轮廓系数在K=3时最高,因为它背后的数据就是三群生成的,这其实是在验证整个流程的正确性。当然实际业务里没有"真实标签"可对照,但这个流程是对的。

7.3 跑正式聚类并输出画像

确定K=3后,跑正式聚类,并把每个簇的特征均值还原到原始量纲,方便业务解读:

# 正式聚类 set.seed(123) final_km <- kmeans(data_scaled, centers = 3, nstart = 25) # 把聚类标签合并回原始数据 data$cluster <- final_km$cluster # 按簇计算各特征均值(原始数据量纲) library(dplyr) cluster_profile <- data %>% group_by(cluster) %>% summarise( count = n(), avg_amount = mean(amount), avg_frequency = mean(frequency), avg_order_value = mean(avg_order_value) ) cluster_profile

输出结果大概长这样:

clustercountavg_amountavg_frequencyavg_order_value
1330302515.1201
23358024.9158
333515053.1502

这时候每个簇的业务画像就非常清晰了:

  • 簇1:月均消费3000+,购买15次,客单价200左右。这是"高频次稳定消费"的核心用户群,日常走量,适合做会员积分、满减促销维持活跃。
  • 簇2:月均消费800左右,购买5次,客单价160左右。整体消费力偏低,属于价格敏感型用户,可以针对性发低频大额券或搭配低价商品促活。
  • 簇3:月均消费1500左右,购买次数仅3次,但客单价高达500。典型"低频高客单"用户,对价格不敏感,适合推送新品、高附加值商品,不需要频繁打扰。

7.4 画图让结果更直观

最后用fviz_cluster把三个簇画出来:

fviz_cluster(final_km, data = data_scaled, ellipse.type = "norm", palette = "Set1", ggtheme = theme_minimal(), main = "电商用户消费行为聚类结果")

图上一共三个点群,基本能看出簇1和簇2在距离上比较近,簇3明显隔得远一些。这个"簇间距离"本身也传递了业务信息:高频用户和价格敏感用户在行为习惯上更接近,高客单用户则自成体系,运营策略需要彻底分开。

8. 学习过程中的常见坑与经验总结:这些教训花了大量时间才想明白

最后这部分我想把学习聚类分析过程中踩过的坑集中整理一下,这些坑有的是代码层面的,有的是方法论层面的,希望你能直接绕开。

8.1 代码层:报错和结果异常的常见原因

问题一:聚类结果每次都不一样。原因就是没设随机种子。kmeans和很多聚类算法都有随机初始化这一步,不设种子的话,每次运行的初始点不同,结果自然不同。解决方法是跑之前先set.seed,设哪个数字都行,但一定要设。

问题二:数据里有NA值,算法一跑就报错。K-means和hclust遇到缺失值都不会自动跳过,而是直接报错或者返回NA。解决办法很简单,在用scale函数之前先做一次完整数据检查:

# 查看每一列的缺失值数量 colSums(is.na(data))

如果发现某些列缺失值特别多,先要想想这列特征是不是真的有必要保留。如果缺失值不多,可以用中位数填充,也可以直接删掉有缺失值的行。

问题三:标准化之后聚类结果还是不对,反而更差了。这种情况多半是数据里有离群点或者某些特征是分类变量。分类变量不能直接放进欧氏距离计算里,比如性别、城市这类值,你需要先转成数值(比如用model.matrix做独热编码),或者干脆剔除后再聚类。如果离群点太严重,可以先单独把离群点抽出来分析,再对剩下的样本聚类。

8.2 方法论层:聚类结果失效的系统性原因

第一,特征选得不对,聚类结果一定不对。我见过一个案例,拿用户ID和注册日期这类完全不含分群信息的特征去做聚类,结果自然是一团糟。聚类分析里,特征选择比算法选择更影响最终效果。选择特征时要想清楚:这些特征能不能刻画样本之间的"相似性"?它们在业务上是否能够形成差异化描述?如果特征本身区分度低,换什么算法都没用。

第二,聚类前必须做标准化,但标准化方式要因场景而异。有时候你对不同特征做标准化以后,某些特征的区分度反而被稀释了,比如一个特征虽然有量纲差异但本身业务含义重大,你就得考虑要不要给它额外加权。基础scale函数是全自动的,想要手动加权的话,可以在标准化后乘一个权重系数。

第三,K值永远没有"正确答案"。不要指望任何算法给你一个上帝视角的最优K。K值的选择本质是一个业务决策,统计指标只能缩小范围,最终要由业务方拍板。

第四,聚类结果一定要用业务逻辑去验证。如果分出来的簇里,某个簇的消费均值显著高于其他簇但购买频次特别低,你要去确认这批用户到底是什么人,是不是代购、企业采购、还是数据异常。聚类只是给你一个"数据视角的切分",真正的解释工作还在人。

第五,多算法交叉验证。我强烈建议同一个数据集上至少跑两种算法,如果K-means分3簇和层次聚类分3簇的样本归属基本一致,那这个聚类结构就比较稳。如果两种算法结果差异巨大,说明数据本身没有清晰的簇结构,再执着于调参意义不大,这时候应该回去重新审视特征或者扩大样本范围。

8.3 跟SPSS、Python等其他工具怎么衔接

热词里经常有人搜SPSS聚类分析和Python数据分析,我简单说下我的感受。

用SPSS做聚类和用R做聚类,算法原理是一模一样的,只是操作界面上不同。SPSS好处是点菜单就能出结果,对不写代码的人友好,但缺点也很明显:流程固定、参数控制不够细、批量处理困难。R的优势在于全程可复现、可视化能力强、可以自由调整算法细节。我自己的使用习惯是:小规模数据演示用SPSS或R都行,但正式项目一定会用R或Python跑,因为需要留下完整的代码记录方便回溯。

Python那边主要用scikit-learn的KMeans、AgglomerativeClustering、DBSCAN,语法比R要更面向对象一些。如果你已经在用Python做数据处理,用scikit-learn做聚类完全没问题;如果你本身就在R环境里做统计分析,直接用R语言就好,没必要为了"流行"硬换Python。聚类分析这个领域,你熟悉的工具就是最好的工具,关键是理解算法逻辑和数据预处理思路,语言只是一个外壳。

这一篇聚类分析写到这里就结束了。下一篇我打算写关联规则和协同过滤在推荐系统里的应用,如果大家对这次的内容有什么疑问,欢迎在评论区提出来,我看到了会尽量回复。

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

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

立即咨询