简介:本资源是面向数据科学学习者与Python开发者的kmodes聚类算法开源实现,专为解决分类变量及混合型数据(数值+类别)的无监督聚类难题而设计,适用于客户分群、文本分析、生物信息等实际场景。包内共30个文件,以20个核心Python源码(含kmodes.py、kprototypes.py及多个benchmark与示例脚本)、2个Markdown说明文档、2个CSV示例数据集(soybean.csv、stocks.csv)为主,辅以测试模块、配置文件(.yaml、.coveragerc)和完整README文档,结构清晰、开箱即用,压缩包仅36KB,轻量高效。已有2008人学习下载,涵盖从算法原理理解到scikit-learn风格API调用的完整实践路径。读者可直接复用示例代码(如iris.py、soybean.py)快速上手k-模式与k-原型聚类,获取聚类中心、标签预测结果,并参考benchmark脚本对比不同初始化策略与并行性能,是掌握非数值型聚类技术的高价值实操素材。
1. kmodes 是什么:当你的数据全是“文字标签”,K-means 就彻底罢工了
你手头有一堆用户资料:职业是“教师”“程序员”“设计师”,城市是“北京”“成都”“杭州”,婚姻状态是“已婚”“未婚”“离异”——全是文字,没有一个数字。你想把相似的人分几组,比如“一线城市年轻未婚白领”“二三线已婚家庭型用户”。这时候,如果你硬套 K-means(最常用的聚类算法),它会当场“报错”或给出荒谬结果:因为 K-means 的核心是算“距离”,而它只认数字坐标——它根本不知道“教师”和“程序员”的距离是多少,“北京”和“杭州”在数学上该相隔多远。kmodes 就是为这种场景专程设计的“翻译官”:它不靠欧氏距离,而是用“不一致个数”来衡量相似性——两个用户在5个字段里有3个值不同,那它们的“kmodes 距离”就是3。打个比方,K-means 像一位只会用直尺量长度的工程师,而 kmodes 是一位擅长比对身份证信息的档案管理员:他不关心“姓名笔画数”,只看“性别、籍贯、出生年份、民族、住址”这五栏里有几栏对不上。这个区别看似微小,却是分类数据聚类能否落地的关键分水岭。搞懂 kmodes,不是为了多学一个库名,而是避免你在处理问卷、用户画像、商品标签、医疗诊断编码这类真实业务数据时,一上来就选错工具、白跑几十小时训练、最后发现分组毫无业务意义——那种“模型跑通了,但没人看得懂”的玄学感,往往就始于没分清数据类型和算法边界。
2. 为什么需要 kmodes:分类数据的聚类,本质是一场“投票式共识”
2.1 分类数据的特殊性:没有大小,只有相同或不同
分类数据(categorical data)的核心特征,是它的取值是离散的、无序的、不可计算的。比如“血型”有 A、B、AB、O 四种,但你不能说 AB 比 A “大 1.5 倍”,也不能说 O 是“零点”;再比如“手机品牌”是苹果、华为、小米、三星,它们之间没有数值梯度,只有“相等”或“不相等”两种关系。K-means 的数学基础——欧氏距离(Euclidean distance)——要求每个维度都能做减法、平方、开根号。当你强行把“苹果=1,华为=2,小米=3”编码后喂给 K-means,算法会误以为“华为到小米的距离(1)”比“苹果到小米的距离(2)”更近,从而扭曲真实语义。kmodes 彻底放弃数值运算,转而采用“汉明距离(Hamming distance)”:对两个样本的每一列,如果值相同,计 0;不同,计 1;最后把所有列的 0/1 加起来。这个总和,就是它们的“不一致总数”。它不假设任何顺序或大小,只忠实反映“有多少地方不一样”。
2.2 kmodes 的核心思想:众数(mode)即中心,而非均值(mean)
K-means 的“中心”叫质心(centroid),是每维数值的平均值。比如三个用户的年龄是 [25, 30, 35],质心就是 30;收入是 [8000, 12000, 10000],质心就是 10000。但对分类数据,“平均职业”是什么?“平均城市”又是什么?没有定义。kmodes 的解法非常直观:它的“中心”不叫质心,而叫众数中心(mode center)。对于某一簇里的所有样本,kmodes 对每一列单独统计频次,把出现次数最多的那个值,作为该列的中心值。例如,某簇内 10 个用户的职业分布是:程序员(4人)、教师(3人)、设计师(2人)、医生(1人),那么这一簇的“职业中心”就是“程序员”。这个过程就像班级投票选班长——不求平均分,只看谁票数最多。代码实现上,它不需要任何矩阵乘法或梯度下降,核心就是一个scipy.stats.mode或手动Counter统计,计算开销极低,且结果天然可解释。
2.3 算法流程:三步走,比 K-means 更“接地气”
kmodes 的迭代流程与 K-means 高度对称,但每一步都替换成分类语义操作:
- 初始化:随机从数据中选 k 个样本,作为初始的 k 个众数中心(mode centers)。注意:这里不是随机生成数值,而是直接“抄”现有行,确保中心本身也是合法的分类组合。
- 分配(Assignment):对每个样本,计算它到 k 个中心的汉明距离(即逐列比对,统计不同列数),把它分给距离最小的那个中心。这步完全不涉及浮点运算,纯逻辑判断,速度极快。
- 更新(Update):对每个簇,重新计算其众数中心:对簇内所有样本的第 1 列,统计各取值频次,取最高频者;对第 2 列同样操作……直到所有列都更新完毕。
提示:kmodes 没有“收敛于零损失”的数学保证,但它一定能在有限步内停止(因为中心取值只能来自有限集合,状态空间有限)。实践中,通常 10–50 次迭代就稳定了。
2.4 为什么不是“改个距离函数”就行?——K-means 的底层假设崩塌了
有人会问:“既然只是距离不同,我能不能把 K-means 的距离函数换成汉明距离,自己写一个?”答案是否定的。因为 K-means 的更新步(Update)严重依赖“均值”的可导性与唯一性。当你用汉明距离分配样本后,若仍用均值更新中心,得到的中心很可能不是原始数据中的合法取值(比如职业均值算出来是 2.7,无法映射回任何真实职业)。而 kmodes 的更新步强制要求中心必须是数据中真实存在的众数,这保证了每一步的中心都是语义合法的。这不是一个“换函数”的工程问题,而是一个“重定义中心概念”的范式切换。就像你不能把汽车引擎装到帆船上指望它靠风力跑得更快——动力源和传动结构必须整体适配。
3. kmodes vs k-prototypes:当你的数据里混着“数字”和“文字”,该怎么办?
3.1 k-prototypes 是 kmodes 的自然延伸:混合数据的“双模引擎”
现实中的数据,很少纯粹是分类的。一份电商用户表,可能包含:城市(分类)、职业(分类)、年龄(数值)、年消费额(数值)、会员等级(有序分类)。kmodes 只能处理全分类字段,K-means 只能处理全数值字段。k-prototypes 就是为这种混合场景(mixed-type data)诞生的:它把数据拆成两部分——分类列用 kmodes 的众数逻辑,数值列用 K-means 的均值逻辑,并用一个统一的损失函数把两者加权合并。其核心公式如下:
$$ \text{Loss} = \sum_{i=1}^{n} \left[ \gamma \cdot d_c(x_i, c_i) + d_n(x_i, c_i) \right] $$
其中:
- $d_c$ 是分类部分的汉明距离(kmodes 部分),
- $d_n$ 是数值部分的欧氏距离(K-means 部分),
- $\gamma$ 是一个超参数,用于平衡两类距离的量纲差异(因为数值距离动辄几百上千,而汉明距离最大只有列数,不加权会导致数值列完全主导聚类)。
注意:$\gamma$ 不是随便设的。经验法则是:先对数值列做标准化(如 Min-Max 或 Z-score),再令 $\gamma$ 约等于数值列标准化后的平均方差(variance)的倒数。很多实现(如
kmodes库)会提供gamma='auto'自动估算。
3.2 实战代码对比:一眼看懂两者的调用差异
下面是一个真实可运行的对比示例,使用kmodes库(pip install kmodes):
import pandas as pd import numpy as np from kmodes.kmodes import KModes from kmodes.kprototypes import KPrototypes # 构造混合数据:前2列分类,后2列数值 data = pd.DataFrame({ 'city': ['Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen', 'Beijing'], 'job': ['Engineer', 'Teacher', 'Engineer', 'Designer', 'Teacher'], 'age': [28, 35, 32, 29, 40], 'income': [15000, 12000, 18000, 14000, 10000] }) # ✅ kmodes:只传入分类列(必须是 object 或 category 类型) km = KModes(n_clusters=2, init='Huang', n_init=5, verbose=1) clusters_kmodes = km.fit_predict(data[['city', 'job']]) # ✅ k-prototypes:传入全部列,但必须指定 categorical=[0,1](索引从0开始) kp = KPrototypes(n_clusters=2, init='Huang', n_init=5, verbose=1) # 注意:k-prototypes 要求数值列是 float,分类列是 str 或 category clusters_kproto = kp.fit_predict(data, categorical=[0, 1])关键区别就在最后一行:k-prototypes的fit_predict()多了一个categorical参数,明确告诉算法“第 0 和第 1 列是分类的,其余是数值的”。这个参数是 k-prototypes 的灵魂,缺了它,算法就不知道如何分流处理。
3.3 性能与稳定性:为什么 k-prototypes 比 kmodes 更“娇气”
虽然 k-prototypes 功能更强,但它也继承并放大了 K-means 的经典弱点:
- 对初始中心更敏感:因为同时优化两类中心,局部最优陷阱更多。
n_init=10或更高是常态。 - $\gamma$ 参数影响巨大:$\gamma$ 过小,数值列被忽略,退化为 kmodes;$\gamma$ 过大,分类列被淹没,退化为 K-means。必须通过轮廓系数(silhouette score)或业务验证反复调试。
- 计算开销略高:每次迭代要分别计算汉明距离和欧氏距离,再加权求和,比纯 kmodes 多约 20–30% 时间。
提示:如果你的数据里数值列占比很小(比如只有 1–2 列年龄/评分),或者你更看重分类字段的业务可解释性,不妨先用 kmodes 单独聚类分类列,再用数值列做后处理(如按簇内均值排序),反而更稳健。
4. 常见误区与易混概念:别再把“模式”当成“平均值”
4.1 误区一:认为 kmodes 是 K-means 的“简单替换”,只需改距离函数
为什么混:网上很多教程只说“kmodes 用汉明距离代替欧氏距离”,让人误以为只是 K-means 的一个配置开关。
怎么区分:K-means 的更新步依赖“均值”的数学性质(可导、唯一、连续),而 kmodes 的更新步依赖“众数”的统计性质(离散、可能不唯一、需频次统计)。二者中心定义、更新逻辑、收敛证明完全不同。强行替换距离函数而不改更新逻辑,得到的是无效中心。
4.2 误区二:把“众数中心”等同于“最常见的样本”
为什么混:看到“mode”就联想到“出现最多的那一行数据”。
怎么区分:众数中心是每列独立取众数后拼成的新行,它本身未必在原始数据中存在。例如,簇内样本为[A, X], [A, Y], [B, X],则众数中心是[A, X](第1列 A 出现2次,第2列 X 出现2次),而[A, X]确实在数据中;但如果样本是[A, X], [A, Y], [B, X], [B, Y],则第1列众数是 A/B 并列,第2列众数是 X/Y 并列,此时算法需按规则(如取字典序最小)选一个,结果[A, X]就是人为选定的中心,未必是原数据行。
4.3 误区三:认为 k-prototypes 能自动处理“有序分类”(如会员等级 L1/L2/L3)
为什么混:L1/L2/L3 看似有顺序,容易被误当作数值。
怎么区分:kmodes/k-prototypes 默认将所有非数值列视为无序分类。L1/L2/L3 若有明确序关系(L3 > L2 > L1),应先转换为数值(1/2/3),再放入 k-prototypes 的数值列;若序关系弱或仅作标签(如“青铜/白银/黄金”只是称号,无严格数值含义),则保留为分类列更安全。混淆会导致距离计算失真。
4.4 误区四:用准确率(accuracy)评估聚类效果
为什么混:刚学完监督学习,习惯用标签比对。
怎么区分:聚类是无监督任务,没有真实标签(ground truth)。kmodes 的评估必须用内部指标:轮廓系数(silhouette score,衡量簇内紧密+簇间分离)、Calinski-Harabasz 指数(簇间离散度/簇内离散度)、或业务指标(如“同一簇用户复购率是否显著高于跨簇”)。拿聚类结果去比对人工打标,属于方法错配。
4.5 误区五:认为增加 k 值一定能提升效果,盲目追求高轮廓系数
为什么混:轮廓系数范围 [-1,1],越高越好,容易陷入数字幻觉。
怎么区分:k 值增大,轮廓系数常先升后降,但更重要的是业务可解释性。k=8 可能轮廓系数 0.65,k=3 是 0.55,但若 k=3 对应“新客/活跃/流失”三类,而 k=8 的每个簇都难以命名,那 k=3 才是真正可用的。kmodes 的价值不在数学最优,而在业务可读。
5. 什么时候该用 kmodes/k-prototypes:一份来自真实项目的血泪经验清单
5.1 明确适用场景:你的数据满足以下任意一条,kmodes 就是首选
| 场景类型 | 具体例子 | 为什么 kmodes 是唯一解 |
|---|---|---|
| 纯文本标签聚类 | 商品 SKU 的属性标签:颜色=["红","蓝","绿"]、尺寸=["S","M","L"]、材质=["棉","涤纶","麻"] | K-means 无法定义“红”与“蓝”的距离;kmodes 直接统计标签匹配度,结果可直接用于商品推荐规则(如“买过红+S+棉的用户,也常买蓝+S+棉”) |
| 问卷与调研数据 | 用户调研表:教育程度=["高中","本科","硕士","博士"]、通勤方式=["地铁","公交","自驾","步行"]、信息获取渠道=["微信","抖音","小红书","知乎"] | 字段全为分类,且无天然序(“博士”不等于“高中×4”);kmodes 聚出的簇可直接映射用户画像,如“高学历通勤族(硕士/博士+地铁+微信)” |
| 医疗与生物编码 | ICD-10 疾病编码、SNOMED CT 临床术语、基因突变类型(BRAF_V600E,EGFR_L858R) | 编码本质是符号,无数值意义;kmodes 能发现共现模式(如BRAF_V600E常与MEK_inhibitor治疗方案同现),支撑临床路径挖掘 |
5.2 谨慎使用场景:这些情况,kmodes 可能翻车,务必先做预处理
- 高基数分类变量(high-cardinality categorical):如
用户ID、手机号、长文本哈希值。kmodes 会为每个唯一值建一个潜在中心,导致 k 值爆炸、内存溢出。对策:必须先做降维——用category_encoders的TargetEncoder或CountEncoder转为数值,或用sklearn.feature_extraction.text的HashingVectorizer压缩。 - 缺失值(NaN)未处理:kmodes 默认将 NaN 视为一个独立类别,导致大量样本因含 NaN 被错误归入同一簇。对策:必须显式填充(
df.fillna("MISSING"))或删除含 NaN 行(df.dropna())。 - 类别极度不均衡:如
职业字段中“程序员”占 80%,其余 20 种职业各占 1%。kmodes 的众数中心会强烈偏向“程序员”,掩盖小众但重要的模式。对策:对少数类做上采样(SMOTE-NC),或改用基于密度的聚类(如 DBSCAN 变种)。
5.3 一份可复现的端到端实战:从原始数据到业务结论
我们以某高校在线课程平台的用户行为日志为例,目标是识别典型学习者类型:
import pandas as pd from kmodes.kprototypes import KPrototypes from sklearn.preprocessing import StandardScaler # 原始数据(模拟) df = pd.read_csv("course_logs.csv") # 字段:course_category(分类), device_type(分类), study_duration_min(数值), video_play_ratio(数值), quiz_score(数值) # 步骤1:清洗与类型声明 df['course_category'] = df['course_category'].astype('category') df['device_type'] = df['device_type'].astype('category') # 步骤2:数值列标准化(关键!) scaler = StandardScaler() num_cols = ['study_duration_min', 'video_play_ratio', 'quiz_score'] df[num_cols] = scaler.fit_transform(df[num_cols]) # 步骤3:调用 k-prototypes(分类列索引:0,1) kp = KPrototypes( n_clusters=4, init='Huang', n_init=10, max_iter=100, verbose=1, random_state=42 ) # categorical=[0,1] 指明前两列是分类的 clusters = kp.fit_predict(df, categorical=[0, 1]) # 步骤4:解读结果——生成可读的簇描述 cluster_summary = df.copy() cluster_summary['cluster'] = clusters for i in range(4): print(f"\n=== Cluster {i} ===") # 分类字段:取众数 cat_mode = cluster_summary[cluster_summary['cluster']==i][['course_category','device_type']].mode().iloc[0] print(f"主修课程: {cat_mode['course_category']}") print(f"常用设备: {cat_mode['device_type']}") # 数值字段:取均值(已标准化,看趋势即可) num_mean = cluster_summary[cluster_summary['cluster']==i][num_cols].mean() print(f"学习时长倾向: {'高' if num_mean['study_duration_min']>0.2 else '中' if num_mean['study_duration_min']>-0.2 else '低'}") print(f"视频完成度: {'高' if num_mean['video_play_ratio']>0.3 else '中' if num_mean['video_play_ratio']>-0.3 else '低'}") # 输出业务结论示例: # Cluster 0: "移动端碎片学习者" —— 主用手机,课程多为职场技能,学习时长短但视频完成度高 # Cluster 1: "深度PC学习者" —— 主用电脑,课程多为编程/数据科学,学习时长最长,quiz得分最高这段代码跑通后,我们不再面对一堆数字标签,而是得到了四个有名字、有行为特征、能直接写进运营方案的用户群。这才是 kmodes 的终极价值:它把机器学习的黑匣子,翻译成了业务语言。
从那以后我每次拿到新数据,第一件事不是急着建模,而是打开df.dtypes和df.nunique(),先问自己三个问题:有没有分类列?分类列是不是高基数?数值列要不要标准化?只要这三个问题答清楚,kmodes/k-prototypes 就不会让我踩坑。希望帮到你。
本文还有配套的精品资源,点击获取