Python用户消费行为分析

重要指标概念:
1、复购率:在某时间窗口内消费两次及以上的用户在总消费用户中占比。这里的时间窗口是月,如果一个用户在同一天下了两笔订单,这里也将他算作复购用户。
2、回购率:在某一个时间窗口内消费的用户,在下一个时间窗口仍旧消费的占比。我1月消费用户1000,他们中有300个2月依然消费,回购率是30%。
3、用户分层,我们按照用户的消费行为,简单划分成几个维度:新用户、活跃用户、不活跃用户、回流用户。
新用户的定义是第一次消费。活跃用户即老客,在某一个时间窗口内有过消费。不活跃用户则是时间窗口内没有消费过的老客。回流用户是在上一个窗口中没有消费,而在当前时间窗口内有过消费。以上的时间窗口都是按月统计。
比如某用户在1月第一次消费,那么他在1月的分层就是新用户;他在2月消费过,则是活跃用户;3月没有消费,此时是不活跃用户;4月再次消费,此时是回流用户,5月还是消费,是活跃用户。
4、用户生命周期,这里定义第一次消费至最后一次消费为整个用户生命。
5、留存率:指用户在第一次消费后,有多少比率进行第二次消费。和回流率的区别是留存倾向于计算第一次消费,并且有多个时间窗口

数据概述

数据是CDNow网站的用户购买明细,一共有用户ID,购买日期,购买数量,购买金额四个字段,没有空值,很干净的数据。后面我们会将时间的数据类型转换,添加其他字段。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
#打开对应文件,数据一共有用户ID,购买日期,购买数量,购买金额四个字段。
path = 'D:\python_data_file\CDNOW_master.txt'
columns = ['user_id','pur_date','pur_quantity','pur_amount']#对列进行命名
data = pd.read_table(path,names = columns,sep='\s+')#指定分隔符,数据中是逗号
data.head()
#数据描述性统计
data.describe()

每笔订单平均购买2.4个商品,标准差在2.3,稍稍具有波动性。中位数在2个商品,75分位数在3个商品,说明绝大部分订单的购买量都不多。最大值在99个,数字比较高。购买金额的情况差不多,大部分订单都集中在小额。

一般而言,消费类的数据分布,都是长尾形态。大部分用户都是小额,然而小部分用户贡献了收入的大头,俗称二八。

data.info()#没有空值,很干净的数据。接下来我们要将时间的数据类型转换。
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 69659 entries, 0 to 69658
Data columns (total 4 columns):
user_id         69659 non-null int64
pur_date        69659 non-null int64
pur_quantity    69659 non-null int64
pur_amount      69659 non-null float64
dtypes: float64(1), int64(3)
memory usage: 2.1 MB
data['pur_dt'] = pd.to_datetime(data.pur_date,format='%Y%m%d')#日期转换为时间序列
data['month'] = data.pur_dt.values.astype('datetime64[M]')#时间序列转化为月份时间序列
data.head()

pd.to_datetime可以将特定的字符串或者数字转换成时间格式,其中的format参数用于匹配。例如19970101,%Y匹配前四位数字1997,如果y小写只匹配两位数字97,%m匹配01,%d匹配01。

另外,小时是%h,分钟是%M,注意和月的大小写不一致,秒是%s。若是1997-01-01这形式,则是%Y-%m-%d,以此类推。

astype也可以将时间格式进行转换,比如[M]转化成月份。我们将月份作为消费行为的主要事件窗口,选择哪种时间窗口取决于消费频率。

data.info()#查看数据信息以及占用内存大小
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 69659 entries, 0 to 69658
Data columns (total 6 columns):
user_id         69659 non-null int64
pur_date        69659 non-null int64
pur_quantity    69659 non-null int64
pur_amount      69659 non-null float64
pur_dt          69659 non-null datetime64[ns]
month           69659 non-null datetime64[ns]
dtypes: datetime64[ns](2), float64(1), int64(3)
memory usage: 3.2 MB

提出问题

1、每月的销量、销售额汇总,每月售出物品的平均单价
2、每笔订单金额与销量的散点图、金额直方图
3、每个用户的购买次数,和购买金额汇总、购买金额与数量的散点图、购买量直方图、用户的消费时间节点、复购率、回购率、用户质量、生命周期、留存率。

拓展:1月、2月、3月的新用户在留存率有没有差异?不同生命周期的用户,他们的消费累加图是什么样的?消费留存,划分其他时间段怎么样?

一、每月的销量、销售额汇总,每月售出物品的平均单价

接下来按月的维度分析。

#按月进行分组,计算每月金额的汇总
data.groupby('month').pur_amount.sum().plot()

按月统计每个月的CD销售额。从图中可以看到,前几个月的销量非常高涨。数据比较异常。而后期的销量则很平稳。

