☰
Python实现RFM模型:从用户分层到运营策略落地
2026/10/21 17:10:01 网站建设 项目流程

做用户分层时,最常被运营部门拿来“压榨”我的需求就是:把用户分成几类,要能指导运营策略。十次里有八次,我会直接扔出一版RFM分析模型。它不是新潮算法,却是检验数据基本功最扎实的一招:只用三个基础字段,就能把用户价值切成八个可操作的横切面。这篇文章记录我用Python从零到一实现RFM分析模型的全过程,覆盖指标口径、打分逻辑、分层细节、可视化和落地策略映射,最后把我在真实数据上踩过的坑一并交代清楚。适合还没实操过RFM的初学者,也适合想让分群结果更经得起业务拷问的分析师。

1. RFM模型到底在算什么

1.1 三个指标背后的业务意义

RFM不是什么高深的统计模型,它只是把每个用户的行为浓缩成三个数字:

  • R(Recency,最近一次消费时间间隔):距分析日期最近一次下单,过去了多少天。
  • F(Frequency,消费频率):在观察周期内,用户累计下了多少单。
  • M(Monetary,消费金额):同一周期内,用户一共贡献了多少营收。

这三个字母拆开看都很朴素,但组合在一起,它回答的是运营最关心的三连问:这个用户现在还活跃吗?他愿不愿意反复买?他到底舍得花多少钱?

这也是RFM的精髓:不用机器学习,不用拟合公式,单靠这三个行为锚点,就能把用户画像描述得相当立体。举个例子,只看消费金额,也许会重点盯住一位客单价极高、但半年没下单的用户,给他发大额满减券;可RFM会立刻告诉你,这人的Recency极差,与其塞优惠券,不如先搞唤回动作。这其实就是RFM和纯粹按金额排序的最大区别——它引入了时间维度和黏性维度。

1.2 为什么这么多年还在用它

你可能会问:现在都到所谓的智能运营时代了,为什么还要用上世纪就提出的模型?我的理解是:RFM的价值在于“低门槛、高解释力、可落地”。

低门槛指的是数据要求低。一套CRM或者订单表里只要有用户标识、下单时间和订单金额,就能建模。我见过不少团队连像样的用户标签表都没有,但RFM永远不会被“没有数据”卡住。

高解释力意味着业务人员能听懂。给运营同事讲随机森林的特征重要性,对方大概率两眼一蒙;但你说“最近没买、以前常买、客单价高”,对方立刻就能接话:这是该挽回的客户。

可落地则是说,它天然连接了运营动作。RFM切出来的每一群人,都能对应到具体的触达时机、权益类型和沟通话术。这也是我为什么坚持用“模型+策略”一体的方式写这篇文章,光算分不分层,等于没做。

2. 指标设计与数据准备:比写代码更值得较劲

2.1 字段要求与口径定义

动手跑数之前,先花半小时和业务方把口径对齐,这是RFM项目最容易被忽略、也最容易出纠纷的环节。项目里我依赖的原始交易明细表至少有四个字段:

  • user_id:用户唯一标识。
  • order_time:订单创建时间,精确到天即可,必要时到秒。
  • order_amount:实付金额,必须排除“未支付”“已退款”的订单。
  • order_id:订单唯一编号,用于统计单量。

实战中经常遇到一个问题:同一笔订单包含多个商品,在明细表里它会展开成多行。如果直接按明细行数算F,一单买了三件商品就会被当成三单,频率立刻虚高。所以,我在计算订单量时强制按order_id去重,这是RFM指标体系里的关键决策。

口径对齐还体现在时间周期上。你要先定义一个“观察窗口”,比如近12个月,还是近6个月,还是上线以来全量。窗口越长,M越容易被老用户的总贡献垫高;窗口越短,新用户的F值又可能普遍偏低。我的建议是:成熟业务用近12个月,增长期业务用近6个月,并在输出报告时主动标注窗口,避免业务方拿不同口径的数字互相比较。

2.2 清洗逻辑与异常值处理

