ロジスティックネットワーク設計システム MEta Logistic network Optimization System

簡易システムビデオ

MELOS (MEta Logistic Optimization System)

MEta Logistic network Optimization System-GF (Green FIeld)

YouTubeVideo("x9c0UQo699g")
YouTubeVideo("ae6Yqish4uM")

連続施設配置問題

平面上に施設が配置可能と仮定した施設問題に対する近似解法を考える。

施設数が1つの場合には、Weiszfeld法と呼ばれる反復法で近似解が得られることが知られている。 複数施設に対しては、k-means法と似た反復解法が考えられる。ここでは、重み付き複数施設配置問題に対するWeiszfeld法を構築する。

以下では、1つの施設を選択するためのWeiszfeld法について解説する。

各顧客 $i$ は平面上に分布しているものとし,その座標を $(X_i,Y_i)$ とする. 顧客は重み $w_i$ をもち,目的関数は施設と顧客の間の距離に重みを乗じたものの和とする. 顧客と施設間の距離は $\ell_p$ ノルム($1 \leq p$)を用いて計算されるものとする. 東京都市圏の道路ネットワークでは $p \approx 1.8901$ と推定されており, 実務的には Euclidノルム($p=2$)に迂回係数($1.3$ 程度) を乗じたものを使う場合が多い.

顧客の集合 $I$ に対して,単一の倉庫の配置地点 $(X,Y)$ を決定する問題は, $$ f(X,Y) = \sum_{i \in I} w_i \left\{ (X-X_i)^p +(Y-Y_i)^p \right\}^{1/p} $$ を最小にする $(X,Y) \in \mathbf{R}^2$ を求める問題になる. これは,以下の Weiszfeld法を拡張した解法を用いることによって容易に求解できる.

上の関数は凸関数であるので, $(X,Y)$ が最適解である必要十分条件は,$(X,Y)$ が $$ \frac{\partial f(X,Y)}{\partial X} =0 $$ および $$ \frac{\partial f(X,Y)}{\partial Y} =0 $$ を満たすことである. $$ \frac{\partial f(X,Y)}{\partial X} =  \sum_{i \in I} w_i (X-X_i) \frac{ |X-X_i|^{p-2}}{\left\{ (X-X_i)^p +(Y-Y_i)^p \right\}^{(p-1)/p} } =0 $$ は陽的に解くことができないが, 以下の $X$ に対する繰り返し式を示唆している. $$ X^{(q+1)}= \frac{ \sum_{i \in I} w_i |X^{(q)}-X_i|^{p-2} X_i/ \left\{ (X^{(q)}-X_i)^p +(Y^{(q)}-Y_i)^p \right\}^{(p-1)/p} }{ \sum_{i \in I} w_i |X^{(q)}-X_i|^{p-2}/ \left\{ (X^{(q)}-X_i)^p +(Y^{(q)}-Y_i)^p \right\}^{(p-1)/p} } $$

$Y$ についても同様の式を導くことができる.

これらの式を利用した解法は,一般に不動点アルゴリズムとよばれ, $1 \leq p \leq 2$ のとき大域的最適解に収束する.

以下のコードでは移動距離は大圏距離distanceを用いる。これはgeopyから読み込んであると仮定する。 顧客の重みは、年間の製品需要量に製品の重量を乗じたものとする。

引数:

  • cust_df : 顧客データフレーム;緯度経度情報を利用する。
  • weight : 顧客の重み;重み付きの大圏距離の和を最小化するものとする。
  • num_of_facilities : 平面上に配置する施設の数を指定する。
  • epsilon : 誤差上限
  • max_iter : 反復回数の上限

返値:

  • X : 施設の緯度のリスト
  • Y : 施設の経度のリスト
  • partition : 顧客の割り当てられた施設の番号を格納したリスト

weiszfeld_numpy[source]

weiszfeld_numpy(cust_df, weight, num_of_facilities, epsilon=0.0001, max_iter=1000)

Weiszfeld法; 複数施設の連続施設配置問題の近似解法 (NumPy version)

weiszfeld[source]

weiszfeld(cust_df, weight, num_of_facilities, epsilon=0.0001, max_iter=1000)

Weiszfeld法; 複数施設の連続施設配置問題の近似解法

weiszfeld関数の適用例

