ABC分析とランク分析とリスク共同管理分析モジュール abc とサプライ・チェイン基本分析システム SCBAS (Supply Chain Basic Analytics System)

サプライ・チェイン・アナリティクスで最初に行うことは、需要データに対するABC分析である。 商品の需要量というのは、売れるものはたくさん売れるが、その数はごく少数であり、他のたくさんのそんなに売れない商品が山ほどあるという性質を持つ。 これをパレートの法則(全体の数値の大部分は、全体を構成するうちの一部の要素が生み出しているという理論。別名、80:20の法則、もう1つの別名、ばらつきの法則)と呼ぶ。

ここでは、仮想の企業の需要を生成し、それに対してABC分析を行う。 同時に、商品を売れている順に順位をつけ、順位の時系列的な変化を示すランク分析を提案する。

さらに、簡単な在庫分析を行う。これは、平均需要量や生産固定費用から、生産ロットサイズや安全在庫量を計算するものであり、 古典的な経済発注量モデルや安全在庫モデル(新聞売り子モデル)に基づくものである。

YouTubeVideo("H2t6bHlsTP8")

データの読み込み

まずは基本となるデータを読み込む。製品データはオプションであり、需要を売り上げや需要を重量や容量で評価したい場合に使う。 基本は、需要データ demand_df だけを使えば十分である。

prod_df = pd.read_csv(folder+"Prod.csv",index_col=0)
prod_df.head()
name weight volume cust_value dc_value plnt_value fixed_cost average_demand standard_deviation inv_cost lot_size safety_inventory target_inventory initial_inventory
0 A 1 0 8 1 1 18 1015.153846 367.846749 0.005769 2516.855181 606.947136 3123.802316 1865.0
1 B 4 0 6 1 1 11 480.596154 169.345187 0.005769 1353.762658 279.419559 1633.182217 956.0
2 C 5 0 6 1 1 16 456.557692 162.361706 0.005769 1591.343248 267.896815 1859.240063 1063.0
3 D 2 0 7 1 1 16 165.230769 47.416591 0.005769 957.329619 78.237375 1035.566994 556.0
4 E 3 0 9 1 1 14 390.846154 134.714885 0.005769 1377.282348 222.279560 1599.561908 910.0
demand_df = pd.read_csv(folder+"demand.csv",index_col=0)
demand_df.head()
date cust prod demand sales
0 2019-01-01 札幌市 A 1 10
1 2019-01-01 札幌市 B 0 0
2 2019-01-01 札幌市 C 0 0
3 2019-01-01 札幌市 D 0 0
4 2019-01-01 札幌市 E 0 0

実際問題のデータの読み込み

実際問題を解きたい場合には、上と同じ形式のデータを作成して、demand_dfという名前のデータフレームに読みこむ。

需要と売り上げのtreemapを生成する関数 demand_tree_map

引数:

  • 需要データフレーム(需要 demand と売り上げ sales の列を含む)

返値:

  • Plotlyのtreemapオブジェクト

demand_tree_map[source]

demand_tree_map(demand_df)

需要と売り上げのtreemapを生成する関数

demand_tree_mapの使用例

dataモジュールのdemand_attribute_compute関数を用いてsales(売り上げ)列を計算する。 それを用いてtreemapを描画する。

demand_df = demand_attribute_compute(demand_df, prod_df, "cust_value", "sales")
fig = demand_tree_map(demand_df)
#fig.show()
Image("../figure/treemap.png")

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_analysis[source]

abc_analysis(df, threshold, agg_col, value_column, abc_name, rank_name)

ABC分析のための関数

abc_analysis関数の使用例