data.groupby('month').pur_quantity.sum().plot()#求每月销量的汇总
#每月购买的物品平均单价
data.groupby('month').pur_amount.sum()/data.groupby('month').pur_quantity.sum()
month
1997-01-01    15.402769
1997-02-01    15.231733
1997-03-01    15.029446
1997-04-01    14.680285
1997-05-01    14.836192
1997-06-01    14.846716
1997-07-01    15.014006
1997-08-01    15.103006
1997-09-01    14.304207
1997-10-01    14.473766
1997-11-01    14.778372
1997-12-01    14.892077
1998-01-01    14.542778
1998-02-01    14.437633
1998-03-01    14.664265
1998-04-01    14.100813
1998-05-01    14.478821
1998-06-01    14.395555
dtype: float64

可以看到按月统计的CD销量,和上图相似,只是隔单价的倍数 为什么会呈现这个原因呢?我们假设是用户身上出了问题,早期时间段的用户中有异常值,第二假设是各类促销营销,但这里只有消费数据,所以无法判断。

二、每笔订单金额与销量的散点图、金额直方图

按订单分,画出金额和销量的散点图

data.plot.scatter(x='pur_amount',y='pur_quantity')

绘制每笔订单的散点图。从图中观察,订单消费金额和订单商品量呈规律性,每个商品十元左右。订单的极值较少,超出1000的就几个。显然不是异常波动的罪魁祸首。

三、每个用户的购买情况

#按用户分组,得到每个用户的日期求和,购买次数求和,购买金额求和
user_grouped = data.groupby('user_id').sum()
user_grouped.head()
user_grouped.describe()#按用户分组求和后的数据统计

从用户角度看,每位用户平均购买7张CD,最多的用户购买了1033张,属于狂热用户了。用户的平均消费金额(客单价)106元,标准差是240,结合分位数和最大值看,平均值才和75分位接近,肯定存在小部分的高额消费用户。

# 前面已经写了user_grouped = data.groupby('user_id').sum()#按用户分组求和
#按用户分,画出每个用户的购买金额与购买量的散点图
user_grouped.plot.scatter(x='pur_amount',y='pur_quantity')

绘制用户的散点图,用户也比较健康,而且规律性比订单更强。因为这是CD网站的销售数据,商品比较单一,金额和商品量的关系也因此呈线性,没几个离群点。

消费能力特别强的用户有,但是数量不多。为了更好的观察,用直方图。

#每笔订单销售金额直方图
plt.figure(figsize=(12,9))
plt.subplot(121)
data.pur_amount.hist(bins=30)
#每个用户的购买数量直方图
plt.subplot(122)
data.groupby('user_id').pur_quantity.sum().hist(bins=30)

plt.subplot用于绘制子图,子图用数字参数表示。121表示分成1*2个图片区域,占用第一个,即第一行第一列,122表示占用第二个。figure是尺寸函数,为了容纳两张子图,宽设置的大一点即可。

从直方图看,大部分用户的消费能力确实不高,高消费用户在图上几乎看不到。这也确实符合消费行为的行业规律。

观察完用户消费的金额和购买量,接下来看消费的时间节点。

#求出用户的第一次购买时间
data.groupby('user_id').month.min()
user_id
1       1997-01-01
2       1997-01-01
3       1997-01-01
4       1997-01-01
5       1997-01-01
           ...    
23566   1997-03-01
23567   1997-03-01
23568   1997-03-01
23569   1997-03-01
23570   1997-03-01
Name: month, Length: 23570, dtype: datetime64[ns]
#求出用户的第一次购买时间并统计用户数量
data.groupby('user_id').month.min().value_counts()
1997-02-01    8476
1997-01-01    7846
1997-03-01    7248
Name: month, dtype: int64
#用户的最后一次购买时间
data.groupby('user_id').month.max()
user_id
1       1997-01-01
2       1997-01-01
3       1998-05-01
4       1997-12-01
5       1998-01-01
           ...    
23566   1997-03-01
23567   1997-03-01
23568   1997-04-01
23569   1997-03-01
23570   1997-03-01
Name: month, Length: 23570, dtype: datetime64[ns]
#用户的最后一次购买时间并统计用户数量
data.groupby('user_id').month.max().value_counts()
1997-02-01    4912
1997-03-01    4478
1997-01-01    4192
1998-06-01    1506
1998-05-01    1042
1998-03-01     993
1998-04-01     769
1997-04-01     677
1997-12-01     620
1997-11-01     609
1998-02-01     550
1998-01-01     514
1997-06-01     499
1997-07-01     493
1997-05-01     480
1997-10-01     455
1997-09-01     397
1997-08-01     384
Name: month, dtype: int64

用groupby函数将用户分组,并且求月份的最小值,最小值即用户消费行为中的第一次消费时间。ok,结果出来了,所有用户的第一次消费都集中在前三个月。我们可以这样认为,案例中的订单数据,只是选择了某个时间段消费的用户在18个月内的消费行为。