cust_df = pd.read_csv(folder + "Cust.csv", index_col=0) #read customer data for using Lat/Lng
cust_df.head()
name lat lon
id
1 札幌市 43.06417 141.34694
2 青森市 40.82444 140.74000
3 盛岡市 39.70361 141.15250
4 仙台市 38.26889 140.87194
5 秋田市 39.71861 140.10250
prod_df = pd.read_csv(folder + "Prod.csv", index_col=0) #read product data (for using weight)
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
total_demand = pd.read_csv(folder + "total_demand.csv")
total_demand.head()
Unnamed: 0 prod cust demand
0 0 A さいたま市 85.23
1 1 A 京都市 67.18
2 2 A 仙台市 75.21
3 3 A 佐賀市 84.23
4 4 A 前橋市 67.18
weight_of_prod ={p:w for (p,w) in zip(prod_df.name, prod_df.weight) } #prepare weight dic.
weight_of_cust = defaultdict(int)
for row in total_demand.itertuples():
    weight_of_cust[row.cust] += weight_of_prod[row.prod]*row.demand 
weight = [weight_of_cust[row.name] for row in cust_df.itertuples()] 

# 関数呼び出し
X, Y, partition = weiszfeld_numpy(cust_df, weight, num_of_facilities = 10, epsilon=0.0001, max_iter = 10)
error= 0 787.1858678800214
error= 1 120.07150815288585
error= 2 11.052680692947462
error= 3 0.0
cust_df["weight"] = weight
cust_df.to_csv(folder + "melos-gf.csv")

連続施設配置の可視化関数

引数:

  • cust_df : 顧客データフレーム
  • X : 施設の緯度のリスト
  • Y : 施設の経度のリスト
  • partition : 顧客の割り当てられた施設の番号を格納したリスト

返値:

  • fig: Plotlyの図オブジェクト

show_optimized_continuous_network[source]

show_optimized_continuous_network(cust_df, X, Y, partition)

連続施設配置の可視化関数

fig = show_optimized_continuous_network(cust_df, X, Y, partition)
#fig.show()
Image("../figure/weiszfeld.png")

インスタンス生成

日本の郵便番号データの読み込み

read_data[source]

read_data(filename='../data/zipcode.csv')

ランダムに地点をサンプリング

sample_locations[source]

sample_locations(df, n_locations, rnd_stat)

インスタンスを1つ生成する関数 mk_instance

mk_instance[source]

mk_instance(df, n_plants, n_dcs, n_custs, n_prods, seed)

複数のインスタンスを生成するジェネレータ関数 make_instances

mk_instances[source]

mk_instances()

インスタンス生成のテスト

class TestInstances(unittest.TestCase):
    def test_one(self):
        df = read_data()
        for n in [10, 100, 1000]:
            print(f"testing location sample, n:{n}")
            for seed in range(1,11):
                print(f"instance {n}:{seed}")
                (province, town, address, latitude, longitude) = sample_locations(df, n, seed)
                nout = 0
                for i in province:
                    print(i, latitude[i], longitude[i], town[i])
                    nout += 1
                    if nout >= 3:
                        print("...")
                        break

    def test_two(self):
        for (weight, cust, plnt, dc, dc_lb, dc_ub, demand, plnt_ub, name) in mk_instances():
            for dic in [cust, plnt, dc]:
                nout = 0
                for i in dic:
                    print(i, dic[i], name[i])
                    nout += 1
                    if nout >= 3:
                        print("...\n")
                        break
test = TestInstances()
#test.test_one()
#test.test_two()

lnd

  • lnd: Gurobi or mupulp models for logistic network design

There are two models:

  • lnd_ms, for multiple-source supply

  • lnd_ss, for single-source supply

The aim is to select a predefined number of places for implementing a distribution center. Data are:

  • customer locations

  • potential distribution center locations

  • plant locations

  • demand, bounds and costs

複数ソース問題

lnd_ms(weight, cust, dc, dc_lb, dc_ub, plnt, plnt_ub, demand, tp_cost, del_cost, dc_fc, dc_vc, dc_num)[source] Logistics network design, multiple source

Gurobi model for multiple-source LND

Return type object of class Model, as defined in gurobipy

Weight weight[p] -> unit weight for product p

Cust dict associating a customer id to its location as (latitute, longitude)

Dc dict associating a distribution center id to its (latitute, longitude)

Dc_lb dc_lb[k] -> lower bound for distribution center k [not used]

Dc_ub dc_ub[k] -> upper bound for distribution center k

Plnt dict associating a plant id to its (latitute, longitude)

Plnt_ub plnt_ub[k] -> upper bound for plant k

Demand demand[k,p] -> units of p demanded by customer k

