基于RFM的精细化用户管理
一、案例背景
用户价值细分是了解用户价值度的重要途径,而销售型公司中对于订单交易尤为关注,因此基于订单交易的价值度模型将更适合运营需求。针对交易数据分析的常用模型是RFM模型,该模型不仅简单、容易理解,且业务落地能力非常强。因此,本节将基于该模型做数据分析和应用。在RFM的结果中,业务部门希望不仅能对用户做分组,还希望能将每个组的用户特征概括和总结出来,这样便于后续精细化运营不同的客户群体,且根据不同群体做定制化或差异性的营销和关怀。
基于业务部门的用户分群需求,我们计划将RFM的3个维度分别作3个区间的离散化,这样出来的用户群体最大有333=27个。如果划分区间过多则不利于用户群体的拆分,区间过少则可能导致每个特征上的用户区分不显著。
从交付结果看,给业务部门做运营的分析结果都要导出成Excel文件,用于做后续分析和二次加工使用。另外,RFM的结果还会供其他模型的建模使用,RFM本身的结果可以作为新的局部性特征,因此数据的输出需要有本地文件和写数据库两种方式。
本节案例选择了4年的订单数据,这样可以从不同的年份对比不同时间下各个分组的绝对值变化情况,方便了解会员的波动。
二、案例主要应用技术
本案例没有使用现有成熟模型包,而是通过python代码手动实现RFM模型,主要用到的库包括time,numpy和pandas。在实现RFM组合时,我们使用了sklearn的随机森林来使用R F M的权重,在结果展示时使用了pyecharts的3D柱形图。
三、案例数据
案例数据是某企业从2015年到2018年共4年的用户订单抽样数据,数据来源于销售系统。数据在Excel中包含5个sheet,前4个sheet以年份为单位存储为单个sheet中,最后一张会员等级表为用户的等级表。
前4张表的数据概要如下:
- 特征变量数:4
- 数据记录数:30774、41278、50839、81349
- 是否有NA值:有
- 是否有异常值:有
具体数据特征如下:
- 会员ID:每个会员的ID唯一,由纯数字组成。
- 提交日期:订单日提交日期。
- 订单号:订单ID,每个订单的ID唯一,由纯数字组成。
- 订单金额:订单金额,浮点型数据。
会员等级表中是所有会员的会员ID对应会员等级的情况,包括以下两个字段。
- 会员ID:该ID可与前面的订单表中的会员ID关联。
- 会员等级:会员等级以数字区分,数字越大,级别越高。
四、案例过程
(1) 导入相关库
# 导入相关库 import numpy as np import pandas as pd import time from sklearn.ensemble import RandomForestClassifier # from pyecharts import Bar3D 老版本的导入方式,已经不适用新版本了 from pyecharts.charts import Bar
(2)读取数据
先定义一个列表sheet_names,目的是方便后续制定sheet名称;然后再通过列表推导式配合pandas的read_excel批量读取sales.xlsx中所有的数据到列表中,形成sheet_datas。
# 创建sheet_names列表,与表格数据对应,方便后续处理 sheet_names = ['2015','2016','2017','2018','会员等级'] #利用pandas的read_excel()函数,根据每一列的sheet_names,从第一行到最后一行依次读取每一行的数据作为sheet_datas sheet_datas = [pd.read_excel('F:/BaiduNetdiskDownload/数据源_8月数据化运营/sales.xlsx',sheet_name=i) for i in sheet_names]
(3)数据审查
- 前N条数据主要查看不同数据列的数据格式,尤其是有特定转换操作之后是否符合源数据文件的格式或得到目标转换要求,以及数据的长度、组成规律、类型等是否与真实数据一致。
- 数据描述性信息主要分析数据分布规律,包括记录数、极值、标准差、分位数结果等,可用于数据集的使用模型、极值的处理等后续计算的辅助判断依据。
- 缺失值信息帮助我们判断数量以及后续应对策略。
- 数据类型用于判断目标类型当前状态,以及后续是否需要做特殊处理。
#zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。 for each_name,each_data in zip(sheet_names,sheet_datas): print('[data summary for {0:=^50}]'.format(each_name)) # 显示各年份的分割线 print('Overview:','\n',each_data.head(4))# 展示数据前4条 print('DESC:','\n',each_data.describe())# 数据描述性信息 print('NA records',each_data.isnull().any(axis=1).sum()) # 缺失值记录数 print('Dtypes',each_data.dtypes) # 数据类型
[data summary for =======================2015=======================] Overview: 会员ID 订单号 提交日期 订单金额 0 15278002468 3000304681 2015-01-01 499.0 1 39236378972 3000305791 2015-01-01 2588.0 2 38722039578 3000641787 2015-01-01 498.0 3 11049640063 3000798913 2015-01-01 1572.0 DESC: 会员ID 订单号 订单金额 count 3.077400e+04 3.077400e+04 30774.000000 mean 2.918779e+10 4.020414e+09 960.991161 std 1.385333e+10 2.630510e+08 2068.107231 min 2.670000e+02 3.000305e+09 0.500000 25% 1.944122e+10 3.885510e+09 59.000000 50% 3.746545e+10 4.117491e+09 139.000000 75% 3.923593e+10 4.234882e+09 899.000000 max 3.954613e+10 4.282025e+09 111750.000000 NA records 0 Dtypes 会员ID int64 订单号 int64 提交日期 datetime64[ns] 订单金额 float64 dtype: object [data summary for =======================2016=======================] Overview: 会员ID 订单号 提交日期 订单金额 0 39288120141 4282025766 2016-01-01 76.0 1 39293812118 4282037929 2016-01-01 7599.0 2 27596340905 4282038740 2016-01-01 802.0 3 15111475509 4282043819 2016-01-01 65.0 DESC: 会员ID 订单号 订单金额 count 4.127800e+04 4.127800e+04 41277.000000 mean 2.908415e+10 4.313583e+09 957.106694 std 1.389468e+10 1.094572e+07 2478.560036 min 8.100000e+01 4.282026e+09 0.100000 25% 1.934990e+10 4.309457e+09 59.000000 50% 3.730339e+10 4.317545e+09 147.000000 75% 3.923182e+10 4.321132e+09 888.000000 max 3.954554e+10 4.324911e+09 174900.000000 NA records 1 Dtypes 会员ID int64 订单号 int64 提交日期 datetime64[ns] 订单金额 float64 dtype: object [data summary for =======================2017=======================] Overview: 会员ID 订单号 提交日期 订单金额 0 38765290840 4324911135 2017-01-01 1799.0 1 39305832102 4324911213 2017-01-01 369.0 2 34190994969 4324911251 2017-01-01 189.0 3 38986333210 4324911283 2017-01-01 169.0 DESC: 会员ID 订单号 订单金额 count 5.083900e+04 5.083900e+04 50839.000000 mean 2.882368e+10 4.332466e+09 963.587872 std 1.409416e+10 4.404350e+06 2178.727261 min 2.780000e+02 4.324911e+09 0.300000 25% 1.869274e+10 4.328415e+09 59.000000 50% 3.688044e+10 4.331989e+09 149.000000 75% 3.923020e+10 4.337515e+09 898.000000 max 3.954554e+10 4.338764e+09 123609.000000 NA records 0 Dtypes 会员ID int64 订单号 int64 提交日期 datetime64[ns] 订单金额 float64 dtype: object [data summary for =======================2018=======================] Overview: 会员ID 订单号 提交日期 订单金额 0 39229691808 4338764262 2018-01-01 3646.0 1 39293668916 4338764363 2018-01-01 3999.0 2 35059646224 4338764376 2018-01-01 10.1 3 1084397 4338770013 2018-01-01 828.0 DESC: 会员ID 订单号 订单金额 count 8.134900e+04 8.134900e+04 81348.000000 mean 2.902317e+10 4.348372e+09 966.582792 std 1.404116e+10 4.183774e+06 2204.969534 min 2.780000e+02 4.338764e+09 0.000000 25% 1.902755e+10 4.345654e+09 60.000000 50% 3.740121e+10 4.349448e+09 149.000000 75% 3.923380e+10 4.351639e+09 899.000000 max 3.954614e+10 4.354235e+09 174900.000000 NA records 1 Dtypes 会员ID int64 订单号 int64 提交日期 datetime64[ns] 订单金额 float64 dtype: object [data summary for =======================会员等级=======================] Overview: 会员ID 会员等级 0 100090 3 1 10012905801 1 2 10012935109 1 3 10013498043 1 DESC: 会员ID 会员等级 count 1.543850e+05 154385.000000 mean 2.980055e+10 2.259701 std 1.365654e+10 1.346408 min 8.100000e+01 1.000000 25% 2.213894e+10 1.000000 50% 3.833022e+10 2.000000 75% 3.927932e+10 3.000000 max 3.954614e+10 5.000000 NA records 0 Dtypes 会员ID int64 会员等级 int64 dtype: object
使用for循环,配合zip函数来读取sheet_names和sheet_datas中的每个值,然后分别输出以下内容:
数据展示:使用数据框的head方法,显示前4条数据
描述性信息:使用数据框的describe方法,显示所有的描述性统计结果
缺失值记录:使用数据框的isnull().any(axis=1)来判断含有缺失值的记录,然后用sum获取总记录数
数据类型:使用数据框的dtypes方法获取所有字段数据类型信息。
通过上述结果我们可以得到如下结论:
- 每个sheet中的数据都能正常读取和识别,无任何错误。
- 日期列(提交日期)已经被自动识别为日期格式,这省去了后期做转换的过程。
- 订单金额的分布是不均匀的,里面有明显的极大值,例如2016年的数据中,最大值为174900,最小值仅为0.1。这样的分布状态,数据会受到极值影响。
- 订单中的最小值竟然包括0、0.1这样的金额,显然不是正常订单。经过与业务方沟通后确认,最大值的订单金额有效,通常是客户一次性购买多个大家电商品;而订单金额为0.1这类是使用优惠券支付的订单,并没有实际意义。除此之外,所有低于1元的订单均有这个问题,因此需要在后续处理中去掉。
- 有的表中存在缺失值记录,但数量不多,因此选择丢弃或填充都可以。
(4)数据预处理。去除异常值和缺失值
#去除异常值与缺失值 for ind,each_data in enumerate(sheet_datas[:-1]):#enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标 sheet_datas[ind] = each_data.dropna()# 丢弃缺失值记录 sheet_datas[ind] = each_data[each_data['订单金额'] > 1]# 丢弃订单金额<=1的记录 sheet_datas[ind]['max_year_date'] = each_data['提交日期'].max() # 增加一列最大日期值
通过for循环配合enumerate方法,获得每个可迭代元素的索引和具体值。由于处理缺失值和异常值只针对订单数据,因此sheet_datas通过索引实现不包含最后一个对象(即会员等级表)。
- 直接将each_data使用dropna丢弃缺失值后的数据框替代原来sheet_datas中的数据框。
- 使用each_data[each_data['订单金额']>1]来过滤出包含订单金额>1的记录数,然后替换原来sheet_datas中的数据框。
- 最后一行代码的目的是在每个年份的数据中新增一列max_year_date,通过each_data['提交日期'].max()获取一年中日期的最大值,这样方便后续针对每年的数据分别做RFM计算,而不是针对4年的数据统一做RFM计算。
(5)汇总数据
# 汇总所有数据 data_merge = pd.concat(sheet_datas[:-1],axis=0)
data_merge['date_interval'] = data_merge['max_year_date'] - data_merge['提交日期']#计算各自年份的最大日期与每个行的日期的差,得到日期间隔
data_merge['year'] = data_merge['提交日期'].dt.year#增加一段新的字段,为每个记录行发生的年份
#将日期间隔转化成文字
data_merge['date_interval'] = data_merge['date_interval'].apply(lambda x: x.days)
#按会员ID做汇总
rfm_gb = data_merge.groupby(['year','会员ID'], as_index=False).agg({'date_interval':'min', #计算最近一次的订单时间
'提交日期':'count', #计算订单的频率
'订单金额':'sum'}) #计算订单的总金额
该代码实现的是基于年份和会员ID,分别做RFM原始值的聚合计算。
这里的分类汇总使用的是groupby方法,以year和会员ID为联合主键,设置as_index=False意味着year和会员ID不作为index列,而是普通的数据框结果列。后面的agg方法实际上是一个“批量”聚合功能的函数,它实现了对data_interval、提交日期、订单金额三列分别以min、count、sum做聚合计算的功能。否则,我们需要分别写3条groupby来实现3个聚合计算。
#重命名列名 rfm_gb.columns = ['year','会员ID','r','f','m'] rfm_gb.head()#预览并打印

