
先说个真实需求场景。我去年接到过一份商场会员分析任务数据非常规整几十万条消费记录字段也就是会员ID、消费时间、金额这些看起来不复杂。可运营同事拿着会员列表完全不知道下一步该怎么办——全场折扣高价值客户觉得没诚意只做满减价格敏感客户薅完羊毛就走。后来我们把这套需求真正做成了“商场客户聚类”用无监督学习把顾客按行为切成几类运营才有了可以落地的抓手。这篇文章把完整链路拆开讲一遍从数据准备、特征构造、K-Means建模、K值选择到聚类结果的业务解读、运营动作和效果评估再把实操中容易踩的坑单独拉出来说。适合刚接触客户分群的数据分析同学也适合想了解聚类算法到底能帮业务做什么的运营朋友。内容都来自实际项目经验代码和参数可以直接拿去用但更重要的是理解每一步背后的取舍。1. 为什么商场客户聚类会出现在业务需求里一刀切运营的尽头是分群1.1 传统会员运营的痛点商场和电商有个很大的区别线下消费行为高度分散而且带有明显的随机性。你打开一个商场的会员明细会发现顾客结构远比想象中复杂。有人每月来四次每次顺路买杯咖啡加一盒水果有人半年不来一次一来就在化妆品专柜花掉四五千还有人专门等大促节点不折扣不动手。这些顾客如果共用一套运营策略结果通常是高价值客户觉得被打扰价格敏感客户养成了“非折扣不买”的习惯中间层的潜在客户又得不到足够的吸引力。我们当时拉过一组数据统一发放的满200减50优惠券核销率最高的客户群恰好也是客单价最低的那批。营销费用花下去了ROI却参差不齐。这种状态下任何精细化运营都无从谈起。因为你既不知道应该优先服务谁也不知道每一类顾客真正在意什么。传统的做法是拍几个阈值比如“年消费超过5000元算高价值客户”“一年到店少于3次算沉睡客户”然后按规则打标签。这种人工分箱方法的问题在于阈值是你定的不是数据告诉你的维度一多规则之间还会互相打脸。1.2 聚类在客户分群中的定位聚类属于无监督学习和分类最大的区别是它不需要预先定义任何标签。你只需要准备好顾客的行为特征让算法自己去发现“哪些人扎堆、哪些人离群”。聚类结果追求的是组内相似度高、组间差异大至于每个组叫什么名字、意味着什么那是建模之后由人来解读的。放在商场场景里聚类最大的价值在于它能同时处理多个维度的组合。RFM三件套里的“最近消费时间R、消费频率F、消费金额M”如果只靠人工分箱你可能只分得出来“高价值/低价值”这样的粗粒度结果。但聚类的搜索空间是连续的它可以发现“虽然频率很高但客单价极低、对折扣极度敏感”的群体也可以发现“频率不高但单次消费金额特别高、从来不领券”的另类群体这些用几套死规则根本描述不出来。1.3 项目需求和最终交付物在动手之前我先和运营对齐了项目目标。商场客户聚类这个项目最终要交付四样东西第一清晰的客户群划分第二每个客户群的命名和画像描述第三针对不同客户群的运营建议第四一套可以每月重复运行的脚本或流程。其中第四点特别关键客户聚类不能做成一次性项目客户行为每个季度都在变模型必须能低成本重跑。这里我要多说一句聚类本身只是中间产物它不直接产生业绩。业绩来自聚类结果被业务用起来之后的那一系列动作。所以从第一天起就要拉着运营一起看特征定义、看聚类输出而不是等全部建完模再丢给人家一张簇ID表。2. 数据准备从会员交易流水到干净的RFM特征表2.1 需要的表和核心字段商场客户聚类需要的数据源一般来自会员系统和交易系统。最常用的表是会员档案表和交易流水表。会员档案表提供客户ID、注册时间、性别、生日等静态属性交易流水表提供每一笔订单的消费时间、金额、品类、门店、是否使用优惠券等动态行为信息。还有一张营销触达表记录客户收到过哪些券、核销了哪些券这类数据在刻画价格敏感度时非常有用。核心字段其实没那么多customer_id、order_date、amount加一个可选的coupon_flag和category_id就够了。很多同学一上来就把所有字段都塞进模型结果特征空间被性别、年龄段、偏好品类这些变量撑得很大K-Means在这种高维稀疏空间里效果非常差。我建议先把RFM三个核心维度做扎实再带着明确的目的加扩展特征。2.2 时间口径与清洗逻辑时间窗口的选取是第一个容易出错的地方。如果是做年度经营规划建议取过去12个月的数据如果关注的是客户全生命周期行为可以用全部历史数据。但无论选哪个必须固定一个分析截止日期cutoff date不能直接取系统当前日期。因为当前日期每天都在变今天跑的R值和下周跑的R值完全不一样后续月度重跑时口径会乱掉。数据清洗有几个隐藏陷阱需要重点处理。第一退款记录不能直接当作负金额参与汇总否则客户的累计消费金额M会被严重扭曲甚至出现负数。我当时的做法是把退款订单单独拎出来只统计净消费如果平台本身有净支付字段直接用那个字段最省事。第二明显测试订单要剔除比如商品名包含“测试”字样、单笔金额异常低0.01元或异常高超出正常区间的记录。第三同一客户同一天在同一门店的多笔订单要按“日-客户-门店”维度去重合并成一次到店消费。否则客户只是分了几单付款频率F就会虚高聚类结果被噪音干扰。2.3 从订单表到RFM特征表的代码清洗完数据后就可以构造最基础的RFM特征了。下面这段代码是项目的核心起点import pandas as pd from datetime import timedelta df pd.read_csv(orders.csv) df[order_date] pd.to_datetime(df[order_date]) # 固定分析截止日最大订单日期 1天 cutoff df[order_date].max() timedelta(days1) rfm df.groupby(customer_id).agg( recency(order_date, lambda d: (cutoff - d.max()).days), frequency(order_date, count), monetary(amount, sum) ).reset_index() print(rfm.head())这段代码里最值得注意的就是recency的计算方式它用的是固定截止日和该客户最后一次消费日期的差单位是天。这个值越小说明客户最近刚来过活跃度越高。frequency用订单记录数做统计因为我们已经在清洗阶段把同日重复订单合并了所以这里的count数可以近似代表到店消费次数。monetary直接求和代表统计周期内的累计消费金额。到这里一张最干净的RFM特征表就出来了。3. 特征工程RFM之外还需要哪些消费行为变量3.1 RFM三件套的标准定义与计算细节RFM是客户分群里最经典的特征组合但在商场场景下有几个细节需要较真。R值有一个容易被忽略的问题如果客户统计周期内没有消费R值会非常大。比如用过去12个月做窗口完全没消费的客户R值就是365。这部分“注册但无消费”的客户我建议单独分出来不放进K-Means。因为他们的行为特征是纯零填充会把聚类中心拉向一个不真实的位置。先把活跃客户跑聚类再把沉睡客户用规则兜底是更稳妥的操作。F值这里有个“订单数”和“到店次数”的选择。商场消费和电商不同顾客可能在一个门店买完衣服又去另一个门店买奶茶形成同一天多笔订单。如果按原始订单数统计一个人一天产生4笔订单就会被当成4次到店这显然不符合直觉。我在项目里测试过两种口径的差异结论是按“日-客户-门店”去重后的到店次数聚类结果在业务解释性上明显更好。M值建议使用实际支付金额也就是扣掉优惠券抵扣后的金额。因为这个数字衡量的是客户真实贡献的现金流而不是商品的吊牌价。3.2 扩展特征客单价、品类集中度、优惠券敏感度RFM只能回答“最近来没来、来得多不多、花了多少钱”但回答不了“为什么来、买什么、看重什么”。所以要在基础特征之上挑几个扩展变量。我常用的是这三个第一平均客单价也就是M除以F。这个指标可以区分“买得多”和“买得贵”两种完全不同的人。有人一年来20次但每次只花30元买饮品有人一年来4次但每次花3000元买服饰。两者的商业价值天差地别但RFM原始特征里可能体现不出来。第二品类集中度。用客户购买Top品类消费金额占总消费金额的比例表示。比例接近1的客户是目的性消费比如专门冲某个品牌或品类来比例分散的客户属于逛购型更容易受活动和陈列影响。这两类人的营销方式完全不同前者要精确推送品牌信息后者适合用全场性活动引导。第三优惠券使用率。这个特征在商场场景里特别重要它直接刻画了客户的价格敏感度。计算方式很简单使用优惠券的订单数除以总订单数。有些客户领券率很高但核销率很低说明优惠门槛不一定合适这也是可以用的信息。3.3 为什么聚类前必须做标准化K-Means是基于欧氏距离的算法它对特征的量纲极度敏感。假设R值是0到365之间的数字F值是1到50M值可能是50到200000。如果不做任何处理M这一列在距离计算中会形成绝对支配地位——其他特征全变成噪声聚类结果几乎等价于“按消费金额排队划分”。解决办法是标准化。我用的是StandardScaler但在有极端大客户的情况下建议先对M做对数压缩再用StandardScalerimport numpy as np from sklearn.preprocessing import StandardScaler features rfm[[recency, frequency, monetary]].copy() features[log_monetary] np.log1p(features[monetary]) # 出于演示这里舍弃原始的monetary使用对数变换后的版本 X features[[recency, frequency, log_monetary]].values scaler StandardScaler() X_scaled scaler.fit_transform(X)这里要特别提醒一个顺序问题标准化必须在聚类之前做。我们项目里有个新人曾经先把数据喂给KMeans聚类完再标准化结果自然是一团乱。因为K-Means迭代时用的已经是未缩放的距离了后补的标准化根本不会改变原来的聚类结果。4. K-Means选型与K值确定算法参数背后的权衡4.1 为什么是K-Means而不是DBSCAN做客户聚类的算法不止K-Means一种但绝大多数场景里K-Means是最合理的第一选择。它的优点是计算快、可解释性强、实现成本低几十万条样本配合五六个特征运行时间以秒计。和DBSCAN比K-Means虽然需要预先指定簇数K但也因此换来了更稳定的结果和层次聚类比K-Means的复杂度低得多能够轻松应付全量客户数据。我整理了一个简单的选型对照表算法适用场景优点缺点K-Means凸形簇、数据量大、维度不高速度快、可解释、易规模化需指定K、对离群值敏感DBSCAN任意形状簇、需识别离群点不需要K、能自动识别噪声密度差异大时不稳、参数难调层次聚类小样本、需要树状结构树状图直观、无需预设K计算复杂度高、不适合几十万条数据GMM高斯混合椭球形分布、需要“属于各簇概率”软聚类、更灵活参数多、计算量大、易过拟合商场客户的行为特征通常不是特别复杂的不规则形状用K-Means已经能得到可读的群体划分。如果你的目标是想知道“这个客户70%属于高价值群、30%属于价格敏感群”那可以考虑GMM它会输出隶属概率。但对绝大多数运营场景来说一个硬标签已经够用了。4.2 K-Means超参数背后需要注意什么K-Means看似参数很少但有两个参数直接影响结果质量不能不重视random_state和n_init。random_state控制随机数种子。K-Means的初始中心点是随机选择的换个种子最终聚类结果可能就变了。为了保证项目可复现、同事之间能对齐结果必须固定它我一般用42。n_init表示算法会用多少个不同初始中心去跑最后返回其中SSE簇内平方和最小的结果。旧版sklearn里默认值是10但新版本里这个参数改成了auto你如果不显式指定的话实际运行次数可能不够稳定。建议显式设成n_init10避免在不同机器上跑出差异很大的结果。还有一个容易被忽略的是max_iter。默认300已经足够但如果特征维度多、样本量大迭代次数不够可能导致模型未收敛。训练完看一眼km.n_iter_如果它已经达到max_iter说明需要调大迭代上限。4.3 K值确定肘部法则、轮廓系数与业务对齐确定K值是K-Means流程里最核心的一步我习惯用三件套交叉验证。第一件套是肘部法则。画出K从2到10的SSE下降曲线找曲率变化最大的点import matplotlib.pyplot as plt from sklearn.cluster import KMeans sse [] for k in range(2, 11): km KMeans(n_clustersk, n_init10, random_state42) km.fit(X_scaled) sse.append(km.inertia_) plt.plot(range(2, 11), sse, markero) plt.xlabel(k) plt.ylabel(SSE) plt.title(Elbow Method) plt.show()需要提醒的是商场客户数据的肘部往往不会特别明显。SSE下降曲线通常是一条平滑递减的弧线“拐点”靠肉眼判断经常有争议。所以肘部法则只能作为参考区间不能当成唯一依据。第二件套是轮廓系数。轮廓系数衡量每个样本与自己簇内样本的紧密度和对其他簇的分离度取值范围从-1到1越大越好from sklearn.metrics import silhouette_score for k in range(2, 7): km KMeans(n_clustersk, n_init10, random_state42).fit(X_scaled) score silhouette_score(X_scaled, km.labels_) print(fk{k}, silhouette_score{score:.4f})轮廓系数高说明簇内紧凑、簇间分离。但注意轮廓系数高不等于业务可解释。有些K值在数学上漂亮分出来的簇却没法命名、没法对应运营动作这时候还是要回到业务判断。第三件套是业务对齐也是最终决策依据。把每个K值对应的簇中心结果打印出来和运营同事一起看分出来的每个群体能不能用一句话说清楚每一簇的占比是否合理有没有占比低于3%的碎簇我当时最终选择K4是因为K4时每个簇的人数占比在19%到33%之间特征均值差异明显运营能直接给每一类客户起名字、想策略。K5时多出一个占比只有2.7%的怪簇显然没有实际意义。5. 聚类结果怎么解读簇ID如何变成客户标签和画像5.1 簇中心就是画像的锚点K-Means训练完成后每个客户会得到一个cluster_id真正难的是把这个数字翻译成业务语言。我的突破口是cluster centers也就是簇在特征空间里的中心点。但有个关键细节模型里存的是标准化后的中心坐标必须用scaler.inverse_transform()还原成原始量纲才能解读。centers km.cluster_centers_ original_centers scaler.inverse_transform(centers) for i, center in enumerate(original_centers): print(fCluster {i}: R{center[0]:.1f}天, F{center[1]:.1f}次, M{np.expm1(center[2]):.1f}元)除了簇中心我还建议看每个簇各特征的中位数而不是只依赖均值。因为消费金额往往右偏个别大客户会把均值拉高中位数更能代表群体的一般水平。5.2 一个四簇结果的示例解读为了更直观我拿当时项目里一版四簇结果做示例数据已做脱敏变形处理但结构是真实的簇人数占比平均R天平均F次/年平均M元/年初步命名024%9141280高频稳定客119%2162200中频高额客233%353460低频价格敏感客324%1121.4210沉睡流失客这个表格一放出来运营同事瞬间就理解了。高频稳定客是商场的“基本盘”来的次数多总消费中等适合维护忠诚度中频高额客虽然来得不频繁但单次消费高是最值得用高价值权益去激活的一批人低频价格敏感客人数最多来的少且花得少但对折扣反应明显沉睡流失客基本处于流失边缘需要决定是召回还是放弃。再结合扩展特征之后画像会更立体。比如低频价格敏感客里很大比例是冲着超市和餐饮折扣来的几乎不逛服饰中频高额客则集中在美妆和精品零售对停车权益比价格折扣更敏感。这些信息让后续运营策略有据可依。5.3 可视化PCA降维散点图与雷达图聚类结果交付业务方时图表比表格更能说明问题。最常用的可视化手段是PCA降维散点图。把标准化后的特征降到二维给每个客户的点按簇着色一眼就能看到各类客户在空间上的分离程度from sklearn.decomposition import PCA pca PCA(n_components2, random_state42) coords pca.fit_transform(X_scaled) plt.figure(figsize(8, 6)) scatter plt.scatter(coords[:, 0], coords[:, 1], ckm.labels_, cmapSet2, s5, alpha0.6) plt.colorbar(scatter) plt.xlabel(PC1) plt.ylabel(PC2) plt.title(Customer Clusters Visualization) plt.show()这里有个非常重要的提醒PCA降维后的散点图只能用于查看簇的分离程度不能把PC1、PC2直接解读成业务维度。它们只是用来保留最大方差的两个数学方向投射到二维平面后距离关系会被压缩甚至扭曲。不少新人拿着PCA图去跟运营讲“PC1轴代表消费力”这是完全错误的。如果要展示各簇在具体业务维度上的差异雷达图比PCA图更合适。可以按团队习惯用Python的matplotlib或直接导数据到Excel里画每条轴线代表一个业务指标最近消费间隔、消费频率、客单价、品类集中度、优惠券使用率把每个簇相对总体均值的位置连成多边形。这样运营一眼就能看出哪个簇“价格敏感”哪个簇“消费力强”哪个簇“逛购型为主”。6. 分群后的运营打法每一类客户对应的差异化动作6.1 四类客户的策略对照表聚类的最终价值必须落到运营动作上。不同类型客户的维护目标和触达策略完全不同我整理了一个可以直接参照的策略表客户群核心目标运营动作建议触达节奏高频稳定客维持忠诚度、提升客单价专属会员价、积分加速、新品优先购、专属停车权益私域每周一次控制打扰强度中频高额客提升到店频次高价值品类定向券、生日礼、专属导购预约短信App每两周一次低频价格敏感客提升频次和客单满减券、大促预热提醒、积分抵现、打包套餐短信公众号每月一次沉睡流失客召回或低成本放弃召回礼包、限时权益、跨品类推荐短信低频触达季度一次具体到我的项目里最明显的效果来自中频高额客。这群人消费能力强、客单价高但到店频率只有每年6次左右。运营原来只给他们发全场通用的满200减30券他们根本无感。后来换成本项目里沉淀的“停车权益高价值品牌定向券”组合两个月内这批客户的到店频次提升了明显的一截。核心逻辑很朴素这群人缺的不是优惠是来商场的理由。6.2 商场场景的落地动作细节商场和电商有一个很大的不同商场运营能调动的杠杆不只是券还有场地、楼层、业态和现场体验。聚类结果可以在好几个层面落地。具体到楼层导购可以按簇画像重新设计引导动线。中频高额客应该被引导到美妆、精品零售所在的楼层高频稳定客的活动动线通常在超市、餐饮之间可以顺路设计一些中间层的促销点位。会员日活动也可以分群排期。低频价格敏感客对折扣反馈热烈把他们安排到大促预热期的爆发节点高频稳定客反而需要避开极端拥挤时段设置“会员专属安静时段”提供更舒适的消费体验。客服话术也能分群。沉睡客户触达时避免用“欢迎回归”这类通用文案而是根据画像直接说“您常用的品牌上新了”或者“您消费最高的品类正在做会员专属活动”召回效果差别很大。6.3 效果评估AB测试和指标体系聚类做完不等于项目结束还要设计一套效果评估体系。我建议在每个客户群内部做AB测试同一客群里的用户随机分成实验组和对照组实验组接收个性化策略对照组沿用原来的统一营销观察6到8周的差异。核心指标建议盯四个复购率、月活跃消费率、客单价、季度LTV。领取和核销率只能是过程指标它不代表真实业绩提升。统计上样本量够大时直接用独立双样本检验或回归模型控制混淆变量。有一点必须强调不要用简单的“活动前后对比”来证明策略有效因为节假日、天气、换季都会影响商场客流你很难把增量单独归因到运营策略上。7. 实战中我踩过的坑与两个可进阶的方向7.1 数据泄漏客户聚类里最隐蔽的坑先讲数据泄漏。我们第一版特征工程犯过一个很低级的错误客户最近消费时间R值用的是“今天”减最后消费日而不是“固定截止日”减最终消费日。结果每天重跑同一份数据聚类结果都在变后来才意识到是日期口径的问题。但更隐蔽的是另一种泄漏构造扩展特征时用全量数据的中位数去填充缺失值。这样做会导致新样本被历史数据“污染”后续每个月重新聚类时新旧数据的口径不一致结果不可比。正确的做法是先确定分析截止日所有特征都只用截止日之前的数据标准化时先记录当前批次的scaler参数后续新数据重跑时沿用这套参数而不是重新fit。这样每个月生产出来的特征才具有可比性。7.2 离群值、随机种子与冷启动K-Means对离群值非常敏感。商场里总有极少数年消费几十万的大客户他们单独形成一个簇后虽然簇中心看起来“价值高”但人数太少谈不上运营策略。我的处理方式是把客户按累计消费金额排序Top 1%单独拿出来不参与K-Means聚类而是另一套人工规则分析。剩下的中间层客户再用聚类切分结果稳定得多。冷启动是另一个容易头疼的问题。新注册客户没有足够的历史行为数据用训练好的KMeans直接predict会把他分进某一个已有的簇但这个分群实际上毫无依据。我们的做法是设定一个简单门槛frequency 1且monetary低于阈值的客户直接标记为“新客待观察”不进模型预测。等他们产生2到3次消费行为后再纳入下次聚类。关于随机种子我再强调一次必须固定random_state并且要在项目文档里记录使用的库版本。换一个环境、换一版sklearn同样的代码也可能产生不同结果。另外不同月份的聚类结果中同一个簇ID的客户群体含义可能完全不同。千万不要偷懒以为下标0永远是“高频客”每个月重跑后都重新检查簇中心并重建画像。7.3 进阶方向Mini-Batch K-Means、GMM与可解释规则如果数据量继续膨胀到百万级常规KMeans可能吃紧可以考虑Mini-Batch K-Means。它在每次迭代时只随机采样一小批样本更新中心计算速度和内存占用都会好很多聚类精度损失在可接受范围内。如果业务想获得软标签也就是每个客户属于每个簇的概率而不是硬性的簇ID可以用高斯混合模型GMM。它允许簇与簇之间存在重叠适合那些行为特征介于两个群体之间的客户。还有一个对业务落地特别有用的方向可解释聚类。聚类结果是黑盒标签但可以在聚类完成后以cluster_id为目标训练一棵简单的决策树用树模型提取“R小于15天且F大于10次则为高频稳定客”这类显式规则。这样运营系统不需要每次都调聚类模型用几条规则就能在CRM里实时打标签。最后说一点个人体会。做这类项目K-Means本身只占三成工作量其余七成精力几乎都消耗在数据口径、特征定义和业务对齐上。每次组织重跑聚类之前先问清楚业务当前最想解决什么问题——是提升客单价、唤醒沉睡客户还是调整品类结构。目标不一样特征选择和数据窗口都会完全不同。先和运营把“想解决什么问题”聊透再安排建模的工作这个顺序千万不要反。