【SCM分析2】PythonのマルチレベルABC分析で出荷傾向を分類する
在庫管理はアイテム(SKU)ごとに行うべしといっても、すべてのアイテムに同じくらいの手間をかける必要はありません。
世の中の現象はパレートの法則に従うことが多く、出荷傾向もこれに従います。
つまり、「上位約20%のアイテムで出荷数トータルの80%を占める」ということです。
これが成り立つのであれば、上位20%のアイテムの適正在庫管理を重点的に行えば、大方の在庫管理はできてしまいます。
このように分類することをABC分類とかABC分析といいます。
しかしここで問題になるのは、何に重きを置くかです。
出荷数が少ないアイテムでも、利益率の高い高級品なら重点的に管理したいでしょう。
このような場合に便利なのがマルチレベルのABC分析です。
通常のABC分類は、出荷数を基準にしたり売上高を基準にしたりというように、1つだけの基準で分類します。
マルチレベルのABC分類とは、出荷数と売上高といったように複数の基準でABC分類することです。
例えば、「この商品は出荷数ではCレベルだが売上高ではAレベル」といったような分類が一度にできます。
これをExcelでやろうとすると、データを降順に並べる⇨データの合計を計算する⇨それぞれのデータの割合を計算する⇨累積割合を計算する⇨累積割合からABCに分類する、といったことを出荷数と売上高についてそれぞれ行う必要があります。
それが、PythonのInventorizeというライブラリーを使えば一挙に計算でき、グラフで可視化することも簡単にできてしまいます。
サプライチェーン分析の第二回となる本記事では、前回使った出荷データを使って、出荷数と出荷頻度のマルチレベル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のgroupbyとagg使えば、次のように集計できます。
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を使えば手軽にできます。