Tp_cost unit transportation cost (from the plant)

Del_cost unit delivery cost (from a dc)

Dc_fc fixed cost for opening a dc

Dc_vc unit (variable) cost for operating a dc

Dc_num (maximum) number of distribution centers to open

複数ソースモデルの定式化

集合・パラメータ・変数を以下に示す.

集合:

  • $Cust$ : 顧客(群)の集合
  • $Dc$ : 倉庫の集合
  • $Plnt$ : 工場の集合
  • $Prod$ : 製品(群)の集合

パラメータ:

需要量,固定費用,取扱量(生産量)上下限の数値は,単位期間(既定値は365日)に変換してあるものとする.

  • $c_{ij}$ : 地点 $ij$ 間の1単位重量あたりの移動費用
  • $w_{p}$ : 製品 $p$ の重量
  • $d_{kp}$ : 顧客 $k$ における製品 $p$ の需要量
  • $LB_j, UB_j$ : 倉庫 $j$ の取扱量の下限と上限;入庫する製品量の下限と上限を表す.
  • $NLB, NUB$ : 開設する倉庫数の下限と上限
  • $f_j$ : 倉庫 $j$ を開設する際の固定費用
  • $v_j$ : 倉庫 $j$ における製品1単位あたりの変動費用
  • $PLB_{ip}, PUB_{ip}$ : 工場 $i$ における製品 $p$ の生産量上限

変数:

  • $x_{ijp}$ : 地点 $ij$ 間の製品 $p$ のフロー量

  • $y_{j}$: 倉庫 $j$ を開設するとき $1$, それ以外のとき $0$ を表す$0$-$1$変数

定式化:

$$ \begin{array}{lll} minimize & \sum_{ i \in Plnt, j \in Dc, p \in Prod} (w_p c_{ij} + v_j) x_{ijp} + \sum_{j \in Dc, k \in Cust, p \in Prod} w_p c_{jk} x_{jkp} + \sum_{j \in Dc} f_j y_j & \\ s.t. & \sum_{j \in Dc} x_{jkp} = d_{kp} & \forall k \in Cust, p \in Prod \\ & \sum_{ i \in Plnt} x_{ijp} = \sum_{k \in Cust} x_{jkp} & \forall j \in Dc, p \in Prod \\ & x_{jkp} \leq d_{kp} y_j & \forall j \in Dc, k \in Cust, p \in Prod \\ & LB_j \leq \sum_{i \in Plnt, p \in Prod} x_{ijp} \leq UB_j & \forall j \in Dc \\ & \sum_{j \in Dc} x_{ijp} \leq PUB_{ip} & \forall i \in Plnt, p \in Prod \\ & NUB \leq \sum_{j in Dc} y_j \leq NUB & \end{array} $$

lnd_ms[source]

lnd_ms(weight, cust, dc, dc_lb, dc_ub, plnt, plnt_ub, demand, tp_cost, del_cost, dc_fc, dc_vc, dc_num)

Logistics network design, multiple source

Gurobi model for multiple-source LND

:rtype: object of class Model, as defined in gurobipy :weight: weight[p] -> unit weight for product p :cust: dict associating a customer id to its location as (latitute, longitude) :dc: dict associating a distribution center id to its (latitute, longitude) :dc_lb: dc_lb[k] -> lower bound for distribution center k [not used] :dc_ub: dc_ub[k] -> upper bound for distribution center k :plnt: dict associating a plant id to its (latitute, longitude) :plnt_ub: plnt_ub[k,p] -> upper bound for plant k for p :demand: demand[k,p] -> units of p demanded by customer k :tp_cost: tp_cost[i,j] -> unit transportation cost from plant i to dc j :del_cost: tp_cost[i,j] -> unit delivery cost from dc i to customer j :dc_fc: fixed cost for opening a dc :dc_vc: unit (variable) cost for operating a dc :dc_num: (maximum) number of distribution centers to open

単一ソース問題

lnd_ss(weight, cust, dc, dc_lb, dc_ub, plnt, plnt_ub, demand, tp_cost, del_cost, dc_fc, dc_vc, dc_num)[source] Logistics network design, single source

Gurobi model for single-source LND

Return type object of class Model, as defined in gurobipy

Weight weight[p] -> unit weight for product p

Cust dict associating a customer id to its location as (latitute, longitude)

Dc dict associating a distribution center id to its (latitute, longitude)

Dc_lb dc_lb[k] -> lower bound for distribution center k [not used]

