简介:针对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参数固定为"gower",type列表用于覆盖默认的类型判断。如果数据列的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就没办法直接用,因为它需要原始数据矩阵做均值更新。常见做法是改用PAM(Partitioning Around Medoids)或层次聚类。PAM是kmeans的变体,簇中心选择的是实际存在的样本点,也就是medoid,而不是虚拟的平均值。这个特性在混合型数据上很有用,因为分类变量的“均值”没有意义,medoid可以代表一个典型样本。
PAM对异常值的鲁棒性也优于kmeans,因为它通过最小化到medoid的总距离来划分样本,不依赖均值。在cluster包中,pam函数可以直接接收daisy返回的距离对象。另一个选择是hclust加上agnes(同样来自cluster包),层次聚类可以在不知道簇个数的情况下先生成树状图,但大数据量下计算较慢。
3.2 用pam做混合型数据聚类的核心代码
下面是一段完整的聚类流程,数据沿用上一节的df:
library(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的第一个参数是距离矩阵,diss=TRUE表示传入的是相异度矩阵而不是原始数据。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怎么选:轮廓系数与NbClust
4.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,改传diss,distance也设为NULL,因为已经提供了距离矩阵。method指定为"pam"。运行后nb_res$Best.nc会给出各个指标推荐的最优簇数,可以取众数作为参考。
这个包有个坑:当距离矩阵不是欧氏距离时,部分指标(比如"truelove"、"db")可能计算异常或者非常耗时。我一般只把NbClust的结果当作参考,最终k还是结合轮廓系数和业务解释性来定。
4.4 业务约束与稳定性检查
数据科学意义上的最优k经常不是业务想要的那个。比如银行想分5类对应5个运营策略,即使轮廓系数在k=7时更高,也会选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章的方法选出k:
library(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,最优是k=2,但业务上分2类太少。这时可以看k=3或k=4的轮廓值下降是否明显。如果k=4时每个簇的样本量比较均衡,簇特征区分明显,就选4。
5.3 执行PAM并解读簇特征
选定k=4后执行聚类,并生成每个簇的画像表:
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_spend=4200、APP_ratio=0.8、level=L4,基本可以定义为“高消费APP核心用户”;第2簇median_spend=300、channel=OFFLINE占大半,就是“低活跃线下散客”。这一步不需要复杂的统计学检验,交叉表就够用。
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.05:
kruskal.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的默认设置。
本文还有配套的精品资源,点击获取