CDNOW网站用户消费行为分析

数据来源于CDNow的一份用户购买CD明细,包含了用户ID、购买日期、购买数量、购买金额四个字段。

分析目的

  • 按月度进行消费趋势分析
  • 用户个体消费分析
  • 用户消费行为分析
  • 复购率和回购率分析

数据导入

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

# 正常显示中文
plt.rcParams['font.sans-serif'] = ['SimHei']
# 坐标轴显示负数
plt.rcParams['axes.unicode_minus'] = False
# 使用R语言绘图风格
plt.style.use('ggplot')

读取数据

df = pd.read_table('CDNOW_master.txt', sep='\s+')
df.head()
image.png

观察数据后发现,数据并没有字段名,需要手动进行添加。

columns = ['user_id', 'order_dt', 'order_products', 'order_amount']
df.columns = columns
df.head()
image.png

查看是否存在缺失值

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 69658 entries, 0 to 69657
Data columns (total 4 columns):
user_id           69658 non-null int64
order_dt          69658 non-null int64
order_products    69658 non-null int64
order_amount      69658 non-null float64
dtypes: float64(1), int64(3)
memory usage: 2.1 MB

通过观察发现,并没有空行存在,但是order_dt应为日期类型,此处为int类型,需要进行数据类型转换。

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

描述性统计

df.describe()
image.png

平均每笔订单购买2.4张cd,标准差为2.33,稍有波动,每笔订单平均消费35.89元,购买量上四分位数为3张,说明大部分用户订单购买量不大,消费金额呈现类似趋势,单笔订单最大值为99,说明有少部分狂热打榜粉丝,一般消费类的数据都是呈现长尾形态,即大部分用户都是小额消费者,小部分用户贡献了主要的消费收益,俗称二八理论。

按月度进行消费趋势分析

每月的消费总金额

grouped_month = df.groupby('month')
order_month_amount = grouped_month.order_amount.sum()
order_month_amount
month
1997-01-01    299048.40
1997-02-01    379590.03
1997-03-01    393155.27
1997-04-01    142824.49
1997-05-01    107933.30
1997-06-01    108395.87
1997-07-01    122078.88
1997-08-01     88367.69
1997-09-01     81948.80
1997-10-01     89780.77
1997-11-01    115448.64
1997-12-01     95577.35
1998-01-01     76756.78
1998-02-01     77096.96
1998-03-01    108970.15
1998-04-01     66231.52
1998-05-01     70989.66
1998-06-01     76109.30
Name: order_amount, dtype: float64

可视化

plt.figure(figsize=(10, 5))
order_month_amount.plot()
plt.title('每月消费总额趋势图')
image.png

前3个月为消费金额的高峰,随后在第4个月下跌,后趋于平稳,总体上呈现下降趋势。

每月订单数量

order_month_count = grouped_month.user_id.count()
order_month_count
month
1997-01-01     8927
1997-02-01    11272
1997-03-01    11598
1997-04-01     3781
1997-05-01     2895
1997-06-01     3054
1997-07-01     2942
1997-08-01     2320
1997-09-01     2296
1997-10-01     2562
1997-11-01     2750
1997-12-01     2504
1998-01-01     2032
1998-02-01     2026
1998-03-01     2793
1998-04-01     1878
1998-05-01     1985
1998-06-01     2043
Name: user_id, dtype: int64

可视化

plt.figure(figsize=(10, 5))
order_month_count.plot()
plt.title('每月订单数量趋势图')
image.png

前3个月为订单量的高峰,都在1万左右,第四个月急剧下跌,后趋于平稳,总体趋势和每月消费总额趋势一致。猜测前三个月应为新专辑发布或促销活动。

每月用户量

user_month_count = grouped_month.user_id.apply(lambda x:len(x.drop_duplicates()))
user_month_count

可视化

plt.figure(figsize=(10, 5))
user_month_count.plot()
plt.title('每月用户数量趋势图')
image.png

前3个月用户量大约在9000左右浮动,第4个月开始用户量急剧下跌,最终每月用户量维持在2000左右。

每月用户消费平均金额

# 每月总消费额 / 每月用户数量
mean_order_amount = order_month_amount / user_month_count
mean_order_amount
month
1997-01-01    38.119618
1997-02-01    39.405173
1997-03-01    41.280478
1997-04-01    50.611088
1997-05-01    48.750361
1997-06-01    46.342826
1997-07-01    55.999486
1997-08-01    49.868900
1997-09-01    47.124094
1997-10-01    48.820430
1997-11-01    56.927337
1997-12-01    51.275402
1998-01-01    49.939349
1998-02-01    49.707905
1998-03-01    52.898131
1998-04-01    46.090132
1998-05-01    47.708105
1998-06-01    50.537384
dtype: float64

可视化

plt.figure(figsize=(10, 5))
mean_order_amount.plot()
plt.title('每月用户平均消费金额趋势图')
image.png