Dc_ub dc_ub[k] -> upper bound for distribution center k

Plnt dict associating a plant id to its (latitute, longitude)

Plnt_ub plnt_ub[k] -> upper bound for plant k

Demand demand[k,p] -> units of p demanded by customer k

Tp_cost unit transportation cost (from the plant)

Del_cost unit delivery cost (from a dc)

Dc_fc fixed cost for opening a dc

Dc_vc unit (variable) cost for operating a dc

Dc_num (maximum) number of distribution centers to open

単一ソースモデルの定式化

集合・パラメータ・変数を以下に示す.

集合:

  • $Cust$ : 顧客(群)の集合
  • $Dc$ : 倉庫の集合
  • $Plnt$ : 工場の集合
  • $Prod$ : 製品(群)の集合

パラメータ:

需要量,固定費用,取扱量(生産量)上下限の数値は,単位期間(既定値は365日)に変換してあるものとする.

  • $c_{ij}$ : 地点 $ij$ 間の1単位重量あたりの移動費用
  • $w_{p}$ : 製品 $p$ の重量
  • $d_{kp}$ : 顧客 $k$ における製品 $p$ の需要量
  • $LB_j, UB_j$ : 倉庫 $j$ の取扱量の下限と上限;入庫する製品量の下限と上限を表す.
  • $NLB, NUB$ : 開設する倉庫数の下限と上限
  • $f_j$ : 倉庫 $j$ を開設する際の固定費用
  • $v_j$ : 倉庫 $j$ における製品1単位あたりの変動費用
  • $PLB_{ip}, PUB_{ip}$ : 工場 $i$ における製品 $p$ の生産量上限

変数:

  • $x_{ijp}$ : 地点 $ij$ 間の製品 $p$ のフロー量

  • $y_{j}$: 倉庫 $j$ を開設するとき $1$, それ以外のとき $0$ を表す$0$-$1$変数

  • $z_{jk}$: 顧客 $k$ が倉庫 $j$ から配送されるとき $1$, それ以外のとき $0$ を表す$0$-$1$変数

定式化:

$$ \begin{array}{lll} minimize & \sum_{ i \in Plnt, j \in Dc, p \in Prod} (w_p c_{ij} + v_j) x_{ijp} + \sum_{j \in Dc, k \in Cust, p \in Prod} w_p c_{jk} d_{kp} z_{jk} + \sum_{j \in Dc} f_j y_j & \\ s.t. & \sum_{j \in Dc} z_{jk} = 1 & \forall k \in Cust \\ & \sum_{ i \in Plnt} x_{ijp} = \sum_{k \in Cust} d_{kp} z_{jk} & \forall j \in Dc, p \in Prod \\ & z_{jk} \leq y_j & \forall j \in Dc, k \in Cust \\ & LB_j \leq \sum_{i \in Plnt, p \in Prod} x_{ijp} \leq UB_j & \forall j \in Dc \\ & \sum_{j \in Dc} x_{ijp} \leq PUB_{ip} & \forall i \in Plnt, p \in Prod \\ & NUB \leq \sum_{j in Dc} y_j \leq NUB & \end{array} $$

lnd_ss[source]

lnd_ss(weight, cust, dc, dc_lb, dc_ub, plnt, plnt_ub, demand, tp_cost, del_cost, dc_fc, dc_vc, dc_num)

Logistics network design, single source

Gurobi model for single-source LND

:rtype: object of class Model, as defined in gurobipy :weight: weight[p] -> unit weight for product p :cust: dict associating a customer id to its location as (latitute, longitude) :dc: dict associating a distribution center id to its (latitute, longitude) :dc_lb: dc_lb[k] -> lower bound for distribution center k [not used] :dc_ub: dc_ub[k] -> upper bound for distribution center k :plnt: dict associating a plant id to its (latitute, longitude) :plnt_ub: plnt_ub[k] -> upper bound for plant k :demand: demand[k,p] -> units of p demanded by customer k :tp_cost: tp_cost[i,j] -> unit transportation cost from plant i to dc j :del_cost: tp_cost[i,j] -> unit delivery cost from dc i to customer j :dc_fc: fixed cost for opening a dc :dc_vc: unit (variable) cost for operating a dc :dc_num: (maximum) number of distribution centers to open

費用生成のための関数

mk_costs[source]

mk_costs(plnt, dc, cust)

Instantiate costs to a given set of plants, dc's and customers

To be adpted to more realistic settings

