流程
1.销量和金额的月份趋势:折线图
2.每笔订单以及按用户分组:散点图
3.用户消费水平:直方图
4.用户首月和最后一次消费的月份统计
5.复购率:数据透视表
6.回购率:函数定义
7.用户分层:面积图;各层用户占比:折线图
8.用户消费贡献:折线图
9.用户生命周期:直方图
10.留存率:函数定义、分组、柱状图
11.用户平均购买周期:直方图
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
%matplotlib inline
#是用于在jupyter下将那些用matplotlib绘制的图显示在页面里而不是弹出一个窗口
plt.style.use('ggplot')
columns = ['id','dt','products','amount']
df = pd.read_csv('CDNOW_master.txt',names = columns,sep = '\s+')#sep表示匹配分隔符,\s+表示一个或多个空白字符(这这里指空格)
df.info()
df.describe()
平均用户购买7张,最多的购买了1033张;平均消费金额为106,标准差为240;平均值和75分位数接近,再加上最大值判断 存在小部分高额消费用户
df['date'] = pd.to_datetime(df.dt,format = '%Y%m%d')
df['month'] = df.date.values.astype('datetime64[M]')
df.info()
user_group = df.groupby('id').sum()
user_group.describe()
df.groupby('month').products.sum().plot()
df.groupby('month').amount.sum().plot()
df.plot.scatter(x = 'amount',y = 'products')
df.groupby('id').sum().plot.scatter(x = 'amount',y = 'products')
前几个月销量非常高,相比于后期数据显得异常,后期则较为平稳
与销量趋势一样,考虑可能是有季度促销活动的原因,也不能排除用户数据异常值
看到订单极值并不多,应该不是他们导致的异常波动
用户购买数量也没有太多极值
plt.figure(figsize = (12,4))
plt.subplot(121)
df.amount.hist(bins = 30)
plt.subplot(122)
df.products.hist(bins = 30)
从直方图看出也看出大部分用户的消费水平并不高
分组用户,求月份的最小值:用户消费行为中第一次的消费时间,集中在前三个月
用户最后一次消费时间,依然集中在前三个月,后续依然存在用户消费但缓慢减少
#复购率
pi_counts = df.pivot_table(index = 'id',columns = 'month',
values = 'dt',aggfunc = 'count').fillna(0)
pi_counts.columns = df.month.sort_values().astype('str').unique()
pi_counts_t = pi_counts.applymap(lambda x:1 if x>1 else (np.NaN if x==0 else 0))
(pi_counts_t.sum() / pi_counts_t.count()).plot()
在pandas中,数据透视有专门的函数pivot_table,功能非常强大。pivot_table参数中, index是设置数据透视后的索引,column是设置数据透视后的列,简而言之,index是你想要 的行,column是想要的列。案例中,我希望统计每个用户在每月的订单量,所以user_id是 index,month是column。
values是将哪个值进行计算,aggfunc是用哪种方法。于是这里用values=order_dt和 aggfunc=count,统计里order_dt出现的次数,即多少笔订单。
applymap针对Dataframe里的所有数据;lambda没有elif的用法所以要两个if else
求出复购率,sum 和 count 都会忽略NaN(图中横坐标不知道为什么没有标注,是月份)
可以看出早期的复购率较低,后期则拥有较稳定的复购率20%左右
#回购率
pi_amount = df.pivot_table(index = 'id',columns = 'month',
values = 'amount',aggfunc = 'mean').fillna(0)
pi_amount.columns = df.month.sort_values().astype('str').unique()
pi_amount_t = pi_amount.applymap(lambda x:1 if x>0 else 0)
def pur_r(data):
s = []
for i in range(17):
if data[i] == 1:
if data[i+1] == 1:
s.append(1)
if data[i+1] == 0:
s.append(0)
else:
s.append(np.NaN)
s.append(np.NaN)
return pd.Series(s,index = pi_amount.columns)
pi_amount_r = pi_amount_t.apply(pur_r,axis = 1)
(pi_amount_r.sum() / pi_amount_r.count()).plot()
新建一个判断函数。data是输入的数据,即用户在18个月内是否消费的记录,status是空列表,后续用来保存用户是否回购的字段。
因为有18个月,所以每个月都要进行一次判断,需要用到循环。if的主要逻辑是,如果用户本月进行过消费,且下月消费过,记为1,下月没有消费过是0。本月若没有进行过消费,为NaN,后续的统计中进行排除。
用apply函数应用在所有行上,获得想要的结果。
用户回购大于复购,约为30%,波动性较强
新客户回购率约在15%左右,差异不是那么明显
(新客户为第一次消费,老客户为更多消费次数)
综合分析复购率和回购率,新客质量整体低于老客,老客忠诚度表现较好,消费频次稍次
进行用户分层:
新用户为第一次消费,活跃用户为在某个时间窗口内有过消费,不活跃用户为在时间窗口内没有消费过的老客,回流用户为在上一个窗口中没有消费,而在当前时间窗口内有过消费
#用户分层
def user_type(data):
s = []
for i in range(18):
#本月未消费
if data[i] == 0:
if len(s) == 0:
s.append('未消费')
else:
if s[i-1] == '未消费':
s.append('未消费')
else:
s.append('unactive')
#本月消费
else:
if len(s) == 0:
s.append('new')
else:
if s[i-1] == '未消费':
s.append('new')
elif s[i-1] == 'unactive':
s.append('return')
else:
s.append('active')
return pd.Series(s,index = pi_amount.columns)
pi_amount_s = pi_amount_t.apply(user_type,axis = 1)
amount_counts = pi_amount_s.replace('未消费',np.NaN).apply(pd.value_counts)
amount_counts
amount_counts.fillna(0).T.plot.area(figsize = (12,6))
#回流和活跃用户占比
return_rata = amount_counts.apply(lambda x:x / x.sum(),axis = 0)
plt.figure(figsize = (12,4))
plt.subplot(121)
return_rata.loc['return'].plot()
plt.subplot(122)
return_rata.loc['active'].plot()
用户回流占比在5%-8%左右,存在下降趋势;活跃用户下降趋势更加明显,3%-5%左右,但在实际情况上其用户质量高于回流用户
#高消费人群贡献
u_amount = df.groupby('id').amount.sum().sort_values().reset_index()
u_amount['_cumsum_'] = u_amount.amount.cumsum()
total = u_amount._cumsum_.max()
u_amount['prop'] = u_amount.apply(lambda x:x._cumsum_ / total,axis = 1)
u_amount.tail()
u_amount.prop.plot()
order_amount为用户消费金额,cumsum是累加函数,最后的2500315.63就是总计消费金额
.tail方法求的是最末n项值
可以看出前20000名用户贡献了40%的消费,而后4000名用户则有60%,呈现28倾向
u_counts = df.groupby('id').dt.count().sort_values().reset_index()
u_counts['_counts_'] = u_counts.dt.cumsum()
total_ = u_counts._counts_.max()
u_counts['prop'] = u_counts.apply(lambda x:x._counts_ / total_,axis = 1)
u_counts.tail()
u_counts.prop.plot()
前两万用户贡献了50%的销量,高消费用户群体贡献了50%销量
#用户生命周期
od_min = df.groupby('id').date.min()
od_max = df.groupby('id').date.max()
(od_max - od_min).describe()
((od_max - od_min) / np.timedelta64(1,'D')).hist()
因为这里的数据类型是timedelta时间,它无法直接作出直方图,所以先换算成数值。换算的方式直接除timedelta函数即可,这里的np.timedelta64(1, 'D'),D表示天,1表示1天,作为单位使用的。因为max-min已经表示为天了,两者相除就是周期的天数。
大部分用户都只消费了1次,不妨排除这个群体,计算消费两次以上的老客生命周期
#一次以上用户生命周期
life_time = (od_max - od_min).reset_index()
life_time['lifetime'] = life_time.date / np.timedelta64(1,'D')
life_time[life_time.lifetime > 0].lifetime.hist(bins = 100,figsize = (12,6))
去除了生命周期为0的客户后,可以看出依然有许多用户的生命周期向0靠拢,呈现双峰趋势;普通型的用户生命周期集中在50—300天,高质量的用户生命周期在400天后,基本属于忠诚用户了
#留存率
u_retention = pd.merge(left = df,right = od_min.reset_index(),
how = 'inner',on = 'id',suffixes = ('','_min'))
u_retention['od_diff'] = u_retention.date - u_retention.date_min
u_retention['_diff_'] = u_retention.od_diff.apply(lambda x:x / np.timedelta64(1,'D'))
bin = [0,3,7,15,30,60,90,180,360]
u_retention['diff_bin'] = pd.cut(u_retention._diff_,bins = bin).astype('str')
pi_retention = u_retention.pivot_table(index = 'id',columns = 'diff_bin',
values = 'amount',aggfunc = sum).drop(['nan'],axis = 1)
pi_retention.mean()
merge相当于SQL的join,在how参数里指定类型为inner join,以user_id作为主键连接,suffixes参数使合并内容中重名项添加后缀
pi_retention_t = pi_retention.fillna(0).applymap(lambda x:1 if x>0 else 0)
(pi_retention_t.sum() / pi_retention_t.count()).plot.bar()
“只有2.5%的用户在第一次消费的次日至3天内有过消费,3%的用户在3~7天内有过消费。数字并不好看,CD购买确实不是高频消费行为。时间范围放宽后数字好看了不少,有20%的用户在第一次消费后的三个月到半年之间有过购买,27%的用户在半年后至1年内有过购买。从运营角度看,CD机营销在教育新用户的同时,应该注重用户忠诚度的培养,放长线掉大鱼,在一定时间内召回用户购买。”
(可能是版本问题,不把date_diff_bin转换为str格式就无法显示(0,3]而且整个数据统计也是错误的,寻找解决方法未果;转换为str之后它只会按字符思维的顺序排列)
(始终不能重排列顺序心态崩了不想自己看就直接复制了)
#平均购买周期
def diff(group):
d = group._diff_.shift(-1) - group._diff_.shift(1)
return d
last_diff = u_retention.groupby('id').apply(diff)
last_diff.mean()
shift为pandas的偏移函数,其括号内的值控制向前或者是向后偏移;
若加入参数axis=1则是左右偏移;
在这里diff中实现的是用户两次消费的时间间隔
平均消费间隔为68天
想要召回用户,在60天左右效果较好
last_diff.hist(bins = 100)
大部分用户的消费间隔还是较短的,所以想要留住用户,可以在用户每次消费之后赠送20-30天内使用的优惠券,消费后一周内询问用户消费体验,15天左右提醒优惠券到期,30天短信提醒推送
总结
问题及优化
- 打开文件的路径,可以直接放在jupyter的打开目录下
- 如果在函数定义中返回结果是列表,然而要用于DataFrame:pd.Series( ,index= )
- 作图后图中有时候无横坐标显示
- 用户生命周期中:大部分用户都只消费一次,可以排除这个群体查看老客用户生命周期
- 分区时间差之后,数据透视表始终缺少(0,3]、多出 NaN 列以及数据不正常:
将所有分组类型改为字符串类型、再去除 NaN 列,再作图(问题在于修改类型之后各组排序混乱,始终无解决方法)