【Python】pulpで1日の献立のPFCバランスを最適化する

[新卒エンジニアによる全部俺カレンダー2022](https://qiita.com/advent-calendar/2022/arie_onlyme2022) 6日目投稿記事です。

## 概要
近年リモートでの勤務が主体となる中、自炊する方も増えてきているのではないでしょうか。
日頃自炊する中で「栄養バランスが整った食事を摂りたい」と健康を意識して料理をする人も増えてきているみたいです。
しかし、「栄養バランスを意識しながら毎日献立を考えるのはめんどくさいな・・」と思うこともしばしばあります。

そこで、pythonのpulpというライブラリを使ってPFCバランス*が最適化された1日の献立を作成するツールを作成してみました！

***PFCバランス**
健康維持に不可欠な3大栄養素である Protein（タンパク質）, Fat（脂質）, Carbohydrate（炭水化物）それぞれの頭文字をとった言葉。

https://online.konamisportsclub.jp/page/ksc/products/original/application/caloridiet/pfc-balance.html

## 実装

必要ライブラリをインストールします。
```
pip install numpy
pip install pandas
pip install pulp
```
pythonファイルとメニュー情報を記述したcsvファイルを作成します。
```
touch main.py
touch menu.csv
```

```main.py
import numpy as np, pandas as pd
from pulp import *

df = df = pd.read_csv("menu.csv") # csvファイルを読み込む

df["個数"] = LpVariable.dicts("個数",df["料理名"], 0,2, cat='Integer').values()

# 条件の範囲内でなるべく多くの食事を摂りたい
problem = LpProblem(sense=LpMaximize)
problem.setObjective(lpDot(df["カロリー"], df["個数"])) 

# 条件を設定
problem += lpDot(df["カロリー"], df["個数"]) <= 2200
problem += lpDot(df["タンパク質"], df["個数"]) >= 100
problem += lpDot(df["タンパク質"], df["個数"]) <= 130
problem += lpDot(df["脂質"], df["個数"]) <= 60
problem += lpDot(df["炭水化物"], df["個数"]) >= 250
problem += lpDot(df["炭水化物"], df["個数"]) <= 350
problem.solve()

total_protein = sum([df["タンパク質"][i] * value(df["個数"][i]) for i in range(len(df))])
total_fat = sum([df["脂質"][i] * value(df["個数"][i]) for i in range(len(df))])
total_carbo = sum([df["炭水化物"][i] * value(df["個数"][i]) for i in range(len(df))])
total_calories = value(problem.objective)
selected_menu = [f'{df["料理名"][i]}: {value(df["個数"][i])}' for i in range(len(df)) if value(df["個数"][i]) >= 1]

# 結果を出力
print(f"摂取カロリー: {total_calories}")
print(f"タンパク質: {total_protein}, 脂質: {total_fat}, 炭水化物: {total_carbo}")
for m in selected_menu:
    print(m)
```

```menu.csv
料理名,カロリー,タンパク質,脂質,炭水化物
親子丼,412,22.4,8.6,63
ビッグマック,525,26,42.6,41.8
サケの塩焼き,187,23.5,4.2,2.7
白米200g,312,5,0.6,74.2
野菜サラダ,17,1.1,0.1,3.8
寿司盛り合わせ（10貫）,611,26.6,10.7,106.5
カレーうどん,396,14.4,8.5,67
生姜焼き,445,25,25,30
ゆで卵,61,5.7,4.7,0.1
納豆,86,7.4,4.5,5.4
```

CSVファイルには各々好きなメニューの情報を入れてください。

**(栄養素情報の参考)**

https://www.asken.jp/calculate/


## コード解説

**同一料理の選択数設定**
```main.py
df["個数"] = LpVariable.dicts("個数",df["料理名"], 0,2, cat='Integer').values()
```
個数に対しての条件を設定します。
`LpVariable.dicts`の引数は(変数名,index,最低値,最高値,変数の型)です。
上記の設定だと、「同じ料理を最低0個・最高2個選択する」という設定になります。


**数理モデルの定義**
```main.py
# 条件の範囲内でなるべく多くの食事を摂りたい
problem = LpProblem(sense=LpMaximize)
problem.setObjective(lpDot(df["カロリー"], df["個数"])) 
```

`LpProblem(sense=LpMaximize)`で数理モデルを定義します。
定義できるモデルには`LpMaximize`と`LpMinimize`の2種類があります
LpMaximize -> 最大化問題
LpMinimize -> 最小化問題

`problem.setObjective(lpDot(df["カロリー"], df["個数"])) `でカロリーの値で最大化問題を行うと定義します。

上記のコードの例だと「設定した制約の中でなるべく多くのカロリーを摂取する」という設定になります。

**PFCバランスを設定**

```main.py
problem += lpDot(df["カロリー"], df["個数"]) <= 2200
problem += lpDot(df["タンパク質"], df["個数"]) >= 100
problem += lpDot(df["タンパク質"], df["個数"]) <= 130
problem += lpDot(df["脂質"], df["個数"]) <= 60
problem += lpDot(df["炭水化物"], df["個数"]) >= 250
problem += lpDot(df["炭水化物"], df["個数"]) <= 350
```

作成したモデルにPFCバランスの条件を設定していきます。
上記のコードの例だと、**「2200kcal以下」「タンパク質は100g以上130g以下」「脂質は60g以下」「炭水化物は250g以上350g以下」** となります。

（最適なPFCバランスは個人の体重・年齢・目指す体型等によって異なります）


## 実行
```bash
python main.py
```

出力

```
摂取カロリー: 2200.0
タンパク質: 108.9, 脂質: 51.4, 炭水化物: 327.5
サケの塩焼き: 1.0
白米200g: 1.0
野菜サラダ: 2.0
寿司盛り合わせ（10貫）: 2.0
生姜焼き: 1.0
```

いい感じに出力してくれました！
朝食にサケの塩焼きとサラダ、昼食に白米・生姜焼き・サラダ、夕食にお寿司20貫といったところでしょうか・・

`menu.csv`に多くのメニューを記述するほどバラエティ豊かな献立になりそうです。
皆さんも献立を考えるのがめんどくさい時は、pulpで最適化してはいかがでしょうか（）

## 参考・関連記事
https://docs.pyq.jp/python/math_opt/pulp.html

https://qiita.com/SaitoTsutomu/items/f8be15f56cbacdbb7bd9

https://qiita.com/Kashalpha/items/582d0ffab82192304ca5