本記事では学習のアウトプットとして、『Python実践データ分析100本ノック』に書かれている各ノックのコードのうち、難解と思われる部分の意味を解説していきます。 「本に書かれている解説だけでは理解が難しい(;一_一)」と感じた方、この記事を読んで理解の一助となれば幸いです。
Python実戦データ分析100本ノックは、データの集計や分析のためのpandasや。グラフ描画に使用するmatplotlib、機械学習を行うためのscikit-learnなど、データ分析に欠かせない要素を実際に自分で手を動かしながら学ぶことができる本です。
今回は第15回として、ノック66-67をやっていきます。
追記:現在第二版が2022年6月に出版されています。
本記事は第1版の内容の解説です。
第二版の解説記事もいずれ書かせていただこうと思っていますので今しばしお待ちください!
第7章 ロジスティクスネットワークの最適設計を行う10本ノック
第7章では、最適化計算を行うためのライブラリを用いて、最適化問題に取り組みます。そして、ノック51-60で学習したネットワーク可視化を実施することで、最適化計算の妥当性を確認します。
ノック66:生産最適化問題を解いてみよう
ノック58でも最適化問題に取り組みましたが、最適化問題を解くには、目的関数と制約条件が必要です。そのためにpulpとortoolpyというライブラリを用いていきます。
LpVariableやlpSumはノック61でも登場していますので、そちらも参照してください。
import pandas as pd
from pulp import LpVariable, lpSum, value
from ortoolpy import model_max, addvars, addvals
df=df_material.copy()
inv=df_stock
#最大化するモデルを作成しています。
m = model_max()
#変数v1は、LpVariableを用いてdict形式で定義します。そして、制約条件をv1に定義します。
#'v%d'%(i)の部分ですが、書式化演算子の % を用いて、書式を新しく設定しています。%dは10進数に書式化します。
#つまり、df_profitリストから取り出した変数i(range(0,2))をv(変数i:10進数)という形に直しています。
#ノック61-62の解説でもう少し詳細に説明しています。
v1 = {(i):LpVariable('v%d'%(i), lowBound=0) for i in range(len(df_profit))}
#制約条件をlpSumを用いて与えます。
#v1のkey(今回はi)を動かして、
#(df_profitのデータに書かれている製品1と製品2の利益)×(上の行で作成した変数v1))の積の和を計算します。
m += lpSum(df_profit.iloc[i]*v1[i] for i in range(len(df_profit)))
for i in range(len(df_material.columns)):
#ここでは、『原料の使用量が在庫を超過しない』という制約条件を与えます。
m += lpSum(df_material.iloc[j, i] * v1[j] for j in range(len(df_profit))) <= df_stock.iloc[:, i]
#最適化問題モデルを解きます。この時点で変数v1が最適化され、最適な生産量と総利益が得られます。
m.solve()
df_plan_sol = df_plan.copy()
#v1は辞書オブジェクトであるため、要素の取り出しの際にはkeys()やitems()を使用する必要があります。
#values()は各要素の値
#items()は各要素のキー値と値のセット をそれぞれ取り出すことができます
for k, x in v1.items():
#df_plan_solにv1の各要素の値をそれぞれ格納します。
df_plan_sol.iloc[k] = value(x)
print(df_plan_sol)
print('総利益:' + str(value(m.objective)))
定義した最適化問題を解きました。
生産1と製品2の最適な生産量とその時の総利益を算出しました。
【補足説明】
制約条件の辺りがやや分かりにくいかもしれないので、もう少し詳しく解説します。
まずはdf_materialとdf_profitのデータ構造を把握しておきます。それぞれ次のようになっています。
テーブルの中身を確認したところでコードの詳細説明に移りますね。
#まず、変数iはdf_profitの行数分(2行分)の幅を持っています。つまり0~1("製品1"と"製品2")です。
m += lpSum(df_profit.iloc[i]*v1[i] for i in range(len(df_profit)))
#ここでの変数iはdf_materialの列数分(3行分)の幅を持っています。つまり0,1,2です。
for i in range(len(df_material.columns)):
#iとjがややこしいかもしれませんが、ここでのjはdf_profitの行データです。つまり("製品1"と"製品2")です。
#不等号より左側では、
#df_materialの行と列をループ変数で動かし製品1と製品2の生産に必要な原料1~原料3の数を引き出して、
#辞書オブジェクトv1との積の和を算出しています。
#不等号より右側では、
#df_materialの列をループ変数で動かして原料1~原料3の在庫数を引き出しています。
m += lpSum(df_material.iloc[j, i] * v1[j] for j in range(len(df_profit))) <= df_stock.iloc[:, i]
どうしてこんな形式になっているかというと、df_materiaのテーブルの形に合わせてテーブル内の数字を引き出そうとしているからです。
df_profitの行とdf_materialの行は製品名が記載されており一致しているので、できるだけ少ないループ変数定義でテーブルデータを引き出したいからなんです。
同じく、df_materialの列とdf_stockの列は原料名が記載されており一致しているので”df_stock.iloc[:, i]”の部分では変数iを使いまわすことができます。
最後の行について、最適化前の変数を含んだ式はこのようになっています。
制約条件1 | 1 * v0 + 2 * v1 <= 40 |
制約条件2 | 4 * v0 + 4 * v1 <= 80 |
制約条件3 | 3 * v0 + 1 * v1 <= 50 |
そして目的関数はこのようになっています。
目的関数 | =5 * v0 + 4 * v1 |
ノック67:最適生産計画が制約条件内に収まっているかどうかを確認しよう
制約条件で計算した「それぞれの原料の使用量」と「在庫利用の効率」を調べる関数を定義します。
def condition_stock(df_plan, df_material, df_stock):
#np.zeros()関数は0で初期化されたndarrayを生成します。
#第一引数に生成したい配列のshape(何行×何行か)を指定します。
#len(.columns)でdf_materialの列(原料1~原料3)数3を指定しています。
#つまり、3行×1列で各要素の値が0である配列を作成します。
flag = np.zeros(len(df_material.columns))
#len(.columns)でdf_materialの列(原料1~原料3)数3回分操作を繰り返します。
for i in range(len(df_material.columns)):
temp_sum = 0
#len(.columns)でdf_materialの行(製品1~製品2)数2回分操作を繰り返します。
for j in range(len(df_material.index)):
#df_materialの行と列をループ変数で動かし、
#製品1と製品2の生産に必要な原料1~原料3の数を引き出して製品1と製品2の生産量との積の和を算出しています。
temp_sum = temp_sum + df_material.iloc[j][i] * float(df_plan.iloc[j])
#temp_sumがF1~F4の各工場の需要以上である場合、各列(各工場に対応しています)flagを1に変換します
if (temp_sum <= float(df_stock.iloc[0][i])):
flag[i] = 1
print(df_material.columns[i] + ' 使用量:' + str(temp_sum) + ' 在庫:' + str(float(df_stock.iloc[0][i])))
return flag
print('制約条件計算結果:' + str(condition_stock(df_plan_sol, df_material, df_stock)))
結果を見ると分かることは、原料1~原料3の使用量と在庫数です。
「flagの値が1となっていること」または、「使用量<=在庫」となっていれば制約条件を満たすため、今回の計算では制約条件を満たしていることが分かります。
これで最適化した後の生産計画が制約条件を満たすことが証明できました。
ここまでで、ノック66-67は完了です。お疲れ様でした。