首页 / 资讯中心 / 文章详情

R语言聚类分析实战:从K-means到DBSCAN的完整指南

R语言聚类分析实战:从K-means到DBSCAN的完整指南 ★ FEATURED ARTICLE
1. 为什么聚类分析值得用R来啃很多人第一次接触聚类是在Excel里拖拽几下鼠标看着散点图凭感觉画圈。数据量小的时候这招管用一旦维度上去、样本量过千肉眼分堆就彻底失效了。聚类分析的本质是让算法根据样本之间的“相似程度”自动把数据划分成若干组组内尽量抱团、组间尽量拉开。它属于无监督学习也就是说你手里没有标签没人告诉你哪个样本属于哪一类全靠数据自身的结构说话。R语言在这个领域的位置很特殊。它不像Python那样在工程部署上占绝对优势但R的统计基因决定了它在聚类算法的覆盖面上极其完整——从最经典的K-means、层次聚类到基于密度的DBSCAN、基于分布的GMM再到模糊聚类、谱聚类几乎每一种方法都有成熟且经过大量实践检验的包。更关键的是R的可视化能力让聚类结果的解读变得直观这对探索性数据分析来说太重要了。这篇文章面向的是已经会用R做基本数据处理、但聚类实操经验不多的读者。我会把重心放在“怎么选方法”“怎么定参数”“怎么判断结果靠不靠谱”这三个实际问题上而不是把每个函数的参数表抄一遍。读完之后你应该能拿到一份自己的数据独立走完从预处理到结果解读的完整链路。提示聚类不是万能的。如果数据本身没有内在的分组结构强行聚类只会得到一堆没有意义的划分。动手之前先想清楚业务问题比选什么算法更重要。2. 动手之前先把数据这关过了2.1 距离度量决定了聚类的“世界观”聚类的底层逻辑是“距离近的归为一类”所以距离怎么定义直接决定了最终的分组形态。最常见的欧氏距离适合连续型变量且各维度量纲一致的情况。但如果你的数据里混了类别变量或者某些维度数值范围差异巨大欧氏距离就会被大数值维度主导。我见过太多人拿到数据直接kmeans()结果分出来的类完全被某一个量纲大的变量绑架。解决办法有两个一是标准化二是换距离度量。标准化用scale()就能搞定把每个维度变成均值0、标准差1。距离度量方面dist()函数支持euclidean、manhattan、maximum、binary等多种选项类别变量多的时候可以考虑Gower距离cluster包里的daisy()函数就是干这个的。# 标准化示例 data_scaled - scale(data_numeric) # 计算距离矩阵 dist_matrix - dist(data_scaled, method euclidean)2.2 缺失值和异常值不处理就是给自己埋雷K-means对缺失值零容忍直接报错。层次聚类稍微宽容一点但缺失值的存在会让距离矩阵计算出现NA后续步骤照样崩。处理缺失值的策略取决于缺失比例低于5%可以考虑直接删除样本5%到20%之间用均值、中位数或KNN插补超过20%就要认真评估这个变量是否还值得保留。异常值的影响更隐蔽。一个极端离群点可能自己单独成一类把原本清晰的结构搅乱。检测异常值可以用箱线图的1.5倍IQR规则也可以用马氏距离。处理方式不一定是删除有时候把异常值单独标记出来、在聚类时排除、最后再单独分析反而是更稳妥的做法。# 用IQR规则标记异常值 Q1 - quantile(data_scaled[,1], 0.25) Q3 - quantile(data_scaled[,1], 0.75) IQR_val - Q3 - Q1 outliers - which(data_scaled[,1] Q1 - 1.5*IQR_val | data_scaled[,1] Q3 1.5*IQR_val)2.3 变量筛选维度不是越多越好维度灾难在聚类里体现得特别明显。变量越多样本之间距离的区分度越低所有点看起来都差不多远。而且高度相关的变量相当于给同一个信息加了权重会扭曲距离计算。动手聚类前建议先看相关系数矩阵把相关系数超过0.8的变量合并或剔除。主成分分析也是常用的降维手段用前几个主成分代替原始变量既降了维度又保留了大部分信息量。# 查看相关系数 cor_matrix - cor(data_scaled) # 找出高相关变量对 high_cor - which(abs(cor_matrix) 0.8 upper.tri(cor_matrix), arr.ind TRUE)3. 主流聚类方法在R里的落地细节3.1 K-means快、简单但别闭着眼睛用K-means是大多数人接触的第一个聚类算法逻辑直白先随机选K个中心点把每个样本分配给最近的中心然后重新计算每个簇的中心反复迭代直到中心不再移动。它的优点是速度快、结果好解释缺点是必须提前指定K值而且对初始中心敏感容易陷入局部最优。R里有两个K-means实现stats::kmeans()和cluster::pam()。前者是标准K-means后者是围绕中心点的划分方法对异常值更稳健。实际用的时候我一般会设置nstart 25让算法跑25次不同初始中心取最优结果这样能大幅降低局部最优的风险。set.seed(123) km_result - kmeans(data_scaled, centers 3, nstart 25) # 查看各簇大小 km_result$size # 查看簇中心 km_result$centersK值怎么定肘部法则和轮廓系数是最常用的两个指标。肘部法则看的是簇内平方和随K值变化的曲线拐点位置就是比较合适的K。轮廓系数则综合衡量了样本与自身簇和其他簇的紧密度与分离度取值在-1到1之间越接近1越好。# 肘部法则 wss - sapply(1:10, function(k) kmeans(data_scaled, centers k, nstart 25)$tot.withinss) plot(1:10, wss, type b, xlab K, ylab 簇内平方和) # 轮廓系数 library(cluster) sil_width - sapply(2:10, function(k) { km - kmeans(data_scaled, centers k, nstart 25) mean(silhouette(km$cluster, dist(data_scaled))[,3]) })3.2 层次聚类不用预设K但计算量是硬伤层次聚类分两种方向凝聚型自底向上每个样本先自成一类逐步合并和分裂型自顶向下先全部归为一类逐步拆分。实际应用中凝聚型占绝对主流。它的最大好处是不需要提前指定簇的数量聚类过程会生成一棵树状图你可以根据树状图在任意高度切一刀得到想要的簇数。代价是计算复杂度。凝聚型层次聚类的时间复杂度大约是O(n³)样本量超过几千就要慎重了。距离矩阵本身也要占内存n个样本需要n²的空间。所以层次聚类更适合小样本、需要探索不同粒度分组场景。# 层次聚类 hc - hclust(dist_matrix, method ward.D2) # 画树状图 plot(hc, hang -1, cex 0.6) # 切分成3类 rect.hclust(hc, k 3, border red)连接方式linkage的选择很关键。ward.D2倾向于产生大小均衡的球形簇是最常用的默认选项。complete全连接对异常值敏感但簇更紧凑average平均连接折中single单连接容易产生链式效应一般不推荐。3.3 DBSCAN能找出任意形状的簇还能识别噪声K-means和层次聚类都假设簇是凸形的、大致球状分布。如果数据里的簇是环形、月牙形或者相互嵌套的这两种方法就歇菜了。DBSCAN基于密度定义簇核心点周围一定半径内至少有指定数量的邻居这些核心点连成一片就构成一个簇不属于任何簇的点被标记为噪声。R里的dbscan包提供了实现。两个核心参数eps是邻域半径minPts是核心点的最小邻居数。eps太小会导致大部分点被当成噪声太大则所有点合并成一类。一个实用的经验是先用k-近邻距离图找拐点来辅助确定eps。library(dbscan) # 用k-近邻距离辅助确定eps kNNdistplot(data_scaled, k 5) abline(h 0.5, col red, lty 2) # 运行DBSCAN db_result - dbscan(data_scaled, eps 0.5, minPts 5) db_result$cluster3.4 高斯混合模型软聚类给出概率而非硬标签前面几种方法都是硬聚类每个样本最终只属于一个簇。高斯混合模型GMM走的是另一条路假设数据由若干个高斯分布混合生成每个样本以一定概率属于每个簇。这种软聚类在样本处于簇边界时特别有用你能看到它到底有多“犹豫”。mclust包是R里做GMM最成熟的工具它能自动根据BIC准则选择最优的簇数量和协方差结构。协方差结构决定了每个簇的形状球形、对角、全矩阵等灵活度很高。library(mclust) gmm_result - Mclust(data_scaled) summary(gmm_result) # 查看每个样本的归属概率 gmm_result$z4. 聚类结果到底靠不靠谱评估与验证4.1 内部指标没有标签时怎么判断好坏内部指标只依赖数据本身和聚类结果不需要外部标签。常用的有轮廓系数、Calinski-Harabasz指数、Davies-Bouldin指数。轮廓系数前面提过这里重点说后两个。CH指数是簇间离散度与簇内离散度的比值越大越好。DB指数是簇内散度与簇间距离的比值越小越好。library(factoextra) fviz_nbclust(data_scaled, kmeans, method silhouette) fviz_nbclust(data_scaled, kmeans, method wss)4.2 稳定性检验换个样本还能不能得到同样的结构一个聚类结果如果只在当前数据集上成立换个抽样就面目全非那它的价值就很有限。稳定性检验的思路是多次有放回抽样每次重新聚类看同一个样本在不同次运行中是否稳定地落在同一个簇里。fpc包里的clusterboot()函数就是做这件事的它会给出每个簇的Jaccard相似度均值超过0.75通常认为该簇是稳定的。library(fpc) boot_result - clusterboot(data_scaled, clustermethod kmeansCBI, k 3, B 100) boot_result$bootmean4.3 业务验证数字之外还得看常识统计指标再漂亮如果分出来的簇在业务上没法解释那这个聚类就是失败的。我习惯在得到聚类结果后把每个簇的中心点或关键特征拉出来逐簇问自己这群样本有什么共同特点它们在业务上代表什么如果某个簇找不到合理的业务解释要么是K值选得不对要么是变量选得有问题要么是数据本身就不该聚类。5. 可视化让聚类结果自己说话5.1 二维散点图最直接但也最容易误导把数据用前两个主成分投影到二维平面然后按簇着色是最常见的可视化方式。factoextra包的fviz_cluster()一行代码就能搞定还会自动画出每个簇的椭圆置信区域。但要注意二维投影只保留了部分信息如果前两个主成分解释的方差比例很低图上的分离程度可能严重失真。library(factoextra) fviz_cluster(km_result, data data_scaled, geom point, ellipse.type convex, ggtheme theme_minimal())5.2 热图高维数据的聚类结构一目了然当变量很多时热图比散点图更有优势。行是样本、列是变量颜色深浅代表数值大小样本按聚类结果排序变量也可以做聚类排序。这样一眼就能看出哪些变量在哪些簇里表现突出。pheatmap包是我用得最顺手的支持行列聚类、注释条、颜色映射等。library(pheatmap) pheatmap(data_scaled[order(km_result$cluster),], cluster_rows FALSE, cluster_cols TRUE, annotation_row data.frame(Cluster factor(km_result$cluster)), show_rownames FALSE)5.3 轮廓图每个样本的归属质量都能看到轮廓图把每个样本的轮廓系数按簇排列能直观看到哪些簇的样本归属清晰、哪些簇的样本处于边界地带。如果某个簇的平均轮廓系数明显低于其他簇说明这个簇的结构不够紧凑可能需要拆分或合并。fviz_silhouette(silhouette(km_result$cluster, dist_matrix))6. 那些只有踩过才知道的坑6.1 标准化不是万能药有时候反而帮倒忙标准化默认所有变量同等重要但实际业务中某些变量就是比其他变量更关键。比如做客户分群时消费金额可能比年龄更重要。这时候无差别标准化会削弱关键变量的影响力。解决办法是加权给重要变量乘上一个大于1的系数后再计算距离。另外如果所有变量本身就是同一量纲、数值范围也接近标准化反而可能引入不必要的噪声。6.2 K-means的“K”不是拍脑袋定的我见过太多人直接kmeans(data, 3)问为什么是3回答“因为业务上就分三类”。业务上有三类不代表数据里就有三类。正确的做法是先跑肘部法则和轮廓系数看看数据支持几个簇再结合业务判断。如果数据说4类最合理、业务说3类那就两个都跑对比哪个更有解释力。6.3 层次聚类的树状图别只看颜色fviz_dend()默认会给树状图着色但颜色的切分位置不一定是最优的。一定要结合rect.hclust()手动指定切分高度或者用cutree()按K值切分后检查各簇大小。有时候树状图看起来分了两大支但其中一支内部还有明显的子结构这时候可能需要切得更细。6.4 DBSCAN的eps对结果影响极大DBSCAN对eps的敏感程度远超K-means对K的敏感程度。eps稍微变一点结果可能从“全是噪声”变成“全是一类”。我的经验是先用kNNdistplot()找到拐点然后在拐点附近以0.05为步长多试几个值对比结果稳定性。如果不同eps下结果差异巨大说明数据本身密度不均匀DBSCAN可能不是最佳选择。6.5 聚类结果不是终点而是起点聚类做完、图画完、报告写完这只是第一步。真正有价值的是后续动作针对每个簇制定差异化策略、把簇标签作为新特征输入到有监督模型里、或者持续监控新样本落入哪个簇。我见过太多聚类项目止步于“我们分出了三类客户”然后就没有然后了。聚类本身不产生价值基于聚类结果做出的决策才产生价值。7. 一套可复用的聚类分析流程把上面这些串起来我平时做聚类项目的标准流程大致是这样的明确目标是探索性分析还是为了后续分类期望得到几个簇业务上怎么用数据预处理处理缺失值、异常值筛选变量必要时降维标准化。方法初筛小样本先跑层次聚类看树状图大样本先跑K-means看肘部法则。参数调优K-means调KDBSCAN调eps和minPtsGMM让BIC自动选。结果评估内部指标加稳定性检验再结合业务解释。可视化散点图、热图、轮廓图多角度呈现。落地应用给簇打标签制定策略持续监控。# 一个完整的K-means流程示例 library(factoextra) library(cluster) # 1. 标准化 df_scaled - scale(df_numeric) # 2. 确定最优K fviz_nbclust(df_scaled, kmeans, method silhouette) # 3. 聚类 set.seed(42) final_km - kmeans(df_scaled, centers 4, nstart 50) # 4. 评估 fviz_cluster(final_km, data df_scaled) fviz_silhouette(silhouette(final_km$cluster, dist(df_scaled))) # 5. 输出簇标签 df$cluster - final_km$cluster这套流程不是死的具体项目里步骤顺序可能调整比如有时候先降维再标准化效果更好有时候稳定性检验要放在业务验证之后。但大框架不变照着走基本不会出大问题。最后说一个我自己的习惯每次聚类分析结束后我都会把随机种子、包版本、关键参数记在一个脚本注释里。过几个月回头再看能准确复现当时的结果这在需要向别人解释“为什么当时分了四类”的时候特别有用。
阅读完成 · 觉得有帮助?
咨询建站