数据分析:CD网站用户消费行为分析

流程

1.销量和金额的月份趋势:折线图
2.每笔订单以及按用户分组:散点图
3.用户消费水平:直方图
4.用户首月和最后一次消费的月份统计
5.复购率:数据透视表
6.回购率:函数定义
7.用户分层:面积图;各层用户占比:折线图
8.用户消费贡献:折线图
9.用户生命周期:直方图
10.留存率:函数定义、分组、柱状图
11.用户平均购买周期:直方图

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt 
from datetime import datetime
%matplotlib inline
#是用于在jupyter下将那些用matplotlib绘制的图显示在页面里而不是弹出一个窗口
plt.style.use('ggplot')
columns = ['id','dt','products','amount']
df = pd.read_csv('CDNOW_master.txt',names = columns,sep = '\s+')#sep表示匹配分隔符,\s+表示一个或多个空白字符(这这里指空格)
df.info()
df.describe()

平均用户购买7张,最多的购买了1033张;平均消费金额为106,标准差为240;平均值和75分位数接近,再加上最大值判断 存在小部分高额消费用户

df['date'] = pd.to_datetime(df.dt,format = '%Y%m%d')
df['month'] = df.date.values.astype('datetime64[M]')

df.info()
user_group = df.groupby('id').sum()
user_group.describe()
df.groupby('month').products.sum().plot()
df.groupby('month').amount.sum().plot()
df.plot.scatter(x = 'amount',y = 'products')
df.groupby('id').sum().plot.scatter(x = 'amount',y = 'products')

前几个月销量非常高,相比于后期数据显得异常,后期则较为平稳



与销量趋势一样,考虑可能是有季度促销活动的原因,也不能排除用户数据异常值



看到订单极值并不多,应该不是他们导致的异常波动

用户购买数量也没有太多极值
plt.figure(figsize = (12,4))
plt.subplot(121)
df.amount.hist(bins = 30)
plt.subplot(122)
df.products.hist(bins = 30)

从直方图看出也看出大部分用户的消费水平并不高



分组用户,求月份的最小值:用户消费行为中第一次的消费时间,集中在前三个月
用户最后一次消费时间,依然集中在前三个月,后续依然存在用户消费但缓慢减少

#复购率
pi_counts = df.pivot_table(index = 'id',columns = 'month',
                          values = 'dt',aggfunc = 'count').fillna(0)
pi_counts.columns = df.month.sort_values().astype('str').unique()

pi_counts_t = pi_counts.applymap(lambda x:1 if x>1 else (np.NaN if x==0 else 0))

(pi_counts_t.sum() / pi_counts_t.count()).plot()

在pandas中,数据透视有专门的函数pivot_table,功能非常强大。pivot_table参数中, index是设置数据透视后的索引,column是设置数据透视后的列,简而言之,index是你想要 的行,column是想要的列。案例中,我希望统计每个用户在每月的订单量,所以user_id是 index,month是column。

values是将哪个值进行计算,aggfunc是用哪种方法。于是这里用values=order_dt和 aggfunc=count,统计里order_dt出现的次数,即多少笔订单。

applymap针对Dataframe里的所有数据;lambda没有elif的用法所以要两个if else



求出复购率,sum 和 count 都会忽略NaN(图中横坐标不知道为什么没有标注,是月份)

可以看出早期的复购率较低,后期则拥有较稳定的复购率20%左右

#回购率
pi_amount = df.pivot_table(index = 'id',columns = 'month',
                          values = 'amount',aggfunc = 'mean').fillna(0)
pi_amount.columns = df.month.sort_values().astype('str').unique()

pi_amount_t = pi_amount.applymap(lambda x:1 if x>0 else 0)

def pur_r(data):
    s = []
    for i in range(17):
        if data[i] == 1:
            if data[i+1] == 1:
                s.append(1)
            if data[i+1] == 0:
                s.append(0)
        else:
            s.append(np.NaN)
    s.append(np.NaN)
    return pd.Series(s,index = pi_amount.columns)

