
简介针对R语言混合型数据聚类分析的真实场景一个完整R脚本演示了从数据导入、预处理到聚类建模与可视化的全流程适合具备基础R语法、希望掌握K-means等聚类方法的数据分析初学者。包内共1个R源文件压缩包仅2KB脚本结构清晰可直接运行修改。目前已有1060人学习热度在同类型小体积资源中表现不错。借助该案例读者可以熟悉dplyr、ggplot2、cluster、factoextra等包的协同用法理解针对地理位置、学费、人数等混合变量进行标准化处理的意义同时通过轮廓系数或肘部法则确定合理簇数并学会用散点图解读各簇特征。该资源以精炼代码呈现了完整分析思路既可用于课堂练习也可作为实际项目前的方法验证参考。1. 混合型数据聚类为什么不能直接跑kmeans做数据分析的经常会遇到这样一桌数据客户信息表里既有年龄、月均消费金额这样的连续数值也有性别、会员等级、购买偏好这样的分类字段甚至还有“不满意/一般/满意”这种有序等级。这类数据就叫混合型数据。直接用kmeans跑流程上没问题结果却经常没法看——分类变量被硬编码成0和1之后欧氏距离会把“性别不同”和“收入差5000元”混在一起算量纲和语义全乱了。真正做聚类分析案例落地时大部分人卡住的不是算法而是“距离怎么算”。这篇就顺着R语言的实现路径讲清混合型数据聚类的核心方法、完整代码和可验证的参数设置最后给一个可以直接改数据的案例脚本。适合手里有真实业务表、打算用R做客户分群或产品分类的从业者。2. 用Gower距离统一量纲混合型数据的距离计算2.1 为什么欧氏距离在混合型数据上失效kmeans本质是最小化组内平方和依赖欧氏距离。欧氏距离要求所有特征都是连续数值并且最好同一量纲。当数据里出现分类变量时常见的处理方式是把分类变量做独热编码变成多个0/1字段。独热编码后的欧氏距离有一个隐蔽问题它把“类别不同”的差异固定成某个数值但这个数值和连续变量的绝对差没有可比性。比如两个客户的性别不同独热编码后该维度的差值是1而收入差可能只有几百元标准化后可能小于0.1。聚类算法会认为性别差异比收入差异更重要这往往是错的。另一个问题是分类变量的水平数影响权重。一个有10个类别的字段独热编码后会变成10列占总特征数比重很大变相抬高了该字段在距离计算中的影响力。所以混合型数据聚类需要一种“不同性质变量各自计算相似度再统一加权平均”的距离度量。Gower距离就是为此设计的。2.2 Gower距离的计算方式Gower距离对每个变量单独计算相似度最后取加权平均。连续变量先做极差归一化然后用“1 - 曼哈顿距离/极差”作为相似度分类变量则看两个样本在该变量上是否相等相等记1不相等记0有序变量可以按排序后的位次计算类似于把等级映射成等距分数后再用连续变量的公式。最终每个样本对的相似度落在0到1之间距离则取1减去相似度。这种设计的价值在于分类变量不再被当作数值而是纯粹比较“是否相同”连续变量则通过极差缩放解决了量纲差异。同时可以通过变量权重调整不同字段的影响力。用R实现时不需要自己逐对计算cluster包里的daisy函数已经支持。2.3 daisy函数的基本写法假设数据框df里有连续变量age、income分类变量gender、city_level有序变量education用以下代码计算距离矩阵library(cluster) # 指定各变量类型 # 1 表示连续2 表示有序分类3 表示无序分类 var_types - c(age 1, income 1, gender 3, city_level 3, education 2) gower_dist - daisy(df, metric gower, type list(numeric c(age, income), factor c(gender, city_level), ordered c(education)))代码里daisy的metric参数固定为gowertype列表用于覆盖默认的类型判断。如果数据列的class已经正确设置比如factor表示分类变量、ordered表示有序变量那么可以省略type。但实际读取数据时字符型列常被读成character数字编码的分类列又会被读成整数最好显式声明避免把有序的“学历等级”当成普通数值计算。2.4 使用daisy时的注意事项daisy默认对连续变量做极差归一化也就是除以该变量的取值范围。如果某个连续变量存在极端离群值极差会被拉大普通样本之间的差异就会变小。比如一个客户月收入10万其余人都在1万以下极差变成9万收入维度的区分度会被压缩。这时候建议先对连续变量做截尾处理或者用apply函数手动缩放到0-1区间后再放进daisy可以理解为先处理分布再算距离。另外daisy返回的是一个dissimilarity对象不是普通矩阵。后续传给聚类函数没问题但如果要查看或保存需要先转成矩阵as.matrix(gower_dist)。对5000条以上的数据距离矩阵的大小会迅速膨胀因为矩阵需要存储所有样本对的距离内存消耗约为8字节乘以样本数的平方。这是用混合型距离做聚类的一个硬性边界后面章节会说应对方法。3. R语言实现daisy与pam的完整聚类流程3.1 距离矩阵拿到之后用什么聚类算法有了Gower距离矩阵kmeans就没办法直接用因为它需要原始数据矩阵做均值更新。常见做法是改用PAMPartitioning Around Medoids或层次聚类。PAM是kmeans的变体簇中心选择的是实际存在的样本点也就是medoid而不是虚拟的平均值。这个特性在混合型数据上很有用因为分类变量的“均值”没有意义medoid可以代表一个典型样本。PAM对异常值的鲁棒性也优于kmeans因为它通过最小化到medoid的总距离来划分样本不依赖均值。在cluster包中pam函数可以直接接收daisy返回的距离对象。另一个选择是hclust加上agnes同样来自cluster包层次聚类可以在不知道簇个数的情况下先生成树状图但大数据量下计算较慢。3.2 用pam做混合型数据聚类的核心代码下面是一段完整的聚类流程数据沿用上一节的dflibrary(cluster) # 计算Gower距离 gower_dist - daisy(df, metric gower) # 设置聚类个数这里先定为4 k - 4 # 执行PAM聚类 pam_result - pam(gower_dist, k k, diss TRUE) # 提取聚类标签 cluster_labels - pam_result$clustering # 查看每个簇的样本量 table(cluster_labels)代码中pam的第一个参数是距离矩阵dissTRUE表示传入的是相异度矩阵而不是原始数据。pam函数内部会执行bootstrap采样来寻找初始medoid默认的pamonce参数为FALSE数据量大时建议设成pamonce TRUE能显著加快计算。得到聚类结果后可以查看每个簇的medoid是哪一行样本以及簇内平均距离等诊断信息pam_result$medoids summary(pam_result)medoids返回的是样本的索引通过索引可以反查原始数据看这个“中心客户”长什么样。这一步在业务上很有价值可以直接作为该群体的典型画像不需要人工从20个平均值里脑补一个虚拟人。3.3 层次聚类作为备选方案PAM适合数千级别的样本量。如果样本量只有几百或者你希望看到完整的聚类层次可以改用agnes# 用agnes做层次聚类方法为Ward hc_result - agnes(gower_dist, method ward) # 根据树状图切出k个簇 hc_clusters - cutree(hc_result, k k)method可选ward、complete、average。其中ward倾向于产生大小比较均衡的簇但要求距离度量是欧氏性质Gower距离不一定满足这点。实际使用时我更常用average或complete它们对距离度量的要求更宽松结果也更稳定。注意cutree之后得到的标签数值顺序和PAM的不一定一致这没关系只是簇编号不同。3.4 从聚类结果回到原始数据做画像聚类完成后不要只看标签要把标签合并回原始数据逐簇计算连续变量的中位数和分类变量的众数比例df$cluster - cluster_labels # 连续变量按簇计算中位数 aggregate(cbind(age, income) ~ cluster, data df, FUN median) # 分类变量的分布 table(df$cluster, df$gender)这一步是聚类分析案例中真正产生业务价值的地方。比如发现簇1的年龄中位数35岁、收入中位数1.2万、女性占比70%这个簇就可以命名为“高消费年轻女性”。混合型数据聚类到这里才算闭环。4. 聚类个数k怎么选轮廓系数与NbClust4.1 平均轮廓系数法PAM和层次聚类都需要指定k。选得太少会强行合并差异大的样本选得太多则产生碎片簇。最常用的内部评估指标是轮廓系数Silhouette。对每个样本计算它与同簇其他样本的平均距离a再计算它到最近邻簇所有样本的平均距离b轮廓系数就是(b - a) / max(a, b)。值越接近1说明样本离自己簇近、离其他簇远0附近表示两个簇边界上的样本负数则说明该样本可能分错了簇。在R里可以直接用silhouette函数传入PAM结果library(cluster) sil - silhouette(pam_result) mean(sil[, 3])sil是一个矩阵第三列是每个样本的轮廓值取平均就得到平均轮廓宽度。通常在0.25以上说明聚类结构可接受0.5以上说明良好。但这个阈值不是绝对的混合型数据因为分类变量的存在平均轮廓系数一般会偏低0.2左右也可能对应有业务意义的划分。4.2 用循环自动比较多个k值手动试k不现实写一个循环从2试到10记录每个k的平均轮廓系数avg_sil - numeric(9) for (k in 2:10) { pam_k - pam(gower_dist, k k, diss TRUE) avg_sil[k - 1] - mean(silhouette(pam_k)[, 3]) } names(avg_sil) - 2:10 avg_sil选平均轮廓系数最大的k。但要注意这个指标倾向于选择样本量均匀的簇划分如果真实数据本身是偏态分布比如有一个大类占了70%轮廓系数可能会偏好把大类拆开。因此不只看最大还要看拐点。一个常见做法是画出k与平均轮廓系数的折线图选择“最后一个明显下降前的k”或“第一个局部峰值”。4.3 NbClust包的30个指标怎么用NbClust包一次性输出了20多个内部指标的综合推荐但它比较慢适合小样本探索。基本用法是把距离矩阵传进去library(NbClust) # 注意NbClust对样本量敏感建议在2000以内使用 nb_res - NbClust(data NULL, diss gower_dist, distance NULL, method pam, min.nc 2, max.nc 10)data设为NULL改传dissdistance也设为NULL因为已经提供了距离矩阵。method指定为pam。运行后nb_res$Best.nc会给出各个指标推荐的最优簇数可以取众数作为参考。这个包有个坑当距离矩阵不是欧氏距离时部分指标比如truelove、db可能计算异常或者非常耗时。我一般只把NbClust的结果当作参考最终k还是结合轮廓系数和业务解释性来定。4.4 业务约束与稳定性检查数据科学意义上的最优k经常不是业务想要的那个。比如银行想分5类对应5个运营策略即使轮廓系数在k7时更高也会选5。这时可以用簇内平均距离和簇间距离的比值做二次验证。另外改变样本顺序重新聚类看每个样本的标签是否稳定是判断k是否合适的长期指标。对于混合型数据由于Gower距离包含分类变量的随机相等性稳定性检查尤其重要。5. 实战案例客户画像混合数据聚类全流程5.1 构造一份包含三类变量的示例数据为了演示我用R生成一份模拟客户数据包含连续变量age和spending无序分类变量channel渠道有序变量level会员等级。这不是真实数据但结构和常见业务表一致set.seed(42) n - 300 df - data.frame( age round(runif(n, 18, 60)), spending round(runif(n, 100, 5000)), channel sample(c(APP, WEB, OFFLINE), n, replace TRUE), level factor(sample(c(L1, L2, L3, L4), n, replace TRUE), levels c(L1, L2, L3, L4), ordered TRUE) )channel是无序分类因为渠道之间没有大小关系level是有序分类因为L1到L4有等级递增关系。注意factor必须加ordered TRUE否则R会把它当成无序因子处理daisy也就无法识别有序信息。5.2 计算距离并确定k先计算Gower距离然后按第4章的方法选出klibrary(cluster) gower_dist - daisy(df, metric gower) # 运行2到8的轮廓系数比较 sil_scores - numeric(7) for (k in 2:8) { pam_k - pam(gower_dist, k k, diss TRUE) sil_scores[k - 1] - mean(silhouette(pam_k)[, 3]) } names(sil_scores) - 2:8 sil_scores假设运行结果为2:0.31 3:0.28 4:0.25 5:0.22 6:0.20 7:0.19 8:0.18最优是k2但业务上分2类太少。这时可以看k3或k4的轮廓值下降是否明显。如果k4时每个簇的样本量比较均衡簇特征区分明显就选4。5.3 执行PAM并解读簇特征选定k4后执行聚类并生成每个簇的画像表k - 4 pam_result - pam(gower_dist, k k, diss TRUE) df$cluster - pam_result$clustering # 连续变量的中位数 med_age - tapply(df$age, df$cluster, median) med_spend - tapply(df$spending, df$cluster, median) # 分类变量的频数占比 chan_tab - prop.table(table(df$cluster, df$channel), margin 1)把输出汇总成一个表result_summary - data.frame( cluster 1:k, n as.vector(table(df$cluster)), median_age round(med_age, 1), median_spend round(med_spend, 0), APP_ratio round(chan_tab[, APP], 2), WEB_ratio round(chan_tab[, WEB], 2) ) print(result_summary)解读这个表时可以先看median_spend把簇分成高消费和低消费再看APP_ratio是否偏低结合level的众数判断是高等级客户还是低等级客户。比如第1簇median_spend4200、APP_ratio0.8、levelL4基本可以定义为“高消费APP核心用户”第2簇median_spend300、channelOFFLINE占大半就是“低活跃线下散客”。这一步不需要复杂的统计学检验交叉表就够用。5.4 保存和复用聚类模型PAM模型可以保存之后用到新样本时需要计算新样本到每个medoid的Gower距离然后归入最近的那个簇。R里没有现成的predict.pam需要手动写一个函数# 计算新样本到各medoid的Gower距离 predict_gower - function(new_df, medoid_indices, train_df) { dist_matrix - as.matrix(daisy(rbind(train_df, new_df), metric gower)) new_start - nrow(train_df) 1 new_dist - dist_matrix[new_start, medoid_indices] which.min(new_dist) }但实际操作中如果新样本量不大更简单的做法是直接对“训练集新样本”重新计算daisy再取距离矩阵中新样本到各medoid的部分。这个方法虽然不够优雅但避免了自定义函数容易犯的变量类型错配问题。6. 结果验证与常见坑轮廓系数和变量权重调整6.1 用簇间差异验证聚类是否有区分度轮廓系数只是内部指标还需要验证每个簇在原始变量上是否有明显的区分度。对连续变量做Kruskal-Wallis检验对分类变量做卡方检验看p值是否都小于0.05kruskal.test(age ~ cluster, data df) chisq.test(table(df$cluster, df$channel))如果某个变量在簇间没有显著差异说明它对当前聚类的贡献很低可以考虑降权或剔除。检验结果可以作为“这个聚类是否真的分开了”的旁证但要注意数据量大时p值容易变小显著不等于业务上有用。6.2 变量权重对聚类结果的影响Gower距离中每个变量默认等权重。但如果某个变量本身就是业务上最重要的分群维度可以调高它的权重。daisy函数支持weights参数不过要注意它与type的配合方式。更灵活的做法是使用cluster包中的daisy后手动组合距离分别计算数值变量和分类变量的Gower距离再按权重加和。# 分别计算两部分的距离 dist_numeric - daisy(df[c(age, spending)], metric gower) dist_factor - daisy(df[c(channel, level)], metric gower) # 按权重合并例如数值变量占0.7 combined_dist - 0.7 * dist_numeric 0.3 * dist_factor这种方法能直观控制数值变量整体和分类变量整体的影响力比逐一给变量设权重容易解释。不过要注意两部分距离矩阵的样本顺序必须一致而且合并后的距离矩阵不一定满足三角不等式对PAM影响不大但对层次聚类可能产生轻微的反常。6.3 有序变量的常见处理误区很多人把有序变量直接当成数值变量处理比如把level的L1到L4映射成1到4。这在等级间隔不完全相等时会产生误导。与其强行映射不如让daisy按有序变量的方式计算它会把每个等级的位置视为等距但实际上这同样假设了线性。如果等级数量很少比如三档可以试两种方式分别聚类看看结果稳定性如何。6.4 大数据量下的实用加速技巧Gower距离矩阵是O(n²)样本超过2万时内存就不够用了。推荐一个处理思路先用全部数据做一次分层抽样比如抽5000条算出聚类结构和medoid然后对剩余样本计算到这些medoid的距离来分配簇。这样做的精度损失通常不大因为medoid本身是真实样本代表性强。还可以用cluster包的clara函数它的原理与PAM近似专门设计给大数据使用内部用抽样替代全量计算。clara_result - clara(gower_dist, k 4, samples 50)注意clara接受的是原始数据矩阵或距离矩阵但它的抽样策略对混合型数据同样适用。当距离矩阵已经非常大时直接把gower_dist传给clara也可能撑爆内存更好的方式是clara(x df, k 4)让它内部处理连续与分类变量不过这样只能用默认的欧氏距离对混合型数据并不友好。所以在实际项目中我更推荐抽样手动距离计算而不是依赖clara的默认设置。本文还有配套的精品资源点击获取