用python分析用户消费行为,数据来源CDNow网站的用户购买明细。一共有用户ID, 购买日期, 购买数量和购买金额四个字段。通过案例数据完成一份基础的数据分析报告
用户购买明细文档的数据如下所示,这里只展示部分数据:
-
读取 用户购买明细.txt
# 读取用户行为数据
# 四个列名分别表示用户ID, 购买日期, 购买数量, 购买金额
columns = ['user_id', 'order_date', 'order_quantity', 'order_amount']
df = pandas.read_csv('data/customer.txt', names=columns, sep='\s+')
数据文件为txt格式,用read_csv加载数据,'\s+'表示匹配任意空白字符
print(df.describe())
用户平均每笔订单购买2.4个商品,标准差在2.3,稍稍具有波动性。中位数在2个商品,75%分位数在3个商品,说明绝大部分订单的购买量都不多。最大值在99个,数字比较高。购买金额大部分都不超过43.7,主要都是小额订单。
一般而言,消费类的数据分布,都是长尾形态。大部分用户都是小额,然而小部分用户贡献了收入的大头,俗称二八。
print(df.info())
共有69659条数据记录,不存在缺失值,所以不需要进行缺失值处理。
-
将消费行为数据粒度转换成每位用户
# 将购买日期列进行数据类型转换
df['date'] = pandas.to_datetime(df['order_date'], format='%Y%m%d')
df['month'] = df['date'].values.astype('datetime64[M]')
# 根据用户id进行分组
user_grouped = df.groupby('user_id').sum()
print(user_grouped.describe())
首先,需对日期列进行数值类型的转换,原始数据的日期列只是数值类型,我们需将其转化为datetime64类型,便于后续分析处理(astype将时间格式进行转换,[M]转化成月份。这里将月份作为消费行为的主要事件窗口,选择哪种时间窗口取决于消费频率);其次,根据用户id进行分组。
从用户角度看,每位用户平均购买7张CD,最多的用户购买了1033张,属于狂热用户了。用户的平均消费金额(客单价)100元,标准差是240。
-
按月对商品销量和销售额进行分析
# 按月的纬度进行分析
# 1. 每月的CD销量
order_month = df.groupby('month')['order_quantity'].sum()
# 2. 每月的销售总额
month_total = df.groupby('month')['order_amount'].sum()
plt.rc('font', family='simhei', size=9)
plt.subplot(2, 1, 1)
plt.plot(order_month, 'yellowgreen', label='每月CD销量')
plt.grid(color='#95a5a6', linestyle='--', linewidth=1, axis='y', alpha=0.4)
plt.legend(loc=1)
plt.subplot(2, 1, 2)
plt.plot(month_total, label='每月销售总额')
plt.grid(color='#95a5a6', linestyle='--', linewidth=1, axis='y', alpha=0.4)
plt.legend(loc=0)
plt.show()
按月对每月的CD销量和销售额进行统计。观察图表,发现前三个销量非常高,数据比较异常,后面则趋于平稳。月销售额也呈现一样的趋势,前几个月高,后几个月平稳,为什么会出现这种情况呢?第一种假设,早期用户存在异常值。第二种假设,早期有各种促销优惠活动。但这里只有消费数据,无法对第二种假设作出判断。
-
分析为什么前三个月销量异常
1. 用户购买量和消费额度分析(散点图和直方图)
# 假设1 : 早期时间段的用户有异常值
user_buy = df.groupby('user_id').sum()
plt.rc('font', family='simhei', size=9)
plt.figure(figsize=(6, 8))
plt.subplot(2, 1, 1)
plt.scatter(df['order_amount'], df['order_quantity'])
plt.grid(color='#95a5a6', linestyle='--', linewidth=1, alpha=0.4)
plt.legend(['购买数量和消费金额'], loc=2)
plt.subplot(2, 1, 2)
plt.scatter(user_buy['order_amount'], user_buy['order_quantity'])
plt.grid(color='#95a5a6', linestyle='--', linewidth=1, alpha=0.4)
plt.legend(['各用户购买数量和消费金额'], loc=2)
plt.show()
# 各用户消费金额和消费数量直方图
plt.rc('font', family='simhei', size=9)
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
df['order_amount'].hist(bins=20)
plt.legend(['消费金额'], loc=1)
plt.subplot(1, 2, 2)
df.groupby('user_id')['order_quantity'].sum().hist(bins=20)
plt.legend(['购买数量'], loc=1)
plt.show()
从每笔订单的散点图中观察,订单消费金额和订单商品量呈规律性。订单的极值较少,超出1000的就几个。显然不是前三个月数据异常的罪魁祸首。
从用户的散点图观察,用户也比较健康,而且规律性比订单更强。因为这是CD网站的销售数据,商品比较单一,金额和商品量的关系也因此呈线性,没几个离群点。消费能力特别强的用户有,但是数量不多。为了更好的观察,用直方图
从直方图看,大部分用户的消费能力确实不高,高消费用户在图上几乎看不到。这也确实符合消费行为的行业规律。
以上就是对用户消费的金额和购买量的观察,接下来看消费的时间节点。
2.用户第一次和最后一次消费时间
# 用户消费的时间节点
# 用户第一次消费时间
first = df.groupby('user_id').month.min().value_counts() # Series.value_counts返回的是该Series对象中独一无二的元素的个数。
# 用户最后一次消费
last = df.groupby('user_id').month.max().value_counts()
用groupby函数将用户分组,并且求月份的最小值,最小值即用户消费行为中的第一次消费时间。结果显示,所有用户的第一次消费都集中在前三个月。我们可以这样认为,案例中的订单数据,只是选择了某个时间段消费的用户在18个月内的消费行为。
观察用户的最后一次消费时间。绝大部分数据依然集中在前三个月。后续的时间段内,依然有用户在消费,但是缓慢减少。
异常趋势的原因获得了解释。因为用户的第一次和最后一次消费大部分集中在前三个月,所以就有了上面我们所看到的前三个月数据异常高的情况。
接下来分析消费中的复购率和回购率。
-
复购率
1. 首先将用户消费数据进行数据透视。
# 首先将消费数据进行数据透视
pivot_counts = df.pivot_table(
index='user_id',
columns='month',
values='order_date',
aggfunc='count'
).fillna(0)
print(pivot_counts.head())
**2. 计算复购率
# 计算复购率
pivot_counts_tf = pivot_counts.applymap(lambda x: 1 if x > 1 else numpy.NaN if x == 0 else 0) # apply() 和applymap()是DataFrame数据类型的函数,map()是Series数据类型的函数。apply()的操作对象是DataFrame的一列或者一行数据, applymap()是element-wise的,作用于每个DataFrame的每个数据。 map()也是element-wise的,对Series中的每个数据调用搜索一次函数。
repurchase_tb = pivot_counts_tf.sum() / pivot_counts_tf.count()
plt.plot(repurchase_tb)
plt.grid(color='#95a5a6', linestyle='--', linewidth=1, alpha=0.4)
plt.show()
复购率指某个时间段内重消费两次及以上的用户在总消费用户中的占比,这里的时间窗口是月,如果一个用户在同一天下了两笔订单,这里也将他算作复购用户。将数据转换一下,消费两次及以上记为1,消费一次记为0,没有消费记为NaN。
用sum和count相除即可计算出复购率。因为这两个函数都会忽略NaN,而NaN是没有消费的用户,count不论0还是1都会统计,所以是总的消费用户数,而sum求和计算了两次以上的消费用户。
图上可以看出复购率在早期,因为大量新用户加入的关系,新客的复购率并不高,譬如1月新客们的复购率只有6%左右。而在后期,这时的用户都是大浪淘沙剩下的老客,复购率比较稳定,在20%左右。单看新客和老客,复购率有三倍左右的差距。
接下来计算回购率。回购率是某一个时间窗口内消费的用户,在下一个时间窗口仍旧消费的占比。
-
回购率
# 计算回购率
pivot_amount = df.pivot_table(
index='user_id',
columns='month',
values='order_amount',
aggfunc='mean'
).fillna(0)
pivot_amount_tf = pivot_amount.applymap(lambda x: 1 if x > 0 else 0)
def purchase_return(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(numpy.NaN)
status.append(numpy.NaN)
return status
pivot_purchase_return = pivot_amount_tf.apply(purchase_return, axis=1)
plt.plot(pivot_purchase_return.sum() / pivot_purchase_return.count())
plt.grid(color='#95a5a6', linestyle='--', linewidth=1, alpha=0.4)
plt.show()
首先将消费金额进行数据透视,接着用applymap+lambda转换数据,只要有过购买,记为1,反之为0;最后新建一个判断函数。data是输入的数据,即用户在18个月内是否消费的记录,status是空列表,后续用来保存用户是否回购的字段。
因为有18个月,所以每个月都要进行一次判断,需要用到循环。if的主要逻辑是,如果用户本月进行过消费,且下月消费过,记为1,没有消费过是0。本月若没有进行过消费,为NaN,后续的统计中进行排除。用apply函数应用在所有行上,获得想要的结果。
最后的计算和复购率大同小异,用count和sum求出。从图中可以看出,用户的回购率高于复购率,约在30%左右,波动性也较强。新用户的回购率在15%左右,和老客差异不大。
将回购率和复购率综合分析,可以得出,新客的整体质量低于老客,老客的忠诚度(回购率)表现较好,消费频次稍次,这是CDNow网站的用户消费特征。
接下来进行用户分层,我们按照用户的消费行为,简单划分成几个维度:新用户、活跃用户、不活跃用户、回流用户。
-
用户分层
新用户的定义是第一次消费。活跃用户即老客,在某一个时间窗口内有过消费。不活跃用户则是时间窗口内没有消费过的老客。回流用户是在上一个窗口中没有消费,而在当前时间窗口内有过消费。以上的时间窗口都是按月统计。
比如某用户在1月第一次消费,那么他在1月的分层就是新用户;他在2月消费国,则是活跃用户;3月没有消费,此时是不活跃用户;4月再次消费,此时是回流用户,5月还是消费,是活跃用户。
unreg-潜在用户(注册了,但为消费),new-新用户(已经消费过一次),unactive-不活跃用户, active- 活跃用户, return-回流用户
# 进行用户分层
def active_status(data):
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
pivot_purchase_status = pivot_amount_tf.apply(active_status, axis=1)
purchase_status_counts = pivot_purchase_status.\
replace('unreg', numpy.NaN).apply(lambda x: pandas.value_counts(x))
#purchase_status_counts.T.plot.area()
return_rate = purchase_status_counts.apply(lambda x: x / x.sum(), axis=1)
plt.rc('font', family='simhei', size=9)
plt.subplot(211)
plt.title('回流用户占比')
plt.plot(return_rate.loc['return'])
plt.subplot(212)
plt.title('活跃用户占比')
plt.plot(return_rate.loc['active'])
plt.show()
分为两部分的判断,以本月是否消费为界。本月没有消费,还要额外判断他是不是新客,因为部分用户是3月份才消费成为新客,那么在1、2月份他应该连新客都不是,用unreg表示。如果是老客,则为unactive。
本月若有消费,需要判断是不是第一次消费,上一个时间窗口有没有消费。对用户进行分层,逻辑确实不会简单,而且这里只是简化版本的。
生成面积图,比较丑。因为它只是某时间段消费过的用户的后续行为,蓝色和灰色区域都可以不看。只看紫色回流和红色活跃这两个分层,用户数比较稳定。这两个分层相加,就是消费用户占比(后期没新客)。
用户回流占比在5%~8%,有下降趋势。所谓回流占比,就是回流用户在总用户中的占比。另外一种指标叫回流率,指上个月多少不活跃/消费用户在本月活跃/消费。因为不活跃的用户总量近似不变,所以这里的回流率也近似回流占比。
活跃用户的下降趋势更明显,占比在3%~5%间。这里用户活跃可以看作连续消费用户,质量在一定程度上高于回流用户。
结合回流用户和活跃用户看,在后期的消费用户中,60%是回流用户,40%是活跃用户/连续消费用户,整体质量还好,但是针对这两个分层依旧有改进的空间,可以继续细化数据。
接下来分析用户质量,因为消费行为有明显的二八倾向,我们需要知道高质量用户为消费贡献了多少份额。
-
用户质量分析
# 计算高质量用户贡献了多少份额(28倾向)
user_amount = df.groupby('user_id')['order_amount'].sum().sort_values().reset_index()
user_amount['amount_cumsum'] = user_amount['order_amount'].cumsum()
amount_total = user_amount['amount_cumsum'].max()
user_amount['percentage'] = user_amount['amount_cumsum'] / amount_total
# 统计销量
user_count = df.groupby('user_id')['order_quantity'].sum().sort_values().reset_index()
user_count['count_cumsum'] = user_count['order_quantity'].cumsum()
count_total = user_count['count_cumsum'].max()
user_count['percentage'] = user_count['count_cumsum'] / count_total
plt.rc('font', family='simhei', size=9)
plt.subplot(211)
plt.title('消费额占比')
plt.plot(user_amount['percentage'])
plt.grid(color='#95a5a6', linestyle='--', linewidth=1, alpha=0.4)
plt.subplot(212)
plt.title('销量占比')
plt.plot(user_count['percentage'])
plt.grid(color='#95a5a6', linestyle='--', linewidth=1, alpha=0.4)
plt.show()
新建一个对象,按用户的消费金额生序。使用cumsum,它是累加函数。逐行计算累计的金额,最后的2500315便是总消费额。并转换成百分比。
制趋势图,横坐标是按贡献金额大小排序而成,纵坐标则是用户累计贡献。
可以很清楚的看到,前20000个用户贡献了40%的消费。后面4000位用户贡献了60%,确实呈现28倾向。
统计一下销量,前两万个用户贡献了45%的销量,高消费用户贡献了55%的销量。在消费领域中,狠抓高质量用户是万古不变的道理。
接下来计算用户生命周期,这里定义第一次消费至最后一次消费为整个用户生命。
-
用户生命周期
# 计算用户生命周期
order_date_min = df.groupby('user_id').date.min()
order_date_max = df.groupby('user_id').date.max()
print((order_date_max - order_date_min).mean())
# 生命周期分布
plt.figure(1)
plt.hist((order_date_max - order_date_min) / numpy.timedelta64(1, 'D'), bins=15)
1.统计出用户第一次消费和最后一次消费的时间,相减,得出每一位用户的生命周期。因为数据中的用户都是前三个月第一次消费,所以这里的生命周期代表的是1月~3月用户的生命周期。因为用户会持续消费,所以理论上,随着后续的消费,用户的平均生命周期会增长。
2.求一下平均,所有用户的平均生命周期是134天,比预想的高,但是平均数不靠谱,还是看一下分布吧。
因为这里的数据类型是timedelta时间,它无法直接作出直方图,所以先换算成数值。换算的方式直接除timedelta函数即可,这里的np.timedelta64(1, 'D'),D表示天,1表示1天,作为单位使用的。因为max-min已经表示为天了,两者相除就是周期的天数。
上表可看到,大部分用户只消费了一次,所有生命周期的大头都集中在了0天。但这不是我们想要的答案,不妨将只消费了一次的新客排除,来计算所有消费过两次以上的老客的生命周期。
# 排除只消费了一次的用户
life_time = (order_date_max - order_date_min).reset_index()
life_time['life_time'] = life_time.date / numpy.timedelta64(1, 'D')
plt.figure(2)
plt.hist(life_time[life_time['life_time'] > 0].life_time, bins=100)
plt.show()
# 消费两次及以上的用户平均生命周期
print(life_time[life_time['life_time'] > 0].life_time.mean())
先转换成DataFrame,再筛选出lifetime>0,即排除了仅消费了一次的那些人。做直方图。
这个图比上面的靠谱多了,虽然仍旧有不少用户生命周期靠拢在0天。这是双峰趋势图。部分质量差的用户,虽然消费了两次,但是仍旧无法持续,在用户首次消费30天内应该尽量引导。少部分用户集中在50天~300天,属于普通型的生命周期,高质量用户的生命周期,集中在400天以后,这已经属于忠诚用户了。
消费两次以上的用户生命周期是276天,远高于总体。从策略看,用户首次消费后应该花费更多的引导其进行多次消费,提供生命周期,这会带来2.5倍的增量。
再来计算留存率,留存率也是消费分析领域的经典应用。它指用户在第一次消费后,有多少比率进行第二次消费。和回流率的区别是留存倾向于计算第一次消费,并且有多个时间窗口。
-
留存率
# 计算留存率
user_purchase = df[['user_id', 'order_quantity', 'order_amount', 'date']]
user_purchase_retention = pandas.merge(
left=user_purchase,
right=order_date_min.reset_index(),
how='inner',
on='user_id',
suffixes=('', '_min')
)
user_purchase_retention['order_date_diff'] = user_purchase_retention.date - user_purchase_retention.date_min
# 将日期列转换成时间数据类型
date_trans = lambda x: x / numpy.timedelta64(1, 'D')
user_purchase_retention['date_diff'] = user_purchase_retention.order_date_diff.apply(date_trans)
这里用到merge函数,它和SQL中的join差不多,用来将两个DataFrame进行合并。我们选择了inner 的方式,对标inner join。即只合并能对应得上的数据。这里以on=user_id为对应标准。这里merge的目的是将用户消费行为和第一次消费时间对应上,形成一个新的DataFrame。suffxes参数是如果合并的内容中有重名column,加上后缀。除了merge,还有join,concat,用户接近,查看文档即可。
并将order_date和order_date_min相减。获得一个新的列,为用户每一次消费距第一次消费的时间差值。
最后,将日期列转换成时间数据类型。接着进行以下操作
# 将时间差值分组
bin = [0, 3, 7, 15, 30, 60, 90, 180, 365]
user_purchase_retention['date_diff_bin'] = pandas.cut(user_purchase_retention.date_diff, bins=bin)
# 用pivot_table数据透视, 获得的结果是用户在第一次消费之后,在后续各时间段的消费总额
pivot_retention = user_purchase_retention.pivot_table(
index='user_id',
columns='date_diff_bin',
values='order_amount',
aggfunc=sum
)
将时间差值分桶。我这里分成0~3天内,3~7天内,7~15天等,代表用户当前消费时间距第一次消费属于哪个时间段呢。这里date_diff=0并没有被划分入0~3天,因为计算的是留存率,如果用户仅消费了一次,留存率应该是0。另外一方面,如果用户第一天内消费了多次,但是往后没有消费,也算作留存率0。
用pivot_table数据透视,获得的结果是用户在第一次消费之后,在后续各时间段内的消费总额。
# 用户在后续各时间段的平均消费
print(pivot_retention.mean())
计算一下用户在后续各时间段的平均消费额,这里只统计有消费的平均值。虽然后面时间段的金额高,但是它的时间范围也宽广。从平均效果看,用户第一次消费后的0~3天内,更可能消费更多。
但消费更多是一个相对的概念,我们还要看整体中有多少用户在0~3天消费。
pivot_retention_trans = pivot_retention.fillna(0).applymap(lambda x: 1 if x > 0 else 0)
(pivot_retention_trans.sum() / pivot_retention_trans.count()).plot.bar()
plt.show()
依旧将数据转换成是否,1代表在该时间段内有后续消费,0代表没有。
只有2.5%的用户在第一次消费的次日至3天内有过消费,3%的用户在3~7天内有过消费。数字并不好看,CD购买确实不是高频消费行为。时间范围放宽后数字好看了不少,有20%的用户在第一次消费后的三个月到半年之间有过购买,27%的用户在半年后至1年内有过购买。从运营角度看,CD机营销在教育新用户的同时,应该注重用户忠诚度的培养,放长线掉大鱼,在一定时间内召回用户购买。
怎么算放长线掉大鱼呢?我们计算出用户的平均购买周期。
# 计算用户的平均购买周期
def diff(group):
d = abs(group.date_diff - group.date_diff.shift(-1))
return d
last_diff = user_purchase_retention.groupby('user_id').apply(diff)
print(last_diff.head(10))
print('用户的平均购买周期:', last_diff.mean())
plt.hist(last_diff, bins=20)
plt.show()
定义一个计算间隔的函数diff,输入的是group,分组后的数据依旧是DataFrame。我们将用户上下两次消费时间相减将能求出消费间隔了。shift函数是一个偏移函数。
x.shift()是往上偏移一个位置,x.shift(-1)是往下偏移一个位置,加参数axis=1则是左右偏移。当我想将求用户下一次距本次消费的时间间隔,用shift(-1)减当前值即可。案例用的diff函数便借助shift方法,巧妙的求出了每位用户的两次消费间隔,若为NaN,则没有下一次。
然后就简单了,用mean函数即可求出用户的平均消费间隔时间是68天。想要召回用户,在60天左右的消费间隔是比较好的。
看一下直方图,典型的长尾分布,大部分用户的消费间隔确实比较短。不妨将时间召回点设为消费后立即赠送优惠券,消费后10天询问用户CD怎么样,消费后30天提醒优惠券到期,消费后60天短信推送。这便是数据的应用了。