(6)确定RFM划分区间
在做RFM划分时,基本逻辑是分别对R、F、M做分箱或者离散化操作,然后才能得到得分离散化后的结果。而离散化本身有多种方法可选,由于我们要对数据做RFM离散化,因此需要先看下数据的基本分布状态。
#查看数据分布 desc_pd = rfm_gb.iloc[:,2:].describe().T#由于只针对rfm三列,因此使用iloc方法,选择从第3列(索引值为2)开始的字段,调用describe方法 print(desc_pd)

从基本概要可以看出,汇总后的数据总共有14万条,r和m的数据分布相对较为离散,表现在min、25%、50%、75%和max的数据没有特别集中;而从f(购买频率)可以看出,大部分用户的分布都趋近于1,表现是从min到75%的分段值都是1,且mean(均值)为1.365。
离散化的方法比较多,我们应根据自身应用特点和场景进行选择。这里我们将选择25%和75%作为区间划分的2个边界值。
有一个问题在于,r和m本身能较好的区分用户特征,但f则无法区分(大量的用户只有1个订单)。针对该问题我们与业务部门进行了沟通,结论是由于行业属性(大家电)的原因,用户发生复购确实很少,1年购买1次是比较普遍(其中包含新客户以及老客户在当年的第1次购买),因此划分时可以使用2和5来作为边界:选择2是因为一般的业务部门认为当年购买2次及2次以上就可以被定义为复购用户(而非累计订单的数量计算复购用户)。选择5是因为业务部门认为普通用户购买5次已经是非常高的次数,超过该次数就属于非常高价值的用户群体。可以说,这个值是基于业务经验和日常数据报表而获得的。
# 定义区间边界 r_bins = [-1,79,255,365] # 注意起始边界小于最小值 f_bins = [0,2,5,130] m_bins = [0,69,1199,206252]
(7)计算RFM因子权重
其实在每个公司中,涉及到会员数据时,一般都会有会员体系,而会员体系中有一个维度,是衡量会员价值度高低的,这个维度是——会员等级。在指定会员等级时,各个公司都已经综合考虑到了多种与公司整体利益相关的因素,设置各种会员权益都与会员等级有关。例如,免运费门槛、优惠券使用、特殊商品优惠价格、会员活动和营销等。因此我们可以基于会员等级来确定RFM三者的权重,基本思路是,建立一个rfm三个维度与会员等级的分类模型,然后通过模型输出维度的权重。
# 匹配会员等级和rfm得分 rfm_merge = pd.merge(rfm_gb,sheet_datas[-1],on='会员ID',how='inner')#使用merge方法合并两个数据框,关联主键是会员ID,匹配方式是内部匹配
rfm_merge.head()

