【SCM分析2】PythonのマルチレベルABC分析で出荷傾向を分類する

2023年10月16日

在庫管理はアイテム(SKU)ごとに行うべしといっても、すべてのアイテムに同じくらいの手間をかける必要はありません。

世の中の現象はパレートの法則に従うことが多く、出荷傾向もこれに従います。

つまり、「上位約20%のアイテムで出荷数トータルの80%を占める」ということです。

これが成り立つのであれば、上位20%のアイテムの適正在庫管理を重点的に行えば、大方の在庫管理はできてしまいます。

このように分類することをABC分類とかABC分析といいます。

 

しかしここで問題になるのは、何に重きを置くかです。

出荷数が少ないアイテムでも、利益率の高い高級品なら重点的に管理したいでしょう。

このような場合に便利なのがマルチレベルのABC分析です。

通常のABC分類は、出荷数を基準にしたり売上高を基準にしたりというように、1つだけの基準で分類します。

マルチレベルのABC分類とは、出荷数と売上高といったように複数の基準でABC分類することです。

例えば、「この商品は出荷数ではCレベルだが売上高ではAレベル」といったような分類が一度にできます。

 

これをExcelでやろうとすると、データを降順に並べるデータの合計を計算するそれぞれのデータの割合を計算する累積割合を計算する累積割合からABCに分類する、といったことを出荷数と売上高についてそれぞれ行う必要があります。

それが、PythonInventorizeというライブラリーを使えば一挙に計算でき、グラフで可視化することも簡単にできてしまいます。

 

サプライチェーン分析の第二回となる本記事では、前回使った出荷データを使って、出荷数出荷頻度のマルチレベルABC分析を行ってみました。

【SCM分析1】曜日/物流センター別の出荷傾向をグラフで可視化する

 

アイテムごとの出荷数と出荷頻度を求める

出荷データ(csvファイル)を下記のコードでPandasのデータフレームraw_dfに読み込みます。

import pandas as pd

raw_df = pd.read_csv('raw_data6.csv')
raw_df['Date'] = pd.to_datetime(raw_df['Date']) # 日付データを日付型に変換
raw_df

まずはこれを集計して、アイテムごとの出荷数出荷頻度(出荷回数)を求めます。

前回行ったようにPandasのgroupbyagg使えば、次のように集計できます。

import numpy as np

qty_cnt_by_sku = raw_df.groupby(['SKU']).agg(Total_Qty = ('Qty', np.sum), Total_CNT = ('Qty', np.count_nonzero)).reset_index()
qty_cnt_by_sku

559のアイテムごとに出荷数と出荷頻度が集計できました。

 

出荷数でABC分類する

次に出荷数でシングルレベルのABC分類をしてみます。

Pythonのinventorizeというライブラリーをインポートして、その中のABCを使えば次のように分類できます。

import inventorize as inv

abc = inv.ABC(qty_cnt_by_sku[['SKU', 'Total_Qty']])
abc

ABCに何個ずつ分類されたかを見てみましょう。

abc.Category.value_counts()
-----
C    468
B     55
A     36

36個のアイテムがA分類されました。

これは全体のアイテム数である559の約6%に当たります。

パレートの法則よりも、更に少数のアイテムに出荷数が偏っていることがわかります。

グラフにすると次のようになります。

import seaborn as sns

sns.countplot(x = 'Category', data = abc)

このように大変偏っていることが可視化できます。

次に、ABCそれぞれのカテゴリーにおける出荷数を可視化してみましょう。

sns.barplot(x = 'Category', y = 'Total_Qty', data = abc)

アイテム数で大多数をしめていたCアイテムが、出荷数ではグラフで目に見えないほど少ないことがわかります。

 

出荷頻度でABC分類する

次に出荷頻度でABC分類してみます。

abc_2 = inv.ABC(qty_cnt_by_sku[['SKU', 'Total_CNT']])
abc_2.Category.value_counts()
-----
C    435
A     69
B     55

A分類されるアイテム数は69個で、これは全体の約12%です。

出荷数でA分類されたアイテムは6%でしたので、だいぶ偏りが小さくなっていることがわかります。

グラフで可視化すると次のようになります。

sns.countplot(x = 'Category', data = abc_2)

 

sns.barplot(x = 'Category', y = 'Total_CNT', data = abc_2)

 

出荷数と出荷頻度でマルチレベルABC分類する

次に、いよいよマルチレベルのABC分類をしてみます。

出荷数と出荷頻度の2レベルでABC分類には、inventorizeのproductmixを使って次のようにコーディングできます。

p_mix = inv.productmix(qty_cnt_by_sku['SKU'], qty_cnt_by_sku['Total_CNT'], qty_cnt_by_sku['Total_Qty'])
p_mix

Product-mixの列に分類結果が表示されました。

例えば、A-Aは出荷頻度でも出荷数でもA分類、A-Cは出荷頻度ではA分類だけれども出荷数ではC分類であることを意味します。

