本記事では学習のアウトプットとして、『Python実践データ分析100本ノック』に書かれている各ノックのコードのうち、難解と思われる部分の意味を解説していきます。 「本に書かれている解説だけでは理解が難しい(;一_一)」と感じた方、この記事を読んで理解の一助となれば幸いです。
Python実戦データ分析100本ノックは、データの集計や分析のためのpandasやグラフ描画に使用するmatplotlib、機械学習を行うためのscikit-learnなど、データ分析に欠かせない要素を実際に自分で手を動かしながら学ぶことができる本です。
私自身はpython初心者で、pythonを触ったことがありませんでしたが、自ら手を動かしてpythonを学習したいと思いこちらの本で学習しました。よければ手に取っていただきたいです。
今回は第12回として、ノック56-60をやっていきます。
追記:現在第二版が2022年6月に出版されています。
本記事は第1版の内容の解説です。
第二版の解説記事もいずれ書かせていただこうと思っていますので今しばしお待ちください
第6章 物流の最適ルートをコンサルティングする10本ノック
第6章では、物流のルートを最適化するために最適化問題に取り組みます。おおまかな流れは以下の通りです。
- 最小化または最大化したいを関数(目的関数)を定義する
- 目的関数の最小化または最大化を実現するために必要な制約条件を定義する
- 制約条件を満たしたうえで目的関数を最小化または最大化する組み合わせを選択する
以上が最適化問題の流れです。
ノック56:輸送ルート情報を読み込んでみよう
#trans_routeファイルは三つの倉庫から4つの工場にどれだけの量が輸送されているかが記載されています。
import pandas as pd
df_tr=pd.read_csv('trans_route.csv',index_col="工場")
df_tr.head()
まずは、各倉庫と各工場間の輸送ルートと輸送量が記載されたファイルを読み込みました。
ノック57:輸送ルート情報からネットワークを可視化してみよう
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
df_tr = pd.read_csv('trans_route.csv', index_col='工場')
#trans_route_posは、W1~W3とF1~F4の各工場と倉庫の位置情報を記載した、2行×7列のデータです。
df_pos = pd.read_csv('trans_route_pos.csv')
#グラフオブジェクトの作成を行います。
G = nx.Graph()
#add_node(頂点)で頂点の追加を行います。
#len(.columns)でdf_posの列(W1~W3とF1~F4まで)数7回分操作を繰り返します。
for i in range(len(df_pos.columns)):
G.add_node(df_pos.columns[i])
#辺の設定&エッジの重みのリスト化を行います。
num_pre = 0
#edge_weightsという重みを格納するためのリストを宣言します。
edge_weights = []
size = 0.1
#len(.columns)でdf_posの列(W1~W3とF1~F4まで)数7回分操作を繰り返します。
for i in range(len(df_pos.columns)):
#iと同じfor文の繰り返し構造ですが、このfor文にすることで全頂点の重みの追加を行います。
for j in range(len(df_pos.columns)):
#if notとすることで、ループ変数が同じ頂点をもつときは操作を行わないようにします。
if not (i == j):
#頂点を結び、辺の追加を行います。
G.add_edge(df_pos.columns[i], df_pos.columns[j])
#エッジの重みの追加を行います。
#1つ上のコードで追加されるedgeに対して、num_preが小さい場合if文の中の操作を行います。
if num_pre < len(G.edges):
#num_preを追加されたedge数で更新します。
num_pre = len(G.edges)
weight = 0
if (df_pos.columns[i] in df_tr.columns) and (df_pos.columns[j] in df_tr.index):
if df_tr[df_pos.columns[i]][df_pos.columns[j]]:
weight = df_tr[df_pos.columns[i]][df_pos.colunms[j]] * size
elif (df_pos.columns[j] in df_tr.columns) and (df_pos.columns[i] in df_tr.index):
if df_tr[df_pos.columns[j]][df_pos.columns[i]]:
weight = df_tr[df_pos.columns[j]][df_pos.columns[i]] * size
edge_weights.append(weight)
#座標の設定を行います。
pos = {}
#df_posの列(W1~W3とF1~F4まで)数7回分操作を繰り返します。
for i in range(len(df_pos.columns)):
#df_wの列(W1~W3とF1~F4まで)名をnodeという変数に格納します。
node = df_pos.columns[i]
#それぞれのnodeに対して、df_posの各行に記載されたデータを座標位置として設定します。
pos[node] = (df_pos[node][0], df_pos[node][1])
#draw()メソッドで描画します。
nx.draw(G, pos, with_labels=True, font_size=16, node_size = 1000, node_color = 'k', font_color='w', width=edge_weights)
#show()メソッドを用いて表示します。
plt.show()
ノック58:輸送コスト関数を作成しよう
1.最小化または最大化したいを関数(目的関数)を定義する
ここからは最適化の1つめのstepです。最小化または最大化したいを関数(目的関数)を定義します。
import pandas as pd
#trans_route.csvは輸送ルートを記載した3行(W1~W3)×4列(F1~F4)のデータです。
df_tr = pd.read_csv('trans_route.csv', index_col='工場')
#trans_cost.csvは倉庫と工場の輸送コストを記載した3行(W1~W3)×4列(F1~F4)のデータです。
df_tc = pd.read_csv('trans_cost.csv', index_col='工場')
#defで輸送コストを算出する関数を定義します。
def trans_cost(df_tr, df_tc):
#costという変数を作成し、0で初期化します。
cost = 0
#len(.index)でdf_tcの行(W1~W3)数3回分操作を繰り返します。
for i in range(len(df_tc.index)):
#len(.columns)でdf_trの列(F1~F4)数4回分操作を繰り返します。
for j in range(len(df_tr.columns)):
#輸送ルートデータの要素(量)×輸送コストデータの要素(金額)を計算し、すべて足し合わせます。
cost += df_tr.iloc[i][j] * df_tc.iloc[i][j]
return cost
#str関数でtrans_cost関数の戻り値(輸送コストの計算結果)を文字列型に変換し、「総輸送コスト:」に接続できるようにしています。
print('総輸送コスト:'+ str(trans_cost(df_tr, df_tc)))
輸送ルートを最適化するために必要な輸送コストを計算する関数を作成できました。
ノック59:制約条件を作ってみよう
2.目的関数の最小化または最大化を実現するために必要な制約条件を定義する
目的関数の最小化または最大化を実現するために必要な制約条件をif文を使用して定義します。
import pandas as pd
#trans_route.csvは輸送ルート情報を記載した3行(W1~W3)×4列(F1~F4)のデータです。
df_tr = pd.read_csv('trans_route.csv', index_col='工場')
#demand.csvは工場の製品生産量に対する需要を記載した1行×4列(F1~F4)のデータです。
df_demand = pd.read_csv('demand.csv')
#supply.csvは倉庫が供給可能な部品数の上限を記載した1行×3列(W1~W3)のデータです。
df_supply = pd.read_csv('supply.csv')
#需要側の制約条件を満たすかどうかを審査します。
#len(.columns)でdf_trの列(F1~F4)数4回分操作を繰り返します。
for i in range(len(df_demand.columns)):
#df_demand.columns[i]はF1~F4を指します。
#df_tr[df_demand.columns[i]]はdf_tr[F〇]という意味になります。
#つまり、各工場のW1~W3との輸送ルート情報を抜き出しています。
#sum()を使用して、各工場ごとのW1~W3の需要の合計を算出し、temp_sumに格納しています。
temp_sum = sum(df_tr[df_demand.columns[i]])
#str関数でF1~F4の工場名、各工場ごとのW1~W3の需要の合計、工場の製品生産量に対する需要を文字列型に変換します。
print(str(df_demand.columns[i]) + 'への輸送量:' + str(temp_sum) + ' (需要量:' + str(df_demand.iloc[0][i])+')')
#各工場ごとのW1~W3の需要の合計が工場の製品生産量に対する需要より上回る場合、需要側の制約条件を満たすと判断します。
if temp_sum >= df_demand.iloc[0][i]:
print('需要量を満たしています。')
else:
print('需要量を満たしていません。輸送ルートを再計算してください。')
#供給側の制約条件を満たすかどうかを審査します。
#len(.columns)でdf_supplyの列(W1~W3)数3回分操作を繰り返します。
for i in range(len(df_supply.columns)):
#df_supply.columns[i]はW1~W3を指します。
#df_tr.loc[df_supply.columns[i]]はdf_tr.loc[W〇]という意味になります。
#つまり、各倉庫からみたF1~F4との輸送ルート情報を抜き出しています。
#sum()を使用して、各倉庫からみたF1~F4への供給限界量の合計を算出し、temp_sumに格納しています。
temp_sum = sum(df_tr.loc[df_supply.columns[i]])
#str関数でW1~W3の倉庫名、各倉庫からみたF1~F4への供給限界量の合計、倉庫が供給可能な部品数の上限を文字列型に変換します。
print(str(df_supply.columns[i])+ 'からの輸送量:' + str(temp_sum)+ '(供給限界:' + str(df_supply.iloc[0][i])+')')
#各倉庫からみたF1~F4への供給限界量の合計が倉庫が供給可能な部品数の上限より下回る場合、供給側の制約条件を満たすと判断します。
if temp_sum <= df_supply.iloc[0][i]:
print('供給限界の範囲内です。')
else:
print('供給限界を超過しています。輸送ルートを再計算してください。')
ノック58で作成した輸送コスト関数を最適化するための需要側と供給側の制約条件を満たすかどうかを確認できるようになりました。
ノック60:輸送ルートを変更して、輸送コストの変化を確認しよう
3.制約条件を満たしたうえで目的関数を最小化または最大化する組み合わせを選択する
制約条件を満たしているかを検査する関数を定義し、制約条件を満たしたうえで目的関数を最小化または最大化する組み合わせを選択します。
import pandas as pd
import numpy as np
#trans_route_new.csvは新しく設計し直された輸送ルート情報を記載した3行(W1~W3)×4列(F1~F4)のデータです。
df_tr_new = pd.read_csv('trans_route_new.csv', index_col='工場')
print(df_tr_new)
#新しく設計し直された輸送ルート情報を用いて総輸送コストを再計算します。
#str関数で再計算結果を文字列型に変換しています。
print('総輸送コスト(変更後):' + str(trans_cost(df_tr_new, df_tc)))
#defで需要側の制約条件を計算する関数を定義します。
def condition_demand(df_tr, df_demand):
#df_demand.columnsは4(F1~F4の列数)を指します。
#np.zeros()は、すべての要素を0とする配列を生成します。
#len(4)としているので、要素数が4の配列を生成し、flagとしています。
flag = np.zeros(len(df_demand.columns))
#len(.columns)でdf_demandの列(F1~F4)数4回分操作を繰り返します。
for i in range(len(df_demand.columns)):
#df_tr[df_demand.columns[i]]はdf_tr[F〇]という意味になります。
#つまり、各工場のW1~W3との輸送ルート情報を抜き出しています。
#sum()を使用して、各工場ごとのW1~W3の需要の合計を算出し、temp_sumに格納しています。
temp_sum = sum(df_tr[df_demand.columns[i]])
#各工場ごとのW1~W3の需要の合計が工場の製品生産量に対する需要より上回る場合、需要側の制約条件を満たすと判断します。
if (temp_sum >= df_demand.iloc[0][i]):
#制約条件を満たすとき、flag配列の要素を0から1と変換します。
flag[i] = 1
return flag
#defで供給側の制約条件を計算する関数を定義します。
def condition_supply(df_tr, df_supply):
#df_supply.columnsは3(W1~W3の列数)を指します。
#len(3)としているので、要素数が3の配列を生成し、flagとしています。
flag = np.zeros(len(df_supply.columns))
#len(.columns)でdf_supplyの列(W1~W3)数3回分操作を繰り返します。
for i in range(len(df_supply.columns)):
#df_supply.columns[i]はW1~W3を指します。
#df_tr.loc[df_supply.columns[i]]はdf_tr.loc[W〇]という意味になります。
#つまり、各倉庫からみたF1~F4との輸送ルート情報を抜き出しています。
#sum()を使用して、各倉庫からみたF1~F4への供給限界量の合計を算出し、temp_sumに格納しています。
temp_sum = sum(df_tr.loc[df_supply.columns[i]])
#各倉庫からみたF1~F4への供給限界量の合計が倉庫が供給可能な部品数の上限より下回る場合、供給側の制約条件を満たすと判断します。
if temp_sum <= df_supply.iloc[0][i]:
#制約条件を満たすとき、flag配列の要素を0から1と変換します。
flag[i] = 1
return flag
print('需要条件計算結果:' +str(condition_demand(df_tr_new, df_demand)))
print('供給条件結果:' + str(condition_supply(df_tr_new, df_supply)))
実行すると、ルート変更後の総輸送コスト、需要条件計算結果(flagの各要素が0か1)、供給条件結果(需要条件計算結果と同じで0か1を表示)を確認できます。flagの全要素が1でないと、制約条件を満たせていないため、実行結果から、変更後のルートは不適であると分かります。
ここまでで、ノック56-60は完了です。お疲れ様でした。 今回は、最適な物流計画を立案する流れを学習できました。