用户消费行为和消费趋势分析案例

一、项目背景:

这是一份单车网站用户消费行为的分析报告,通过对用户消费记录,深入分析用户消费行为,建立 RFM 模型,进行用户分层,发掘高价值用户并进行针对性管理和维护,实现用户运营精细化。

二、分析思路:

image.png

三、分析工具:

使用Python,PowerBI对数据进行了可视化分析

四、分析目的:

结合数据集,分析用户消费趋势和行为特征,了解业务和用户现状,进一步挖掘用户行为特征信息,为决策提供数据支持。

五、分析过程:

#导入数据
import pandas as pd  
import numpy as np 
columns=['user_id','order_dt','order_products','order_amount']
df=pd.read_table('bicycle_master.txt',names=columns, sep='\s+')
  • user_id:用户ID
  • order_dt:购买日期
  • border_products:购买产品数
  • order_amount:购买金额
df.info()
image.png
df.head()
image.png
df.describe()
image.png

大部分订单只消费了少量商品(平均2.4),存在一定极值干扰
用户的消费金额比较稳定,平均消费35元,中位数在25元,有一定极值的干扰

df['order_dt']=pd.to_datetime(df.order_dt,format="%Y%m%d")
df['month']=df.order_dt.values.astype('datetime64[M]')
df['month']
image.png

对时间进行解析,一开始默认的格式是 int64的,默认会是 datetime64[ns] 类型,后面中括号表示 时间间隔是 ns
下面是对 order_dt 列(取values),转换类型为datetime64[M],默认就会是每月的第一天了,同理设置为[Y]就是每年的1月1日,然后生成新的一列 month

1.进行用户消费趋势的分析(按月)

  • 每月的消费总金额
  • 每月的消费次数
  • 每月的产品购买量
  • 每月的消费人数
grouped_month=df.groupby('month')
grouped_month_info = grouped_month[['order_amount','user_id','order_products']].agg({'order_amount':sum, 'user_id': 'count', 'order_products':sum})
grouped_month_info
image.png
grouped_month_info.rename(columns = {'order_amount':'消费金额', 'user_id': '消费次数', 'order_products': '产品购买量'}, inplace=True)
grouped_month_info.head()
grouped_month_info['消费人次'] = grouped_month['user_id'].unique().map(len)
#也可以用 grouped_month_info['消费人次'] = grouped_month['user_id'].unique().apply(lambda x:len(x))
grouped_month_info.head()
image.png
#重置索引, 将month字段转换为字符型,
grouped_month_info = grouped_month_info.reset_index()
grouped_month_info['month'] = grouped_month_info['month'].astype(str)

将处理过的数据,导入到Excel表格中

grouped_month_info.to_excel(r'.\临时表\月销售额、销售次数、产品购买量、消费人数.xlsx')

上面进行的汇总分析,其实可以用数据透视的方法更快实现,一次性求出结果

df.pivot_table(index='month',
              values=['order_products','order_amount','user_id'],
              aggfunc={'order_products':'sum',
                      'order_amount':'sum',
                      'user_id':'count'}).head()

将Excel数据导入到powerBI,可视化数据


image.png

由上图可知,消费金额在前三个月达到最高峰,后续消费较为稳定,有轻微下降趋势

image.png

由上图可知,产品购买量在前三个月达到最高峰,后续消费较为稳定,有轻微下降趋势


image.png

前三个月消费订单人数在10000笔左右,后续月份的平均消费人数则在2500人
每月消费人数低于每月消费次数,但差异不大
前三个月每月的消费人数在8000-10000之间,后续月份,平均消费人数在2000不到

去重的方法有多种,这里也可以使用数据库思想,df.groupby(['month','user_id']).count().reset_index()

2.用户个体消费分析

  • 用户消费金额,消费次数的描述统计
  • 用户消费金额和消费次数的散点图
  • 用户消费金额的分布图
  • 用户消费次数的分布图
  • 用户累计消费金额占比(百分之多少的用户占了百分之多少的消费额)