以下では、製品と顧客に対してABC分析を行い、得られた3種類のデータフレーム(元のデータフレームに列を追加したもの:new_df、製品データフレームagg_df_prod、顧客データフレーム:agg_df_cust)を示す。

  • 確認用の小規模例

  • 製品を売り上げを元に4つのクラスに、閾値[0.6, 0.2, 0.1, 0.1]を用いて分類 (これを行うためには、data_generationモジュールのdemand_attribute_compute関数で、Salesの列を生成しておく必要がある。)

  • 製品を需要量を元に3つのクラスに、閾値[0.4, 0.5, 0.1]を用いて分類

  • 顧客を需要量を元に4つのクラスに、閾値[0.3, 0.3, 0.3, 0.1]を用いて分類
df = pd.DataFrame({"prod":[1,2,3,4,5], "demand": [10,30,40,50,80]})
threshold = [0.5, 0.3, 0.2]
agg_df, new_df, category = abc_analysis(df, threshold, "prod", "demand", "abc", "rank")
print(category)
new_df
{0: ['5', '4'], 1: ['3'], 2: ['2', '1']}
prod demand abc rank
0 1 10 C 4
1 2 30 C 3
2 3 40 B 2
3 4 50 A 1
4 5 80 A 0
#agg_df_sales, new_df, category_sales = abc_analysis(
#    demand_df, [0.6, 0.2, 0.1, 0.1], 'prod', 'sales', "sales ABC", "sales Rank")
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.3, 0.3, 0.3, 0.1], 'cust', 'demand', "customer_ABC", "customer_rank")
new_df.head()
index cust prod demand sales prod_ABC prod_rank customer_ABC customer_rank
date
2019-01-01 0 札幌市 A 0 0 C 8 D 35
2019-01-01 1 札幌市 B 0 0 B 3 D 35
2019-01-01 2 札幌市 C 0 0 B 2 D 35
2019-01-01 3 札幌市 D 1 7 A 0 D 35
2019-01-01 4 札幌市 E 0 0 B 4 D 35
agg_df_prod.head()
demand rank abc
prod
B 200193 0 A
F 97227 1 B
C 74981 2 B
J 25882 3 B
E 21301 4 B
agg_df_cust.head()
demand rank abc
cust
大分市 75453 0 A
神戸市 48865 1 A
水戸市 48202 2 A
富山市 35161 3 B
大阪市 27467 4 B

需要データフレームからABC分析の図とデータフレームを生成する関数 generate_figures_for_abc_analysis

顧客と製品の両方に対するABC分析を同時に行い、結果の図とデータフレームを同時に得るには、この関数を用いる。

引数:

  • demand_df : 需要データフレーム
  • 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とランクを加えた新しい需要データフレーム

generate_figures_for_abc_analysis[source]

generate_figures_for_abc_analysis(demand_df, cumsum=True, cust_thres='0.4, 0.5, 0.1', prod_thres='0.4, 0.5, 0.1')

generate_figures_for_abc_analysis関数の使用例

fig_prod, fig_cust, agg_df_prod, agg_df_cust, new_df = generate_figures_for_abc_analysis(
    demand_df, cumsum = False, cust_thres="0.7, 0.2, 0.1", prod_thres="0.7, 0.2, 0.1")
#fig_cust.show()
Image("../figure/generate_figure_for_abc.png")
agg_df_cust.head()
demand rank abc cumsum_cust
cust
松山市 83692 0 A 83692
福井市 69250 1 A 152942
山形市 36595 2 A 189537
盛岡市 17293 3 A 206830
大津市 15092 4 A 221922
new_df.head()
index cust prod demand sales prod_ABC prod_rank customer_ABC customer_rank
date
2019-01-01 0 札幌市 A 0 0 C 8 C 35
2019-01-01 1 札幌市 B 0 0 B 3 C 35
2019-01-01 2 札幌市 C 0 0 A 2 C 35
2019-01-01 3 札幌市 D 1 7 A 0 C 35
2019-01-01 4 札幌市 E 0 0 B 4 C 35

ランク分析のための関数 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の場合)を返す辞書

rank_analysis[source]

rank_analysis(df, agg_col, value_column)

全期間分のランク分析のための関数

