
简介围绕R语言混合型数据聚类分析一份可运行的完整R脚本被整理为压缩包主要面向具备基础R知识、希望掌握K-means等聚类方法的数据分析学习者。包内仅含1个R文件整体大小2KB脚本核心覆盖数据读取、缺失值与异常值处理、z-score标准化、聚类算法选择、簇数评估及结果可视化的全过程。案例使用包含地理位置、学生人数、学费、教学质量等混合型变量的美国大学数据实际调用dplyr、ggplot2、cluster、factoextra等包并通过轮廓系数或肘部法则确定最优K值最终以散点图、柱状图等形式展示不同簇的特征便于理解聚类结果的业务含义。该资源已有1060人学习使用读者可通过修改数据路径轻松迁移至自身业务数据快速掌握混合型数据聚类的完整技术链路。1. 为什么 R 语言处理混合型数据聚类时要绕开 k-means客户画像里同时有年龄段、年收入、所在地区、最近一次购买时间这种“数值 分类”掺杂的表在很多数据分析案例里最不好处理。很多刚接触聚类分析的人第一反应是标准化后跑 k-means但 k-means 的均值计算对性别、地区这类无序分类变量没有意义硬编码成 0/1 又会让距离计算被虚拟变量带偏。R 语言面对这类混合型数据业界最稳妥的落地路径是 Gower 距离 PAMk-medoids算法用cluster包的daisy()算距离再用pam()聚类。这篇文章要解决的问题很具体拿到一份混着数值和分类字段的 R 语言数据集怎么选距离、怎么定聚类数、怎么解释每一类的业务含义。读者定位是做过常规 k-means、想往混合数据场景进一步的人我会把 Gower 距离的公式拆开讲再给出一套能直接跑通的 R 语言代码最后补上聚类数选择、轮廓系数验证和 k-prototypes 方案的对比。这套流程不依赖特定业务场景银行、零售、用户运营的表格都能复用。2. Gower 距离原理与 daisy 函数在混合型数据中的具体用法2.1 为什么混合型数据不能用欧氏距离k-means 或层次聚类里默认的欧氏距离要求所有变量都落在同一量纲且全部是数值型。性别“男”“女”没有数值大小之分距离 0 和 1 只是编码差异不代表“男到女比男到男远 1 倍”。把分类变量做 one-hot 展开后再算欧氏距离会出现两个问题一是分类水平多的字段如省份 31 类在距离中占比过大二是稀疏 0/1 列之间的距离被压缩数值变量的贡献被稀释。Gower 距离的思想是对每一列单独定义“相异度”最后做加权平均。数值列用绝对差除以全距把差异归一化到 [0,1]无序分类列看是否相同相同为 0、不同为 1。这样每列的贡献天然等权不依赖量纲也不怕分类变量水平数悬殊。具体到 R 语言cluster::daisy()函数的method gower就是这套逻辑的标准实现。2.2 用 daisy() 构造混合型数据的距离矩阵先造一个最典型的混合型数据集包含 3 个数值变量年龄、年收入、消费次数和 2 个分类变量性别、城市等级分别代表用户画像中的常见字段。注意这里收入单位是“元”年龄是“岁”消费次数是“次”三者量纲完全不同直接用欧氏距离会把年收入的差异放大到主导地位。# 加载 cluster 包daisy 和 pam 都在里面 library(cluster) # 构造示例数据200 条用户记录5 个字段 set.seed(2024) n - 200 user_df - data.frame( age round(runif(n, 18, 60)), # 数值型年龄 income round(runif(n, 3000, 30000)), # 数值型月收入单位元 purchase_cnt round(runif(n, 0, 30)), # 数值型近 3 个月消费次数 gender sample(c(男, 女), n, replace TRUE), # 无序分类 city_tier sample(c(一线, 二线, 三线), n, replace TRUE) # 有序但按无序处理 ) # 用 Gower 距离计算相异度矩阵 gower_dist - daisy(user_df, metric gower) # 查看距离矩阵结构 gower_distdaisy()返回的是一个dissimilarity对象本质上是一个扁平化的距离向量不直接展示成方阵可以配合as.matrix()转换后查看具体数值。metric gower是混合型数据聚类的核心参数它会自动识别数值列和分类列。gender和city_tier在 R 里当前是字符型daisy()会自动当作无序分类变量处理但稳妥起见建议先转为 factor避免某些版本的包因类型识别问题报错。2.3 类型参数 type 与数值变量标准化的边界daisy()除了metric参数外还有一个容易被忽略的type参数。默认情况下它通过typeof()判断变量类型字符型和因子型默认当作无序分类数值型默认当作数值处理。如果你的数据里存在有序分类比如消费等级低/中/高需要显式指定type list(ordratio level)否则会被当成无序分类丢失等级信息。# 为消费等级增加有序分类语义 user_df$level - factor(c(低, 中, 高), ordered TRUE) gower_dist2 - daisy(user_df, metric gower, type list(ordratio level))数值变量是否要先标准化这是混合型数据聚类里常见的误用点。Gower 距离内部已经把每个数值变量的差除以全距range归一化到 [0,1] 区间所以scale()不是必须的。但在一种情况下我会手动预处理当数值变量存在极端离群点时比如某个用户年收入 100 万全距被拉大其他用户的差异会被压缩。此时先对数值列做 Winsorize缩尾或者用log1p()变换再送入daisy()距离分布更稳健。提示daisy()的stand参数在metric gower下无效不需要设置。若使用metric euclidean时才需要自己先 scale。3. PAM 算法与 R 语言混合数据聚类的完整代码流程3.1 从距离矩阵到 k-medoidsPAM 为什么比 k-means 更合适有了 Gower 距离矩阵下一步选择聚类算法。k-means 要求输入原始数据矩阵并反复计算簇内均值混合型数据的“均值”无法定义而 PAMPartitioning Around Medoids算法只需要距离矩阵每一步都用“簇内到某点距离之和最小”的样本点medoid代表这个簇分类变量不参与均值运算自然兼容。R 语言cluster::pam()接收的就是daisy()的输出对象不需要把距离矩阵展开成方阵节省内存。时间复杂度比 k-means 高数据量超过 5 万行时我会改用cluster::clara()它的抽样策略能在可接受精度下处理十万级样本小于 5000 行的小样本场景直接用pam()结果最稳定。3.2 用 pam() 完成聚类的三步代码最常见做法是先用轮廓宽度silhouette width在 k 2 到 8 的范围内粗筛一次候选 k再结合业务含义定最终聚类数。下面的代码先算出一个样本 k 4 的 PAM 聚类并把簇标签合并回原始数据框。# PAM 聚类k 设为 4基于上一步的 Gower 距离矩阵 pam_result - pam(gower_dist, k 4, diss TRUE) # 簇标签合并回原始数据 user_df$cluster - pam_result$clustering # 查看每个簇的样本量 table(user_df$cluster) # 查看每个簇的中心点medoid在原数据中的行号 print(pam_result$medoids) # 提取每个簇内数值变量的均值和中位数 aggregate(cbind(age, income, purchase_cnt) ~ cluster, data user_df, FUN function(x) round(mean(x), 2))diss TRUE是让pam()明确知道第一个参数是相异度矩阵而不是原始数据如果漏了写pam 会尝试把距离对象当作数据矩阵处理直接报错或者跑出无意义结果。pam_result$medoids给出每个簇最具代表性的样本序号在用户画像场景里可以直接拉出这条记录当典型画像。聚合时用aggregate()按簇统计均值分类变量则用table(user_df$cluster, user_df$gender)交叉表观察分布差异。3.3 解读聚类结果数值和分类变量分别怎么看混合数据聚类结果的解读与纯数值聚类不同需要分别看两类变量的区分度。数值变量看均值差异比如簇 1 收入 2.1 万、簇 3 收入 0.6 万说明收入是主要分群维度分类变量看比例分布比如簇 4 里一线城市占 70%而簇 2 里三线城市占 65%这个簇就能用“三线年轻用户”这样的标签命名。# 分类变量分布交叉表 prop.table(table(user_df$cluster, user_df$gender), margin 1) # 带数值变量的描述性统计 library(dplyr) user_df %% group_by(cluster) %% summarise(across(c(age, income, purchase_cnt), list(mean mean, sd sd), .names {col}_{fn}))簇命名不一定要在代码里自动生成手动结合业务字段打标签更可控。比如“高收入高频次一线城市用户”比“cluster_1”在生产环境里更有交流价值。这一步完成后检查每个簇的样本占比如果某个簇只有 3% 的数据说明 k 可能偏大或存在离群点聚集下一步用聚类数评估决定是否调参。4. 聚类数选择与混合数据聚类可视化的 3 个常用方法4.1 用平均轮廓宽度评估哪个 k 更合理聚类数 k 是混合数据聚类绕不开的问题。常见做法是循环 k 2 到 10计算每次聚类结果的平均轮廓宽度取值越大说明簇内紧密、簇间分离越好。cluster包的silhouette()函数能直接作用于pam对象注意它是按样本算轮廓宽度需要手动取mean()得到平均分数。# 循环计算 k 2 到 8 的平均轮廓宽度 avg_sil - sapply(2:8, function(k) { pam_temp - pam(gower_dist, k k, diss TRUE) sil - silhouette(pam_temp$clustering, gower_dist) mean(sil[, 3]) }) # 输出每个 k 对应的得分 data.frame(k 2:8, avg_silhouette round(avg_sil, 4)) # 找出得分最高的 k k_best - which.max(avg_sil) 1 print(k_best)这段代码在 200 条样本上几乎瞬时完成数据量在 1 万以内也能接受。轮廓宽度对离群点敏感如果每次跑出来的最高 k 都是 2先检查数据里是否存在大量噪声变量我一般会先用summary(daisy_result)查看每列的平均相异度找出与其他变量距离结构差异过大的字段考虑剔除后再重新评估。4.2 gap statistic 作为交叉验证的第二指标平均轮廓宽度偏向于选择紧凑的球形簇而 Gower 距离计算出的簇形状不一定规则所以业界常搭配 gap statistic 来交叉验证。cluster::clusGap()虽不支持daisy对象但我一般会先把原始数据做一次 isometric 映射也就是用 PAM 距离矩阵做主坐标分析PCoA再对降维后的数值坐标跑clusGap()。# 先对 Gower 距离做 PCoA 降维得到数值型坐标 library(vegan) pcoa_res - cmdscale(gower_dist, k 6, eig TRUE) # 用潜变量坐标计算 gap statistic library(cluster) gap_stat - clusGap(pcoa_res$points, FUNcluster pam, K.max 8, B 50) print(gap_stat)B 50是蒙特卡洛参考分布的抽样次数越大越稳定但越耗时。判读标准是取最小的 k满足Gap(k) Gap(k1) - se(k1)也就是第一次出现“增加类别不再有显著收益”的位置。这个方法对 200 条以上的数据才稳定样本太少时参考分布的方差大结论参考价值有限。4.3 用 ggplot2 画混合数据聚类散点图混合型数据没有直接的原始坐标可以画散点图标准做法是对距离矩阵做主坐标分析后取前两个主坐标轴作为二维坐标再用ggplot2按簇着色。library(ggplot2) # 取前两个主坐标 pcoa_df - data.frame(pcoa_res$points[, 1:2]) colnames(pcoa_df) - c(Dim1, Dim2) pcoa_df$cluster - factor(pam_result$clustering) # 绘制聚类散点图 ggplot(pcoa_df, aes(x Dim1, y Dim2, color cluster)) geom_point(alpha 0.6, size 2) stat_ellipse(aes(fill cluster), geom polygon, alpha 0.15) labs(title PAM Clustering on Gower Distance (PCoA Projection), x Principal Coordinate 1, y Principal Coordinate 2)stat_ellipse()默认画 95% 置信椭圆用于观察簇的重叠程度重叠区域过大说明 k 可能不是最合适或者某些变量区分度太弱。这个图不直接等价于原始空间的实际距离关系只是高维距离在二维的低维投影解释时保留余地。生产报告中我会附上 PCoA 前两个轴的方差贡献率像eig / sum(eig)这样输出告诉读者这个图能解释多少比例的距离结构。5. 进阶技巧k-prototypes 替代实现与聚类结果落地验证5.1 什么时候该换 k-prototypes 而不是 PAMPAM Gower 距离的组合适合中小样本但样本量到 5 万以上时PAM 的 O(k(n-k)^2) 时间复杂度会让计算变得很慢clara()的抽样方案又可能牺牲小簇的识别精度。另一个替代方案是clustMixType包的kproto()它直接对混合型数据做 k-prototypes 聚类原理是数值变量用欧氏距离、分类变量用匹配相异度两者加权求和不需要先算距离矩阵内存占用低速度明显快于 PAM。k-prototypes 适合 10 万行以上的用户分群因为它可以在原始数据框上直接运行不需要保存庞大的相异度矩阵。代价是分类和数值两类变量之间需要一个权重参数lambda默认是 0.5含义是数据集中分类变量比例的标准差。经验做法是用数据里分类变量个数除以总变量个数作为初始值比如 5 个字段中 2 个分类lambda取 0.4 附近再通过不同取值跑出的簇大小分布判断稳定性。# 安装并加载 k-prototypes 包 # install.packages(clustMixType) library(clustMixType) # 数值变量先做标准化避免量纲影响 df_kproto - user_df df_kproto$income - scale(df_kproto$income) df_kproto$purchase_cnt - scale(df_kproto$purchase_cnt) # k-prototypes 聚类k 先取 4 kproto_result - kproto(x df_kproto, k 4, lambda 0.4, verbose FALSE) # 查看簇标签 table(kproto_result$cluster)kproto()会默认把所有数值变量标准化处理但不会把分类变量做 one-hot所以内存和速度都有优势。注意lambda越大分类变量在聚类决策中权重越大如果发现某个簇完全由单一分类水平主导说明 lambda 偏大调低后重新跑。5.2 用分组统计和外部指标验证聚类结果是否可解释聚类出来不是终点还要让业务方信服。我常用的验证方式有两种一是内部验证看每个簇在关键指标上的区分度用summary或分组统计看是否有业务意义二是外部验证如果数据里有已知的类别标签比如用户是否流失可以用调整兰德指数ARI或纯度指标对比聚类结果和真实标签的重合度。# 如果数据里有真实标签计算 ARI library(mclust) # 假设 user_df$churn 是 0/1 的真实流失标签仅作示例 # adjustedRandIndex(user_df$churn, pam_result$clustering) # 混合数据聚类常用验证簇间数值变量差异的显著性 fit - aov(income ~ cluster, data user_df) summary(fit)aov()的 p 值能告诉业务方每个数值变量在簇间是否有显著差异通常 p 0.05 说明这个字段真正贡献了分群。对分类变量可以用卡方检验chisq.test(table(user_df$cluster, user_df$gender))。无论 ARI 还是卡方目的都只是量化“这个聚类结果和业务经验是否吻合”不要理解为聚类正确性的唯一判断标准。5.3 把聚类结果导出为生产可用的表分析完成后常见做法是把簇标签写入数据库表或在 R 环境里保存方便下游报表和推荐策略使用。导出前注意行顺序和原始数据行号保持一致对于大表建议输出user_id cluster两列避免把全部原始字段复制一份。# 导出结果为 csv保留原始 ID result_df - data.frame(user_id user_df$user_id, cluster user_df$cluster) # write.csv(result_df, cluster_result.csv, row.names FALSE) # 保存 R 对象便于后续重复使用不重新跑距离计算 saveRDS(pam_result, pam_gower_result.rds)之后重新加载readRDS(pam_gower_result.rds)时可以直接对新样本计算到各 medoid 的 Gower 距离再归入最近簇不需要整体重新聚类。这样每次新增一批用户数据只需对增量数据调daisy()计算与 medoid 的距离成本比全量重跑低很多。聚类分析到这一步基本收尾数据预处理、距离计算、PAM 聚类、聚类数选择、结果验证与落库R 语言的这套链路在真实数据分析案例里已经能稳定复用到多种混合型业务表。本文还有配套的精品资源点击获取