# rf获得rfm因子得分 clf = RandomForestClassifier()#调用随机森林分类起模型 clf = clf.fit(rfm_merge[['r','f','m']],rfm_merge['会员等级'])#使用随机森林分类器模型在我们的数据上进行拟合 weights = clf.feature_importances_ #返回'r','f','m'三部分权重 print('feature importance:',weights)
feature importance: [0.41323361 0.00608028 0.5806861 ]
上述过程非常简单,先建立rf模型对象,然后将rfm三列作为特征,将会员等级作为目标输入模型中进行训练,最后通过模型的feature_importances_获得权重信息。
由上述结果可知,在这3个维度中,用户的等级首先侧重于会员的价值贡献度(实际订单的贡献),其次是新近程度,最后是频次。这种逻辑与很多公司的整体会员等级一致。例如某国内电商的会员等级,是基于历史累计订单金额进行升级,如果最近一段时间(例如1年内)没有购物,则会降低会员等级。
(8) RFM计算过程
# RFM分箱得分 rfm_gb['r_score'] = pd.cut(rfm_gb['r'], r_bins, labels=[i for i in range(len(r_bins)-1,0,-1)])# 计算R得分 rfm_gb['f_score'] = pd.cut(rfm_gb['f'], f_bins, labels=[i+1 for i in range(len(f_bins)-1)])# 计算F得分 rfm_gb['m_score'] = pd.cut(rfm_gb['m'], m_bins, labels=[i+1 for i in range(len(m_bins)-1)])# 计算M得分
每个rfm的过程使用了pd.cut方法,基于自定义的边界区间进行划分,labels用来显示每个离散化后的具体值。F和M的规则是值越大,等级越高;而R的规则是值越小,等级越高。因此关于R的labels的规则与F和M相反。在labels指定时需要注意,4个区间的结果是划分为3份,因此labels的数量上通过减1实现边界数量与区间数量的平衡,而i+1则实现了区间从1开始,而不是0。
备注:pd.cut方法
计算总得分:
方法1 加权得分
#方法一:加权得分 rfm_gb = rfm_gb.apply(np.int32) # cate转数值 rfm_gb['rfm_score'] = rfm_gb['r_score'] * weights[0] + rfm_gb['f_score'] * weights[1] + rfm_gb['m_score'] * weights[2]
方法2 RFM组合
方法二:RFM组合 rfm_gb['r_score'] = rfm_gb['r_score'].astype(np.str)#3列使用astype方法将数值型转换为字符串类型,然后使用pandas的字符串处理库中str的cat方法做字符串合并 rfm_gb['f_score'] = rfm_gb['f_score'].astype(np.str) rfm_gb['m_score'] = rfm_gb['m_score'].astype(np.str) rfm_gb['rfm_group'] = rfm_gb['r_score'].str.cat(rfm_gb['f_score']).str.cat(rfm_gb['m_score'])
这种方式是传统的做会员分组的方式。目标是将3列作为字符串组合为新的分组。
代码中,现针对3列使用astype方法将数值型转换为字符串类型,然后使用pandas的字符串处理库中str的cat方法做字符串合并,该方法可以将右侧的数据合并到左侧,再连续使用两个str.cat方法得到总的R、F、M字符串组合。
这里str库中cat方法,用于字符串对象合并,语法如下:
参数介绍:
others:要合并的另外一个对象(右侧对象),如果为空,则将左侧对象进行组合。
sep:合并的分隔符,默认为空,可自定义,例如“,”、“;”等。
na_rep:如果遇到NA(缺失值)时如何处理,默认为忽略。
需要注意的是:该方法用于对series做组合,而不能是数据框,适用于一维数据或者字符串。
举例:将左侧对象进行组合
import pandas as pd pd.Series(['a', 'b', 'c']).str.cat(['A', 'B', 'C'], sep = ';' )

(9)将RFM结果导出excel
#保存RFM结果到excel rfm_gb.to_excel('sales_rfm_score1.xlsx')# 保存数据为Excel
浙公网安备 33010602011771号