rank_analysis_all_periods[source]

rank_analysis_all_periods(df, agg_col, value_col, agg_period)

期別のランク分析のための関数

ランク分析の関数の使用例

全期間分の顧客需要のランク分析と、3ヶ月を1期とした各期間に対する製品のランク分析。

#demand_df = pd.read_csv(folder + "demand.csv", index_col="date")
dic = rank_analysis(demand_df, 'cust', 'demand') #全ての期間に対する顧客需要量のランク分析
dic.popitem()
('名古屋市', 47)
dic = rank_analysis_all_periods(demand_df, 'prod', 'demand', "3m") #3ヶ月を1期として製品のランク分析
dic.popitem()
('D', [4, 4, 4, 4])

ランク分析の可視化関数 show_rank_analysis

ランクの時系列的な変化を表す図を生成するための関数。

引数:

  • df: ランク分析を行う対象の需要データフレーム.列に集約を行うための列と,値を格納した列が必要.
  • agg_period: 集約を行う期間を "1m"(月次)のような文字列で与える.
  • top_rank: 上位 top_rank 個のものだけを表示する

返値:

  • Plotlyの図オブジェクト

show_rank_analysis[source]

show_rank_analysis(demand_df, agg_period='1m', top_rank=1)

ランク分析の可視化関数

show_rank_analysis関数の使用例

fig = show_rank_analysis(demand_df, agg_period ="1m", top_rank = 10)
#fig.show()
Image("../figure/show_rank_analysis.png")

リスク共同管理分析 risk_pooling_analysis

在庫をサプライ・チェインの上流(供給側)でもつか、下流(需要側)でもつかは、複数の需要地点(顧客)における需要の相関で決まる。 一般には、上流で在庫を共有することによって在庫の削減ができる。これをリスク共同管理 (risk pooling) とよぶ。

ここでは、製品ごとに、顧客の需要の標準偏差とリスク共同管理した場合の標準偏差の差を計算する。 また、それを需要の総量で割った比率(削減率)も計算する。 これは、標準偏差を平均値で割ることによる無次元の指標(変動係数: coefficient of variation: CV)に相当するものである。

この値が大きい製品ほど、リスク共同管理の効果が大きいので、サプライ・チェインの上流で在庫を保持した方が良いことになり、 逆に小さい製品ほど、下流で在庫を保持した方が良いことになる。

引数:

  • demand_df: 需要のデータフレーム
  • agg_period: 標準偏差を計算する際に用いる需要の集約を行う期(規定値は週)

返値:

  • inv_reduction_df : 標準偏差とその差と削減率を製品ごとに計算したデータフレーム; Rank列は製品の順位

risk_pooling_analysis[source]

risk_pooling_analysis(demand_df, agg_period='1w')

リスク共同管理の効果を見るための関数

在庫を顧客側においた場合と、倉庫側においた場合の差を、標準偏差を計算することによって推定する。

risk_pooling_analysis関数の使用例

1週間を単位とした標準偏差をもとに、倉庫に置いた場合と工場に置いた場合の差を計算し、それを需要の総量で除した削減率(ReductionRationの列)を計算する。 Plotlyによる可視化では、削減率の大きいものから棒グラフで表示し、需要の大きさのランクで色分けをする。

inv_reduction_df = risk_pooling_analysis(demand_df, agg_period="1w")
inv_reduction_df.head()
prod agg_std sum_std reduction reduction_ratio
rank
9 D 54.869128 103.007492 48.138365 0.005095
10 A 53.558054 98.783859 45.225805 0.004980
8 G 95.078579 138.773958 43.695379 0.003027
7 H 106.326120 147.853907 41.527787 0.002660
6 I 144.471385 185.021633 40.550248 0.001984

在庫削減量の可視化関数 show_inventory_reduction

show_inventory_reduction[source]

show_inventory_reduction(inv_reduction_df)

在庫削減量の可視化関数

