SCMOPTのケーススタディ

ABC分析

注意:このケーススタディは,幾つかの企業のコンサルティングから抽出したもので,特定の企業を想定したものではない.データや登場人物は,架空のものである.


あなたは新たに結成されたマーケティングチームに配属された新人社員だ.新しい上司から,需要と製品の膨大なデータを渡されて,分析するようにと命令された. さて,何から始めれば良いだろうか?

まずは,データの概要を調べることから始める. これをデータ探索と呼ぶ.本来はその後でデータの前処理を行うが,ここではSCMOPTのランダムデータ生成関数を用いる.

そのため,データの概要は以下のように既知である.

  • 1000個の顧客と100個の製品があり,1年分の需要が与えられている. 顧客と製品はパレート分布にしたがい,製品の方が偏った分布になっている.
  • 全部で36500000の需要データがあり, 年次と週次の周期がある.
  • 欠損値はない.

資料とデータへのリンク

https://www.dropbox.com/sh/pnkdixxsf8i3o8t/AADU1udy_D7IZf4PQBL0dg1Wa?dl=0

random_seed = 1
cust_df = generate_cust(num_locations = 1000, random_seed = random_seed, prefecture = None, no_island=True)
prod_df = generate_many_prod(100, weight_bound=(1, 5), cust_value_bound =(5,10),
                              dc_value_bound=(2, 2), plnt_value_bound=(1, 1),
                              fc_bound=(100000,200000), random_seed=random_seed)
weekly_ratio = [0.0, 1.0, 1.2, 1.3, 0.9, 1.5, 0.2]  # 0 means Sunday
yearly_ratio = [1.0 + np.sin(i)*0.5 for i in range(13)]  # 0 means January
demand_df = generate_demand(cust_df, prod_df, cust_shape=1., prod_shape=0.9, weekly_ratio=weekly_ratio, yearly_ratio=yearly_ratio,
                            start="2019/01/01", periods=365, epsilon=1., random_seed=random_seed)
fig = plot_cust(cust_df)
plotly.offline.plot(fig);

顧客数1000,製品数100というのはそれほど大規模な問題例ではない.普通のメーカーのサブブランド程度の規模感だ. それでも(たった)1年分の需要データで, 36500000 件にもなる. csvファイルに保存しても,Excelでは開くことさえ出来ない.

サプライ・チェイン分析は,最初にこのような大きな問題例を,本質を損なわず,かつ取り扱いが容易になるようにすることから始めるのが良い.

通常の(実際の)需要データは,パレートの法則(全体の数値の大部分は、全体を構成するうちの一部の要素が生み出しているという理論。別名、80:20の法則、もう1つの別名、ばらつきの法則)に従う. これを利用して取り扱う製品数を減らすための方法が,ABC分析である.

SCMOPTに含まれる基本情報分析システムSCBASにはABC分析をするための関数が準備されているので,それを使うことにする. 製品についてはAカテゴリーに8割の需要をもつものを入れたいので,閾値に 0.8, 0.1, 0.1 と入力する.

%%time
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.8, 0.1, 0.1")
CPU times: user 33.4 s, sys: 1.27 s, total: 34.6 s
Wall time: 34.6 s

カテゴリーAに含まれる25個の製品だけで全体の80%以上をカバーしていることが分かる. Aカテゴリーの製品だけを抜き出して保管しておく. 以降では,これらの製品に絞って解析を行う.

try:
    new_df.reset_index(inplace=True)
except:
    pass
demand_df = new_df[new_df.prod_ABC=="A"].copy() #copyをしないと後でwarningが出る
demand_df.head()
index date cust prod demand prod_ABC prod_rank customer_ABC customer_rank
1 1 2019-01-01 合同会社藤本建設 第 52 支店 prod1 9 A 9 C 592
23 23 2019-01-01 合同会社藤本建設 第 52 支店 prod23 4 A 17 C 592
26 26 2019-01-01 合同会社藤本建設 第 52 支店 prod26 7 A 12 C 592
28 28 2019-01-01 合同会社藤本建設 第 52 支店 prod28 3 A 22 C 592
29 29 2019-01-01 合同会社藤本建設 第 52 支店 prod29 20 A 3 C 592

製品データフレームにABCなどの情報を入れて,Aカテゴリーの製品だけを抜き出しておく.

agg_df_prod.reset_index(inplace=True)
new_prod_df = add_abc(prod_df, agg_df_prod)
new_prod_df.head()
name weight volume cust_value dc_value plnt_value fixed_cost demand rank abc
0 prod0 2 0 7 2 1 198248 899728 67 C
1 prod1 5 0 5 2 1 173318 17943312 9 A
2 prod2 1 0 6 2 1 100525 6205975 27 B
3 prod3 3 0 9 2 1 124882 6615820 26 B
4 prod4 1 0 10 2 1 169253 1681309 57 C
prod_df = new_prod_df[ new_prod_df.abc =="A" ].copy()
prod_df.head()
name weight volume cust_value dc_value plnt_value fixed_cost demand rank abc
1 prod1 5 0 5 2 1 173318 17943312 9 A
23 prod23 1 0 7 2 1 132883 11973827 17 A
26 prod26 5 0 7 2 1 122017 15065424 12 A
28 prod28 4 0 6 2 1 146786 9855368 22 A
29 prod29 2 0 7 2 1 164331 47678205 3 A

顧客の集約

25個の製品で全体の80%以上をカバーしている. 顧客数1000は解析するには多すぎるので,地点の近さによって集約を行う.

需要の集約を行い,顧客の需要量の合計を地点の重みとして,重み付きのクラスタリングを行う.

%%time
total_demand_df, demand_cust_df, demand_prod_df  = make_total_demand(demand_df, start="2019/01/01", finish="2050/12/31", num_of_days=365)
CPU times: user 2.08 s, sys: 381 ms, total: 2.47 s
Wall time: 2.48 s
weight = demand_cust_df.demand.values

顧客間の移動時間と距離をOSRMを用いて計算しておく.

durations,  distances = compute_durations(cust_df)

集約する点の数を色々変えてkmean法でクラスタリングを行う.評価値(各地点から集約地点への重み付き距離の和)が変化しなくなる集約数を求める. それには,エルボー(肘)法を用いる.

後で集約した地点を候補地点として実際の倉庫を決めるので,曲げた肘に相当する場所(施設数が30あたり)ではなく,少し大きめの50くらいが良い. ここでは,集約数を50としてクラスタリングを行う.

集約のための方法としては,kmeans法を使うべきではない.kmeans法は高速であるためエルボー法と相性が良いが,クラスタリングは重み付き重心を求めているにすぎない.重心は距離の自乗なので,本来の重み付き距離の和を評価値にした場合とは異なった地点に収束する.重み付き直線(大圏)距離の和を最小にするための方法として反復Weiszfeld法と呼ばれる解法がある.これは不動点アルゴリズムを利用したもので,kmeans法より遅いが,より正確な地点を算出する.もう一つの方法として,階層的クラスタリングを用いたものがある.この方法は直線(大圏)距離を用いるのではなく,実際の道路距離を用いることができる.ここでは,地図から計算した本当の移動時間を尺度としてクラスタリングを行うことにする.

その結果を地図上に描画すると以下のようになる.

%%time
X, Y, partition, cost = hierarchical_clusterning(cust_df, weight, durations, num_of_facilities = 50, linkage="complete")
print(cost)
1658717743983.196
CPU times: user 97.8 ms, sys: 7.99 ms, total: 106 ms
Wall time: 105 ms