电商数据分析报告

一、数据集介绍

该数据集是跨国数据集,包含2010年12月12日至2011年12月9日期间发生的所有在英国注册的非商店在线零售业务的交易。该公司主要销售礼品,并且大部分客户是批发商。

二、字段介绍

InvoiceNo: 发票号码,每笔交易分配唯一的6位整数,而退货订单的代码以字母'c'开头。
StockCode: 产品代码,每个不同的产品分配唯一的5位整数。
Description: 产品描述,对每件产品的简略描述。
Quantity: 产品数量,每笔交易的每件产品的数量。
InvoiceDate: 发票日期和时间,每笔交易发生的日期和时间。
UnitPrice: 单价(英镑),单位产品价格。
CustomerID:顾客号码,每个客户分配唯一的5位整数。
Country: 国家的名字,每个客户所在国家/地区的名称。

三、分析目的

通过分析销售数据来了解在线零售业务的消费情况,分析用户消费数据分析用户消费行为。

分析问题:

  • 商店消费情况
    • 每月成交金额
    • 每月销售金额
    • 每月消费人数
    • 每月订单数量
    • 每月消费产品数量
    • 每月客单价
    • 消费国家分布
    • 下单时间分布
    • 每月退款产品数
    • 每月退款金额
    • 每月退款率
  • 用户消费行为
    • 用户消费次数、用户消费金额、用户购买产品数量
    • 消费次数与消费金额关系
    • 用户购买周期
    • 新用户、活跃用户、不活跃用户、回流用户、回流率
    • 复购率和回购率
    • RFM

四、分析内容

载入库和获取数据

# 载入库
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

# 导入数据
df = pd.read_csv('data.csv', encoding='ISO-8859-1')
df.head()
数据集前5行.JPG
df.info()
数据集信息.JPG

用户ID部分缺失,无法通过其他信息填充,因此后期清洗将用户ID缺失记录删除。

df.describe()
数据集统计信息.JPG
# 将产品数量为负数的记录数与退款订单的记录数进行对比
df[df['Quantity']<0].count() == df['InvoiceNo'].str.startswith('C').sum()
验证数据.JPG

部分产品数量是负数,经过验证是退款订单的产品数为负数

数据清洗和处理

# 删除用户ID缺失的记录
df = df[df['CustomerID'].notnull()].copy()

# 数据类型转换
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
df['CustomerID'] = df['CustomerID'].astype('O')

# 计算时间变量
df['Month'] = df['InvoiceDate'].values.astype('datetime64[M]')
df['Day'] = df['InvoiceDate'].dt.day
df['Dayofweek'] = df['InvoiceDate'].dt.dayofweek
df['Hour'] = df['InvoiceDate'].dt.hour

# 计算价格
df['Price'] = df['Quantity'] * df['UnitPrice']

探索性数据分析

商店销售情况分析

原数据集粒度为订购商品,转换为订单粒度,看一下数据情况

# 订单描述性统计分析
df.groupby('InvoiceNo')[['Quantity','Price']].sum().describe()
订单粒度描述性统计分析.JPG

因为零售商大部分客户是批发商,用户每笔订单平均产品数量和平均金额波动大,中位数和75分位数相差挺大,说明部分批发商批发数量大。

每月成交金额
# 商店消费情况绘图函数
def store_plot(data, xlab, ylab, title):
    plt.figure(figsize=(10,5))
    plt.plot(data)
    plt.xlabel(xlab)
    plt.ylabel(ylab)
    plt.title(title)

# 每月成交金额,包含退款订单的金额
store_plot(data=df[df['Quantity']>0].groupby('Month')['price'].sum(), 
           xlab='月份', 
           ylab='成交金额', 
           title='每月成交金额')
每月成交金额.jpeg

从2010年12月至2011年11月月成交金额总体呈现上涨趋势,从2010年8月开始高涨,可能是因为公司主要销售的产品包括礼品,而后半年国外有各种节日导致对礼品需求大增,也有可能是接近年底有各种促销活动,2011年2月和2011年4月都有一个明显的下降,暂时不清楚具体原因。

每月销售金额
 # 每月销售金额,成交金额-退款金额
