简介:这是一份名为 kmodes 的 Python 库源码压缩包,专注于 k 模式与 k 原型聚类算法,适用于分类变量或混合类型数据的无监督聚类场景。相比传统 k 均值,该库采用汉明距离处理分类属性,并支持数值与分类混合数据的有效划分,可应用于客户分群、市场细分等任务。包体共 30 个文件,以 Python 脚本为主(20 个),包含核心算法实现、测试用例与示例程序,另附 README 文档、CSV 示例数据及 CI 配置,压缩包仅 36KB,结构轻量清晰。已有 2008 人学习下载。读者可从中获得可直接调用的 KModes 接口、k 原型与 k 模式的完整实现,以及基于 iris、大豆、股票等数据集的演示代码,便于快速理解算法原理并嵌入自身项目。
1. kmodes:给分类数据聚类的 Python 实现,绕开欧氏距离这个坑
做数据聚类时,大家默认会想到 k-means,但一旦遇到性别、城市、品类、职业这类枚举型字段,k-means 算出来的欧氏距离往往毫无意义——"男性"和"女性"之间没有真实的数值距离,硬编码成 0/1 只会让聚类结果变成随机打分。kmodes 库就是干这个的:它基于简单匹配距离实现了 k-modes 和 k-prototypes 两个算法,前者处理纯分类数据,后者支持分类列加数值列的混合场景,API 仿照 scikit-learn 设计,fit/predict 一把梭。适合做用户画像分群、商品属性聚类这类活儿的从业者,尤其是手里数据全是"标签"而没有连续量,或数值列只有零星几列、不想丢掉信息做纯编码的人。下面把这套库从原理到踩坑完整过一遍。
2. 简单匹配距离与 k-modes:为什么离散字段能直接聚类
2.1 从 k-means 到 k-modes:距离定义换了个世界观
k-means 的核心是用欧氏距离衡量样本间差异,聚类中心是"均值向量"。问题在于分类数据的属性值没有大小关系,比如学历字段的值是"本科""硕士""博士",编码成 1、2、3 后,1 和 3 的差被当成 2,但其实"本科"和"博士"的差异不该比"本科"和"硕士"更大。k-modes 换了一套匹配逻辑:两个样本在一个属性上取值相同,距离贡献为 0,不同则贡献为 1,所有属性累加就是总距离。这个距离叫简单匹配距离,数学表达为:
d(x, y) = sum over attributes of indicator(x_j != y_j)
相应地,簇中心不再是一串均值,而是每个属性上出现频率最高的那个值——叫做 mode(众数)。更新规则也直白:分配样本到最近中心后,把每个簇内每个属性的众数作为新中心,迭代到收敛。整体流程和 k-means 几乎一致,但"距离"和"中心"的定义完全换了一套。
kmodes 库在实现时做了两个重要改动:一是用 scipy 的稀疏矩阵或 numpy 的稠密数组存储数据,属性值要求是非负整数(字符串会被内部映射为整数 ID);二是加入了 n_init 多次随机初始化机制,避免落入局部最优。实际调用时,你基本可以照搬 sklearn 的写法。
2.2 快速上手:十行代码跑通 KModes 聚类
假设你手上有一份用户偏好数据,全是分类字段:操作系统(Windows/Linux/macOS)、浏览器(Chrome/Firefox/Safari)、付费习惯(免费/月付/年付)。先用 pandas 读进来,然后直接喂给 KModes:
import pandas as pd from kmodes.kmodes import KModes # 原始数据是字符串,kmodes 需要数值,但可以直接传 string,库内部会做编码 df = pd.DataFrame({ 'os': ['Windows', 'Linux', 'macOS', 'Windows', 'Windows', 'Linux'], 'browser': ['Chrome', 'Firefox', 'Safari', 'Chrome', 'Chrome', 'Firefox'], 'payment': ['free', 'monthly', 'yearly', 'free', 'yearly', 'monthly'] }) # 指定聚类数 k=2,n_init 做 10 次随机初始化取最优 km = KModes(n_clusters=2, init='Huang', n_init=10, verbose=0) clusters = km.fit_predict(df) print(clusters) # 每个样本的簇标签 print(km.cluster_centroids_) # 每个簇最典型的值组合 print(km.cost_) # 总成本,越小说明簇内一致性越高这段代码里 init='Huang' 是默认初始化策略,它先挑选距离彼此最远的样本作为初始中心,比纯随机更稳定;n_init=10 表示重复 10 次,最终保留成本最小的一次。km.cost_ 返回的是所有样本到所属簇中心的匹配距离总和,可以用作模型选择的参考指标。
参数层面,最常见需要调的是 n_clusters 和 n_init。n_clusters 的确定没有数学闭式解,一般用成本随 k 变化的折线图来找拐点;n_init 建议至少 10,数据量大时可以降到 5 以省时间。还有一个 init 参数可以选 'Cao' 或 'random',Cao 算法在处理高类别基数时更鲁棒,但计算慢一些。
2.3 成本函数与模型选择:没有轮廓系数也能判断好坏
在 k-means 里我们用惯性(inertia)或轮廓系数评估聚类质量,k-modes 也有对应物:总成本 cost_,即所有样本到其簇中心的简单匹配距离和。这个值会随 k 增大单调下降,所以不能用它直接选最优 k,但可以画"成本-簇数"曲线,找下降速度骤缓的拐点。
import matplotlib.pyplot as plt costs = [] K_range = range(2, 8) for k in K_range: km = KModes(n_clusters=k, init='Huang', n_init=10, random_state=42) km.fit_predict(df) costs.append(km.cost_) plt.plot(K_range, costs, marker='o') plt.xlabel('k') plt.ylabel('cost') plt.title('Elbow curve for KModes') plt.show()这个拐点法不严格,但在分类聚类场景下比瞎猜强得多。需要注意:如果数据量大,每个 k 跑 10 次初始化会对性能有明显冲击,建议先在小样本上找拐点,再对选定 k 跑全量。
3. k-prototypes:混合数据聚类的桥接方案,gamma 是灵魂
3.1 分类型和数值型怎么统一距离,gamma 权重解决量纲问题
真实业务数据很少清一色全是分类字段,更多是"用户画像:城市、年龄段、会员等级"加"行为数据:月消费金额、购买次数"。k-prototypes 的核心思路是把简单匹配距离和欧氏距离拼在一起:分类部分用匹配距离,数值部分用欧氏距离,然后加一个权重 gamma 让两者对总距离的贡献可比。
公式大致长这样:
d(x, y) = d_cat(x, y) + gamma * d_num(x, y)
其中 d_cat 是分类列的匹配距离总和,d_num 是对数值列做标准化后的欧氏距离。gamma 的默认值是所有分类列方差倒数的某种综合估计,但实际用下来我一般会手工调——它决定了算法更偏向"分类相似"还是"数值相似"。gamma 太大,数值列主导分群,分类列形同虚设;gamma 太小则反过来。一般从 1 开始,画出不同 gamma 下的簇分布,用业务可解释性去判断,而不是纯看指标。
3.2 实际操作:混合数据的拟合与预测
继续上面的例子,现在给数据加两列数值:月消费金额、登录天数。用 KPrototypes 聚类时,需要指明哪些列是分类列,哪些是数值列。
import numpy as np from kmodes.kprototypes import KPrototypes df = pd.DataFrame({ 'os': ['Windows', 'Linux', 'macOS', 'Windows', 'Windows', 'Linux'], 'browser': ['Chrome', 'Firefox', 'Safari', 'Chrome', 'Chrome', 'Firefox'], 'payment': ['free', 'monthly', 'yearly', 'free', 'yearly', 'monthly'], 'spend': [120, 45, 300, 80, 250, 60], 'days': [15, 3, 28, 10, 20, 5] }) # 分类列索引:0,1,2;数值列索引:3,4 kp = KPrototypes(n_clusters=2, init='Cao', n_init=10, verbose=0) clusters = kp.fit_predict(df, categorical=[0, 1, 2]) # 预测新样本,传入同样的 categorical 索引 new_samples = np.array([ ['Linux', 'Firefox', 'monthly', 50, 4], ['Windows', 'Chrome', 'free', 200, 22] ]) pred = kp.predict(new_samples, categorical=[0, 1, 2]) print(pred)这里最关键的是 categorical 参数:它是列索引列表,指向所有分类列。数值列会在内部做标准化处理,但你也可以提前自己对数值列做 z-score 或 min-max 归一化——如果数据里有极端值或量纲差异巨大的列,预归一化比依赖库默认处理更可靠。
3.3 gamma 的调参方法,再讲一遍:网格搜索太慢,直接试 3~5 个值
gamma 参数在构造函数里直接传,比如 KPrototypes(n_clusters=2, gamma=2.0)。我的习惯是先不传 gamma,用库给的默认值跑一轮看簇中心是否合理;然后分别设 gamma=0.5、1、2、4 各跑一遍,比较各方案下簇中心的业务可解释性。
比如在用户分群场景里,gamma=0.5 时,每个簇内部数值列方差可能很大,但分类列非常整齐;gamma=4 时恰好相反。具体选择要看业务目标:是做营销分群(分类特征更重要),还是做异常识别(数值行为更关键)。调参没有银弹,同一份数据在不同场景下最优 gamma 可能差好几倍。
4. 避坑与常见问题:这几处翻车概率最高
4.1 类别基数差异大,聚类结果被高基数列绑架
现象:数据里有一列"城市"有 200 个取值,另一列"性别"只有 2 个取值,聚类结果几乎只按城市分组,性别差异完全体现不出来。
原因:简单匹配距离里每个属性权重一样,高基数列更容易制造距离差异,从而主导簇的划分。
解决:对高基数列做业务归并,比如城市按一线/二线/其他归类,或者把该列拆成多个业务含义明确的哑变量再参与聚类。必要时也可以手动调整个别列的权重,但 kmodes 库目前不支持按列权重,所以最好的做法还是预处理时先降基数。
4.2 聚类数量 k 的确定:拐点法失灵时怎么办
现象:成本-簇数画出来没有明显拐点,是一条平滑下降的曲线,选哪个 k 都没底。
原因:数据本身没有清晰的簇结构,或者分类列太多导致每个样本都"各有各的不同",成本下降均匀。
解决:放弃纯数据驱动,改用业务口径定 k。比如运营要按"高价值-中价值-低价值"做三档运营,那就直接 k=3。另一种做法是抽样本跑多次,看 k=2~8 下簇中心的稳定性,选簇中心最稳定且业务可解释的 k。
4.3 初始中心选不好,结果反复横跳
现象:同一份数据,固定 n_clusters,跑两次结果差异很大,簇标签和簇中心都不一样。
原因:n_init 太低,随机初始化落到差局部最优;或者数据规模小、噪声大,随机初始化本身就很敏感。
解决:n_init 至少调到 20,并固定 random_state 方便复现;如果还是不稳,在数据预处理阶段可以先做一次 PCA 或类别归并减少噪声维度。实际项目里,我还遇到过因为数据顺序不同导致结果变化的情况,这种情况建议先 shuffle 数据再拟合。
4.4 字符串编码踩坑:类别值不连续导致中心值异常
现象:KModes 内部把字符串映射成整数,但映射是字典序,比如 '10' 会被排在 '2' 前面,和业务认知不符。
原因:字符串的字典序和业务顺序是两码事,尤其数值型字符串('2' vs '10')更明显。
解决:如果分类列本质是有序的(比如等级:低/中/高),先手动映射成有顺序的整数再喂给算法;如果无序,保持字符串让库做映射即可。另外,所有列的数据类型必须一致,混入 NaN 会直接报错或导致距离计算异常。
4.5 数值列没做标准化,混合聚类结果被量纲带偏
现象:用 KPrototypes 时,数值列有"月消费金额"(几百到几万)和"购买次数"(个位数),聚类结果几乎只看金额。
原因:欧氏距离的贡献和量纲成正比,金额列天然权重远大于次数列。
解决:进入 KPrototypes 之前,先对数值列做 z-score 归一化。不要依赖库的默认行为——虽然库内部会对数值列做处理,但自己控制预处理过程更可预测,也方便排查问题。
5. 进阶技巧:聚类稳定性评估与画像输出
这一步是项目落地前的最后校验。我一般会在拿到聚类结果后做三件事:聚类稳定性评估、簇画像统计、结果落库供下游使用。
稳定性评估用 Bootstrap 重采样:随机抽取 80% 样本,重新跑同样的聚类,看簇中心是不是和全量结果基本一致。
from sklearn.utils import resample stable_cnt = 0 n_repeat = 30 for i in range(n_repeat): sample_idx = resample(range(len(df)), n_samples=int(len(df) * 0.8), random_state=i) kp_tmp = KPrototypes(n_clusters=3, init='Cao', n_init=10) kp_tmp.fit_predict(df.iloc[sample_idx], categorical=[0, 1, 2]) # 这里以簇中心与全量模型的匹配度做简单对比 if kp_tmp.cluster_centroids_.shape == kp.cluster_centroids_.shape: stable_cnt += 1 print(f'稳定率: {stable_cnt / n_repeat:.2f}')稳定率的合理下限在 0.8 左右,低于这个值说明数据和 k 的选择不匹配,需要考虑换 k 或者优化预处理。
簇画像输出则用 groupby 统计每簇的典型特征,直接可视化或写表:
df_pred = df.copy() df_pred['cluster'] = clusters profile = df_pred.groupby('cluster').agg({ 'os': lambda x: x.mode()[0], 'browser': lambda x: x.mode()[0], 'spend': 'median', 'days': 'mean' }) print(profile)这步看似简单,但做出来之后才能跟业务方对齐簇的语义,而不是停留在"算法分了 3 堆"。画像表加上简单柱状图,就能直接放进报告里当作分群依据。
回到开头那句话:kmodes 不是多厉害的模型,但它把分类数据聚类的流程标准化了,省去自己写匹配距离和众数更新的繁琐代码。从那以后我每次做用户分群,只要数据里有一半以上是分类列,就默认先跑一版 KModes 或 KPrototypes 看看分群结构,再决定要不要上 embedding 或主题模型。这套流程不算快,但每一步翻车都能定位到具体原因,这比黑匣子式的深度学习分群踏实得多。希望帮到你。
本文还有配套的精品资源,点击获取