尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

kmodes实战:用k-modes与k-prototypes解决分类数据聚类难题

kmodes实战:用k-modes与k-prototypes解决分类数据聚类难题 简介kmodes是一个面向数据分析和机器学习从业者的Python库专注于分类数据及混合数据的聚类场景。它同时实现了k模式与k原型两种聚类算法以汉明距离处理分类属性、以欧几里得距离处理数值属性弥补了传统k-means无法直接处理非数值型特征的局限API设计贴近scikit-learn便于快速集成到现有工作流。压缩包共30个文件大小约36KB以20个Python源码文件为核心辅以2个CSV示例数据集、2个Markdown说明文档以及测试用例、初始化配置、许可证等配套内容覆盖从算法原理到实际调用的完整链路。资源内含stocks、iris、soybean等经典数据集脚本以及benchmark系列性能对比脚本读者既能对照源码理解距离计算、聚类中心更新等核心实现细节又能通过示例数据直接体验模型训练与预测结合文档可快速掌握n_clusters、init等参数调优思路。目前已有2003人学习下载适合希望深入掌握分类数据聚类原理、或在项目中应用k-modes与k-prototypes算法的开发者参考。 写这篇东西的起因是上个月帮业务部门做会员分层时又看到有人把性别、城市等级、购买品类这类字段全部 one-hot 化之后丢给 kmeans结果跑出来的“用户画像”怎么看怎么假。其实这类分类数据聚类有更对口的工具kmodes。这个 Python 库实现了两种专门处理分类数据的聚类算法——k模式k-modes和 k原型k-prototypes前者处理纯分类字段后者处理分类字段和数值字段混合的场景。这篇文章就把它的原理、接口、调参经验和实战坑一次性讲清楚适合正在做用户画像、市场细分、人群分层的数据分析同学参考。1. 分类数据聚类为什么kmeans在这里失灵了1.1 一个把kmeans用哭的场景先说说我最初接手的一个需求。数据里有八千多个注册会员字段有七八个看起来很简单性别、年龄段、所在城市等级、注册渠道、最近购买品类、支付方式偏好、月均消费金额、会员活跃度。前几个字段全部是文本型比如“男/女”“一线/二线/三线”“App/小程序/线下门店”“美妆/数码/食品”。月均消费金额是数值。让我头疼的正是前面那些字段——它们不是数字而是分类变量彼此之间没有大小和远近的概念。我当时的第一反应和你可能一样把分类字段转成数字再聚类。但直接映射成 0、1、2、3 是不对的。城市等级里 “三线0、二线1、一线2” 看似合理可“1-0”的距离真的代表“从三线到二线”的程度吗性别映射成 0 和 1 之后均值 0.6 又是什么意思分类变量的取值是离散标签硬编码成数字会给算法注入虚假的连续关系和虚假的距离语义聚类结果自然没法解释。1.2 为什么one-hot之后依然不行既然直接编码不行那就 one-hot 展开。这也是很多人包括当年的我最常走的弯路。one-hot 确实解决了“类别距离”的语义问题但随之而来的是三个新麻烦。第一维度暴涨。原本一个“购买品类”字段有 12 个取值one-hot 之后变成 12 列七八个分类字段加起来轻松超过 40 列。数据薄、特征宽kmeans 在这种高维稀疏空间里计算欧氏距离绝大多数维度的贡献都是 0中心点会被少数活跃维度带偏。第二簇中心不可解释。kmeans 的簇中心是“均值向量”在 one-hot 后的空间里均值可能是 0.6、0.3、0.1 这种“概率分布式”的数字。我拿着中心向量去给业务解释“这一人群的特点”最后只能说“他们有 60% 概率偏好某品类”业务同学听得一头雾水。第三one-hot 把分类变量内部的频次分布信息丢掉了。某个类别占比 80%另一个占比 5%one-hot 编码后它们是平等的 0/1 维度算法完全感知不到这个先验差异。而 k-modes 算法在设计时就考虑到了这一点它用“众数”代表簇中心天然保留类别本身的分布语义。1.3 kmodes在聚类算法家族里的定位接触到的聚类方法不少实际选型时核心是看数据类型。我用一张表总结一下常用算法和处理对象的匹配关系算法适配数据中心/代表点典型场景kmeans连续数值均值用户消费金额、行为数值分群k-modes纯分类众数画像标签、偏好品类聚类k-prototypes分类数值混合均值众数电商会员、风控人群分层DBSCAN数值/空间密度可达异常检测、地理空间聚集AGNES视距离而定无固定中心小样本、层次关系挖掘如果你拿到的数据全是文本型标签让我选我会直接上 k-modes如果既有“年龄”又有“性别”那就用 k-prototypes。这两个算法正是 kmodes 这个库的核心也是本文标题里“k模式”和“k原型”的由来。它填补了 kmeans 族算法在分类数据上的空白而且接口设计得很像 scikit-learn上手成本非常低。2. k-modes算法拆解众数当中心0-1距离算匹配2.1 中心向量的更新逻辑均值换成众数k-modes 的整体迭代思路和 kmeans 很像初始化 k 个中心 - 分配样本到最近中心 - 更新中心 - 判断收敛。关键差异在于“中心”怎么算。kmeans 的中心是簇内所有样本每个维度的算术平均比如年龄均值 28.7 岁。但 k-modes 的中心是一个“众数向量”也就是簇内样本在每个分类特征上出现次数最多的那个取值。举个例子一个簇里有三个样本分别是红色大圆形、红色大方形、蓝色大圆形那么这个簇的众数中心就是红色大圆形。这个设计非常巧妙。中心向量里的每一个取值都真实存在于样本中运营同学看聚类结果时不需要理解“0.6 代表什么”只需要看“这一簇用户偏好红色、大尺寸、圆形”一句话就能读懂。这也是 k-modes 在业务侧接受度远高于 one-hotkmeans 的原因。2.2 距离函数为什么不匹配计数比欧氏距离更合理在 kmeans 里样本和中心的距离是欧氏距离计算的是数值差的平方和而在 k-modes 里距离函数换成了简单的“0-1 不匹配计数”逐特征比较样本和中心的取值相等记 0不相等记 1所有特征累加。样本和中心的差异越大这个值越大。例如样本蓝色小圆形与中心红色大圆形比较前两个特征不匹配第三个特征匹配所以距离是 2。这个距离本质上是在统计“你和簇中心有几个地方不一样”直接面向分类数据做判断不引入任何数值计算上的伪逻辑。有人可能会问为什么不用汉明距离、编辑距离这类更复杂的度量在多数表格型分类数据中各特征都是独立枚举值特征之间没有序列关系逐特征比较就是最直观、最不会误导算法的做法。算法迭代时目标函数是“簇内样本与中心的总不匹配数最小化”不匹配数越低说明簇内成员在各类别取值上的共识度越高。2.3 用纯Python理解核心迭代过程为了把问题说透我写了一个极简的思路演示代码不依赖 kmodes 库只还原算法主循环。它虽然简陋但能帮你建立“分类数据聚类到底在算什么”的直觉。from collections import Counter import numpy as np def kmodes_core(X, k, max_iter100): # X: numpy数组每个元素是分类取值(字符串或整数均可) rng np.random.default_rng(0) centers X[rng.choice(len(X), k, replaceFalse)].copy() for _ in range(max_iter): # 1) 分配逐特征比较不相等则累计1 dist np.array([[np.sum(x ! c) for c in centers] for x in X]) labels np.argmin(dist, axis1) # 2) 更新每个簇每列取众数作为新中心 new_centers [] for j in range(k): cluster X[labels j] if len(cluster) 0: new_centers.append(centers[j]) else: new_centers.append([ Counter(cluster[:, f]).most_common(1)[0][0] for f in range(X.shape[1]) ]) new_centers np.array(new_centers) # 3) 收敛判断 if np.array_equal(new_centers, centers): break centers new_centers return labels, centers这段代码里最核心的就是两行np.sum(x ! c)完成了 0-1 距离计算Counter(...).most_common(1)[0][0]完成了众数提取。实际项目中你不需要自己写这十几行kmodes 库已经把这套逻辑封装得很成熟了但理解这几个关键点能帮你少走很多参数调优的弯路。k-modes 算法并非完美无缺它对初始中心敏感容易陷入局部最优这正是后面要单独讲初始化和 n_init 的原因。3. k-prototypes算法把数值属性和分类属性放在一个距离公式里3.1 混合距离公式与gamma权重绝大多数真实业务数据都不是“纯分类”的更多是“年龄性别消费金额购买偏好”这种混合形态。k-modes 处理不了数值列kmeans 处理不了分类列k-prototypes 就是来解决这个撕裂问题的。它的距离公式是两段加权相加。设样本 X 和中心 C 有 p 个数值属性、q 个分类属性距离 D 数值部分的欧氏距离 gamma × 分类部分的不匹配数。数值部分衡量“差多少”分类部分衡量“不一样到哪一步”gamma在 kmodes 库中对应参数 gamma则负责把两段距离放在同一个量纲上比较。gamma 的直觉意义是“一个分类属性不匹配等价于数值属性差多少”。如果 gamma 设得太大分类属性的差异被放大数值相似的样本可能被强行分开设得太小分类属性形同虚设聚类结果几乎完全由数值属性主导。在你没有先验知识时kmodes 库会根据数据自动估算 gamma 的初始值你也可以后续手动调节观察聚类结果是否更贴合业务预期。3.2 中心更新数值列均值、分类列众数k-prototypes 的中心更新策略是“两套规则各管各的”数值列继续用簇内均值分类列用簇内众数。这样得到的中心是一个混合向量比如“年龄 31.2 岁、男性、月均消费 850 元、偏好数码品类”业务描述时自然流畅。这个设计在数学上是有依据的数值部分最小化欧氏距离时最优解是均值分类部分最小化不匹配数时最优解是众数。两段目标函数可以分别优化所以 kmodes 库实现中会把数值列和分类列拆开处理迭代时保持各自的更新规则不变。理解这一点你就能解释为什么 KPrototypes 需要你单独告诉它哪些列是分类列——因为算法内部要用完全不同的距离和中心更新方式来对待这两类特征。3.3 什么时候该用k-modes什么时候该用k-prototypes我在项目里总结出一个很简单的选型经验如果一张表里所有特征都是枚举型标签比如“性别城市等级渠道偏好”直接用 KModes如果表里有年龄、金额、时长这类数值字段同时又有分类字段那就用 KPrototypes 并传入分类列索引如果表里全是连续数值直接回到 KMeans 即可不需要强行用 kmodes 库。这里有轻微的重复但有实际意义。遇到混合数据却只用 k-modes数值信息的价值会被浪费只用 kmeans 又会让分类字段失真。选型对了聚类效果至少提升一半。4. kmodes库实战基础安装、数据准备和核心API4.1 环境安装与数据预处理kmodes 是标准 PyPI 包安装没有特殊操作一行命令即可pip install kmodes它依赖 numpy、scipy、pandas 和 scikit-learn。我实测在 Python 3.8 到 3.11 的环境下都能正常工作。如果你在 pandas 2.x 的新环境里安装建议直接装 0.12.x 以上版本旧版本在 pandas 2.0 上偶尔会有接口兼容问题。数据预处理这里有一个必须注意的点kmodes 不接受缺失值。传入的数据里如果存在 NaN训练时会直接报错或产生不可预期的结果。一般做法是对分类列用众数填充对数值列用中位数或均值填充如果缺失比例过高也可以直接删除对应样本。另外分类列最好保持为字符串类型或者用 pandas 的astype(category)转换。不要在喂入 kmodes 之前做 one-hot 编码这一点前面已经解释过了kmodes 的设计初衷就是直接消费原始分类取值。4.2 KModes和KPrototypes的核心参数与属性两个类的核心参数基本一致关键差异在于 KPrototypes 多了 categorical 这个参数。下面是常用参数说明表参数说明经验值n_clusters聚类数 k用肘部法或业务诉求确定init初始化方式Huang、Cao、random优先 Caon_init随机初始化的运行次数取代价最小结果建议 10 以上random_state随机种子保证结果可复现固定一个整数verbose是否打印迭代日志调试时置 1平时默认 0categoricalKPrototypes 专用指定分类列索引或列名必传模型训练完成后值得关注的属性有三个cluster_centroids_是最终的簇中心直接打印出来就能看到每个簇的“代表性用户”cost_是目标函数最终值即簇内总不匹配数KPrototypes 中还会叠加上数值部分的加权距离它是后续选择 k 值的重要依据n_iter_是实际迭代轮数如果这个数字特别大而 cost 下降不明显说明初始中心可能选得不好。4.3 第一个跑通的聚类代码下面这段代码是我最常用的最小可用模板直接分为 KModes 和 KPrototypes 两种情况。实际使用时你只需要替换文件路径和分类列索引。import pandas as pd from kmodes.kmodes import KModes from kmodes.kprototypes import KPrototypes df pd.read_csv(members.csv) # 假设第2、3、6列是分类列 cat_cols_idx [2, 3, 6] # 纯分类数据用 KModes model1 KModes(n_clusters4, initCao, n_init10, random_state42) df[cluster] model1.fit_predict(df.iloc[:, cat_cols_idx].to_numpy()) # 混合数据用 KPrototypes必须告诉它分类列的位置 model2 KPrototypes(n_clusters4, initCao, n_init10, random_state42) df[cluster] model2.fit_predict(df.to_numpy(), categoricalcat_cols_idx) # 查看中心 print(model2.cluster_centroids_) print(cost:, model2.cost_)跑完这段df 里就会多出一个 cluster 列每一行样本属于哪个簇一目了然。这段代码是我所有 kmodes 项目的基础模板后面的调优和评估都是在这个框架上叠加的。5. 初始化方法与n_init让结果稳定不漂移5.1 Huang、Cao、random三种init的行为差异k-modes/k-prototypes 的迭代过程也是坐标下降式的初始中心一旦选得不好很容易陷入局部最优。kmodes 库提供了三种初始化方式提到了很多次这里展开说说区别。initHuang是默认方式源自 Huang 在 1997 年论文里的做法直接选取数据集中前 k 个样本来初始化中心。优点是计算量小、速度快缺点是结果受数据顺序影响极大。如果 CSV 文件恰好按某种业务规则排序前 k 个样本可能都集中在同一个群体里聚类很容易跑偏。initCao是 Cao 等人提出的改进方法核心思路是先根据每个属性的频次分布计算样本的密度优先选择那些“彼此差异大且能代表不同分布区域”的样本作为初始中心。实测下来Cao 初始化的聚类结果通常比 Huang 更稳定cost 也更低。它多出来的计算时间在中小数据集上完全可以接受。initrandom就是完全随机抽 k 个样本当中心效果波动最大一般只在对比实验时用。如果你拿不定主意我的建议是直接选 Cao。5.2 为什么n_init可以救回局部最优即使选定了初始化方式单次运行仍然可能落入局部最优。kmodes 库的n_init参数和 kmeans 里的逻辑一样把整个聚类过程重复跑 n_init 次每次都换一组初始中心最后只保留目标函数 cost 最小的那次结果。举个例子n_init10意味着算法要完整跑 10 遍完整的“分配-更新-收敛”循环然后挑最好的。这当然会增加耗时但分类数据聚类通常比高维数值聚类更快10 次运行的开销通常可控。我在实际项目中基本都会设成 10数据量特别大的时候先跑一遍 3 次快速探索确定大致的 k 后再用 10 次跑最终版本。还有一个容易被忽略的参数是random_state。它是随机种子设置了固定整数后不管跑多少次结果都能完整复现。这对于写报告、做调试、跟业务沟通都非常重要——否则每次重启脚本聚类结果都不一样你都没法解释到底哪个是“正确”的。5.3 参数选择的经验建议根据我的经验参数组合可以按数据规模分三档。数据量在一万行以内initCao、n_init10、固定random_state这是最省心的组合数据量在十万行级别可以先抽样两万行调参确定 k 和 gamma 之后再全量跑这时n_init可以降到 5百万行以上建议用 MiniBatch 思路或者先做分层抽样kmodes 本身没有 mini-batch 版本硬扛大数据会非常慢。分类列特别多比如超过 10 列时我还会对比 Huang 和 Cao 两种初始化的cost_差异。如果两者差距不大用 Huang 更省时间如果 Cao 明显更优那就多花点时间换更好的聚类质量。这个对比只需要很小的代码量却能让你对模型的稳定性做到心里有数。6. 完整实战电商会员混合数据聚类与结果解读6.1 数据说明与预处理用一个我最近刚做完的案例来讲透整个流程。某电商平台需要把 8000 名会员做一次分层字段包括年龄数值、会员天数数值、月均消费金额数值、性别分类、城市等级分类、购买品类偏好分类、支付方式偏好分类、活跃度等级分类。前期数据处理我按固定套路来先检查缺失值分类列性别、支付方式用众数填充金额这类数值列用中位数填充然后把分类列统一转成字符串保险起见没做 one-hot最后把数据组装成 numpy 数组记录分类列的索引位置。数据准备这一步大约花十几分钟却决定了后续训练是否能顺利进行。6.2 用肘部法确定k值聚类和分类不同没有标签告诉你该分成几类。k 值的确定我习惯用“肘部图”对不同 k 值分别跑 KPrototypes记录每个模型的 cost_然后画一条 k-cost 曲线。曲线拐点处就是比较合适的 k 值。import matplotlib.pyplot as plt data df.to_numpy() cat_idx [3, 4, 5, 6, 7] costs [] K_range range(2, 11) for k in K_range: model KPrototypes(n_clustersk, initCao, n_init10, random_state42) model.fit_predict(data, categoricalcat_idx) costs.append(model.cost_) plt.plot(list(K_range), costs, markero) plt.xlabel(k) plt.ylabel(cost) plt.title(KPrototypes Elbow Curve) plt.show()在这份数据上k 从 2 增加到 4 时 cost 下降非常快从 4 到 5 之后下降趋于平缓所以我最终选了 4 个簇。要注意肘部法给出的是“数据层面的最优”最终定 k 还要结合业务侧看每个簇是否可分、可解释、可落地。6.3 聚类结果的可解释性分析确定 k4 后用最基础的参数组合重新建模然后重点看cluster_centroids_。我在上面已经提到过这个属性的价值这里直接展示解读逻辑。四个簇的中心向量被我提取出来后大致是这样簇年龄月均消费性别城市等级品类偏好活跃度022310女二线美妆高1382100男一线数码中245520女三线食品低329880男新一线服饰高看到这个结果我很明确地给业务方总结出四类人群簇 0 是年轻女性美妆用户价格敏感度高簇 1 是中年高消费数码人群典型的高价值客群簇 2 是三线城市家庭型用户偏日常刚需簇 3 是追求潮流的青年男性服饰人群。这四类在业务侧都有清晰的运营动作簇 1 重点做专属客服和高端产品推荐簇 0 适合新品试用和社交裂变簇 3 适合联名款推送簇 2 则以促销唤醒为主。聚类分析做到这一步才算真正闭环——不是算出几个数字就结束了而是要落到可执行的分层策略上。7. 避坑清单我在真实项目里踩过的七个问题7.1 缺失值kmodes不接受NaN这个坑我帮同事排查了整整一个下午。训练时 KPrototypes 直接抛出一堆晦涩的 numpy 错误一开始根本没有联想到是缺失值问题。后来检查数据才发现某个分类列里有 300 多个空值。kmodes 底层在比较字符串时不认识 NaN所以会直接报错或者输出乱码。正确的做法是在训练前把所有列过一遍isna().sum()分类列众数填充数值列中位数填充。7.2 categorical参数传的是列索引不是列名不少人在 KPrototypes 里直接把[gender, city_level]这样的列名传给 categorical在旧版本中会直接报错因为旧版本只接收整数索引。新版其实已经支持列名但为了兼容性和保险起见我还是建议先记下分类列的整数索引比如[3, 4, 5, 6, 7]。训练前打印一下df.dtypes确认列顺序能省去很多定位问题的精力。7.3 数值列要标准化但分类列不要动KPrototypes 的数值部分用的是欧氏距离如果数值列量纲差异巨大比如“年龄”在 0-100 范围内“月均消费金额”在 0-100000 范围内后者就会在距离计算中占据绝对主导分类属性的影响被完全淹没。我一般在建模前用StandardScaler只对数值列做标准化分类列原样保留。这个细节重要到什么程度呢有一版模型忘记标准化聚类结果变成了纯按消费金额一刀切运营看到后直摇头。7.4 版本匹配问题pandas 2.x和kmodes的兼容性如果你用 pandas 2.0 及以上版本又装了比较老的 kmodes 版本训练时可能遇到AttributeError或者底层 C 扩展不兼容的问题。我个人实测下来把 kmodes 升级到 0.12.2 以上可以解决绝大多数冲突。安装时如果碰到编译相关的报错优先选择官方预编译的 wheel 包不要试图在本地重新编译。7.5 性能瓶颈大数据量下n_init别设太大kmodes 的中心更新是逐特征比较和统计不像 kmeans 有高效的矩阵运算优化所以整体速度比 kmeans 慢一个量级。在几十万行数据上用默认参数跑可能要等上很长时间。我的处理方式是先抽样跑通流程、确定 k 和 gamma再在必要的时候全量训练全量训练时把n_init降到 3-5并开启verbose1观察每轮进度。如果你对某个聚类结果不放心可以多跑几个随机种子对比 cost选最小者。7.6 结果评估无标签时看cost和业务解释有标签时看ARI/NMI分类数据聚类没有绝对的“标准答案”评估方式分两种。没有真实标签时主要看 cost 是否在同 k 值下明显低于其他初始化组合以及每个簇的中心向量是否具备业务上的可解释性。如果某两个簇的中心向量几乎一致说明 k 选大了该合并。如果数据本身带有用户标签比如是否流失、是否高价值就用adjusted_rand_score或normalized_mutual_info_score这类外部指标对比聚类结果与真实标签的吻合度。外部指标我一般放在最后做验证不会用它作为唯一的调参依据。7.7 不要漏掉业务校验这一关模型跑完之后我习惯做一步“抽样核对”每个簇随机抽 20 条真实样本人工看这些样本的记录是不是和簇中心描述的画像一致。这一步看似笨拙却能在最短时间内发现数据倾斜、编码错误或者聚类失效的问题。上一版用 one-hotkmeans 的方案恰恰就是在这步暴露出“中心均值根本对不上任何真实用户”的硬伤。聚类结果只有通过业务校验才算真正可以交付。后面我把这套流程固化成了周度跑批的脚本每周末自动把新增会员分一次层再把聚类中心的变化情况同步到运营活动里。至少到目前为止这个处理分类数据的库还在稳定服役。如果你也正在给一堆全是文本型字段的表格找聚类方案直接从 kmodes 开始试就对了别把时间先浪费在 one-hot 和 kmeans 的组合上。本文还有配套的精品资源点击获取
返回列表