本项目是针对某CD网站的用户消费记录进行分析,了解用户的整体消费趋势以及用户的消费行为特征,并针对用户的消费特征制定对应的策略,以改善网站的运营效果,提高网站的销售额。
本次分析的数据集来源于某CD网站,这份数据集涵盖了该网站在1997年1月1日至1998年6月30日期间内用户的消费记录。数据集的每一行表示一个用户的购买记录,由用户ID(user_id)、用户消费时间(order_dt)、用户购买数量(order_products)、用户消费金额(order_amount)组成,并以空格隔开。
链接:CD数据集 提取码:ydqa
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline # 可以将matplotlib的图表直接嵌入到Notebook之中
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False #用来正常显示负号
col_name = ['user_id','order_dt','order_products','order_amount'] # 设置字段名
data = pd.read_table('./CDNOW_master.txt',names=col_name,sep='\s+')
data.head() # 显示前5行
data.info() #查看数据整体情况
data['order_dt'] = pd.to_datetime(data['order_dt'],format='%Y%m%d')
为了后续分析的展开,在这里根据order_dt的值新添加月份的字段
data['month'] = data['order_dt'].values.astype('datetime64[M]')
data.head()
data.describe()
综合以上,该批用户中,大部分用户的消费金额不是很高,小部分用户贡献了销售额的绝大部分,需要再进一步挖掘和维持这部分用户。
grouped_month = data.groupby("month") # 按月分组
# 设置画布的大小,添加子图
plt.figure(figsize=(20,15))
# 每月的消费总金额,按月分组,对order_amount求和
plt.subplot(231)
grouped_month.order_amount.sum().plot(fontsize=20)
plt.title('总销售额',fontsize=24)
# 每月的消费次数,按月分组,统计每月的消费记录数
plt.subplot(232)
grouped_month.user_id.count().plot(fontsize=20)
plt.title('总消费次数',fontsize=24)
# 每月的销量,按月分组,对order_products求和
plt.subplot(233)
grouped_month.order_products.sum().plot(fontsize=20)
plt.title('总销量',fontsize=24)
# 每月的消费用户数量
plt.subplot(234)
grouped_month["user_id"].nunique().plot(fontsize=20)
plt.title('消费的用户数量',fontsize=24)
# 每月用户平均消费金额的趋势
plt.subplot(235)
(order_amount_month/unique_user_id_month).plot(fontsize=20)
plt.title('人均消费金额',fontsize=24)
# 每月用户平均消费次数的趋势(每月总的消费次数/每月的消费人数)
plt.subplot(236)
((grouped_month.user_id.count())/(grouped_month["user_id"].nunique())).plot(fontsize=20)
plt.title('人均消费次数',fontsize=24)
plt.tight_layout() # 自动调整子图参数,使之填充整个图像区域
1. 从总销售额/总消费次数/总销量/消费的用户数量趋势图分析:
2. 从人均消费金额/人均消费次数趋势图分析:
前面的分析主要是根据时间维度,按月分析用户整体的消费情况,接下来从用户的个体角度出发,分析用户个体的消费金额、购买数量以及消费次数特征。主要分析以下几个内容:
# 根据用户ID进行分组
grouped_user_id = data.groupby("user_id")
grouped_user_id.sum().describe()
绘制用户消费金额和购买数量的散点图,可以了解用户消费金额和购买数量的分布情况。
grouped_user_id.sum().plot.scatter(x="order_products",y="order_amount",fontsize=16)
plt.xlabel("购买数量",fontsize=20)
plt.ylabel("消费金额",fontsize=20)
# 过滤消费金额在4000元以上的离群点
grouped_user_id.sum().query("order_amount<4000").plot.scatter(x="order_products",y="order_amount",fontsize=16)
plt.xlabel("购买数量",fontsize=20)
plt.ylabel("消费金额",fontsize=20)
# 设置画布的大小,添加子图
plt.figure(figsize=(20,5))
# 用户消费金额分布图绘制
plt.subplot(121)
grouped_user_id.sum().order_amount.plot.hist(bins=20,fontsize=16)
plt.title('用户消费金额分布',fontsize=24)
# 用户购买数量分布图绘制
plt.subplot(122)
grouped_user_id.sum().order_products.plot.hist(bins=20,fontsize=16)
plt.title('用户购买数量分布',fontsize=24)
# 设置画布的大小,添加子图
plt.figure(figsize=(20,5))
# 用户消费金额分布图绘制
plt.subplot(121)
grouped_user_id.sum().query("order_amount<1000").order_amount.plot.hist(bins=20,fontsize=16)
plt.title('用户消费金额分布',fontsize=24)
# 用户购买数量分布图绘制
plt.subplot(122)
grouped_user_id.sum().query("order_products<100").order_products.plot.hist(bins=20,fontsize=16)
plt.title('用户购买数量分布',fontsize=24)
# 新建DataFrame,包含用户消费金额、用户消费金额的累加、累加值的占比
# 用户的消费金额按照降序方式排列
df_amount_cumsum = grouped_user_id.sum().sort_values(by="order_amount",ascending=False)
# 新添加用户消费金额的累加值
df_amount_cumsum["amount_cumsum"] = df_amount_cumsum.cumsum()["order_amount"]
# 把原来多余的列删除,方便查看
df_amount_cumsum.drop("order_products",axis = 1,inplace = True)
# 计算累加值占比
df_amount_cumsum["persent"] = df_amount_cumsum.amount_cumsum.apply(lambda x : x/df_amount_cumsum.amount_cumsum.max())
# 将user_id从索引转化为列
df_amount_cumsum.reset_index(inplace = True)
df_amount_cumsum.head()
plt.figure(figsize=(8,4),dpi=80)
df_amount_cumsum.persent.plot(fontsize=16)
# 用户消费次数的描述统计
grouped_user_id.order_amount.count().describe()
grouped_user_id.order_amount.count().reset_index().query("order_amount<=20").order_amount.plot.hist(bins=20)
用户消费时间间隔是指每一位用户前后两次消费的时间间隔;通过分析用户订单的时间间隔,了解用户的消费习惯,可以更好的决定营销方案的实施周期。
#每个用户的每次购买时间间隔
# shift()方法是将数据错位输出,即整列数据下拉一行,第一行用NaT填充
order_diff = grouped_user_id.apply(lambda x:x["order_dt"]-x["order_dt"].shift())
order_diff .head(5)
order_diff.describe()
# 用户两次购买时间间隔的分布图
plt.figure(figsize=(10,4))
# 删除空值,消除单位days转化为数值型,绘制直方图
(order_diff.dropna() / np.timedelta64(1, 'D')).plot.hist(bins = 50,fontsize=16)
plt.xlabel("消费间隔时间/天",fontsize=20)
plt.ylabel("频数",fontsize=20)
plt.title('用户消费间隔分布图',fontsize=20)
这里定义的用户生命周期是指用户的最后一次消费与第一次消费的时间间隔。
user_life = grouped_user_id.order_dt.agg(["min","max"])
(user_life["max"]-user_life["min"]).describe()
# 用户生命周期分布图
plt.figure(figsize=(10,4))
((user_life["max"]-user_life["min"])/np.timedelta64(1,"D")).plot.hist(bins=25,fontsize=16)
plt.xlabel("生命周期/天",fontsize=20)
plt.ylabel("频数",fontsize=20)
plt.title('用户生命周期分布图',fontsize=20)
user_dt = (user_life["max"]-user_life["min"])/np.timedelta64(1,"D")
user_dt[user_dt.values>0].plot.hist(bins=50,figsize = (10,4),fontsize=16)
plt.xlabel("生命周期/天",fontsize=20)
plt.ylabel("频数",fontsize=20)
plt.title('用户生命周期分布图',fontsize=20)
RFM模型是衡量客户价值和客户创利能力的重要工具和手段。该模型通过一个客户的近期购买行为、购买的总体频率以及消费的金额三项指标来描述该客户的价值状况。
备注说明:1.由于数据集的日期比较久远,就以最大的日期作为时间的基准,离最大日期越远,则表示消费时间距"今"越久;2.三项指标的高低评判标准是与平均值作比较的,其中消费次数和消费金额大于平均值则为1,低于平均值则为0,而消费时间相反。
# 用透视表新建DataFrame,这里不用包括order_products字段
rfm_demo = data.pivot_table(index="user_id",
values=["order_dt","order_amount"],
aggfunc={"order_dt":"max","order_amount":"sum"})
# 用分组统计每一个用户的消费次数,后续需要合并到新的DataFrame表中
con_times = grouped_user_id.order_products.count()
# 合并
rfm = rfm_demo.join(con_times,how="inner")
# 修改字段名,order_amount对应“M”,这里的order_products是消费次数的统计字段,对应“F”
rfm.rename(columns={"order_amount":"M","order_products":"F"},inplace=True)
# 新添加列,“R”对应距“今”的天数
rfm['R'] = (rfm_demo.order_dt.max()-rfm.order_dt)/ np.timedelta64(1,'D')
# 删除多余的order_dt字段
rfm.drop("order_dt",axis=1,inplace=True)
rfm.head()
三项指标各自与基准值对比,正值说明指标优于基准值,负值则相反,代码如下:
# 由于最近消费取1,R的处理与F,M相反,分开单独处理
# R值为负值时,说明该值要大于平均值,即最近一次购买在很久之前
df_r = rfm[["R"]].apply(lambda x:x.mean()-x)
df_fm = rfm[["F","M"]].apply(lambda x:x-x.mean())
# 组成一个新的DataFram,供后续判断使用
df_rfm = df_r.join(df_fm)
df_rfm.head()
# 定义一个标签函数,使得df_rfm生成一个标签列,例如:“111“→“对应重要价值客户“
def rfm_func(x):
level = x.apply(lambda x : "1" if x >=0 else "0")
label = level.R + level.F + level.M
d = {"111":"重要价值客户","101":"重要发展客户","011":"重要保持客户",
"001":"重要挽留客户","100":"一般发展客户","110":"一般价值客户",
"010":"一般保持客户","000":"一般挽留客户"}
result = d[label]
return result
rfm["label"] = df_rfm[["R","F","M"]].apply(rfm_func,axis=1)
rfm.head(10)
# 分析不同层次用户消费金额和人数占比情况
# 生成一个只关于M值的DF,直接用sum()函数,生成的是Series格式的
rfm1 = rfm.groupby("label").M.agg([sum]).sort_values(by='sum',ascending=False)
# 修改列名:将生成的sum列修改成M列
rfm1.rename(columns={"sum":"M"},inplace=True)
# 计算不同层次的用户的M和人数占比,以百分比的形式表示
rfm1["M占比"] = (rfm1["M"]/rfm1.M.sum()).apply(lambda x: format(x,'.2%'))
rfm1["人数"] = rfm.groupby("label").M.count()
rfm1["人数占比"] = (rfm1["人数"]/rfm1["人数"].sum()).apply(lambda x: format(x,".2%"))
rfm1
本次分析用户的活跃程度分为以下四种:
本次分析的时间窗口采用的长度是一个月,即统计每一个月不同活跃程度的用户占比分布情况。
# 使用数据透视表,统计每一个用户在每一个月的消费次数情况
# 对于没有消费的空值,用0填充
data_pivot = data.pivot_table(index="user_id",
columns="month",
values="order_dt",
aggfunc='count').fillna(0)
user_con=data_pivot.applymap(lambda x: 1 if x>0 else 0)
user_con.head(3)
# 用户的本月消费状态需要根据上一个月的消费状态确定
# 所以要先判断是否是第一次记录
def status_func(data):
user_status = [] # 设计空列表,存放用户每一个月的活跃状态
for i in range(len(user_con.columns)): # 每一个月都进行一下判断
# 若本月没有消费
if data[i]==0:
# 是否是第一次记录
if len(user_status) > 0: # 不是第一次记录,再判断上一次记录的是否是未注册
if user_status[i-1] == "unreg": # 上一状态是未注册,本月没消费,本次依旧为未注册
user_status.append("unreg")
else:
user_status.append("unactive") # 已注册过,则这一次为不活跃
# 是第一次记录
else:
user_status.append("unreg")
# 本月有消费
else:
if len(user_status) > 0: # 判断是否是第一次记录
if user_status[i-1] == 'unactive': # 上一个状态是不活跃,本月有消费,则为回流用户
user_status.append("return")
elif user_status[i-1] == "unreg": # 上一个状态是未注册,本月有消费,为新客
user_status.append("new")
else:
user_status.append("active") # 上个月为新/活跃/回流,本月有消费,为活跃
else:
user_status.append("new") # 是第一次记录,有消费,为新客
# 用上述判断出来的状态值替代0,1
data.iloc[0:]=user_status
return data
user_status = user_con.apply(statues_func,axis=1)
user_status.tail(5)
# 将未注册的替换成空值,统计其他消费状态,每一个月的人数
statue_grouped_month = user_status.replace('unreg',np.NaN).apply(lambda x:pd.value_counts(x)).fillna(0).T
statue_grouped_month
# 绘制面积图plot.area()
statue_grouped_month.plot.area(figsize = (10,4),fontsize=16)
plt.legend(fontsize=14)
plt.xlabel("month",fontsize=20)
上图为每一个月用户不同活跃程度人数的分布情况,从图可以看出:
status_rate = status_grouped_month.apply(lambda x:x/x.sum(),axis=1)
plt.figure(figsize=(10,5),dpi=80)
ax1 = plt.subplot(221)
status_rate["new"].plot(title="new_rate",fontsize=12)
ax2 = plt.subplot(222)
status_rate["unactive"].plot(title="unactive_rate",fontsize=12)
ax3 = plt.subplot(223)
status_rate["return"].plot(title="return_rate",fontsize=12)
ax3 = plt.subplot(224)
status_rate["active"].plot(title="active_rate",fontsize=12)
plt.tight_layout() # 调整图与图之间的距离
buy_times = grouped_user_id.order_dt.count().reset_index()
# 统计仅消费一次和消费多次的用户
buy_one = (buy_times["order_dt"]==1).value_counts().values
# 绘制饼图
plt.figure(figsize=(8,4),dpi=80)
plt.pie(x = buy_one,
autopct = '%.1f%%',
shadow = True,
explode = [0.08,0],
textprops = {'fontsize' : 16})
plt.axis('equal')
plt.legend(['仅消费一次','多次消费'],loc="upper right",fontsize=14)
复购率是指在一个时间窗口内,购买多次的用户占比(购买多次的用户人数/总的消费人数)。
# 复购率涉及到每一个用户、每一个月的订单情况,所以可以先用user_id做索引,月份做列名来统计一下
re_purchase_demo = data.pivot_table(index="user_id",columns="month",
values="order_dt",
aggfunc="count")
# 转换:消费2次以上记为1,消费1次记为0,消费0次记为NAN
# 转换的目的:这样可以利用sum()计算出2次消费以上的人,用count()计算有过消费的用户
re_purchase = re_purchase_demo.applymap(lambda x: 1 if x>1 else 0 if x==1 else np.NaN)
plt.figure(figsize = (10,4))
plt.xlabel('month', fontsize=16)
plt.ylabel('复购率',fontsize=16)
plt.title('复购率趋势图',fontsize=18)
(re_purchase.sum()/re_purchase.count()).plot(fontsize=14)
记某一个时间窗口内消费的用户数量为A,在下一个时间窗口仍旧消费的用户数量记为B,这里定义B/A为回购率。通过分析回购率,可以大致预测下个时间窗口用户的消费人数。
本次回购率分析以一个月的时间长度作为一个时间窗口。
# 新建数据透视表,统计每一个用户的每月消费次数
back_purchase_demo = data.pivot_table(index="user_id",columns="month",
values="order_dt",
aggfunc="count")
# 区分有消费和没消费的,有消费的记为1,没有消费的记为0.
back_purchase = back_purchase_demo.applymap(lambda x:1 if x>0 else 0 )
#如果本月进行消费,下月也进行消费,则记为1;如果下月没有消费,则记为0,若本月没有记为消费,则记为nan
def purchase_func(data):
status = []
for i in range(len(back_purchase.columns)-1): # 减1是排除最后一个月,最后一个月分析不了
# 本月有消费
if data[i]==1:
if data[i+1]==1:
status.append(1) # 本月消费并且下个月也消费了,则为1
else:
status.append(0) # 本月消费,但下个月没有消费,则为0
else:
status.append(np.NaN) # 本月没有消费的
status.append(np.NaN) # 最后一个月,没有下个月可以作对比,用NaN填充。
data.iloc[0:]=status
return data
back_user = back_purchase.apply( purchase_func,axis=1)
back_user.head()
# 绘制趋势图
plt.figure(figsize=(10,8))
# 绘制回购率趋势图
plt.subplot(211)
plt.ylabel("回购率",fontsize=10)
plt.title("用户回购率趋势图",fontsize=16)
(back_user.sum()/back_user.count()).plot(fontsize=14)
# 绘制每月回购用户数趋势图
plt.subplot(212)
plt.ylabel("回购频次",fontsize=10)
plt.title("回购用户数量趋势图",fontsize=16)
back_user.sum().plot(fontsize=14)
plt.tight_layout() # 调整图与图之间的距离
留存率分析主要是针对一年以内,用户在首购后,在以下各个周期内的再次购买的分布情况。本次选定的周期为(7天,15天,30天,60天,90天,180天,365天)。
# 用户第一次消费的时间
order_dt_min = data.groupby("user_id").order_dt.agg(["min"]).reset_index()
# 组建新的DataFrame,并计算用户每次消费距离首次消费的时间间隔
retain_user = pd.merge(data[["user_id","order_dt","order_amount"]],order_dt_min,how="inner")
retain_user["date_interval"] = (retain_user["order_dt"] - retain_user["min"])/np.timedelta64(1,'D')
retain_user.head(10)
# 将时间间隔进行分组[0,7,15,30,60,90,180,365]
bins=[0,7,15,30,60,90,180,365]
retain_user['date_interval_bins']=pd.cut(retain_user.date_interval,bins=bins)
retain_user.head(10)
# 用户在不同留存周期的消费金额
retention_demo=retain_user.pivot_table(index='user_id',columns='date_interval_bins',
values='order_amount',aggfunc=sum,dropna=False)
retention_demo.head()
# 绘图
plt.figure(figsize=(8,4),dpi=80)
retention_demo.mean().plot.bar(fontsize=12)
plt.title("留存后平均消费金额")
plt.ylabel("消费金额")
#1代表有消费,0代表没有
retention=retention_demo.applymap(lambda x:1 if x>0 else 0)
retention.head()
# 每个周期的留存率
# 取X轴,Y轴数据
x1 = (retention.sum()/retention.count()).index.astype(str)
y1 = (retention.sum()/retention.count()).values
# 绘制折线图
plt.figure(figsize=(10,5),dpi=80)
plt.plot(x1,y1, marker='o')
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title('留存率',fontsize=16)
# 添加数据标签
for a, b in zip(x1, y1):
plt.text(a, b, round(b,2), ha='left', va='top', fontsize=12)