fig = show_inventory_reduction(inv_reduction_df)
#fig.show()
Image("../figure/show_inventory_reduction.png")

安全在庫、ロットサイズ、目標在庫(基在庫レベル)の設定

全ての需要が1つの工場で生産していると仮定したとき、その生産ロットサイズや安全在庫量は、古典的な経済発注量モデルと新聞売り子モデルで計算できる。

  • 安全在庫量: 安全在庫係数 $z$, リード時間 $L$、需要の標準偏差 $\sigma$ としたとき $z\sqrt{LT}\sigma$

  • 経済発注量(生産ロットサイズ): 生産固定費用 $FC$、需要の平均値 $d$、在庫費用 $h$ としたとき $\sqrt{2 FCd/h}$

  • 保管費率(無次元): $r$ は以下の量の和とする。

    1. 利子率(投資額利率)
    2. 保険料率: 製品の種類および企業の方針によっても異なる.
    3. 消耗費率および陳腐化率: 製品の腐敗,破損,目減りなどを考慮して計算
    4. 税率: 在庫に課せられる法的な税率(日本では0)
  • 在庫費用: $h$ は、保管比率 $r$ に製品の価値を乗じたものを週あたりに換算したもの

  • 目標在庫量 $=$ 安全在庫量 $+$ ロットサイズ

  • 初期在庫量 $=$ 安全在庫量と目標在庫量の平均

引数:

  • prod_df: 製品データフレーム
  • demand_df: 需要データフレーム
  • inv_reduction_df : 標準偏差とその差と削減率を製品ごとに計算したデータフレーム
  • z: 安全在庫係数 
  • LT: リード時間
  • r: 保管比率
  • num_days: 標準偏差を計算する際の日数(既定値は $7$)

返値: 以下の列情報を加えた製品データフレーム prod_df

  • average_demand: 平均需要
  • standard_deviation: 需要の標準偏差
  • inv_cost: 在庫費用
  • safety_inventory: 安全在庫量
  • target_inventory: 目標在庫量(基在庫レベル)
  • initial_inventory: 初期在庫量

compute_safery_stock[source]

compute_safery_stock(prod_df, demand_df, inv_reduction_df, z=1.65, LT=1, r=0.3, num_days=7)

工場における安全在庫量の計算

工場を1箇所に集約したと仮定する。複数工場の場合には、顧客と工場の紐付け情報が必要になる。

compute_safery_stock関数の使用例

1週間を基本単位として、在庫量削減データフレームを計算し、それをもとに工場での在庫量を求める。

inv_reduction_df = risk_pooling_analysis(demand_df, agg_period="1w")
prod_df = compute_safery_stock(prod_df, demand_df, inv_reduction_df, z = 1.65, LT = 1, r = 0.3, num_days=7)
prod_df.to_csv(folder + "Prod_with_inventory.csv")
prod_df.head()
name weight volume cust_value dc_value plnt_value fixed_cost average_demand standard_deviation inv_cost lot_size safety_inventory target_inventory initial_inventory
0 A 1 0 8 1 1 18 174.653846 53.558054 0.005769 1043.954022 88.370789 1132.324811 610.0
1 B 4 0 6 1 1 11 3849.865385 1414.291432 0.005769 3831.555820 2333.580863 6165.136683 4249.0
2 C 5 0 6 1 1 16 1441.942308 539.759175 0.005769 2828.068835 890.602639 3718.671474 2304.0
3 D 2 0 7 1 1 16 181.692308 54.869128 0.005769 1003.885784 90.534061 1094.419845 592.0
4 E 3 0 9 1 1 14 409.634615 145.890668 0.005769 1409.997636 240.719603 1650.717239 945.0

生産・在庫シミュレーション inventory_simulation

上で生成したデータを用いて、シミュレーションを行う。生産は、安全在庫量を下回ったときに行われ、目標在庫量になるように生産量を決める。

引数:

  • prod_df: 製品データ
  • demand_df: 需要データ

