Python 3.7.6 (default, Jan 8 2020, 20:23:39) [MSC v.1916 64 bit (AMD64)]
Type "copyright", "credits" or "license" for more information.
IPython 7.12.0 -- An enhanced Interactive Python.
• R, Rencency,即每个客户有多少天没回购了,可以理解为最近一次购买到现在
隔了多少天。
• F, Frequency,是每个客户购买了多少次。
• M, Monetary,代表每个客户平均购买金额,这里也可以是累计购买金额。
import pandas as pd
df = pd.read_excel(r'F:\Python\用实战玩转Pandas数据分析\07 RFM建模实战\RFM\PYTHON-RFM实战数据.xlsx')
df.head()
Out[1]:
品牌名称 买家昵称 付款日期 订单状态 ... 邮费 省份 城市 购买数量
0 小程在线 叫我李2 2019-01-01 00:17:59 交易成功 ... 6 上海 上海市 1
1 小程在线 0cyb1992 2019-01-01 00:59:54 交易成功 ... 0 广东省 广州市 1
2 小程在线 萝污萌莉 2019-01-01 07:48:48 交易成功 ... 8 山东省 东营市 1
3 小程在线 atblovemyy 2019-01-01 09:15:49 付款以后用户退款成功,交易自动关闭 ... 0 江苏省 镇江市 1
4 小程在线 小星期鱼 2019-01-01 09:59:33 付款以后用户退款成功,交易自动关闭 ... 0 上海 上海市 1
[5 rows x 9 columns]
我们发现在订单状态中,除了交易成功的,还有用户退款导致交易关闭的,那还包括其他状态吗?
df['订单状态'].unique()
Out[2]: array(['交易成功', '付款以后用户退款成功,交易自动关闭'], dtype=object)
只有这两种状态,其中退款订单对于我们模型价值不大,需要在后续清洗中剔除。
接着再观察数据的类型和缺失情况:
df.info()
RangeIndex: 28833 entries, 0 to 28832
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 品牌名称 28833 non-null object
1 买家昵称 28833 non-null object
2 付款日期 28833 non-null datetime64[ns]
3 订单状态 28833 non-null object
4 实付金额 28833 non-null int64
5 邮费 28833 non-null int64
6 省份 28833 non-null object
7 城市 28832 non-null object
8 购买数量 28833 non-null int64
dtypes: datetime64[ns](1), int64(3), object(5)
memory usage: 2.0+ MB
订单一共 28833 行,没有任何缺失值, Nice!类型方面,付款日期是时间格式,
实付金额、邮费和购买数量是数值型,其他均为字符串类型。
剔除退款
df = df.loc[df['订单状态'] == '交易成功',:]
print('剔除退款后还剩:%d行' % len(df))
剔除退款后还剩:27793行
关键字段提取
剔除之后,觉得我们订单的字段还是有点多,而 RFM 模型只需要买家昵称,付款时间和实付金额这 3 个关键字段,所以提取之:
df = df[['买家昵称','付款日期','实付金额']]
df.head()
Out[5]:
买家昵称 付款日期 实付金额
0 叫我李2 2019-01-01 00:17:59 186
1 0cyb1992 2019-01-01 00:59:54 145
2 萝污萌莉 2019-01-01 07:48:48 194
5 重碎叠 2019-01-01 10:00:07 197
6 iho_jann 2019-01-01 10:00:08 168
pd.to_datetime('2019-11-11') > pd.to_datetime('2019-1-1')
Out[7]: True
要拿到所有用户最近一次付款时间,只需要按买家昵称分组,再选取付款日期的最大值即可:
r = df.groupby('买家昵称')['付款日期'].max().reset_index()
r.head()
Out[8]:
买家昵称 付款日期
0 .blue_ram 2019-02-04 17:49:34.000
1 .christiny 2019-01-29 14:17:15.000
2 .willn1 2019-01-11 03:46:18.000
3 .托托m 2019-01-11 02:26:33.000
4 0000妮 2019-06-28 16:53:26.458
为了得到最终的 R 值,用今天减去每位用户最近一次付款时间,就得到 R 值了,这份订单是 7 月 1 日生成的,所以这里我们把“2019-7-1”当作“今天”:
r['R'] = (pd.to_datetime('2019-7-1') - r['付款日期']).dt.days
r = r[['买家昵称','R']]
r.head(8)
Out[9]:
买家昵称 R
0 .blue_ram 146
1 .christiny 152
2 .willn1 170
3 .托托m 170
4 0000妮 2
5 0009797王 127
6 000xyx0 73
7 000米粒儿米粒0 148
搞定 F 值
df['日期标签'] = df['付款日期'].astype(str).str[:10]
dup_f = df.groupby(['买家昵称','日期标签'])['付款日期'].count().reset_index()
f = dup_f.groupby('买家昵称')['付款日期'].count().reset_index()
f.columns = ['买家昵称','F']
f.head()
Out[12]:
买家昵称 F
0 .blue_ram 1
1 .christiny 1
2 .willn1 1
3 .托托m 1
4 0000妮 1
# M 值构造
我们只需要得到每个用户总金额,再用总金额除以购买频次,就能拿到用户平均支付金额:
sum_m = df.groupby('买家昵称')['实付金额'].sum().reset_index()
sum_m.columns = ['买家昵称','总支付金额']
com_m = pd.merge(sum_m,f,left_on = '买家昵称',right_on = '买家昵称',how = 'inner')
#计算用户平均支付金额
com_m['M'] = com_m['总支付金额'] / com_m['F']
com_m.head()
Out[13]:
买家昵称 总支付金额 F M
0 .blue_ram 49 1 49.0
1 .christiny 183 1 183.0
2 .willn1 34 1 34.0
3 .托托m 37 1 37.0
4 0000妮 164 1 164.0
三个指标合并:
rfm = pd.merge(r,com_m,left_on = '买家昵称',right_on = '买家昵称',how = 'inner')
rfm = rfm[['买家昵称','R','F','M']]
rfm.head()
维度确认的核心是分值确定,按照设定的标准,我们给每个消费者的 R/F/M 值打分,分值的大小取决于我们的偏好, 即我们越喜欢的行为,打的分数就越高:
• 以 R 值为例, R 代表了用户有多少天没来下单,这个值越大,用户流失的可能性越大,我们当然不希望用户流失,所以 R 越大,分值越小。
• F 值代表了用户购买频次, M 值则是用户平均支付金额,这两个指标是越大越好,即数值越大,得分越高。
rfm['R-SCORE'] = pd.cut(rfm['R'],bins = [0,30,60,90,120,1000000],labels = [5,4,3,2,1],right = False).astype(float)
rfm.head()
Out[15]:
买家昵称 R F M R-SCORE
0 .blue_ram 146 1 49.0 1.0
1 .christiny 152 1 183.0 1.0
2 .willn1 170 1 34.0 1.0
3 .托托m 170 1 37.0 1.0
4 0000妮 2 1 164.0 5.0
rfm['F-SCORE'] = pd.cut(rfm['F'],bins = [1,2,3,4,5,1000000],labels = [1,2,3,4,5],right = False).astype(float)
rfm['M-SCORE'] = pd.cut(rfm['M'],bins = [0,50,100,150,200,10000000],labels = [1,2,3,4,5],right = False).astype(float)
rfm.head()
每个客户和平均值对比后的 R、 F、 M,只有 0 和 1(0 表示小于平均值, 1 表示大于平均值)两种结果,整体组合下来共有 8 个分组,是比较合理的一个情况。我们来判断用户的每个分值是否大于平均值:
rfm['R是否大于均值'] = (rfm['R-SCORE'] > rfm['R-SCORE'].mean()) * 1
rfm['F是否大于均值'] = (rfm['F-SCORE'] > rfm['F-SCORE'].mean()) * 1
rfm['M是否大于均值'] = (rfm['M-SCORE'] > rfm['M-SCORE'].mean()) * 1
rfm.head()
Out[17]:
买家昵称 R F M ... M-SCORE R是否大于均值 F是否大于均值 M是否大于均值
0 .blue_ram 146 1 49.0 ... 1.0 0 0 0
1 .christiny 152 1 183.0 ... 4.0 0 0 1
2 .willn1 170 1 34.0 ... 1.0 0 0 0
3 .托托m 170 1 37.0 ... 1.0 0 0 0
4 0000妮 2 1 164.0 ... 4.0 1 0 1
[5 rows x 10 columns]
先引入一个人群数值的辅助列,把之前判断的 R\F\M 是否大于均值的三个值给串联起来:
rfm['人群数值'] = (rfm['R是否大于均值'] * 100) + (rfm['F是否大于均值'] * 10) + (rfm['M是否大于均值'] * 1)
rfm.head()
人群数值是数值类型,所以位于前面的 0 就自动略过, 比如 1 代表着“001”的高消费唤回客户人群, 10 对应着“010”的一般客户。
为了得到最终人群标签,再定义一个判断函数,通过判断人群数值的值,来返回对应的分类标签:
def transform_label(x):
if x == 111:
label = '重要价值客户'
elif x == 110:
label = '消费潜力客户'
elif x == 101:
label = '频次深耕客户'
elif x == 100:
label = '新客户'
elif x == 11:
label = '重要价值流失预警客户'
elif x == 10:
label = '一般客户'
elif x == 1:
label = '高消费唤回客户'
elif x == 0:
label = '流失客户'
return label
最后把标签分类函数应用到人群数值列:
rfm['人群类型'] = rfm['人群数值'].apply(transform_label)
rfm.head()
客户分类工作的完成,宣告着 RFM 模型建模的结束,每一位客户都有了属于自己的 RFM 标签。
各类用户占比情况:
count = rfm['人群类型'].value_counts().reset_index()
count.columns = ['客户类型','人数']
count['人数占比'] = count['人数'] / count['人数'].sum()
count
不同类型客户消费金额贡献占比:
rfm['购买总金额'] = rfm['F'] * rfm['M']
mon = rfm.groupby('人群类型')['购买总金额'].sum().reset_index()
mon.columns = ['客户类型','消费金额']
mon['金额占比'] = mon['消费金额'] / mon['消费金额'].sum()
mon
客户类型
result = pd.merge(count,mon,left_on = '客户类型',right_on = '客户类型')
result
Out[23]:
客户类型 人数 人数占比 消费金额 金额占比
0 高消费唤回客户 7338 0.288670 1338153.0 0.381098
1 流失客户 6680 0.262785 444617.0 0.126624
2 频次深耕客户 5427 0.213493 981893.0 0.279638
3 新客户 4224 0.166168 270869.0 0.077142
4 重要价值客户 756 0.029740 269230.0 0.076675
5 消费潜力客户 450 0.017703 64075.0 0.018248
6 重要价值流失预警客户 360 0.014162 116665.0 0.033226
7 一般客户 185 0.007278 25803.0 0.007349