Подход математической оптимизации с Python и Pulp

Макдональдс - супер здоровый вариант! - Никто никогда

Если вы спросите кого-нибудь о том, что они думают о еде в McDonald’s, стандартным ответом будет то, что им это нравится, но они знают, что не должны есть это все время. Макдоналдс рекламируется как вредный для здоровья, что даже привело к документальному фильму Моргана Сперлока Я очень большой о том, как ежедневное употребление пищи в Макдоналдсе приводит к очень серьезным проблемам со здоровьем.

Вот моя проблема с этим. Конечно, если вы съедаете три Bigmac в день, ваше сердце взорвется, но это, вероятно, верно для многих ресторанов. Я решил определить, есть ли

оптимальное сочетание пунктов меню в McDonald’s, которое строго следует некоторым рекомендациям по питанию

и как бы выглядело это McHealthy Combo?

Для этого я обращаюсь к мощи линейной оптимизации и python. Мы собираемся открыть его настежь, так что успокаивайтесь!

Данные и предположения

Первым шагом на этом пути был поиск данных меню от McDonald’s. Я взглянул на Kaggle, веб-платформу для анализа данных с множеством интересных наборов данных с открытым исходным кодом. После быстрого поиска мне удалось легко найти полное меню в красивом табличном формате [1]. Он включал количество калорий, тип еды (напитки, гамбургеры и т. Д.), А также содержание всех макроэлементов, таких как натрий и жир.

Вторая часть заключалась в том, чтобы найти законный источник, рассказывающий нам, из чего состоит здоровая диета. Еще один быстрый поиск в Google, и я смог найти разбивку по питанию, предоставленную NHS [2]. Базовое ежедневное потребление, необходимое среднестатистическому человеку, по их мнению, составляет:

Энергия: ~ 2000 ккал

Всего жиров: ‹70 г (насыщенных‹ 20 г)

Углеводы ›260г

Сахар: ~ 90 г + -10 г

Белок: ~ 50 г + -10 г

Натрий: ‹6 г

Итак, теперь у меня есть меню и цели по питанию. Вопрос в том, как найти оптимальную комбинацию блюд для здоровья? Что ж, вот где проявляется магия линейного программирования. Это хорошая техника, которую можно адаптировать к этой проблеме и реализовать в пакете python Pulp. Все, что нам действительно нужно знать, - это ваши ограничения (данные о питании выше) и наш набор переменных (пункт меню McDonald’s). Кто голоден… ЗА ИСТИНУ!

Обработка данных

Сначала мы убеждаемся, что установили пакет pulp в python с помощью нашего старого друга pip:

pip install pulp

После завершения установки мы можем перейти к импорту наших пакетов:

import numpy as np
import pandas as pd
from pulp import *
import plotly.plotly as py
from plotly.offline import init_notebook_mode, iplot
init_notebook_mode(connected=True)
import plotly.graph_objs as go
import matplotlib.pyplot as plt # matplotlib
import os

Затем мы просто загрузим наш набор данных в объект фрейма данных pandas:

McData = pd.read_csv('../input/menu.csv')

Ради интереса мы можем использовать пакет Plotly, чтобы получить представление о данных. Давайте посмотрим на график зависимости углеводов от калорий, окрашенный в зависимости от типа элемента меню. Для этого я сначала определю новую функцию для построения диаграммы рассеяния с помощью Plotly:

# The function creates a Scatter graph object (go) and uses the data frame .isin() selection to extract the requested information
def make_scatter(McData,category,x_cat,y_cat):
    return  go.Scatter(
       x = McData[McData['Category'].isin([category])][x_cat],
       y = McData[McData['Category'].isin([category])][y_cat],
       mode = "markers",
       name = category,
       text=  McData.Item)

Теперь мы можем взглянуть на несколько корреляций. Как я уже сказал выше, давайте найдем калории против углеводов:

# Define our categories to plot
x_cat = 'Calories'; y_cat = 'Carbohydrates'
# Create a list of scatter plots to view all at once
data = [make_scatter(McData,cat,x_cat,y_cat) for cat in
   McData.Category.unique().tolist()]
# Define the plot layout (title, ticks etc.)
layout = dict(title = 'McDonalds Nutrition',
   xaxis= dict(title= 'Calories',ticklen=5,zeroline= False),
   yaxis= dict(title= 'Carbohydrates(g)',ticklen= 5,zeroline=False))
# Finally we will plot the data with the layout
fig = dict(data = data, layout = layout)
iplot(fig)

Давайте сделаем еще один. Как насчет натрия и жира?

Теперь, когда у нас есть представление о данных, мы можем продолжить и настроить код оптимизации, который поможет нам выбирать из дискретного набора переменных (пунктов меню).

Мы собираемся использовать линейное программирование в качестве метода оптимизации. Я не буду вдаваться в подробности здесь, но это очень быстрый метод расчета, когда нет корреляций более высокого порядка, которые входят в целевую функцию.

Первое, что нужно сделать, это определить нашу Целевую функцию. Цель - это то, что мы пытаемся минимизировать или максимизировать. В этом случае, предположим, мы хотим вписаться во все наши питательные макроэлементы, однако мы также хотим сократить количество калорий.

Наша цель - минимизировать калорийность