store_plot(data=df.groupby('Month')['Price'].sum(), 
           xlab='月份', 
           ylab='销售金额', 
           title='每月销售金额')
每月销售金额.jpeg

月销售金额跟月成交金额趋势非常相似,可以看到2011年1月销售金额较成交金额有较大差异,可能是由于这个月的退款金额比较大。

每月消费人数
# 每月消费人数
store_plot(data=df[df['Quantity']>0].groupby('Month')['CustomerID'].nunique(), 
           xlab='月份', 
           ylab='消费人数', 
           title='每月消费人数')
每月消费人数.jpeg
每月订单数量
# 每月订单数量
store_plot(data=df[df['Quantity']>0].groupby('Month')['InvoiceNo'].nunique(), 
           xlab='月份', 
           ylab='订单数量', 
           title='每月订单数量')
每月订单数量.jpeg
每月消费产品数量
# 每月销售产品数量
store_plot(data=df[df['Quantity']>0].groupby('Month')['Quantity'].sum(), 
           xlab='月份', 
           ylab='销售产品数量', 
           title='每月销售产品数量')
每月销售产品数量.jpeg
每月客单价
# 每月客单价
store_plot(data=df[df['Quantity']>0].groupby('Month')['Price'].sum()/df[df['Quantity']>0].groupby('Month')['InvoiceNo'].nunique(), 
           xlab='月份', 
           ylab='客单价', 
           title='每月客单价')
每月客单价.jpeg
  • 月订单数量、月消费人数和销售产品数量趋势都跟月成交金额趋势相似,每月客单价趋势与月成交金额趋势差别比较大
  • 2011年1月较2010年12月消费人数和订单数量下降,销售产品数量和客单价上升,尤其是客单价很高,因此2011年1月与2010年12月成交金额持平主要原因是1月客单价远远高于12月客单价
  • 2011年2月和2011年4月的成交金额环比减少是由于消费人数、订单数量、销售产品数量和客单价都减少的原因
  • 2011年5月至2011年10月总体呈上升趋势原因是消费人数、订单数量、销售产品数量和客单价总体持平或上升
  • 2011年11月成交金额大涨主要是因为购买用户数、订单数和购买产品数增多,这个月客户购买的产品价格比较低
  • 由于数据集中只有成交信息、产品信息、地区和时间,具体上升或下降的原因没办法具体判断,推测可能是年底的促销活动或节日引起的
消费国家分布
# 国家用户数量分布
plt.figure(figsize=(12,8))
df[df['Quantity']>0].groupby('Country')['CustomerID'].nunique().sort_values(ascending=True).plot.barh()
plt.xlabel('用户数量')
plt.ylabel('国家')
plt.title('不同国家用户数量')
不同国家用户数量分布.png
# 国家订单数量分布
plt.figure(figsize=(12,8))
df[df['Quantity']>0].groupby('Country')['InvoiceNo'].nunique().sort_values(ascending=True).plot.barh()
plt.xlabel('订单数量')
plt.ylabel('国家')
plt.title('不同国家订单数量')
不同国家订单数量分布.png
# 国家成交金额分布
plt.figure(figsize=(12,8))
df[df['Quantity']>0].groupby('Country')['Price'].sum().sort_values(ascending=True).plot.barh()
plt.xlabel('成交金额')
plt.ylabel('国家')
plt.title('不同国家成交金额')
不同国家成交金额分布.png
  • 因为该公司在英国,因此可以看到客户数量、订单数量和成交金额英国都远远超过其他国家,英国是客户来源的主要国家,属于重要价值国家。
  • 除英国外,德国和法国在用户数量、订单数量和成交金额方面都属于前5,属于重点维持国家。
  • 荷兰、爱尔兰和澳大利亚虽然用户数量和订单数量少,但是成交金额位于前6,这三个国家的客单价比较高,属于重点发展国家。
  • 重点维持国家和重点发展国家可以进行适当推广、完善物流服务等等。