:rtype: tuple with transportation costs :plnt: dict associating a plant id to its (latitute, longitude) :dc: dict associating a distribution center id to its (latitute, longitude)

最適化のテスト関数

class TestSolving(unittest.TestCase):
    def test_one(self):
        """
        Test optimizing the location of a small number of dc's from set of candidates
        """
        TIME_LIM = 300 # allow gurobi to use 5 minutes
        import time
        from mk_instances import mk_instances
        from clustering import preclustering

        models = {
            "multiple source":lnd_ms,
            "single source":lnd_ss
            }

        for k in models:
            for (weight, cust, plnt, dc, dc_lb, dc_ub, demand, plnt_ub, name) in mk_instances():
                print(f"* using {k} model *")
                print(f"*** new instance, {len(plnt)} plants + {len(dc)} dc's + {len(cust)} customers ***")
                start = time.process_time()
                (tp_cost, del_cost, dc_fc, dc_vc) = mk_costs(plnt, dc, cust)
                dc_num = (20 + len(dc))//10
             
                model = models[k](weight, cust, dc, dc_lb, dc_ub, plnt, plnt_ub, 
                                  demand, tp_cost, del_cost, dc_fc, dc_vc, dc_num)
                # model.write("lnd_ss.lp")
                model.setParam('TimeLimit', TIME_LIM)
                model.optimize()
             
                EPS = 1.e-6
                for x in model.getVars():
                    if x.X > EPS:
                        print(x.varName, x.X)
             
                end = time.process_time()
                print(f"solving MIP used {end-start} seconds")
                print()

clustering

Use scikit-learn models for clustering a set of distribution centers.

The aim is to select a predefined number of candidates for implementing a distribution center. Data are:

number of candidates to select

customer locations

potential distribution center locations

plant locations

See TestSolving below for examples of usage.

preclustering(cust, dc, prods, demand, n_clusters)

Clustering as a predecessor for optimization, to be used in logistics network design

Return type list of selected dc’s (a subset of dc)

Cust dict associating a customer id to its location as (latitute, longitude)

Dc dict associating a distribution center id to its (latitute, longitude)

Prods list (set) of products

Demand demand[k,p] -> units of p demanded by customer k

N_clusters number of clusters to use

preclustering[source]

preclustering(cust, dc, prods, demand, n_clusters)

Clustering as a predecessor for optimization, to be used in logistics network design

:rtype: list of selected dc's (a subset of dc) :cust: dict associating a customer id to its location as (latitute, longitude) :dc: dict associating a distribution center id to its (latitute, longitude) :prods: list (set) of products :demand: demand[k,p] -> units of p demanded by customer k :n_clusters: number of clusters to use

クラスタリングのテスト

class TestClustering(unittest.TestCase):
    def test_one(self):
        """
        Test optimizing the location of a small number of dc's from set of candidates
        """
        from mk_instances import mk_instances
        import time
        for (weight, cust, plnt, dc, dc_lb, dc_ub, demand, plnt_ub, name) in mk_instances():
            start = time.process_time()
            prods = weight.keys()
            n_clusters = (10 + len(dc))//5
            cluster_dc = preclustering(cust, dc, prods, demand, n_clusters)
            end = time.process_time()
            print("clustered dc's, used {} seconds".format(end-start))
            print("selected", len(cluster_dc), "dc's out of", len(dc.keys()), "possible positions")

クラスタリング後にソルバーで解を求めるテスト

class TestClusteringAndSolving(unittest.TestCase):
    def test_one(self):
        """
        Test clustering a set of potential dc's, then optimizing the location of a small number of them
        """
        TIME_LIM = 300 # allow gurobi to use 5 minutes
        import time
        from mk_instances import mk_instances
        from clustering import preclustering

        models = {
            "multiple source":lnd_ms,
            "single source":lnd_ss
            }

        for (weight, cust, plnt, dc, dc_lb, dc_ub, demand, plnt_ub, name) in mk_instances():
            # prepare costs for optimization part
            (tp_cost, del_cost, dc_fc, dc_vc) = mk_costs(plnt, dc, cust)

            # clustering part
            prods = weight.keys()
            n_clusters = (10 + len(dc))//5
            cluster_dc = preclustering(cust, dc, prods, demand, n_clusters)

            # optimization part
            start = time.process_time()
            dc_num = (90 + len(cluster_dc))//50
         
            for k in models:
                print(f"*** new instance, {len(plnt)} plants + {len(dc)} dc's + {len(cust)} customers ***")
                print(f"***** dc's clustered into {len(cluster_dc)} groups, for choosing {dc_num} dc's")
                print(f"* using {k} model *")
                model = models[k](weight, cust, cluster_dc, dc_lb, dc_ub, plnt, plnt_ub,
                                  demand, tp_cost, del_cost, dc_fc, dc_vc, dc_num)
                model.setParam('TimeLimit', TIME_LIM)
                model.optimize()
                # model.write("lnd.lp")
             
                EPS = 1.e-6
                for x in model.getVars():
                    if x.X > EPS:
                        print(x.varName, x.X)
             
                end = time.process_time()
                print(f"solving MIP used {end-start} seconds")
                print()

