简介:kmodes 是一个面向 Python 数据分析与机器学习开发者的聚类工具库,专门解决分类数据及混合数值与分类数据的聚类难题。它实现了 k 模式与 k 原型两种算法,分别适用于纯分类变量和混合类型数据,弥补了传统 k 均值在处理非数值特征时的不足,适合需要客户分群、市场细分等场景的进阶学习者。资源包共 30 个文件,以 Python 源码为主(20 个 py 文件),涵盖核心算法模块、单元测试、示例脚本与 CSV 示例数据,并有 README、配置文件等辅助文档,压缩包仅 36KB,轻量便于阅读和二次开发。已有 2003 人学习下载。通过源码与示例,读者可以深入理解汉明距离计算、簇中心迭代更新等原理,掌握类似 scikit-learn 的 KModes 接口用法,并参考参数调整与聚类质量评估思路,灵活应对纯分类或混合型数据的实际聚类任务。 做数据分析这几年,我有个特别深的体会:聚类这件事,绝大多数人一上手就只会kmeans,遇到分类数据就傻眼。
比如你手里有一份客户资料,里面全是"性别、居住城市、会员等级、购买品类"这种离散字段,数值型特征少得可怜。这时候硬套kmeans,结果基本是灾难——欧氏距离算不出来的不说,就算硬编码成数字,聚类结果也完全不可解释。
所以当我在GitHub上看到kmodes这个库的时候,第一反应是:终于有人把这件事做对了。它用k-modes和k-prototypes两种算法,专门解决分类数据的聚类问题,而且API设计完全对标scikit-learn,用起来几乎零门槛。这篇博文我就把它的核心原理、实操代码和踩坑经验一次性讲透,给正在做用户分群、市场细分、异常检测的朋友一个可以直接抄作业的参考。
1. 什么时候轮得到kmodes出手:分类数据聚类的痛点与定位
1.1 kmeans处理不了分类数据,根本原因在哪
先看一个最基础的场景。假设你有这么一批数据:
| 用户ID | 所在城市 | 会员等级 | 常用支付方式 | 月消费次数 |
|---|---|---|---|---|
| 001 | 北京 | 金卡 | 微信 | 23 |
| 002 | 上海 | 银卡 | 支付宝 | 15 |
| 003 | 广州 | 金卡 | 微信 | 28 |
这里"月消费次数"可以用欧氏距离算,但"所在城市""会员等级""常用支付方式"全是离散值。你要是硬把它们编码成1、2、3,问题马上就来了:
- "北京"编码成1,"上海"编码成2,难道意味着上海到北京的距离是1?那"广州"编码成3,是不是就比上海离北京更远?
- 实际上城市之间完全不存在这种数值上的大小关系,这类变量在统计学里叫无序分类变量,对它做算术运算本身就没有数学意义。
- 更麻烦的是,编码后kmeans会强行计算"均值中心"。比如"北京=1,上海=2,广州=3"这3个点,均值是2,也就是"上海"。放在分类场景里,这个"均值"根本不存在,聚类中心毫无物理含义。
这就是kmeans在处理分类数据时最致命的问题:它假设数据存在于一个可以计算均值和欧氏距离的向量空间里,而分类数据根本不满足这个前提。
1.2 kmodes的定位:专攻离散特征的聚类专用工具
kmodes这个名字,看字面就很好理解——它就是kmeans的"分类数据版"。这个Python库的核心作用,就是用**众数(mode)替代均值,用汉明距离(Hamming distance)**替代欧氏距离,从而在纯分类特征上完成聚类。
它包含两个主要算法:
- k-modes:专门处理全部特征是分类变量的数据集。比如用户画像、商品属性、问卷调查结果这类数据。
- k-prototypes:处理"分类变量+数值变量"混合的数据集,这也是实际业务里最常见的情况。比如客户数据里既有性别、城市这类离散字段,又有消费金额、访问次数这类连续字段。
实际使用中,k-prototypes的出场率远高于k-modes,因为现实世界几乎没有纯分类的数据集。但理解k-modes是理解k-prototypes的前提,所以下面的原理部分我会按顺序来讲。
1.3 一个指标判断你该不该用kmodes
不需要太复杂的判断逻辑,你就看两点:
- 你的数据集里,分类变量的数量占比是不是超过一半?
- 这些分类变量是不是无序的(例如城市、颜色、支付方式),而不是有序的(例如小学、初中、高中这种有递进关系的)?
只要这两个条件同时满足,优先考虑kmodes;如果分类变量只占一小部分,而且它们是有序的,其实你可以继续用kmeans,或者用主成分分析降维后再聚类。判断的关键不是"能不能用",而是"用了之后聚类结果能不能解释"。
2. 从原理到实现:k-modes和k-prototypes的细节拆解
2.1 k-modes的聚类核心:众数替代均值,匹配度替代距离
k-modes的整体流程和kmeans非常像,区别集中在两个关键步骤上。
先回忆一下kmeans在做什么:初始化k个中心,计算每个样本到中心的欧氏距离,分配到最近的中心,然后重新计算每个簇的均值作为新中心,不断迭代直到收敛。
k-modes把这三步做了如下替换:
- 距离度量:用汉明距离。两个样本在一个属性上取值相同,距离就是0,不同就是1。把所有属性上的差异加起来,就是两个样本之间的距离。
- 中心表示:不再用均值,而是用"众数"。对于每个簇,在每个属性上选取出现频率最高的那个类别,组合起来就是这个簇的"中心"。例如某簇在"城市"属性上北京出现10次、上海5次、广州2次,那这个簇的城市中心就是北京。
- 分配逻辑:样本被分到与它汉明距离最小的那个簇。
用一段伪代码来概括就是这个样子:
初始化k个聚类中心(可以随机选k个样本作为初始中心) while 未收敛: 对每个样本: 计算它与每个中心的汉明距离 把它分配给距离最小的簇 对每个簇: 更新中心为簇内所有样本的众数(逐属性取众数) 验证收敛: 如果中心不再变化或达到最大迭代次数,停止用生活化一点的类比来说:kmeans是在一堆坐标点上找"几何重心",k-modes是在一堆标签卡片里找"最多人贴的那张标签"。前者计算的是空间平均,后者统计的是投票结果。
这里有个很反直觉的点值得注意:k-modes的"距离"不是几何概念,而是"不匹配的数量"。两个样本在5个属性中有3个属性取值不同,那距离就是3。这注定了它的结果是离散的,比如簇中心可能刚好落在某个真实样本上,也可能是一个在真实样本中根本不存在的"组合人"。后者恰恰是正确的——class数据聚类的意义就在于把相似特征的人归为一类,而不是造出一个具体的人。
2.2 k-prototypes:混合特征怎么统一距离
k-prototypes的思路更巧妙:它把分类变量的汉明距离和数值变量的欧氏距离加权相加,得到一个混合距离。公式长这样:
d = 数值特征的欧氏距离 + λ × 分类特征的汉明距离
关键就在这个λ(lambda)上,它决定了两类特征在聚类过程中的"话语权"。设置策略有几种:
- 经验值方案:数据中数值型特征的方差比较大时,λ取小一点,比如0.1~0.5;分类特征的种类多、区分度高时,λ可以适当调大。
- 计算方案:用数值型特征的标准差σ和分类特征的类目数的关系推断。网上的常见做法是让λ约等于数值特征方差的平均值除以分类特征平均匹配频率的比值,但这类计算在真实业务中过于理想化。
- 实操中我推荐的做法:先在λ=0.5起步,跑一轮聚类,然后观察不同λ下的轮廓系数变化。通常0.5是一个经得起检验的起点,再根据聚类结果的可解释性微调。
k-prototypes的核心优势就在这里:它不需要把分类变量做one-hot编码,也不需要把数值变量离散化,直接一个一把原始数据喂进去。节省预处理时间的同时,保留了原始字段的业务含义,这对后续结果解释至关重要。
2.3 复杂度与收敛性:大数据量下的真实表现
k-modes每次迭代的复杂度大约是O(nmk),n是样本数,m是特征数,k是簇数。这意味着它的扩展性和kmeans基本属于同一量级,百万级样本配十几个特征,跑起来压力不大。
收敛性方面,k-modes的优化目标和kmeans类似——最小化簇内不匹配总数。每一步迭代都会让目标函数单调不增,所以算法保证会收敛到局部最优。需要注意的是,它跟kmeans一样对初始中心敏感,多跑几次不同的初始化(n_init参数)取最优结果,几乎是必需品。
我在真实项目中拿过一份10万行、8个字段(其中6个分类字段)的数据测试,设置k=5、n_init=10,用普通笔记本跑一轮大概40秒到1分钟,完全在可接受范围内。
3. 环境准备与安装:pip一行命令背后的隐藏坑
3.1 三步完成环境准备
kmodes的安装非常标准,如果你已经能正常运行scikit-learn,那基本上只需要一步操作:
pip install kmodes考虑到国内网络环境,如果直接pip下载太慢,用清华镜像会快很多:
pip install kmodes -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后验证一下是否成功:
python -c "from kmodes.kmodes import KModes; from kmodes.kprototypes import KPrototypes; print('kmodes ready')"如果顺利打印出kmodes ready,说明环境没有问题。
3.2 依赖库的版本兼容性要注意
kmodes的核心依赖是numpy、pandas、scikit-learn和scipy。多数情况下,新版本库都能兼容,但有几个坑我在不同环境里踩到过:
- Python 3.10以下版本用得很稳,在3.11及更高版本上,建议把kmodes升级到0.12.2及以上,老版本可能存在编译问题。
- numpy版本过高(比如2.x)时,个别旧版kmodes会报
module 'numpy' has no attribute 'bool8'之类的错误。解决方式是升级kmodes到最新版,或者把numpy临时降级到1.26.x,二选一,推荐前者。 - 如果你同时使用pandas 2.x,记得把kmodes更新到最新版,因为早期版本对pandas的API变更不敏感,但保险起见一起升级更省心。
我的建议是:直接建一个虚拟环境,统一安装最新版kmodes、numpy、pandas和scikit-learn,不要贪图省事用系统全局环境。数据项目的依赖管理不是小事,虚拟环境是性价比最高的防护。
3.3 不需要额外装的东西
kmodes的接口完全仿照scikit-learn,所以你不需要额外学一套新语法,会fit、predict、cluster_centers_这几个基本操作就够了。可视化方面,matplotlib和seaborn够用,不需要专门装聚类可视化的第三方包;如果你想做降维展示,scikit-learn里自带的PCA和TSNE也完全够用了。
4. 完整实操:用kmodes完成一个真实的客户分群
4.1 数据准备:构造一份混合类型数据集
我用一份模拟的电商客户数据作为示例,字段设计如下:
- 分类字段:所在城市、会员等级、常用支付方式、性别
- 数值字段:月均消费金额、月均消费次数
这是典型的需要用k-prototypes处理的数据结构。实际读取数据时主要用pandas:
import pandas as pd import numpy as np data = pd.DataFrame({ 'city': ['北京', '上海', '广州', '深圳', '北京', '上海', '广州', '深圳', '北京', '上海'], 'member_level': ['青铜', '白银', '黄金', '铂金', '钻石', '青铜', '白银', '黄金', '铂金', '钻石'], 'payment': ['微信', '支付宝', '微信', '银行卡', '支付宝', '微信', '支付宝', '银行卡', '微信', '支付宝'], 'gender': ['男', '女', '男', '女', '男', '女', '男', '女', '男', '女'], 'spend': [3500, 1200, 800, 6000, 20000, 2500, 300, 1500, 8000, 9000], 'frequency': [23, 8, 5, 30, 45, 15, 2, 12, 28, 35] })结构上要注意一点:数值列和分类列要分开管理。在调用KPrototypes之前,先把列名按类型整理好,这一步看起来简单,但它直接决定了后面聚类结果的正确性。
4.2 KPrototypes完整流程:从初始化到结果解读
第一步,导入模型并指定分类列的索引。KPrototypes的入参categorical是一个索引列表,对应DataFrame中所有分类特征的列位置:
from kmodes.kprototypes import KPrototypes # 指定分类列的索引:city=0, member_level=1, payment=2, gender=3 categorical_cols = [0, 1, 2, 3] model = KPrototypes(n_clusters=3, init='Huang', n_init=5, verbose=1, random_state=42) clusters = model.fit_predict(data, categorical=categorical_cols) data['cluster'] = clusters这里的几个参数值得细说:
- init='Huang':初始化中心的方法。Huang方法是从数据集中选k个众数组合较远的样本作为初始中心,比完全随机初始化更稳定。文档里还支持Cao方法,它基于样本密度选取初始中心,在类别特征多且区分度高时效果更好。我一般默认用Huang,如果轮廓系数不理想,再换Cao对比。
- n_init=5:随机初始化5次,取目标函数最小的结果。实际项目中我会设为10,计算量大点但结果更稳。
- verbose=1:实时打印迭代过程中的代价函数值,方便观察收敛情况。代价函数在这里其实就是所有样本到其所属中心的总不匹配数,数字越小说明聚类越紧凑。
第二步,查看聚类中心,理解每个簇的实际含义:
# 聚类中心:数值列返回均值,分类列返回众数 centroids = model.cluster_centroids_ print(centroids)k-prototypes的中心由两部分组成:数值特征的均值和分类特征的众数。比如第一个簇的中心可能是"城市=北京、会员等级=铂金、支付方式=微信、性别=男、消费金额=8500、月均消费次数=30"。这个结果可以直观解释成"一线城市高消费男性用户",业务含义非常清晰。
第三步,对新样本做预测,这一步在生产环境几乎必用:
new_data = pd.DataFrame([['上海', '黄金', '支付宝', '女', 6000, 20]]) pred_cluster = model.predict(new_data, categorical=categorical_cols) print(pred_cluster)需要注意,传入predict的新数据列顺序要和训练集完全一致,特别是分类列的位置不能变,否则模型会静默地按错位的数据计算距离,得到完全不可用的结果。这个坑我在实际项目中遇到过一次,数据量小的时候还真不显眼。
4.3 KModes纯分类数据的实操Demo
如果你的数据全是分类字段,那就用KModes:
from kmodes.kmodes import KModes pure_categorical_data = data[['city', 'member_level', 'payment', 'gender']].copy() km = KModes(n_clusters=3, init='Huang', n_init=5, verbose=1, random_state=42) pure_clusters = km.fit_predict(pure_categorical_data) pure_categorical_data['cluster'] = pure_clusters # 查看每个簇的众数中心 print(km.cluster_centroids_)代码逻辑几乎一样,只是不需要传categorical参数,因为KModes默认所有列都是分类的。画个轮廓图对比一下:
from sklearn.metrics import silhouette_score # 对KModes距离需要自定义样本间距离,直接用库从簇内不匹配总数估算不是标准做法 # 更直观的做法是先看簇内样本各属性的众数分布是否稳定关于KModes的聚类质量评估,有一个细微但重要的点:sklearn的轮廓系数是针对欧氏距离设计的,不能直接用在k-modes上,除非你自定义距离矩阵。如果真的需要量化评估,可以参考kmodes文档中提供的代价函数值变化,以及聚类结果在业务上的可解释性,这两个指标比抽象的轮廓系数更实用。
4.4 关键参数选择:k值和lambda值怎么定
k值(簇数)的选择
和kmeans一样,k值不会自动算出最优,需要人工判断。实操中我用两个手段配合:
- 肘部法则的改编版:绘制"簇内不匹配总数"随k变化的曲线。这个值从k=2开始会迅速下降,降到某个拐点后下降速度明显变缓,这个拐点对应的k就是比较合适的簇数。注意这个和kmeans的SSE肘部图是同一个逻辑,只是Y轴从欧氏距离之和变成了汉明距离不匹配数之和。
- 业务可解释性校验:跑出结果后,逐个簇看中心特征组合。如果某个簇的中心特征是"北京+上海"混杂,说明k值太小或者初始中心选的不好,需要调整。否则即使统计指标好看,落到业务上仍然没法用。
lambda值的选择
对于KPrototypes,这个λ直接影响分类特征和数值特征的权重比。我一般这样操作:
- 先固定λ=0.5,跑一轮,记录聚类结果。
- 尝试λ=0.1、0.5、1.0、2.0等几个值,观察每个簇的"纯度"变化——比如重点关注分类特征在每个簇中是否规律明显,数值特征的分辨是否清晰。
- 选择让聚类结果最平衡、各簇最可解释的那个λ。
有一个小经验:如果你发现聚类结果几乎全被数值特征主导(比如按消费金额分成了高、中、低三档,分类特征完全没起作用),大概率是λ偏小;反过来,如果数值特征完全失去区分度,每个簇的均值几乎相同,那就是λ偏大了。调整的方向很直观。
4.5 别忘了处理缺失值和字符串标准化
实际数据里分类字段经常有脏数据,比如"北京"和"北京市"并存、"VIP"和"vip"混用。这些看似小的问题对聚类结果的影响很大——因为k-modes完全靠"取值是否相等"计算距离,"北京"和"北京市"在算法看来是两个毫无关系的类别。
所以在喂数据前,必须做的清洗包括:
- 去空格、统一大小写、规范同义表达,比如把"北京市"替换为"北京"。
- 缺失值不能直接留空。分类字段可以用"未知"填充,数值字段用中位数或均值填充。千万不能删行,尤其样本量不够大时。
- 对高基数分类特征(比如城市几百个、SKU几千个),可以直接保留,但要注意如果分类取值过多,汉明距离会偏向这个字段,导致聚类几乎只看这一个特征。这种情况下可以考虑先对高基数特征做频次编码,比如把出现次数低于某个阈值的类别合并为"其他"。
5. 常见问题排查与项目实战心得
5.1 问题速查表:从报错到结果异常
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
报错ValueError: cannot specify both categorical and ... | 同时传了多个参数 | 核对KPrototypes中只传categorical,不要重复指定其他分类参数 |
| 聚类结果和预期不符 | 分类特征数据未清洗 | 检查特征值是否有同义不同表述、空格、大小写差异 |
警告ConvergenceWarning | 达到最大迭代次数仍未完全收敛 | 调大max_iter,如从100调至300 |
| 数值列全是0或很小 | λ设置过小 | 调大λ后重新聚类 |
| 分类特征完全不起作用 | λ设置过大,数值特征被稀释 | 调小λ或使用KModes单独看分类特征效果 |
5.2 实测中值得注意的三个细节
第一,categorical参数的位置和顺序不能错。KPrototypes要求categorical传入的是列索引列表,而这些索引是相对于传入的DataFrame的。如果你训练时用的是df的0到3列,预测新样本时也必须保持同样的0到3列顺序,不能先筛选列再预测。这其实是所有基于位置的API最容易出问题的地方。
第二,n_init不要省。k-modes的初始化对结果影响极大,一次初始化很可能陷进很差的局部最优。用Huang或Cao初始化 + n_init=10,是我跑过的各种数据集上最稳的组合。代价是多花点时间,但减少反复试错的成本远比这点时间高得多。
第三,聚类质量不要只盯着数学指标。kmodes这类算法,最终目的是帮业务做分群,所以每跑完一轮,一定要把簇中心打印出来,逐条看业务解释是否通顺。我见过有人拿轮廓系数选出来一个"最优"模型,但簇里混着互相矛盾的特征组合,这种结果在业务上是废的。宁可指标差一点,也要保证每个簇能被一两句话讲清楚。
5.3 这个库还有什么可挖的
kmodes本身提供的是基础的KModes和KPrototypes两个类,但它的设计让我们很容易扩展。
- 并行调参:配合sklearn的GridSearchCV或Optuna,利用kmodes的接口做k值和λ的搜索。虽然需要自定义评估函数,但是可操作性很强。
- 加速版本:官方库没有GPU版本,但如果你数据量太大(千万级),可以考虑抽样跑一次确定簇中心,再全量分配;或者用Mini-Batch的方式,每一批只更新一部分样本所属的簇。
- 与后续分析联动:聚完类不是终点,每个簇的画像可以和后续的决策树、关联规则分析衔接,整个pipeline可以写成独立的模块。我倾向把kmodes封装在一个通用分群类里,输入DataFrame、输出带簇标签和簇画像的报表,这样后期不同项目复用成本会低很多。
6. 写在最后:基于个人经验的小结
kmodes这个库的定位很清晰:解决kmeans解决不了的分类数据聚类问题。它不花哨,但非常实用,适合各种需要做用户分群、群体画像、异常检测的实战项目。
从我自己的经历来说,最初踩坑主要是在不理解原理的情况下把它当kmeans用,结果调出来的参数完全没意义。后来认真琢磨了"众数中心"和"汉明距离"这两个设计,再结合业务场景去解释聚类结果,才能把一组数字真正变成可落地的分群方案。
最后分享一个我实践中的小心得:拿到任何聚类任务,先别急着调库,先把数据的变量类型梳理清楚。哪些是类别字段、哪些是数值字段、哪些高基数字段需要合并,这些判断决定了选哪个算法、怎么设参数。工具只是把决策落实的手段,真正影响结果好坏的,永远是你对数据和业务的理解深度。
本文还有配套的精品资源,点击获取