案例:英国电商用户行为数据分析
Part 1. 数据获取
1.1 数据集简介
https://archive.ics.uci.edu/ml/datasets/online+retail#
该数据集为英国在线零售商在2010年12月1日至2011年12月9日间发生的所有网络交易订单信息。
1.2 数据集内容
数据集为xlsx格式,文件大小22.6M。数据共计8个字段,541908条。具体字段如下:
InvoiceNo:发票编号。为每笔订单唯一分配的6位整数。若以字母'C'开头,则表示该订单被取消。
StockCode:产品代码。为每个产品唯一分配的编码。
Description:产品描述。
Quantity:数量。每笔订单中各产品分别的数量。
InvoiceDate:发票日期和时间。每笔订单发生的日期和时间。
UnitPrice:单价。单位产品价格,单位为英镑。
CustomerID:客户编号。为每个客户唯一分配的5位整数。
Country:国家。客户所在国家/地区的名称。
Part 2. 提出问题
根据数据集提出问题如下:
1.订单维度:笔单价和连带率是多少?订单金额与订单内商品件数的关系如何?
2.客户维度:客单价是多少?客户消费金额与消费件数的关系如何?
3.商品维度:商品的价格定位是高是低?哪种价位的商品卖得好?哪种价位的商品带来了实际上最多的销售额?
4.时间维度:各月/各日的销售情况是什么走势?可能受到了什么影响?
5.区位维度:客户主要来自哪几个国家?哪个国家是境外主要市场?哪个国家的客户平均消费能力最强?
6.客户行为:客户的生命周期、留存情况、购买周期如何?
根据上述问题按如下思路进行分析:
Part 3. 清洗数据
数据清洗部分思路如下:
3.0 导入数据
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
sales_df = pd.read_excel('./Online Retail.xlsx', sheet_name='Online Retail')
再查看一下各列的数据类型:
3.1 列名重命名
观察到字段InvoiceDate并非只包含了日期信息,同时也涵盖了具体的时分秒维度,故将其重命名为InvoiceTime:
# 列名重命名
sales_df.rename(columns={
'InvoiceDate': 'InvoiceTime'}, inplace= True)
3.2 删除重复值
我们规定,若所有字段的值都完全相同,则视为重复数据,仅保留一条。
删除重复值,并根据去重前后的行数变化计算重复值数量:
# 删除重复值
rows_before = sales_df.shape[0] # 留意pandas对象的shape属性
sales_df.drop_duplicates(inplace= True)
rows_after = sales_df.shape[0]
print('原行数:', rows_before, '现行数:', rows_after, '删除行数:', rows_before - rows_after)
由于重复数据的删除,此时索引值和行数已不相符,进行索引的重设,并删除原索引:
# 重设索引,并删除原索引
sales_df.reset_index(drop=True, inplace = True) # 如果没有drop的话,会保留原索引
3.3 缺失值处理
# 查看缺失值
sales_df.isnull().sum()
Description是商品的文字描述,不是我们的分析重点,存在1454个空值,不予处理。
CustomerID是客户的唯一编号,很重要,缺失了135037行,将近总行数的1/5。但并不能直接删去有缺失值的行,会影响整体销售情况的分析。也无法采用插值法。姑且用‘0’来填充。虽说客户ID都是五位数字,但填充前还是先确认下是否真的不存在ID为‘0’的客户。
sales_df[sales_df['CustomerID'] == '0']
# 填充缺失的CustomerID
sales_df['CustomerID'].fillna('0', inplace=True)
3.4 一致化处理
3.4.1 时间相关信息的一致化
首先将InvoiceTime转为pandas能处理的时间格式datetime:
# 一致化处理
sales_df['InvoiceTime']=pd.to_datetime(sales_df['InvoiceTime'], errors='coerce')
再新增字段Date存放InvoiceTime中的日期部分:
sales_df['Date'] = pd.to_datetime(sales_df['InvoiceTime'].dt.date, errors='coerce')
新增Month存放月份信息:
sales_df['Month'] = sales_df['InvoiceTime'].dt.month # 这样得到month依旧是一个整形数据
由于我们对时间相关字段的操作有可能产生缺失值,再次查看缺失值情况:
sales_df.isnull().sum()
3.4.2 重新规整数字类型
将UnitPrice转为浮点型,Quantity和CustomerID转为整型:
sales_df['Quantity'] = sales_df['Quantity'].astype('int32')
sales_df['UnitPrice'] = sales_df['UnitPrice'].astype('float')
sales_df['CustomerID'] = sales_df['CustomerID'].astype('int32')
sales_df['InvoiceNo'] = sales_df['InvoiceNo'].astype('str') # 后面检查C字段订单的时候,发现这里需要是字符串类型
增加字段SumPrice用于存放该行数据的总价:
# 计算总价
sales_df['SumPrice'] = sales_df['Quantity'] * sales_df['UnitPrice']
3.5 异常值处理
为了探究是否存在异常值,我们查看总体的描述性统计情况:
sales_df.describe()
发现Quantity数量、UnitPrice单价、SumPrice总价都存在负值的情况,且绝对值较大。而总价是由数量和单价相乘得到,总价为负实际上也是数量和单价二者之一为负值导致的。
故查看数量或单价非正值的数据:
sales_df[(sales_df['Quantity'] <= 0)|(sales_df['UnitPrice'] <= 0)]
初步浏览,注意到主要是 ①C字头的被取消订单 和 ②单价为0的免费订单 导致的异常。
3.5.1 C字头的取消订单
取消订单产生的负值在之后销售情况的分析中会产生干扰,考虑将sales_df分为只含成功订单和只含取消订单两部分。
我们需要探究取消订单是直接在原订单上进行的修改,还是用来抵消原订单的新增数据。以上图第一行为例来说,即是否存在536379订单,与C536379订单对应。(注意这种思考的逻辑)
将sales_df分为成功订单和取消订单两部分,划分依据是发票编号InvoiceNo是否含有“C”:
query_c = sales_df['InvoiceNo'].str.contains('C')
# 只含取消订单
sales_cancel = sales_df.loc[query_c,:].copy()
# 只含成功订单
sales_success = sales_df.loc[-query_c,:].copy()
为sales_cancel增加字段SrcInvoiceNo,用于存放去掉“C”的发票编号:
# 增加原订单号
sales_cancel['SrcInvoiceNo'] = sales_cancel['InvoiceNo'].str.split('C', expand=True)[1]
将sales_cancel和sales_success进行合并:
print('merge之前,sales_cancel的shape为:{}'.format(sales_cancel.shape))
print('merge之前,sales_success的shape为:{}'.format(sales_success.shape))
new_data = pd.merge(sales_cancel, sales_success, left_on='SrcInvoiceNo',right_on='InvoiceNo')
print('merge之后,new_data的shape为:{}'.format(new_data.shape))
可以确认发现取消订单和成功订单并无对应关系。
原来的536641行数据中,取消订单占了9251行。
3.5.2 单价为0的免费订单
推测单价为0的订单是促销活动的赠品,对于订单量、件单价、连带率等指标的计算造成干扰,故也单独分出一张表存放,之后再对免费订单进行分析:
query_free = sales_success['UnitPrice'] == 0
# 只含免费订单
sales_cancel = sales_success.loc[query_free,:].copy()
# 只含普通订单
sales_success = sales_success.loc[-query_free,:]
print(sales_success.describe())
注意到还有一类异常订单,单价为负值。
3.5.3 单价为负的订单
查询出此类订单,并消除:
query_minus = sales_success['UnitPrice'] < 0
sales_success = sales_success.loc[-query_minus, :]
print('至今,sales_success还有数据:{}'.format(sales_success.shape))
print(sales_success.describe())
至此数据清洗告一段落,sales_success还余524878行数据:!
数据可视化之前的所有code:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 读入数据
sales_df = pd.read_excel(