上一期我们洗了40行销售脏数据,日期格式、金额符号、产品名写法,挨个收拾干净。今天换个更值钱的活:电商老板问你,500个用户里,哪些值得发优惠券唤醒,哪些已经流失别浪费钱?拍脑袋不行,得用数据说话。
RFM模型就是干这个的,电商分析用了几十年的老办法。R是Recency,最近一次买是多久以前;F是Frequency,买过几次;M是Monetary,一共花了多少钱。三个维度一交叉,用户分层清清楚楚。
环境:pandas 2.x加plotly,没装的跑pip install pandas plotly。数据我用代码现场生成500个用户的模拟订单,和真实订单一个结构,你换成自己的CSV直接跑。
第一步,造数据,算出RFM三列:
python
import pandas as pd
import numpy as np
np.random.seed(7)
users = [f"U{i:04d}" for i in range(500)]
orders = []
for u in users:
for _ in range(np.random.randint(1, 15)):
orders.append({
"user": u,
"date": pd.Timestamp("2026-01-01")
+ pd.to_timedelta(np.random.randint(0, 280), "D"),
"amount": round(float(np.random.gamma(2, 80)), 2),
})
df = pd.DataFrame(orders)
ref = pd.Timestamp("2026-10-20")
rfm = df.groupby("user").agg(
Recency=("date", lambda s: (ref - s.max()).days),
Frequency=("date", "count"),
Monetary=("amount", "sum"),
)
第二步,打分,每个维度5分制。注意R的方向是反的:离现在越近分越高,别打反了。打分我用rank(pct=True)转成分位数再切,这样真实数据里大量重复值也不会让qcut报错:
python
rfm["R"] = pd.cut(rfm["Recency"].rank(pct=True, ascending=False),
bins=5, labels=[5, 4, 3, 2, 1]).astype(int)
rfm["F"] = pd.cut(rfm["Frequency"].rank(pct=True),
bins=5, labels=[1, 2, 3, 4, 5]).astype(int)
rfm["M"] = pd.cut(rfm["Monetary"].rank(pct=True),
bins=5, labels=[1, 2, 3, 4, 5]).astype(int)
第三步,分群。四象限最实用:R、F都高于中位数的是明星用户;R高F低是潜力新客;R低F高是沉睡用户,曾经爱买现在不来了;双低的是流失用户,发券都未必回来。
python
rm, fm = rfm["R"].median(), rfm["F"].median()
def segment(row):
if row["R"] >= rm and row["F"] >= fm:
return "明星用户"
if row["R"] >= rm:
return "潜力新客"
if row["F"] >= fm:
return "沉睡用户"
return "流失用户"
rfm["segment"] = rfm.apply(segment, axis=1)
print(rfm.groupby("segment").agg(
人数=("R", "count"),
平均客单价=("Monetary", "mean"),
GMV=("Monetary", "sum"),
))
跑完打印一张分群表:每群人数、平均客单价、贡献的GMV。我的模拟数据跑出来头部效应明显:明星用户139人贡献21万,沉睡用户173人贡献近30万,两群加起来六成用户、八成销售额。优惠券该发给谁,一目了然。
第四步,plotly画出来。散点图x轴是距今天数,y轴是购买次数,点的大小是总花费,颜色是分群。一张图,老板看一眼就懂:
python
import plotly.express as px
fig = px.scatter(
rfm.reset_index(),
x="Recency", y="Frequency",
size="Monetary", color="segment",
title="RFM用户分层",
labels={"Recency": "距今多少天没买了", "Frequency": "买过几次"},
)
fig.show()
验证:检查四群人数加起来等于500,分群标签没有空值,散点图四个颜色都出现了。数字对不上,后面全是错的,先对数再画图。
踩坑四个。第一,qcut重复边界:真实订单日期大量重复,qcut报"Bin edges must be unique"。用rank(pct=True)加cut,或者rank(method="first"),提前避开。第二,R打分方向搞反:距今30天给了1分,距今300天给了5分,分群全反,结论反着交差,灾难。第三,日期没转datetime就做减法,字符串相减直接报错,先pd.to_datetime。第四,分群完不看业务:沉睡用户里混着只买过一次的和买过20次的,唤醒策略能一样吗?分群是起点,不是终点。
收藏这篇,下次老板要用户分层,换上真实订单CSV,这套代码改个文件名就能交差。
下期预告:数据分析实战第3期讲时间序列。销售额的周末效应怎么量化,节假日到底拉动了多少,pandas重采样加plotly一次讲透,关注我不错过。
讨论时间:你们公司做用户分层用RFM还是别的模型?效果怎么样?评论区交流下。
------ Cyrus