前3个月用户平均消费金额在40元左右,后续有所上升,最终在46-57范围内波动。

用户平均消费次数

# 总消费次数 / 每月用户数量
mean_order_count = grouped_month.user_id.count() / user_month_count
mean_order_count
1997-01-01    1.137922
1997-02-01    1.170144
1997-03-01    1.217766
1997-04-01    1.339830
1997-05-01    1.307588
1997-06-01    1.305686
1997-07-01    1.349541
1997-08-01    1.309255
1997-09-01    1.320299
1997-10-01    1.393148
1997-11-01    1.356016
1997-12-01    1.343348
1998-01-01    1.322056
1998-02-01    1.306254
1998-03-01    1.355825
1998-04-01    1.306889
1998-05-01    1.334005
1998-06-01    1.356574

可视化

plt.figure(figsize=(10, 5))
mean_order_count.plot()
plt.title('每月用户平均消费次数趋势图')
image.png

平均消费次数总体呈上升趋势,前3个月在1.25次以下,后续几个月在1.35次附近波动。

用户个体消费分析

消费商品数与消费总金额的描述性统计

grouped_user = df.groupby('user_id')
grouped_user.sum().describe()
image.png

从用户角度看,单个用户平均购买cd数量为7个,中位数为3个,标准差为16.98;平均消费金额为106.8,中位数为43.4,标准差为240.9。明显存在极大值干扰,存在一小部分高消费人群。总体上符合二八法则。

消费金额与消费商品数的散点图

plt.figure(figsize=(10, 5))
grouped_user.sum().query('order_amount < 4000').plot.scatter(x='order_amount', y='order_products')
plt.title('消费金额与消费产品数散点图')
image.png

可以看出,消费金额与消费产品数量呈线性关系。

用户消费金额分布图

plt.figure(figsize=(10, 5))
grouped_user.sum().order_amount.plot.hist(bins=20)
plt.title('用户消费金额分布')
image.png

极大部分用户集中在2000元以下,整体消费能力不高,高消费用户数量极少,符合消费市场行业规律。
过滤掉消费数量大于100的订单,减小极大值影响。

plt.figure(figsize=(10, 5))
grouped_user.sum().query('order_products < 100').order_amount.hist(bins=20)
plt.title('用户消费金额分布(过滤后)')
image.png

可以看出,过滤掉消费数量大于100的订单后,大部分用户的消费金额都小于250,高消费用户不多,符合市场规律。

用户消费次数的分布图

plt.figure(figsize=(10, 5))
grouped_user.count().query('order_products < 100').order_dt.hist(bins=40)
plt.title('用户消费次数分布图')
image.png

从图上看大部分用户只消费1次两次,高频消费者很少这也满足cd消费市场行业规律。

累计消费金额占比

plt.figure(figsize=(10, 5))
user_cumsum = grouped_user.sum().sort_values(by='order_amount', ascending=False).apply(lambda x: x.cumsum() / x.sum())
user_cumsum.reset_index().order_amount.plot()
plt.title('消费金额累计百分比')
image.png

按照用户消费金额进行降序排序,由图可知前30-40%的用户贡献了80%的消费额。只需要重点维护这写用户,就可以把业绩kpi完成80%。

用户消费行为分析

用户首次消费

grouped_user.order_dt.min().value_counts().plot(figsize=(10, 5))
plt.title('用户首次消费')
image.png

用户首次消费集中在前3个月,2月10日至2月25日有较为强烈的波动。

用户最后一次消费

grouped_user.order_dt.max().value_counts().plot(figsize=(10, 5))
plt.title('用户最后一次消费')

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

新老客户消费比

计算只买了一次的消费群体

grouped_user.count().query('order_dt == 1').order_dt.count()
11907

总消费人数

grouped_user.count().reset_index().user_id.count()
23569

大部分人只消费一次。

grouped_month_user = df.groupby(['month', 'user_id'])
tmp = grouped_month_user.order_dt.agg(['min']).join(grouped_user.order_dt.min())
tmp['new'] = (tmp['min'] == tmp.order_dt)
tmp.reset_index().groupby('month').new.apply(lambda x: x.sum() / x.count()).plot(figsize=(10, 5))
plt.title('新用户占比')
image.png

可以看出,只有前3个月有新客户,后续都是老客户进行消费。

用户分层

使用RFM模型对用户分层。
构建rfm透视表。

rfm = df.pivot_table(index='user_id', 
                     values=['order_dt', 'order_amount', 'order_products'],
                     aggfunc={'order_dt': 'max', 'order_products': 'sum', 'order_amount': 'sum'})
rfm.head()
image.png

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.head()
image.png

用户分层

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.head()
image.png

计算每层客户的R、F、M的和

rfm.groupby('label').sum()
image.png

绝大部分消费是由重要保持客户产生的,维持好这部分客户,完成kpi有极大的帮助。

rfm.groupby('label').count()
image.png

一般发展客户基数较大,可对这部分客户进行挽留以保持高额增长。

