Python入門トップページ


目次

  1. 「ゴチ」バトルの問題
  2. ナップサック問題と両替問題
  3. 準備
  4. Python で問題を解く
  5. ファイルを読み込んで問題を解く
  6. 遺伝的アルゴリズムを実装する
  7. ソルバーのクラスを作成する
  8. 参考資料

「ゴチ」バトル

ソルバーのクラスを作成する

ここでは「ファイルを読み込んで問題を解く」のプログラムを外部からも利用できるようにするために,ソルバーのクラスとして独立させることを考えます.最後のステップではソルバーのクラスとメインモジュールのファイルを分離して,メインモジュールからソルバーのクラスをインポートして利用する方法を示します.

目次に戻る

ソルバーのクラス化

まずは,ソルバーをクラス化してメインモジュールも同じファイルに含めた状態でのプログラム全体を示します.

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_dfcapacity を渡します.

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.tatalself.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

上記のプログラムにさらにコマンドライン引数を取得するコードを追加してもよいでしょう.

目次に戻る