pi_amount_r = pi_amount_t.apply(pur_r,axis = 1)

(pi_amount_r.sum() / pi_amount_r.count()).plot()

新建一个判断函数。data是输入的数据,即用户在18个月内是否消费的记录,status是空列表,后续用来保存用户是否回购的字段。

因为有18个月,所以每个月都要进行一次判断,需要用到循环。if的主要逻辑是,如果用户本月进行过消费,且下月消费过,记为1,下月没有消费过是0。本月若没有进行过消费,为NaN,后续的统计中进行排除。

用apply函数应用在所有行上,获得想要的结果。


image.png

用户回购大于复购,约为30%,波动性较强

新客户回购率约在15%左右,差异不是那么明显

(新客户为第一次消费,老客户为更多消费次数)

综合分析复购率和回购率,新客质量整体低于老客,老客忠诚度表现较好,消费频次稍次

进行用户分层:

新用户为第一次消费,活跃用户为在某个时间窗口内有过消费,不活跃用户为在时间窗口内没有消费过的老客,回流用户为在上一个窗口中没有消费,而在当前时间窗口内有过消费

#用户分层
def user_type(data):
    s = []
    for i in range(18):
        #本月未消费
        if data[i] == 0:
            if len(s) == 0:
                s.append('未消费')
            else:
                if s[i-1] == '未消费':
                    s.append('未消费')
                else:
                    s.append('unactive')
        #本月消费
        else:
            if len(s) == 0:
                s.append('new')
            else:
                if s[i-1] == '未消费':
                    s.append('new')
                elif s[i-1] == 'unactive':
                    s.append('return')
                else:
                    s.append('active')
    return pd.Series(s,index = pi_amount.columns)

pi_amount_s = pi_amount_t.apply(user_type,axis = 1)

amount_counts = pi_amount_s.replace('未消费',np.NaN).apply(pd.value_counts)
amount_counts

amount_counts.fillna(0).T.plot.area(figsize = (12,6))

#回流和活跃用户占比
return_rata = amount_counts.apply(lambda x:x / x.sum(),axis = 0)
plt.figure(figsize = (12,4))
plt.subplot(121)
return_rata.loc['return'].plot()
plt.subplot(122)
return_rata.loc['active'].plot()

用户回流占比在5%-8%左右,存在下降趋势;活跃用户下降趋势更加明显,3%-5%左右,但在实际情况上其用户质量高于回流用户

#高消费人群贡献
u_amount = df.groupby('id').amount.sum().sort_values().reset_index()
u_amount['_cumsum_'] = u_amount.amount.cumsum()
total = u_amount._cumsum_.max()
u_amount['prop'] = u_amount.apply(lambda x:x._cumsum_ / total,axis = 1)
u_amount.tail()

u_amount.prop.plot()

order_amount为用户消费金额,cumsum是累加函数,最后的2500315.63就是总计消费金额

.tail方法求的是最末n项值



可以看出前20000名用户贡献了40%的消费,而后4000名用户则有60%,呈现28倾向

u_counts = df.groupby('id').dt.count().sort_values().reset_index()
u_counts['_counts_'] = u_counts.dt.cumsum()
total_ = u_counts._counts_.max()
u_counts['prop'] = u_counts.apply(lambda x:x._counts_ / total_,axis = 1)
u_counts.tail()

u_counts.prop.plot()


前两万用户贡献了50%的销量,高消费用户群体贡献了50%销量

#用户生命周期

od_min = df.groupby('id').date.min()
od_max = df.groupby('id').date.max()

(od_max - od_min).describe()
((od_max - od_min) / np.timedelta64(1,'D')).hist()


因为这里的数据类型是timedelta时间,它无法直接作出直方图,所以先换算成数值。换算的方式直接除timedelta函数即可,这里的np.timedelta64(1, 'D'),D表示天,1表示1天,作为单位使用的。因为max-min已经表示为天了,两者相除就是周期的天数。