下单时间分布
# 星期
plt.figure(figsize=(12,8))
df[df['Quantity']>0].groupby('Dayofweek')['InvoiceNo'].nunique().plot.bar()
plt.xticks(range(6),['周一','周二','周三','周四','周五','周日'], rotation=0)
plt.xlabel('星期')
plt.ylabel('订单数')
plt.title('订单数随星期变化')
订单数随星期变化.jpeg

绝大多数订单在周一至周四完成。

# 小时
plt.figure(figsize=(12,8))
df[df['Quantity']>0].groupby('Hour')['InvoiceNo'].nunique().plot()
plt.xlabel('小时')
plt.ylabel('订单数')
plt.title('订单数随小时变化')
订单数随小时变化.jpeg

订单集中在上午10点至下午3点,中午12点是高峰期,这段时间内注意维持好网站的稳定性。

每月退款订单数
# 每月退款订单数量
store_plot(data=df[df['Quantity']<0].groupby('Month')['InvoiceNo'].nunique(), 
           xlab='月份', 
           ylab='退款订单数量', 
           title='每月退款订单数量')
每月退款订单数量.jpeg

每月退款订单数量2010年12月至2011年10月围绕280上下波动,略微呈上涨趋势,虽然2011年11月订单数量非常大,但同时退款订单数量也非常大。

每月退款金额
# 每月退款金额
store_plot(data=df[df['Quantity']<0].groupby('Month')['Price'].sum().abs(), 
           xlab='月份', 
           ylab='退款金额', 
           title='每月退款金额')
每月退款金额.jpeg
  • 每月退款金额围绕25000波动。
  • 2011年1月退款订单数量不大,因为客单价高,所以退款金额非常高,也印证了我们之前的猜测。
  • 由于2011年11月客单价很低,即使退款订单很多,退款金额也不大。
每月退款率
# 每月退款率
store_plot(data=df[df['Quantity']<0].groupby('Month')['InvoiceNo'].nunique()/df[df['Quantity']>0].groupby('Month')['InvoiceNo'].nunique(), 
           xlab='月份', 
           ylab='退款率', 
           title='每月退款率')
每月退款率.jpeg

每月退款率呈下降趋势,这是个不错的现象。

用户消费行为分析
用户消费次数、用户消费金额、用户购买产品数量

将数据集粒度转换成用户,看一下用户消费行为。

df[df['Quantity']>0].groupby('CustomerID').agg({'InvoiceNo':'nunique',
                                                'Quantity':'sum',
                                                'Price':'sum'}).describe()
用户粒度描述性统计.JPG
  • 超过一半客户至少消费了两次,甚至有客户消费了210次,属于非常忠诚的用户了。
  • 因为大部分客户是批发商,超过75%的客户购买数量在100以上。
  • 用户平均消费金额是2053元,标准差是8988元,用户平均消费金额大于75分位数,存在部分高额消费用户。
消费次数与消费金额关系
# 消费次数与消费金额关系
plt.figure(figsize=(12,8))
plt.scatter(x=df[df['Quantity']>0].groupby('CustomerID')['InvoiceNo'].nunique(),
            y=df[df['Quantity']>0].groupby('CustomerID')['Price'].sum())
plt.xlabel('消费次数')
plt.ylabel('消费金额')
plt.title('消费次数与消费金额关系')
plt.savefig('消费次数与消费金额关系.jpeg')
消费次数与消费金额关系.jpeg

消费次数和消费金额有较弱的相关性,用户消费次数越大,消费金额越大,可以引导用户多次消费,那么多长时间召回呢?我们来看看用户的消费间距。

用户消费周期
# 用户消费周期
purchase_time = df[df['Quantity']>0].groupby('CustomerID').apply(lambda x: x['InvoiceDate']-x['InvoiceDate'].shift()).dt.days
purchase_time.describe()
用户消费周期.JPG
purchase_time[purchase_time>0].describe()
老用户消费周期.JPG

至少消费两次的用户有一半消费间隔在一个月内,75%消费间隔在两个月内,可以在10天、28天、58天三个时间点对用户进行推送和提醒。

新用户、活跃用户、不活跃用户、回流用户、回流率
pivoted_amount = df[df['Quantity']>0].pivot_table(index='CustomerID',
                                                  columns='Month',
                                                  values='Price',
                                                  aggfunc='mean').fillna(0)