观察用户的最后一次消费时间,绝大部分数据时间依然集中在前三个月,后续的时间段内,依然有用户在消费,但是缓慢减少 异常趋势的原因获得了解释,现在针对消费数据进一步细分。我们要明确,这只是部分用户的订单数据,所以有一定局限性。在这里,我们统一将数据上消费的用户定义为新客。

接下来分析消费中的复购率和回购率。首先将用户消费数据进行数据透视。

#通过数据透视表pivot_table得到每个用户(不是每笔订单)每月的消费次数记录(这里values=‘pur_date’要想清楚)
pivoted_counts = data.pivot_table(index='user_id',columns='month',values = 'pur_date',aggfunc='count').fillna(0)
pivoted_counts.head()
#这是一步保险操作,计算要使用的日期列名,然后在pivoted_counts上修改
columns_month = data.month.sort_values().astype('str').unique()
columns_month
array(['1997-01-01', '1997-02-01', '1997-03-01', '1997-04-01',
       '1997-05-01', '1997-06-01', '1997-07-01', '1997-08-01',
       '1997-09-01', '1997-10-01', '1997-11-01', '1997-12-01',
       '1998-01-01', '1998-02-01', '1998-03-01', '1998-04-01',
       '1998-05-01', '1998-06-01'], dtype=object)
pivoted_counts.columns = columns_month#和上面的输出一样的结果
pivoted_counts.head()

在pandas中,数据透视有专门的函数pivot_table,功能非常强大。pivot_table参数中,index是设置数据透视后的索引,column是设置数据透视后的列,简而言之,index是你想要的行,column是想要的列。案例中,我希望统计每个用户在每月的订单量,所以user_id是index,month是column。

values是将哪个值进行计算,aggfunc是用哪种方法。于是这里用values=order_dt和aggfunc=count,统计里order_dt出现的次数,即多少笔订单。

使用数据透视表,需要明确获得什么结果。有些用户在某月没有进行过消费,会用NaN表示,这里用fillna填充。

生成的数据透视,月份是1997-01-01 00:00:00表示,比较丑。将其优化成标准格式。

首先求复购率,复购率的定义是在某时间窗口内消费两次及以上的用户在总消费用户中占比。这里的时间窗口是月,如果一个用户在同一天下了两笔订单,这里也将他算作复购用户。

将数据转换一下,消费两次及以上记为1,消费一次记为0,没有消费记为NaN。

#转换成 是否消费 表
pivoted_counts_trasf = pivoted_counts.applymap(lambda x:1 if x>1 else np.NAN if x==0 else 0)
pivoted_counts_trasf.head()

applymap针对DataFrame里的所有数据。用lambda进行判断,因为这里涉及了多个结果,所以要两个if else,记住,lambda没有elif的用法

pivoted_counts_trasf.sum()#这求的是每月消费两次及以上的用户数
1997-01-01     844.0
1997-02-01    1178.0
1997-03-01    1479.0
1997-04-01     631.0
1997-05-01     436.0
1997-06-01     458.0
1997-07-01     469.0
1997-08-01     355.0
1997-09-01     352.0
1997-10-01     380.0
1997-11-01     410.0
1997-12-01     410.0
1998-01-01     324.0
1998-02-01     315.0
1998-03-01     473.0
1998-04-01     286.0
1998-05-01     298.0
1998-06-01     323.0
dtype: float64
pivoted_counts_trasf.count()#求的是每月有消费的用户数
1997-01-01    7846
1997-02-01    9633
1997-03-01    9524
1997-04-01    2822
1997-05-01    2214
1997-06-01    2339
1997-07-01    2180
1997-08-01    1772
1997-09-01    1739
1997-10-01    1839
1997-11-01    2028
1997-12-01    1864
1998-01-01    1537
1998-02-01    1551
1998-03-01    2060
1998-04-01    1437
1998-05-01    1488
1998-06-01    1506
dtype: int64
(pivoted_counts_trasf.sum()/pivoted_counts_trasf.count()).plot(figsize = (12,4))
#二者相除,画出用户的每月的复购率折线图

用sum和count相除即可计算出复购率。因为这两个函数都会忽略NaN,而NaN是没有消费的用户,count不论0还是1都会统计,所以是总的消费用户数,而sum求和计算了两次以上的消费用户。这里用了比较巧妙的替代法计算复购率,SQL中也可以用。

图上可以看出复购率在早期,因为大量新用户加入的关系,新客的复购率并不高,譬如1月新客们的复购率只有6%左右。而在后期,这时的用户都是大浪淘沙剩下的老客,复购率比较稳定,在20%左右。

单看新客和老客,复购率有三倍左右的差距。

接下来计算回购率。回购率是某一个时间窗口内消费的用户,在下一个时间窗口仍旧消费的占比。我1月消费用户1000,他们中有300个2月依然消费,回购率是30%。

回购率的计算比较难,因为它设计了横向跨时间窗口的对比。

