はじめに
サプライ・チェイン・アナリティクスで最初に行うことは、需要データに対するABC分析である。 商品の需要量というのは、売れるものはたくさん売れるが、その数はごく少数であり、他のたくさんのそんなに売れない商品が山ほどあるという性質を持つ。 これをパレートの法則(全体の数値の大部分は、全体を構成するうちの一部の要素が生み出しているという理論。別名、80:20の法則、もう1つの別名、ばらつきの法則)と呼ぶ。
ここでは、仮想の企業の需要を生成し、それに対してABC分析を行う。 同時に、商品を売れている順に順位をつけ、順位の時系列的な変化を示すランク分析を提案する。
さらに、簡単な在庫分析を行う。これは、平均需要量や生産固定費用から、生産ロットサイズや安全在庫量を計算するものであり、 古典的な経済発注量モデルや安全在庫モデル(新聞売り子モデル)に基づくものである。
prod_df = pd.read_csv(folder+"Prod.csv",index_col=0)
prod_df.head()
demand_df = pd.read_csv(folder+"demand.csv",index_col=0)
demand_df.head()
Kaggleデータの読み込み
以下で配布されているデータを読み込む.
https://www.kaggle.com/kyanyoga/sample-sales-data
Kaggleデータを用いたい場合には,以下を実行する. ただし,製品データがないので,製品関連の関数は適用できない.
kaggle_df = pd.read_csv(folder + "sales_data_sample.csv",encoding="latin")
kaggle_df.head()
kaggle_df["date"] = pd.to_datetime(kaggle_df.ORDERDATE)
kaggle_df.rename(columns={"PRODUCTLINE":"prod", "CITY":"cust", "QUANTITYORDERED":"demand", "SALES":"sales"}, inplace=True)
demand_df = kaggle_df[["date","cust","prod","demand","sales"]].copy()
demand_df.head()
fig = demand_tree_map(demand_df);
plotly.offline.plot(fig);
ABC分析とランク分析を行うための関数 abc_analysis
ABC分析のための関数を記述する、基本的には、需要データ demand_df だけあれば良いが、顧客や製品に関連した量を分析に加えたいときには、顧客データ cust_df や 製品データ prod_df も読み込んでおく。
古典的なABC分析では、3つのカテゴリーに製品や顧客を分類していたが、場合によっては4つに分類したい場合もあるだろう。 ここでは、より一般的にユーザーが与えた任意の数への分類を行う関数を準備する、カテゴリーに含まれる需要量を、ユーザーが与えた閾値をもとにして分類を行う。
引数:
- df: ABC分析を行うための需要データフレーム.列に集約を行うための列と値を格納した列が必要.
- threshold: A,B,C の分類を行うための閾値.上位 threshold[0]の要素がAランク,次に上位の threshold[1]がBランク、と順に決めていく。 リストの長さは(アルファベットの数以下の)任意の正数であるが、合計が1以上になるような数値のリストとして与える.
- agg_col: 集約を行う列名を文字列として与える.
- value_column: A,B,Cの分類を行うための数値データを保管した列名を文字列として与える.
- abc_name: 分類した結果を入れるための列名を文字列として与える.
- rank_name: 数値データの順位を入れた列名を文字列として与える.
返値: 以下の3つのオブジェクトのタプル:
- agg_df: agg_colによって集約したデータをvalue_columnの数値の大きい順に並べたデータフレーム.abcとrankの情報が付加されている.
- new_df: 元のデータフレームにabc_nameで与えた列にA(=0),B(=1),C(=2)の分類を,rank_nameで与えた列にランク(順位)を入れている.
- category: 0,1,2, ...をキーとして与えると A,B,C,... ランクに属するデータ名(agg_colの要素)のリストを値として返す辞書.
顧客・製品の組に対するABC分析 abc_analysis_all
顧客・製品ごとに需要予測を行う際に,予測しなくても良い組を予め抜き出しておくことが重要になる.そのため,顧客・製品の組に対してABC分析とランク分析を行う関数を準備しておく.
引数:
- df: ABC分析を行うための需要データフレーム.列に集約を行うための列と値を格納した列が必要.
- threshold: A,B,C の分類を行うための閾値.上位 threshold[0]の要素がAランク,次に上位の threshold[1]がBランク、と順に決めていく。
返値: 以下の3つのオブジェクトのタプル:
- agg_df: 顧客・製品によって集約したデータをdemandの数値の合計(sum)の大きい順に並べたデータフレーム. abcとrankと需要の標準偏差 (std) の情報が付加されている.
- category: 0,1,2, ...をキーとして与えると A,B,C,... ランクに属するデータ名(agg_colの要素)のリストを値として返す辞書.
agg_df_prod, new_df, category_prod = abc_analysis(
demand_df, [0.4, 0.5, 0.1], 'prod', 'demand', "prod_ABC", "prod_rank")
agg_df_cust, new_df, category_cust = abc_analysis(
demand_df, [0.4, 0.3, 0.3, 0.1], 'cust', 'demand', "customer_ABC", "customer_rank")
new_df.head()
agg_df_prod.head()
agg_df_cust.head()
agg_df,category = abc_analysis_all(demand_df, threshold=[0.4, 0.3, 0.3, 0.1])
agg_df.head()
new_prod_df = add_abc(prod_df, agg_df_prod)
new_prod_df.head()
fig = demand_tree_map_with_abc(new_df)
plotly.offline.plot(fig);
需要データフレームからABC分析の図とデータフレームを生成する関数 generate_figures_for_abc_analysis
顧客と製品の両方に対するABC分析を同時に行い、結果の図とデータフレームを同時に得るには、この関数を用いる。
引数:
- demand_df : 需要データフレーム
- value_name: 分析をするデータが入っている列名(既定値は"demand")
- cumsum : 図を累積値で描画するときには True
- cust_thres : 顧客のABC分析するときの閾値を文字列で表したもの
- prod_thres : 製品のABC分析するときの閾値を文字列で表したもの
返値:
- fig_prod : 製品のABC分析のPlotly図オブジェクト
- fig_cust : 顧客のABC分析のPlotly図オブジェクト
- agg_df_prod : 製品をインデックスとしたABC分析のデータフレーム
- agg_df_cust : 顧客をインデックスとしたABC分析のデータフレーム
- new_df : 顧客と製品のABCとランクを加えた新しい需要データフレーム
fig_prod, fig_cust, agg_df_prod, agg_df_cust, new_df = generate_figures_for_abc_analysis(
demand_df, value_name="sales", cumsum = False, cust_thres="0.7, 0.2, 0.1", prod_thres="0.7, 0.2, 0.1")
plotly.offline.plot(fig_prod);
ランク分析のための関数 rank_analysis と rank_analysis_all_periods
全ての期に対するランク分析を行う関数 rank_analysis と、期ごと(集約する単位は文字列で与える)のランク分析を行う関数 rank_analysis_all_periodsを記述する。
引数:
- df: ランク分析を行う対象の需要データフレーム.列に集約を行うための列と,値を格納した列が必要.
- agg_col: 集約を行う列名を文字列として与える.
- value_column: ランクを計算するための数値データを保管した列名を文字列として与える.
- agg_period (rank_analysis_all_periodsの場合のみ) : 集約を行う期間を "1m"(月次)のような文字列で与える.
返値:
- 名前を入れるとランク(rank_analysisの場合)もしくランクの期別リスト(rank_analysis_all_periodsの場合)を返す辞書
dic = rank_analysis(demand_df, 'cust', 'demand') #全ての期間に対する顧客需要量のランク分析
dic.popitem()
dic = rank_analysis_all_periods(demand_df, 'prod', 'demand', "1d")
#dic.popitem()
fig = show_rank_analysis(demand_df, value_name="sales", agg_period ="1d", top_rank = 10)
plotly.offline.plot(fig);
リスク共同管理分析 risk_pooling_analysis
在庫をサプライ・チェインの上流(供給側)でもつか、下流(需要側)でもつかは、複数の需要地点(顧客)における需要の相関で決まる。 一般には、上流で在庫を共有することによって在庫の削減ができる。これをリスク共同管理 (risk pooling) とよぶ。
ここでは、製品ごとに、顧客の需要の標準偏差とリスク共同管理した場合の標準偏差の差を計算する。 また、それを需要の総量で割った比率(削減率)も計算する。 これは、標準偏差を平均値で割ることによる無次元の指標(変動係数: coefficient of variation: CV)に相当するものである。
この値が大きい製品ほど、リスク共同管理の効果が大きいので、サプライ・チェインの上流で在庫を保持した方が良いことになり、 逆に小さい製品ほど、下流で在庫を保持した方が良いことになる。
引数:
- demand_df: 需要のデータフレーム
- agg_period: 標準偏差を計算する際に用いる需要の集約を行う期(規定値は週)
返値:
- inv_reduction_df : 標準偏差とその差と削減率を製品ごとに計算したデータフレーム; Rank列は製品の順位
inv_reduction_df = risk_pooling_analysis(demand_df, agg_period="1w")
inv_reduction_df.head()
fig = show_inventory_reduction(inv_reduction_df)
plotly.offline.plot(fig);
fig = show_mean_cv(demand_df, show_name=True)
#plotly.offline.plot(fig);
安全在庫、ロットサイズ、目標在庫(基在庫レベル)の設定関数 inventory_analysis
全ての需要が1つの工場で生産していると仮定したとき、その生産ロットサイズや安全在庫量は、古典的な経済発注量モデルと新聞売り子モデルで計算できる。
安全在庫量: 安全在庫係数 $z$, リード時間 $L$, 需要の標準偏差 $\sigma$ としたとき $z\sqrt{LT}\sigma$
経済発注量(生産ロットサイズ): 生産固定費用 $FC$、需要の平均値 $d$、在庫費用 $h$ としたとき $\sqrt{2 FCd/h}$
保管費率(無次元): $r$ は以下の量の和とする。
- 利子率(投資額利率)
- 保険料率: 製品の種類および企業の方針によっても異なる.
- 消耗費率および陳腐化率: 製品の腐敗,破損,目減りなどを考慮して計算
- 税率: 在庫に課せられる法的な税率(日本では0)
在庫費用: $h$ は、保管費率 $r$ に製品の価値(製品データのplnt_value列)を乗じたものを週あたりに換算したもの
目標在庫量 $=$ 安全在庫量 $+$ ロットサイズ
初期在庫量 $=$ 安全在庫量と目標在庫量の平均
引数:
- prod_df: 製品データフレーム
- demand_df: 多期間の製品別需要データフレーム
- inv_reduction_df : 標準偏差とその差と削減率を製品ごとに計算したデータフレーム
- z: 安全在庫係数
- LT: リード時間
- r: 年間保管費率
- num_days: 標準偏差を計算する際の日数(既定値は $7$)
返値: 以下の列情報を加えた製品データフレーム prod_df
- average_demand: num_days間の平均需要
- standard_deviation: 需要の標準偏差
- inv_cost: 在庫費用 $h = r \times $ plnt_value を1週間に換算したもの
- safety_inventory: 安全在庫量
- target_inventory: 目標在庫量(基在庫レベル); $d LT + z\sqrt{LT}\sigma$
- initial_inventory: 初期在庫量
prod_df = pd.read_csv(folder+"Prod.csv",index_col=0)
inv_reduction_df = risk_pooling_analysis(demand_df, agg_period="1w")
prod_df2 = inventory_analysis(prod_df, demand_df, inv_reduction_df, z = 1.65, LT = 1, r = 0.3, num_days=7)
prod_df2.set_index("index", inplace=True)
prod_df2.to_csv(folder + "Prod_with_inventory.csv")
prod_df2.head()
production_df = inventory_simulation(prod_df, demand_df)
production_df[prod_df.name[0]].head()
fig = show_prod_inv_demand(prod_df.name[3], production_df, scale="1d")
plotly.offline.plot(fig);