• 2

  • 285

数据分析项目-用户消费行为分析

perpetual

python交流

3 days ago

数据来源:CDNow网站的用户购买明细。
共有用户ID,购买日期,购买数量,购买金额四个字段。

1. 数据预处理

import pandas as pd
import numpy as np
# 加载数据可视化包
import matplotlib.pyplot as plt
# 可视化显示在页面
%matplotlib inline 
# 更改设计风格
plt.style.use('ggplot')

columns = ['user_id','order_dt','order_products','order_amount']
df = pd.read_table('CDNOW.txt',names=columns,sep='\s+')
复制代码
  • read_table打开,原始数据不包含表头,所以需要赋予字段名。
    如果文本中的分割符既有空格又有制表符(‘/t’),sep参数用‘/s+’,可以匹配任何空格。

    - user_id: 用户id
    - order_dt: 购买日期
    - order_products: 购买数量
    - order_amount: 购买金额
    复制代码

消费行业或电商行业一般是通过订单数,订单额,购买日期,用户ID这四个字段来分析。

df.head()
复制代码

观察数据,判断数据是否正常识别。值得注意的是一个用户可能在一天内购买多次,用户ID为2的用户在1月12日买了两次,这个细节不要遗漏。

df.info()
复制代码

  • 查看数据是否存在空值:原数据没有空值。
    如果存在空值,处理方法见数据分析-缺失值处理方法总结

  • 查看数据类型:原数据中order_dt为int类型,需要转化为datetime。
    进行数据处理的时候,经常会遇见数据类型的问题,拿到数据时,首先要确定拿到的是正确的数据类型,如果数据类型不正确需要进行数据类型的转化,再进行数据处理。

#数据类型转换
df['order_dt'] = pd.to_datetime(df.order_dt,format="%Y%m%d")
df['month'] = df.order_dt.values.astype('datetime64[M]')
复制代码
  • Y表示4位数的年份,如1990;y表示2位数,如90.
df.describe()
复制代码

  • 大部分订单只消费了少量商品(平均2.4),有一定极值干扰。
  • 用户消费金额比较稳定,平均消费35.89元,中位数是25.98,有一定极值干扰。
  • 一般而言,消费类的数据分布,都是长尾形态。大部分用户都是小额,然而小部分用户贡献了收入的大头,俗称二八。

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

2.1 每月的消费总金额

grouped_month = df.groupby(by='month')
order_month_amount = grouped_month.order_amount.sum()
order_month_amount.head()
复制代码

order_month_amount.plot()
复制代码

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

2.2 每月的消费订单数

comsume_month_count = grouped_month.user_id.count().plot()
复制代码

前三个月消费订单数在10000笔左右,后续月份的平均消费订单数则在2500左右。

2.3 每月的产品购买量

grouped_month.order_products.sum().plot()
复制代码

每月的产品购买量一样呈现早期购买量多,后期平稳下降的趋势。为什么会呈现这个原因呢?我们假设是用户身上出了问题,早期时间段的用户中有异常值,第二假设是各类促销营销,但这里只有消费数据,所以无法判断。

2.4 每月的消费人数

方法一:
user_month = df.groupby('month').user_id.apply(lambda x:len(x.drop_duplicates()))
user_month.plot()

方法二:
groupby_user = df.groupby(['month','user_id']).count().reset_index()[['month','user_id']]
groupby_user.groupby('month').count().plot()
复制代码

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

2.5 更简便的方法:数据透视

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

数据透视表是更简单的方法,更加快捷。实际操作中具体看哪个方便简单。

2.6 每月用户平均消费金额的趋势

avg_month_amount = order_month_amount/user_month
avg_month_amount.plot()
复制代码

2.7 每月用户平均消费次数的趋势

avg_month_count = comsume_month_count/user_month
avg_month_count.plot()
复制代码

3. 用户个体消费分析

  • 用户消费金额、消费次数的描述统计
  • 用户消费金额和消费次数的散点图
  • 用户消费金额的分布图
  • 用户消费次数的分布图
  • 用户累计消费金额占比(百分之多少的用户占了百分之多少的消费额)

3.1 用户消费金额、消费次数的描述统计