Затем мы должны определить наши ограничения. Поскольку мы знаем, какое ежедневное потребление должно быть основано на [2], мы можем установить их в качестве ограничений для оптимизации. В идеальном мире вы бы потребляли НОЛЬ калорий и получали все необходимые питательные вещества (очевидно, во многих отношениях это нереально), поэтому, чтобы внести это в оптимизацию, мы определяем следующее:

Преобразуйте данные в словари, и именно так переменные ограничения должны войти в функции оптимизации:

# Convert the item names to a list
MenuItems = McData.Item.tolist()
# Convert all of the macro nutrients fields to be dictionaries of the item names
Calories = McData.set_index('Item')['Calories'].to_dict()
TotalFat = McData.set_index('Item')['Total Fat'].to_dict()
SaturatedFat = McData.set_index('Item')['Saturated Fat'].to_dict()
Carbohydrates = McData.set_index('Item')['Carbohydrates'].to_dict()
Sugars = McData.set_index('Item')['Sugars'].to_dict()
Protein = McData.set_index('Item')['Protein'].to_dict()
Sodium = McData.set_index('Item')['Sodium'].to_dict()

Формат ограничений должен выглядеть следующим образом, если мы распечатываем; например, натрий:

Теперь, когда у нас есть все данные в правильных форматах, мы можем приступить к настройке оптимизатора!

Настройка оптимизатора

В этом примере мы выполняем оптимизацию минимизация:

# Set it up as a minimization problem
prob = LpProblem("McOptimization Problem", LpMinimize)

Кроме того, мы можем сказать оптимизатору, что нас интересуют только целочисленные решения. т.е. мы скажем, что невозможно иметь только 0,5 шт. (без половинных чизбургеров). Вдобавок к этому мы можем выбрать максимальное и минимальное количество элементов для решения:

MenuItems_vars = LpVariable.dicts("MenuItems",MenuItems,lowBound=0,
   upBound=10,cat='Integer')

Вы видите, как мы даем нижнюю границу 0 и верхнюю 10? Если бы мы этого не сделали, в меню могли бы быть отрицательные элементы. Это как компенсировать то, что вы съели,… отдавая это обратно. Давайте не будем этого делать. Однако верхняя граница немного более свободна и просто говорит о том, что мы не будем покупать более 10 единиц одного предмета.

На этом этапе мы можем продолжить и ввести ограничения в прогон:

# First entry is the calorie calculation (this is our objective)
prob += lpSum([Calories[i]*MenuItems_vars[i] for i in MenuItems]),
   "Calories"
# Total Fat must be <= 70 g
prob += lpSum([TotalFat[i]*MenuItems_vars[i] for i in MenuItems]) <=
   70, "TotalFat"
# Saturated Fat is <= 20 g
prob += lpSum([SaturatedFat[i]*MenuItems_vars[i] for i in
   MenuItems]) <= 20, "Saturated Fat"
# Carbohydrates must be more than 260 g
prob += lpSum([Carbohydrates[i]*MenuItems_vars[i] for i in
   MenuItems]) >= 260, "Carbohydrates_lower"
# Sugar between 80-100 g
prob += lpSum([Sugars[i]*MenuItems_vars[i] for i in MenuItems]) >=
   80, "Sugars_lower"
prob += lpSum([Sugars[i]*MenuItems_vars[i] for i in MenuItems]) <=
   100, "Sugars_upper"
# Protein between 45-55g
prob += lpSum([Protein[i]*MenuItems_vars[i] for i in MenuItems]) >=
   45, "Protein_lower"
prob += lpSum([Protein[i]*MenuItems_vars[i] for i in MenuItems]) <=
   55, "Protein_upper"
# Sodium <= 6000 mg
prob += lpSum([Sodium[i]*MenuItems_vars[i] for i in MenuItems]) <=
   6000, "Sodium"

Теперь мы запускаем решатель, чтобы (будем надеяться) найти оптимальный набор пунктов меню, чтобы он был супер здоровым!

# Tadaaaaa
prob.solve()

Быстрая проверка, чтобы убедиться, что решение действительно найдено:

print("Status:", LpStatus[prob.status])

Ну наконец то! Посмотрим на результат!

# Get the total calories (minimized)
print("Total Calories = ", value(prob.objective))
# Loop over the constraint set and get the final solution
results = {}
for constraint in prob.constraints:
    s = 0
    for var, coefficient in prob.constraints[constraint].items():
        sum += var.varValue * coefficient
    results[prob.constraints[constraint].name.replace('_lower','')
        .replace('_upper','')] = s

Результаты …….

Всего калорий: 1430

Представляем McHealthy Combo! В основном это яблоки, салат и овсянка. Действительно, очень скучно. Это показало, что фавориты фанатов, такие как BigMacs и картофель фри, не являются суперполезным выбором, поскольку они богаты определенными питательными веществами и высококалорийны и могут помешать оптимизации в целом.

Это был очень интересный проект, поэтому, если у вас есть какие-либо пожелания или идеи, дайте мне знать!

Ссылки и ссылки

[1] https://www.kaggle.com/mcdonalds/nutrition-facts

[2] https://www.nhs.uk/live-well/eat-well/what-are-reference-intakes-on-food-labels/

[3] https://www.kaggle.com/kapastor/optimizing-mcdonalds-nutrition