背景:公司某产品线多年合作的个别优质客户已停止合作。具体原因可能技术上的,或是客户那边换了领导,和我们的老板不熟不想合作。亦或者是市场所致,即我们的业务不满足客户们的需求,被细分业务领域更强的单位给代替了。
从历史数据中挖线索不难发现,这些多年合作客户在某一时间节点上,处于一个半月以上未产生业务量,但一年的业务量及产生的金额多。RFM这种多指标综合模型(https://www.jianshu.com/p/666ef32ba938),往往可以根据此类业务特性,达到提前预警流失客户的功能,做到提前预知客户的合作态度,使得后续业务动作变得更为有效。
基本流程与思路:按传统做静态RFM模型,然后以1个月为步长1年回滚数据的筛选方式,去找历史阶段中某一客户在RFM上的用户分层结果,最重要的是它能反映客户"上一期"的变化,有可能从“重要客户”变为“不重要客户”。
读取数据
df = pd.read_csv('RFM建模数据.csv')
df['time'] = pd.to_datetime(df['time'])
df
首先我们看一下这份数据的范围
选择2021.5.15-2022.5.15的数据,给数据留出一点空间,为了在数据更新后,能选择2021.6.15-2022.6.15的数据。
df_model = df[(df['time']>='2021-05-15')&(df['time']<'2022-05-16')]
为什么不用小于等于
如果使用小于等于,时间不会选到5.15
构造R、F、数据更新日期
构造F
max_time = df_model['time'].max()#2022-05-15
(df_model.groupby(['code','cate'])
.agg(最近诊断时间 = pd.NamedAgg(column='time',aggfunc='max')#以code和cate作为分组,计算time的最大值
,F = pd.NamedAgg(column='code',aggfunc='count'))#以code和cate作为分组,计算code的产生的业务量
.reset_index()
)
上述写法为pandas链式操作,省去了赋值变量即每一次步骤做一个等号的场景,能够极大提升数据处理的效率。参见:https://zhuanlan.zhihu.com/p/530218705
构造R
RFM_pre = (df_model.groupby(['code','cate'])
.agg(最近诊断时间 = pd.NamedAgg(column='time',aggfunc='max') #以code和cate作为分组,计算time的最大值
,F = pd.NamedAgg(column='code',aggfunc='count'))#以code和cate作为分组,计算code的产生的业务量
.reset_index()
.assign(数据更新日期 = max_time
,R = lambda x:(x['数据更新日期'] - x['最近诊断时间']).dt.days
)
)[['code', 'cate', '最近诊断时间', '数据更新日期', 'R','F']]
RFM_pre
构造M
看一下单价表
weight_M = pd.read_excel(r'金额表.xlsx',sheet_name='Sheet1')
weight_M
按不同subcate(子类别)去计算产生的总金额
(df_model.groupby(['code','cate','subcate']).code.count().to_frame('次数').reset_index()
.pipe(lambda x:x.merge(weight_M,how = 'left'))
.assign(总金额 = lambda x:x['次数']*x['price'])
)
weight_M = pd.read_excel(r'金额表.xlsx',sheet_name='Sheet1')
M_group = (df_model.groupby(['code','cate','subcate']).code.count().to_frame('次数').reset_index()#按code,cate,subcate计算统计量
.pipe(lambda x:x.merge(weight_M,how = 'left'))
.assign(总金额 = lambda x:x['次数']*x['price'])
.groupby(['code','cate']).总金额.sum().to_frame('总金额').reset_index()#将项目小类做个汇总
).rename(columns={'总金额':'M'})
RFM_result = (RFM_pre
.merge(M_group,how='left')
.fillna(0)
)
RFM_result
看一下相关性系数
没什么问题,我以为F(频率)、M(金额)的相关性系数较高。如果碰到此种情形,我认为去掉M即可,RFM模型降级成RF模型,维度越低可视化效果越好。
遍历每个大类的RFM结果
需要根据自己的业务逻辑去定阈值,这里设定的阈值是45天未发生业务,一年业务量的中位数,以及一年产生金额的中位数。
LS = []
for cate in RFM_result['cate'].unique():
RFM = (RFM_result[RFM_result['cate']==cate]
.assign(R_ = lambda x:np.where(x['R']>45,0,1)#如果超过一个半月未发生业务,则返回0,否则1
,F_ = lambda x:np.where(x['F']>x['F'].median(),1,0)#业务量大于中位数的返回1,否则0
,M_ = lambda x:np.where(x['M']>x['M'].median(),1,0)#产生金额大于中位数的返回1,否则0
)
)
LS.append(RFM)
RFM_model_res = pd.concat(LS).reset_index(drop=True)
RFM_model_res
拼接RFM编码
RFM_model_res['RFM'] = RFM_model_res['R_'].astype(str)+RFM_model_res['F_'].astype(str)+RFM_model_res['M_'].astype(str)
RFM_model_res
给RFM打标签
def transfer_user(x):
if x['RFM']== '000':
return '可能流失客户'
elif x['RFM']=='010':
return '紧急回访客户'
elif x['RFM']=='100':
return '一般发展客户'
elif x['RFM']=='110':
return '一般价值客户'
elif x['RFM']=='001':
return '紧急挽留客户'
elif x['RFM']=='011':
return '紧急维护客户'
elif x['RFM']=='101':
return '重要发展客户'
elif x['RFM']=='111':
return '优质客户'
RFM_model_res['用户分层'] = RFM_model_res.apply(transfer_user,axis=1)
RFM_model_res
此时,RFM建模完成了。但这只是静态模型,记得一开始选择时间段是2021.5.15-2022.5.15。若倒推"一个月",选择2021.4.15-2022.4.15是不是可以了解这些用户类型是否发生了变化?例如某些客户从"一般发展客户"变成了"紧急维护客户"。
有了这个想法之后,后续的数据处理设计就有了模糊的创造点。先尝试自定义一个函数,使得以上流程可以通过仅改变时间就能返回想要的结果。
动态RFM模型的第一次设计
将上述数据处理流程封装到RFM_Model里面。
def RFM_Model(df,start_time,end_time):
df_model = df[(df['time']>= start_time)&(df['time']< end_time)]
max_time = df_model['time'].max()
RFM_pre = (df_model.groupby(['code','cate'])
.agg(最近诊断时间 = pd.NamedAgg(column='time',aggfunc='max')
,F = pd.NamedAgg(column='code',aggfunc='count'))
.reset_index()
.assign(数据更新日期 = max_time
,R = lambda x:(x['数据更新日期'] - x['最近诊断时间']).dt.days
)
.sort_values(by='最近诊断时间')
)[['code', 'cate', '最近诊断时间',
'数据更新日期', 'R','F'
]]
weight_M = pd.read_excel(r'金额表.xlsx',sheet_name='Sheet1')
M_group = (df_model.groupby(['code','cate','subcate']).code.count().to_frame('次数').reset_index()
.pipe(lambda x:x.merge(weight_M,how = 'left'))
.assign(总金额 = lambda x:x['次数']*x['price'])
.groupby(['code','cate']).总金额.sum().to_frame('总金额').reset_index()
).rename(columns={'总金额':'M'})
RFM_result = (RFM_pre
.merge(M_group,how='left')
.fillna(0)
)
LS = []
for cate in RFM_result['cate'].unique():
RFM = (RFM_result[RFM_result['cate']==cate]
.assign(R_ = lambda x:np.where(x['R']>45,0,1)
,F_ = lambda x:np.where(x['F']>x['F'].median(),1,0)
,M_ = lambda x:np.where(x['M']>x['M'].median(),1,0)
)
)
LS.append(RFM)
RFM_model_res = pd.concat(LS).reset_index(drop=True)
RFM_model_res['RFM'] = RFM_model_res['R_'].astype(str)+RFM_model_res['F_'].astype(str)+RFM_model_res['M_'].astype(str)
#import swifter
def transfer_user(x):
if x['RFM']== '000':
return '可能流失客户'
elif x['RFM']=='010':
return '紧急回访客户'
elif x['RFM']=='100':
return '一般发展客户'
elif x['RFM']=='110':
return '一般价值客户'
elif x['RFM']=='001':
return '紧急挽留客户'
elif x['RFM']=='011':
return '紧急维护客户'
elif x['RFM']=='101':
return '重要发展客户'
elif x['RFM']=='111':
return '优质客户'
#RFM_model_res['用户分层'] = RFM_model_res.swifter.apply(transfer_user,axis=1)
RFM_model_res['用户分层'] = RFM_model_res.apply(transfer_user,axis=1)
return RFM_model_res
调用自定义函数RFM_Model
拼接相邻一个月,不同时间段的RFM
mod1 = RFM_Model(df,'2021-05-15','2022-05-16').assign(类型= '2022-05-15')
mod2 = RFM_Model(df,'2021-04-15','2022-04-16').assign(类型= '2022-04-15')
(pd.concat([mod1,mod2])
.pipe(lambda x:pd.crosstab([x['code'],x['cate']],x['类型'],values=x['用户分层'],aggfunc=sum)).reset_index()
).fillna('本期无数据')
简单查一下变化
code为794的客户在ECG的大类(产品线)中,从"优质客户"变为了"紧急回访客户",
下一步检查下794客户的RFM明细
看来逻辑是跑得通的。
动态RFM模型的第二次设计
再次回想我们是怎么选择时间的,开始时间:2021-05-15,2021-04-15....,结束时间:2022-05-15,2021-04-15。现在就是让RFM_Model(df,start_time,end_time)中start_time和end_time参数不断发生改变,最终拿到2020-15至2022-05-15的所有RFM结果。
参数怎么选择
从end_time来看,我们要依次选择2022.5.15,2022.4.15,...2021.5.15
- 首先:选择2021.5-2022.5的每月结尾时间点
end_time_ls = list(pd.date_range('2021-05-15','2022-06-01',freq='M').astype(str))
end_time_ls
- 其次:使用replace函数将尾数替换为15,用列表推导式就能得到这个时间段每月的15号
end_time_rs = [end_time_ls[x].replace(end_time_ls[x][8:10],'15') for x in range(len(end_time_ls))]
end_time_rs
- 最后:梳理好逻辑后放到循环里面跑结果,那么模型就会依次输入想要的时间,并输出最终的RFM模型
from tqdm import tqdm
end_time_ls = list(pd.date_range('2021-05-15','2022-06-01',freq='M').astype(str))
end_time_rs = [end_time_ls[x].replace(end_time_ls[x][8:10],'15') for x in range(len(end_time_ls))]
start_time_ls = list(pd.date_range('2020-05-15','2021-06-01',freq='M').astype(str))
start_time_rs = [start_time_ls[x].replace(start_time_ls[x][8:10],'15') for x in range(len(start_time_ls))]
RFM_LS = []
for time_index in tqdm(range(len(end_time_rs))):
rfm_mod = RFM_Model(df,start_time_rs[time_index],list(pd.to_datetime(end_time_rs)+pd.DateOffset(days=1))[time_index])
rfm_mod['类型'] = end_time_rs[time_index]
RFM_LS.append(rfm_mod)
RFM_application = pd.concat(RFM_LS).reset_index(drop=True)
RFM_application
因为这次的循环带上了自定义模型以及时间运算,因此使用了tqdm工具,它可以看到循环进度,如果运行时间过长那么需要重新设计程序。
这时候我们再去看RFM动态结果
(RFM_application
.pipe(lambda x:pd.crosstab([x['code'],x['cate']],x['类型'],values=x['用户分层'],aggfunc=sum)).reset_index()
).fillna('本期无数据')
这时,我们分13次记录了2020.5.15-2022.5.15的用户分层结果,可以看到"可能流失客户"会变成"无数据","优质客户"变成了"紧急维护客户",RFM模型因增加了一个时间维度而动了起来。
PowerBI中的应用效果
将第一次建模结果展示在第一页,给一个页面跳转,去看用户分层的历史变化。
总结+闲谈
本次建模还有诸多不足,无法通过PowerBI的DAX函数,对RFM进行复杂操作。如果可以,那么RFM模型的动态效果将十分亮眼。欣赏PowerBI佐罗了作品https://zhuanlan.zhihu.com/p/387233842,实属令人羡慕。佐罗的文章和视频我看了一些,这个人的优点:
1.思维有张力,不仅有着大开大合般的分析思路,而且在技术细节上精益求精,这种能力十分适合做数据分析师。
2.有着一颗让业务员自己设计"模型"的心,曾在视频中谈到工程师最大的问题就是不断地对业务需求,而业务需求又五花八门,难以统一。DAX语言的出现给解决这一痛点带来了希望,使得业务人员自己设计"模型"成为了可能。譬如本案例,如果DAX功力够深,可以设计RFM模型让业务员自己去设置模型里的阈值,去返回不同模型的客户分层结果。那么,业务员的个性化需求就得到了满足。
PowerBI实在太难了,随着它逐渐走进人们的视野,DAX的各种黑箱、玄乎操作,将在数据可视化领域成为人们口中津津乐道的“炼丹术”。