grouped_user = df.groupby(by='user_id')
grouped_user.sum().describe()
复制代码

  • 从用户角度看,每位用户平均购买7张CD,最多购买了1033张。用户的平均消费金额(客单价)106元,标准差是240,结合分位数和最大值看,平均值和75分位接近,肯定存在小部分的高额消费用户。
  • 用户平均消费106元,中位数只有43,判断同上,有极值干扰

3.2 用户消费金额和消费次数的散点图

grouped_user.sum().plot.scatter(x='order_amount',y='order_products')
复制代码

受少数极值影响,散点图下方大部分点堆积在一起。同时我们可以看到大部分点横坐标是小于4000的,所以我们可以做一个筛选。

grouped_user.sum().query('order_amount<4000').plot.scatter(x='order_amount',y='order_products')
复制代码

每个用户购买CD的次数和金额呈现强烈的正比关系。这很容易理解,CD的单价波动范围较小。上图反应了商品单价,如果商品种类丰富,单价范围较广,可能会呈现扇形扩散形状。

3.3 用户消费金额的分布图

grouped_user.sum().order_amount.plot.hist(bins=20)
复制代码

从直方图可知,用户消费金额绝大部分呈现集中趋势,小部分异常值干扰了判断。可以使用过滤操作排除异常。

3.4 用户消费次数的分布图

grouped_user.sum().query('order_products<100').order_products.plot.hist(bins=40)
复制代码

  • 潜意识:消费相关的数据,基本上都是这种类型的数据分布。小部分用户消费金额占大头。
  • 使用切比雪夫定理过滤掉异常值,计算95%的数据的分布情况。如上面购买产品数<100是如何确定的,均值7+标准差17*5=92

3.5 用户累计消费金额占比(百分之多少的用户占了百分之多少的消费额)

#cumsum滚动累加求和
user_cumsum = grouped_user.sum().sort_values('order_amount').apply(lambda x:x.cumsum()/x.sum())
user_cumsum.reset_index().order_amount.plot()
复制代码

按用户消费金额进行升序排列,由图可知50%的用户仅贡献了15%的消费额度。而排名前5000的用户就贡献了60%的消费额。
也就是说我们只要维护了这5000个用户就可以把业绩KPI完成60%,如果能把5000个用户运营的更好就可以占比70%—80%之间。

4. 用户消费行为

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

在很多行业里面首购是一个很重要的维度,它和渠道息息相关,尤其是针对客单价比较高客户留存率比价低的行业,第一次客户从哪里来可以拓展出很多运营方式。

#求月份的最小值,即用户消费行为中的第一次消费时间
grouped_user.min().order_dt.value_counts().plot()
复制代码

用户第一次购买分布,集中在前三个月。其中,在2月11日至2月25日有一次剧烈的波动。
具体原因可以做假设,比如渠道发生了变化,比如奖励激励的方式发生了变化,还比如2月份可能和春节相关,但国外没有春节,可以排除掉。总之就是可以罗列出123点去分析。

4.2 用户最后一次消费

用户最后一次消费和流失相关

grouped_user.max().order_dt.value_counts().plot()
复制代码

有明显的断崖式下跌,前面3个月新用户来的比较多,后面几个月没有新增,主要是老客户(熟客)的消费,从侧面说明绝大部分用户只消费了一次就不再消费。

用户最后一次购买的分布比第一次分布广,大部分最后一次购买集中在前三个月,说明有很多用户购买了一次后就不再进行购买。随着时间的递增,最后一次购买数也在递增,消费呈现流失上升的状况,用户忠诚度在慢慢下降。

4.3 新老客消费比(多少用户仅消费了一次)

user_life = grouped_user.order_dt.agg(['min','max'])
user_life.head()
复制代码

(user_life['min'] == user_life['max']).value_counts()
复制代码

有一半用户仅消费了一次

4.4 用户分层

4.4.1 RFM

RFM模型通过一个客户的近期购买行为、购买的总体频率以及花了多少钱3项指标来描述该客户的价值状况。

