LoginSignup
1
1

健康的に鳥貴族で酒を飲みたいので数理最適化問題でメニューを勧めてもらった!

Posted at

健康的に鳥貴族を食べたい

鳥貴族は、3大都市圏を中心に店舗を展開している居酒屋チェーン店です。
全品税込み370円なので、とても安価で食べられるのでかなり好きです。よく行ってます。

ただ、居酒屋メシというものはどうしてもPFCバランスが悪くなりがちで健康面で心配です。
酒以外の要素で健康を害したいとは思いません。

なので、今回は取りたい栄養素をあらかじめ入力することでそれを満たす最小カロリーの鳥貴族のメニューをリコメンドしてくれるプログラムを作ってみました。

今回の課題

幸いなことに、鳥貴族はメニューの栄養素をすべて公開してくれています。
例として、もも貴族焼 たれは以下の栄養素となっております。

成分
エネルギー 350
タンパク質 26.3
脂質 21.6
炭水化物 9.5
食塩 1.9

このようになっているので、
以下の条件を満たすようなメニューの組み合わせを探します。

成分 範囲
エネルギー 500 ~ 1000 kcal
タンパク質 50 ~ 100g
脂質 0 ~ 30g
炭水化物 0 ~ 50g
食塩 1 ~ 8g
メニュー数 3 ~ 5個

またアウトプットは以下の条件を満たすとします。

  • 条件を満たす組みあわせを10個提示する
  • 絶対食べたいメニューは1個まで追加することができる
  • 好きなメニューを可能な限り選ぶようにする。

計算

今回は線形計画法の問題として解きたいと考えているので、PulPというソルバーを使ってPythonで実行させました。
便利です。
さらに入力をしやすくするように、今回はFlaskを使って行いました。

おおまかなコードはこのような感じです。

    solutions = []
    nutritional_totals = []
    item_penalties = {i: 0 for i in data.index}
    for _ in range(max_solutions):
        problem = pulp.LpProblem("Menu_Optimization", pulp.LpMaximize if maximize else pulp.LpMinimize)
        menu_vars = pulp.LpVariable.dicts("Menu", data.index, cat=pulp.LpBinary)
        # メニュー名とメニューIDの対応関係を持つ辞書を作成
        menu_id_mapping = dict(zip(data['メニュー名'], data.index))
        menu_id_mapping = {menu_name.strip(): menu_id for menu_name, menu_id in menu_id_mapping.items()}
        # menu_id_mappingの内容を確認
        print("Menu ID Mapping:", menu_id_mapping)

        # 必ず含めるメニューの制約
        for menu_name in must_have_menus:
            clean_menu_name = menu_name.strip()
            print("Menu Name:", clean_menu_name)
            if clean_menu_name in menu_id_mapping:
                menu_id = menu_id_mapping[clean_menu_name]
                print("Menu ID:", menu_id)
                problem += menu_vars[menu_id] == 1  # このメニューIDが選択されることを保証



        # 目的関数の追加
        problem += pulp.lpSum([(data.loc[i, objective] + item_penalties[i]) * menu_vars[i] for i in data.index])

        # 制約の追加
        for nutrient, bounds in constraints.items():
            if nutrient == '好み':
                problem += pulp.lpSum([data.loc[i, nutrient] * menu_vars[i] for i in data.index]) >= human_rights_ave * pulp.lpSum([menu_vars[i] for i in data.index])

            else:
                min_val, max_val = bounds
                problem += pulp.lpSum([data.loc[i, nutrient] * menu_vars[i] for i in data.index]) >= min_val
                problem += pulp.lpSum([data.loc[i, nutrient] * menu_vars[i] for i in data.index]) <= max_val

        if min_items is not None:
            problem += pulp.lpSum(menu_vars.values()) >= min_items
        if max_items is not None:
            problem += pulp.lpSum(menu_vars.values()) <= max_items

        problem.solve()
        if pulp.LpStatus[problem.status] == 'Optimal':
            selected_items = [i for i, var in menu_vars.items() if var.varValue == 1]
            solutions.append(selected_items)
                        # 栄養価の計算
            nutritional_totals.append({
                'Calories': sum(data.loc[i, 'カロリー'] * menu_vars[i].varValue for i in selected_items),
                'Protein': sum(data.loc[i, 'タンパク質'] * menu_vars[i].varValue for i in selected_items),
                'Carbs': sum(data.loc[i, '炭水化物'] * menu_vars[i].varValue for i in selected_items),
                'Fat': sum(data.loc[i, '脂質'] * menu_vars[i].varValue for i in selected_items),
                'Salt': sum(data.loc[i, '塩分'] * menu_vars[i].varValue for i in selected_items),
                'Like': sum(data.loc[i, '好み'] * menu_vars[i].varValue for i in selected_items) / len(selected_items) if selected_items else 0
            })
            for i in selected_items:
                item_penalties[i] += data.loc[i, 'カロリー'] 
            problem = pulp.LpProblem("Menu_Optimization", pulp.LpMaximize if maximize else pulp.LpMinimize)
        else:
            break

10個選んでもらうため、一度選んだアイテムに関しては、ペナルティを設けました。
今回の主目的は完全な最適化ではないので、問題はないはず。

鳥貴族のメニューが入ったcsvを用意し、新たに好みというカラムを作りました。

これの値を決めることで、平均値が一定以上、つまり自分が食べたいものを中心に選択してもらうことになります。

image.png

それを加味しないと、ちょっと寂しいメニューになりがち

