CD用户消费数据分析
数据是来自于CDNow网站的用户购买明细。一共有用户ID,购买日期,购买数量,购买金额四个字段。
我们通过案例数据完成一份基础的数据分析报告
数据:‘CDNOW_master.txt’
原始数据
本次目录:
步骤
0,导入数据
使用的是基于ananconda的jupyter notebook
首先导入模块和数据集,并设置列名,因为原始数据没有
import pandas as pd
import numpy as np
columns = ['user_id','order_dt','order_products','order_amount']
df = pd.read_table('CDNOW_master.txt',names = columns,sep = '\s+')
其中+处理原字符的空格
·user_id:用户ID
·order_dt:购买日期
·order_products:购买产品数
·order_amount:购买金额
查看前五行数据:
df.head()
df的前五行数据
因为后面处理需要,尽量要把数据转化成需要的类型,对于object等属性有二元法等 但之前都需要检查一下每个字段的结构类型
顺便再看一下大概整体数据的大概情况
df.info()
df.describe()
数据类型
数据的情况
观察可以看出
没有null值 还算干净不用清洗
日期的格式有问题,在后面的换算也有问题,显示是整数(interger),所以等下需要调整
而在describe中,我们可以先看看有没有极值干扰的情况,方便后续数据的清洗
·在产品数一列中,最小值1最大值99而中位数和平均值却为2和2.4,有极值干扰情况,消费金额同理
现在要利用datetime对时间序列进行处理
df['order_dt'] = pd.to_datetime(df.order_dt,format='%Y%m%d')
为了方便后面计算按月处理的统计情况,需要再创造一个按月编排的列,还是利用datetime以及values返回值对日期进行换算,修改参数改为提取M就可以了
df['month'] = df.order_dt.values.astype('datetime64[M]')
其中df.order_dt.values是一个array形式的列表,方便datetime提取
数组形式的列表
修改完和添加完后的结果
修改为时间的格式和添加了月份后
数据清洗完,进行一个大方向的清洗
1,进行用户消费趋势的分析(按月)
·每月的消费总金额
·每月的消费次数
·每月的产品购买量
·每月的消费人数
1.1 每月的消费总金额
grouped_month = df.groupby('month')
order_month_amount = grouped_month.order_amount.sum()
order_month_amount.head()
每月的各项维度,所以是对各个月份进行聚合
再用聚合的月份和sum去计算每个月的消费金额
聚合的月份和金额的sum
加载可视化模块
#加载数据可视化包
import matplotlib.pyplot as plt
#可视化显示在页面
%matplotlib inline
#更改设计风格
plt.style.use('ggplot')
order_month_amount.plot()
每月的消费总金额
1.2 每月的订单数量
订单次数,为用户出现的次数,只需用count()对user_id这一列进行分析就好了
grouped_month.user_id.count().plot()
每月的消费次数
对比上面会发现每月消费次数和每月消费金额的趋势类似,因为他们是正比关系,但是能在oct发现波每月消费商品件数易是发生在这个时间段。
13每月消费商品件数
grouped_month.order_products.sum().plot()
每月产品购买数
前三个月卖的很好,后面出现颓势趋至平稳,小有波动
可以推测为是受节日或天气影响
1.4 每月用户数量
用户数量即为ueser_id.count,但要进行去重操作,因为一个用户可以在一个月有多次消费
在这里可以使用duplicates(参数相关地址:https://blog.csdn.net/u010665216/article/details/78559091)
df.groupby('month').user_id.apply(lambda x:len(x.drop_duplicates())).plot()
user_id.(lambda x:len(x.drop_duplicates()))对用户进行去重
其中lambda可以理解为一次性的def函数,这个(lambda x:len(x.drop_duplicates()使用时需要用apply调用,用完这个函数去重完后再用len返回得到个数
方法2:df.groupby(['month','user_id']).count().reset_index()直接利用聚合的月份再去聚合也可以
方法2结果 每月的消费人数
可以看出,初时月份增长很快,但很快衰落,说明用户粘性不足
当然以上也可以用透视表+aggfunc的方法做
aggfunc可以理解为一个自定义的describe
#也可以用数据透视的方法,比较简单,直接用聚合完的月份进行索引,再用agg
df.pivot_table(index='month',
values=['order_products','order_amount','user_id'],
aggfunc={'order_products':'sum',
'order_amount':'sum',
'user_id':'count'}).head()
方法2
2,用户个体消费分析
- 用户消费金额,消费次数的描述统计
- 用户消费金额和消费散点图
- 用户消费次数等等分布图
- 用户消费金额的分布图
- 用户累计消费金额占比(百分之多少的用户占了百分之多少的消费额)
那同上,因为是个体用户消费分析,开始时我们先对用户进行聚合
grouped_user = df.groupby('user_id')
2.1 用户消费金额,消费次数的描述统计
grouped_user.sum().describe()
用户购买的产品数和消费金额
·用户购买CD的平均数是7张,但是中位数有3,说明只有极少数用户构建买了大量CD
·用户平均消费106元,中位数43,判断同上,有极值干扰
2.2用户消费金额和消费散点图
grouped_user.sum().plot.scatter(x = 'order_amount',y = 'order_products')
为进行过滤的用户消费金额和消费散点图
可以看出该该散点图有很大的极值干扰,几个极值点把图拉伸了,可以用query过滤掉
grouped_user.sum().query('order_amount < 6000').plot.scatter(x = 'order_amount',y = 'order_products')
过滤后的散点图
可以看出呈现大概一个线性关系
如果种类多样的话会呈一个扩散的样子
2.3 用户消费次数的分布图
grouped_user.count().query('order_products<100').order_amount.hist(bins=40)
用户消费次数的分布图
2.4用户消费金额的分布图
count改成sum即可
grouped_user.sum().order_amount.plot.hist(bins=20)
用户消费金额的分布图
从直方图可知,用户消费金额,绝大部分呈现集中趋势,小部分异常值干扰了判断,可以用过滤操作解决异常,还是用query
grouped_user.sum().query('order_products < 92').order_amount.plot.hist(bins=20)
过滤后的分布图
·在这边可以看出调整的参数为92,如何确定这个数字
·可以使用切比雪夫定理过滤掉异常值,计算95%的数据的分布情况
·切比雪夫定理 证明95% 的数据都距离在平均数五个标准差之内,而之前用describe函数得出均值为7,标准差16.9,17*5+7=92即为正常数据分布大概范围
从直方图看,大部分用户的消费能力确实不高,大多只消费了一次或两次,高消费用户在图上几乎看不到,这也确实符合消费行为的行业规律,即“二八法则”
2.5 用户累计消费金额占比
先看效果图,方便后面理解
用户累计消费金额占比
首先用sort对消费金额进行排序
user_cumsum=grouped_user.sum().sort_values('order_amount')
image.png
可以看出已经排序完了,而前面的消费金额0可以猜测是优惠活动,但当然也可以选择过滤掉
然后使用cumsum函数,和sum的区别在于他是滚动求和,我的理解是一个斐波那契数列的调用
而前面的排序也是为了方便现在的滚动求和
user_cumsum=grouped_user.sum().sort_values('order_amount').cumsum
再加一个cumsum后
图片只能看前几行所以看不到金额的滚动求和效果,但在左边的产品数上已经开始累加了
后面几行
可以看出最后的累加值是2500315.63
剩下要做的是把这个消费金额的每一行除以二百五十万
user_cumsum=(grouped_user.sum().sort_values('order_amount').cumsum())/2500315.63
只需要加个括号除去就可以了
只看右边的消费金额占比,左边不用管
根据上面的原理我们可以修改一下
首先原理为user_cumsum.order_amount.cumsum()/user_cumsum.order_amount.sum()
消费总金额的累加金额除以总金额
2500315.63 为消费的金额的总金额,在此处因为lambda格式要求,x表示从指定列里依次取的每一个元素,即x.sum()
每一行的累加值 可以表示成 lambda x:x.cumsum()
即表示从x里一次一次取一个数累加,而要达到上面效果可以写成
apply(lambda x:x.cunsum() / x.xum() )
那么就有
user_cumsum=grouped_user.sum().sort_values('order_amount').apply(lambda x: x.cumsum()/x.sum())
产品的叠加和消费金额的叠加
user_cumsum.reset_index().order_amount.plot()
用户累计消费金额占比
reset_index()删除掉索引,方便作图
按照升序排列,这前两万个用户,他们的消费占比是0.4,剩下的用户消费占比是0.6,偏向二八定律
二八定律
用图中可以看出, 50%的用户仅贡献了15%的消费额度。而排名前500的用户就贡献了将近40%的消费额
3.用户消费行为
- 用户第一次消费(首购)
- 用户最后一次消费
- 新老客消费比
- 多少用户仅消费一次?
- 每月新客占比
- 用户分层
- RFM
- 新老活跃回流流失
- 用户购买周期(按订单)
- 用户消费周期描述
- 用户消费周期分布
- 用户生命周期(按第一次&最后一次消费)
- 用户生命周其描述
- 用户生命周期分布
用户的维度:grouped_user
3.1用户第一次消费(首购)
grouped_user.min().order_dt.value_counts().plot()
min().order_dt.value_counts()
最近的时间(即第一次购买的时间)的频率
用户第一次购买分布
用户第一次购买分布,集中在前三个月
其中,在2.11-2.25有一次剧烈的波动
3.2 用户最后一次消费
改为max即可
grouped_user.max().order_dt.value_counts().plot()
用户最后一次购买分布
用户最后一次购买的分布比第一次分布广 大部分最后一次购买,集中在前三个月,说明有很多用户购买了一次后就不在进行购买 用户的黏性需要提升
3.3·新老客消费比
- 多少用户仅消费一次?
- 每月新客占比
第一个基于上面的方法有min()和max(),然后两个一样的就是消费一次的用户
先建个维度为时间min和时间max的表
user_life = grouped_user.order_dt.agg(['min','max'])
user_life.head()
第一次消费时间和最后一次消费时间
对比两个维度
(user_life['min'] == user_life['max']).value_counts()
对比结果
有一半用户,消费了一次 占比为50.18%
接着是新客占比
也是要用到聚合用户,但只要再加一个聚合月份就可以了
# 按月份和用户ID分组
grouped_month_user=df.groupby(['month','user_id'])
# 用当月用户订单日期最小值与用户订单日期最小值联结
tmp=grouped_month_user.order_dt.agg(['min']).join(grouped_user.order_dt.min())
# 判断用户当月订单日期最小值是否与用户订单日期最小值相等,新建字段new
tmp['new']=(tmp['min']==tmp.order_dt)
# 作新客占比折线图
tmp.reset_index().groupby('month').new.apply(lambda x: x.sum()/x.count()).plot()
plt.title('新客占比百分比')
只需要三个属性,用户id,月份最小值,用户最大值
由之前知道新客即订单日期最小值是否与用户订单日期最小值相等的user
第一行代码聚合用户和月份
第二行代码新建一个tmp转换成最小值, 求出用户在当月首购时间和用户在所有时间段的首购时间,具体看下图
第三行代码对比两个最小值,两个一样则为true
第四行代码还是用一次性函数 ,新客占比,(新客)/(总用户)
tmp 新客占比百分比
可以看出,只有前三个月的新客占比不为零,后续月份新客占比百分比为零,这说明只有前三个月有新用户的增加,后续月份消费的用户是前三个月加入的老客户,并没有新客户的加入
3.4 - 用户分层
- RFM
- 新老活跃回流流失
3.4.1 rfm
首先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
然后想转成下面三个维度
R:消费最后一次消费时间的度量,数值越小越好
F:消费的总商品数,数值越大越好
M:消费的总金额,数值越大越好
首先求R,计算每位用户最后一次消费时间与全部用户最后一次消费时间的差值,因为结果会是负的,需要再加个负号转成正值。表示消费时间距离现在的时间长短,当然不想加反过来也可以。
/ np.timedelta64(1,'D')这一段因为之前有转换datetime的原因,显示结果是XX days,为了方便后面计算我们要把day去掉,相当于把每一行都以1days,比如545 days就变成 545
rfm['R'] = -(rfm.order_dt - rfm.order_dt.max()) / np.timedelta64(1,'D')
接着是F,M,其实就是product和amount,用rename改一下就好了
rfm.rename(columns = {'order_products':'F','order_amount':'M'},inplace = True)
#rfm = rfm.drop('order.dt',1)
rfm.head()
最后结果RFM
作图的话,我想分成三个维度,上中下,即平均值上,平均值下,平均值
直接写个一次性函数每个值减去平均值
rfm[['R','F','M']].apply(lambda x:x-x.mean())
划分完
可以看出有正值有负值,正值即大于平均值,反之亦然,可以三个维度嘛可以表示成111,101之类的标准,就是2的3次方八个级别
def rfm_func(x):
level = x.apply(lambda x:'1' if x>=1 else '0')
label = level.R +level.F +level.M
d = {
'111':'重要价值客户',
'011':'重要保持客户',
'101':'重要发展客户',
'001':'重要挽留客户',
'110':'一般价值客户',
'010':'一般保持客户',
'100':'一般发展客户',
'000':'一般挽留客户',
}
result = d[label]
return result
rfm['label'] = rfm[['R','F','M']].apply(lambda x:x-x.mean()).apply(rfm_func,axis=1)
定义级别的名字随意
RFM
对lable聚合可以看每个群体的贡献
[图片上传失败...(image-f9cb7d-1552834422281)]
然后可视化
rfm.plot.scatter('F','R')
image.png
如果希望给之前的八个标签定义颜色
就需要在前面添加,定义每个标签的颜色分类
rfm.loc[rfm.label == '重要价值客户','color'] = 'g'
rfm.loc[~(rfm.label == '重要价值客户'),'color'] = 'r'
rfm.plot.scatter('F','R',c=rfm.color)
定义了其中两个标签
~符号是除去他之外的所有元素,这边省事就定义了两个,但可以一个一个添加
image 大概原理
那从上面的图可以看出,重要保持客户对于消费总金额的占比远大于其他客户的占比,这说明绝大部分收益是由重要保持客户贡献的,只要能保证这部分客户不流失和增加,那么公司收益将得到有力保障
而下面的可视图可以看出极值干扰的情况,这也许造成了上面利用到平均数方面的代码出现误差,我们可以进行人工修改或者再定义维度进行优化
3.4.2新,老,活跃,回流,流失 /不活跃
先做一个消费次数的透视图,userid为索引,月为列
pivoted_counts = df.pivot_table(index = 'user_id',
columns = 'month',
values = 'order_dt',
aggfunc = 'count').fillna(0)
pivoted_counts.head()
消费次数
可以看出有消费次数0,1,2的一个表,但是我们只要知道他有没有消费,所以转换成有无消费的透视表,tail与head类似,最前面和最后面
而这里还有一个问题,对于一些尾部数据来说,他们可能前两个月份没有注册,第三个月才第一次消费,而因为前面用来fillna(0)的关系他会自动填充,所以还要再修改一下,在进行一次判断
# 当月有消费记为1,没有消费记为0
df.purchase = pivoted_counts.applymap(lambda x: 1 if x > 0 else 0)
df.purchase.tail()
applymap用法
# 定义columns_month
columns_month=df.groupby('month').sum().reset_index().month
columns_month
接着进行一个多重判断,将用户状态分为unreg(未注册)、new(新客)、active(活跃用户)return(回流用户)和unactive(不活跃用户):
status=[]
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
status=[]
for i in range(18):
新建一个list名为status,后面就是从透视图一个一个取值进行运算,然后后面运算慢慢填充status,因为是18个月份就写18
然后分类讨论嘛,可以想象成类似一个决策树那样
if data[i]==0: 这里0就是之前applymap生成的结果
if data[i]==0: 就是没有进行过消费
len(status)>0: 对未注册用户的判断,len(status)代表运算阶段 >0就代表进入下一个月,然后就是unreg用户了,status[I-1]表示给列表第i-1个位置赋值,这之前是一个未注册用户,其他比较好励教
函数编写思路:
若本月没有消费
若之前是未注册,则依旧为未注册
若之前有消费,则为流失/不活跃
其他情况,为未注册
若本月有消费
若是第一次消费,则为新用户
若之前有过消费,则上个月为不活跃,则为回流
若上个月为未注册,则为新用户
除此之外,为活跃
#应用上面的函数
purchase_stats=df_purchase.apply(active_status,axis=1)
purchase_stats.head()
运行完
看看尾部数据
尾部数据
由上表可知,每月的用户消费状态
活跃用户,持续消费的用户,对应的使消费运营的质量
回流用户,之前不消费本月才消费,对应的是唤回运营
不活跃用户,对应的是流失
# 将unreg替换为空值,以便后续计算回购率、复购率
# 计算每个月各种状态的计数
purchase_stats_ct=purchase_stats.replace('unreg',np.NaN).apply(lambda x: pd.value_counts(x))
purchase_stats_ct
用户流失情况
方便观看也可以这样
purchase_stats_ct.fillna(0).T.head()
用户流失情况
作图,可以用第二行代码做一个面积图
#purchase_stats_ct.fillna(0).T.plot()
purchase_stats_ct.fillna(0).T.plot.area()
用户流失情况面积图
然后可以算各类用户占比
写个一次性函数,占比嘛 就是等于X/总
上面代码改一下就可以了
#purchase_stats_ct.fillna(0).T.apply(lambda x:x/x.sum(),axis = 1)
占比
由用户分层面积图可知,在前三个月用户人数不断增加,新增用户数量很高,活跃用户的数量也比较高,但在后续月份没有了新用户的注册,活跃用户数量也较高峰期有所降低,但在后续月份保持稳定的水平,同时也有稳定的回流用户。
3.5用户购买周期(按订单)
- 用户消费周期描述
- 用户消费周期分布
用户的维度:grouped_user
3.5.1用户生命周其描述
首先grouded_user,然后订单间隔就是下一个订单减去上一个订单
这种上减下,下减上,可以用shift函数,方便理解可以试一下下面两个函数
df.order_dt.head(10)
df.order_dt.shift().head(10)
这个一次性函数就是两个表相减,后面是shift完后的列表,相对于前面是整体的列往下哦平移了一行,第一行为空值
order_diff=grouped_user.apply(lambda x: x.order_dt-x.order_dt.shift())
order_diff.head(10)
订单相隔天数
每个用户的第一笔订单都是空值 ,0 days是同一天买的
可以用describe看看描述统计
描述统计
3.5.2用户消费周期分布
然后可以看见后面有days,还是用上面的方法去掉 方便后面运算或者可视化
order_diff/np.timedelta64(1,'D'))
然后再可视化
(order_diff/np.timedelta64(1,'D')).hist(bins=20)
plt.title('用户消费周期分布')
用户消费周期分布
订单周期呈指数分布
用户的平均购买周期是68天
绝大部分用户的购买周期都低于100天
3.6用户生命周期(按第一次&最后一次消费)
用户生命周其描述
用户生命周期分布
这个和3.3的新老客户消费比类似
3.3的user_life(user_life['max']-user_life['min']).describe()
image.png
image.png
((user_life['max']-user_life['min'])/np.timedelta64(1,'D')).hist(bins=20)
image.png
`用户的生命周期受只够买一次的用户影响比较厉害(可以排除)
·用户均消费134天,中位数仅0天
那我们可以进行过滤
u_1=(user_life['max']-user_life['min']).reset_index()[0]/np.timedelta64(1,'D')
u_1[u_1>0].hist(bins=40)
plt.title('用户生命周期分布')
image.png
这是双峰趋势图。部分质量差的用户,虽然消费了两次,但是仍旧无法持续,在用户首次消费30天内应该尽量引导。少部分用户集中在50天~300天,属于普通型的生命周期,高质量用户的生命周期,集中在400天以后,这已经属于忠诚用户了
4.复购率和回购率分析
-复购率(自然月内,购买多次的用户占比)
-回购率(曾经购买过的用户 在某一时期内的再次购买的占比)
4.1复购率
之前在3.4.2里有写过的透视图,一个消费次数,月为列,user为索引的pivoted_counts
pivoted_counts.head()
用户在每个月的消费次数
可以看出图中有三类值,
0就是没有买,1是买了1次,大于1则是复购
purchase_r=pivoted_counts.applymap(lambda x: 1 if x>1 else np.NaN if x==0 else 0)
因为有三类,所以要分类两次 中间的np.NaN是赋予空值,也是两个if的时候用到的方法
X=0 输出就是空值 没有消费过,后面计算也用不上 np.NaN if x==0
X=1 输出就是 0 消费一次 既不是大于1也不是为0 else 0
X>1 输出就是 1 消费大于一次 x: 1 if x>1
接下来就是算复购率,再次购买人数/总购买人数
即(purchase_r.sum()/purchase_r.count())
因为之前消费次数大于1的都转为1,小于1的都变成0和空值了,所以sum就是复购人数,count的话会对0和1计数并且排除空值
可视化
(purchase_r.sum()/purchase_r.count()).plot(figsize=(10,4))
plt.title('复购率')
复购率
复购率稳定在20%左右,前三个月因为有大量新用户涌入,而这些用户只购买了一次,所以导致复购率降低,后续月份用户数量比较稳定,所以复购率也稳定在21%左右,即后续月份每月有大概21%的用户会在一个月内消费两次以上
4.2 回购率
回购率因为有一月用户和二三月刚注册用户 要分情况
购买明细表:
购买明细表
定义一个函数,将消费两次以上记为1,消费一次记为0,没有消费记为空值:
# 定义函数
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)
status.append(np.NaN)
return pd.Series (status,index=columns_month)
运行后结果
然后可视化
(purchase_b.sum()/purchase_b.count()).plot(figsize=(10,4))
plt.title('回购率')
回购率
前三个月因为有大量的新用户涌入,但是超过一半的人只消费了一次,所以前三个月回购率比较低,后续月份用户人数比较稳定,回购率也比较稳定,稳定在30%左右,即当月消费人数中有30%左右的用户会在下一个月再次消费