某电商公司收购了一家欧洲电商公司。收购后,需要对这家公司的销售情况进行摸底。需要从这家电商公司的原始订单数据中分析出公司的业务是在变好还是变差,公司哪些产品最受欢迎,以及主要的销售区域和有哪些优质的用户等信息,以提供给公司的战投部门做进一步的经营策略制定。
这份数据在上一篇中已经初次见过,当时并没有进行深入的分析。
下面是该数据集的正式介绍,这是某欧洲电商公司将近一年的交易数据,字段如下:
| 字段 | 含义 |
|---|---|
| InvoiceNo | 订单编号,以字母 C 开头表示取消的订单 |
| StockCode | 商品编码 |
| Description | 商品描述 |
| Quantity | 购买数量 |
| InvoiceDate | 下单日期时间 |
| UnitPrice | 商品单价 |
| CustomerID | 客户 ID |
| Country | 客户所在国家 |
首先导入必要的工具包:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import random
from plotly import tools
import plotly.express as px
from plotly.offline import init_notebook_mode, iplot, plot
import plotly.graph_objs as go
加载数据,并查看源数据的摘要:
df_goods = pd.read_csv("data.csv")
df_goods
输出如下:
如上所示,整个数据集一共包含 54 万条数据,每行数据包含八列,字段的含义可以参考上方的说明表。
DataFrame 有一个非常有用的函数:describe,提供了数据概览的功能。这个函数会将 DataFrame 中包含的数值特征单独拎出来,并给出一些基本的统计特征。这对于初始的分析非常有用。
代码如下:
df_goods.describe()
输出如下所示:
从上面的输出可以看出,表格中一共有 Quantity、UnitPrice 和 CustomerID 这三个列被认定是数值列,虽然订单 ID:InvoiceNo 看起来也是数值列,但是因为有取消订单(以字母 C 开头就不算数值了),所以没有被列进来。
先解释一下上面输出表格的每一行含义:
从统计信息中,大概可以看出以下信息:
CustomerID 是这次分析的目标,只感兴趣有 CustomerID 字段的记录,所以处理以下缺失值,首先看一下目前缺失值的情况。
df_goods.isnull().sum()
输出如下:
InvoiceNo 0
StockCode 0
Description 1454
Quantity 0
InvoiceDate 0
UnitPrice 0
CustomerID 135080
Country 0
dtype: int64
CustomerID 存在 13 万条缺失值,Description 存在 1400 条,基本和刚才的判断一致,由于 CustomerID 是本次的核心字段,所以简单删除缺失值即可。
df_goods = df_goods.dropna()
df_goods.isnull().sum()
输出如下:
InvoiceNo 0
StockCode 0
Description 0
Quantity 0
InvoiceDate 0
UnitPrice 0
CustomerID 0
Country 0
dtype: int64
这样,缺失值就处理完毕了。
数据集中存在被取消的订单,现在来看一下量级有多少。
因为表中的数据是以商品为维度,一个订单可能在 DataFrame 中有多条记录。先来看一下一共有多少个订单。
df_goods["InvoiceNo"].nunique()
输出如下:
22190
接下来,看一下过滤出被取消的记录,保存在 df_cancelled 中。
df_cancelled = df_goods[df_goods["InvoiceNo"].str.startswith("C")]
df_cancelled
输出如下:
最新的 df_goods 有大概 40 万条记录,其中 8905 条是取消的,整体来看还好,并不是很多。不过从上面的数据中还能看到,似乎被取消的订单数量都是负值,这可以解释为什么 Quantity 字段会有很多 0 以下的取值。
再看一下被取消的订单总数:
df_cancelled.InvoiceNo.nunique()
输出如下:
3654
可以看到,虽然商品数占比较少,但是订单总数的占比还是不低的。为了避免对分析产生影响,将这些订单从 DataFrame 中清除。
df_goods = df_goods[~df_goods["InvoiceNo"].str.startswith("C")]
df_goods
输出如下:
(原书此处配有运行截图,未随迁移入库,略)
再查看下统计数据:
df_goods.describe()
输出如下:
可以看到,随着刚才的清洗,UnitPrice 和 Quantity 已经没有负值了。但是最大值似乎仍然存在异常(远远超过均值和 75% 值)。
在上一篇中,通过散点图来筛查异常值的范围,但整个过程还是比较费劲的。比如能看到存在异常大的值,但因为这些值把图像的尺度弄得很大,导致无法很好地判断出超过多少就属于异常值。
之前绘制的 Quantity 散点图如下(原书此处配有插图,未随迁移入库)。0 附近的数据非常密集,但是从这个坐标轴看很难看出最佳的剔除范围。
前文中学习了 plotly,plotly 的图表是可以放大的,看起来正好可以满足分析的需要,而且通过直方图还能够更直观地看出正常值的分布。代码如下:
fig = px.histogram(df_goods.Quantity)
fig.show()
输出如下:
上图中,横轴是 Quantity 的取值,纵轴的取值对应的个数。可以看到,绝大多数值都集中在 0 附近,现在来尝试看看到底主流分布的范围是多少。首先把鼠标放在横轴上,按住往左拖动曲线,拖动完毕后如下(原书此处配有插图,未随迁移入库):
然后框选感兴趣的区域放大,如下(原书此处配有插图,未随迁移入库):
从上图中不难看出,0~24 的占了绝大多数,所以基本可以认为 Quantity >= 25 则是异常数据。
同样的方法分析 UnitPrice。
fig1 = px.histogram(df_goods.UnitPrice)
fig1.show()
从上图中,基本可以认为 >=5 的 UnitPrice 即为异常的数据。
所以,合理的 Quantity 最大值为 25, UnitPrice 为 5。 回想之前通过 describe 描述的统计数据,Quantity 的均值为 13.02,UnitPrice 均值为 3.11,这也和分析的合理最大值能够呼应上。 接下来就继续洗掉非合理的数据。
df_goods = df_goods[df_goods.UnitPrice < 5]
df_goods = df_goods[df_goods.Quantity < 25]
df_goods
输出如下:
(原书此处配有运行截图,未随迁移入库,略)
现在数据集已经准备好,可以做进一步的分析了。
首先,分析一下最受欢迎的商品,以及它到底有多受欢迎。一般来说,先用 value_counts 来查看。代码如下所示,使用 head 函数是因为先取前几条查看即可。
df_goods.Description.value_counts().head()
输出如下:
WHITE HANGING HEART T-LIGHT HOLDER 1669
JUMBO BAG RED RETROSPOT 1328
PARTY BUNTING 1274
LUNCH BAG RED RETROSPOT 1206
ASSORTED COLOUR BIRD ORNAMENT 1157
Name: Description, dtype: int64
可以看到,WHITE HANGING HEART T-LIGHT HOLDER 这件商品是最受欢迎的,看起来像是一个手提的灯座。
为了更直观地分析,也可以直接把 value_counts 返回的 Series 以直方图的形式表示出来。代码如下:
# 取前20条记录来画图
buying_count = df_goods.Description.value_counts()[:20]
sns.set()
# 不需要重新计数,直接使用 buying_count series的 index 和value 绘制即可
sns.barplot(buying_count.index, buying_count.values)
输出如下:
可以看到上面的图有点问题,由于商品名称比较长,所以全部都重叠到了一起。像这样的情况,用以下两个方式解决:
# 取前20条记录来画图
buying_count = df_goods.Description.value_counts()[:20]
sns.set()
plt.figure(figsize=(20,9))
# 不需要重新计数,直接使用 buying_count series的 index 和value 绘制即可
sns.barplot(buying_count.index, buying_count.values)
plt.xticks(rotation = 90)
输出如下:
可以看到,除了第一名的 WHITE HANGING HEART T-LIGHT HOLDER 远高于第二名,后面的几种商品差距都并不是特别大。
下面来分析订单主要都销往了哪些国家。首先,因为考虑的是订单维度的数据,所以第一步将数据表按照订单的维度进行聚合。另外,为了方便分析优质的用户,直接用订单号、国家客户 ID 进行聚合。
df_customer_country_map = df_goods[['CustomerID', 'InvoiceNo', 'Country']].groupby(['CustomerID', 'InvoiceNo', 'Country']).count()
# 重新给生成的 DataFrame 分配索引
df_customer_country_map = df_customer_country_map.reset_index(drop = False)
df_customer_country_map
输出如下:
(原书此处配有运行截图,未随迁移入库,略)
聚合表已经有了,先将国家的频率数据整理出来。
countries = df_customer_country_map['Country'].value_counts()
为了展示国家类型的数据,plotly 提供了世界地图的工具,可以在世界地图上展示国家的频率信息。代码如下,基本用法注释中给出了说明。
# 生成地图数据源基础配置,是一个字典
# type 指定生成的图为世界地图
# locations 传入地图的数据源
# locationmode 表示用什么表示国家,比如经纬度,或者国家名字,这里使用的是国家的名字
# z 指定完整的数据源
# text 指定鼠标悬浮的时候显示的文字
# colorbar 设置在图的右边展示颜色信息的配置
# colorscale 表示不同的频率显示的颜色分级
data = dict(type='choropleth',
locations = countries.index,
locationmode = 'country names', z = countries,
text = countries.index, colorbar = {'title':'订单数'},
colorscale=[[0, 'rgb(255,255,255)'],
[0.1, "rgb(0, 255,255)"],
[1, 'rgb(255,0,255)']])
# 生成地图布局配置, 指定图的标题
layout = dict(title='订单国家分布')
# 生成图表对象
fig3 = go.Figure(data = [data], layout = layout)
# 画图
iplot(fig3, validate=False)
输出如下所示:
地图上国家的颜色代表了该国家的订单数,紫色代表订单数最多,而浅青色代表订单数最少。初步看大部分地区订单都较少,但欧洲有部分国家颜色较深,将其放大,如下(原书此处配有插图,未随迁移入库):
从上图中可以看出,英国的订单数占到了绝大多数,旁边青色的德国和法国也有一定的订单,但相比英国也很少。基本可以说该家公司的客户基本都在英国,没有做国际贸易。
下面分析优质的用户,已经有了订单维度的表,只需要按照 CustomerID 来进行聚合,并使用 count 函数来指定 InvoiceNo 和 Country 字段聚合之后显示计数。
df_customer_order_count = df_customer_country_map.groupby("CustomerID").count()
df_customer_order_count
输出如下:
因为 groupby 之后使用了 count,所以 InvoiceNo 和 Country 的值都是一样的,都代表该用户有多少条订单记录,也就是订单数。
接下来使用条形图来看下单数最多的 20 个用户,代码如下:
# 以订单数排序,并取前20
df_customer_order_count = df_customer_order_count.sort_values(by="InvoiceNo",ascending=False).head(20)
# 重置index,不然默认还是以 用户Id 排序
df_customer_order_count = df_customer_order_count.reset_index(drop=False)
df_customer_order_count = df_customer_order_count.rename(columns = {"InvoiceNo":"OrderCount"})
# 绘制条形图
fig = plt.figure(figsize=(20,9))
sns.barplot(df_customer_order_count.index, df_customer_order_count.OrderCount)
# 指定 x 轴的刻度为用户 Id
plt.xticks(df_customer_order_count.index, df_customer_order_count.CustomerID, rotation=90)
输出如下:
可以看到,前两名用户基本平分秋色,每个人都是 190 单左右,远超过第三名。不过整体来说,这 20 个用户都应该算优质用户。
最后一步是分析销售额的趋势,首先在原始数据表中是没有销售额一列的。只有单元价格和数量,所以首先需要建立销售额一列:
df_goods["TotalPrice"] = df_goods["UnitPrice"] * df_goods["Quantity"]
df_goods
输出如下:
可以看到,新建的 TotalPrice 一列已经成功了,在表格的最后。
第二步,将 InvoiceDate 字段改为 datetime 类型,并设置为数据表的索引。
df_goods.InvoiceDate = pd.to_datetime(df_goods.InvoiceDate)
df_goods = df_goods.set_index("InvoiceDate")
df_goods
输出如下:
(原书此处配有运行截图,未随迁移入库,略)
现在日期已经是索引了,然后只关注 TotalPrice,所以这里首先将 TotalPrice 单独拿出来,并且新增一个代表日期的列。虽然 InvoiceDate 也有日期,但是它也包含了时间,所以需要一个单纯日期的列用来做聚合。
df_cum = df_goods[["TotalPrice"]]
df_cum["date"] = df_cum.index.date
df_cum
输出如下:
(原书此处配有运行截图,未随迁移入库,略)
接下来首先用 date 列来聚合,含义就是把每一天的所有销售额都加起来,然后直接调用 plot 函数画图即可。
df_cum = df_cum.groupby("date").sum()
df_cum.plot()
输出如下:
整体来看,销售额随着时间还是逐步增长的,说明该公司还具备一定的成长空间。
本节电商数据的可视化分析实战总结如下。回顾一下本节涉及的新内容:
接下来,将会用一整个模块,综合使用之前学习到的知识进行实战,加深印象,为投身数据分析行业做好准备。
| 库 | 本文编写时 | 当前稳定版 | 升级要点 |
|---|---|---|---|
| Python | 3.8-3.12 | 3.14 | 3.12+ 起性能显著提升;3.14 PEP 649/750 |
| NumPy | 1.x/2.0 | 2.3.x | np.float_ 等别名移除;NEP 50 类型提升 |
| Pandas | 1.x/2.x | 3.0.x | Copy-on-Write 默认开启;inplace 移除;字符串 dtype 变化 |
| Matplotlib | 3.x | 3.x 稳定版 | API 兼容,样式更新 |
| Seaborn | 0.12/0.13 | 0.13.x | API 稳定 |
| scikit-learn | 1.x | 1.7.x | API 稳定,新算法持续加入 |
本文讲解的数据分析流程(读取→清洗→分析→可视化)与核心 API 在最新版本中成立;升级时重点关注 Pandas 3.0 的 Copy-on-Write 与 NumPy 2.x 的类型变化。