直接读原始表就开始算R-F-M,是新手最容易犯的错。数据清洗在RFM项目里不是走过场,它直接影响分群边界和后续策略的可信度。下面这几类脏数据,我在实际数据集里都碰到过:

  • 重复行:同一订单在同步链路里重复上报,导致金额翻倍。
  • 未支付订单:只创建了订单但未付款,金额为0或无效状态。
  • 超大数据:单笔金额远超正常客单价,比如测单、B端大客户、内部采购。
  • 测试账号:内部测试单混在真实订单里,污染R值。

一个实用又好用的清洗流程长这样:先剔除订单状态字段里所有非“已支付/已完成”的记录,然后按订单号去重,最后做金额的极端值筛查。极端值的处理不需要做复杂的统计建模,直接用百分位数看分布,比如把金额超过99.5分位的记录单独抽出来让业务确认。这类订单可能是真实的“鲸鱼客户”,也可能是数据事故,业务判断比单纯删行靠谱得多。

2.3 演示数据的加载与预览

为了让后续代码可运行,我先造一份结构简单、规模适中的模拟订单数据,方便你直接跑通流程。真实项目中,你只需把数据源替换成自己的表。

import pandas as pd import numpy as np # 构造演示数据:包含用户ID、下单时间、支付金额 rng = np.random.default_rng(42) n = 2000 user_ids = np.arange(1, 301) df = pd.DataFrame({ "user_id": rng.choice(user_ids, size=n), "order_time": pd.date_range("2024-01-01", "2024-12-31", periods=n).strftime("%Y-%m-%d"), "order_amount": np.round(rng.lognormal(3.0, 1.0, n) * 10, 2), }) df["order_id"] = ["order_" + str(i) for i in range(1, n + 1)] df = df.drop_duplicates(subset=["order_id"]) df.head()

这里生成的数据有细长尾,更贴近真实电商场景:大部分用户订单金额集中在中小区间,少数大额贡献突出。数据加载后,下一步会进行标准清洗。

3. 核心实现:用Python基于群组和聚合计算出R、F、M指标

3.1 计算逻辑与三步走的代码结构

计算R、F、M指标的套路并不复杂,按下面三步走:

  1. 确定节点:analysis_date。它既可以是“今天”,也可以是“数据日期的最大值”。通常使用数据日期的最大值,保证分析结论可复现。
  2. 按用户分组:对每个user_id计算R = (analysis_date - max(order_date))的天数;F = 去重后的订单数;M = 订单金额之和。
  3. 合并结果:把三个指标拼到同一张表里,形成中间宽表。

R的计算注意一个细节:R是“越小越好”,最近买过的人R值小,长时间没买的人R值大。这与直觉相反,容易在排序时搞反方向,所以在后续打分逻辑中,R值的部分需要对方向做一次“颠倒”处理。

3.2 聚合计算的标准写法

直接上代码:

# 统一时间格式,并确保金额数字类型正确 df["order_time"] = pd.to_datetime(df["order_time"]) df["order_amount"] = pd.to_numeric(df["order_amount"], errors="coerce") # 1. 先做基础清洗:过滤空金额、过滤金额为0的记录 df = df[df["order_amount"] > 0] # 2. 设定分析节点为数据中的最大日期,便于复现 analysis_date = df["order_time"].max() # 3. 计算RFM三指标 rfm = df.groupby("user_id").agg( R=("order_time", lambda x: (analysis_date - x.max()).days), F=("order_id", "nunique"), M=("order_amount", "sum"), ).reset_index() # 4. 做合理性检查 print(rfm.describe())

输出结果你会看到,R的最小值是0,表示数据期内最后一天还有下单;F的平均值会结合“窗口内平均下单次数”来理解,而M由于长尾因素,平均值往往明显大于中位数。这是后面打分和分层的现实依据。

3.3 M值为什么建议取对数

关于M值的处理,我需要专门提供一个反面经验:M值天然呈严重的右偏分布,头部大客户的金额可能是普通用户的几十倍。如果直接用金额的绝对分位数做打分,会导致大部分用户集中到极低分段,没法区分中间层的价值层级。

实操上,我习惯对M先做log1p变换,即np.log1p(M),再继续后面的分位数打分。这样既保留了金额大小次序,又压缩了极端值的干扰,打分更均匀、更有区分度。代码合并在指标计算阶段进行,省得到打分环节再重复映射。