- 最近一次消费 (Recency)
- 消费频率 (Frequency)  F值越大,表示客户交易越频繁,反之则表示客户交易不够活跃。  
- 消费金额 (Monetary)   M值越大,表示客户价值越高,反之则表示客户价值越低。
复制代码
rfm = df.pivot_table(index = 'user_id',
                     values = ['order_products','order_amount','order_dt'],
                     aggfunc = {'order_dt':'max',
                                'order_amount':'sum',
                                'order_products':'sum'})
rfm.head()
#order_amount消费总金额,order_products消费总产品数,order_products最近一次消费时间
复制代码

rfm距今 天数

rfm['R']=(rfm.order_dt.max()-rfm.order_dt)/np.timedelta64(1,'D')  #除以单位,变成数值
rfm.rename(columns = {'order_products':'F','order_amount':'M'},inplace = True)
rfm.head()
复制代码

#将RFM区分为不同维度,高于均值,低于均值
rfm[['R','F','M']].apply(lambda x:x-x.mean())
复制代码

def rfm_func(x):
    level = x.apply(lambda x:'1' if x>=0 else '0')
    label = level.R + level.F + level.M
    d = {
         '111':'重要价值客户',
         '011':'重要保持客户',
         '101':'重要挽留客户',
         '001':'重要发展客户',
         '110':'一般价值客户',
         '010':'一般保持客户',
         '100':'一般挽留客户',
         '000':'一般发展客户'
    }
    result = d[label]
    return result

rfm['label'] = rfm[['R','F','M']].apply(lambda x:x-x.mean()).apply(rfm_func,axis=1)
rfm
复制代码

用户分层计数

rfm.loc[rfm.label == '重要价值客户','color'] = 'g'
rfm.loc[~(rfm.label == '重要价值客户'),'color'] = 'r'
rfm.plot.scatter('F','R',c=rfm.color)
复制代码

rfm.groupby('label').sum()
复制代码

从RFM分层可知,大部分用户为重要保持客户,但是这是由于极值的影响,所以RFM的划分标准应该以业务为准

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

4.4.2 用户生命周期(新、老、活跃、回流、流失)

pivoted_counts = df.pivot_table(index = 'user_id',
                                columns = 'month',
                                values = 'order_dt',
                                aggfunc = 'count').fillna(0)
pivoted_counts.head()
复制代码

生命周期的划分只需要知道用户本月是否消费,消费次数在这里并不重要,需要将模型进行简化
有些用户在某月没有进行过消费,会用NaA表示,这里用filna填充。

df_purchase = pivoted_counts.applymap(lambda x: 1 if x>0 else 0)
df_purchase.tail()
复制代码

对于尾部数据,user_id2W+的数据是有问题的,因为从实际的业务场景上说,他们一月和二月都没有注册三月份才是他们第一次消费。透视会把他们一月和二月的数据补上为0,这里面需要进行判断将第一次消费作为生命周期的起始,不能从一月份开始就粗略的计算。

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 pd.Series(status, index = df_purchase.columns)
复制代码

主要分为两部分的判断,以本月是否消费为界。
若本月没有消费

  • 若之前是未注册,则依旧为未注册
  • 若之前有消费,则为流失/不活跃
  • 其他情况,为未注册

若本月有消费

  • 若是第一次消费,则为新用户
  • 若之前有过消费,且上个月为不活跃,则为回流
  • 若上个月为未注册,则为新用户
  • 除此之前,为活跃

业内主流写法
这里用户生命周期的状态变化是用数据透视表一次性做的,但在实际业务场景中我们可能用SQL把它作为中间表来处理。我们有了明细表,会通过明细表来计算出状态表;也就是它的数据上个月是什么样的情况得出来,比如上个月是新用户或者回流用户,我们直接用上个月的状态left join本月的状态。直接用SQL进行对比
可以用pandas将每个月的状态计算出来,不是逐行而是月份计算,先算出一月份哪些用户是新购买的,然后判断二月份是否购买,两者left join

purchase_status = df_purchase.apply(active_status,axis = 1)
purchase_status.tail()
复制代码

#每月不同活跃用户的计数
purchase_status_ct = purchase_status.replace('unreg',np.NaN).apply(lambda x:pd.value_counts(x))
purchase_status_ct
复制代码

purchase_status_ct.fillna(0).T.head()
#purchase_status_ct.fillna(0).T.plot.area()
复制代码

