【定期発注方式】PythonのInventorizeライブラリで適正在庫シミュレーションしてみた
定期発注方式とは?
定期発注方式とは一定間隔で在庫数をチェックして、目標在庫数との差を発注する方式です。
1週間間隔の定期発注方式であれば、毎週決められた曜日にまとめて発注できるので、仕入先が遠くて輸送コストがかかるような場合に向いている発注方式です。
この方式では目標在庫数の設定が肝になりますが、その計算式は次のようになります。
目標在庫数=安全在庫+需要予測在庫
=√(リードタイム+発注間隔)×標準偏差*安全係数+(リードタイム+発注間隔)×1日当たりの平均出荷数
参照)適正在庫を維持するための発注数の決め方をわかりやすく【定期発注方式の場合】
Excelを使った自作シミュレーション
これをExcelでシミュレーションする方法については下記の記事にまとめています。
単一SKU用
【無料サンプル付き】適正在庫シミュレーションをExcelで簡単に作る方法
複数SKU用
【VBAサンプル付き】適正在庫シミュレーションをエクセルマクロで自動化(前編)
【VBAサンプル付き】適正在庫シミュレーションをエクセルマクロで自動化(後編)
Pythonを使った自作シミュレーション
またPythonでシミュレーションする方法については下記の記事にまとめています。
単一SKU用
Pythonで適正在庫シミュレーションソフトを作ってみた【単一SKU用】
複数SKU用
行列演算を使った適正在庫シミュレーションをPythonで実装する【複数SKU用】
Pythonのinventorizeライブラリでシミュレーション
以上のようにExcelでもPythonでも比較的簡単に自作できるのですが、Pythonのinventorizeというライブラリーを使えば、たったの1行で全く同じシミュレーション結果が得られることがわかりました。
使う関数はPeriodic_review_normalです。
関数の引数の意味
指定する引数は次の通りです。
単位期間は日としています。
Demand:需要データをリストとして渡します。90日間のシミュレーションをする場合には、要素数90のリストになります。
Mean:需要データの平均です。データはシミュレーション期間前のものを使います。
Sd:需要データの標準偏差です。データはシミュレーション期間前のものを使います。
Leadtime:発注してから納品されるまでの日数です。
service_level:1―許容欠品率です。
Review_period:発注間隔日数です。
shortage_cost:欠品した場合に発生する商品1個当たりのペナルティ金額です。必須項目ではありません。
inventory_cost:商品1個を1日在庫しておくのにかかる在庫維持コストです。必須項目ではありません。
ordering_cost:1回の発注にかかるコストです。必須項目ではありません。
関数の実行方法
それでは実際の例で試してみましょう。
データは下記の記事で行ったシミュレーションと同じものを使います。
Pythonで適正在庫シミュレーションソフトを作ってみた【単一SKU用】
あるアイテムについて次のような需要実績データがあるものとします。
この時、最初の45日間のデータで需要の平均と標準偏差を求め、残りの75日間のデータでシミュレーションするものとします。
まず、需要の平均と標準偏差を求め、avとsdにそれぞれ格納します。
import numpy as np
import pandas as pd
# データフレームに読み込む
raw_df = pd.read_csv('raw_data.csv')
# 日付を文字列から日付データに変換
raw_df['Date'] = pd.to_datetime(raw_df['Date'])
# 最初の45日分を学習データ、残りをテストデータに分ける
learn_df = raw_df.iloc[:45]
test_df = raw_df.iloc[45:]
# 学習データをNumpy配列に変換する
learn_data = learn_df.to_numpy()
# 学習データから需要の平均と標準偏差を求める
av = learn_data[:, 1].mean()
sd = learn_data[:, 1].std(ddof = 1)
そして、その他のパラメータを次のように設定します。
ld = 3 # リードタイム=3日
oc = 2 # 発注サイクル=2日
so = 0.05 # 許容欠品率=5%
これで必要な引数はすべて定義できましたので、次のようにしてPeriodic_review_normalを実行します。
result = inv.Periodic_review_normal(
test_df['Item_A'],
av,
sd,
ld,
1-so,
oc
)
実行後、resultの中身を見てみると次のようになっています。
前半には75日分のシミュレーション結果がデータフレーム形式で表示されています。
毎日の需要や在庫数の経過などが表示されています。
後半は辞書形式になっていて、75日間の平均在庫数や安全在庫数などのサマリーが表示されています。
今回はshortage_cost、inventory_cost、ordering_costを引数として指定しなかっため、欠品コスト/在庫維持コスト/発注コストはゼロになっています。
シミュレーション結果の検証
それでは日々のシミュレーション結果の中身を検証してみましょう。
見やすくするためにデータフレームをcsvファイルとして出力します。
result[0].to_csv('result.csv')
このcsvファイルの中身を見ていきます。
最初の10日分のシミュレーション結果は次のようになっています。
まず各コラムの意味ですが、”demand”は関数の引数として与えた需要数です。
“sales”は売上数ですが、需要数と同じです。
“inventory_level”は手元にある在庫数、”inventory_position”は発注済みでまだ入庫されていない分(パイプライン在庫)も含めた在庫数です。
“order”は発注数です。
“max”は目標在庫数で、発注日が来たら“inventory_position”と目標在庫数との差を発注します。
“received”は入庫数です。
1日目は初期値で何も動きはありません。
2日目には950個売れるので、在庫数は初期の8036個から950個減って7086個になります。
同時に発注間隔が2日なので、発注数も決めます。
この日の在庫数である7086個が目標在庫数の8036個になるように発注数を決めるため、950個が発注数になりますが、発注行為自体は次の日の3日目に行います。(ここが少しわかりにくい)
この950個は発注してから3日後の6日目に入庫します。
3日目には160個売れるので在庫は7086引く160で6926個になります。
しかしこの日に950個を発注するので、在庫ポジションは6926足す950で7876個になります。
4日目は355個売れるので在庫数、在庫ポジションとも355個減り、それぞれ6571個、7521個になります。
同時にこの日は発注数を決める日なので目標在庫数8036個から在庫ポジション7521個を引いた515個を発注数と決め、発注自体は次の日ので5日目に行います。
以下同様に繰り返すことにより、毎日の在庫数の推移が計算されます。
Pythonを使った自作シミュレーションとの比較
それではこの結果がPythonで自作シミュレーションした結果と一致しているかを確認してみましょう。
在庫数の推移を比べて見ると次のようになりました。
x = test_data[:, 0]
plt.figure(figsize=(10, 6))
plt.plot(x, current_stock_list, label = 'Stock_1')
plt.plot(x, result[0]['inventory_level'][1:], label = 'Stock_2')
plt.legend()
plt.ylabel("inventory")
plt.show
“Stock_1”が自作シミュレーションによる在庫推移、”Stock_2”がライブラリによる在庫推移を表しています。
少しずつズレていることがわかります。
原因はライブラリでは発注日に発注数を計算するが発注自体は次の日に行っているためでした。
なぜこのようにしているのか理由は不明ですが、自作シミュレーションでも発注日の次の日に発注するように変えることによって2つの在庫推移はピッタリと一致しました。
具体的には下記の赤枠部分をiからi-1に変えました。
このようにPythonでわざわざ自分でコーディングしなくても、inventorizeライブラリを使って適正在庫シミュレーションが簡単にできることがわかりました。