4. 打分规则与分层命名:让每个用户都有归属

4.1 两种常用打分方式:分位数法 vs 业务阈值法

拿到R、F、M后,还没法直接说用户价值高还是低。需要把连续值离散化成可比较的分数。我常用的打分方式有两种。

分位数法:把每个指标的值排个队,按分位数切成2段、3段、5段,代码短、边界自动,适合数据量足够、分布平稳的业务。

业务阈值法:不按统计规律,而是按运营经验定死不变量,比如“近30天下单算高活性、近90天下单算中活性、其余算低活性”。适合数据量小、业务规则强或者领导已经“拍板”了阈值的场景。

两者没有谁绝对高明的区分,本质上是统计驱动还是业务驱动。我个人的原则是:首次探索用分位数,建模交付时跟业务拍板倾向用业务阈值,因为运营在排计划时更希望看到“连续30天未下单”这类可解释、可测算的数字,而非“落在P20以下”这种统计黑话。

4.2 qcut切分的边界问题和R值方向处理

在Python里做分位数切分,最常见的坑是pd.qcut会报Bin edges must be unique,这通常是因为某指标数据大量重复。比如数据窗口短,很多用户F值都是1,你想按三分位切F,切出来边界就重复了。

我习惯用rank方法绕过这个限制:先给指标排一下名次,再对名次切分。这样分位数是稳定的,不受重复值影响。

rfm["R_score"] = pd.qcut(rfm["R"].rank(method="first"), 5, labels=[5, 4, 3, 2, 1]) rfm["F_score"] = pd.qcut(rfm["F"].rank(method="first"), 5, labels=[1, 2, 3, 4, 5]) rfm["M_score"] = pd.qcut(rfm["M"].rank(method="first"), 5, labels=[1, 2, 3, 4, 5])

为什么R_score的标签是反着写的?因为R值越小越好,排名越靠前(天数短)的用户得分越高,所以排名1~20%的用户应该拿5分,排名80%~100%的用户拿1分。这个方向极其容易搞反,我把代码注释都写清楚了,也建议你在实际项目里加个反向核对。

4.3 基于三重奏的八类用户分层策略

在得到三个维度的分数后,自然会组合出足足125种排列。为了分析方便,业务通用做法是把每项分数再做一次“高/低”二值化分类。例如把4分以上算高分组,3分以下算低分组,再按R/F/M的高中低组合成8个大类。

客户类型RFM运营策略联想
重要价值客户高高高核心高净值活跃用户,优先维护、提供VIP差异化服务
重要发展客户高低高愿意花钱但黏度不足,目标是用频次运营提高复购率
重要保持客户低高高有消费能力但已经流失,需要及时唤醒和挽回动作
重要挽留客户低低高曾经的高消费力,但活跃度和频率双低,适合大额权益重点挽回
一般价值客户高高低活跃但客单不高,靠交叉销售或打包套餐提客单价
一般发展客户高低低刚入门用户,尝试提升购买频率和兴趣品类推送
一般保持客户低高低复购有余、价值偏低,谨慎控制营销成本,做轻度触达
一般挽留客户低低低基本处于沉默或长尾区,成本不允许沉淀的,可暂不主攻

这张表我几乎每次汇报都要复用一遍。不同业务的阈值可能不同,但“高价值高活跃优先重仓、低频高贡献定向激活、高活跃低客单促客单”的主逻辑是通用的。

4.4 加权总分法:不想卡八类的折中方案

八类分法信息量大,有时也会让管理层难以一次性消化。我还常常把三分数加权成一个总分,再按总分划分成黄金用户、白银用户、青铜用户。

rfm["total_score"] = rfm["R_score"] + rfm["F_score"] + rfm["M_score"] rfm["level"] = pd.cut( rfm["total_score"], bins=[0, 6, 10, 15], labels=["青铜用户", "白银用户", "黄金用户"], )

这里选权重的时候要斟酌:我会根据不同行业形态,把R或M的权重适当调高。比如面向企业客户的业务,M比F更能反映价值;而高频刚需的零售业务,R和F对活跃度更敏感。为了让结果简单透明,建议默认三权重相等,只用逻辑判断和阈值调整总分段。