#将消费金额进行数据透视,这里作为练习,使用了平均值。
pivoted_counts_amount = data.pivot_table(index = 'user_id',columns = 'month',values = 'pur_amount',aggfunc='mean').fillna(0)
columns_month = data.month.sort_values().astype('str').unique()
pivoted_counts_amount.columns = columns_month
pivoted_counts_amount.head()
#得出每个用户在每个月消费金额的平均值
pivoted_amount = pivoted_counts_amount.applymap(lambda x:1 if x>0 else 0)
pivoted_amount.head()#再次使用applymap+lambda转换数据,只要有购买过,记为1,反之为0
#得到各用户在该月是否购买的透视表
def purchase_return(data):#新建一个判断函数。
    a=[]
    for i in range(17):#索引从0到16,因为if条件中要取后一个数(i+1)进行判断,所以这里只用循环前十七个列名就可以了
        if data[i] == 1:#如果该月有消费
            if data[i+1]==1:#且下一个月也有消费
                a.append(1)#就添加1表示回购用户
            if data[i+1] ==0:#如果下个月没有消费
                a.append(0)#添加0
        else:#如果该月没有消费
            a.append(np.NaN)#添加nan值
    a.append(np.NaN)#这里添加一次NaN是对最后一个月的所有用户记为NaN,无论他消费没有都不用考虑他是否回购,因为后面没有日期了(时间窗口就这么大)
    return pd.Series(a,index=columns_month)#这个地方要带上索引,用一个Series呈现,否则列表中的元素无法展开应用到DataFrame中
pivoted_purchase_return = pivoted_amount.apply(purchase_return,axis=1)#指定轴向很关键
pivoted_purchase_return.head()

data是输入的数据,即用户在18个月内是否消费的记录,status是空列表,后续用来保存用户是否回购的字段。

因为有18个月,所以每个月都要进行一次判断,需要用到循环。if的主要逻辑是,如果用户本月进行过消费,且下月消费过,记为1,没有消费过是0。本月若没有进行过消费,为NaN,后续的统计中进行排除。

用apply函数应用在所有行上,获得想要的结果。

#每月 回购(本月与下月都消费了的)总次数
pivoted_purchase_return.sum()
1997-01-01    1155.0
1997-02-01    1680.0
1997-03-01    1773.0
1997-04-01     852.0
1997-05-01     747.0
1997-06-01     746.0
1997-07-01     604.0
1997-08-01     528.0
1997-09-01     532.0
1997-10-01     624.0
1997-11-01     632.0
1997-12-01     512.0
1998-01-01     472.0
1998-02-01     569.0
1998-03-01     517.0
1998-04-01     458.0
1998-05-01     446.0
1998-06-01       0.0
dtype: float64
#每月(只在本月消费和本月与下月都消费了的)总次数
pivoted_purchase_return.count()
1997-01-01    7814
1997-02-01    9610
1997-03-01    9506
1997-04-01    2822
1997-05-01    2214
1997-06-01    2339
1997-07-01    2180
1997-08-01    1772
1997-09-01    1739
1997-10-01    1839
1997-11-01    2028
1997-12-01    1864
1998-01-01    1537
1998-02-01    1551
1998-03-01    2058
1998-04-01    1436
1998-05-01    1488
1998-06-01       0
dtype: int64
#计算回购率
(pivoted_purchase_return.sum()/pivoted_purchase_return.count()).plot(figsize=(12,4))

最后的计算和复购率大同小异,用count和sum求出。从图中可以看出,用户的回购率高于复购,约在30%左右,波动性也较强。新用户的回购率在15%左右,和老客差异不大。

将回购率和复购率综合分析,可以得出,新客的整体质量低于老客,老客的忠诚度(回购率)表现较好,消费频次稍次,这是CDNow网站的用户消费特征。

接下来进行用户分层,我们按照用户的消费行为,简单划分成几个维度:新用户、活跃用户、不活跃用户、回流用户。

新用户的定义是第一次消费。活跃用户即老客,在某一个时间窗口内有过消费。不活跃用户则是时间窗口内没有消费过的老客。回流用户是在上一个窗口中没有消费,而在当前时间窗口内有过消费。以上的时间窗口都是按月统计。

比如某用户在1月第一次消费,那么他在1月的分层就是新用户;他在2月消费国,则是活跃用户;3月没有消费,此时是不活跃用户;4月再次消费,此时是回流用户,5月还是消费,是活跃用户。

分层会涉及到比较复杂的逻辑判断。