grouped_user=df.groupby('user_id')
grouped_user_info = grouped_user.sum()
grouped_user_info.head()
image.png
#用户消费金额,消费次数的描述统计
grouped_user.sum().describe()
image.png

用户平均购买了7张,但是中位值只有3,说明小部分用户购买了大量货物
用户平均消费106元,中位值有43,判断同上,有极值干扰

将处理过的数据,导入到Excel表格中

grouped_user_info.to_excel(r'.\临时表\用户个体消费行为分析.xlsx')
image.png
  • pd.cut() 利用进行切片分组
grouped_user_sum_order_amount = grouped_user.order_amount.sum()
grouped_user_sum_order_amount_lst = [i for i in range(0,int(grouped_user_sum_order_amount.max())+50,50)]
grouped_user_sum_order_amount = pd.cut(grouped_user_sum_order_amount, bins=grouped_user_sum_order_amount_lst,labels = grouped_user_sum_order_amount_lst[1:])
grouped_user_sum_order_amount
image.png

将处理过的数据,导入到Excel表格中

grouped_user_sum_order_amount.to_excel(r'.\临时表\消费金额分布直方图.xlsx')

从直方图可知,用户消费金额,绝大部分呈现集中趋势,小部分异常值干扰了判断,可以使用过滤操作排除异常
使用切比雪夫定理过滤掉异常值,因为切比雪夫定理说明,95%的数据都分布在5个标准差之内,剩下5%的极值就不要了
order_amount (mean = 106 ,std = 241) mean+5std = 1311


image.png

利用cumsum 求金额累加值

user_cumsum=grouped_user.sum().sort_values('order_amount').apply(lambda x:x.cumsum()/x.sum())
user_cumsum.tail(10)
image.png

将处理过的数据,导入到Excel表格中

# 这里 reset_index() 是为了得到一个自然数的行标签,表示的就是人数,下面的图就可以看出来多少个少占多少百分比
user_cumsum.reset_index().order_amount.to_excel(r'.\临时表\累积销售.xlsx')
image.png

按照用户消费金额进行升序排序,由图可以知道50%的用户仅贡献了11%的消费额度,而排名前5000的用户就贡献了60%的消费额度

3.用户消费行为

  • 用户第一次消费(首购)

  • 用户最后一次消费

  • 新老客户消费比

    • 多少用户仅消费一次
    • 每月新客占比
  • 用户分层

    • RFM模型
    • 新、老、活跃、回流、流失
  • 用户购买周期(按订单)

    • 用户消费周期描述
    • 用户消费周期分布
  • 用户生命周期(按第一次和最后一次消费) -用户生命周期描述

    • 用户生命周期分布
  • 用户第一次消费(首购)

grouped_user_min = grouped_user.min().order_dt.value_counts().reset_index().rename(columns={'index':'first_date'})
grouped_user_min['first_date'] =grouped_user_min['first_date'].astype(str)
grouped_user_min.head()
image.png

将处理过的数据,导入到Excel表格中

grouped_user_min.to_excel(r'.\临时表\用户首购.xlsx')
image.png
  • 用户最后一次消费
grouped_user_max = grouped_user.max().order_dt.value_counts().reset_index().rename(columns={'index':'last_date'})
grouped_user_max['last_date'] =grouped_user_max['last_date'].astype(str)
grouped_user_max.head()

将处理过的数据,导入到Excel表格中

grouped_user_max.to_excel(r'.\临时表\用户最后一次购买.xlsx')
image.png
  • 新老客户消费比
#新老客消费比
# 统计只消费了一次的用户
(user_life['min']==user_life['max']).value_counts()
image.png

有一半的用户,只消费了一次

grouped_um = df.groupby(['month','user_id']).order_dt.agg(["min","max"])    
grouped_um["new"] = (grouped_um["min"] == grouped_um["max"] )                # 新增列 True 为 新用户
grouped_um.reset_index().groupby("month").new.value_counts()
image.png
  • 用户分层
