数据分析之——用户消费行为分析
内容摘要
- 项目背景
- 项目目的
- 分析思路
- python数据分析实现
- PowerBI可视化看板设置
一、项目背景
- 这是一份用户消费行为的分析报告;
数据来源于网上,是用户在一家单车网站上的消费记录,其时间跨度为1997年1月至1998年4月。 - 数据格式:txt文档,具体信息见图
- 第一列——user_id:用户ID
第二列——order_dt:购买日期
第三列——order_prodects:购买产品数
第四列——order_amount:购买金额
二、项目目的
通过数据分析:
1、了解本网站的基本营业情况;
2、对用户行为进行分析,掌握用户的分布情况,便于之后的策略制定;
三、分析思路
1、基础数据整理清洗
2、针对月份进行用户消费趋势的分析——便于掌握公司的营收发展趋势
3、针对用户个体消费进行分析
4、针对用户消费行为进行分析
5、针对用户购买周期进行分析
6、针对用户复购回购进行分析
四、python数据分析实现
-
一、基础数据整理清洗
数据清洗的目的在于对数据进行审查和校验,其主要操作有:
(1)解析原始数据列名,理解其意义并做通俗化替换;
(2)查看数据源每一条信息的数据类型是否正确;
(3)查看数据有无缺失值情况;
(4)查看数据有无异常值情况;
(5)数据一致化处理(多用于针对时间数据)- 首先导入数据,解析原始数据信息,做通俗化替换。
#导入必要的包
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
#导入数据并设置列名
#解析原始数据列名,理解其意义并做通俗化替换
columns=['user_id','order_dt','order_products','order_amount']
df=pd.read_table(r':\bicycle_master.txt',sep='\s+',names=columns)
#查看数据
df.head(2)
- 查看数据源每一列信息的数据类型是否正确,尤其需要注意可能会计算的数据和时间格式。
#查数据源每一条信息的数据类型是否正确
df.info()
#发现其中order_dt 购买日期应该为时间格式类型,并非整型数据,需要进行数据处理
-数据源中时间数据的类型为整型,需要修改成datetime格式,并且为接下来时间序列分析做准备需要将年和月提取出来。
#pd.to_datetime函数进行转换
df['order_dt']=pd.to_datetime(df['order_dt'],format='%Y%m%d')
#同时考虑到后续对数据分分析,将其按照年、月将时间提取出来
#生成年、月
df['Year']=df['order_dt'].astype('datetime64[Y]')
df['Month']=df['order_dt'].astype('datetime64[M]')
#查看数据
df.head(2)
#查看数据描述
df.describe()
-
发现大部分订单只消费了少量商品,75%用户的产品购买量仅3个;
用户的消费金额比较稳定,平均消费35元,中位数在25元,存在一定极值的干扰。 -
基础数据整理结束
-
二、针对月份进行用户消费趋势的分析
如果以天为尺度进行分析,具有较大的波动性和随机性,并且数据量较大;以月为尺度进行分析,能够避免一定的随机性,并且还可以看出一定的分布趋势,故选择以月为尺度进行数据分析。
以月为尺度进行分析,较为重要的是消费金额,消费频率,购买产品量和消费人数
消费人数的变化可以看出该平台对用户粘性的变化程度;消费水平一方面与消费人数有关,另一方面可能与当月的产品及活动有关。- 每月的消费总金额
- 每月的消费次数
- 每月的产品购买量
- 每月的消费人数
#需要对Month进行一个groupby,并对购买金额和购买产品数量进行求和,对购买次数(用户id)进行计数
grouped=df.groupby(df['Month']).agg({"order_amount":'sum','user_id':'count','order_products':'sum'})
grouped.head()
#统计每月的消费人数,即统计用户id唯一值的个数
order_month_person=grouped_month.user_id.nunique()
order_m_p=order_month_person.to_frame()
order_m_p.head()
-
对用户人数统计可以看出,4月后出现明显的下跌趋势,将数据进行保存,用于后期powerbi绘图
-
三、针对用户个体消费进行分析
- 用户消费金额,消费个数描述性统计
- 用户消费金额和消费次数的散点图
- 用户消费金额的分布图
- 用户消费次数的分布图
- 用户累计消费金额占比
#新建一个按照用户分类的数据
user_info=df.groupby('user_id')
#用户消费金额,消费个数描述性统计
user_info.sum().describe()
(1)用户消费金额,消费个数描述性统计
- 用户平均购买为7,但是中位值只有3,说明购买量大的用户占少数
- 用户平均消费106元,中位值有43,判断同上,有极值干扰
(2)用户消费金额和产品个数的散点图
#用户消费金额和消费次数的散点图
user_info.sum().plot.scatter(x='order_amount',y='order_products')
#绘制消费金额和消费个数的散点图
user_cost_number=df.groupby('user_id').agg({'order_amount':'sum','order_products':'count'})
user_cost_number.plot.scatter(x='number',y='cost')
(3)用户消费金额的分布图
user_info.sum().order_amount.plot.hist(bins=20)
#从直方图可知,用户消费金额,绝大部分呈现集中趋势,小部分异常值干扰了判断,可以使用过滤操作排除异常
#使用切比雪夫定理过滤掉异常值,因为切比雪夫定理说明,95%的数据都分布在5个标准差之内,剩下5%的极值就不要了
#order_amount (mean = 106 ,std = 241) mean+5std = 1311
#过滤操作 pd.query(条件)
user_info.sum().query('order_amount<1311').order_amount.plot.hist(bins=20)
(4)用户消费次数的分布图
#过滤操作
#先筛选再取数
user_cost_number.query('number<50').number.plot.hist(bins=20)
(5)用户累计消费金额占比
# cumsum 是求累加值
user_cumsum=user_info.sum().sort_values('order_amount').apply(lambda x:x.cumsum()/x.sum())
# 这里 reset_index() 是为了得到一个自然数的行标签,表示的就是人数,下面的图就可以看出来多少个少占多少百分比
user_cumsum.reset_index().order_amount.plot()
- 四、针对用户消费行为进行分析
- 用户第一次消费(首购)
- 用户最后一次消费
- 新老客户消费比
- 多少用户仅消费一次
- 每月新客占比
- 用户分层
- RFM模型
- 新、老、活跃、回流、流失
(1)用户第一次消费及最后一次消费
grouped_user=df.groupby(df['user_id'])
#第一次消费=消费时间最小=min()
grouped_user.min().order_dt.value_counts().plot()
#最后一次消费=消费时间最大=max()
grouped_user.max().order_dt.value_counts().plot()
- 发现新用户仅在前三个月,随后便没有新用户的产生;最后一次消费呈现断崖式的下降
(2)新老客户消费比
- 多少用户仅消费一次
- 每月一次消费与多次消费比例
#仅消费一次说明第一次第一次消费时间=最后一次消费时间
user_life=grouped_user.order_dt.agg([max,min])
#选择第一次==最后一次的
#统计数量
(user_life['max']==user_life['min']).value_counts()
"""
结果如下
True 12054
False 11516
dtype: int64
"""
- 有一半的用户,只消费了一次
#每月新客占比
#df.groupby(['Month','user_id']) 会有多个日期,因此pivot_table不显示order_dt
grouped_um = df.groupby(['Month','user_id']).order_dt.agg(["min","max"]) #按月分组下的userid分组,求每月的最早购买日期和最晚消费日期
grouped_um["new"] = (grouped_um["min"] == grouped_um["max"] ) # 新增列 True 为一次消费的用户
#统计true和false的个数
grouped_um.groupby(['Month']).new.value_counts()
(3)用户分层
- RFM模型
- 新、老、活跃、回流、流失
# 用户分层
# 画 RFM,先对原始数据进行透视
#消费时间-最近;消费频率-求和;消费金额-求和
rfm=df.pivot_table(index='user_id',
values=['order_products','order_amount','order_dt'],
aggfunc={'order_dt':'max',
'order_amount':'sum',
'order_products':'sum'})
rfm.head()
# 得到最近一次消费,一般是计算 today 距离最近一次消费,这里因为时间太久远,就随便用的max值
# 数值越大就越久远,分子得到的是一些天数类似 545 days(因为是时间格式相减),处以一个单位,就不会有单位了只留下数值
#这里取负数是为了接下与mean比较是标定0、1而定的,因为越近越好,其时间越小
rfm['R']= -(rfm.order_dt - rfm.order_dt.max())/np.timedelta64(1,'D')
# 重命名,也就是 R:最后一次消费距今天数,F:消费总金额 ,M:消费总产品数
# R :消费时间 F:消费金额 M:消费频次
rfm.rename(columns={'order_products':"F",'order_amount':'M'},inplace=True)
#这里需要定义一个函数,针对RFM的表现进行8种类型的区分
#这个RFM的标准都选择为其平均值
def rfm_func(x):
level=x.apply(lambda x:'1' if x>=0 else '0')
# level 的类型是 series,index 是 R、F、M
#print(type(level))
#print(level.index)
label=level.R + level.F + level.M
d={
# R 为1 表示比均值大,离最早时间近,F为1 表示 消费金额比较多,M 为1 表示消费频次比较多,所以是重要价值客户
'111':'重要价值客户',
'011':'重要保持客户',
'101':'重要发展客户',
'001':'重要挽留客户',
'110':'一般价值客户',
'010':'一般保持客户',
'100':'一般发展客户',
'000':'一般挽留客户',
}
result=d[label]
return result
# 注意这里是要一行行的传递进来,所以 axis=1,传递一行得到一个 111,然后匹配返回一个值
rfm['label']=rfm[['R','F','M']].apply(lambda x:x-x.mean()).apply(rfm_func,axis=1)
- 从RFM 分层可知,大部分用户是重要保持客户,但是这是由于极值的影响,所以 RFM 的划分标准应该以业务为准,也可以通过切比雪夫去除极值后求均值,并且 RFM 的各个划分标准可以都不一样
- 尽量用小部分的用户覆盖大部分的额度,这样对之后计划的指定可以更具有针对性
#新、老、活跃、回流、流失
# 数据透视, userid为索引,月为列,求每月的消费次数,这里填充了
pivoted_counts=df.pivot_table(index='user_id',
columns='Month',
values='order_dt',
aggfunc='count').fillna(0)
# 转变一下消费,有消费为1,没有消费为0
df_purchase=pivoted_counts.applymap(lambda x:1 if x>0 else 0)
df_purchase.tail()
# 这里由于进行数据透视,填充了一些 null 值为0,而实际可能用户在当月根本就没有注册,
#这样会误导第一次消费数据的统计,所以写一个函数来处理
def active_status(data):
status=[]
# 数据一共有18个月份,每次输入一行数据,这样进行逐月判断
for i in range(18):
# 若本月没有消费,上面处理过的结果
if data[i]==0:
if len(status)>0:
if status[i-1]=='unreg':
status.append('unreg')
else:
status.append('unactive')
else:
# 之前一个数据都没有,就认为是未注册
status.append('unreg')
# 若本月消费
else:
if len(status)==0:
status.append('new')
else:
if status[i-1]=='unactive':
status.append('return')
elif status[i-1]=='unreg':
status.append('new')
else:
status.append('active')
return status
"""
若本月没有消费,这里只是和上个月判断是否注册,有缺陷,可以判断是否存在就可以了
若之前是未注册,则依旧为未注册
若之前有消费,则为流失/不活跃
其他情况,为未注册
若本月有消费
若是第一次消费,则为新用户
如果之前有过消费,则上个月为不活跃,则为回流
如果上个月为未注册,则为新用户
初次之外,为活跃
return:回流
new:新客
unreg:未注册
active:活跃
"""
#传入数据
purchase_stats=df_purchase.apply(lambda x: pd.Series(active_status(x),index=df_purchase.columns),axis=1)
# 这里把未注册的替换为空值,这样 count 计算时不会计算到
# 得到每个月的用户分布
purchase_stats_ct=purchase_stats.replace('unreg',np.NaN).apply(lambda x:pd.value_counts(x))
purchase_stats_ct
#计算各种占比
returnratee=purchase_stats_ct.apply(lambda x:x/x.sum(),axis=0)
#将NAN填充成0并转至
purchase_stats_ct_info = purchase_stats_ct.fillna(0).T
#绘制面积图
purchase_stats_ct_info .plot.area()
- 五、针对用户购买周期进行分析
- 用户购买周期(按订单)
- 用户消费周期描述
- 用户消费周期分布
- 用户生命周期(按第一次和最后一次消费) -
- 用户生命周期描述
- 用户生命周期分布
- 用户购买周期(按订单)
(1)用户购买周期(按订单)
- 用户消费周期描述
- 用户消费周期分布
# 用户购买周期(按订单)
#注意shift函数的使用
# 计算相邻两个订单的时间间隔,shift 函数是对数据进行错位,所有数据会往下平移一下,所以可以
order_diff=grouped_user.apply(lambda x:x.order_dt-x.order_dt.shift())
order_diff.head(10)
#用户消费周期描述
order_diff.describe()
#用户消费周期分布
# 去除单元值np.timedelta64(1,'D'))
(order_diff/np.timedelta64(1,'D')).hist(bins=20)
- 订单周期呈指数分布
- 用户的平均购买周期是68天
- 绝大部分用户的购买周期都低于100天
(2)用户生命周期(按第一次和最后一次消费)
- 用户生命周期描述
- 用户生命周期分布
# 用户生命周期(按第一次和最后一次消费)描述
(user_life['max']-user_life['min']).describe()
#用户生命周期分布
((user_life['max']-user_life['min'])/np.timedelta64(1,"D")).hist(bins=40)
- 用户的生命周期受只购买一次的用户影响比较厉害(可以排除)
- 用户均消费134天,中位数仅0天
# 提取大于0的数据
u_1=(user_life['max']-user_life['min']).reset_index()
u_1
# 提取大于0的数据
u_1=(user_life['max']-user_life['min']).reset_index()[0]/np.timedelta64(1,"D")
u_1[u_1>0].hist(bins=40)
- 六、针对用户复购回购进行分析
- 复购率
- 自然月内,购买多次的用户占比(即,购买了两次以上)
- 回购率
- 曾经购买过的用户在某一时期的再次购买的占比(可能是在三个月内)
- 复购率
# 复购率
pivoted_counts.head(10)
# 区分一个,和一个以上的情况,以便于计算复购率,大于1为1,等于0 为0,等于1为0
purchase_r=pivoted_counts.applymap(lambda x: 1 if x>1 else np.NaN if x==0 else 0)
purchase_r.head()
# 复购人数/总消费人数(不会计算nan值)
#绘图
(purchase_r.sum()/purchase_r.count()).plot(figsize=(10,4))
- 复购率稳定在20%所有,前一个月因为有大量新用户涌入,而这批用户只购买了一次,所以导致复购率降低
#回购率使用的数据集
df_purchase.head()
#回购率
# 需要使用函数来判断是否回购:当月消费过的用户下个月也消费了叫做回购,这个定义可以改变
def purchase_back(data):
'''判断每一个月是否是回购,根据上个月是否购买来判断,上个月消费下个月没有购买就不是回购'''
status=[]
for i in range(17):
if data[i]==1:
if data[i+1]==1:
status.append(1)
if data[i+1]==0:
status.append(0)
else:
status.append(np.NaN)
# 第18个月补充NaN
status.append(np.NaN)
return status
#遍历每一个用户每一行
indexs=df['month'].sort_values().astype('str').unique()
purchase_b = df_purchase.apply(lambda x :pd.Series(purchase_back(x),index = indexs),axis =1)
purchase_b.head()
#计算比例
purchase_b_backshop = purchase_b.sum()/purchase_b.count()
#绘图
purchase_b_backshop.plot()
五、PowerBI可视化看板设置