地点間の距離の分布の可視化関数 distance_histgram

実際問題においては、あまり遠い倉庫から顧客に配送することは少なく、同じように、工場から倉庫への輸送もあまり遠いものは行わない。 そのため、遠い距離の輸送・配送経路は、あらかじめ削除しておいた方が効率的だ。 ただし、特定の工場でしか生産されたいないものは、その工場から運ぶしかないので、経路を削除しすぎると実行不能になってしまう。

以下では、そのための指針になるような可視化を考える。まずは、距離のヒストグラムを表示する。

distance_histgram[source]

distance_histgram(cust_df, dc_df, plnt_df)

工場・倉庫間と倉庫・顧客間の距離のヒストグラムを返す関数

distance_histgram関数の使用例

cust_df = pd.read_csv(folder+"Cust.csv", index_col="id")
dc_df = pd.read_csv(folder+"DC.csv", index_col=0)
plnt_df = pd.read_csv(folder+"Plnt.csv", index_col=0)
fig = distance_histgram(cust_df, dc_df, plnt_df)
#plotly.offline.plot(fig);
Image("../figure/distance_histgram.png")

輸送・配送経路の生成関数 make_network

輸送経路(工場と倉庫間)と配送経路(倉庫と顧客間)を生成する関数

引数:

  • cust_df : 顧客データフレーム
  • dc_df : 倉庫データフレーム
  • plnt_df : 工場データフレーム
  • plnt_dc_threshold : 工場・倉庫間の距離の上限 (km)
  • dc_cust_threshold : 倉庫・顧客間の距離の上限 (km)
  • unit_tp_cost : 工場・倉庫間の単位距離・重量あたりの輸送費用 (円/km/単位重量)
  • unit_del_cost : 倉庫・顧客間の単位距離・重量あたりの輸送費用 (円/km/単位重量)
  • lt_lb : リード時間(単位は日で自然数)を決めるためのパラメータで、最短リード時間(発注から到着までの最短日数)を与える。
  • lt_threshold: リード時間(単位は日で自然数)を決めるためのパラメータ;移動距離がこの値を超えたときには、1日を加算する。
  • stage_time_bound: 点上での処理に必要な時間(生産時間、処理時間)を生成のための下限と上限のタプル

返値:

  • trans_df : 輸配送ネットワークのデータフレーム;列は以下の通り。
    • from_node : 出発地点
    • to_node : 到着地点
    • dist : 距離 (km)
    • cost : 費用 (円)
    • lead_time : リード時間
    • stage_time: 点上での処理に必要な時間(生産時間、処理時間)
    • kind: 経路の種類 ("plnt-dc", "dc-cust"のいずれかの文字列)
  • graph : NetworkXのグラフインスタンス
  • position : NetworkXの点の位置(経度、緯度のタプル)

点を表す辞書のキーに対しては、同名の工場,倉庫,顧客がある場合にエラーするので, 工場は "Plnt"、倉庫は "DC"、顧客は "Cust_"を先頭に負荷して区別するものとする。

make_network[source]

make_network(cust_df, dc_df, plnt_df, plnt_dc_threshold=999999.0, dc_cust_threshold=999999.0, unit_tp_cost=1.0, unit_del_cost=1.0, lt_lb=1, lt_threshold=800.0, stage_time_bound=(1, 1))

輸送・配送経路の生成

make_network関数の使用例

trans_df, graph, position = make_network(cust_df, dc_df, plnt_df, plnt_dc_threshold = 10000.,
                                         dc_cust_threshold = 200.,unit_tp_cost = 1., unit_del_cost = 10., lt_lb =3, lt_threshold = 800.,stage_time_bound=(1,2))