5. 可视化输出与业务落地:算完分只是开始

5.1 用一张图看懂客户群分布

RFM分析结果给到业务方,如果只丢一张表,会被吐槽“看不懂”。

在我的项目里,第一个可视化一定是一张各分层占比柱状图,配上平均M和平均R。它能让人立刻意识到分层均匀不合理的地方,比如“重要挽留客户”占比过大,提示市场近段流失率不低。实现起来无非是matplotlib加groupby:

level_order = ["黄金用户", "白银用户", "青铜用户"] count_by_level = rfm["level"].value_counts().reindex(level_order) count_by_level.plot(kind="bar", color=["#d4a017", "#aaaaaa", "#cd7f32"]) plt.title("客户分层分布") plt.ylabel("用户数") plt.show()

如果项目需要生成报告,我会用plotly画交互式散点图:横轴是R,纵轴是M,点的大小代表F,颜色代表层级,这样运营点开某个点能看到具体用户和最近订单时间。交互图在周会上演示时确实能让理解成本大幅下降。

5.2 导出报表模板,给运营可直接操作的名单

算完分层和可视化,下一步往往是把名单落表。别直接整个数据表一股脑导出,更实用的做法是按运营动作拆开成多个Sheet:每个Sheet对应一类客户的待办清单,附上用户ID、最近一次购买时间、累计金额、手机号等对接字段。同时做一个全量汇总表,方便管理层下钻。

with pd.ExcelWriter("RFM用户分层清单.xlsx") as writer: rfm.to_excel(writer, sheet_name="全量汇总", index=False) for level in level_order: rfm[rfm["level"] == level].to_excel(writer, sheet_name=level, index=False)

这里有一个实操小技巧:导出前先检查M_score和R_score的分布方向,确保“黄金用户”里的用户真的是“最近购买+高金额+高频”,别因为R方向的打分层级搞反,把流失用户误标成高活跃用户。这种低级错误在真实汇报中相当尴尬。

5.3 定期滚动计算与策略验证

RFM不是跑一次就结束的一次性分析。用户的行为随时在变,今天的高价值用户下个月可能就滑落到挽留组。业务一旦跑起来,最好把这份分析脚本固化到定时任务里,按周或按日重构。

指标更新频率取决于运营节奏:日常电商可以按日重算,把每日分层流失用户推到运营后台做动态预警;稳定期业务按周重算就够。更重要的是,每个季度至少要复盘一次分层结果和运营动作的关系:某个分组的用户数量有没有因为触达策略发生迁移?高价值组人数有没有提升?这些验证反过来又会影响下一季度RFM的分层阈值,形成闭环。

6. 实战中遇到的坑与排查经验

6.1 R值的“好数值坏方向”陷阱

R值算出来是“距今天数”,这个值越小代表用户越活跃。但在做展示时,如果把R直接放进散点图,很容易给人错觉:横轴R越大越有价值。在实际业务周会上我就翻过车,运营盯着图反问:这些右边离得远的客户不是该流失了吗?为什么算成重要客户?排查后发现,只是展示时忘了把横轴做逆向处理,数据本身没错,错在可视化编码。

解决方式很简单:在产出图表前先转成active_days = -R,或者把坐标轴标签改成“最近购买天数(近更好)”,并调转横轴方向。

6.2 qcut报错Bin edges must be unique怎么处理

这是RFM代码运行中排名第一高的报错。原因前面已经提到:某个字段大量重复。最常见的解法是加一个rank(method="first"),但要注意,这会让相同值被打散到不同分位区间,在实际业务中会造成“相同消费金额的用户,一个得分高一个得分低”的现象。严格来说也有业务风险。

我在要求严格口径时会选用method="min"的排名,或者直接用pd.cut配业务阈值进行等宽切分,让边界确定性更高。更精细的团队,还可以改用分位数加抖动,不过对多数应用场景来说,rank + qcut已经能安全落地。

6.3 金额为0、退款单、运费单的处理