def active_status(data):
    a=[]#新建一个空列表
    for i in range(18):#这里要对所有标签进行循环
        if data[i] == 0:#若本月没有消费
            if len(a)>0:#表示本月不是第一个月
                if a[i-1]=='unreg':#前一个月也没注册
                    a.append('unreg')#那也是未注册
                else:#前一个月注册了
                    a.append('unactive')#那就是不活跃
            else:#如果本月是第一个月
                a.append('unreg')#那肯定就是未注册
        else:#本月有消费
            if len(a)==0:#本月是第一个月
                a.append('new')#那就是新用户
            else:#本月不是第一个月
                if a[i-1]=='unactive':#上个月是不活跃用户
                    a.append('return')#那就是回流用户
                elif a[i-1]=='unreg':#上个月是未注册用户
                    a.append('new')#那就是新用户
                else:#上个月是活跃或回流或新用户
                    a.append('active')#这个月消费了就是活跃用户
    return pd.Series(a,index=columns_month)#同样的道理,返回一个带索引的Series
pivoted_purchase_status = pivoted_amount.apply(active_status,axis=1)#指定轴向很关键
pivoted_purchase_status.head()

函数写得比较复杂,主要分为两部分的判断,以本月是否消费为界。本月没有消费,还要额外判断他是不是新客,因为部分用户是3月份才消费成为新客,那么在1、2月份他应该连新客都不是,用unreg表示。如果是老客,则为unactive。 本月若有消费,需要判断是不是第一次消费,上一个时间窗口有没有消费。大家可以多调试几次理顺里面的逻辑关系,对用户进行分层,逻辑确实不会简单,而且这里只是简化版本的。 从结果看,用户每个月的分层状态以及变化已经被我们计算出来。我是根据透视出的宽表计算,其实还有一种另外一种写法,只提取时间窗口内的数据和上个窗口对比判断,封装成函数做循环,它适合ETL的增量更新。

#把未注册用户替换成nan值(这样后面用value_counts函数的时候不会把nan值计算进去),并应用匿名函数计算各个月各个层级的用户个数
pivoted_status_counts = pivoted_purchase_status.replace('unreg',np.NaN).apply(lambda x:pd.value_counts(x))
pivoted_status_counts.head()#其中填充的nan值表示这个月没有对应层级的用户

unreg状态排除掉,它是「未来」才作为新客,这么能计数呢。换算成不同分层每月的统计量。

pivoted_status_counts.fillna(0).T.plot.area(figsize=(12,6))#面积图了解一下

生成面积图,比较丑。因为它只是某时间段消费过的用户的后续行为,红色和黄色区域都可以不看。只看绿色回流和蓝色活跃这两个分层,用户数比较稳定。这两个分层相加,就是消费用户占比(后期没新客)。

return_rata = pivoted_status_counts.apply(lambda x:x/x.sum(),axis =1)
return_rata.head()#对横向axis=1的各层,求每层级各月用户数量所占比例
pivoted_status_counts.apply(lambda x:x/x.sum(),axis =0)
#试一下(我就试试)指定轴向axis=0,计算的是每月各层级用户数量所占比例
pivoted_status_counts.apply(lambda x:x.sum(),axis=1)#验证性地计算一下每层级用户的数量
active       12847.0
new          23502.0
return       18954.0
unactive    344796.0
dtype: float64
#各个月份的回流用户在总回流用户中的占比
return_rata.loc['return'].plot(figsize=(12,6))

用户回流占比在5%~8%,有下降趋势。所谓回流占比,就是该月回流用户在总回流用户中的占比。另外一种指标叫回流率,指上个月多少不活跃/消费用户在本月活跃/消费。因为不活跃的用户总量近似不变,所以这里的回流率也近似回流占比。

#各个月份的活跃用户在总活跃用户中的占比
return_rata.loc['active'].plot(figsize=(12,6))

活跃用户的下降趋势更明显,占比在3%~5%间。这里用户活跃可以看作连续消费用户,质量在一定程度上高于回流用户。

结合回流用户和活跃用户看,在后期的消费用户中,60%是回流用户,40%是活跃用户/连续消费用户,整体质量还好,但是针对这两个分层依旧有改进的空间,可以继续细化数据。

接下来分析用户质量,因为消费行为有明显的二八倾向,我们需要知道高质量用户为消费贡献了多少份额。

#根据用户id分组,对各用户的消费金额进行求和,并排序
user_amount=data.groupby('user_id').pur_amount.sum().sort_values().reset_index()
user_amount['amount_cumsum'] = user_amount.pur_amount.cumsum()
user_amount.tail()

新建一个对象,按用户的消费金额生序。使用cumsum,它是累加函数。逐行计算累计的金额,最后的2500315便是总消费额。

#转换成百分比,相当于计算用户对行业的累积贡献度,内个意思差不多
amount_total = user_amount.amount_cumsum.max()
user_amount['prop'] = user_amount.apply(lambda x:x.amount_cumsum/amount_total,axis=1)#轴向很关键
user_amount.tail()
#画出这个累积贡献度的图
user_amount.prop.plot()

绘制趋势图,横坐标是按贡献金额大小排序(其实就是前面排序后重置了的索引)而成,纵坐标则是用户累计贡献。可以很清楚的看到,前20000个用户贡献了40%的消费。后面4000位用户贡献了60%,确实呈现28倾向。