rfm.loc[rfm.label == '重要保持客户', 'color'] = 'r'
rfm.loc[~(rfm.label == '重要保持客户'), 'color'] = 'g'
rfm.plot.scatter('F', 'R', c=rfm.color, figsize=(10, 5))
plt.title('F-R')
image.png

从RFM分层可知,大部分用户为重要保持客户,但是这是由于极值的影响,所以RFM的划分标准应该以业务为准。尽量用小部分的用户覆盖大部分的份额,不要为了数据好看划分等级。

用户生命周期

pivoted_counts = df.pivot_table(index='user_id',
                               columns='month', 
                               values='order_dt',
                               aggfunc='count').fillna(0)
pivoted_counts.head()
image.png

用户每个月的消费次数,对于生命周期的划分只需要知道用户本月是否消费,消费次数在这里并不重要,需要将模型进行简化。使用数据透视表,需要明确获得什么结果。有些用户在某月没有进行过消费,会用NaA表示,这里用fillna填充。

df_purchase = pivoted_counts.applymap(lambda x: 1 if x > 0 else 0)
df_purchase.tail()
image.png

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

columns_month = df.groupby('month').sum().reset_index().month
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('new')
    return pd.Series(status, index=columns_month)
pivoted_status = df_purchase.apply(active_status, axis=1)
pivoted_status.head()
image.png

每月不同活跃用户计数

purchase_status_ct = pivoted_status.replace('unreg', np.NaN).apply(lambda x: pd.value_counts(x))
purchase_status_ct
image.png
plt.figure(figsize=(10, 5))
purchase_status_ct.fillna(0).T.plot.area()
plt.title('各类用户面积分布图')
image.png

用户购买周期

order_diff = grouped_user.apply(lambda x: x.order_dt - x.order_dt.shift()).reset_index()
order_diff
user_id   
2        0        NaT
         1     0 days
3        2        NaT
         3    87 days
         4     3 days
         5   227 days
         6    10 days
         7   184 days
4        8        NaT
         9    17 days
Name: order_dt, dtype: timedelta64[ns]
plt.figure(figsize=(10, 5))
(order_diff / np.timedelta64(1, 'D')).hist(bins=20)
plt.title('用户消费周期分布')
image.png
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

平均消费周期为68天,绝大部分用户购买周期在100天以内。

用户生命周期

user_life = grouped_user.order_dt.agg(['min', 'max'])
user_life.head()
image.png
plt.figure(figsize=(10, 5))
((user_life['max'] - user_life['min']) / np.timedelta64(1, 'D')).hist(bins=40)
plt.title('用户生命周期分布')
image.png

用户生命周期受只购买一次的客户影响较大,此处应该排除掉。

plt.figure(figsize=(10, 5))
u_1 = (user_life['max'] - user_life['min']) / np.timedelta64(1, 'D')
u_1[u_1 > 0].hist(bins=40)
plt.title('用户生命周期分布(删除之后买一次用户后)')
image.png

这是双峰趋势图。部分质量差的用户,虽然消费了两次,但是仍旧无法持续,在用户首次消费30天内应该尽量引导。少部分用户集中在50天~300天,属于普通型的生命周期,高质量用户的生命周期,集中在400天以后,这已经属于忠诚用户了。

复购率和回购率分析

复购率

所谓复购率就是在一个自然月内,购买多次的用户占比。

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.sum() / purchase_r.count()).plot(figsize=(10, 5))
plt.title('复购率')
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)
    status.append(np.NaN)
    return pd.Series(status, index=columns_month)
purchase_b = df_purchase.apply(purchase_back, axis=1)
purchase_b.head()
image.png
(purchase_b.sum() / purchase_b.count()).plot(figsize=(10, 5))
plt.title('回购率')
image.png

回购率在30%左右,新客的整体质量低于老客,老客的忠诚度(回购率)表现较好。

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

推荐阅读更多精彩内容

  • 目录一.项目背景与数据来源二、提出问题三. 数据处理3.1 导入数据--3.1.1 导入常用的库--3.1.2 导...
    全糖布丁烤奶阅读 3,824评论 1 15
  • 该数据来源于某网站的消费记录,现针对该数据对用户的消费趋势及消费行为进行分析。链接:https://pan.bai...
    Runningbetter阅读 1,168评论 0 2
  • 前言:本文数据量来源于网上,是一份CD的消费数据,数据链接会放在文章最后,请需要者自取。本文分析的主要工具为:Py...
    黑哥666阅读 4,152评论 2 7
  • 前言:本文数据量来源于网上,是一份CD的消费数据,数据链接会放在文章最后,请需要者自取。本文分析的主要工具为:Py...
    雅_2f4f阅读 1,776评论 0 7
  • 先说说为什么要读李嘉诚传吧!其实是自己有意识的想要多看些人物传记,觉得自己不够自信,不够强大,需要学习;觉得他们成...
    露珠DJR阅读 256评论 0 0