columns_month = df['Month'].sort_values().astype('str').unique()
pivoted_amount.columns = columns_month

pivoted_purchase = pivoted_amount.applymap(lambda x: 1 if x>0 else 0)

# 用户分层
def active_status(data):
    status = []
    for i in range(13):
        
        # 若本月没有消费
        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=columns_month)

pivoted_purchase_status = pivoted_purchase.apply(lambda x: active_status(x), axis=1)

purchase_status_counts = pivoted_purchase_status.replace('unreg',np.NaN).apply(lambda x:pd.value_counts(x))

purchase_status_counts.fillna(0).T.plot.area(figsize=(12,6))
plt.xticks(range(12), columns_month)
plt.title('用户分层')
用户分层.jpeg
# 回流率
return_rate = purchase_status_counts.apply(lambda x:x / x.sum(), axis=1)
return_rate.loc['return'].plot(figsize=(12,6))
plt.xticks(range(1,13), columns_month)
plt.title('回流率')
回流率.jpeg

在后期消费中新用户占比减少,回流用户和活跃用户增多,回流率一直在上涨,整体用户质量不错。

复购率和回购率
# 复购率
pivoted_counts = df[df['Quantity']>0].pivot_table(index='CustomerID', 
                                                  columns='Month',
                                                  values='InvoiceNo',
                                                  aggfunc='nunique').fillna(0)
columns_month = df['Month'].sort_values().astype('str').unique()
pivoted_counts.columns = columns_month

pivoted_counts_transf = pivoted_counts.applymap(lambda x: 1 if x>1 else np.NaN if x==0 else 0)

(pivoted_counts_transf.sum() / pivoted_counts_transf.count()).plot(figsize=(12,6))
plt.xticks(range(12), columns_month)
plt.title('复购率'
复购率.jpeg

由于早期新用户占比大,前期复购率比后期复购率低,且后期复购率波动大,但复购率基本在20%以上。

# 回购率
def purchase_return(data):
    status = []
    for i in range(12):
        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)

pivoted_purchase_return = pivoted_purchase.apply(purchase_return,axis=1)

(pivoted_purchase_return.sum() / pivoted_purchase_return.count()).plot(figsize=(12,6))
plt.xticks(range(12), columns_month)
plt.title('回购率')
回购率.jpeg

用户的回购率高于复购率,都大于30%,后期因为回流用户增多,这段时间老用户的忠诚度表现较好,所以后期回购率较高。

RFM
# rfm
rfm = df[df['Quantity']>0].groupby('InvoiceNo').agg({'daydiff':'min',
                                                     'Quantity':'sum',
                                                     'Price':'sum'})
rfm.rename(columns={'daydiff':'R',
                    'Quantity':'F',
                    'Price':'M'},inplace=True)

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.median()).apply(rfm_func, axis=1)
rfm.groupby('label').sum()
rfm分层.JPG
rfm.groupby('label')['M'].count()
rfm用户数量.JPG

五、总结

  • 从2011年5月开始成交金额总体上升,11月达到高峰,成交金额主要受到消费人数、订单数量和客单价的影响,但由于数据只包含部分成交数据没有办法得知上涨或下降的具体原因,推测可能是年中以后节日增多导致礼品需求量增大或促销活动引起。
  • 英国是主要的客户来源地区,而德国、法国、荷兰、爱尔兰和澳大利亚成交金额表现不错,可以尝试拓展这些国家的市场;订单下单时间高峰期主要在10点至15点,这段时间要维护好网站的稳定。
  • 退款订单数略微呈上涨趋势,2011年11月退款订单数最高;退款金额总体持平,2011年1月退款金额高;退款率在下降,这是个好现象。
  • 多数用户至少消费了两次,由于大部分客户是批发商,购买数量和金额较大;用户消费次数越多,消费金额越大,可以在半个月、一个月、两个月三个时间段对用户进行召回,引导客户进行多次消费。
  • 活跃用户比例和回流用户比例随时间增大,后期老用户忠诚度较好,因此复购率和回购率表现不错;可以通过RFM模型对用户价值进行分类,不同价值用户采取不同的业务决策。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容