purchase_status_ct.fillna(0).T.apply(lambda x:x/x.sum(),axis=1)
复制代码

由上表可知,每月的用户消费状态变化

  • 活跃用户,持续消费的用户,对应的是消费运营的质量
  • 回流用户,之前不消费本月才消费,对应的是唤回运营
  • 不活跃用户,对应的是流失

4.5 用户购买周期(按订单)

4.5.1 用户消费周期描述

order_diff = grouped_user.apply(lambda x:x.order_dt - x.order_dt.shift())
order_diff.head(10)
#将用户分组后,每个用户的订单购买时间进行错位相减
复制代码

user_id 1为空值,表示该客户只购买过一个订单。user_id为2 的用户第二笔订单与第二笔订单在同一天购买。

4.5.2 用户消费周期分布

(order_diff / np.timedelta64(1,'D')).hist(bins=20)
复制代码

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

4.6 用户生命周期(按第一次&最后一次消费)

4.6.1 用户生命周期描述

(user_life['max'] - user_life['min']).describe()
复制代码

数据偏移比较大,中位数是0天也就是超过50%的用户生命周期是0天只购买了一次,但是平均生命周期有134天,最大值是544天。

4.6.2 用户生命周期分布

((user_life['max'] - user_life['min']) / np.timedelta64(1,'D')).hist(bins=40)
复制代码

  • 用户的生命周期受只购买一次的用户影响比较厉害(可以排除)
  • 用户均消费134天,中位数仅0天
#过滤只购买一次的用户
u_1 = ((user_life['max'] - user_life['min']).reset_index()[0] / np.timedelta64(1,'D'))
u_1[u_1 > 0].hist(bins = 40)
复制代码

筛选出lifetime>0,既排除了仅消费了一次那些人,有不少用户生命周期靠拢在0天,部分质量差的用户虽然消费了两次,但是仍然无法持续,在用户首次消费30天内应该尽量引导。少部分用户集中在50—300天,属于普通型的生命周期。高质量用户的生命周期,集中在400天以后,这属于忠诚用户。

5. 复购率和回购率分析

复购率:自然月内,购买多次的用户占比
回购率:曾经购买过的用户在某一时期内的再次购买占比

5.1 复购率

pivoted_counts
复制代码

purchase_r = pivoted_counts.applymap(lambda x: 1 if x > 1 else np.NaN if x==0 else 0)
purchase_r.head()
复制代码

(purchase_r.sum() / purchase_r.count()).plot(figsize = (10,4))
复制代码

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

用sum和count相除即可计算出复购率。因为这两个函数都会忽略NAN,而NAN是没有消费的用户,count不论是0还是1都会统计,所以是总的消费用户数,而sum求何计算了两次以上的消费用户。这里用了比较巧妙的替代法计算复购率,SQL中也可以用。
图上可以看出复购率在早期因为大量新用户加入的关系,新客的复购率并不高,譬如1月新客们的复购率只有6%左右。而在后期,这时的用户都是大浪淘沙剩下的老客户,复购率比较稳定,在20%左右。单看新客和老客,复购率有三倍左右的差距。

5.2 回购率

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

df_purchase.head()
复制代码

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)
    status.append(np.NaN)
    return status
复制代码
purchase_b = df_purchase.apply(purchase_back,axis=1)
purchase_b.head()
复制代码

  • 0代表当月消费过次月没有消费过,1代表当月消费过次月依然消费。
  • 新建一个判断函数。data是输入数据,既用户在18个月内是否消费的记录,status是空列表,后续用来保存用户是否回购的字段。因为有18个月,所以每个月都要进行一次判断,需要用到循环。if的主要逻辑是,如果用户本月进行过消费,且下月消费过,记为1,没有消费过是0.本月若没有进行过消费,为NAN,后续的统计中进行排除。apply函数应用在所有行上,获得想要的结果。
  • 最后计算和复购率大同小异,用count和sum求出,从图中可以看出,用户的回购率高于复购,约在30%左右,和老客户差异不大。从回购率和复购率综合分析可以得出,新客的整体质量低于老客,老客的忠诚度(回购率)表现较好,消费频次稍次,这是CDNow网站的用户消费特征。
免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

Python

285

Relevant articles

未登录头像

No more data