#按用户id进行分组,把各用户的购买日期进行计数(每个用户买了多少天),对天数进行排序,并重置索引
user_counts = data.groupby('user_id').pur_date.count().sort_values().reset_index()
user_counts['counts_cumsum'] = user_counts.pur_date.cumsum()
user_counts.tail()#用户购买天数排序
image.png
counts_total = user_counts.counts_cumsum.max()#用户购买时间(天数)的总和
user_counts['prop'] = user_counts.apply(lambda x:x.counts_cumsum/counts_total,axis=1)
user_counts.tail()#累计贡献度(累积天数占比)的计算,指定轴向很关键
user_counts.prop.plot()#用户购买时间占比

统计一下销量,前两万个用户贡献了45%的销量,高消费用户贡献了55%的销量。在消费领域中,狠抓高质量用户是万古不变的道理。

接下来计算用户生命周期,这里定义第一次消费至最后一次消费为整个用户生命。

user_purchase = data[['user_id','pur_quantity','pur_amount','pur_dt']]
#提取原始数据中用户ID,购买数量,购买金额以及购买日期这四列的值
user_purchase_min = user_purchase.groupby('user_id').pur_dt.min()#计算出每位用户的第一次购买时间
user_purchase_max = user_purchase.groupby('user_id').pur_dt.max()#每位用户的最后一次购买时间
(user_purchase_max-user_purchase_min).head(10)#二者相减就是各个用户的消费的生命周期
user_id
1      0 days
2      0 days
3    511 days
4    345 days
5    367 days
6      0 days
7    445 days
8    452 days
9    523 days
10     0 days
Name: pur_dt, dtype: timedelta64[ns]

统计出用户第一次消费和最后一次消费的时间,相减,得出每一位用户的生命周期。因为数据中的用户都是前三个月第一次消费,所以这里的生命周期代表的是1月~3月用户的生命周期。因为用户会持续消费,所以理论上,随着后续的消费,用户的平均生命周期会增长。

(user_purchase_max-user_purchase_min).mean()#好奇地算下平均值
Timedelta('134 days 20:55:36.987696')

求一下平均,所有用户的平均生命周期是134天,比预想的高,但是平均数不靠谱,还是看一下分布吧,大家有兴趣可以用describe,更详细。

(user_purchase_max-user_purchase_min).describe()#各用户生命周期描述性统计
count                       23570
mean     134 days 20:55:36.987696
std      180 days 13:46:43.039788
min               0 days 00:00:00
25%               0 days 00:00:00
50%               0 days 00:00:00
75%             294 days 00:00:00
max             544 days 00:00:00
Name: pur_dt, dtype: object
((user_purchase_max-user_purchase_min)/np.timedelta64(1,'D')).hist(bins=15)
#这里用除法是去掉数据后面的单位days,画出直方图

因为这里的数据类型是timedelta时间,它无法直接作出直方图,所以先换算成数值。换算的方式直接除timedelta函数即可,这里的np.timedelta64(1, 'D'),D表示天,1表示1天,作为单位使用的。因为max-min已经表示为天了,两者相除就是周期的天数。 看到了没有,大部分用户只消费了一次,所有生命周期的大头都集中在了0天。但这不是我们想要的答案,不妨将只消费了一次的新客排除,来计算所有消费过两次以上的老客的生命周期。

(user_purchase_max-user_purchase_min)/np.timedelta64(1,'D')
#验证性地看下输出了什么(去掉单位后各用户的消费生命周期)
user_id
1          0.0
2          0.0
3        511.0
4        345.0
5        367.0
         ...  
23566      0.0
23567      0.0
23568     28.0
23569      0.0
23570      1.0
Name: pur_dt, Length: 23570, dtype: float64
life_time = (user_purchase_max-user_purchase_min).reset_index()#重置索引,把用户ID单独作为一列,里面有个drop参数为True的话可以删掉原索引
life_time.head()
#筛选出lifetime>0,即排除了仅消费了一次的那些人。做直方图。
life_time['life_time'] = life_time.pur_dt/np.timedelta64(1,'D')
#新建life_time列,值为 去单位后的pur_dt(也就是生命周期的数值表示)
life_time[life_time.life_time>0].life_time.hist(bins=100,figsize=(12,6))
#把生命周期大于0的数据画出直方图,直方图的高度表示该区间段的用户个数

这个图比上面的靠谱多了,虽然仍旧有不少用户生命周期靠拢在0天。这是双峰趋势图。

部分质量差的用户,虽然消费了两次,但是仍旧无法持续,在用户首次消费30天内应该尽量引导。

少部分用户集中在50~300天,属于普通型的生命周期;高质量用户的生命周期,集中在400天以后,这已经属于忠诚用户了。