#trans_df, graph, position = make_network(cust_df, dc_df, plnt_df,unit_tp_cost = 1., unit_del_cost = 10., lt_lb =3, lt_threshold = 800.,stage_time_bound=(1,2))
trans_df.to_csv(folder + "trans_cost.csv")
trans_df.head()
from_node to_node dist cost lead_time stage_time kind
0 Odawara 札幌市 884.563252 884.563252 5 1 plnt-dc
1 Odawara 青森市 630.586550 630.586550 4 2 plnt-dc
2 Odawara 盛岡市 520.723595 520.723595 4 2 plnt-dc
3 Odawara 仙台市 363.801278 363.801278 4 1 plnt-dc
4 Odawara 秋田市 499.430315 499.430315 4 2 plnt-dc
#%matplotlib inline
nx.draw(graph,pos=position)

需要が0の地点を除く関数 remove_zero_cust

実際問題では、年間需要が全くない顧客がいる可能性がある。これは、データベースに登録された顧客だが、1年間全く発注しなかったものや、 統廃合によってすでにない顧客などがあげられる。これをあらかじデータからのぞいておく関数が、以下のremove_zero_custである。

引数:

  • cust_df: 顧客データフレーム
  • demand_df: 需要データフレーム

返値:

  • 新しい(需要が0の顧客を除いた)顧客データフレーム(需要量の合計を表す列が追加されている)。

倉庫の配置候補地点を顧客上としている場合には、倉庫の候補地点からも除くべきであるが、必ずしも除く必要がない場合もあるので、除く必要がある場合には、手作業で削除するものとする。

remove_zero_cust[source]

remove_zero_cust(cust_df, demand_df)

需要が0の地点を除く関数

demand_df = pd.read_csv(folder+"demand.csv")
new_cust_df = remove_zero_cust(cust_df, demand_df)
new_cust_df.head()
id name lat lon total_demand
0 1 札幌市 43.06417 141.34694 0.279178
1 2 青森市 40.82444 140.74000 0.347397
2 3 盛岡市 39.70361 141.15250 0.763288
3 4 仙台市 38.26889 140.87194 0.311233
4 5 秋田市 39.71861 140.10250 0.277534

サプライ・チェイン全体の可視化関数 plot_scm

引数:

  • cust_df : 顧客データフレーム
  • dc_df : 倉庫データフレーム
  • plnt_df : 工場データフレーム
  • graph : NetworkXのグラフインスタンス
  • position : NetworkXの点の位置(経度、緯度のタプル)

返値:

  • PlotlyのFigureオブジェクト(顧客、倉庫、工場と輸配送経路)

plot_scm[source]

plot_scm(cust_df, dc_df, plnt_df, graph, position)

顧客、倉庫、工場データフレームを入れると、PlotlyのFigureオブジェクトに地図を入れて返す関数

plot_scm関数の使用例

#cust_df = pd.read_csv("Cust.csv")
#dc_df = pd.read_csv("DC.csv")
#plnt_df = pd.read_csv("Plnt.csv")
fig = plot_scm(cust_df, dc_df, plnt_df, graph, position)
#plotly.offline.plot(fig);
Image("../figure/plot_scm.png")

年間(計画期間)の総需要を計算する関数 make_total_demand

引数:

  • demand_df: 需要データフレーム
  • start: 計画期間の開始日
  • finish: 計画期間の終了日
  • num_of_days: 推定したい総需要の日数(既定値は365)

返値:

  • 総需要を入れたデータフレーム

make_total_demand[source]

make_total_demand(demand_df, start='1900/01/01', finish='2050/12/31', num_of_days=365)

年間(計画期間)の総需要を計算する関数

demand_df = pd.read_csv(folder + "demand.csv")
total_demand_df = make_total_demand(demand_df, start="2019/01/01", finish="2050/12/31", num_of_days=365)
#total_demand_df = make_total_demand(demand_df)
total_demand_df.head()
prod cust demand
0 A さいたま市 85.233516
1 A 京都市 67.184066
2 A 仙台市 75.206044
3 A 佐賀市 84.230769
4 A 前橋市 67.184066

数値の桁数を表示する関数 digit_histgram

需要データと輸配送費用の数値の桁数(ならびに小数点以下の桁数)をヒストグラムで表示する。

digit_histgram[source]

digit_histgram(df, col_name='')

数値の桁数のヒストグラムを返す関数

digit_histgram関数の使用例

  • 年間需要データフレームの需要列の桁数

  • 輸配送データフレームの費用列の桁数

