实战篇(二)电商用户画像
一、问题分析
现有一份订单(部分)运营数据,需要根据这个数据对用户分组。
RFM介绍
我们知道,用户的消费行为本身是不能直接用于数据分析的,这就需要我们把用户的行为转化成具体的数值,也就是可量化的指标,让我们对用户有更直观的认识。而且我们还可以将这些指标用在数据分析、广告精准投放、产品推荐系统等多个运营场景,来提升我们产品和服务的精准度。
这个具体的数值呢,就是 RFM(Recency、Frequency、Monetary ),它是用户画像的衍生品,也是目前很多互联网厂商普遍采用的分析方式。具体来看:
R 是新进度,代表自用户上次消费以来的天数。这个指标其实也就是用户的热乎度,比如说你上次使用 App 是一个月前,你的系统里 R 值就是 30。如果你今天又用了一次 App,那 R 值就会立刻被更新为 1;
F 是消费频率,代表用户是否频繁使用服务。这也是用户黏性的风向标;
M 是消费金额,代表用户在一段时间内消费的总金额,这个不难理解。
通过用户消费行为的新进度、消费的总体频率,还有消费总金额这三项指标,我们可以将用户划分为不同的类别或集群,来直观地描述用户的价值。什么意思呢?简单来说就是,只要我们从用户的基本信息和消费行为数据中得出 RFM 值,就可以根据它对用户分组画像了。
问题分析
首先,我们要搞清楚,给用户做分组画像属于监督学习问题,还是无监督学习问题?我们要通过历史订单数据来给用户分组,这是没有任何已知标签可以做参照的,数据集中并没有一个字段指明用户的价值是“高”还是“低”,所以这显然是一个无监督学习问题。
todo
求出 RFM 值,利用 RFM 值,给用户分组画像,进而绘制出高价值、中等价值和低价值用户的分布情况。
二、实现步骤
仍然遵循机器学习项目的步骤。
1、数据预处理
import pandas as pd # 导入Pandas数据处理工具包 df_sales = pd.read_csv('C:/mydemo/python/order_data.csv') # 读入数据 df_sales.head() #显示头几行数据2、数据可视化
import matplotlib.pyplot as plt #导入Matplotlib的pyplot模块 #构建月度的订单数的DataFrame df_sales['消费日期'] = pd.to_datetime(df_sales['消费日期']) #转化日期格式 df_orders_monthly = df_sales.set_index('消费日期')['订单号'].resample('M').nunique() #每个月的订单数量 #设定绘图的画布 ax = pd.DataFrame(df_orders_monthly.values).plot(grid=True,figsize=(12,6),legend=False) ax.set_xlabel('月份') # X轴label ax.set_ylabel('订单数') # Y轴Label ax.set_title('月度订单数') # 图题 #设定X轴月份显示格式 plt.xticks( range(len(df_orders_monthly.index)), [x.strftime('%m.%Y') for x in df_orders_monthly.index], rotation=45) plt.show() # 绘图3、数据清洗
在刚才的可视化过程中,我们已经完成了对消费日期的观察,并没有发现什么异常。所以,现在我们重点要处理的是用户码、单价和(订单中产品的)数量。
(1)去重
首先,我们用 Pandas 中的 drop_duplicates 方法把完全相同的重复数据行删除掉。
df_sales = df_sales.drop_duplicates() #删除重复的数据行(2)去掉NaN
还可以用 DataFrame 的 isna().sum() 函数,来统计 NaN 的个数。尝试后发现这个数据集中没有 NaN 值。
(3)脏数据
此外,对于数量、金额等类型的数据,我们还常常会使用 describe 方法来查看这些字段的统计信息是否有脏数据。
df_sales.describe() #df_sales的统计信息发现有负数,具体的处理方式是,用 loc 属性通过字段名(也就是列名)访问数据集,同时只保留“数量”字段大于 0 的数据行:
df_sales = df_sales.loc[df_sales['数量'] > 0] #清洗掉数量小于等于0的数据在 DataFrame 对象中,loc 属性是通过行、列的名称来访问数据的,我们做数据预处理时会经常用到;还有一个常被用到的属性是 iloc,它是通过行列的位置(也就是序号)来访问数据的。
4、特征
要求出 RFM 的值,实际上就是在完成机器学习项目中的“特征工程”环节,也就是对原始数据集中的信息进行选择、提取、合并、加工、转换,甚至是基于原始信息构建出新的、对于模型的训练更具有意义的特征。
R 值(最近一次消费的天数)和 F 值(消费频率),我们通过数据集中的消费日期就能得到;M 值(消费金额)需要计算:
df_sales['总价'] = df_sales['数量'] * df_sales['单价'] #计算每单的总价 df_sales.head() #显示头几行数据现在,在这个数据集中,用户码、总价和消费日期这三个字段,给我们带来了每一个用户的 R、F、M 信息。其中:
- 一个用户上一次购物的日期,也就是最新的消费日期,就可以转化成这个用户的 R 值;
- 一个用户下的所有订单次数之和,就是消费频率值,也就是该用户的 F 值;
- 把一个用户所有订单的总价加起来,就是消费金额值,也就是该用户的 M 值。
(1)构建用户层级表
生成一个以用户码为关键字段的 Dataframe 对象 df_user,然后在这个 Dataframe 对象中,逐步加入每一个用户的新近度(R)、消费频率(F)、消费金额(M),以及最终总的分组信息。
在代码实现上,我们用 Dataframe 的 unique() 这个 API,就能创建出以用户码为关键字段的用户层级表 df_user,然后我们再设定字段名,并根据用户码进行排序,最后显示出这个表
df_user = pd.DataFrame(df_sales['用户码'].unique()) #生成以用户码为主键的结构df_user df_user.columns = ['用户码'] #设定字段名 df_user = df_user.sort_values(by='用户码',ascending=True).reset_index(drop=True) #按用户码排序 df_user #显示df_user(2)求出 R 值
R 值代表自用户上次消费以来的天数,它与最近一次消费的日期相关。所以,用表中最新订单的日期(拉出来这张表的日期)减去上一次消费的日期,就可以确定对应用户的 R 值。R 值越大,说明该用户最近一次购物日距离当前日期越久,那么这样的用户就越是处于休眠状态。
df_sales['消费日期'] = pd.to_datetime(df_sales['消费日期']) #转化日期格式 df_recent_buy = df_sales.groupby('用户码').消费日期.max().reset_index() #构建消费日期信息 df_recent_buy.columns = ['用户码','最近日期'] #设定字段名 df_recent_buy['R值'] = (df_recent_buy['最近日期'].max() - df_recent_buy['最近日期']).dt.days #计算最新日期与上次消费日期的天数 df_user = pd.merge(df_user, df_recent_buy[['用户码','R值']], on='用户码') #把上次消费距最新日期的天数(R值)合并至df_user结构 df_user.head() #显示df_user头几行数据(3)求出 F 值
df_frequency = df_sales.groupby('用户码').消费日期.count().reset_index() #计算每个用户消费次数,构建df_frequency对象 df_frequency.columns = ['用户码','F值'] #设定字段名称 df_user = pd.merge(df_user, df_frequency, on='用户码') #把消费频率整合至df_user结构 df_user.head() #显示头几行数据这段代码的核心就是通过给消费日期做 count() 计数,来求出每一个用户的消费次数。
(4)求出 M 值
M 值很容易求出,它就是用户消费的总和:
df_revenue = df_sales.groupby('用户码').总价.sum().reset_index() #根据消费总额,构建df_revenue对象 df_revenue.columns = ['用户码','M值'] #设定字段名称 df_user = pd.merge(df_user, df_revenue, on='用户码') #把消费金额整合至df_user结构 df_user.head() #显示头几行数据5、插曲——为什么要使用机器学习来分组
我们希望看看 R 值、F 值和 M 值的分布情况,以便为用户分组作出指导。
df_user['R值'].plot(kind='hist', bins=20, title = '新进度分布直方图') #R值直方图 df_user.query('F值 < 800')['F值'].plot(kind='hist', bins=50, title = '消费频率分布直方图') #F值直方图 df_user.query('M值 < 20000')['M值'].plot(kind='hist', bins=50, title = '消费金额分布直方图') #M值直方图可以看到,我们求出的 R 值、F 值和 M 值的覆盖区间都很大。就拿 R 值来说,有的用户 7 天前购物,R 值为 7;有的用户 70 天前购物,R 值为 70;还有的用户 187 天前购物,R 值为 187。
如果说我们的目标是根据 R 值把用户分为几个不同的价值组,那么怎么分组比较合适呢?首先就有两个问题:
(1)分成多少个组比较好?
(2)从哪个值到哪个值归为第一组(比如 0-30 天是一组),从哪个值到哪个值归为第二组(比如 30 天 -70 天是一组)?
可以凭借经验来人为确定。比如说,把用户分为高、中、低三个组,比如 R 值为 0 到 50 的分为一个组,50 到 150 的分为一组,150 天以上的归为一组。这样的人为分组似乎也可以,但它存在一些弊端:首先,分组的准确性完全取决于人的经验,如果分得不准,效果就不好。其次,人为分组是静态的,如果用户情况变化了,我们还是用同样的区间来分组,就不是很合适。
其实该怎么分组,我们说了不算,要数据说了算。所以,要解决这个问题,还是要通过机器学习算法,根据数据的实际情况来动态地确定分组。因为只有这样的模型才是动态的,才能长期投入使用。
6、选择算法
在无监督学习中,聚类和降维是两种最常见的算法,很显然,我们的问题适合用聚类算法来解决。这里我直接选用 K-Means(K- 均值)算法了,并使用手肘法确定K值。
这里我要特别说明一下,尽管我们前面说要把用户分为高、中、低三个价值组,但是 R、F、M 的值却可以分成很多组,并不一定都是 3 组。
下面我们就用代码找出 R 值的手肘点。请你注意,这里我会先定义一个找手肘点的函数,因为后面在对 R 值、F 值和 M 值聚类的过程中,我们都要用到这个函数。
from sklearn.cluster import KMeans #导入KMeans模块 def show_elbow(df): #定义手肘函数 distance_list = [] #聚质心的距离(损失) K = range(1,9) #K值范围 for k in K: kmeans = KMeans(n_clusters=k, max_iter=100) #创建KMeans模型 kmeans = kmeans.fit(df) #拟合模型 distance_list.append(kmeans.inertia_) #创建每个K值的损失 plt.plot(K, distance_list, 'bx-') #绘图 plt.xlabel('k') #X轴 plt.ylabel('距离均方误差') #Y轴 plt.title('k值手肘图') #标题在这段代码中,核心部分是拟合 kmeans 模型之后,通过 kmeans.inertia_ 计算损失值。损失会随着 K 值的增大而逐渐减小,而那个拐点就是手肘。然后我们调用下面这个函数,显示 R 值、F 值和 M 值聚类的 K 值手肘图:
show_elbow(df_user[['R值']]) #显示R值聚类K值手肘图 show_elbow(df_user[['F值']]) #显示F值聚类K值手肘图 show_elbow(df_user[['M值']]) #显示M值聚类K值手肘图可以看到,R、F、M 值的拐点大概都在 2 到 4 之间附近,这就意味着我们把用户分成 2、3、4 个组都行。这里我选择 3 作为 R 值的簇的个数,选择 4 作为 F 值的簇的个数,选择 3 作为 M 值的簇的个数。
现在我们已经选定好了算法,并确定了 R、F、M 每个特征下簇的个数,也就是 K 值。接下来我们就可以开始创建聚类模型了。
7、创建和训练模型
(1)创建模型
创建模型是把 n_clusters 参数,即K传入函数,这样,我们就在程序中创建了一个 K-Means 聚类模型。
from sklearn.cluster import KMeans #导入KMeans模块 kmeans_R = KMeans(n_clusters=3) #设定K=3 kmeans_F = KMeans(n_clusters=4) #设定K=4 kmeans_M = KMeans(n_clusters=4) #设定K=4(2)训练模型
我们不是第一次见到 fit 这个方法了,fit,翻译成中文就叫做拟合模型。基本上所有的机器学习模型都是用 fit 语句来进行模型训练的。
kmeans_R.fit(df_user[['R值']]) #拟合模型 kmeans_F.fit(df_user[['F值']]) #拟合模型 kmeans_M.fit(df_user[['M值']]) #拟合模型8、使用模型进行聚类,并给用户分组
模型训练好了,现在我们就用它给 R、F、M 值聚类。
(1)给 R、F、M 值聚类
我们先用 kmeans 模型中的 predict 方法给 R 值聚类。“predict”翻译成中文是“预测”,不过作为无监督学习方法,它其实就是使用模型进行聚类,而且,也不需要进一步的评估过程。这也是监督学习和无监督学习不一样的地方。
df_user['R值层级'] = kmeans_R.predict(df_user[['R值']]) #通过聚类模型求出R值的层级 df_user.head() #显示头几行数据下面我们用 groupby 语句来看看 0、1、2 这几个簇的用户基本统计数据:
df_user.groupby('R值层级')['R值'].describe() #R值层级分组统计信息如果你注意看 0、1 和 2 这三个簇,也就是三个组,就会发现形成的簇没有顺序。你看,0 群的用户最多 670 个人,均值显示他们平均购物间隔是 31 天,上次购物距今是 0 天到 94 天,这是相对频繁的购物用户群。1 群的用户平均购物间隔为 295 天,上次购物距现在是 231 天到 372 天,这是在休眠中的用户;而 2 群的用户平均购货间隔则变成了 157 天,介于两者之间,他们上次购物距今是从 95 天到 225 天。你会发现这个从0到2的顺序既不是升序,也不是降序。
这其实是聚类这种算法本身的问题。聚类,作为一种无监督学习算法,是不知道顺序的重要性的,它只是盲目地把用户分群(按照其空间距离的临近性),而不管每个群的具体意义,因此也就没有排序的功能。这也就是我前面说的“聚类后概念化”的具体意思。聚类并不知道那组人的价值高低,所以也就无法确定顺序,需要我们人为来排序。
(2)为聚类的层级做排序
那么,下面我们就用一段代码,把聚类的结果做一个排序,让 0、1、2 这三个组体现出价值的高低。
#定义一个order_cluster函数为聚类排序 def order_cluster(cluster_name, target_name,df,ascending=False): df_new = df.groupby(cluster_name)[target_name].mean().reset_index() #按聚类结果分组,创建df_new对象 df_new = df_new.sort_values(by=target_name,ascending=ascending).reset_index(drop=True) #排序 df_new['index'] = df_new.index #创建索引字段 df_new = pd.merge(df,df_new[[cluster_name,'index']], on=cluster_name) #基于聚类名称把df_new还原为df对象,并添加索引字段 df_new = df_new.drop([cluster_name],axis=1) #删除聚类名称 df_new = df_new.rename(columns={"index":cluster_name}) #将索引字段重命名为聚类名称字段 return df_new #返回排序后的df_new对象在上述代码中,为聚类做排序的是 order_cluster 函数。那么接下来,我们再调用这个 order_cluster 函数,把用户表重新排序。我们知道,消费天数间隔的均值越小,用户的价值就越高,所以我们在这里采用降序,也就是把 ascending 参数设为 False:
df_user = order_cluster('R值层级', 'R值', df_user, False) #调用簇排序函数 df_user = df_user.sort_values(by='用户码',ascending=True).reset_index(drop=True) #根据用户码排序 df_user.head() #显示头几行数据此时,各用户的层级值就发生了变化,比如用户 14688 的簇编号从 1 变成了 2,因为这个用户 7 天前曾经购物,其 R 值相对偏低,放在高分的 2 层级是合适的。其实,上面的代码中,我们并没有改变用户的分组,而只是改变了每一个簇的编号,这样层级关系就能体现出来了。
下面我们重新显示各个层级的信息:
df_user.groupby('R值层级')['R值'].describe() #R值层级分组统计信息你会看到,此时各个簇已经形成了次序。0 层级的用户,平均新近度是 298 天,1 层级的用户,平均新近度是 157 天,而 R 值最高的用户组(2 层级),平均新近度仅有 32 天。这说明用户上一次消费距今的天数越少,其 R 值的价值越高。
R 值聚类做好后,我们按照同样的方法可以根据用户购买频率给 F 值做聚类,并用刚才定义的 order_cluster 函数为聚类之后的簇进行排序,确定层级。因为消费次数越多,价值越高,所以我们把 order_cluster 函数的 ascending 参数设定为 True,也就是升序:
df_user['F值层级'] = kmeans_F.predict(df_user[['F值']]) #通过聚类模型求出F值的层级 df_user = order_cluster('F值层级', 'F值',df_user,True) #调用簇排序函数 df_user.groupby('F值层级')['F值'].describe() #F值层级分组统计信息df_user = df_user.sort_values(by='用户码',ascending=True).reset_index(drop=True) #根据用户码排序 df_user.head()最后,我们依葫芦画瓢,给 M 值做聚类,并且对聚类的结果做排序,分出层级。因为代码和 R 值、F 值聚类十分相似,我就直接给出所有代码,不再说明了。
df_user['M值层级'] = kmeans_M.predict(df_user[['M值']]) #通过聚类模型求出M值的层级 df_user = order_cluster('M值层级', 'M值',df_user,True) #调用簇排序函数 df_user.groupby('M值层级')['M值'].describe() #M值层级分组统计信息 df_user = df_user.sort_values(by='用户码',ascending=True).reset_index(drop=True) #根据用户码排序 df_user.head() #显示头几行数据(3)为用户整体分组画像
我们这里采用简单叠加的方法把 R、F、M 三个层级的值相加,用相加后得到的值,作为总体价值,来给用户进行最终的分层。当然了,如果你对其中某一个指标看得比较重,也可以加权之后再相加。
df_user['总分'] = df_user['R值层级'] + df_user['F值层级'] + df_user['M值层级'] #求出每个用户RFM总分因为 R 值有 3 个层级(0,1,2),F 值有 4 个层级(0,1,2,3),M 值有 4 个层级(0,1,2,3),我们把三个维度的值相加,那每一个用户的得分有可能是 0 到 8 当中的某一个值,也就是说出现了 9 个层次。我这里就按照下面的规则,来确定用户最终的价值分层。当然了,你也可以尝试用其它的阈值来确定你的价值分层。0-2 分,低价值用户3-4 分,中价值用户5-8 分,高价值用户。
举例来说,就是如果一个用户在 R 值拿到了 2 分,在新近度这个维度为高价值用户,但是在消费频率和消费金额这两个维度都只拿到 0 分,那么最后得分就为 2,总体只能评为低价值用户。
#在df_user对象中添加总体价值这个字段 df_user.loc[(df_user['总分']<=2) & (df_user['总分']>=0), '总体价值'] = '低价值' df_user.loc[(df_user['总分']<=4) & (df_user['总分']>=3), '总体价值'] = '中价值' df_user.loc[(df_user['总分']<=8) & (df_user['总分']>=5), '总体价值'] = '高价值' df_user #显示df_user(4)散点图
现在,有了用户的价值分组标签,我们就可以做很多进一步的分析,比如说选取 R、F、M 中任意两个维度,并把高、中、低价值用户的散点图进行呈现:
#显示高、中、低价值组分布散点图(F值与M值) plt.scatter(df_user.query("总体价值 == '高价值'")['F值'], df_user.query("总体价值 == '高价值'")['M值'],c='g',marker='*') plt.scatter(df_user.query("总体价值 == '中价值'")['F值'], df_user.query("总体价值 == '中价值'")['M值'],marker=8) plt.scatter(df_user.query("总体价值 == '低价值'")['F值'], df_user.query("总体价值 == '低价值'")['M值'],c='r') 各价值组的用户分布散点图如下图所示:借此,我们可以发现,高价值用户(绿色五星)覆盖在消费频率较高的区域,和 F 值相关度高。而在总消费金额大于 5000 元的用户中,中高价值的用户(绿色五星和红色圆点)都有。
代码附录
#1、数据导入和清洗 import pandas as pd # 导入Pandas数据处理工具包 df_sales = pd.read_csv('C:/mydemo/python/order_data.csv') # 读入数据 df_sales = df_sales.drop_duplicates() #删除重复的数据行 df_sales = df_sales.loc[df_sales['数量'] > 0] #清洗掉数量小于等于0的数据 df_sales['总价'] = df_sales['数量'] * df_sales['单价'] #计算每单的总价 #2、构建用户表 df_user = pd.DataFrame(df_sales['用户码'].unique()) #生成以用户码为主键的结构df_user df_user.columns = ['用户码'] #设定字段名 df_user = df_user.sort_values(by='用户码',ascending=True).reset_index(drop=True) #按用户码排序 #3、用户r f m #3.1 r df_sales['消费日期'] = pd.to_datetime(df_sales['消费日期']) #转化日期格式 df_recent_buy = df_sales.groupby('用户码').消费日期.max().reset_index() #构建消费日期信息 df_recent_buy.columns = ['用户码','最近日期'] #设定字段名 df_recent_buy['R值'] = (df_recent_buy['最近日期'].max() - df_recent_buy['最近日期']).dt.days #计算最新日期与上次消费日期的天数 df_user = pd.merge(df_user, df_recent_buy[['用户码','R值']], on='用户码') #把上次消费距最新日期的天数(R值)合并至df_user结构 df_user.head() #显示df_user头几行数据 #3.2 f df_frequency = df_sales.groupby('用户码').消费日期.count().reset_index() #计算每个用户消费次数,构建df_frequency对象 df_frequency.columns = ['用户码','F值'] #设定字段名称 df_user = pd.merge(df_user, df_frequency, on='用户码') #把消费频率整合至df_user结构 df_user.head() #显示头几行数据 #3.3 m df_revenue = df_sales.groupby('用户码').总价.sum().reset_index() #根据消费总额,构建df_revenue对象 df_revenue.columns = ['用户码','M值'] #设定字段名称 df_user = pd.merge(df_user, df_revenue, on='用户码') #把消费金额整合至df_user结构 df_user.head() #显示头几行数据 #4、建模(省略k的求值分析过程) from sklearn.cluster import KMeans #导入KMeans模块 kmeans_R = KMeans(n_clusters=3) #设定K=3 kmeans_F = KMeans(n_clusters=4) #设定K=4 kmeans_M = KMeans(n_clusters=4) #设定K=4 #5、训练模型 kmeans_R.fit(df_user[['R值']]) #拟合模型 kmeans_F.fit(df_user[['F值']]) #拟合模型 kmeans_M.fit(df_user[['M值']]) #拟合模型 #6、聚类 #定义一个order_cluster函数为聚类排序 def order_cluster(cluster_name, target_name,df,ascending=False): df_new = df.groupby(cluster_name)[target_name].mean().reset_index() #按聚类结果分组,创建df_new对象 df_new = df_new.sort_values(by=target_name,ascending=ascending).reset_index(drop=True) #排序 df_new['index'] = df_new.index #创建索引字段 df_new = pd.merge(df,df_new[[cluster_name,'index']], on=cluster_name) #基于聚类名称把df_new还原为df对象,并添加索引字段 df_new = df_new.drop([cluster_name],axis=1) #删除聚类名称 df_new = df_new.rename(columns={"index":cluster_name}) #将索引字段重命名为聚类名称字段 return df_new #返回排序后的df_new对象 df_user['R值层级'] = kmeans_R.predict(df_user[['R值']]) #通过聚类模型求出R值的层级 df_user = order_cluster('R值层级', 'R值', df_user, False) #调用簇排序函数 df_user = df_user.sort_values(by='用户码',ascending=True).reset_index(drop=True) #根据用户码排序 df_user.groupby('R值层级')['R值'].describe() #R值层级分组统计信息 df_user['F值层级'] = kmeans_F.predict(df_user[['F值']]) #通过聚类模型求出F值的层级 df_user = order_cluster('F值层级', 'F值',df_user,True) #调用簇排序函数 df_user.groupby('F值层级')['F值'].describe() #F值层级分组统计信息 df_user = df_user.sort_values(by='用户码',ascending=True).reset_index(drop=True) #根据用户码排序 df_user['M值层级'] = kmeans_M.predict(df_user[['M值']]) #通过聚类模型求出M值的层级 df_user = order_cluster('M值层级', 'M值',df_user,True) #调用簇排序函数 df_user = df_user.sort_values(by='用户码',ascending=True).reset_index(drop=True) #根据用户码排序 #7、分组 df_user['总分'] = df_user['R值层级'] + df_user['F值层级'] + df_user['M值层级'] #求出每个用户RFM总分 #在df_user对象中添加总体价值这个字段 df_user.loc[(df_user['总分']<=2) & (df_user['总分']>=0), '总体价值'] = '低价值' df_user.loc[(df_user['总分']<=4) & (df_user['总分']>=3), '总体价值'] = '中价值' df_user.loc[(df_user['总分']<=8) & (df_user['总分']>=5), '总体价值'] = '高价值' df_user #显示df_user