下面看下400+占老客户比多少,筛选出life_time>0的就是老客户,然后求得比率是31.7%。就是说老客户中,生命周期在400天以上的占比31.7%。

A= life_time[life_time.life_time>400].user_id.count()#生命周期超过400的用户个数
B=life_time[life_time.life_time>0].user_id.count()#生命周期大于0的个数
C = life_time.user_id.count()#用户的个数
prop_400 = A/B
prop_400#生命周期大于400天的在大于0用户中的占比
0.31703716568252865
A/C#生命周期在400天以上的客户占总量的15.49%
0.15490029698769622
life_time[life_time.life_time>0].life_time.mean()#生命周期大于0的用户平均生命周期
276.0448072247308

消费两次以上的用户生命周期是276天,远高于总体。从策略看,用户首次消费后应该花费更多的引导其进行多次消费,提供生命周期,这会带来2.5倍的增量。

再来计算留存率,留存率也是消费分析领域的经典应用。它指用户在第一次消费后,有多少比率进行第二次消费。和回流率的区别是留存倾向于计算第一次消费,并且有多个时间窗口。

#接下来要进行数据的连接,先单独看看要连接的数据
user_purchase
user_purchase_min.reset_index()
#进行数据的连接,类似mysql里的内连接,指定左表和右表,连接点为用户ID
user_purchase_retention = pd.merge(left = user_purchase,right=user_purchase_min.reset_index(),
                                  how='inner',on ='user_id',suffixes=('','_min'))
user_purchase_retention.head()#将用户消费日期和用户第一次消费日期对应上

这里用到merge函数,它和SQL中的join差不多,用来将两个DataFrame进行合并。我们选择了inner 的方式,对标inner join。即只合并能对应得上的数据。这里以on=user_id为对应标准。这里merge的目的是将用户消费行为和第一次消费时间对应上,形成一个新的DataFrame。suffxes参数是如果合并的内容中有重名column,加上后缀。除了merge,还有join,concat,用户接近,查看文档即可。

user_purchase_retention['pur_dt_diff'] = user_purchase_retention.pur_dt-user_purchase_retention.pur_dt_min
#这里将order_date和order_date_min相减。获得一个新的列,为用户每一次消费距第一次消费的时间差值。
user_purchase_retention.head()
#创建一个新列dt_diff,values为pur_dt_diff去单位后的值
dt_trans = lambda x:x/np.timedelta64(1,'D')
user_purchase_retention['dt_diff'] = user_purchase_retention.pur_dt_diff.apply(dt_trans)
user_purchase_retention.head()
bin = [0,3,7,15,30,60,90,180,365]#对时间差值数据进行分箱操作
user_purchase_retention['dt_diff_bin'] = pd.cut(user_purchase_retention.dt_diff,bins=bin)
user_purchase_retention.head(10)

将时间差值分桶。我这里分成0~3天内,3~7天内,7~15天等,代表用户当前消费时间距第一次消费属于哪个时间段呢。这里date_diff=0并没有被划分入0~3天,因为计算的是留存率,如果用户仅消费了一次,留存率应该是0。另外一方面,如果用户第一天内消费了多次,但是往后没有消费,也算作留存率0。

#用pivot_table数据透视,获得的结果是用户在第一次消费之后,在后续各时间段内的消费总额。
pivoted_retention = user_purchase_retention.pivot_table(index = 'user_id',columns='dt_diff_bin',values='pur_amount',aggfunc='sum',dropna=False)
pivoted_retention.head(10)#要传递dropna=False,不然整行为Nan值的行会被删去
image.png
pivoted_retention.mean()#用户在后续各时间段的平均消费额
dt_diff_bin
(0, 3]        35.905798
(3, 7]        36.385121
(7, 15]       42.669895
(15, 30]      45.964649
(30, 60]      50.215070
(60, 90]      48.975277
(90, 180]     67.223297
(180, 365]    91.960059
dtype: float64

计算一下用户在后续各时间段的平均消费额,这里只统计有消费的平均值。虽然后面时间段的金额高,但是它的时间范围也宽广。从平均效果看,用户第一次消费后的0~3天内,更可能消费更多。

但消费更多是一个相对的概念,我们还要看整体中有多少用户在0~3天消费。

#只要有消费就记为1,没有记为0
pivoted_retention_trans = pivoted_retention.fillna(0).applymap(lambda x:1 if x>0 else 0)
pivoted_retention_trans.head()#依旧将数据转换成是否,1代表在该时间段内有后续消费,0代表没有。
pivoted_retention_trans.sum()
#各个区间段的有后续消费的用户的个数(因为前面有消费记为1,所以这里求的是总个数而不是总金额)
dt_diff_bin
(0, 3]         633
(3, 7]         828
(7, 15]       1433
(15, 30]      2133
(30, 60]      3057
(60, 90]      2350
(90, 180]     4644
(180, 365]    6134
dtype: int64
pivoted_retention_trans.count()#各个区间段的用户总个数
dt_diff_bin
(0, 3]        23570
(3, 7]        23570
(7, 15]       23570
(15, 30]      23570
(30, 60]      23570
(60, 90]      23570
(90, 180]     23570
(180, 365]    23570
dtype: int64
(pivoted_retention_trans.sum()/pivoted_retention_trans.count()).plot.bar()#画出柱状图