ちなみに2列目はsales、3列目はrevenueとなっていますが、それぞれ出荷頻度と出荷数のことです。

Productmixは元々sales(売上個数)とrevenue(売上高)でマルチレベルABC分析するためのツールで、次のような文法で使います。

 

productmix(SKUs, sales, revenue, na.rm = TRUE, plot = FALSE)

 

今回はこのsalesとしてTotal_CNT(出荷頻度)、revenueとしてTotal_Qty(出荷数)を指定したために、上記のように表示されています。

それでは、それぞれのレベルに何個ずつ分類されたかを見てみましょう。

p_mix.product_mix.value_counts()
-----
C_C    426
B_C     36
A_B     32
A_A     31
B_B     15
C_B      8
A_C      6
B_A      4
C_A      1

出荷頻度でも出荷数でもA分類(A-A)のアイテムは31個ありますが、出荷頻度ではA分類でも出荷数ではB分類のアイテム(A-B)も32個あることがわかります。

このように、多角的に分析できることがマルチレベルABC分析のメリットです。

 

それでは、この結果をグラフで可視化してみましょう。

分類されたアイテム数は次のようになります。

sns.countplot(x = 'product_mix', data = p_mix)

これを出荷頻度数で見ると次のようになります。

sns.barplot(x = 'product_mix', y = 'sales', data = p_mix)

更に出荷数の切り口で見ると次のようになります。

sns.barplot(x = 'product_mix', y = 'revenue', data = p_mix)

このように分析の切り口によって全く違う分布になることがわかります。

 

DC別にマルチレベルABC分類する

ところで、複数のDCがある場合、DCによって出荷傾向が異なる可能性が考えられます。

そこで最後に、これを分析してみます。

まず、DC別/アイテム別に出荷数と出荷頻度を集計します。

qty_cnt_by_sku_dc = raw_df.groupby(['DCCode', 'SKU']).agg(Total_Qty = ('Qty', np.sum), Total_CNT = ('Qty', np.count_nonzero)).reset_index()
qty_cnt_by_sku_dc

次に、この結果をinventorizeのproductmix_storelevelに入れます。

これは先程行ったマルチレベルABC分析を店舗別に行う関数です。

店舗をDCに置き換えることで、DC別に分析することができます。

p_mix_s = inv.productmix_storelevel(qty_cnt_by_sku_dc['SKU'], qty_cnt_by_sku_dc['Total_CNT'], qty_cnt_by_sku_dc['Total_Qty'], qty_cnt_by_sku_dc['DCCode'])
p_mix_s

この結果をDC別にABC分類の結果をカウントしてみると、次のようになります。

p_mix_s2 = p_mix_s.groupby(['storeofsku', 'product_mix']).count().reset_index().iloc[:, 0:3]
p_mix_s2.value_counts()
-----
storeofsku  product_mix  sku
DC01        A_A          31     1
DC04        A_A          24     1
DC03        B_A          3      1
            B_B          16     1
            B_C          40     1
            C_A          1      1
            C_B          14     1
            C_C          379    1
DC04        A_B          31     1
DC01        A_B          19     1
DC04        A_C          12     1
            B_A          6      1
            B_B          5      1
            B_C          41     1
            C_A          2      1
            C_B          6      1
DC03        A_C          5      1
            A_B          35     1
            A_A          30     1
DC02        C_C          176    1
            C_B          1      1
            B_C          37     1
            B_B          8      1
            B_A          3      1
            A_C          13     1
            A_B          24     1
            A_A          29     1
DC01        C_C          110    1
            C_B          3      1
            B_C          32     1
            B_B          9      1
            B_A          3      1
            A_C          12     1
DC04        C_C          107    1

この結果をDC01について可視化してみましょう。

p_mix_dc01 = p_mix_s2[p_mix_s2['storeofsku'] == 'DC01'].iloc[:, 0:3]
sns.barplot(x = 'product_mix', y = 'sku', data = p_mix_dc01)

同様にDC03について可視化してみます。

p_mix_dc03 = p_mix_s2[p_mix_s2['storeofsku'] == 'DC03'].iloc[:, 0:3]
sns.barplot(x = 'product_mix', y = 'sku', data = p_mix_dc03)

2つのDCで分布が異なることが一目でわかります。

 

まとめ

実はDC03はリージョナルDCと呼ばれる広域をカバーするDCで、DC01はローカルDCと呼ばれる狭域を担当するDCです。

ローカルDCにはある程度動きの早い商品のみを在庫して、リージョナルDCにはそれ以外の動きの遅い商品も在庫しているため、C-Cに分類される商品の割合がより大きくなっているわけです。

DC01には219アイテム、DC03には523アイテムが在庫されているのですが、この在庫配置が最適であるかどうかは再考の余地が大いにあります。

そのための材料を与えてくれるマルチレベルABC分析は重要で、Pythonを使えば手軽にできます。