一些订单表里,金额字段可能是用户实付金额,包含运费。要不要扣除运费?要看目标指标定义为平台收入还是用户花费。如果业务目的是分析消费力,建议用扣除退款金额后的净实付,同时把运费剔除。

退款单尤其值得注意:如果只看订单正金额,而不减退款,M值会被高估。我一般在清洗阶段就把“已全额退款”的订单剔除,把“部分退款”的订单金额更新为净支付金额。对异常负数,先定位原始单号,多为取数口径错误,排查不全的不建议直接取绝对值来糊弄。

6.4 用户量级很大时,如何优化计算性能

当用户量或订单量达到数千万行级别时,直接在groupby里套lambda去求差会造成明显耗时。我的建议是先创建一个user_id和最大订单时间的小表,再和订单金额表做二次join聚合;或者干脆用pandas的groupby().agg配合高效聚合算子,尽量避免逐行遍历。

# 更高效的计算路径: latest_order = df.groupby("user_id")["order_time"].max().to_frame("last_order") user_stats = df.groupby("user_id").agg( F=("order_id", "nunique"), M=("order_amount", "sum"), ) rfm_fast = latest_order.join(user_stats).reset_index() rfm_fast["R"] = (analysis_date - rfm_fast["last_order"]).dt.days

再往上,如果数据量大到单机内存跑不动,可以过渡到polars或者直接上SQL。本质上,这类SQL一句话就可以实现同样的计算逻辑,pandas不是必须的。

6.5 业务方质疑“凭什么这样分层”怎么回应

RFM项目的成败不只是算法,还在于信任建立。业务方可能会问:为什么R阈值是30天而不是45天?为什么M的高分组不是前30%而是前20%?

我的应对办法十分实在:把打分和分层规则的推导过程透明化。比如在每个结果表里附加一个说明Sheet,写清楚分析周期、分析节点、R/F/M对应阈值、分层规则,以及阈值对业务波动的敏感性说明。解释清楚“为什么这样分”之后,业务方通常能更快接受结果。反正,数据模型要配合运营经验一起使用,而不是用代码替代人为判断。

6.6 对特殊行业和特殊用户的鲁棒性改进

RFM并不是放之四海而皆准。低频高消的行业比如房产、汽车,用户一生可能就消费一次,F值几乎无意义;这时可以用R和M做二维视角,再叠加联系次数、互动深度等行为指标替换F。

对B2B客户开展RFM也有坑:一个企业里的多个联系人和多个采购环节,都会关联到一笔大单,按单一用户维度分析会造成大量噪音。建议按企业ID、集团ID重新聚类之后再算R和F,这样层级清晰,也更符合业务运转逻辑。

7. 把RFM变成运营日常工具:我的最终落地心得

如果你照着上面的代码跑通了整套流程,你已经掌握了一个能在绝多数公司沉淀下来的分析框架。但要让它真正成为团队日常运营的工具,我特别想提醒你三件事。

报表要自动化、可刷新。不要每次手工跑脚本、手工发邮件,把核心分析固化到一个调度里,让运营每周一自己打开最新结果表就行。

阈值要有动态监听的意识。RFM的分位数切分是自动边界的,但当业务进入大促节点、季节性淡旺季时,R和F的整体分布会整体位移,分层结果也会跟着移动。你需要在模型旁边做一个分布漂移预警,当某指标分位点的日期边界变化超过设定值时,及时调整规则。

最终要回到行动。我最常对团队说的一句话是:RFM不是Excel里的一个sheet,而是运营所有动作回测的起点。哪个分层的客户对你发出的优惠券响应率高,是黄金客户还是挽留客户,只有每次把触达结果回流到RFM模型上一同观察,才能真正挖掘出用户经营的门道。这也是我为什么坚持在分群结果里永远保留渠道来源、最近触达时间这类字段——它们让模型随时可以解释运营决策带来的增量。

最后再分享一个小经验:在给老板汇报时,永远附上一张分层用户数量占比和对应营收贡献占比的对照表。只看用户占比,青铜客户占大头很正常;拉上营收贡献再往后看,黄金客户那百分之五到底出了多少力,往往一眼能看懂。这也是RFM最有说服力的地方,就让数据自己说话。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询