只有2.5%的用户在第一次消费的次日至3天内有过消费,3%的用户在3~7天内有过消费。数字并不好看,CD购买确实不是高频消费行为。时间范围放宽后数字好看了不少,有20%的用户在第一次消费后的三个月到半年之间有过购买,27%的用户在半年后至1年内有过购买。从运营角度看,CD机营销在教育新用户的同时,应该注重用户忠诚度的培养,放长线掉大鱼,在一定时间内召回用户购买。

怎么算放长线掉大鱼呢?我们计算出用户的平均购买周期。

grouped  = user_purchase_retention.groupby('user_id')
i = 0
for user,group in grouped:遍历这个groupby对象
    print(user,group)
    i+=1
    if i ==2:
        break

我们将用户分组,groupby分组后的数据,也是能用for进行循环和迭代的。第一个循环对象user,是分组的对象,即user_id;第二个循环对象group,是分组聚合后的结果。为了举例我用了print,它依次输出了user_id=1,user_id=2时的用户消费数据,是一组切割后的DataFrame。

大家应该了解分组循环的用法,但是网不建议大家用for循环,它的效率非常慢。要计算用户的消费间隔,确实需要用户分组,但是用apply效率更快。

def diff(group):#定义函数是每个分组的dt_diff列的值上下相减,也就是进行差分求时间差
    d = group.dt_diff-group.dt_diff.shift(-1)
    #shift函数返回偏移的数值,这里传递-1是向上偏移一个单位,最后一个值为nan值,若传递1是向下偏移一个单位,第一个值为nan值
    return d
last_diff = user_purchase_retention.groupby('user_id').apply(diff)
last_diff.head(10)
user_id   
1        0      NaN
2        1      0.0
         2      NaN
3        3    -87.0
         4     -3.0
         5   -227.0
         6    -10.0
         7   -184.0
         8      NaN
4        9    -17.0
Name: dt_diff, dtype: float64

定义一个计算间隔的函数diff,输入的是group,通过上面的演示,大家也应该知道分组后的数据依旧是DataFrame。我们将用户上下两次消费时间相减将能求出消费间隔了。shift函数是一个偏移函数,和excel上的offset差不多

x = pd.Series([1,2,3,4,5])
print(x.shift(1),x.shift(-1))
0    NaN
1    1.0
2    2.0
3    3.0
4    4.0
dtype: float64 
0    2.0
1    3.0
2    4.0
3    5.0
4    NaN
dtype: float64

x.shift()是往上偏移一个位置,x.shift(-1)是往下偏移一个位置,加参数axis=1则是左右偏移。当我想将求用户下一次距本次消费的时间间隔,用shift(-1)减当前值即可。案例用的diff函数便借助shift方法,巧妙的求出了每位用户的两次消费间隔,若为NaN,则没有下一次。

#用mean函数即可求出用户的平均消费间隔时间是68天。想要召回用户,在60天左右的消费间隔是比较好的。
last_diff.mean()
-68.97376814424265
last_diff.hist(bins=20)#看一下直方图

典型的长尾分布,大部分用户的消费间隔确实比较短。不妨将时间召回点设为消费后立即赠送优惠券,消费后10天询问用户CD怎么样,消费后30天提醒优惠券到期,消费后60天短信推送。这便是数据的应用了。
假若大家有兴趣,不妨多做几个分析假设,看能不能用Python挖掘出更有意思的数据,1月、2月、3月的新用户在留存率有没有差异?不同生命周期的用户,他们的消费累加图是什么样的?消费留存,划分其他时间段怎么样?

你若想要追求更好的Python技术,可以把上述的分析过程都封装成函数。当下次想要再次分析的时候,怎么样只用几个函数就搞定,而不是继续重复码代码。

这里的数据只是用户ID,消费时间,购买量和消费金额。如果换成用户ID,浏览时间,浏览量,能不能直接套用?浏览变成评论、点赞又行不行?消费行为变成用户其他行为呢?我可以明确地告诉你,大部分代码只要替换部分就能直接用了。把所有的结果分析出来需要花费多少时间呢?

Python的优势就在于快速和灵活,远比Excel和SQL快。这次是CD网站的消费行为,下次换成电商,换成O2O,一样可以在几分钟内计算出用户生命周期,用户购买频次,留存率复购率回购率等等。这对你的效率提升有多大帮助?

参考知乎一位大佬https://zhuanlan.zhihu.com/p/27910430的文章,受益匪浅,基本都弄懂后,把每行代码都加上自己的理解,然后改正了一些运行出现的小错误。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342