rfm=df.pivot_table(index='user_id',
                  values=['order_products','order_amount','order_dt'],
                  aggfunc={'order_dt':'max',
                          'order_amount':'sum',
                          'order_products':'count'})
rfm.head()

得到最近一次消费,一般是计算 today 距离最近一次消费,这里因为时间太久远,就随便用的max值
数值越大就越久远,分子得到的是一些天数类似 545 days(因为是时间格式相减),处以一个单位,就不会有单位了只留下数值
重命名,也就是 R :消费时间 F:消费金额 M:消费频次

rfm['R']= (rfm.order_dt - rfm.order_dt.max())/np.timedelta64(1,'D')
rfm.rename(columns={'order_products':"F",'order_amount':'M'},inplace=True)
rfm[['R','F','M']].apply(lambda x:x-x.mean())
image.png

用函数打标签

def rfm_func(x):
    level=x.apply(lambda x:'1' if x>=0 else '0')
    # level 的类型是 series,index 是 R、F、M
#     print(type(level))
#     print(level.index)
    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,传递一行得到一个 111,然后匹配返回一个值
rfm['label']=rfm[['R','F','M']].apply(lambda x:x-x.mean()).apply(rfm_func,axis=1)
rfm.loc[rfm.label=='重要价值客户','color']='重要价值客户'
rfm.loc[~(rfm.label=='重要价值客户'),'color']='非重要价值客户'
rfm.head()
image.png

将处理过的数据,导入到Excel表格中

rfm.to_excel(r'.\临时表\RFM模型.xlsx')
image.png
rfm.groupby('label').sum()
image.png
rfm.groupby('label').sum()
image.png

从RFM 分层可知,大部分用户是重要保持客户,但是这是由于极值的影响,所以 RFM 的划分标准应该以业务为准,也可以通过切比雪夫去除极值后求均值,并且 RFM 的各个划分标准可以都不一样

尽量用小部分的用户覆盖大部分的额度
不要为了数据好看划分等级

  • 用户生命周期
    新客,活跃,回流,流失(一段时间不消费,或者不活跃)
# 数据透视, userid为索引,月为列,求每月的消费次数,这里填充了
pivoted_counts=df.pivot_table(index='user_id',
                             columns='month',
                             values='order_dt',
                             aggfunc='count').fillna(0)
pivoted_counts.head()
image.png
# 转变一下消费,有消费为1,没有消费为0
df_purchase=pivoted_counts.applymap(lambda x:1 if x>0 else 0)
df_purchase.tail()
image.png
# 这里由于进行数据透视,填充了一些 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             

若本月没有消费,这里只是和上个月判断是否注册,有缺陷,可以判断是否存在就可以了
若之前是未注册,则依旧为未注册
若之前有消费,则为流失/不活跃
其他情况,为未注册

若本月有消费
若是第一次消费,则为新用户
如果之前有过消费,则上个月为不活跃,则为回流
如果上个月为未注册,则为新用户
初次之外,为活跃

return:回流
new:新客
unreg:未注册
active:活跃

purchase_stats=df_purchase.apply(lambda x: pd.Series(active_status(x),index=df_purchase.columns),axis=1)
purchase_stats.head()
image.png
# 这里把未注册的替换为空值,这样 count 计算时不会计算到
# 得到每个月的用户分布
purchase_stats_ct=purchase_stats.replace('unreg',np.NaN).apply(lambda x: x.value_counts())
purchase_stats_ct
image.png
returnratee=purchase_stats_ct.apply(lambda x:x/x.sum(),axis=0)
returnratee
image.png
purchase_stats_ct_info = purchase_stats_ct.fillna(0).T
purchase_stats_ct_info
image.png

将处理过的数据,导入到Excel表格中