返値:

  • キーを製品名、値を、需要 demand、在庫 inventory、生産量 production を列とした時系列データフレームとした辞書

inventory_simulation[source]

inventory_simulation(prod_df, demand_df)

(Q,R)方策のシミュレーション

inventory_simulation関数の使用例

製品 A に対する需要、在庫、生産量を表すデータフレームを表示する。

production_df = inventory_simulation(prod_df, demand_df)
production_df[prod_df.name[0]].head()
demand inventory production
2019-01-01 34 1831.0 0.0
2019-01-02 33 1798.0 0.0
2019-01-03 44 1754.0 0.0
2019-01-04 31 1723.0 0.0
2019-01-05 56 1667.0 0.0

生産、在庫、需要の可視化関数 show_prod_inv_demand

show_prod_inv_demand[source]

show_prod_inv_demand(prod_name, production_df, scale='1d')

生産、在庫、需要の可視化関数

show_prod_inv_demand関数の使用例

fig = show_prod_inv_demand(prod_df.name[3], production_df, scale="1d")
#fig.show()
Image("../figure/prod_demand_inv.png")

生産、需要、在庫の可視化

上で作成した production_df をDashで可視化するインターフェイスを作成する。

Dashを用いたABC分析のダッシュボード

demand_dfを用いて、ABC分析、ランク分析、リスク共同管理分析を行う。

封筒の裏の計算モデル

d = 250 #年間稼働日 (日/年)
h = 7.5 #稼働時間(休憩時間を除く) (h/日)
gph = 8 #guaranteed pay hours(最低保障時間) (h/日)
c1 = 20 #運転手の時給 ($/h) 
c2 = 10 #補助員の時給 ($/h)
benefit = 0.3 #福利厚生費の割合(最低保証時間分の費用に対する)
f = 10000 #トラックの固定費用 ($/年)
v = 0.08 #トラックの変動費用(燃料費除く) ($/mile)
mpg = 7 #1ガロンの走行距離 (mile/gallon) 
cpg = 2.8 #1ガロンあたりの費用 ($/gallon)
spr = 15 #spots per route (1ルートあたりの件数)(件/route)
mpr  =128 #miles per route(1ルートあたりの走行距離) (miles/route)
vc = v + cpg/mpg   #トラックの総変動費用 ($/mile)
print("トラックの総変動費用 ($/mile)",vc)

vcpr = vc*mpr   #variable cost per truck;1ルートあたりの変動費用 ($/day/truck)
print("1ルートあたりの変動費用 ($/day/truck)",vcpr)

fcpd = f/d + (c1+c2)*gph*(1.+benefit) #1日あたりの固定費用(トラック+人件費) ($/day)
print("1日あたりの固定費用(トラック+人件費) ($/day)",fcpd)

cpm =  fcpd/h/60 #cost per minute(1分あたりの固定費用) ($/minute)
print("1分あたりの固定費用($/minute)",cpm)

cpd =  (fcpd+vcpr)/spr  #cost per delivery 1件あたりの配達費用 ($/件)
print("1件あたりの配達費用 ($/件)",cpd)
トラックの総変動費用 ($/mile) 0.48
1ルートあたりの変動費用 ($/day/truck) 61.44
1日あたりの固定費用(トラック+人件費) ($/day) 352.0
1分あたりの固定費用($/minute) 0.7822222222222222
1件あたりの配達費用 ($/件) 27.562666666666665
A = 100  #面積 (miles^2)
N = 100 #件数
L = 8    #デポから顧客への平均距離 (miles)
detour = 1.5 #迂回係数
import math
total_distance = (2*N*L/spr + 0.72*math.sqrt(A*N))*detour
print("総走行距離",total_distance)

print("変動費用/日", total_distance*vc)
print("固定費用/日", N/spr*fcpd)
総走行距離 268.0
変動費用/日 128.64
固定費用/日 2346.666666666667