大部分用户都只消费了1次,不妨排除这个群体,计算消费两次以上的老客生命周期

#一次以上用户生命周期
life_time = (od_max - od_min).reset_index()
life_time['lifetime'] = life_time.date / np.timedelta64(1,'D')
life_time[life_time.lifetime > 0].lifetime.hist(bins = 100,figsize = (12,6))

去除了生命周期为0的客户后,可以看出依然有许多用户的生命周期向0靠拢,呈现双峰趋势;普通型的用户生命周期集中在50—300天,高质量的用户生命周期在400天后,基本属于忠诚用户了


#留存率
u_retention = pd.merge(left = df,right = od_min.reset_index(),
                      how = 'inner',on = 'id',suffixes = ('','_min'))
u_retention['od_diff'] = u_retention.date - u_retention.date_min
u_retention['_diff_'] = u_retention.od_diff.apply(lambda x:x / np.timedelta64(1,'D'))

bin = [0,3,7,15,30,60,90,180,360]
u_retention['diff_bin'] = pd.cut(u_retention._diff_,bins = bin).astype('str')
pi_retention = u_retention.pivot_table(index = 'id',columns = 'diff_bin',
                                      values = 'amount',aggfunc = sum).drop(['nan'],axis = 1)
pi_retention.mean()

merge相当于SQL的join,在how参数里指定类型为inner join,以user_id作为主键连接,suffixes参数使合并内容中重名项添加后缀


pi_retention_t = pi_retention.fillna(0).applymap(lambda x:1 if x>0 else 0)
(pi_retention_t.sum() / pi_retention_t.count()).plot.bar()

“只有2.5%的用户在第一次消费的次日至3天内有过消费,3%的用户在3~7天内有过消费。数字并不好看,CD购买确实不是高频消费行为。时间范围放宽后数字好看了不少,有20%的用户在第一次消费后的三个月到半年之间有过购买,27%的用户在半年后至1年内有过购买。从运营角度看,CD机营销在教育新用户的同时,应该注重用户忠诚度的培养,放长线掉大鱼,在一定时间内召回用户购买。”

(可能是版本问题,不把date_diff_bin转换为str格式就无法显示(0,3]而且整个数据统计也是错误的,寻找解决方法未果;转换为str之后它只会按字符思维的顺序排列)

(始终不能重排列顺序心态崩了不想自己看就直接复制了)

#平均购买周期
def diff(group):
    d = group._diff_.shift(-1) - group._diff_.shift(1)
    return d
last_diff = u_retention.groupby('id').apply(diff)
last_diff.mean()

shift为pandas的偏移函数,其括号内的值控制向前或者是向后偏移;

若加入参数axis=1则是左右偏移;

在这里diff中实现的是用户两次消费的时间间隔

平均消费间隔为68天

想要召回用户,在60天左右效果较好

last_diff.hist(bins = 100)

大部分用户的消费间隔还是较短的,所以想要留住用户,可以在用户每次消费之后赠送20-30天内使用的优惠券,消费后一周内询问用户消费体验,15天左右提醒优惠券到期,30天短信提醒推送

总结

问题及优化

  1. 打开文件的路径,可以直接放在jupyter的打开目录下
  2. 如果在函数定义中返回结果是列表,然而要用于DataFrame:pd.Series( ,index= )
  3. 作图后图中有时候无横坐标显示
  4. 用户生命周期中:大部分用户都只消费一次,可以排除这个群体查看老客用户生命周期
  5. 分区时间差之后,数据透视表始终缺少(0,3]、多出 NaN 列以及数据不正常:
    将所有分组类型改为字符串类型、再去除 NaN 列,再作图(问题在于修改类型之后各组排序混乱,始终无解决方法)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,961评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,444评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,009评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,082评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,101评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,271评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,738评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,395评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,539评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,434评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,481评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,160评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,749评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,816评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,038评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,548评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,140评论 2 341