fig = digit_histgram(total_demand_df, "demand")
#plotly.offline.plot(fig);
Image("../figure/digit_hist_demand.png")
fig = digit_histgram(trans_df, "cost")
#plotly.offline.plot(fig);
Image("../figure/digit_hist_trans.png")

桁数を丸め

小数以下の桁数を丸めることによって、最適化が高速になる。以下では、需要を小数点2以下2桁で丸め、輸配送費用を整数に丸めている。

rounded_total_demand_df = total_demand_df.round({"demand":2})
rounded_trans_df = trans_df.round({"cost":0})
rounded_total_demand_df.head()
prod cust demand
0 A さいたま市 85.23
1 A 京都市 67.18
2 A 仙台市 75.21
3 A 佐賀市 84.23
4 A 前橋市 67.18

年間需要量 total_demand.csv ならびに輸送費用 trans_cost.csv として保管

rounded_total_demand_df.to_csv(folder + "total_demand.csv")
rounded_trans_df.to_csv(folder + "trans_cost.csv")
rounded_total_demand_df.head()
prod cust demand
0 A さいたま市 85.23
1 A 京都市 67.18
2 A 仙台市 75.21
3 A 佐賀市 84.23
4 A 前橋市 67.18
rounded_total_demand_df.tail()
prod cust demand
465 J 静岡市 172.47
466 J 高松市 205.56
467 J 高知市 109.30
468 J 鳥取市 129.35
469 J 鹿児島市 304.84

データフレームからロジスティック・ネットワーク設計モデルを解く関数

引数:

  • prod_df: 製品データフレーム
  • cust_df : 顧客データフレーム
  • dc_df : 倉庫データフレーム(固定費用、変動費用を追加)
  • plnt_df : 工場データフレーム
  • plnt_prod_df: 工場・製品データフレーム
  • total_demand_df: 総需要データフレーム
  • trans_df: 輸送ネットワークデータフレーム
  • dc_num: 開設したい倉庫数;下限と上限を指定したい場合にはタプル. 既定値は None で指定なし.
  • single_sourcing: 単一ソースの場合にTrue、それ以外のときFalse
  • max_cpu: 計算時間上限

返値:

  • flow_df: 輸・配送量が正の経路を入れたデータフレーム
  • dc_df: 倉庫の開設の有無を表す列 (open_close) を追加

solve_lnd[source]

solve_lnd(prod_df, cust_df, dc_df, plnt_df, plnt_prod_df, total_demand_df, trans_df, dc_num=None, single_sourcing=True, max_cpu=100)

データフレームからロジスティック・ネットワーク設計モデルを解く関数

solve_lnd関数の使用例

cust_df = pd.read_csv(folder + "Cust.csv")
plnt_df = pd.read_csv(folder + "Plnt.csv")
total_demand_df = pd.read_csv(folder + "total_demand.csv")
trans_df = pd.read_csv(folder + "trans_cost.csv")
dc_df = pd.read_csv(folder + "DC.csv")
prod_df = pd.read_csv(folder + "Prod.csv")
plnt_prod_df = pd.read_csv(folder + "Plnt-Prod.csv")
#flow_df, dc_df, cost_df, violation_df = solve_lnd(prod_df, cust_df, dc_df, plnt_df, plnt_prod_df, total_demand_df, 
#                           trans_df, dc_num= None, single_sourcing=False, max_cpu = 10)
#cost_df
cost value
0 transportation (plant to dc) 178848580.0
1 delivey (dc to customer) 59505434.0
2 dc fixed 79900000.0
3 dc variable 9381152.5
4 infeasible penalty 0.0
#費用の可視化
#ig = px.bar(cost_df, y="cost", x="value", orientation='h')
#plotly.offline.plot(fig);

結果の可視化関数 show_optimized_network

引数:

  • cust_df: 顧客データフレーム
  • dc_df: 倉庫データフレーム
  • plnt_df: 工場データフレーム
  • prod_df: 製品データフレーム
  • flow_df: 最適化されたフロー量を保管したデータフレーム
  • position : NetworkXの点の位置(経度、緯度のタプル)

返値:

  • Plotlyの図オブジェクト

show_optimized_network[source]

show_optimized_network(cust_df, dc_df, plnt_df, prod_df, flow_df, position)

顧客、倉庫、工場、輸・配送データフレームを入れると、PlotlyのFigureオブジェクトに地図を描画

show_optimized_network関数の使用例

fig = show_optimized_network(cust_df, dc_df, plnt_df, prod_df, flow_df, position)
#plotly.offline.plot(fig);
Image("../figure/network_design.png")