ここでは「ファイルを読み込んで問題を解く」のプログラムを外部からも利用できるようにするために,ソルバーのクラスとして独立させることを考えます.最後のステップではソルバーのクラスとメインモジュールのファイルを分離して,メインモジュールからソルバーのクラスをインポートして利用する方法を示します.
まずは,ソルバーをクラス化してメインモジュールも同じファイルに含めた状態でのプログラム全体を示します.
GochiSolver.py
import numpy as np
import pandas as pd
from ortoolpy import knapsack
import argparse
class GochiSolver:
"""
ゴチのソルバー
"""
def __init__(self):
self.W = [] # 価格
self.C = 0 # 設定金額
self.ID = []
self.Menu = []
self.Size = []
self.total = 0
self.order_ids = []
self.menu_df = None
self.order_df = None # 注文するメニューのデータフレーム
def set_data(self, weight_df, capacity):
self.W = weight_df['price'].tolist()
self.C = capacity
self.ID = weight_df['no'].tolist()
self.Menu = weight_df['menu'].tolist()
self.Size = weight_df['size'].tolist()
def show(self):
print("W:", self.W)
print("C: ", self.C)
print("ID: ", self.ID)
print("Menu: ", self.Menu)
print("Size: ", self.Size)
def solve(self):
# 問題を解く
self.total, self.order_ids = knapsack(self.W, self.W, self.C)
# 注文するかしないか
orders = np.zeros(len(self.ID), dtype=np.int64) # ゼロで初期化
orders[self.order_ids] = 1 # 注文するメニューを設定
# 全メニューのデータフレームに注文データ(0-1変数)も追加する
self.menu_df = pd.DataFrame(
{
'no': self.ID,
'menu': self.Menu,
'size': self.Size,
'price': self.W,
'order' : orders
}
)
# 注文メニューだけのデータフレーム
self.order_df = self.menu_df[self.menu_df.order > 0]
def get_args():
"""
コマンドライン引数を取り出す処理を行う関数
"""
# オブジェクトを生成する
parser = argparse.ArgumentParser()
# 設定金額はオプショナル引数
parser.add_argument("-c", "--capacity", type=int, help="Capacity")
args = parser.parse_args()
return(args)
def main():
args = get_args()
if args.capacity:
capacity = args.capacity
else:
capacity = 2980
url = "https://github.com/rinsaka/sample-data-sets/blob/master/gochi-menu.csv?raw=true"
# url = "gochi-menu.csv" # カレントディレクトリから読み込む場合
weight_df = pd.read_csv(url)
gochi = GochiSolver()
gochi.set_data(weight_df, capacity)
# gochi.show() # 確認のために読み込んだデータを表示する
gochi.solve()
if gochi.C == gochi.total:
print("解が見つかりました")
else:
print("解は見つかりませんでした")
print(f"- 差額: {gochi.total - gochi.C}")
print(f"- 設定金額: {gochi.C}")
print(f"- 注文合計: {gochi.total}")
print("")
print(gochi.order_df)
if __name__ == "__main__":
main()
上のプログラムはコマンドラインから実行することを前提にしています.次のコマンドで設定金額をデフォルトの 2980 として計算できます.
python GochiSolver.py ⏎
解が見つかりました
- 設定金額: 2980
- 注文合計: 2980.0
no menu size price order
8 8 むしホタテにんにく風味 NaN 788 1
11 11 海鮮入りおこげ NaN 898 1
21 21 黒胡椒入り鳥唐揚げ NaN 498 1
22 22 黒酢いり広東風キムチ NaN 398 1
31 31 豚足とろけるほど醤油煮込み NaN 398 1
設定金額を変更するには -c
オプションを指定します.たとえば,2985 に設定します.
python GochiSolver.py -c 2985 ⏎
解が見つかりました
- 設定金額: 2985
- 注文合計: 2985.0
no menu size price order
28 28 ビーフン五目炒め (大) 1180 1
30 30 豚スペアリブ唐揚げ特製ソースかけ (大) 1180 1
33 33 北京ダック NaN 625 1
あるいは --capacity
オプションでも設定金額を設定できます.例えば 2988 に設定します.コマンドライン引数の詳細についてはこちらを参照してください.
python GochiSolver.py --capacity 2988 ⏎
解が見つかりました
- 設定金額: 2988
- 注文合計: 2988.0
no menu size price order
1 1 かに肉と青梗菜のクリーム煮込み NaN 598 1
19 19 干し海老白菜芯炒め NaN 498 1
21 21 黒胡椒入り鳥唐揚げ NaN 498 1
22 22 黒酢いり広東風キムチ NaN 398 1
31 31 豚足とろけるほど醤油煮込み NaN 398 1
32 32 軟らかい豚バラ煮込み NaN 598 1
上のソースを作成する過程について簡単に説明します.次の通り,5行目から GochiSolver
クラスを定義します.このクラスにはインスタンス初期化のための特殊メソッド __init__( )
を設置し,いくつかのクラス属性を初期化します.また,show( )
メソッドはクラス属性の値を表示するだけの命令を入れておきます.さらに,set_data( )
と solve( )
というメソッドも準備しておきます.このとき,これら2つのメソッドの中身がまだないので,pass
とだけ入力しておきます.35行目からは main( )
モジュールです.ここでは GochiSolver
クラスからインスタンスを生成して,その属性を表示するだけにとどめておきます.
GochiSolver.py
import numpy as np
import pandas as pd
from ortoolpy import knapsack
class GochiSolver:
"""
ゴチのソルバー
"""
def __init__(self):
self.W = [] # 価格
self.C = 0 # 設定金額
self.ID = []
self.Menu = []
self.Size = []
self.total = 0
self.order_ids = []
self.menu_df = None
self.order_df = None # 注文するメニューのデータフレーム
def set_data(self, weight_df, capacity):
pass
def show(self):
print("W:", self.W)
print("C: ", self.C)
print("ID: ", self.ID)
print("Menu: ", self.Menu)
print("Size: ", self.Size)
def solve(self):
pass
def main():
gochi = GochiSolver()
gochi.show()
if __name__ == "__main__":
main()
python GochiSolver.py ⏎
W: []
C: 0
ID: []
Menu: []
Size: []
main( )
では,CSV ファイルを読み込んで weight_df
というデータフレームに格納します.また,設定金額を capacity
変数に格納します.さらに,GochiSolver
クラスの set_data( )
メソッドを呼び出し,weight_df
と capacity
を渡します.
GochiSolver.py
def main():
url = "https://github.com/rinsaka/sample-data-sets/blob/master/gochi-menu.csv?raw=true"
# url = "gochi-menu.csv" # カレントディレクトリから読み込む場合
weight_df = pd.read_csv(url)
capacity = 2980
gochi = GochiSolver()
gochi.set_data(weight_df, capacity)
gochi.show()
まだ set_data( )
メソッドを作成していないので,データはセットされていません.
python GochiSolver.py ⏎
W: []
C: 0
ID: []
Menu: []
Size: []
set_data( )
メソッドには次のような内容を指定しました.main
から weight_df
データフレームと capacity
変数を受け取るので,データフレームの各列を tolist()
関数でリストに変換してクラス属性へセットします.また,capacity
もクラス属性 self.C
へセットします.
GochiSolver.py
def set_data(self, weight_df, capacity):
self.W = weight_df['price'].tolist()
self.C = capacity
self.ID = weight_df['no'].tolist()
self.Menu = weight_df['menu'].tolist()
self.Size = weight_df['size'].tolist()
この状態でプログラムを実行すると,CSVファイルから読み込んだ内容がクラス属性に正しくセットされていることを確認できます.
python GochiSolver.py ⏎
W: [928, 598, 880, 1180, 598, 1280, 1860, 2580, 788, 2588, 898, 898, 598, 698, 698, 980, 1280, 880, 1000, 498, 698, 498, 398, 598, 680, 980, 550, 880, 1180, 880, 1180, 398, 598, 625]
C: 2980
ID: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
Menu: ['あわびのオイスターソース煮込み', 'かに肉と青梗菜のクリーム煮込み', 'タチウオ四川風炒め', 'タチウオ四川風炒め', 'にんにくの芽と蜂の巣の四川風炒め', 'フカヒレスープ', 'ふかひれ姿煮込み', 'ふかひれ姿煮込み', 'むしホタテにんにく風味', '伊勢海老チリ or マヨ', '海鮮いため', '海鮮入りおこげ', '海老サラダ', '海老チリソース', '海老マヨネーズ', '活アゲマキ貝とニンニクの芽炒め', '活アゲマキ貝とニンニクの芽炒め', '活カキ入りチヂミ', '活カキ入りチヂミ', '干し海老白菜芯炒め', '牛肉と生椎茸オイスタソース炒め', '黒胡椒入り鳥唐揚げ', '黒酢いり広東風キムチ', '蒸し豚肉の辛ソース', '赤酒粕入り豚バラ煮込みと花巻挟み', '赤酒粕入り豚バラ煮込みと花巻挟み', 'ジャージャーメン', 'ビーフン五目炒め', 'ビーフン五目炒め', '豚スペアリブ唐揚げ特製ソースかけ', '豚スペアリブ唐揚げ特製ソースかけ', '豚足とろけるほど醤油煮込み', '軟らかい豚バラ煮込み', '北京ダック']
Size: [nan, nan, '(小)', '(大)', nan, nan, '(小)', '(大)', nan, nan, nan, nan, nan, nan, nan, '(小)', '(大)', '(小)', '(大)', nan, nan, nan, nan, nan, '(小)', '(大)', nan, '(小)', '(大)', '(小)', '(大)', nan, nan, nan]
GochiSolver
クラスの solve( )
メソッドでは問題を解いて,その結果をクラス属性 (self.tatal
と self.order_ids
) に格納します.そのうえで,main
では solve( )
メソッドを呼び出して,計算結果を表示します.
GochiSolver.py
class GochiSolver:
...(省略)...
def solve(self):
# 問題を解く
self.total, self.order_ids = knapsack(self.W, self.W, self.C)
def main():
url = "https://github.com/rinsaka/sample-data-sets/blob/master/gochi-menu.csv?raw=true"
# url = "gochi-menu.csv" # カレントディレクトリから読み込む場合
weight_df = pd.read_csv(url)
capacity = 2980
gochi = GochiSolver()
gochi.set_data(weight_df, capacity)
# gochi.show() # ここはコメントアウトしています
gochi.solve()
print("total:", gochi.total)
print("ids:", gochi.order_ids)
実行すると正しく計算できていることを確認できました.
python GochiSolver.py ⏎
total: 2980.0
ids: [8, 11, 21, 22, 31]
solve( )
モジュールを改良して,計算結果をより理解しやすい(あるいは利用しやすい)形式で保存できるようにします.具体的には次のコードの8行目のとおりメニューの数と同じ大きさの配列をゼロで初期化したあと,10行目のように注文する料理にだけ1をセットします.途中に print()
文を追加しているので,実行結果にその処理過程が表示されていることに注意してください.
GochiSolver.py
class GochiSolver:
...(省略)...
def solve(self):
# 問題を解く
self.total, self.order_ids = knapsack(self.W, self.W, self.C)
print(self.order_ids)
orders = np.zeros(len(self.ID), dtype=np.int64) # ゼロで初期化
print(orders)
orders[self.order_ids] = 1 # 注文するメニューを設定
print(orders)
def main():
url = "https://github.com/rinsaka/sample-data-sets/blob/master/gochi-menu.csv?raw=true"
# url = "gochi-menu.csv" # カレントディレクトリから読み込む場合
weight_df = pd.read_csv(url)
capacity = 2980
gochi = GochiSolver()
gochi.set_data(weight_df, capacity)
gochi.solve()
python GochiSolver.py ⏎
[8, 11, 21, 22, 31]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1 0 0]
それぞれの料理を注文するかどうかのリストが準備できたので,全メニューのデータフレームを作成し,クラスの属性にセットします.これをメインモジュールで print
します.
GochiSolver.py
class GochiSolver:
...(省略)...
def solve(self):
# 問題を解く
self.total, self.order_ids = knapsack(self.W, self.W, self.C)
orders = np.zeros(len(self.ID), dtype=np.int64) # ゼロで初期化
orders[self.order_ids] = 1 # 注文するメニューを設定
# 全メニューのデータフレームに注文データ(0-1変数)も追加する
self.menu_df = pd.DataFrame(
{
'no': self.ID,
'menu': self.Menu,
'size': self.Size,
'price': self.W,
'order' : orders
}
)
def main():
url = "https://github.com/rinsaka/sample-data-sets/blob/master/gochi-menu.csv?raw=true"
# url = "gochi-menu.csv" # カレントディレクトリから読み込む場合
weight_df = pd.read_csv(url)
capacity = 2980
gochi = GochiSolver()
gochi.set_data(weight_df, capacity)
gochi.solve()
print(gochi.menu_df)
全てのメニューについて注文する (1),注文しない (0) のいずれかが表示できるようになりました.
python GochiSolver.py ⏎
no menu size price order
0 0 あわびのオイスターソース煮込み NaN 928 0
1 1 かに肉と青梗菜のクリーム煮込み NaN 598 0
2 2 タチウオ四川風炒め (小) 880 0
3 3 タチウオ四川風炒め (大) 1180 0
4 4 にんにくの芽と蜂の巣の四川風炒め NaN 598 0
5 5 フカヒレスープ NaN 1280 0
6 6 ふかひれ姿煮込み (小) 1860 0
7 7 ふかひれ姿煮込み (大) 2580 0
8 8 むしホタテにんにく風味 NaN 788 1
9 9 伊勢海老チリ or マヨ NaN 2588 0
10 10 海鮮いため NaN 898 0
11 11 海鮮入りおこげ NaN 898 1
12 12 海老サラダ NaN 598 0
13 13 海老チリソース NaN 698 0
14 14 海老マヨネーズ NaN 698 0
15 15 活アゲマキ貝とニンニクの芽炒め (小) 980 0
16 16 活アゲマキ貝とニンニクの芽炒め (大) 1280 0
17 17 活カキ入りチヂミ (小) 880 0
18 18 活カキ入りチヂミ (大) 1000 0
19 19 干し海老白菜芯炒め NaN 498 0
20 20 牛肉と生椎茸オイスタソース炒め NaN 698 0
21 21 黒胡椒入り鳥唐揚げ NaN 498 1
22 22 黒酢いり広東風キムチ NaN 398 1
23 23 蒸し豚肉の辛ソース NaN 598 0
24 24 赤酒粕入り豚バラ煮込みと花巻挟み (小) 680 0
25 25 赤酒粕入り豚バラ煮込みと花巻挟み (大) 980 0
26 26 ジャージャーメン NaN 550 0
27 27 ビーフン五目炒め (小) 880 0
28 28 ビーフン五目炒め (大) 1180 0
29 29 豚スペアリブ唐揚げ特製ソースかけ (小) 880 0
30 30 豚スペアリブ唐揚げ特製ソースかけ (大) 1180 0
31 31 豚足とろけるほど醤油煮込み NaN 398 1
32 32 軟らかい豚バラ煮込み NaN 598 0
33 33 北京ダック NaN 625 0
注文するメニューだけのデータフレームを作成します.また,解が見つかったかどうかをチェックして,見つからなかった場合にはその差額も表示するようにします.
GochiSolver.py
class GochiSolver:
...(省略)...
def solve(self):
# 問題を解く
self.total, self.order_ids = knapsack(self.W, self.W, self.C)
orders = np.zeros(len(self.ID), dtype=np.int64) # ゼロで初期化
orders[self.order_ids] = 1 # 注文するメニューを設定
# 全メニューのデータフレームに注文データ(0-1変数)も追加する
self.menu_df = pd.DataFrame(
{
'no': self.ID,
'menu': self.Menu,
'size': self.Size,
'price': self.W,
'order' : orders
}
)
# 注文メニューだけのデータフレーム
self.order_df = self.menu_df[self.menu_df.order > 0]
def main():
url = "https://github.com/rinsaka/sample-data-sets/blob/master/gochi-menu.csv?raw=true"
# url = "gochi-menu.csv" # カレントディレクトリから読み込む場合
weight_df = pd.read_csv(url)
capacity = 2980
gochi = GochiSolver()
gochi.set_data(weight_df, capacity)
gochi.solve()
if gochi.C == gochi.total:
print("解が見つかりました")
else:
print("解は見つかりませんでした")
print(f"- 差額: {gochi.total - gochi.C}")
print(f"- 設定金額: {gochi.C}")
print(f"- 注文合計: {gochi.total}")
print("")
print(gochi.order_df)
python GochiSolver.py ⏎
解が見つかりました
- 設定金額: 2980
- 注文合計: 2980.0
no menu size price order
8 8 むしホタテにんにく風味 NaN 788 1
11 11 海鮮入りおこげ NaN 898 1
21 21 黒胡椒入り鳥唐揚げ NaN 498 1
22 22 黒酢いり広東風キムチ NaN 398 1
31 31 豚足とろけるほど醤油煮込み NaN 398 1
最後に,コマンドライン引数によって設定金額を指定できるように変更します.まず,プログラムの先頭で argparse
をインポートします.
GochiSolver.py
import argparse
get_args
関数を定義して,コマンドラインのオプショナル引数として設定金額を取り出す処理を記述します.
GochiSolver.py
def get_args():
"""
コマンドライン引数を取り出す処理を行う関数
"""
# オブジェクトを生成する
parser = argparse.ArgumentParser()
# 設定金額はオプショナル引数
parser.add_argument("-c", "--capacity", type=int, help="Capacity")
args = parser.parse_args()
return(args)
def main():
args = get_args()
if args.capacity:
capacity = args.capacity
else:
capacity = 2980
url = "https://github.com/rinsaka/sample-data-sets/blob/master/gochi-menu.csv?raw=true"
# url = "gochi-menu.csv" # カレントディレクトリから読み込む場合
weight_df = pd.read_csv(url)
gochi = GochiSolver()
gochi.set_data(weight_df, capacity)
gochi.solve()
if gochi.C == gochi.total:
print("解が見つかりました")
else:
print("解は見つかりませんでした")
print(f"- 差額: {gochi.total - gochi.C}")
print(f"- 設定金額: {gochi.C}")
print(f"- 注文合計: {gochi.total}")
print("")
print(gochi.order_df)
設定金額を変更するには -c
オプションを指定します.たとえば,5555 に設定します.
python GochiSolver.py -c 2999 ⏎
解が見つかりました
- 設定金額: 2999
- 注文合計: 2999.0
no menu size price order
2 2 タチウオ四川風炒め (小) 880 1
12 12 海老サラダ NaN 598 1
21 21 黒胡椒入り鳥唐揚げ NaN 498 1
31 31 豚足とろけるほど醤油煮込み NaN 398 1
33 33 北京ダック NaN 625 1
あるいは --capacity
オプションでも設定金額を設定できます.今度は 3000 に設定すると,ちょうど 3000 になるメニューの組み合わせを見つけることができませんでした.
python GochiSolver.py --capacity 3000 ⏎
解は見つかりませんでした
- 差額: -1.0
- 設定金額: 3000
- 注文合計: 2999.0
no menu size price order
11 11 海鮮入りおこげ NaN 898 1
22 22 黒酢いり広東風キムチ NaN 398 1
24 24 赤酒粕入り豚バラ煮込みと花巻挟み (小) 680 1
31 31 豚足とろけるほど醤油煮込み NaN 398 1
33 33 北京ダック NaN 625 1
最後にメインモジュールを分離します.具体的には def get_args():
以降のコードをすべて削除し,class GochiSolver:
に関する記述のみを残します.同時に,import argparse
の文も不要になったので削除します.
GochiSolver.py
import numpy as np
import pandas as pd
from ortoolpy import knapsack
class GochiSolver:
"""
ゴチのソルバー
"""
def __init__(self):
self.W = [] # 価格
self.C = 0 # 設定金額
self.ID = []
self.Menu = []
self.Size = []
self.total = 0
self.order_ids = []
self.menu_df = None
self.order_df = None # 注文するメニューのデータフレーム
def set_data(self, weight_df, capacity):
self.W = weight_df['price'].tolist()
self.C = capacity
self.ID = weight_df['no'].tolist()
self.Menu = weight_df['menu'].tolist()
self.Size = weight_df['size'].tolist()
def show(self):
print("W:", self.W)
print("C: ", self.C)
print("ID: ", self.ID)
print("Menu: ", self.Menu)
print("Size: ", self.Size)
def solve(self):
# 問題を解く
self.total, self.order_ids = knapsack(self.W, self.W, self.C)
# 注文するかしないか
orders = np.zeros(len(self.ID), dtype=np.int64) # ゼロで初期化
orders[self.order_ids] = 1 # 注文するメニューを設定
# 全メニューのデータフレームに注文データ(0-1変数)も追加する
self.menu_df = pd.DataFrame(
{
'no': self.ID,
'menu': self.Menu,
'size': self.Size,
'price': self.W,
'order' : orders
}
)
# 注文メニューだけのデータフレーム
self.order_df = self.menu_df[self.menu_df.order > 0]
一方でメインのファイル(今回は gochi.py という名前にしました)には,最低限のコードだけを記述します.1行目でインポートしていますが,インポートの書き方の詳細についてはこちらを参照してください.
gochi.py
from GochiSolver import GochiSolver
import pandas as pd
capacity = 2980
url = "https://github.com/rinsaka/sample-data-sets/blob/master/gochi-menu.csv?raw=true"
weight_df = pd.read_csv(url)
gochi = GochiSolver()
gochi.set_data(weight_df, capacity)
gochi.solve()
if gochi.C == gochi.total:
print("解が見つかりました")
else:
print("解は見つかりませんでした")
print(f"- 差額: {gochi.total - gochi.C}")
print(f"- 設定金額: {gochi.C}")
print(f"- 注文合計: {gochi.total}")
print("")
print(gochi.order_df)
実行時にはメインモジュールのファイル (gochi.py) を指定することに注意してください.
python gochi.py ⏎
解が見つかりました
- 設定金額: 2980
- 注文合計: 2980.0
no menu size price order
8 8 むしホタテにんにく風味 NaN 788 1
11 11 海鮮入りおこげ NaN 898 1
21 21 黒胡椒入り鳥唐揚げ NaN 498 1
22 22 黒酢いり広東風キムチ NaN 398 1
31 31 豚足とろけるほど醤油煮込み NaN 398 1
上記のプログラムにさらにコマンドライン引数を取得するコードを追加してもよいでしょう.