ここでは,サプライ・チェイン最適化で用いる基本的なランダムデータを生成する.生成されたデータは,csvファイルやExcelのファイルとして保管され,様々な最適化システムで用いられる.
folder = "../data/"
fns = ["demand_with_promo", "promo"]
sheet_name =["demand_with_promo", "promo"]
df ={}
for fn in fns:
df[fn] = pd.read_csv(folder+fn+".csv", index_col=0)
df[fn] = df[fn].iloc[:100,:]
with pd.ExcelWriter('forecast_small.xlsx') as writer:
for i,fn in enumerate(fns):
df[fn].to_excel(writer, sheet_name=sheet_name[i], index=False)
folder = "../data/"
fns = ["demand", "prod_for_scbas"]
df ={}
for fn in fns:
df[fn] = pd.read_csv(folder+fn+".csv", index_col=0)
with pd.ExcelWriter('scbas.xlsx') as writer:
for fn in fns:
df[fn].to_excel(writer, sheet_name=fn)
folder = "../data/melos/"
fns = ["melos-gf", "time"]
df ={}
for fn in fns:
df[fn] = pd.read_csv(folder+fn+".csv",index_col=0)
with pd.ExcelWriter('melos-gf.xlsx') as writer:
for fn in fns:
df[fn].to_excel(writer, sheet_name=fn, index=False)
folder = "../data/"
fns = ["Cust", "Prod", "demand", "DC", "Plnt", "Plnt-Prod", "time"]
df ={}
for fn in fns:
df[fn] = pd.read_csv(folder+fn+".csv", index_col=0)
with pd.ExcelWriter('melos.xlsx') as writer:
for fn in fns:
df[fn].to_excel(writer, sheet_name=fn, index=False)
folder = "../data/bom/"
fns = ["ssa01", "ssa_bom01"]
sheet_name =["stage", "bom"]
df ={}
for fn in fns:
df[fn] = pd.read_csv(folder+fn+".csv", index_col=0)
with pd.ExcelWriter('messa.xlsx') as writer:
for i,fn in enumerate(fns):
df[fn].to_excel(writer, sheet_name=sheet_name[i], index=False)
folder = "../data/optseq/"
name ="ex22_"
fns = ["act","mode", "res", "act_mode", "mode_res", "temp", "non_res", "non_lhs", "state" ]
df ={}
for i in fns:
fn =name+i
df[fn] = pd.read_csv(folder+fn+".csv", index_col=1)
df[fn].drop("Unnamed: 0", axis=1, inplace=True)
with pd.ExcelWriter('optseq.xlsx') as writer:
for i in fns:
fn =name+i
df[fn].to_excel(writer, sheet_name=i)
folder = "../data/metroIV/"
fns = ["node", "job", "vehicle", "shipment", "break", "time"]
df ={}
for fn in fns:
if fn =="break":
df[fn] = pd.read_csv(folder+fn+".csv", index_col=1)
else:
df[fn] = pd.read_csv(folder+fn+"02.csv", index_col=1)
df[fn].drop("Unnamed: 0", axis=1, inplace=True)
with pd.ExcelWriter('metro.xlsx') as writer:
for fn in fns:
df[fn].to_excel(writer, sheet_name=fn)
folder = "../data/shift/"
fns = ["period", "break", "day", "job", "staff", "requirement"]
df ={}
for fn in fns:
df[fn] = pd.read_csv(folder+fn+".csv", index_col=1)
df[fn].drop("Unnamed: 0", axis=1, inplace=True)
with pd.ExcelWriter('optshift.xlsx') as writer:
for fn in fns:
df[fn].to_excel(writer, sheet_name=fn)
顧客データのランダム生成関数 generate_cust
日本の郵便番号データからランダムに地点をサンプリングすることによって、仮想の顧客データを生成する。 社名はFakeパッケージを用いて生成する。 配送計画問題の例題を作成する場合は、あまり遠い地点を選ばないように、都道府県名を引数 prefecture で指定する。 例えば、ロジスティクス・ネットワーク設計モデルやサービスネットワーク設計モデルにおいては、日本全国からランダムに選択し、配送計画モデルにおいては1つの県から選択する。
引数:
- num_locations: 地点数
- random_seed: 乱数の種(同じ問題例が欲しい場合には、同じ種を与える。)
- prefecture: 都道府県名(例えば"千葉県"のような文字列で与える。)省略するか空白の文字列の場合には、日本全国から選択する。
- no_island: 元になる郵便番号データから離島を除いたものを使う場合 True (既定値)
返値:
- ランダムに生成された顧客データ
データの列で必須なものは,名前 (name) と緯度・経度 (lat, lon; latitude, Longitudeの略) があれば良いが、リアリティを出すために,郵便番号と住所情報も付加されているが,最適化で必要な列は以下のものだけである.
顧客データの列:
- name: 名称
- lat: 緯度
- lon: 経度
cust_df = generate_cust(num_locations = 1000, random_seed = 1, prefecture = None, no_island=True)
#cust_df.to_csv(folder+"case/cust100.csv")
cust_df.head()
df = enumerate_near_points(cust_df, 0.001)
df.head()
import pandas as pd
cust_df = pd.read_csv(folder+"Cust.csv", index_col="id")
cust_df.head()
total_demand_df = pd.read_csv(folder+"total_demand.csv")
demand_df = pd.pivot_table(total_demand_df, index="cust",values="demand")
cust_df = pd.read_csv(folder+"Cust.csv")
fig = plot_cust(cust_df, demand_df.demand)
plotly.offline.plot(fig);
製品データの生成関数 generate_prod, generate_many_prod
製品名は仮想のデータとし、大文字のアルファベットとする.
製品名をA,B,C...とする場合には,26以下の整数を製品数num_prodに指定する(generate_prod).より大きい問題例の場合には,製品名を"prod1"のように数字をつけて表す(generate_many_prod).
基本となる製品は、以下のデータをもつ。
- weight: 製品の重量を表す。これは引数 weight_bound (下限と上限のタプル) 間の一様乱数によって生成された整数とする。 (単位重量)
- volume: 製品の容積を表す。これは引数 volume_bound (下限と上限のタプル) 間の一様乱数によって生成された整数とする。(単位容積)
- cust_value : 顧客上での製品の価値を表す。これは引数 cust_value_bound (下限と上限のタプル) 間の一様乱数によって生成された整数とする。(円)
- dc_value : 倉庫上での製品の価値を表す。これは引数 dc_value_bound (下限と上限のタプル) 間の一様乱数によって生成された整数とする。(円)
- plnt_value : 工場における製品の価値を表す。これは引数 plnt_value_bound (下限と上限のタプル) 間の一様乱数によって生成された整数とする。(円)
- fixed_cost: 製品の生産固定費用を表す。これは引数fc_bound (下限と上限のタプル) 間の一様乱数によって生成された整数とする。(円/回)
引数:
- num_prod: 製品数(generate_prodの場合は26以下の整数)
- weight_bound: 製品の重量を生成するための下限と上限のタプル
- volume_bound: 製品の容積を生成するための下限と上限のタプル
- cust_value_bound: 顧客上での製品の価値を生成するための下限と上限のタプル
- dc_value_bound: 倉庫上での製品の価値を生成するための下限と上限のタプル
- plnt_value_bound: 工場における製品の価値を生成するための下限と上限のタプル
- fc_bound: 製品の生産固定費用生成のための下限と上限のタプル
- random_seed: 乱数の種
返値:
- 製品データフレーム
列名:
- name: 製品の名称
- weight: 製品の重量 (単位はkg)
- volume: 製品の容量 (単位は $m^3$)
- cust_value: 顧客上での製品の価値
- dc_value: 倉庫上での製品の価値
- plnt_value: 工場における製品の価値
- fixed_cost: 製品を生産する際の段取り(固定)費用
prod_df = generate_prod(10, weight_bound=(1, 5), cust_value_bound =(5,10), fc_bound=(10,20))
#prod_df.to_csv(folder+"Prod.csv")
prod_df.head()
#prod_df.head()
需要データの生成関数 generate_demand
顧客データと製品データを与えると、仮想の需要データを生成する関数を準備しておく。 需要は、以下のパラメータを用いて生成される。基本的な分布はパレート分布とし、週次ならびに月次の波動と誤差項を加える。
引数:
- cust_df: 顧客データフレーム
- prod_df: 製品データフレーム
- cust_shape: 顧客のパレート分布の形状を定める定数 (正の値であり,小さいほど分布が偏る.)
- prod_shape: 製品のパレート分布の形状を定める定数 (正の値であり,小さいほど分布が偏る.)
- weekly_ratio: 週次の波動を表すリスト(0が日曜日);長さは7
- yearly_ratio: 年次の波動を表すリスト(0が1月);長さは12
- start: 開始日
- periods: 計画期間数
- epsilon: 誤差項の標準偏差
- random_seed: 乱数の種
返値:
- demand_df: 需要データフレーム
列名:
- date: 日付
- cust: 顧客名
- prod: 製品名
- demand: 需要量
weekly_ratio = [1.0, 1.0, 1.2, 1.3, 0.9, 1.5, 0.2] # 0 means Sunday
yearly_ratio = [1.0 + np.sin(i) for i in range(13)] # 0 means January
demand_df = generate_demand(cust_df, prod_df, cust_shape=1.7, prod_shape=1.6, weekly_ratio=weekly_ratio, yearly_ratio=yearly_ratio,
start="2019/01/01", periods=365, epsilon=1.)
#demand_df.to_csv(folder + "demand_all.csv")
demand_df.head()
weekly_ratio = [1.1, 1.2, 1.0, 0.9, 0.8, 0.95, 1.0] # 0 means Sunday
yearly_ratio = [1.0 + 0.1*np.sin(i) for i in range(13)] # 0 means January
demand_df = generate_demand_normal(cust_df, prod_df, cust_loc=100., cust_scale=10, prod_loc=10., prod_scale=4., weekly_ratio=weekly_ratio, yearly_ratio=yearly_ratio,
start="2019/01/01", periods=365, epsilon=100.)
demand_df.to_csv(folder + "demand_normal.csv")
demand_df.head()
プロモーション情報の生成とプロモーションを加味した需要の生成関数 generate_demand_with_promo
引数:
- cust_df: 顧客データフレーム
- prod_df: 製品データフレーム
- promo_df: プロモーション情報を入れたデータフレーム(promo_dfは、需要と同じ日付を入れた列dateと、プロモーション名を列名とし、その効果を列データとした列を持つものとする。)
- cust_shape: 顧客のパレート分布の形状を定める定数
- prod_shape: 製品のパレート分布の形状を定める定数
- weekly_ratio: 週次の波動を表すリスト(0が日曜日);長さは7
- yearly_ratio: 年次の波動を表すリスト(0が1月);長さは12
- start: 開始日
- periods: 計画期間数
- epsilon: 誤差項の標準偏差
返値:
- demand_df: プロモーション情報を加味した需要データフレーム
start="2019/01/01"
periods =365*3
date_col = []
num_promo =2
promo_cols = [ [] for promo_ in range(num_promo)]
for t in pd.date_range(start, periods=periods):
date_col.append(t)
if t.weekday() == 2: # Tsuesday promotion
promo_cols[0].append(1)
else:
promo_cols[0].append(0)
if t.day%10 ==0: # 10th, 20th 30th day promotion
promo_cols[1].append(1)
else:
promo_cols[1].append(0)
promo_df = pd.DataFrame(data={"date": date_col, "promo_0": promo_cols[0], "promo_1": promo_cols[1]})
#promo_df.head(20)
#promo_df.to_csv(folder + "promo.csv")
promo_df.head()
weekly_ratio = [0.1, 1.0, 1.2, 1.3, 0.9, 1.5, 0.2] # 0 means Sunday
yearly_ratio = [1.0 + np.sin(i) for i in range(13)] # 0 means January
demand_with_promo_df = generate_demand_with_promo(cust_df[:], prod_df[:], promo_df, cust_shape=1.7, prod_shape=1.6, weekly_ratio=weekly_ratio, yearly_ratio=yearly_ratio,
start="2019/01/01", periods=365*2, epsilon=1.)
#demand_with_promo_df.to_csv(folder + "demand_with_promo_all.csv")
demand_with_promo_df.tail()
if TEST:
class_df = pd.DataFrame({"class_name":["C","B","A"], "basic_demand": [20, 10, 5]})
weekly_ratio = [0.0, 1.0, 1.2, 1.3, 0.9, 1.5, 0.2] # 0 means Sunday
start="2019/01/01"
periods = 60
max_lt = 30 # reservation in starts max_lt before check-in day
reserve_demand_df = generate_reservation_demand(class_df, weekly_ratio=[1]*7, start="2019/01/01", periods=7, max_lt = 1)
reserve_demand_df.to_csv(folder+"reserve_demand.csv")
reserve_demand_df.head()
OD需要量の生成
サービスネットワーク設計問題に対しては、地点間の需要量を定義する必要がある。荷物の始点を発生地点(origin)、終点を集中地点(destination)と呼ぶ。 地点間の需要量は、ODフロー量と呼ばれる。
ここでは、重力法 (gravity method) を用いて需要量を算出する。 県別の人口を入れた顧客データを読み込み、以下の式によって地点 $i$ から地点 $j$ へのODフロー量を計算する。
記号:
- $P_i$ : 地点 $i$ の人口
- $d_{ij}$ : 地点 $i,j$ 間の距離(ここでは大圏距離とする。)
- $D_{ij}$ : 地点 $i,j$ 間のODフロー量
- $\alpha$ : 発生地点の人口がODフロー量に与える影響度
- $\beta$ : 集中地点の人口がODフロー量に与える影響度
重力法: $$ D_{ij}= P_i^{\alpha} P_j^{\beta}/d_{ij} $$
都道府県の人口を入れたデータCust_with_population.csvを読み込む。
# cust_df = pd.read_csv(folder+"Cust.csv", index_col=0)
# pop_df = pd.read_csv("population.csv")
# cust_df.reset_index(inplace=True)
# cust_df["population"] = pop_df["pop"].str.replace(",","").astype(float)
# cust_df.to_csv(folder + "Cust_with_population.csv")
cust_df = pd.read_csv(folder+"Cust_with_population.csv", index_col=0)
cust_df.head()
n = len(cust_df)
D = np.zeros( (n,n) )
Distance = np.zeros( (n,n) )
alpha, beta = 1.,0.5
for i, row1 in enumerate(cust_df.itertuples()):
for j, row2 in enumerate(cust_df.itertuples()):
if i==j:
D[i,j] = 0.
else:
D[i,j] = (row1.population**alpha) * (row2.population**beta) /great_circle( (row1.lat, row1.lon), (row2.lat,row2.lon)).km
Distance[i,j] = great_circle( (row1.lat, row1.lon), (row2.lat,row2.lon)).km
od_df = pd.DataFrame(D, index=cust_df.name, columns=cust_df.name)
od_df.head()
#fig = px.imshow(D)
#fig.show()
od_df.to_csv(folder + "od.csv")
fig = px.line(demand_df, x="date", y="demand", color ="prod")
#fig = px.line(demand_with_promo_df, x="date", y="demand", color ="prod")
#plotly.offline.plot(fig)
Image("../figure/demand_series.png")
fig = px.histogram(demand_df, x="demand")
#fig = px.histogram(demand_with_promo_df, x="demand")
#plotly.offline.plot(fig)
Image("../figure/demand_hist.png")
需要の属性を生成する関数 demand_attribute_compute
需要量に応じて、以下のような諸量を計算し、需要データフレームに属性(列)として追加する関数
- 製品の売り上げ: 需要と製品の顧客上での価値($=$価格)の積
- 重量合計:需要量と製品重量の積
- 容積合計:需要量と製品容積の積
引数:
- demand_df : 需要データフレーム
- prod_df : 製品データフレーム
- attribute_col_name : 需要にかけ合わせる製品データの列名;例えば、売り上げを計算したい場合には "cust_value" とする。
- new_col_name : 計算結果を格納するための列名;例えば、売り上げを保管したい場合には "sales" とする。
返値:
- 新しい列を追加した需要データフレーム
demand_df = demand_attribute_compute(demand_df, prod_df, "cust_value", "sales")
#demand_df.to_csv(folder + "demand.csv")
demand_df.head()
倉庫データの生成関数 generate_dc
倉庫の開設可能地点は、全ての顧客上と仮定する。 製品ごとの需要に製品の重量 weight を乗じたものの合計を計算し、それを倉庫の予定開設個数で割ることによって、倉庫の容量を設定する。 倉庫の容量の下限と上限を決める率 lb_ratio, ub_ratio を乗じて、下限と上限を決める。
引数:
- cust_df: 顧客データフレーム
- demand_df: 需要データフレーム
- prod_df: 製品データフレーム
- num_dc =5: 目標とする倉庫数(これにあわせて容量を決める.
- lb_ratio=0.8: 容量の下限を決めるパラメータ
- ub_ratio =1.2: 容量の上限を決めるパラメータ
- vc_bound =(0.,0.5): 変動費用の下限と上限を表すパラメータ
- fc_bound =(10000,10000): 固定費用の下限と上限を表すパラメータ
- random_seed: 乱数の種
返値のデータフレームの列名と意味は以下の通り.
- name: 倉庫名称
- lb: 容量下限
- ub: 容量上限
- fc: 固定費用
- vc: 変動費用
- lat: 緯度
- lon: 経度
demand_df = pd.read_csv(folder+"demand.csv")
dc_df = generate_dc(cust_df, demand_df, prod_df, num_dc=5, lb_ratio=0.0, ub_ratio=10.3)
dc_df.to_csv(folder + "DC.csv")
dc_df.head()
工場データと生産情報データの生成関数 generate_plnt
工場は3つで、小田原、大阪、千葉にあると仮定する。 大阪工場では約半分の製品を、千葉では大阪工場で製造していない約半分の製品を生産でき、小田原工場では全ての製品を生産できるものとする。 生産容量は、総需要量と設定しておく。
引数:
- prod_df : 製品データフレーム
- demand_df : 需要データフレーム
- lead_time_bound: 工場での生産リード時間を決めるためのパラメータ; タプルで与えた下限と上限の間の一様整数乱数とする。
返値:
- plnt_df: 工場データフレーム
- plnt_prod_df: 工場・製品データフレーム
prod_df = pd.read_csv(folder+"Prod.csv")
demand_df = pd.read_csv(folder+"demand.csv")
plnt_df, plnt_prod_df = generate_plnt(prod_df, demand_df, lead_time_bound=(25,30))
plnt_df.to_csv(folder + "Plnt.csv")
plnt_df.head()
plnt_prod_df.to_csv(folder + "Plnt-Prod.csv")
plnt_prod_df.head()