Подход математической оптимизации с 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