あとは、それぞれの範囲を選べる入力フォームと絶対食べたいものの入力フォームを作ります。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>栄養バランスに合わせて、鳥貴族のメニューを出すやつ</title>
</head>
<body>
    <h1>栄養バランスに合わせて、鳥貴族のメニューを出すやつ</h1>
    <form method="post" action="/solve">
        <div>
            <label for="protein_min">Minimum Protein (g):</label><br>
            <input type="number" id="protein_min" name="protein_min" placeholder="Minimum Protein" value="50"><br>
            <label for="protein_max">Maximum Protein (g):</label><br>
            <input type="number" id="protein_max" name="protein_max" placeholder="Maximum Protein" value="100"><br>
            <small>タンパク質の範囲を入力してください</small>
        </div>
        <br>
        <div>
            <label for="salt_min">Minimum Salt (mg):</label><br>
            <input type="number" id="salt_min" name="salt_min" placeholder="Minimum Salt" value="1"><br>
            <label for="salt_max">Maximum Salt (mg):</label><br>
            <input type="number" id="salt_max" name="salt_max" placeholder="Maximum Salt" value="10"><br>
            <small>塩分の範囲を入力してください</small>
        </div>
        <br>
        <div>
            <label for="carbs_min">Minimum Carbs (g):</label><br>
            <input type="number" id="carbs_min" name="carbs_min" placeholder="Minimum Carbs" value="20"><br>
            <label for="carbs_max">Maximum Carbs (g):</label><br>
            <input type="number" id="carbs_max" name="carbs_max" placeholder="Maximum Carbs" value="50"><br>
            <small>炭水化物の範囲を入力してください</small>
        </div>
        <br>
        <div>
            <label for="fat_min">Minimum Fat (g):</label><br>
            <input type="number" id="fat_min" name="fat_min" placeholder="Minimum Fat" value="5"><br>
            <label for="fat_max">Maximum Fat (g):</label><br>
            <input type="number" id="fat_max" name="fat_max" placeholder="Maximum Fat" value="10"><br>
            <small>脂質の範囲を入力してください</small>
        </div>
        <br>
        <div>
            <label for="calories_min">Minimum Calories:</label><br>
            <input type="number" id="calories_min" name="calories_min" placeholder="Minimum Calories" value="500"><br>
            <label for="calories_max">Maximum Calories:</label><br>
            <input type="number" id="calories_max" name="calories_max" placeholder="Maximum Calories" value="1000"><br>
            <small>カロリーの範囲を入力してください</small>
        </div>
        <br>
        <div>
            <label for="like_ave">Average Human Rights:</label><br>
            <input type="number" id="like_ave" name="like_ave" placeholder="like" value="2" step="0.1"><br>
            <small>食べたい物の平均好み度を入れてください</small>
        </div>
        <br>
        <div>
            <label for="min_items">Minimum Items:</label><br>
            <input type="number" id="min_items" name="min_items" placeholder="Minimum Number of Items" value="3"><br>
            <small>メニューの最低数を入力してください</small>
        </div>
        <br>
        <div>
            <label for="max_items">Maximum Items:</label><br>
            <input type="number" id="max_items" name="max_items" placeholder="Maximum Number of Items" value="5"><br>
            <small>メニューの最大数を入力してください</small>
        </div>
        <br>
        <div>
            <h3>絶対に食べたいメニューを選択してください:</h3>
            <select name="must_have_menus" multiple size="5">
                {% for menu_item in menu_items %}
                <option>{{ menu_item }}</option>
                {% endfor %}
            </select>
        </div>
        
    
        <button type="submit">Submit</button>
    </form>
</body>
</html>

このような形で入力フォームを作り

<!-- templates/results.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>結果</title>
</head>
<body>
    <h1>おすすめメニュー</h1>
    {% for solution, total in zipped_solutions %}
    <div>
        <h2>Solution {{ loop.index }}</h2>
        <ul>
            {% for item_name, calories in solution %}
            <li>{{ item_name }}: {{ calories|format_float(0) }} カロリー</li>
            {% endfor %}
        </ul>
        <p>Total Nutritional Values: Calories - {{ total['Calories']|format_float }}, Protein - {{ total['Protein']|format_float }}, Carbs - {{ total['Carbs']|format_float }}, Fat - {{ total['Fat']|format_float }}, Salt - {{ total['Salt']|format_float }}</p>

    </div>
    {% endfor %}
</body>
</html>

このようなコードで出力させました。
Flaskは便利です。

実用

成分 範囲
エネルギー 500 ~ 1000 kcal
タンパク質 50 ~ 100g
脂質 0 ~ 30g
炭水化物 0 ~ 50g
食塩 1 ~ 7g
メニュー数 3 ~ 5個

この条件で「トリキの唐揚げ」を含むメニューの組み合わせをカロリーが低い順に並べてみました!!

結果です!

image.png

色々な組み合わせがあってよさそう!
おもったより鳥貴族には低カロリーなメニューもあるもんだ。
今度行きます!

結論

数値による最適化が人間にとって最適化とは限らない
数値的に最適化されると、チャンジャ!胸肉!チャンジャ!胸肉!みたいな地獄が待っているので好み度を入れたのがかなり効いた。
多種多様なメニューを食べたいので、こういう決め方もありだなとは感じた。

おわりに

数理最適化を使って健康的な鳥貴族のメニューを選ぶ方法、Flaskを使用したローカルのアプリを作成しました。
今度は、これをサーバーなどにあげて、実際に鳥貴族から使えるようにしたいです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1