资讯详情

RFM模型实战:英国电商客户分层与用户周期分析

📅 2026/10/16 12:09:45 | 华诺云谱 👁 阅读
RFM模型实战:英国电商客户分层与用户周期分析
1. 为什么我选择用RFM模型来拆英国电商数据电商数据分析这个领域最容易犯的毛病就是一上来就堆图表——折线图、柱状图、饼图铺满整个仪表盘看起来很唬人但看完之后你问他所以呢该做什么他答不上来。我做了这么多年数据项目最深的体会就是分析的价值不在于你展示了多少数据而在于你帮业务方做了多少可执行的决策。这次我拿到的是一个英国在线零售商的交易数据集覆盖了相当长一段时间的销售记录。原始需求很简单——帮我看看客户情况。但看看客户情况这五个字太模糊了翻译成可执行的分析目标至少得回答三个问题哪些客户值得重点维护哪些客户正在流失不同客户群体该怎么区别对待要回答这三个问题RFM模型几乎是绕不开的经典框架。RRecency看最近一次购买时间FFrequency看购买频次MMonetary看消费金额。这三个维度组合起来就能把客户分成高价值活跃客户沉睡的大客户新客流失边缘客户等若干群体。但光有RFM还不够因为RFM是一个截面指标——它告诉你客户现在处于什么状态但不告诉你客户怎么变成这个状态的。所以我加上了用户周期分析也就是从时间维度去看客户的活跃-沉睡-回流轨迹把截面和纵截面结合起来才能形成完整的客户画像。这篇文章我会把整个项目的思路、代码、踩过的坑全部摊开来讲。适合谁看如果你手头正好有一份电商交易数据想做客户分层但不知道从哪下手那这篇可以直接抄作业。如果你已经做过RFM但觉得效果一般那用户周期分析这部分可能会给你一些新思路。代码用Python写的pandas matplotlib seaborn没有花哨的东西都是能跑起来的实用方案。2. 数据理解与预处理别急着建模先把数据摸透2.1 数据集的基本情况与字段解读这份英国电商数据集的核心字段包括发票编号InvoiceNo、商品编码StockCode、商品描述Description、数量Quantity、发票时间InvoiceDate、单价UnitPrice、客户IDCustomerID、国家Country。数据量大概在几十万到上百万行级别时间跨度大约一年。拿到数据的第一件事不是写代码而是用眼睛看。我习惯先打开CSV文件扫几眼前几百行感受一下数据长什么样。这一步很多人跳过直接df.head()了事但其实手动翻一翻能发现很多describe()看不出来的问题。比如我翻的时候就注意到几个情况有些InvoiceNo是以C开头的这通常代表取消订单有些Quantity是负数说明是退货CustomerID有大量空值意味着这部分交易没有关联到具体客户。这些细节直接决定了后面的清洗策略。如果你不先搞清楚这些上来就跑RFM结果里混着一堆退货和匿名交易分层结果肯定是不准的。2.2 数据清洗的关键决策与操作细节数据清洗这一步我的原则是每一步清洗都要有业务理由不能为了清洗而清洗。下面是我实际执行的清洗步骤和背后的思考。第一步处理缺失的CustomerID。数据中有相当比例的记录没有CustomerID。这些记录要不要保留我的判断是不保留。原因很简单RFM分析的核心是按客户聚合没有客户ID就没法聚合。虽然这些交易本身是真实的营收但在客户分层的语境下它们提供不了任何客户维度的信息。所以直接drop掉。df df.dropna(subset[CustomerID]) df[CustomerID] df[CustomerID].astype(int)第二步剔除取消订单和退货。InvoiceNo以C开头的记录是取消订单Quantity为负的是退货。这两类数据在RFM分析中会严重干扰结果——一个客户如果频繁下单又频繁取消他的Frequency会虚高但实际价值可能很低。df df[~df[InvoiceNo].astype(str).str.startswith(C)] df df[df[Quantity] 0] df df[df[UnitPrice] 0]注意UnitPrice我也过滤了大于0的因为数据里有少量单价为0的记录可能是赠品或数据录入错误。赠品算不算消费金额从RFM的M维度来看赠品不产生实际营收所以我选择排除。第三步创建总价字段。原始数据只有Quantity和UnitPrice需要计算每笔交易的总金额。df[TotalPrice] df[Quantity] * df[UnitPrice]第四步处理重复记录。数据中存在完全重复的行可能是系统导出时的重复。直接去重。df df.drop_duplicates()清洗完之后我一般会做一个快速的数据概览确认清洗没有误伤print(f清洗后记录数: {len(df)}) print(f唯一客户数: {df[CustomerID].nunique()}) print(f时间范围: {df[InvoiceDate].min()} 到 {df[InvoiceDate].max()}) print(f总营收: {df[TotalPrice].sum():,.2f})注意清洗的每一步都建议单独执行并检查中间结果不要一口气跑完整个pipeline。我有一次就是因为把退货过滤和去重写在一起结果去重把退货记录里的正常订单也误删了排查了半天才发现。2.3 探索性分析先看清楚数据在说什么清洗完之后别急着上RFM。先做几组探索性分析对数据的整体分布有个感觉。交易时间分布按小时、按星期、按月分别看交易量的分布。我当时发现这个英国零售商的高峰交易时段集中在上午10点到下午3点周四的交易量明显高于其他工作日。这个信息对后续做用户周期分析很有用——如果你发现某个客户的购买时间突然从工作日变成了周末可能意味着他的购物习惯在变化。客单价分布用describe()看TotalPrice的分布你会发现均值远大于中位数说明存在少数极高金额的订单拉高了均值。这种右偏分布在做RFM分位数切分时需要注意用等频分箱qcut比等宽分箱cut更合适。国家分布虽然叫英国电商数据但实际上有大量非英国本土的交易。英国本土客户占比大概在90%以上其余来自其他欧洲国家。做客户分层时可以考虑是否要把非英国客户单独拿出来分析因为不同国家客户的购买行为差异可能很大。月度营收趋势按月的营收折线图能帮你判断业务的季节性。我这份数据里11月的营收明显高于其他月份这跟年末购物季的规律吻合。如果你的数据里某个月突然暴跌那就要去排查是不是数据缺失或者业务出了问题。3. RFM模型构建从打分到分层3.1 RFM指标的计算逻辑RFM的计算本身不复杂但有几个关键决策点需要想清楚。R值的计算基准日怎么定最常见的做法是用数据集中的最大日期加一天作为基准日。为什么加一天因为如果直接用最大日期那么当天有购买的客户R值为0而R值通常我们希望是正整数。加一天之后当天购买的客户R值为1表示1天前购买过。snapshot_date df[InvoiceDate].max() pd.Timedelta(days1) rfm df.groupby(CustomerID).agg({ InvoiceDate: lambda x: (snapshot_date - x.max()).days, InvoiceNo: nunique, TotalPrice: sum }).reset_index() rfm.columns [CustomerID, Recency, Frequency, Monetary]这里Frequency用的是nunique而不是count因为一个发票编号可能对应多行商品记录用nunique才是真正的购买次数。R、F、M的权重问题。经典RFM三个维度是等权的但实际业务中未必。比如对于快消品电商R可能比M更重要因为客户很容易流失对于奢侈品电商M的权重可能更高。我这次先用等权处理后面会讲怎么根据业务调整。3.2 打分策略等频分箱 vs 自定义阈值打分这一步我试过两种方案各有适用场景。方案一等频分箱qcut。把每个指标按分位数切成5等份分别打1-5分。这种方案的好处是每个分数段的客户数量大致相等分布均匀。但缺点是分段的边界完全由数据驱动可能不符合业务直觉。比如R值切出来可能是[1, 15, 42, 98, 250, 365]你很难跟业务方解释为什么98天是一个分界线。rfm[R_Score] pd.qcut(rfm[Recency], 5, labels[5,4,3,2,1]) rfm[F_Score] pd.qcut(rfm[Frequency].rank(methodfirst), 5, labels[1,2,3,4,5]) rfm[M_Score] pd.qcut(rfm[Monetary], 5, labels[1,2,3,4,5])注意R值的打分是反的——Recency越小越近购买分数越高所以labels是[5,4,3,2,1]。方案二自定义阈值。根据业务经验设定分界点。比如R值30天以内5分30-60天4分60-90天3分90-180天2分180天以上1分。这种方案的好处是解释性强业务方一听就懂。缺点是需要对业务有足够了解否则阈值可能设得不合理。我最终的方案是混合使用先用qcut跑一版看分布再根据分布情况调整阈值。比如如果qcut切出来第一段是0-15天但业务上认为30天内都算近期活跃那我就手动把阈值调到30天。3.3 客户分层从RFM得分到业务标签有了R、F、M三个维度的分数之后怎么组合成客户标签最简单的做法是把三个分数拼成一个字符串比如555代表最近购买、高频、高消费的客户。但这样会产生125种组合太细了业务方根本用不过来。我的做法是先把每个维度二值化——高于平均分的算高低于平均分的算低这样就有8种组合对应8类客户客户类型RFM特征描述重要价值客户高高高最近买过、买得频繁、花得多重要保持客户低高高以前买得多但最近不来了重要发展客户高低高最近买过、客单价高但频次低重要挽留客户低低高花得多但很久没来频次也低一般价值客户高高低最近买、频次高但客单价低一般保持客户低高低以前频次高但最近不来了一般发展客户高低低最近买过但频次和金额都低一般挽留客户低低低三个维度都低基本流失rfm[R_Level] (rfm[R_Score] 3).map({True: 高, False: 低}) rfm[F_Level] (rfm[F_Score] 3).map({True: 高, False: 低}) rfm[M_Level] (rfm[M_Score] 3).map({True: 高, False: 低}) def label_customer(row): if row[R_Level] 高 and row[F_Level] 高 and row[M_Level] 高: return 重要价值客户 elif row[R_Level] 低 and row[F_Level] 高 and row[M_Level] 高: return 重要保持客户 # ... 其余组合 else: return 一般挽留客户 rfm[Customer_Label] rfm.apply(label_customer, axis1)分完层之后统计每类客户的数量和总营收贡献你会发现一个典型的二八分布——重要价值客户可能只占20%左右但贡献了60%以上的营收。这个结论本身就是给业务方最直观的输入。4. 用户周期分析RFM看不到的那一面4.1 为什么要做用户周期分析RFM有一个天然的局限它是静态的。你拿某一天的数据算RFM得到的是那一天的客户状态。但客户状态是动态变化的——一个上个月还是重要价值客户的人这个月可能已经变成重要挽留客户了。如果你只看截面就错过了客户状态迁移的轨迹。用户周期分析要解决的就是这个问题。核心思路是把时间轴切成若干个窗口比如按月在每个窗口内计算客户的活跃状态然后追踪客户在不同窗口之间的状态变化。这样你就能看到哪些客户在持续活跃哪些客户从活跃变成了沉睡哪些沉睡客户又回流了4.2 用户生命周期阶段的定义我一般把用户生命周期分为五个阶段新客期首次购买后的第一个月。这个阶段的客户对品牌还没有形成忠诚度需要重点培育。成长期购买频次和金额在上升的客户。说明客户对产品越来越认可是重点维护对象。成熟期购买行为稳定的客户。频次和金额波动不大是营收的基本盘。沉睡期超过一定时间没有购买的客户。这个一定时间需要根据业务周期来定快消品可能30天耐用品可能90天。流失期超过更长时间没有购买且没有回流迹象的客户。# 按月计算每个客户的购买情况 df[YearMonth] df[InvoiceDate].dt.to_period(M) monthly_customer df.groupby([CustomerID, YearMonth]).agg({ TotalPrice: sum, InvoiceNo: nunique }).reset_index()4.3 用户状态迁移矩阵的构建有了每个月的客户状态之后就可以构建状态迁移矩阵了。这个矩阵的行是上个月的状态列是这个月的状态单元格里的数字是从上个月状态迁移到本月状态的客户数量。# 为每个客户每个月打上生命周期标签 def get_lifecycle_stage(customer_months): stages [] for i, row in customer_months.iterrows(): if i 0: stages.append(新客期) elif row[months_since_last] 3: stages.append(流失期) elif row[months_since_last] 1: stages.append(沉睡期) elif row[freq_trend] 0: stages.append(成长期) else: stages.append(成熟期) return stages这个迁移矩阵能告诉你很多RFM看不到的信息。比如你可能会发现沉睡期客户回流到成长期的比例其实不低说明沉睡客户值得花力气去召回。或者你可能会发现新客期客户直接跳到流失期的比例很高说明新客的留存环节有问题。4.4 复购率与回购周期的计算用户周期分析里还有两个关键指标复购率和回购周期。复购率在某个时间窗口内购买了两次及以上的客户占比。这个指标反映了客户的粘性。# 计算每个客户的购买次数 customer_freq df.groupby(CustomerID)[InvoiceNo].nunique() repeat_rate (customer_freq 1).sum() / len(customer_freq)回购周期客户两次购买之间的平均间隔天数。这个指标直接指导你的营销节奏——如果平均回购周期是45天那你应该在客户购买后35天左右开始触达而不是等到60天。# 计算每个客户的回购周期 df_sorted df.sort_values([CustomerID, InvoiceDate]) df_sorted[prev_date] df_sorted.groupby(CustomerID)[InvoiceDate].shift(1) df_sorted[gap] (df_sorted[InvoiceDate] - df_sorted[prev_date]).dt.days avg_gap df_sorted.groupby(CustomerID)[gap].mean().median()我实测下来这份数据的复购率大概在65%左右中位回购周期在30-40天之间。这意味着有三分之一的客户只买了一次就再也没回来而回头客的平均回购节奏是一个月出头。这两个数字直接决定了后续营销策略的设计。5. 可视化与业务洞察输出5.1 RFM三维散点图与客户分布可视化这块我不追求花哨追求的是一眼能看懂。RFM最直观的可视化是三维散点图但三维图在二维屏幕上其实不好读。我一般用三个两两组合的二维散点图代替R vs F、R vs M、F vs M每个点代表一个客户颜色代表客户分层标签。import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 3, figsize(18, 5)) pairs [(Recency, Frequency), (Recency, Monetary), (Frequency, Monetary)] for ax, (x, y) in zip(axes, pairs): sns.scatterplot(datarfm, xx, yy, hueCustomer_Label, alpha0.5, axax) ax.set_title(f{x} vs {y}) plt.tight_layout() plt.show()从散点图上你能直观地看到重要价值客户集中在左下角低Recency、高Frequency重要保持客户集中在左上角高Recency、高Frequency而一般挽留客户散落在右上角高Recency、低Frequency、低Monetary。5.2 客户分层占比与营收贡献对比比散点图更有冲击力的是分层占比 vs 营收贡献的对比图。用两个并排的柱状图或者一个双轴图左边是客户数量占比右边是营收贡献占比。我这份数据跑出来的结果是重要价值客户占客户总数的18%左右但贡献了约55%的营收而一般挽留客户占了将近30%的客户数营收贡献不到8%。这个对比一出来业务方立刻就知道该把资源往哪里投了。5.3 用户周期迁移桑基图用户周期迁移最适合用桑基图来展示。桑基图能清晰地展示从一个月到下一个月的客户流向——多少人从成长期流向了成熟期多少人从沉睡期回流到了成长期多少人从新客期直接流失了。不过桑基图在Python里画起来稍微麻烦一点需要用到plotly的sankey模块。如果不想折腾用热力图来展示迁移矩阵也够用transition_matrix pd.crosstab( lifecycle_df[prev_stage], lifecycle_df[curr_stage], normalizeindex ) sns.heatmap(transition_matrix, annotTrue, fmt.1%, cmapYlOrRd)热力图的每一行代表上个月的状态每一列代表这个月的状态单元格里的百分比表示迁移概率。你重点看对角线状态不变和右上角状态恶化就行了。5.4 从数据到行动分层运营策略建议分析做到最后一定要落到行动建议上。不然业务方看完报告只会说哦知道了然后该干嘛干嘛。针对我这份数据的分层结果我给出的策略是这样的重要价值客户这是核心资产策略是维护增值。具体动作包括专属客服通道、新品优先体验、积分加倍。不需要打折因为他们已经在买了打折反而损失利润。重要保持客户这是最值得挽回的群体。他们曾经花了很多钱但最近不来了。策略是唤醒召回。具体动作发送个性化召回邮件附上他们之前常买品类的优惠券电话回访了解流失原因。重要发展客户客单价高但频次低说明他们有消费能力但购买需求不频繁。策略是提频。具体动作推荐关联品类、设置阶梯满减刺激凑单、定期推送新品。一般挽留客户数量大但价值低不值得投入太多资源。策略是低成本触达。具体动作批量邮件营销、社交媒体再营销不投入人工客服资源。6. 实操中踩过的坑与排查技巧6.1 数据清洗阶段的典型问题问题一CustomerID是浮点数。原始数据里CustomerID读进来是float64类型因为有空值。dropna之后虽然没空值了但类型还是float。如果不转成int后面groupby的时候会出现12345.0和12345被当成两个不同客户的情况。这个坑我踩过排查了半天才发现是类型问题。问题二InvoiceDate的时区问题。如果数据里带时区信息而你在计算Recency的时候没有统一时区算出来的天数可能差一天。建议在读数据的时候就统一处理pd.to_datetime(df[InvoiceDate]).dt.tz_localize(None)。问题三退货订单的识别不完整。除了InvoiceNo以C开头的还有一些退货是通过负数的Quantity体现的但InvoiceNo不带C。所以过滤条件要同时检查InvoiceNo和Quantity两个字段。6.2 RFM分层的边界情况处理边界情况一F值高度集中。如果大部分客户的购买频次都是1-2次qcut切出来的5个分段可能边界是[1, 1, 2, 2, 3]导致打分没有区分度。这时候要么减少分段数改成3段要么用自定义阈值。边界情况二M值存在极端值。少数客户的消费金额可能是中位数的几十倍qcut切出来的分段会非常不均匀。建议先对M值做对数变换再分箱或者用rank之后再qcut。边界情况三新客户没有历史数据。如果客户只买了一次他的F1R值取决于购买时间。这类客户在RFM里通常会被打成一般发展客户或一般挽留客户但实际上他们是最有潜力的新客。建议单独标记新客标签不要跟老客混在一起分层。6.3 用户周期分析中的常见误区误区一窗口切得太细。有人把时间窗口切成周结果发现大部分客户在周维度上都是不活跃的因为购买周期本来就比一周长。窗口大小应该跟业务周期匹配快消品按月耐用品按季度。误区二忽略季节性因素。如果你在11月做分析会发现大量客户在11月活跃然后12月突然沉睡——其实他们只是过了购物季。做周期分析时要把季节性因素考虑进去或者至少用同比而不是环比。误区三把相关当因果。你发现沉睡客户回流后客单价更高这不代表让客户沉睡就能提高客单价。可能只是那些本来就有高消费能力的客户恰好沉睡了一段时间。做归因分析时要谨慎。6.4 性能优化百万级数据的处理技巧数据量大的时候pandas的groupby和apply会非常慢。几个实用的优化技巧把CustomerID设成索引用df.set_index(CustomerID)groupby会快很多避免用apply尽量用向量化操作。比如计算Recency可以用groupby(CustomerID)[InvoiceDate].max()然后直接做减法如果数据超过内存用dtype优化把float64降成float32把object类型的低基数字段转成category终极方案是用polars替代pandas同样的操作能快5-10倍API也很接近# 优化示例用向量化替代apply snapshot_date df[InvoiceDate].max() pd.Timedelta(days1) rfm df.groupby(CustomerID).agg( Recency(InvoiceDate, lambda x: (snapshot_date - x.max()).days), Frequency(InvoiceNo, nunique), Monetary(TotalPrice, sum) )7. 这个分析框架还能怎么扩展RFM加用户周期分析这个组合本身已经能覆盖大部分客户分析的需求了。但如果你想让分析更深入还有几个方向可以扩展。加入品类维度。现在的RFM只看总金额但客户在不同品类上的消费行为可能完全不同。一个客户可能在A品类是高价值客户在B品类是沉睡客户。按品类分别做RFM能帮你发现交叉销售的机会。加入渠道维度。如果数据里有渠道信息网页、App、线下可以分析不同渠道客户的RFM差异。通常App客户的频次更高网页客户的客单价更高这些差异直接影响渠道运营策略。预测模型的引入。RFM是描述性的如果你想预测客户未来会不会流失可以基于RFM特征训练一个分类模型。特征工程的时候除了R、F、M三个原始值还可以加入趋势特征最近三个月F值的变化率、波动特征M值的标准差等。自动化监控。把整个分析流程封装成脚本每月自动跑一次输出客户分层的变动报告。重点关注两个指标重要价值客户的数量变化是不是在减少、沉睡客户的回流率召回策略有没有效果。我个人在实际操作中的体会是RFM模型最大的价值不在于模型本身有多复杂而在于它提供了一个共同语言——业务方、运营方、数据方都能用重要价值客户沉睡客户这些标签来沟通减少了大量你说的这个客户到底指谁的扯皮时间。而用户周期分析则补上了时间维度让这个共同语言从静态变成了动态。两套方法配合使用基本能覆盖电商客户分析80%以上的场景。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