一、项目背景
CDNow是美国的一家网上唱片公司,成立于1994年,后来被贝塔斯曼音乐集团收购。为了平台创造出更多的利润,并且能够合理的投放广告,现使用网站1997年1月至1998年6月期间的用户消费数据进行分析。
二、分析目标
通过对该CD网站上的用户消费数据进行分析,得出用户消费行为,建立RFM模型,分析复购率、回购率等关键指标结果,以便更清楚了解用户,为进一步的营销策略提供依据。
三、分析框架
数据清洗
- 数据加载
- 数据观察与清洗
用户消费趋势分析(按月)
- 月产品销售额
- 月产品销售量
- 月消费次数 与 月消费人数
- 每月用户平均消费金额趋势
- 每月用户平均消费次数趋势
用户个体消费分析
- 用户消费金额与商品购买量的描述统计
- 用户消费金额和商品购买量散点图
- 用户消费分布图
- 用户累计消费金额占比
用户消费行为分析
- 用户第一次消费(首购)
- 用户最后一次消费
- 新老客户消费比(多少用户仅消费一次,每月新用户占比)
- 用户分层(RFM模型,新、老、活跃、回流、流失,回流用户占比)
- 用户消费周期(按订单)(用户消费周期描述,用户消费周期分布)
- 用户生命周期(用户生命周期描述,用户生命周期分布)
复购率和回购率分析
- 复购率
- 回购率
留存率分析
四、数据清洗
# 导入常用包
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 加载可视化数据包
%matplotlib inline # 可视化显示在页面,%代表内置命令,inline 显示图标
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.style.use('ggplot') # 更改设计风格,使用 ggplot style 绘图
1.数据加载
# 加载数据
columns = ['user_id','order_dt','order_products','order_amount']
df = pd.read_table('CDNow_master.txt',names = columns, sep = '\s+') # s+ 自动处理多个空格
本数据集为 CDNow 网站 1997年1月至1998年6月的用户行为数据,共约 7 万行,4 列,分别是:
- user_id:用户ID
- order_dt:购买日期
- order_products:购买产品数
- order_amount:购买金额
2.数据观察与清洗
print(df.info())
print('-'*60)
print(df.isnull().sum())
print('-'*60)
print(df.head())
print('-'*60)
print(df.tail())
print('-'*60)
print(df.describe())
Out:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 69659 entries, 0 to 69658
Data columns (total 4 columns):
user_id 69659 non-null int64
order_dt 69659 non-null int64
order_products 69659 non-null int64
order_amount 69659 non-null float64
dtypes: float64(1), int64(3)
memory usage: 2.1 MB
None
------------------------------------------------------------
user_id 0
order_dt 0
order_products 0
order_amount 0
dtype: int64
------------------------------------------------------------
user_id order_dt order_products order_amount
0 1 19970101 1 11.77
1 2 19970112 1 12.00
2 2 19970112 5 77.00
3 3 19970102 2 20.76
4 3 19970330 2 20.76
------------------------------------------------------------
user_id order_dt order_products order_amount
69654 23568 19970405 4 83.74
69655 23568 19970422 1 14.99
69656 23569 19970325 2 25.74
69657 23570 19970325 3 51.12
69658 23570 19970326 2 42.96
------------------------------------------------------------
user_id order_dt order_products order_amount
count 69659.000000 6.965900e+04 69659.000000 69659.000000
mean 11470.854592 1.997228e+07 2.410040 35.893648
std 6819.904848 3.837735e+03 2.333924 36.281942
min 1.000000 1.997010e+07 1.000000 0.000000
25% 5506.000000 1.997022e+07 1.000000 14.490000
50% 11410.000000 1.997042e+07 2.000000 25.980000
75% 17273.000000 1.997111e+07 3.000000 43.700000
max 23570.000000 1.998063e+07 99.000000 1286.010000
结果显示:
- 数据集不存在缺失值;
- 后续需要对 order_dt 列进行分析计算,需要将数据类型 int64 转换为日期型 datetime64[ns];
- 后续需要按月分析,所以将日期进行解析(实际业务是否按月分析,取决于消费频率);
- 用户平均商品购买量较少,为 2.4,有一定极值干扰;
- 用户消费金额比较稳定, 平均值为 35 美元, 中位数为 25 美元, 有一定极值干扰;
- 数据呈右偏分布。
# 解析时间
df['order_dt'] = pd.to_datetime(df.order_dt,format="%Y%m%d") # 数据类型int64转换为datetime64[ns],ns 代表时间间隔
df['month'] = df.order_dt.values.astype('datetime64[M]') # 添加新列 month,对order_dt列(取values),转换类型为datetime64[M],默认是每月的第1天,同理设置为[Y]就是每年的1月1日
3.用户消费趋势分析(按月)
# 按月分组
grouped_month = df.groupby('month')
3.1 月产品销售额
# 月产品销售额
grouped_month.order_amount.sum()
grouped_month.order_amount.sum().head()
Out:
month
1997-01-01 299060.17
1997-02-01 379590.03
1997-03-01 393155.27
1997-04-01 142824.49
1997-05-01 107933.30
Name: order_amount, dtype: float64
# 折线图绘制
plt.figure(1,figsize = (10,4)) # 创建画板
plt.title('月产品销售额')
plt.ylabel('销售额/美元')
grouped_month.order_amount.sum().plot() # plot - 折线图
结果显示:
- 销售额在前3个月持续增长,在第3个月达到最高峰,后续消费较为稳定,有轻微下降趋势。
3.2 月产品销售量
# 月产品销售量
grouped_month.order_products.sum()
grouped_month.order_products.sum().head()
Out:
month
1997-01-01 19416
1997-02-01 24921
1997-03-01 26159
1997-04-01 9729
1997-05-01 7275
Name: order_products, dtype: int64
# 折线图绘制
plt.figure(1,figsize = (10,4))
plt.title('月产品销售量')
plt.ylabel('销售量/张')
grouped_month.order_products.sum().plot()
结果显示:
- 销量在前3个月持续增长,并在3月达到最高峰,后续销量较为稳定,且有轻微下降趋势。
3.3 月消费次数 与 月消费人数
# 月消费次数
grouped_month.user_id.count()
grouped_month.user_id.count().head()
Out:
month
1997-01-01 8928
1997-02-01 11272
1997-03-01 11598
1997-04-01 3781
1997-05-01 2895
Name: user_id, dtype: int64
# 月消费人数
grouped_month['user_id'].unique().map(len) #使用map 和len 函数去重计算
# grouped_month.user_id.apply(lambda x: len(x.drop_duplicates())) #重复值处理函数drop_duplicates
grouped_month['user_id'].unique().map(len).head()
Out:
month
1997-01-01 7846
1997-02-01 9633
1997-03-01 9524
1997-04-01 2822
1997-05-01 2214
Name: user_id, dtype: int64
# 折线图绘制
plt.figure(1, figsize = (10, 4))
plt.title('月消费次数 与 月消费人数')
plt.ylabel('次数/人数')
grouped_month.user_id.count().plot(label = '次数')
grouped_month['user_id'].unique().map(len).plot(label = '人数')
plt.legend() # 给图像加上图例
结果显示:
- 每月消费人数低于每月消费次数,但差异不大;
- 前3个月每月的消费人数在8000-10000之间,后续月份的平均消费人数稳定在2000左右。
# 以上汇总分析,可以用数据透视的方法更快实现,但数据透视表进行去重操作比较麻烦,不建议使用
pivot_df = df.pivot_table(index = 'month',
values = ['order_products','order_amount','user_id'],
aggfunc = {'order_products':'sum','order_amount':'sum','user_id':'count'})
pivot_df.head()
Out:
order_amount order_products user_id
month
1997-01-01 299060.17 19416 8928
1997-02-01 379590.03 24921 11272
1997-03-01 393155.27 26159 11598
1997-04-01 142824.49 9729 3781
1997-05-01 107933.30 7275 2895
# 数据透视表绘制
pivot_df.plot(figsize = (10, 4))
3.4 每月用户平均消费金额趋势
# 每月用户平均消费金额 = 月产品销售额 / 月消费人数
amount = grouped_month.order_amount.sum()
num = grouped_month['user_id'].unique().map(len)
avg_amount = amount/num
avg_amount.head()
Out:
month
1997-01-01 38.116259
1997-02-01 39.405173
1997-03-01 41.280478
1997-04-01 50.611088
1997-05-01 48.750361
dtype: float64
# 折线图绘制
plt.figure(1, figsize = (10, 4))
plt.title('每月用户平均消费金额趋势')
plt.ylabel('金额/美元')
avg_amount.plot()
结果显示:
- 每月用户平均消费金额在1月最低,约38美元,11月达到最高,约57美元。
3.5 每月用户平均消费次数趋势
# 每月用户平均消费次数趋势 = 月消费次数 / 月消费人数
times = grouped_month.user_id.count()
num = grouped_month['user_id'].unique().map(len)
avg_times = times/num
avg_times.head()
Out:
month
1997-01-01 1.137905
1997-02-01 1.170144
1997-03-01 1.217766
1997-04-01 1.339830
1997-05-01 1.307588
Name: user_id, dtype: float64
# 折线图绘制
plt.figure(1, figsize = (10, 4))
plt.title('每月用户平均消费次数趋势图')
plt.ylabel('消费次数/次')
avg_times.plot()
结果显示:
- 每月用户平均消费次数均在1次以上,1月份最低,约为1.1次,10月份最高,约为1.4次。
4.用户个体消费分析
# 按用户分组
grouped_user = df.groupby('user_id')
4.1 用户消费金额与商品购买量的描述统计
# 用户消费金额与商品购买量的描述统计
grouped_user.sum().describe()
Out:
order_products order_amount
count 23570.000000 23570.000000
mean 7.122656 106.080426
std 16.983531 240.925195
min 1.000000 0.000000
25% 1.000000 19.970000
50% 3.000000 43.395000
75% 7.000000 106.475000
max 1033.000000 13990.930000
结果显示:
- 用户总购买量为 23570 张,平均每位用户购买 7 张,中位数为 3 张,平均数大于中位数,呈右偏分布,说明小部分用户购买了大部分的CD。
- 用户平均消费 106 美元,中位数为 43 美元,高频消费用户集中在小部分用户中,存在极值干扰。
4.2 用户消费金额和商品购买量散点图
# 图形绘制
plt.figure(figsize = (12,4))
# 用户消费金额散点图
plt.subplot(121)
plt.scatter(x = 'order_amount', y = 'order_products',data=df)
plt.xlabel('每笔订单消费金额')
plt.ylabel('每笔订单购买数量')
# 用户购买数量散点图
plt.subplot(122)
plt.scatter(x = 'order_amount',y = 'order_products',data = grouped_user.sum())
plt.xlabel('每位用户消费金额')
plt.ylabel('每位用户购买数量')
结果显示:
- 绝大部分的数据集中分布,小部分极值对分析有一定的干扰,使用query函数对order_amount进行筛选,排除极值的干扰。
# 散点图绘制
plt.figure(figsize = (12,4))
# 用户消费金额散点图
plt.subplot(121)
plt.scatter(x = 'order_amount', y = 'order_products',data = df.query('order_amount<800'))
plt.xlabel('每笔订单消费金额')
plt.ylabel('每笔订单购买数量')
# 用户购买数量散点图
plt.subplot(122)
plt.scatter(x = 'order_amount',y = 'order_products',data = grouped_user.sum().query('order_amount<4000'))
plt.xlabel('每位用户消费金额')
plt.ylabel('每位用户购买数量')
结果显示:
- 用户消费金额与产品购买量基本呈线性分布,产品购买越多,消费金额越高。
4.3 用户消费分布图
为过滤异常值,这里以order_amount 和order_product为过滤条件,使用切比雪夫定理过滤掉4%的极值。
切比雪夫定理
任意一个数据集中,位于其平均数m个标准差范围内的比例(或部分)总是至少为1-1/m2,其中m为大于1的任意正数。对于m=2,m=3和m=5有如下结果:
- 所有数据中,至少有3/4(或75%)的数据位于平均数2个标准差范围内。
- 所有数据中,至少有8/9(或88.9%)的数据位于平均数3个标准差范围内。
- 所有数据中,至少有24/25(或96%)的数据位于平均数5个标准差范围内
# 直方图绘制
plt.figure(figsize=(12, 4))
plt.subplot(121)
ax1 = grouped_user.order_amount.sum().hist(bins = 100) # bins是分组
ax1.set_xlabel('金额/美元')
ax1.set_ylabel('人数/人')
# order_amount (mean = 106 ,std = 241) mean+5std = 1311
ax1.set_xlim(0, 1400)
ax1.set_title('用户消费金额分布')
plt.subplot(122)
ax2 = grouped_user.order_products.sum().hist(bins = 100)
ax2.set_xlabel('CD 数/张')
ax2.set_ylabel('人数/人')
# order_product (mean = 7 ,std = 17) mean+5std = 92
ax2.set_xlim(0, 100)
ax2.set_title('用户购买数量分布')
结果显示:
- 用户消费金额呈现集中分布,大部分用户消费在200美元以内;
- 用户购买数量呈现集中分布,大部分用户购买CD数少于20张。
4.4 用户累计消费金额占比
# cumsum 求累加值,按照用户消费金额进行升序排序
user_cumsum = grouped_user.sum().sort_values('order_amount').apply(lambda x:x.cumsum() / x.sum())
# reset_index() 是为了得到一个自然数的行标签,表示累计用户数量
user_cumsum.reset_index().order_amount.tail()
Out:
23565 0.985405
23566 0.988025
23567 0.990814
23568 0.994404
23569 1.000000
Name: order_amount, dtype: float64
# 曲线图绘制
plt.figure(figsize=(10, 4))
user_cumsum.reset_index().order_amount.plot()
plt.title('用户累计消费金额占比')
plt.xlabel('人数/人')
plt.ylabel('百分比/%')
plt.axhline(y = 0.8,ls = "--",c = "blue",lw = 1) # 添加水平直线
plt.axvline(x = len(df.user_id.unique())*0.8,ls = "--",c = "green",lw = 1) # 添加垂直直线
结果显示:
- 按照用户消费金额进行升序排序,50%的用户仅贡献了11%的消费额度,而排名前5000的用户就贡献了60%的销售额,基本符合二八定律。
5.用户消费行为分析
5.1 用户第一次消费(首购)
# 首次购买即日期最小值
grouped_user.min().order_dt.value_counts() # value_counts()计数函数
grouped_user.min().order_dt.value_counts().head()
Out:
1997-02-08 363
1997-02-24 347
1997-02-04 346
1997-02-06 346
1997-03-04 340
Name: order_dt, dtype: int64
# 折线图绘制
plt.figure(figsize=(10, 4))
plt.title('用户第一次购买时间分布')
plt.xlabel('人数/人')
plt.ylabel('百分比/%')
grouped_user.min().order_dt.value_counts().plot()
结果显示:
- 用户第一次购买的时间集中分布在前3个月,其中在02月11日 - 02月25日期间有一次剧烈波动。
5.2 用户最后一次消费
# 对最大日期进行计数
grouped_user.month.max().value_counts()
grouped_user.month.max().value_counts().head()
Out:
1997-02-01 4912
1997-03-01 4478
1997-01-01 4192
1998-06-01 1506
1998-05-01 1042
Name: month, dtype: int64
# 折线图绘制
plt.figure(figsize=(10, 4))
plt.title('用户最后一次购买时间分布')
plt.xlabel('人数/人')
plt.ylabel('百分比/%')
grouped_user.max().order_dt.value_counts().plot()
结果显示:
- 用户最后一次购买的时间分布范围较广,大部分用户的最后一次购买时间集中在前3个月,说明这部分用户只购买了一次,忠实用户较少。
5.3 新老用户消费比
# 得到第一次和最后一次消费情况
user_life = grouped_user.order_dt.agg(['min','max'])
user_life.head()
Out:
min max
user_id
1 1997-01-01 1997-01-01
2 1997-01-12 1997-01-12
3 1997-01-02 1998-05-28
4 1997-01-01 1997-12-12
5 1997-01-01 1998-01-03
a. 多少用户仅消费一次
# 统计只消费了一次的用户,如果 min,max 日期相同,说明只消费了一次
(user_life['min']==user_life['max']).value_counts()
Out:
True 12054
False 11516
dtype: int64
结果显示:
- 有约 50% 的用户仅消费一次。
b. 每月新用户占比
# 按month,user_id分组,求每月的第一次消费日期和最后一次消费日期
user_life_month = df.groupby(['month','user_id']).order_dt.agg(["min","max"])
# 新增new列,True即为新用户
user_life_month["new"] = (user_life_month["min"] == user_life_month["max"])
# 再次按month分组,计算新用户占比
user_life_month_pct = user_life_month.groupby("month").new.apply(lambda x:x.value_counts()/x.count()).reset_index()
# 折线图绘制
user_life_month_pct[user_life_month_pct.level_1].plot(x='month',y='new',figsize=(10, 4))
plt.title('用户最后一次购买时间分布')
plt.ylabel('百分比/%')
5.4 用户分层
a. 构建RFM模型
RFM模型是衡量客户价值和客户创利能力的重要工具和手段。该机械模型通过一个客户的近期购买行为、购买的总体频率以及花了多少钱三项指标来描述该客户的价值状况。
- R(Recency):客户最近一次交易时间的间隔。R值越大,表示客户交易发生的日期越久,反之则表示客户交易发生的日期越近。
- F(Frequency):客户在最近一段时间内交易的次数。F值越大,表示客户交易越频繁,反之则表示客户交易不够活跃。
- M(Monetary):客户在最近一段时间内交易的金额。M值越大,表示客户价值越高,反之则表示客户价值越低。
# 画RFM,先对原始数据进行透视
rfm = df.pivot_table(index = 'user_id',
values = ['order_amount','order_dt','order_products'],
aggfunc = {'order_amount':'sum','order_dt':'max','order_products':'count'})
rfm.head()
Out:
order_amount order_dt order_products
user_id
1 11.77 1997-01-01 1
2 89.00 1997-01-12 2
3 156.46 1998-05-28 6
4 100.50 1997-12-12 4
5 385.61 1998-01-03 11
# 得到最近一次消费,一般是计算距离 today 最近的一次消费,这里因为时间太久远,就使用 max值
# 时间格式相减,结果是XXX days,除以一个单位'D'
rfm['R'] = -(rfm.order_dt - rfm.order_dt.max()) / np.timedelta64(1, 'D')
# 重命名:R-最后一次消费距今天数,F-消费总商品数,M-消费总金额
rfm.rename(columns = {'order_products': 'F', 'order_amount':'M'},inplace=True)
rfm.head()
Out:
M order_dt F R
user_id
1 11.77 1997-01-01 1 545.0
2 89.00 1997-01-12 2 534.0
3 156.46 1998-05-28 6 33.0
4 100.50 1997-12-12 4 200.0
5 385.61 1998-01-03 11 178.0
def rfm_func(x):
level = x.apply(lambda x:'1' if x>=0 else '0')
# level 的类型是 series,R、F、M是其索引
# 字符串拼接
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,传递一行得到一个 label,然后匹配返回一个值
rfm['label'] = rfm[['R', 'F', 'M']].apply(lambda x:x-x.mean()).apply(rfm_func,axis=1)
rfm.head()
Out:
M order_dt F R label
user_id
1 11.77 1997-01-01 1 545.0 一般发展客户
2 89.00 1997-01-12 2 534.0 一般发展客户
3 156.46 1998-05-28 6 33.0 重要保持客户
4 100.50 1997-12-12 4 200.0 一般保持客户
5 385.61 1998-01-03 11 178.0 重要保持客户
# 图形绘制
plt.figure(figsize=(10, 4))
for label,gropued in rfm.groupby('label'):
x = gropued['F']
y = gropued['R']
plt.scatter(x,y,label = label) # 利用循环绘制函数
plt.legend(loc='best') # 图例位置
plt.xlabel('Frequency')
plt.ylabel('Recency')
结果显示:
- 大部分用户是“重要保持客户”,但是这是由于极值的影响,所以 RFM 的划分标准应该以业务为准,也可以通过切比雪夫定理去除极值后求均值,并且 RFM 的各个划分标准可以都不一样。
- 尽量用小部分的用户覆盖大部分的额度
- 不要为了数据好看划分等级
# 将“重要价值客户”与“非重要价值客户”进行再次分类
rfm.loc[rfm.label=='重要价值客户','color'] = '重要价值客户'
rfm.loc[~(rfm.label=='重要价值客户'),'color'] = '非重要价值客户'
# 散点图绘制
plt.figure(figsize=(10, 4))
for label,gropued in rfm.groupby('color'):
x = gropued['F']
y = gropued['R']
plt.scatter(x,y,label = label) # 利用循环绘制函数
plt.legend(loc='best') # 图例位置
plt.xlabel('Frequency')
plt.ylabel('Recency')
结果显示:
- 红色部分为重要价值的客户,是需要重点维护对象。
# 各用户层次总消费金额、消费频次
rfm.groupby('label').sum()
Out:
M F R
label
一般价值客户 36200.21 1782 237754.0
一般保持客户 141127.20 7371 309037.0
一般发展客户 409272.88 15589 6750356.0
一般挽留客户 75781.48 3064 311519.0
重要价值客户 103260.14 1950 194091.0
重要保持客户 1591666.47 38490 517048.0
重要发展客户 96849.09 877 278754.0
重要挽留客户 46158.16 536 56855.0
结果显示:
- “重要保持客户”的消费频次和消费金额最高,其次为“一般发展客户”。
# 各用户层次对应的用户人数
rfm.groupby('label').count()
Out:
M order_dt F R
label
一般价值客户 543 543 543 543
一般保持客户 1974 1974 1974 1974
一般发展客户 13608 13608 13608 13608
一般挽留客户 1532 1532 1532 1532
重要价值客户 449 449 449 449
重要保持客户 4617 4617 4617 4617
重要发展客户 579 579 579 579
重要挽留客户 268 268 268 268
结果显示:
- “一般发展客户”这一层次的用户最多,为 13608 人,其次为“重要保持客户”。
b. 新、老、活跃、回流、流失
# 通过每月是否消费来划分用户,缺失值用0填充
pivoted_counts = df.pivot_table(index = 'user_id',
columns = 'month',
values = 'order_dt',
aggfunc = 'count').fillna(0)
pivoted_counts.columns = df.month.sort_values().astype('str').unique() # 将month数据类型转换为str
pivoted_counts.head()
Out:
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
user_id
1 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 2.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 1.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 2.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
4 2.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
5 2.0 1.0 0.0 1.0 1.0 1.0 1.0 0.0 1.0 0.0 0.0 2.0 1.0 0.0 0.0 0.0 0.0 0.0
# 转变一下消费,有消费为1,没有消费为0
df_purchase = pivoted_counts.applymap(lambda x: 1 if x> 0 else 0)
df_purchase.head()
Out:
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
user_id
1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
3 1 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 1 0
4 1 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0
5 1 1 0 1 1 1 1 0 1 0 0 1 1 0 0 0 0 0
# 这里由于进行数据透视,填充了一些 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
用户活跃状态active_status判断说明:
若本月没有消费,这里只是和上个月判断是否注册,有一定的缺陷,应该判断是否存在就可以了
- 若之前有数据,是未注册,则依旧为未注册
- 若之前有数据,不是未注册,则为流失/不活跃
- 若之前没有数据,为未注册
若本月有消费
- 若是第一次消费,则为新用户
- 若之前有过消费,上个月为不活跃,则为回流
- 若之前有过消费,上个月为未注册,则为新用户
- 若之前有过消费,其他情况为活跃
purchase_states = df_purchase.apply(active_status,axis = 1)
purchase_states.head()
Out:
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
user_id
1 new unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive
2 new unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive unactive
3 new unactive return active unactive unactive unactive unactive unactive unactive return unactive unactive unactive unactive unactive return unactive
4 new unactive unactive unactive unactive unactive unactive return unactive unactive unactive return unactive unactive unactive unactive unactive unactive
5 new active unactive return active active active unactive return unactive unactive return active unactive unactive unactive unactive unactive
# 将未注册 unreg 数据替换为空值,这样count不会计算到,得到每个月的用户分布
purchase_states_ct = purchase_states.replace('unreg',np.NaN).apply(lambda x:pd.value_counts(x))
purchase_states_ct
Out:
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
active NaN 1157.0 1681 1773.0 852.0 747.0 746.0 604.0 528.0 532.0 624.0 632.0 512.0 472.0 571.0 518.0 459.0 446.0
new 7846.0 8476.0 7248 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
return NaN NaN 595 1049.0 1362.0 1592.0 1434.0 1168.0 1211.0 1307.0 1404.0 1232.0 1025.0 1079.0 1489.0 919.0 1029.0 1060.0
unactive NaN 6689.0 14046 20748.0 21356.0 21231.0 21390.0 21798.0 21831.0 21731.0 21542.0 21706.0 22033.0 22019.0 21510.0 22133.0 22082.0 22064.0
# 转置,并用 0 填充 NaN 值
purchase_states_ct.fillna(0).T.head()
Out:
active new return unactive
1997-01-01 0.0 7846.0 0.0 0.0
1997-02-01 1157.0 8476.0 0.0 6689.0
1997-03-01 1681.0 7248.0 595.0 14046.0
1997-04-01 1773.0 0.0 1049.0 20748.0
1997-05-01 852.0 0.0 1362.0 21356.0
# 绘制面积图,描述用户分布
purchase_states_ct.fillna(0).T.plot.area(figsize=(10,4))
结果显示:
- 蓝色区域代表新用户,前3个月大量涌入,后期没有新增;
- 红色区域代表的活跃用户非常稳定,是属于核心用户;
- 紫色区域代表回流用户,与红色区域这两部分之和就是消费用户人数占比;
- 灰色区域代表不活跃用户。
c. 回流用户占比
# 每月各类用户占比
rate = purchase_states_ct.fillna(0).T.apply(lambda x:x/x.sum())
rate.head()
Out:
active new return unactive
1997-01-01 0.000000 0.332881 0.000000 0.000000
1997-02-01 0.090011 0.359610 0.000000 0.019337
1997-03-01 0.130776 0.307510 0.031390 0.040606
1997-04-01 0.137934 0.000000 0.055342 0.059981
1997-05-01 0.066283 0.000000 0.071854 0.061739
# 折线图绘制
plt.figure(figsize=(20, 8))
plt.plot(rate['return'],label='return')
plt.plot(rate['active'],label='active')
plt.title('回流&活跃用户占比')
plt.xlabel('month')
plt.ylabel('百分比/%')
plt.legend()
结果显示:
- 用户每月回流用户比占 5% ~ 8% 之间,有下降趋势,说明用户有流失倾向;
- 活跃用户的占比在 3% ~ 5% 间,下降趋势更显著,活跃用户可以看作连续消费用户,忠诚度高于回流用户。
5.5 用户消费周期(按订单)
a. 用户消费周期描述
# 错行相减计算相邻两个订单的时间间隔,shift 函数是对数据进行错位,所有数据会往下平移一下
order_diff = grouped_user.apply(lambda x:x.order_dt - x.order_dt.shift())
order_diff.head(10)
Out:
user_id
1 0 NaT
2 1 NaT
2 0 days
3 3 NaT
4 87 days
5 3 days
6 227 days
7 10 days
8 184 days
4 9 NaT
Name: order_dt, dtype: timedelta64[ns]
# 描述性统计
order_diff.describe()
count 46089
mean 68 days 23:22:13.567662
std 91 days 00:47:33.924168
min 0 days 00:00:00
25% 10 days 00:00:00
50% 31 days 00:00:00
75% 89 days 00:00:00
max 533 days 00:00:00
Name: order_dt, dtype: object
b. 用户消费周期分布
# 直方图绘制
plt.figure(figsize=(10, 4))
(order_diff / np.timedelta64(1, 'D')).hist(bins = 20)
plt.title('用户消费周期直方图')
plt.xlabel('天数/天')
plt.ylabel('人数/人')
结果显示:
- 订单周期呈指数分布;
- 用户的平均购买周期是 68 天;
- 绝大部分用户的购买周期低于 100 天。
5.6 用户生命周期
a. 用户生命周期描述
# 用户生命周期 = 最后一次购买时间 - 第一次购买时间
user_life = grouped_user.order_dt.agg(['min', 'max'])
user_life.head()
Out:
min max
user_id
1 1997-01-01 1997-01-01
2 1997-01-12 1997-01-12
3 1997-01-02 1998-05-28
4 1997-01-01 1997-12-12
5 1997-01-01 1998-01-03
# 用户生命周期描述
(user_life['max'] - user_life['min']).describe()
Out:
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
dtype: object
结果显示:
- 用户平均生命周期 134 天;
- 用户生命周期中位数仅为 0 天,大部分用户仅消费1次,这部分用户属于低质量用户;
- 用户最大生命周期的为 544 天,几乎是数据集的总天数,该用户属于核心用户。
b. 用户生命周期分布
# 直方图绘制
plt.figure(figsize=(10,4))
((user_life['max'] - user_life['min']) / np.timedelta64(1, 'D')).hist(bins = 40)
plt.title('用户生命周期直方图')
plt.xlabel('天数/天')
plt.ylabel('人数/人')
结果显示:
- 生命周期为0,即仅消费1次的用户对结果分布影响较大,可以将这部分用户筛选掉。
# 直方图绘制
plt.figure(figsize=(10,4))
u_l = ((user_life['max'] - user_life['min']).reset_index()[0] / np.timedelta64(1,'D'))
u_l[u_l > 0].hist(bins = 40) # 筛选掉生命周期为0的用户
plt.title('消费2次以上用户生命周期直方图')
plt.xlabel('天数/天')
plt.ylabel('人数/人')
plt.axvline(x = u_l[u_l > 0].mean() ,ls = "--",c = "green",lw = 1) # 消费两次以上用户平均生命周期
u_l[u_l > 0].describe()
Out:
count 11516.000000
mean 276.044807
std 166.633990
min 1.000000
25% 117.000000
50% 302.000000
75% 429.000000
max 544.000000
Name: 0, dtype: float64
结果显示:
- 用户生命周期分布呈双峰结构;
- 消费两次以上的用户平均生命周期是 276 天,远高于总体的134天;
- 在用户首次消费后引导其进行多次消费,可以有效提高用户生命周期。
6.复购率和回购率分析
复购率:自然月内,购买多次的用户占比。
回购率:曾经购买的用户在某一时期内的再次购买的占比。
6.1 复购率
# 消费两次及以上为 1 ,消费1次为 0 ,没有消费为空
purchase_r = pivoted_counts.applymap(lambda x: 1 if x > 1 else np.NaN if x==0 else 0)
# 复购率 = 复购人数 / 总消费人数(不计算NaN值)
repurchase_rate = purchase_r.sum() / purchase_r.count()
repurchase_rate.head()
Out:
1997-01-01 0.107571
1997-02-01 0.122288
1997-03-01 0.155292
1997-04-01 0.223600
1997-05-01 0.196929
dtype: float64
# 折线图绘制
plt.figure(figsize=(10, 4))
plt.title('复购率')
plt.xlabel('month')
plt.ylabel('百分比/%')
repurchase_rate.plot()
结果显示:
- 复购率稳定在 20% 左右,前三个月因为有大量新用户涌入,而这批用户只购买了一次,所以导致复购率降低。
6.2 回购率
# 回购:当月消费的用户在下个月消费
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
# 0:当月消费下个月未消费,1:当月消费下个月仍消费,NaN:当月未消费
purchase_b = df_purchase.apply(purchase_back,axis = 1)
# 回购率 = 回购次数 / 总购买次数
repurchasing = purchase_b.sum() / purchase_b.count()
repurchasing.head()
Out:
1997-01-01 0.147464
1997-02-01 0.174504
1997-03-01 0.186161
1997-04-01 0.301914
1997-05-01 0.337398
dtype: float64
plt.figure(figsize=(20,8))
plt.subplot(211)
(purchase_b.sum() / purchase_b.count()).plot()
plt.title('用户回购率图')
plt.ylabel('百分比%')
# 绘制用户每月消费&回购折线图
plt.subplot(212)
plt.plot(purchase_b.sum(),label='每月回购人数')
plt.plot(purchase_b.count(),label='每月消费人数')
plt.xlabel('month')
plt.ylabel('人数')
plt.legend()
结果显示:
- 用户回购率高于复购率,约在 30% 左右,波动性较强。
- 新用户回购率在 15 % 左右,与老用户相差不大。
- 回购人数在前三月之后趋于稳定。
7.留存率分析
# 每一次消费距第一次消费的时间差值
user_purchase = df[['user_id','order_products','order_amount','order_dt']]
# 表连接,与用户最早消费时间进行连接
user_purchase_retention = pd.merge(left = user_purchase,
right = user_life['min'].reset_index(),
how = 'inner',
on = 'user_id')
# # 新增“order_dt_diff”列,等于每一次消费距第一次消费的时间差值
user_purchase_retention['order_dt_diff'] = user_purchase_retention['order_dt']-user_purchase_retention['min']
# 新增“dt_diff”列,去掉单位“days”
user_purchase_retention['dt_diff'] = user_purchase_retention.order_dt_diff.apply(lambda x: x/np.timedelta64(1,'D'))
user_purchase_retention.head()
Out:
user_id order_products order_amount order_dt min order_dt_diff dt_diff
0 1 1 11.77 1997-01-01 1997-01-01 0 days 0.0
1 2 1 12.00 1997-01-12 1997-01-12 0 days 0.0
2 2 5 77.00 1997-01-12 1997-01-12 0 days 0.0
3 3 2 20.76 1997-01-02 1997-01-02 0 days 0.0
4 3 2 20.76 1997-03-30 1997-01-02 87 days 87.0
user_purchase_retention['dt_diff_bin'] = pd.cut(user_purchase_retention.dt_diff, bins = bin)
user_purchase_retention.head()
Out:
user_id order_products order_amount order_dt min order_dt_diff dt_diff dt_diff_bin
0 1 1 11.77 1997-01-01 1997-01-01 0 days 0.0 NaN
1 2 1 12.00 1997-01-12 1997-01-12 0 days 0.0 NaN
2 2 5 77.00 1997-01-12 1997-01-12 0 days 0.0 NaN
3 3 2 20.76 1997-01-02 1997-01-02 0 days 0.0 NaN
4 3 2 20.76 1997-03-30 1997-01-02 87 days 87.0 (60, 90]
pivoted_retention = user_purchase_retention.groupby(['user_id','dt_diff_bin']).order_amount.sum().unstack()
pivoted_retention.head()
Out:
dt_diff_bin (0, 30] (30, 60] (60, 90] (90, 120] (120, 150] (150, 180] (180, 365]
user_id
3 NaN NaN 40.3 NaN NaN NaN 78.41
4 29.73 NaN NaN NaN NaN NaN 41.44
5 13.97 38.90 NaN 45.55 38.71 26.14 155.54
7 NaN NaN NaN NaN NaN NaN 97.43
8 NaN 13.97 NaN NaN NaN 45.29 104.17
# 时间差值分桶,代表用户当前消费时间距离第一次消费时间差属于哪个时间段
bin = [0,30,60,90,120,150,180,365]
# 新增列“dt_diff_bin”将消费时间差分配到对应的分桶中
# 用户仅消费1次,dt_diff = 0,并不会划分到0-30分桶中,用户在一天消费多次之后没有消费,dt_diff = 0
user_purchase_retention['dt_diff_bin'] = pd.cut(user_purchase_retention.dt_diff, bins = bin)
user_purchase_retention.head()
user_id order_products order_amount order_dt min order_dt_diff dt_diff dt_diff_bin
0 1 1 11.77 1997-01-01 1997-01-01 0 days 0.0 NaN
1 2 1 12.00 1997-01-12 1997-01-12 0 days 0.0 NaN
2 2 5 77.00 1997-01-12 1997-01-12 0 days 0.0 NaN
3 3 2 20.76 1997-01-02 1997-01-02 0 days 0.0 NaN
4 3 2 20.76 1997-03-30 1997-01-02 87 days 87.0 (60, 90]
# 按user_id 和 dt_diff_bin 进行分组,unstack:将数据的行“旋转”为列
pivoted_retention = user_purchase_retention.groupby(['user_id','dt_diff_bin']).order_amount.sum().unstack()
pivoted_retention_trans = pivoted_retention.fillna(0).applymap(lambda x: 1 if x >0 else 0)
pivoted_retention_trans.head()
Out:
dt_diff_bin (0, 30] (30, 60] (60, 90] (90, 120] (120, 150] (150, 180] (180, 365]
user_id
3 0 0 1 0 0 0 1
4 1 0 0 0 0 0 1
5 1 1 0 1 1 1 1
7 0 0 0 0 0 0 1
8 0 1 0 0 0 1 1
# 各分桶用户平均消费金额
pivoted_retention.mean()
Out:
dt_diff_bin
(0, 30] 51.540649
(30, 60] 50.215070
(60, 90] 48.975277
(90, 120] 48.649005
(120, 150] 51.399450
(150, 180] 49.932592
(180, 365] 91.960059
dtype: float64
结果显示:
- 时间跨度越大,分桶用户消费金额越高;
- 虽然后面时段的消费更高,但是其时间跨度也更大。从平均效果看,用户第一次消费后的 0~30 天,可能消费更多。
# 用户留存用户率 =月留存用户 / 总留存用户
retention_rate = (pivoted_retention_trans.sum()/pivoted_retention_trans.count())*100
retention_rate
Out:
dt_diff_bin
(0, 30] 38.057354
(30, 60] 28.279371
(60, 90] 21.739130
(90, 120] 19.888992
(120, 150] 19.333950
(150, 180] 18.556892
(180, 365] 56.743756
dtype: float64
# 柱状图绘制
plt.figure(figsize=(10,4))
retention_rate.plot.bar()
plt.ylabel('百分比/%')
plt.xlabel('留存时间')
plt.title('用户留存率')
结果显示:
- 第一个月的留存率达到 38%,第二个月就下降到 28% 左右,之后几个月趋于稳定;
- 有 20% 左右的用户在第一次购买后的三个月到半年之间有过消费记录;
- 有 57% 左右的用户在半年以后,一年以内有过消费记录。
五、总结
- 用户消费趋势(每月)方面,前3个月有大量新用户涌入,消费金额、消费订单数、产品购买量均达到高峰,后续每月较为稳定。前3个月消费次数都在10000笔左右,后续月份的平均2500;前3个月产品购买量达到20000甚至以上,后续月份平均7000;前3个月消费人数在8000-10000之间,后续月份平均2000不到。
- 用户个体消费方面,小部分用户购买了大量的CD,拉高了平均消费金额。用户消费金额集中在0100元,有大约17000名用户。用户购买量集中在05,有大约16000名用户。50%的用户仅贡献了15%的消费额度,15%的用户贡献了60%的消费额度。大致符合二八法则。
- 用户消费行为方面,首购和最后一次购买的时间,集中在前三个月,说明很多用户购买了一次后就不再进行购买。而且最后一次购买的用户数量也在随时间递增,消费呈现流失上升的状况。
- 从整体消费记录来看,有一半的用户,只消费了一次。从每月新用户占比来看,1997年1月新用户占比高达90%以上,后续有所下降,1997年4月到1998年6月维持在81%左右,1998年6月以后无新用户。
- 从RFM模型来看,在8种客户中,重要保持客户的消费频次和消费金额最高,人数排在第二位;而一般发展客户消费频次和消费金额排第二位,人数却是最多。
- 从用户分层情况来看,新用户从第4月份以后没有新增;活跃用户有所下降;回流用户数量趋于稳定,每月1000多。流失/不活跃用户,数量非常多,基本上每月都在20000以上。
- 用户购买周期方面,平均购买周期是68天,最小值0天,最大值533天。绝大部分用户的购买周期都低于100天。
- 用户生命周期方面,由于只购买一次的用户(生命周期为0天)占了接近一半,排除这部分用户的影响之后,用户平均生命周期276天,中位数302天。
- 复购率和回购率方面,复购率稳定在20%左右,回购率稳定在30%左右,前3个月因为有大量新用户涌入,而这批用户只购买了一次,所以导致复购率和回购率都比较低。
- 用户留存率方面,用户的第一个月留存率为38%,第二个月下降到28%,后趋于稳定;有 20% 左右的用户在第一次购买后的三个月到半年之间有过消费记录;有 57% 左右的用户在半年以后,一年以内有过消费记录;距离上一次购买时间越大的用户,平均消费金额越高。