完全栄養マクドナルド食問題を解いてみた

# 概要

Pythonの数理最適化ライブラリであるPuLPを使用する練習として、多くの人がやっている完全栄養マクドナルド食問題（一日に必要な栄養素を全て取れる、マクドナルドの商品の組み合わせを探す問題）にチャレンジしてみました。

# 参考
参考にしたのは、以下の記事です。

https://qiita.com/youwht/items/9098d560f28d16aa5567

https://qiita.com/takobaya391076/items/49b15c1fa36734b3fa53

# 問題設定

1日に必要な栄養素を取りつつ、塩分や脂肪は上限を超えないようにして、できるだけ低カロリーな食事になることを目指します。

1日に必要な栄養素の基準としては、以下のグリコのサイトを参考にしました。

https://jp.glico.com/navi/e07.html

年齢や性別で推奨量・目安量が違うので、今回は女性・30～49(歳)の値を基準値とします。また、脂質については総エネルギーに占める割合で目標量が決まりますが、今回は上限として45gを設定しました。以下が、今回の分析における栄養素の条件になります。


|栄養名|下限|上限|
|--|--|--|
|タンパク質（g）|50.0||
|脂質（g）||45.0|
|カリウム（mg）|2000.0||
|カルシウム（mg）|650.0||
|リン（mg）|800.0|	3000.0|
|鉄（mg）|10.5|	40.0|
|ビタミンA（μg）|	700.0|	2700.0|
|ビタミンB1（mg）|	1.1|	|
|ビタミンB2（mg）|	1.2|	|
|ナイアシン（mg）|	12.0|	250.0|
|ビタミンC（mg）|	100.0|	|
|食物繊維（g）|	18.0|	|
|食塩相当量（g）|	|	6.5|



また、1回の食事で同じものを2つ食べることになるのは辛いので、同じ商品は最大3つまでとします。一応ですが、たくさん食べることもできないので、1日に食べる食事の合計の重さは2kg以下に設定します。まー、これは特に影響がないでしょう。

マクドナルドの商品の栄養価は、マクドナルドのwebページで公開されています。

https://www.mcdonalds.co.jp/quality/allergy_Nutrition/nutrient/

以上の条件を反映した整数計画問題を作成して、PuLPを使いカロリーの低い組み合わせを探します。

# 結果

分析に使用したコードは最後に紹介するとして、分析の結果は以下のようになりました。

```python:出力結果
個数_えだまめコーン = 3.0
個数_りんご&クリーム = 1.0
個数_ケチャップ = 1.0
個数_サイドサラダ = 3.0
個数_シャカチキ_チェダーチーズ味シーズニング = 1.0
個数_シャカチキ_レッドペッパー味シーズニング = 2.0
個数_プチパンケーキ = 1.0
個数_プレミアムローストコーヒー(S) = 3.0
個数_マックグリドル_ベーコンエッグ = 1.0
個数_マックシェイク_チョコレート(M) = 1.0
個数_マックシェイク_チョコレート(S) = 1.0
個数_野菜生活100(S) = 1.0

総カロリー:1604.0kcal
```

シャカチキ用のパウダーはあるが、シャカチキはないという悲しい結果。。。
総カロリーが約1600キロカロリーなので、ダイエット食としては悪くないですね。

とりあえず朝食、昼食、夕食に分けてみると、

**朝食**

+ プチパンケーキ
+ りんご&クリーム
+ えだまめコーン
+ シャカチキ_レッドペッパー味シーズニング
+ サイドサラダ
+ プレミアムローストコーヒー(S)
+ マックシェイク_チョコレート(S)

**昼食**

+ マックグリドル_ベーコンエッグ
+ ケチャップ
+ えだまめコーン
+ シャカチキ_チェダーチーズ味シーズニング
+ サイドサラダ
+ プレミアムローストコーヒー(S)
+ マックシェイク_チョコレート(M)

**夕食**

+ えだまめコーン
+ シャカチキ_レッドペッパー味シーズニング
+ サイドサラダ
+ プレミアムローストコーヒー(S)
+ 野菜生活100(S)

ダイエット中なので、夜は炭水化物を控えるということですね。
シャカチキ用のパウダーは、えだまめコーンあたりにかけて食べましょう。

摂取する栄養は以下の通りです。

```python:出力結果
タンパク質:52.1
脂質:43.3
炭水化物:251.6
ナトリウム:2610.0
カリウム:2915.0
カルシウム:816.0
リン:1345.0
鉄:10.5
ビタミンA:770.0
ビタミンB1:1.3
ビタミンB2:1.4
ナイアシン:12.0
ビタミンC:102.0
コレステロール:247.0
食物繊維:18.4
食塩相当量:6.5
```

# コード

計算に使ったコードは以下になります。

```Python:分析コード
import pandas as pd
import numpy as np
import pulp

df = pd.read_csv("マクドナルド栄養価.csv",index_col=0)

# 欠損値を0埋め
df = df.fillna(0)

# カロリーが0の商品を除く
df = df[df['エネルギー']>0]

# 栄養値の上限・下限の入力
df_max_min = pd.read_csv("1日分の栄養.csv",index_col=0)

# 変数の設定
df["個数"] = pulp.LpVariable.dicts('個数',df.index,lowBound=0,upBound=3,cat='Integer').values()

# 最小化問題
problem = pulp.LpProblem('完全栄養マクドナルド問題',sense=pulp.LpMinimize)

# 目的変数の設定
problem.setObjective(pulp.lpDot(df["エネルギー"],df["個数"]))

# 拘束条件
for nutrition in df_max_min.index:
    if not np.isnan(df_max_min.loc[nutrition,"max"]):
      problem.addConstraint(pulp.lpDot(df[nutrition],df["個数"])<=df_max_min.loc[nutrition,"max"])
    if not np.isnan(df_max_min.loc[nutrition,"min"]):
      problem.addConstraint(pulp.lpDot(df[nutrition],df["個数"])>=df_max_min.loc[nutrition,"min"])

# 重量に条件
problem.addConstraint(pulp.lpDot(df["製品重量"],df["個数"])<=2000)

# 問題を解く
status = problem.solve()
print(pulp.LpStatus[problem.status])

# 答えの表示
for v in problem.variables():
  if v.varValue != 0:
    print(f'{v.name} = {v.varValue}')

print(f"総カロリー:{pulp.value(problem.objective)}kcal")

for nutrition in df_max_min.index:
  print(nutrition + ":" + str(round(pulp.lpDot(df[nutrition], df["個数"]).value(),ndigits=1)))
```