purchase_stats_ct_info.index = purchase_stats_ct_info.index.astype(str)
purchase_stats_ct_info.to_excel(r'.\临时表\用户分层-新、活跃、流失、回流.xlsx')
image.png
purchase_stats_ct_T = purchase_stats_ct.fillna(0).T.apply(lambda x:x/x.sum(),axis=1)
purchase_stats_ct_T
image.png
  • 用户购买周期(按订单)
order_diff=grouped_user.apply(lambda x:x.order_dt-x.order_dt.shift())
order_diff.head(10)
image.png
order_diff_info = (order_diff/np.timedelta64(1,'D'))
order_diff_cut_lst = [i for i in range(0,int(order_diff_info.max())+1,10)]
order_diff_info_hist = pd.cut(order_diff_info,bins=order_diff_cut_lst,labels=order_diff_cut_lst[1:])
order_diff_info_hist
image.png
order_diff_info_hist = order_diff_info_hist.fillna(10)
order_diff_info_hist.to_excel(r'.\临时表\用户购买周期时间差频率直方图.xlsx')
image.png

订单周期呈指数分布
用户的平均购买周期是68天
绝大部分用户的购买周期都低于100天

  • 用户生命周期(按第一次和最后一次消费)
user_life.head()
image.png
# 用户生命周期(按第一次和最后一次消费)
(user_life['max']-user_life['min']).describe()
image.png
user_life_info = ((user_life['max']-user_life['min'])/np.timedelta64(1,"D"))
user_life_lst = [i for i in range(0,int(user_life_info.max())+1,10)]
user_life_info_hist = pd.cut(user_life_info,bins=user_life_lst,labels=user_life_lst[1:])
user_life_info_hist
image.png
user_life_info_hist_2 = user_life_info_hist.fillna(10)
user_life_info_hist_2.to_excel(r'.\临时表\用户生命周期频率直方图.xlsx')
image.png

用户的生命周期受只购买一次的用户影响比较厉害(可以排除)
用户均消费134天,中位数仅0天

user_life["差值"]=(user_life["max"] - user_life["min"])/np.timedelta64(1,"D")
user_life.head(5)
image.png
user_life_info_hist
image.png
user_life_info_hist.to_excel(r'.\临时表\用户生命周期频率直方图(忽略一次购买).xlsx')
image.png

4.复购率和回购率分析

  • 复购率
    • 自然月内,购买多次的用户占比(即,购买了两次以上)
  • 回购率
    • 曾经购买过的用户在某一时期的再次购买的占比(可能是在三个月内)
purchase_r=pivoted_counts.applymap(lambda x: 1 if x>1 else np.NaN if x==0 else 0)
# 区分一个,和一个以上的情况,以便于计算复购率,大于1为1,等于0 为0,等于1为0
purchase_r=pivoted_counts.applymap(lambda x: 1 if x>1 else np.NaN if x==0 else 0)
purchase_r.head()
image.png
purchase_r_reshop = (purchase_r.sum()/purchase_r.count()).reset_index(name = 'reshop')
purchase_r_reshop
image.png
purchase_r_reshop['month'] = purchase_r_reshop['month'].astype(str)
purchase_r_reshop.to_excel(r'.\临时表\复购人数与总消费人数比例.xlsx')
image.png

复购率稳定在20%所有,前一个月因为有大量新用户涌入,而这批用户只购买了一次,所以导致复购率降低

  • 回购率,知道是否消费就可以了
df_purchase.head()
image.png
# 需要使用函数来判断是否回购:当月消费过的用户下个月也消费了叫做回购,这个定义可以改变
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
indexs = df_purchase.columns
indexs=df['month'].sort_values().astype('str').unique()
purchase_b = df_purchase.apply(lambda x :pd.Series(purchase_back(x),index = indexs),axis =1)
purchase_b.head()
purchase_b_backshop = purchase_b.sum()/purchase_b.count()
purchase_b_backshop.index = purchase_b_backshop.index.astype(str)
purchase_b_backshop.to_excel(r'.\临时表\回购率.xlsx')
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容