K-means实战:电商用户分层从原理到sklearn实现与运营落地
K-means这个算法说实话很神奇。你说它简单吧面试必考、数据分析必用、用户分层必提几乎是机器学习入门绕不开的第一个模型你说它复杂吧真正做业务的人天天用它做人群划分、商品聚类、异常检测用了好几年也未必把里头那点门道琢磨透。我经常跟团队里的小朋友讲K-means不是你跑通一个demo就算会了也不是你丢进sklearn调一个KMeans(n_clusters3)就完事了。真正的功夫在数据预处理在K值选择在聚类结果怎么解读、怎么反哺业务。这篇博文我不打算给你讲虚的直接用一个电商用户分层的完整案例把K-means的原理、sklearn实现、代码注释、结果解读从头到尾过一遍。你会看到我怎么处理用户交易数据怎么选特征怎么做标准化怎么选K怎么解读每个簇背后的业务含义以及最后怎么把分群结果落到运营动作上。代码我会贴全注释会写到“照着敲就能跑”的程度。如果你想在实战项目里真正用一次K-means这篇文章可以帮你省掉不少我当年踩坑的时间。1. 先从原理说起K-means到底在算什么1.1 一句话版本和“两个步骤反复迭代”的工作机制K-means做的事情用大白话说就是给你一堆点你想把它们分成K堆分堆的标准是每个点离自己那堆的中心最近。这里没有标签没有人告诉你哪些点该是一伙的所以它属于无监督学习。它内部的工作机制就两个步骤反复转第一步是分配。假设现在已经有了K个中心点每个点去认领离自己最近的那个中心形成K个簇。第二步是更新。把每个簇里所有点的坐标求平均用算出来的均值作为这个簇新的中心点。分配完再更新更新完再分配如此反复直到中心点基本不再挪动或者达到预设的最大迭代次数算法就收敛了。这个“中心”在数学上就是簇内样本的均值K-means里的“means”指的就是这个。你可以把这个过程想象成班级调座位一开始随便选几个位置当“组长位”每个同学坐到离自己最近的组长旁边坐好之后组长挪到这一组的正中间大家再重新选择离自己最近的组长……一直到所有人都稳定下来不再换组为止。实际跑起来差不多就是这个画面。1.2 目标函数为什么它一定是在做“簇内紧凑”很多人不知道K-means其实是在最小化一个目标函数这个函数叫簇内误差平方和也就是SSESum of Squared Errors。Sklearn里叫inertia惯性也叫簇内离差平方和。公式长这样L 求和_{i1}^{K} 求和_{x in C_i} || x - u_i ||^2翻译成人话就是对每个簇算一下这个簇里的点和簇中心的距离平方然后全部加起来。K-means的全过程就是在找一组簇中心让这个总和尽量小。所以K-means本质上是在做“让每个簇内部尽量紧凑”这件事至于簇和簇之间是不是分得开它不是直接优化的目标。这也是为什么K-means对异常点非常敏感——一个离群点距离中心十万八千里它的距离平方会瞬间把损失拉高中心点为了“照顾”这个离群点就很容易被带偏。后面讲实战的时候我会再细说怎么处理这个问题。1.3 K-means的三个无法回避的局限先把这个讲了因为后面所有细节的选择都是围绕这些问题展开的。第一个局限是K值必须人为指定。算法本身不知道数据应该分成几类你给它几个它就分几个。这是K-means最让新手头疼的地方后面我会用肘部法则加上业务理解去解决这个问题。第二个局限是初始化敏感、容易陷入局部最优。初始中心点选得不好很可能收敛到一个“凑合”的结果而不是全局最优。Sklearn里这个问题的解法是用k-means初始化再加上多次随机初始化取最优参数上就是n_init和init这两个参数。第三个局限是它假设类别的形状是凸的、大小相近的。K-means用欧式距离和均值来决策天然适合球形分布的数据。如果数据是月牙形的、环形的或者长条形的K-means基本无能为力。所以它适合用户分层这类相对规整的业务数据但图像、文本这种高维稀疏场景就不合适了。2. sklearn实现带注释的完整模板代码2.1 环境依赖和导入先说你手上得有这些库numpy、pandas、matplotlib、sklearn。如果你用的是Anaconda基本上都装好了。没有的话pip install一下就行这里不展开讲安装过程了网上教程一大把。下面是导入部分的代码注释我会写详细一点# 1. 数据处理三件套 import numpy as np # 数组和数值计算sklearn底层也依赖它 import pandas as pd # 读取表格数据、数据清洗、聚合统计 import matplotlib.pyplot as plt # 画K值曲线图和聚类散点图 # 2. 中文字体设置不然画图会乱码 plt.rcParams[font.sans-serif] [SimHei] # 用来正常显示中文标签 plt.rcParams[axes.unicode_minus] False # 用来正常显示负号 # 3. sklearn核心模块 from sklearn.cluster import KMeans # KMeans聚类器 from sklearn.preprocessing import StandardScaler # 标准化把特征变成均值0、方差1 from sklearn.decomposition import PCA # 降维用于把高维特征映射到二维画图 from sklearn.metrics import silhouette_score # 轮廓系数评估聚类效果好坏的指标之一2.2 KMeans类最核心的几个参数Sklearn的KMeans类我相信大家都见过但参数不见得都搞清楚了。我把最常用的几个列出来这些在实际项目中直接决定模型跑出来是什么结果。KMeans( n_clusters3, # 聚成的类别数也就是K值必须人工指定 initk-means, # 初始化中心点的策略可以避免初始点太近导致局部最优 n_init10, # 随机初始化跑几次最后选inertia最小的一次结果 max_iter300, # 单次运行的迭代上限防止不收敛时死循环 tol1e-4, # 中心点变动小于这个值时认为收敛 random_state42 # 固定随机种子让结果可复现 )这里我想强调两个很容易被忽略的参数。一个是n_init新版sklearn里它的默认值是10也就是说算法会随机初始化10次每次都跑完整的迭代最后挑inertia最小的那次作为最终输出。这10次里大部分结果应该是接近的但确实会存在某一次初始化特别不走运、掉进局部最优解的情况取最小值就是降低这个风险。另一个是random_state。很多人跑代码不设这个参数结果每次运行出来的标签顺序都不一样一会儿0号簇是高频用户一会儿1号簇是高频用户就把自己搞懵了。实际项目里这个参数一定要固定不然别人跑你的代码结果对不上你也没法跟业务同事复现结果。2.3 最小可运行示例聚类一个二维点集在所有业务案例之前先跑一个最简单的二维数据例子。这个例子不是为了炫技而是让你把KMeans的输入输出彻底搞清楚。# 生成三坨人工数据模拟三种不同类型的人群 np.random.seed(42) cluster_1 np.random.normal(loc(2, 3), scale0.5, size(100, 2)) # 左上角一团 cluster_2 np.random.normal(loc(8, 8), scale0.6, size(100, 2)) # 右上角一团 cluster_3 np.random.normal(loc(3, 9), scale0.4, size(100, 2)) # 左上偏上的一团 X np.vstack([cluster_1, cluster_2, cluster_3]) # 纵向拼接得到300行2列的数据 # 初始化模型并训练 model KMeans(n_clusters3, initk-means, random_state42) model.fit(X) # fit方法完成整个聚类过程 labels model.labels_ # 每个样本的簇标签形状是(300,)取值0、1、2 centers model.cluster_centers_ # 每个簇的中心坐标形状是(3, 2) # 第一行是0号簇的中心第二行是1号簇的中心依此类推 inertia model.inertia_ # 簇内误差平方和越小说明簇内越紧凑这里你需要记住几个关键属性fit之后模型会有labels_、cluster_centers_、inertia_这三个最常用的结果。labels_就是每个点的分组编号cluster_centers_就是每个组最中心的坐标点inertia_是一个数值告诉你这次聚类整体的紧密度。拿到labels之后你可以把散点图按照labels着色画出来就能直观看到聚类效果plt.figure(figsize(8, 5)) plt.scatter(X[:, 0], X[:, 1], clabels, cmapviridis, s30, alpha0.8) plt.scatter(centers[:, 0], centers[:, 1], cred, markerX, s200, label聚类中心) plt.title(K-means聚类结果可视化) plt.legend() plt.show()这一步跑通了基本API就算掌握了。接下来真正核心的工作——怎么让聚类在业务数据里有意义——才是重头戏。3. 实战案例电商用户分层从零到落地3.1 业务背景为什么要做用户分层案例背景我用一个常见的电商场景。假设你在运营一个电商平台平台上有一批VIP用户但运营同学反映一个困惑这批用户数量在增长总消费金额却没有同步上升。如果只是把“VIP”当成一个整体来做运营你会发现这个群体内部差异极大——有人一个月买十几次有人三个月才买一次但每次花好几千还有人开了会员之后几乎不再登录。这种时候要做的事情就是用户分层。把Users按照消费行为切成几个特征鲜明的群体每个群体给不同的运营策略。这就是K-means最经典的落地场景之一。3.2 模拟一份用户交易数据因为没有真实数据库我在这里模拟一份用户数据。实际生产中的表结构通常就是这样一张用户表下面是近90天的行为汇总字段# 固定随机种子保证模拟数据每次生成一致 np.random.seed(42) n 1000 # 模拟1000个用户 # 字段1最近一次消费距离今天的天数越小代表越活跃 # 用指数分布模拟因为“刚买过的人多很久没买的人少” recency np.random.exponential(30, n).clip(0, 180) recency np.round(recency, 1) # 字段2近90天消费次数 # 用帕累托分布模拟因为消费频次典型特征是“少量人高频多数人低频” frequency np.random.pareto(1.5, n) * 3 frequency np.minimum(np.round(frequency, 1), 60) # 上限设为60次防止极端值 # 字段3累计消费金额 # 让金额和频次正相关频次越高金额越高同时加入噪声模拟真实波动 amount frequency * 80 np.random.normal(0, 120, n) amount np.maximum(amount, 10).round(1) # 设置最低消费10元 # 字段4是否购买过付费会员0代表否1代表是 vip_flag np.random.binomial(1, 0.15, n) # 拼成DataFrame方便后续处理 df pd.DataFrame({ recency_days: recency, # 最近消费距今天数 frequency_90d: frequency, # 90天内消费次数 total_amount: amount, # 累计消费金额 vip_flag: vip_flag # 是否VIP }) df.head()这份数据一共四个字段前三个是连续变量vip_flag是二值变量。很多同学一看有四个字段直接全部怼进KMeans就完事了这是不对的。二值变量进距离计算会产生一堆问题后面讲特征处理的时候细说。3.3 拿到数据后别急着聚类先看分布我见过太多人拿到数据第一件事就是调KMeans跑聚类跑完之后看结果一脸懵因为根本不知道这些数字到底长什么样。正确的打开方式是先做描述性统计# 查看各字段的统计描述重点关注极值和分布 df.describe().T你会看到recency_days均值在30天左右但最大值可能到180frequency_90d均值可能在5次左右但最大的有60次total_amount差异更大从几十到几千都有可能。这里要特别注意量纲问题。recency是“天”frequency是“次”amount是“元”三个字段的单位和取值范围完全不同。K-means用的是欧式距离如果一个字段的数值范围很大它就会主导距离计算其他字段变得形同虚设。举个例子金额如果是几千块消费次数只有几次那算欧式距离时基本就是金额在起决定作用。你这样聚类出来只是按金额一刀切而已根本谈不上“分层”。所以标准化的动作不做后面的聚类结果基本没有意义。3.4 特征工程怎么把业务信息转成聚类友好的输入这一节是整个项目里信息密度最高的部分我会把每步操作的原因说透。第一步是选特征。用户分层的经典框架是RFM模型也就是Recency最近一次消费时间、Frequency消费频次、Monetary消费金额。这套框架在传统CRM里用了很多年就是因为这三个维度从不同角度刻画了用户的消费能力和忠诚度Recency看你是不是还活跃Frequency看你有多依赖平台Monetary看你贡献了多少收入。我这里用的三个连续变量恰好就是RFM的变体。第二步是处理vip_flag。二值变量要不要进聚类模型我的建议是除非你有充分理由否则别放。原因很简单K-means是基于欧式距离的二值变量和其他连续变量混在一起距离计算时会引入一个奇怪的维度“是不是VIP”这一个属性跟金额差了好几个数量级标准化之后又权重不明。如果想让VIP信息参与分层更靠谱的做法是基于它生成一个连续型业务指标比如“VIP消费占比”或者“VIP时长”这类变量才值得进模型。第三步是标准化。这一步不用纠结选哪种我习惯用StandardScaler也就是z-score标准化把每个特征变成均值0、方差1。# 定义参与聚类的特征 features [recency_days, frequency_90d, total_amount] # 标准化把不同量纲的数据压缩到同一尺度 scaler StandardScaler() X_scaled scaler.fit_transform(df[features]) # 标准化之后每一列的均值约为0标准差约为1 # 这样聚类时并不会因为金额数值大就支配整个距离计算为什么用StandardScaler而不是MinMaxScaler两者都可以但StandardScaler对异常值的容忍度相对高一些。MinMaxScaler把数据强制压到[0,1]一旦有一个极端大值其他所有值都会被压得非常接近0反而会削弱区分度。StandardScaler虽然也受异常值影响但至少保留了分布形状。第四步是降维可视化。标准化之后的数据是三维的没法直接画图给业务看。为了讲清楚聚类结果我用PCA降到二维。注意PCA降维只用做可视化不作为聚类输入# 标准化后的数据是三维的用PCA降到二维以便画散点图 pca PCA(n_components2, random_state42) X_pca pca.fit_transform(X_scaled) # 看一下两个主成分的方差解释比例一般两个维度能保住70%以上就不错 explained_variance_ratio pca.explained_variance_ratio_ print(两个主成分解释的方差比例, explained_variance_ratio)3.5 K值选择肘部法则为主业务理解为辅聚类前必须解决K值问题。这个案例里我可以直接告诉你分四类但在实际工作中没人给你标准答案。常用方法是算不同K值的inertia然后画一条折线图找“肘部”。原理是这样的K越大簇越多inertia必然越小。但K从3增加到4时inertia下降特别明显K从5增加到6时下降就变缓了。这个拐点就像人的手肘拐点对应的K就是比较合理的值。因为这说明再增加类别数对“簇内紧凑度”的提升不会再那么显著。话不多说直接上代码# 计算K从1到10的inertia并画折线图找拐点 k_range range(1, 11) inertias [] for k in k_range: km KMeans(n_clustersk, initk-means, random_state42) km.fit(X_scaled) inertias.append(km.inertia_) # 画出肘部图 plt.figure(figsize(8, 5)) plt.plot(k_range, inertias, markero) plt.xticks(k_range) plt.xlabel(K值) plt.ylabel(簇内误差平方和 (inertia)) plt.title(K值选择肘部法则) plt.show()跑完图你大概率会看到一个比较明显的拐点落在K3或者K4的位置。这时候光靠图形可能还是有点含糊我会再追加一个轮廓系数来验证。轮廓系数的计算逻辑对每个样本要算两个值一个是它到同簇其他样本的平均距离a叫簇内距离另一个是它到最近的其他簇所有样本的平均距离b叫最近簇距离。轮廓系数就是(b-a)/max(a,b)范围在-1到1之间越大说明同类紧凑、异类远离整体聚类质量越高。sil_scores [] for k in range(2, 11): km KMeans(n_clustersk, initk-means, random_state42) labels_k km.fit_predict(X_scaled) score silhouette_score(X_scaled, labels_k) sil_scores.append(score) # 轮廓系数最高的K值倾向于得到质量更高的聚类 best_k np.argmax(sil_scores) 2 # 因为循环从2开始 print(轮廓系数最高的K值, best_k)结合肘部法则和轮廓系数再加上业务上运营团队能承接的群组数量我最终选了K4。这里要额外说明K值选择不存在“唯一正确答案”不同K值给你的是不同视角的用户划分关键是选一个业务上能用、解释得通的数量。3.6 执行聚类并解读用户画像选定K4之后重新跑聚类然后把结果合并回原始数据# 最终模型 final_model KMeans(n_clusters4, initk-means, n_init10, random_state42) df[cluster] final_model.fit_predict(X_scaled) # 查看每个簇的用户数量分布 df[cluster].value_counts().sort_index()接下来是最重要的一步解读用户画像。这一步很多人忽略其实这才是用户分层的价值所在。我的方法是对每个簇分组统计核心特征的均值然后用业务语言给每个簇起名字# 按簇分组计算每个簇的用户画像 profile df.groupby(cluster).agg( avg_recency(recency_days, mean), avg_frequency(frequency_90d, mean), avg_amount(total_amount, mean), vip_ratio(vip_flag, mean), count(cluster, size) ).round(2) print(profile)跑出来你会看到四类用户0号簇最近消费天数极小消费频次高金额高VIP占比也高。这是全平台最优质的一群人我要叫它“高价值活跃用户”。1号簇消费频次中等金额中等最近消费天数也不算远。这类人购买力尚可、活跃度尚可属于“潜力用户”有希望向0号簇转化。2号簇消费频次特别低但单次金额很高VIP占比也不低。这就是典型的“长尾VIP”大概属于低频高客单价人群他们可能买奢侈品、大件商品一年买不了几次但每次贡献巨大。3号簇消费频次低、金额低、距离上次消费天数长活跃度和价值双低属于“沉睡/低价值用户”。这四个群的命名不是算法给你的是我根据统计特征和业务经验手工贴上去的。这一步技巧在于你要结合业务背景去看数字而不是直接复制输出结果。为了更直观还可以通过PCA降维之后的坐标画一张带颜色的散点图每个点按cluster着色图里能看出来不同簇的空间分布。这张图放给运营同学看他们一眼就能接受。3.7 从聚类结果到运营策略的闭环聚类本身不产生业务价值产生价值的是“分完之后做什么”。对于高价值活跃用户也就是0号簇策略是维系和增长。这些人贡献高、活跃度高VIP占比高应该重点推送新品、专属折扣、积分加速活动目标是提高客单价和会员续费率而不是刺激她们复购——因为她们已经很频繁了。对于潜力用户也就是1号簇策略是引导升级。他们的消费频次和金额处在腰部当前还不是最高贡献群体但最近有活跃迹象。可以针对这类人做“首充会员优惠”、“连续签到送券”、“满额升级”等动作目标是推动他们往高价值人群迁移。对于长尾VIP也就是2号簇策略是完全不同的。这些人金额高、频次低买的是高价低频商品如果用常规的“唤醒复购”策略反而可能打扰他们。更合适的做法是逢重大活动、周年庆、新品季集中触达服务上设置专属客服、生日礼遇维护好客情关系。对于低价值沉睡用户也就是3号簇策略是用低成本方式唤醒比如短信召回、老用户回归礼包。如果触达多次仍然没有反应就应该在运营投入上果断降低优先级把预算花在更有希望转化的人群上。这样一圈闭环下来你手里的K-means输出就不再是几个冰冷的数值标签而是一整套运营策略的输入。4. 实战中绕不开的坑常见问题与排查实录4.1 为什么我跑出来的结果每次都不一样这个问题几乎每周都有人问我答案就是两个没设random_state或者n_init设成了1。KMeans初始中心点是随机选的不同初始点可能收敛到不同的局部最优。Sklearn里n_init默认10会多次随机初始化取最优所以总体稳定但如果别人机器上的版本不同、或者你自己反复换设备跑标签顺序依然可能变。解决办法就是固定random_state。代码里加上random_state42任何人在任何机器上跑出来结果一致这是可复现性的基本要求。4.2 新用户来了怎么打标签聚类模型和分类模型的差异在这里体现得特别明显。分类模型训练完可以直接对新样本predictKMeans也有predict方法但你要注意一个前提新样本必须经过和训练时完全一样的标准化处理。正确流程是这样# 新来一个用户20天前买过一次90天内消费2次金额300元 new_user [[20, 2, 300]] new_user_scaled scaler.transform(new_user) # 用训练时的scaler做标准化 cluster_id final_model.predict(new_user_scaled) print(新用户归类到簇, cluster_id[0])注意我用的是scaler.transform而不是fit_transform。fit_transform会在新数据上重新计算标准化参数这就等于把之前的模型尺度破坏了。很多人在这里翻车训练时用fit_transform预测时也习惯性用fit_transform结果模型输出完全不对。4.3 聚类出来的簇大小严重不均有问题吗有时候你会看到某个簇占了总样本的80%其他几个簇只有零星几个点。这种情况通常意味着数据处理有问题或者K值选择不合适。常见原因有三个一是特征中混入了大量零值构成的稀疏数据导致大部分样本聚成一团二是数据中存在极端离群点被单独拉成一个簇三是K值太小没有把数据内部的子群体拆开。应对方法是检查特征分布对离群点做winsorize截尾处理或者换用DBSCAN这类基于密度的聚类算法来处理不规则分布。用户分层这种业务场景里如果有个小簇人数太少没法形成运营策略就算聚类指标再好也没有业务意义。4.4 标准化时要不要包含vip_flag这类离散变量我前面建议不把vip_flag直接放进聚类模型但很多人不舍得扔觉得信息浪费了。这里再补充一个折中方案把离散变量按业务含义转成连续特征再决定是否放入。比如把vip_flag从“是否VIP”替换成“近90天VIP消费占总消费的比例”这个特征的取值范围是0到1含义是“用户对平台的依赖深度”在业务上比一个0/1标签更有解释力放进模型后也不会严重干扰距离计算。4.5 高维数据下K-means的表现为什么这么差当特征维度很高比如文本向量、图像向量有成千上万维时K-means的表现会显著退化原因是“维度灾难”导致的距离度量不再可靠。在高维空间所有点的欧式距离都会趋向于差不多聚类边界变得模糊不清。业界做法是用PCA等降维手段先降维比如降到几十维再聚类或者直接改用适用于高维稀疏数据的算法比如MiniBatchKMeans配合文本向量、或者是谱聚类。做用户分层这种结构化表格数据的话几十个特征以内用K-means问题不大特征太多的时候就要先做特征筛选或降维。4.6 聚类结果怎么验证是好的聊一个会气死算法工程师但很真实的话题聚类质量最终要靠业务拍板。inertia小、轮廓系数高只能代表数学层面聚得开不代表业务上有意义。最实用的验证方式是抽样看每个簇里的原始样本人工判断这个簇的人是否真的有共同特征、是否值得单独制定运营策略。我会做一张简单表格把每个簇的用户ID抽样列出来让运营同事盲看20个样本问他们“这20个人是不是一类人”。如果运营觉得看不懂说明聚类特征或者K值还需要调整。这种“人类反馈”在无监督项目里永远是最后一锤定音的工具。我个人在实际操作中最大的体会就是K-means的代码可能你花十分钟就全写完了但前面做特征、选K、解读结果的时间通常要用掉整个项目的八成。跑通模型只是开始把聚类结果翻译成业务语言才是这项工作的真正价值所在。最后再分享一个压箱底的习惯项目结束之后一定要把scaler和final_model同时保存下来封装成一个完整的predict函数。下次再来新用户数据一行代码就能完成从原始数值到分群标签的转换不用再把训练代码翻出来跑一遍。这个习惯能为你省下大量时间。如果你正想做用户分层、人群画像这类项目建议不要急着调用现成工具先拿真实数据认真走一遍流程观察分布、做特征、尝试不同K值、解读每一类用户。做完这一套你对K-means的理解会比背一百遍面试题都深刻。