商品库存分析
学习目标
-
知道库存管理的ABC模型
-
知道库存管理的XYZ模型
-
完成ABC-XYZ建模案例
无论是传统零售还是电商领域,关键产品的缺货会对销售和营销成本产生巨大的负面影响。在目前这个流量越来越贵的时代,好多流量都是付费购买来的,当我们用真金白银换来了用户的访问,应该尽量避免产品缺货。缺货会给用户带来很差的产品体验,并且会使用户流向竞品,导致用户流失。
虽然我们可以通过保持一个过量库存的方式避免缺货情况,但是这会导致库存周转率较低,带来更大的资金和仓储成本,我们可以通过一套库存管理方法将库存量控制在合理的水平,实现不伤害用户体验,保证日常正常经营的前提下压缩备货带来的资金和仓储成本。
一、电商中的库存管理方法
1.1 概述
-
ABC 管理法是管理库存的经典方法。通过计算每个 SKU 的销售收入在所有 SKU 产生的总收入中的累积百分比贡献进行排名,来对商品进行分类,它基于帕累托原则(80/20 规则):
-
A 类商品贡献了80%的销售收入,这些商品需要严格控制库存,避免缺货
-
B类商品贡献了接下来的10%的销售收入,对于这类商品,库存控制可以适当放松
-
剩下的10%收入由C类商品贡献,但C类商品种类最多,分配到的进货成本和仓储资源优先级最低
-
-
ABC库存管理虽然有效,但是有自身局限性,它只从销售收入单一维度来衡量商品的重要性。但实际情况确更加复杂,如:
-
某些商品周期性较强
-
某些商品会受季节影响
-
还有些商品的销量受售价的影响比较敏感,竞争对手做活动搞促销都会营销到销量
-
除此之外经济因素,政策影响都会对销量造成影响从而给库存管理带来更大挑战
-
-
引入XYZ库存管理方法可以帮助我们适应更加复杂的情况。与 ABC 库存管理不同,ABC 库存管理着眼于每个 SKU 的收入贡献,XYZ 库存管理根据 SKU 的需求变化对其进行分类。当对产品的需求相当稳定时,预测相对简单。然而,当它受到变化的影响时,需求预测就变成了一个挑战
1.2 XYZ库存管理方法
-
通过 XYZ 库存管理,在很长一段时间内测量每个 SKU 的销售需求,以捕捉需求的季节性变化,然后计算每个 SKU 的方差,并根据其变化对分数进行排名。
-
X 类:需求变化最小的产品。 这些产品的销售比较稳定,这意味着采购经理更容易预测它们,很容易避免缺货。
-
Y 类:比 X 类中的产品变化更大。由于季节性等因素,需求会随时间变化,因此更难准确预测
-
Z 类:需求波动起伏较大,比较难预测,除非有冗余较大的备货,否则很难避免缺货
-
1.3 将ABC 与 XYZ组合使用
-
ABC法和XYZ法是从不同的角度来处理库存管理的问题,而且对运营和采购人员都很有用,所以可以将它们结合起来
-
将ABC与XYZ组合起来的用法很简单,对于每个SKU我们组成 AX、AY、AZ、BX、BY、BZ、CX、CY 和 CZ九个类别,通过这九个类别,我们可以帮助运营和采购人员了解两者对收入的重要性以及需求的变化
-
在这个项目中,我们将使用电子商务交易数据的 12 个月快照来创建一个 ABC-XYZ 库存管理模型,该模型对每个 SKU 进行分类,并为其管理提出一些建议,以帮助零售商避免出现代价高昂的缺货
二 代码实现
2.1、加载数据&数据清洗
-
导入使用到的模块
from datetime import timedelta import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt sns.set_style("dark") sns.set()
-
加载交易数据
我们还是使用在线电商零售数据,计算每个SKU在一段时间内的销售数量和销售收入,这里加载12个月的数据
data1 = pd.read_excel('data/online_retail_II.xlsx',sheet_name='Year 2009-2010') data2 = pd.read_excel('data/online_retail_II.xlsx',sheet_name='Year 2010-2011') data = pd.concat([data1,data2],ignore_index=True) data
-
我们将日期转换成日期时间类型,并从订单日期中提取12个月的数据,我们提取一整年的数据,每个月的销售数据都是完整,这样可以避免分析的误差
data['InvoiceDate']= pd.to_datetime(data['InvoiceDate']) df_12m = data[(data['InvoiceDate'] > '2010-01-01') & (data['InvoiceDate'] <= '2010-12-31')] df_12m.info()
-
调整一下列名,方便后续分析,并创建新列提取月份信息
df_12m.columns= ['OrderNo', 'sku', 'Description', 'quantity', 'date_created', 'UnitPrice', 'CustomerID', 'Country'] df_12m = df_12m.assign(month = df_12m['date_created'].dt.month ) df_12m.head()
-
从数据中去掉退货的商品
df_12m = df_12m.query('quantity>0&UnitPrice>0') #查看数据情况 df_12m.shape[0]
509089
-
为了快速了解数据集中的季节性,使用 Seaborn 绘制每月销售量的柱状图。 从图中可以看出整体销量呈一定周期性,Q1,Q3销量较高
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="month", y="quantity", data=df_12m, palette="Blues_d")\ .set_title("Quantity by month",fontsize=15)
-
创建销售收入字段,查看订单价值在一年中的变化情况。波动情况跟销量的波动基本相符
df_12m = df_12m.assign(revenue =df_12m['quantity']*df_12m['UnitPrice']) f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="month", y="revenue", data=df_12m, palette="Blues_d")\ .set_title("Revenue by month",fontsize=15)
2.2、构建XYZ模型
-
我们基本了解了数据情况,为了方便后续计算,我们先将数据按sku 和 month分组,计算不同sku每个月的总销量
df_12m_units = df_12m.groupby(['sku','month'])['quantity'].sum().to_frame().reset_index() df_12m_units.head()
-
上面的数据是长格式,而不是宽格式,需要对其进行转换。 我们使用 pivot() 透视表将每个 SKU 最为行索引,将月份作为列,每月销售数量最为值
df_12m_units = df_12m_units.pivot(index='sku', columns='month', values='quantity')\ .add_prefix('m').reset_index().fillna(0) df_12m_units.head()
-
XYZ模型需要计算标准差,标准差的大小代表了每月销量的波动情况
-
通过dataframe的.std(axis=1) 来计算每行中值的标准差
-
df_12m_units['std_demand'] = df_12m_units[['m1','m2','m3','m4','m5','m6', 'm7','m8','m9','m10','m11','m12']].std(axis=1) df_12m_units.head()
-
计算总需求
-
接下来我们需要计算每个sku的年需求量,实际就是每月数据求和sum()
-
df_12m_units = df_12m_units.assign(total_demand = df_12m_units['m1'] + df_12m_units['m2'] + \ df_12m_units['m3'] + df_12m_units['m4'] + \ df_12m_units['m5'] + df_12m_units['m6'] + \ df_12m_units['m7'] + df_12m_units['m8'] + \ df_12m_units['m9'] + df_12m_units['m10'] + \ df_12m_units['m11'] + df_12m_units['m12'] ) df_12m_units.head()
-
计算每月的平均需求
-
计算完总需求之后,我们接下来计算每个SKU的平均需求
-
df_12m_units = df_12m_units.assign(avg_demand = df_12m_units['total_demand'] / 12 ) df_12m_units.head()
-
计算需求变异系数(Coefficient of Variation)
-
最后,计算全年每个 SKU 的需求变化量。 变化系数可以衡量销量的可预测性,变异系数接近零的值意味着变异最小且可预测性更高,而高值则相反。
-
df_12m_units['cov_demand'] = df_12m_units['std_demand'] / df_12m_units['avg_demand']
-
计算完变异系数我们将数据按照变异系数高低排序,通过head() 和 tail() 查看需求变化最大和最小的产品
-
从下面的数据中看出cov_demand 的值均为 3.46,这是因为它们在一年内仅有一个月有销量
-
df_12m_units.sort_values(by='cov_demand', ascending=False).head(5)
df_12m_units.sort_values(by='cov_demand', ascending=True).head(5)
-
我们分别计算变异系数的最小,最大和平均值,最低 CV 为 0.25,最高为 3.46,总体平均值为 1.54。 这意味着该业务包含大量需求波动或季节性需求的产品,这将使采购人员更难以控制。
df_12m_units['cov_demand'].min()
0.13519331703568235
df_12m_units['cov_demand'].mean()
1.545608726082916
df_12m_units['cov_demand'].max()
3.4641016151377557
-
绘制 CV的直方图,查看整个产品系列中变异系数的分布,CV值>1的说明需求变化比较剧烈,库存难以预测
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.distplot(df_12m_units['cov_demand']).set_title("Coefficient of Variation",fontsize=15)
-
区分XYZ
-
我们最终的目的是将所有的商品按照需求波动的情况划分成X,Y,Z三类,最常见的方法是使用需求的变异系数大小来作为划分依据,区分的阈值根据业务实际情况来确定
-
这里将0.5和1作为阈值,变异系数<0.5的作为X类,变异系数>1的作为Z类
-
划分为X类的,全年需求都比较稳定
-
划分为Z类的,波动比较大,而且很可能只有零星的销售
-
-
我们创建一个函数用来通过cov_demand 值的大小来为每一个SKU打上X、Y、Z的标签
-
def xyz_classify_product(cov): if cov <= 0.5: return 'X' elif cov > 0.5 and cov <= 1.0: return 'Y' else: return 'Z'
df_12m_units['xyz_class'] = df_12m_units['cov_demand'].apply(xyz_classify_product) df_12m_units.xyz_class.value_counts()
Z 2796 Y 1027 X 288 Name: xyz_class, dtype: int64
-
检验XYZ分类结果:大多数易于预测的 SKU 基于单位数量的需求相对较低。 销量最高的是 Y 和 Z 类中的那些。
df_12m_units.head()
df_12m_units.groupby('xyz_class').agg( total_skus=('sku', 'nunique'), total_demand=('total_demand', 'sum'), std_demand=('std_demand', 'mean'), avg_demand=('avg_demand', 'mean'), avg_cov_demand=('cov_demand', 'mean'), )
2.3、XYZ数据可视化
-
为了将 XYZ 数据可视化,了解每个类别在一年中的需求变化情况,我们需要再次调整我们的数据。 我们将创建一个名为 df_monthly 的新DataFrame,并将使用 groupby() 函数按产品的 xyz_class 对产品进行分组,然后按月汇总收入。
df_monthly = df_12m_units.groupby('xyz_class').agg( m1=('m1', 'sum'), m2=('m2', 'sum'), m3=('m3', 'sum'), m4=('m4', 'sum'), m5=('m5', 'sum'), m6=('m6', 'sum'), m7=('m7', 'sum'), m8=('m8', 'sum'), m9=('m9', 'sum'), m10=('m10', 'sum'), m11=('m11', 'sum'), m12=('m12', 'sum'), ) df_monthly.head()
-
为了方便可视化,我们需要将数据从宽格式转换为长格式,使用 unstack() 函数并重置索引并重命名列
df_monthly_unstacked = df_monthly.unstack('xyz_class').to_frame() df_monthly_unstacked = df_monthly_unstacked.reset_index().rename(columns={'level_0': 'month', 0: 'demand'}) df_monthly_unstacked.head()
-
先可视化 X 类商品,全年对这些产品的需求相对稳定。 在 产品季节性较弱的企业中,X类商品的需求几乎没有变化,但当前的业务上来看,还是有一些变化。表明该零售商的季节性 SKU 比大多数零售商多。
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="month", y="demand", data=df_monthly_unstacked[df_monthly_unstacked['xyz_class']=='X'], palette="Blues_d")\ .set_title("X class demand by month",fontsize=15)
-
Y 类商品的需求显示出较强的波动,10月11月的需求最多
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="month", y="demand", data=df_monthly_unstacked[df_monthly_unstacked['xyz_class']=='Y'], palette="Blues_d")\ .set_title("Y class demand by month",fontsize=15)
-
Z 类 SKU 是最不可预测的,因为有些商品每年仅销售一次。
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="month", y="demand", data=df_monthly_unstacked[df_monthly_unstacked['xyz_class']=='Z'], palette="Blues_d")\ .set_title("Z class demand by month",fontsize=15)
-
我们把XYZ类的数据绘制到一张图中,需求变化的对比会更加明显
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="month", y="demand", hue="xyz_class", data=df_monthly_unstacked, palette="Blues_d")\ .set_title("XYZ demand by month",fontsize=15)
2.4、ABC库存分析
-
XYZ 分析仅从需求商品数量角度角度来衡量,因此我们接下来需要做 ABC 库存分析从销售金额的角度进行分析。 这里将重新使用我们之前创建的 df_12m数据框,然后将运行聚合来计算每个 SKU 的收入指标。
df_skus = df_12m.groupby('sku').agg( unique_purchases=('sku', 'nunique'), total_units=('quantity', 'sum'), total_revenue=('revenue', 'sum'), ).sort_values(by='total_revenue', ascending=False).reset_index() df_skus.head()
-
要计算 ABC 类别,我们需要知道所有 SKU 产生的总收入以及每个 SKU 产生的累积收入贡献百分比。
-
我们需要按照SKU产生的总收入的降序对它们进行排序,这一点至关重要
-
我们可以使用 cumsum() 函数计算收入的累计总和,然后计算收入的百分比并将其存储在DataFrame中。
-
df_skus['revenue_cumsum'] = df_skus['total_revenue'].cumsum() df_skus['revenue_total'] = df_skus['total_revenue'].sum() df_skus['revenue_running_percentage'] = (df_skus['revenue_cumsum'] / df_skus['revenue_total']) * 100 df_skus.head()
-
与 XYZ 分类一样,我们创建一个函数,根据每个产品对收入百分比的贡献将其打上对应类别标签。 这里将收入最高的 80% 的 SKU 分配给 A 类,接下来的 10% 的收入产生者分配给 B 类,其余的分配给 C 类。(类别划分的阈值,可以根据具体业务情况调整)
def abc_classify_product(percentage): if percentage > 0 and percentage <= 80: return 'A' elif percentage > 80 and percentage <= 90: return 'B' else: return 'C'
-
接下来,应用上面的 abc_classify_product() 函数为每个SKU打上ABC标签。 同时我们也计算一下产品销售排名,使用 rank() 函数
df_skus['abc_class'] = df_skus['revenue_running_percentage'].apply(abc_classify_product) df_skus['abc_rank'] = df_skus['revenue_running_percentage'].rank().astype(int) df_skus.head()
-
接下来检查ABC每个类的统计数据:
-
A 类中有 856个,产生了我们 80% 的收入
-
B 类中有 542个,产生了接下来的 10%
-
C 类中的 2713个,它们产生了底部 10% 的销售额
-
显然我们的精力要主要关注 A 和 B 类SKU
-
df_abc = df_skus.groupby('abc_class').agg( total_skus=('sku', 'nunique'), total_units=('total_units', sum), total_revenue=('total_revenue', sum), ).reset_index() df_abc
-
以下对ABC每个类别的销售收入进行可视化 ,可以看到我们使用的 80/10/10 分割
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="abc_class", y="total_revenue", data=df_abc, palette="Blues_d")\ .set_title("Revenue by ABC class",fontsize=15)
-
C 类中存在大量 SKU,仅占公司总收入的 10%。
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="abc_class", y="total_skus", data=df_abc, palette="Blues_d")\ .set_title("SKUs by ABC class",fontsize=15)
-
从销量来看,A类明显有很多,占收入的80%,但C类也有很多货,说明这里有很多便宜的季节性商品,或者 只是零星销售的东西。
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="abc_class", y="total_units", data=df_abc, palette="Blues_d")\ .set_title("Quantity by ABC class",fontsize=15)
2.5、ABC-XYZ库存分析
-
最后一步是将 XYZ 和 ABC 标签数据结合起来。 我们将从 ABC 分析中选择我们想要保留的关键指标并创建一个新的DataFrame,然后我们将其加入 XYZ 标签。
df_abc = df_skus[['sku','abc_class','abc_rank','total_revenue']] df_xyz = df_12m_units.copy() df_abc_xyz = df_abc.merge(df_xyz, on='sku', how='left') df_abc_xyz.head()
-
创建ABC-XYZ分类
-
现在我们已经将所有数据集合并到了一起,我们可以通过将 abc标签连接到 xyz标签来创建 ABC-XYZ 类名称
-
我们通过对新的 abc_xyz标签进行分组并按类别计算 SKU 的数量、需求量、平均需求和总收入来创建一个新的汇总统计DataFrame。
-
df_abc_xyz['abc_xyz_class'] = df_abc_xyz['abc_class'].astype(str) + df_abc_xyz['xyz_class'].astype(str)
-
通过下面的结果看出
-
我们的大部分收入来自 AY,AZ 类产品,这些类别,贡献了大部分收入,但需求波动比较大
-
在底部,我们有一小部分 C 类 SKU,它们贡献很小,但从好的方面来说变化不大,因此更容易预测。
-
df_abc_xyz_summary = df_abc_xyz.groupby('abc_xyz_class').agg( total_skus=('sku', 'nunique'), total_demand=('total_demand', sum), avg_demand=('avg_demand', 'mean'), total_revenue=('total_revenue', sum), ).reset_index() df_abc_xyz_summary.sort_values(by='total_revenue', ascending=False)
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="abc_xyz_class", y="total_skus", data=df_abc_xyz_summary, palette="Blues_d")\ .set_title("SKUs by ABC-XYZ class",fontsize=15)
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="abc_xyz_class", y="total_revenue", data=df_abc_xyz_summary, palette="Blues_d")\ .set_title("Revenue by ABC-XYZ class",fontsize=15)
f, ax = plt.subplots(figsize=(15, 6)) ax = sns.barplot(x="abc_xyz_class", y="total_demand", data=df_abc_xyz_summary, palette="Blues_d")\ .set_title("Demand by ABC-XYZ class",fontsize=15)
三、业务解读
ABC XYZ分类的业务解读
-
通过组合 ABC 和 XYZ 数据,我们生成了九个不同的类。 带有 A 前缀的类都贡献高收入,B 类贡献中等收入,C 类贡献低收入。
-
X 后缀的类别需求稳定,易于预测和管理。 Y 后缀的类具有可变但可预测的需求,因此比 X 类更难管理,但仍然容易实现。 Z 类是最难的。 它们零星地以不同的数量出售,因此更难预测和管理。
-
AX:高价值 需求稳定 易于预测 易于管理 BX:中等价值 需求稳定 易于预测 易于管理 CX:低价值 需求稳定 易于预测 易于管理
-
AY:高价值 可变需求 更难预测 更难管理 BY:中等价值 可变需求 更难预测 更难管理 CY:低价值 可变需求 更难预测 更难管理
-
AZ:高价值 零星需求 难以预测 难以管理 BZ:中等价值 零星需求 难以预测 难以管理 CZ:低价值 零星需求 难以预测 难以管理
库存管理手段应用
-
避免缺货的最简单解决方是多备货,这有很多优点:
-
损失更少的销售额,所以 AOV 和收入会更高;
-
更好的客户保留率和更高的满意度,因为客户在看到商品缺货时可能去竞品购买
-
不需要紧急补货,通常紧急补货需要溢价
-
-
保持库存高冗余也有一些缺点:
-
库存上花费更多;更多的资金被库存占用;
-
需要更多空间、设备和人员来处理库存;
-
保险费用会随着您的库存估值而上涨。
-
我们需要找到合适的平衡点
-
国际注册专业会计师协会在 ABC XYZ 分类的实际应用方面提供了一些非常好的建议,可以采用以下方法:
-
AX:自动补货 需求稳定 易于预测 易于管理 BX:自动补货 需求稳定 易于预测 易于管理 CX:自动补货 需求稳定 易于预测 易于管理
-
AY:半自动补货 低缓冲 更难预测 更难管理 BY:半自动补货 可变需求 更难预测 更难管理 CY:半自动补货 可变需求 更难预测 更难管理
-
AZ:按订单购买 无缓冲 无库存 BZ:下单订货 无缓存 无库存 难以管理 CZ:自动补货 高缓冲 难以预测 难以管理
小结
-
ABC库存管理模型:从收入角度SKU分成高贡献,中贡献和低贡献三类
-
XYZ库存管理模型:从销售变化角度讲SKU分成需求波动小,需求波动中等,需求波动大三类
-
ABC-XYZ模型将所有SKU划分成9类
-
需求波动我们可以通过变异系数进行计算
-
ABCXYZ类别阈值划应结合不同业务情况划定