diff --git a/README.md b/README.md index f94f348..0caebc3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Nutrient Object Management System (noms) -Note: I'm not able to maintain this package right now, so there is a chance that the current API integration is broken/will break given the new FoodData Central API from the USDA. +Code adatped to accomodate the new FoodData Central API from the USDA. noms is a fun and simple Python package that allows you to obtain and work with highly detailed nutrition data for nearly 8,000 entries from the USDA Standard Reference Food Composition Database. No mainstream nutrition tracker apps reflect the level of detail that the USDA has compiled. With noms you can track: 1. Proximates including macronutrients (protein, carbs, and fat), calories, fiber and water content @@ -10,7 +10,7 @@ noms is a fun and simple Python package that allows you to obtain and work with This amounts to 41 nutrients being tracked, but many more are available from the database such as amino acids and other lipids. These can be viewed in all_nutrient_ids.txt, and support for other nutrients will be added in the future as requested. You can add support for these yourself by editing noms/objects/nutrient_ids.json accordingly with entries from all_nutrient_ids.txt. -Note: The Standard Reference Database is used explicitly without the addition of the USDA's Branded Foods database, as only the former allows for highly detailed reports which track 168 different nutrients -- much more information than you would find on an item's nutrition facts! This is especially valuable for nutritionists or people interested in their own health to explore the nutritional content of whole foods. +Note: The Standard Reference Database is used explicitly without the addition of the USDA's Branded Foods database, as only the former allows for highly detailed reports which track 168 different nutrients -- much more information than you would find on an item's nutrition facts! This is especially valuable for nutritionists or people interested in their own health to explore the nutritional content of whole foods. ## Installation The noms package is listed on PyPI and can be installed with pip. Simply do: @@ -38,19 +38,23 @@ print(search_results) ================================================================================================================ Search results for 'Raw Broccoli' on USDA Standard Reference Database ================================================================================================================ -Name Group ID -Broccoli, raw Vegetables and Vegetable Pro.. 11090 -Broccoli raab, raw Vegetables and Vegetable Pro.. 11096 -Broccoli, leaves, raw Vegetables and Vegetable Pro.. 11739 -Broccoli, stalks, raw Vegetables and Vegetable Pro.. 11741 -Broccoli, chinese, raw Vegetables and Vegetable Pro.. 11994 -Broccoli, flower clusters, raw Vegetables and Vegetable Pro.. 11740 +description dataType ID +Broccoli, raw Foundation 747447 +Broccoli, raw SR Legacy 170379 +Broccoli raab, raw SR Legacy 170381 +Broccoli, chinese, raw SR Legacy 169404 +Broccoli, leaves, raw SR Legacy 169329 +Broccoli, stalks, raw SR Legacy 169331 +Broccoli, flower clusters, raw SR Legacy 169330 +Broccoli, raw Survey (FNDDS) 1103170 +Broccoli raab, raw Survey (FNDDS) 1103084 +Broccoli, chinese, raw Survey (FNDDS) 1103184 ================================================================================================================ ``` ## Requesting Food Data From the Database In this example, the ids correlate with Raw Broccoli (11090) and a Cola Beverage (14400). The numbers afterwards represent the mass of that food, in grams. More mass for a given food equals a greater amount of each nutrient in equal proportion (twice the broccoli has twice the vitamins). ```python -food_list = client.get_foods({'11090':100, '14400':100}) +search_results = client.get_foods({'747447':100, '174826':100}) ``` ## Initializing a Meal With a List of Foods The foods() method returned a list of two Food objects when given the arguments above, but if you would like to generate a report, analyze or sort a group of foods, they should be merged into a Meal object. This is done by simply constructing a Meal instance with a list of Food objects. @@ -58,7 +62,7 @@ The foods() method returned a list of two Food objects when given the arguments m = noms.Meal(food_list) ``` -## Generating and Displaying a Report +## Generating and Displaying a Report The report is a dictionary which shows if RDAs (or Adequate Intakes) are being met or exceeded. These values are assigned by default in noms.objects.nutrient_dict, but support will be added to modify these settings in the future. ```python r = noms.report(m) @@ -66,29 +70,16 @@ for i in r: print(i) ``` ``` -{'name': 'Protein', 'rda': 125.0, 'limit': None, 'value': 2.89, 'state': 'deficient', 'unit': 'g'} -{'name': 'Fat', 'rda': 55.56, 'limit': None, 'value': 0.39, 'state': 'deficient', 'unit': 'g'} -{'name': 'Carbs', 'rda': 250.0, 'limit': None, 'value': 16.2, 'state': 'deficient', 'unit': 'g'} -{'name': 'Calories', 'rda': 2000, 'limit': None, 'value': 71.0, 'state': 'deficient', 'unit': 'kcal'} -{'name': 'Water', 'rda': 2000, 'limit': None, 'value': 179.61, 'state': 'deficient', 'unit': 'g'} -{'name': 'Caffeine', 'rda': 0, 'limit': 400, 'value': 8.0, 'state': 'satisfactory', 'unit': 'mg'} +{'name': 'Protein', 'rda': 125.0, 'limit': None, 'value': 2.57, 'state': 'deficient', 'unit': 'g'} +{'name': 'Fat', 'rda': 55.56, 'limit': None, 'value': 0.34, 'state': 'deficient', 'unit': 'g'} +{'name': 'Carbs', 'rda': 250.0, 'limit': None, 'value': 7.3999999999999995, 'state': 'deficient', 'unit': 'g'} +{'name': 'Calories', 'rda': 2000, 'limit': None, 'value': 31.0, 'state': 'deficient', 'unit': 'kcal'} +{'name': 'Water', 'rda': 2000, 'limit': None, 'value': 188.87, 'state': 'deficient', 'unit': 'g'} +{'name': 'Caffeine', 'rda': 0, 'limit': 400, 'value': 0.0, 'state': 'satisfactory', 'unit': 'mg'} {'name': 'Theobromine', 'rda': 0, 'limit': 300, 'value': 0.0, 'state': 'satisfactory', 'unit': 'mg'} -{'name': 'Sugar', 'rda': 0, 'limit': 50.0, 'value': 10.67, 'state': 'satisfactory', 'unit': 'g'} -{'name': 'Fiber', 'rda': 28.0, 'limit': None, 'value': 2.6, 'state': 'deficient', 'unit': 'g'} -{'name': 'Calcium', 'rda': 1000, 'limit': 2500, 'value': 49.0, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Iron', 'rda': 8, 'limit': 45, 'value': 0.84, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Magnesium', 'rda': 300, 'limit': 700, 'value': 21.0, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Phosphorus', 'rda': 700, 'limit': 4000, 'value': 76.0, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Potassium', 'rda': 1400, 'limit': 6000, 'value': 318.0, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Sodium', 'rda': 1000, 'limit': 2300, 'value': 37.0, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Zinc', 'rda': 12, 'limit': 100, 'value': 0.43, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Copper', 'rda': 0.9, 'limit': 10, 'value': 0.05, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Fluoride', 'rda': 400, 'limit': 10000, 'value': 57.0, 'state': 'deficient', 'unit': 'µg'} -{'name': 'Manganese', 'rda': 1.8, 'limit': None, 'value': 0.21, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Selenium', 'rda': 70, 'limit': 400, 'value': 2.6, 'state': 'deficient', 'unit': 'µg'} -{'name': 'Vitamin A', 'rda': 900, 'limit': 20000, 'value': 623.0, 'state': 'deficient', 'unit': 'IU'} -{'name': 'Vitamin E', 'rda': 15, 'limit': 1000, 'value': 0.78, 'state': 'deficient', 'unit': 'mg'} -{'name': 'Vitamin D', 'rda': 1000, 'limit': 8000, 'value': 0.0, 'state': 'deficient', 'unit': 'IU'} +{'name': 'Sugar', 'rda': 0, 'limit': 50.0, 'value': 0.0, 'state': 'satisfactory', 'unit': 'g'} +{'name': 'Fiber', 'rda': 28.0, 'limit': None, 'value': 2.4, 'state': 'deficient', 'unit': 'g'} +{'name': 'Calcium', 'rda': 1000, 'limit': 2500, 'value': 46.0, 'state': 'deficient', 'unit': 'mg'} ... continued ``` ## Sorting Foods in a Meal By a Specific Nutrient @@ -101,8 +92,8 @@ for food in m.foods: print(food.nutrients[ni]) ``` ``` -{'nutrient_id': 269, 'name': 'Sugar', 'group': 'Proximates', 'unit': 'g', 'value': 8.97} -{'nutrient_id': 269, 'name': 'Sugar', 'group': 'Proximates', 'unit': 'g', 'value': 1.7} +{'nutrient_id': 269, 'name': 'Sugar', 'group': 'Proximates', 'unit': 'g', 'value': 0.0} +{'value': 0.0, 'name': 'Sugar', 'unit': 'g', 'nutrient_id': 269.0} ``` Note that this sorts the foods in the Meal object from greatest to least in terms of how much sugar each food has. @@ -110,76 +101,76 @@ Note that this sorts the foods in the Meal object from greatest to least in term Because it would be computationally expensive to generate a food recommendation in the context of every food in the database, and it may be unrealistic to recommend any food from the database as it may be hard to access, you must define a list of foods that will serve as a pantry object. Here is an example pantry object containing many common whole foods. ```python - pantry = { - # DAIRY AND EGG - "01001":100, # butter, salted - "01145":100, # butter, without salt - "01079":100, # 2% milk - "01077":100, # milk, whole - "01086":100, # skim milk - "01132":100, # scrambled eggs - "01129":100, # hard boiled eggs - "01128":100, # fried egg - # MEAT - "15076":100, # atlantic salmon - "07935":100, # chicken breast oven-roasted - "13647":100, # steak - "05192":100, # turkey - # FRUIT - "09037":100, # avocado - "09316":100, # strawberries - "09050":100, # blueberry - "09302":100, # raspberry - "09500":100, # red delicious apple - "09040":100, # banana - "09150":100, # lemon - "09201":100, # oranges - "09132":100, # grapes - # PROCESSED - "21250":100, # hamburger - "21272":100, # pizza - "19088":100, # ice cream - "18249":100, # donut - # DRINK - "14400":100, # coke - "14429":100, # tap water - "14433":100, # bottled water - "09206":100, # orange juice - "14278":100, # brewed green tea - "14209":100, # coffee brewed with tap water - # (milk is included in dairy group) - # GRAIN - "12006":100, # chia - "12220":100, # flaxseed - "20137":100, # quinoa, cooked - "20006":100, # pearled barley - "20051":100, # white rice enriched cooked - "20041":100, # brown rice cooked - "12151":100, # pistachio - "19047":100, # pretzel - "12061":100, # almond - # LEGUME - "16057":100, # chickpeas - "16015":100, # black beans - "16043":100, # pinto beans - "16072":100, # lima beans - "16167":100, # peanut butter smooth - # VEGETABLE - "11124":100, # raw carrots - "11090":100, # broccoli - "11457":100, # spinach, raw - "11357":100, # baked potato - "11508":100, # baked sweet potato - "11530":100, # tomato, red, cooked - "11253":100, # lettuce - "11233":100, # kale - "11313":100, # peas - "11215":100, # garlic - # OTHER - "04053":100, # olive oil - "19904":100, # dark chocolate - "11238":100, # shiitake mushrooms - "19165":100, # cocoa powder +pantry = { + # DAIRY AND EGG + "173410":100, # butter, salted + "173430":100, # butter, without salt + "1097517":100, # 2% milk + "1097512":100, # milk, whole + "1097521":100, # skim milk + "1100335":100, # scrambled eggs + "173424":100, # hard boiled eggs + "173423":100, # fried egg + # MEAT + "175167":100, # atlantic salmon + "174608":100, # chicken breast oven-roasted + "1099608":100, # steak + "1099888":100, # turkey + # FRUIT + "1103883":100, # avocado + "1102710":100, # strawberries + "1102702":100, # blueberry + "1102708":100, # raspberry + "1105430":100, # red delicious apple + "173945":100, # banana + "1102594":100, # lemon + "1102597":100, # oranges + "1102665":100, # grapes + # PROCESSED + "1099796":100, # hamburger + "1101112":100, # pizza + "1100918":100, # ice cream + "174993":100, # donut + # DRINK + "1104331":100, # coke + "1104492":100, # tap water + "1104493":100, # bottled water + "171354":100, # orange juice + "1104292":100, # brewed green tea + "171890":100, # coffee brewed with tap water + # (milk is included in dairy group) + # GRAIN + "1100612":100, # chia + "1103860":100, # flaxseed + "168917":100, # quinoa, cooked + "170285":100, # pearled barley + "168880":100, # white rice enriched cooked + "1101628":100, # brown rice cooked + "1100549":100, # pistachio + "171370":100, # pretzel + "1100555":100, # almond + # LEGUME + "1100429":100, # chickpeas + "1100410":100, # black beans + "1100393":100, # pinto beans + "1100383":100, # lima beans + "324860":100 # peanut butter smooth + # VEGETABLE + "1103193":100, # raw carrots + "1103183":100, # broccoli + "1103136":100, # spinach, raw + "1102880":100, # baked potato + "1103261":100, # baked sweet potato + "170050":100, # tomato, red, cooked + "1103358":100, # lettuce + "1103116":100, # kale + "1103645":100, # peas + "1103845":100, # garlic + # OTHER + "1103861":100, # olive oil + "1104032":100, # dark chocolate + "168436":100, # shiitake mushrooms + "1097878":100, # cocoa powder } pantry_food = client.get_foods(pantry) ``` @@ -190,11 +181,11 @@ for rec in recommendations: # a recommendation is a list containing the calculated loss after the recommendation # is applied, the index of the pantry for the recommendation, and the amount of that # food / 100g - print(str(round(rec[2] * 100, 2)) + "g", "of", pantry_food[rec[1]].desc["name"]) + print(str(round(rec[2] * 100, 2)) + "g", "of", pantry_food[rec[1]].desc) ``` ``` -50.36g of Seeds, chia seeds, dried -53.3g of Nuts, almonds -56.76g of Peanut Butter, smooth (Includes foods for USDA's Food Distribution Program) +72.59g of Chia seeds +72.94g of Almond butter +77.01g of Peanut butter, smooth style, with salt ``` It is reasonable that the function returned these foods from the pantry as the current daily nutrition is low in protein and Omega-3s, which chia seeds satisfy the most. diff --git a/noms/__init__.py b/__init__.py similarity index 88% rename from noms/__init__.py rename to __init__.py index 5cf7827..f1836f5 100644 --- a/noms/__init__.py +++ b/__init__.py @@ -1,18 +1,18 @@ -name = "noms" -__version__ = "0.1.7" -__version_info__ = tuple(int(i) for i in __version__.split('.')) - -# client object -from noms.client.main import Client -# food and meal objects -from noms.objects.food import Food, Meal -# csv reports -from noms.report import export_report, report - -# nutrient dict -from noms.objects.nutrient_dict import index_from_name -from noms.objects.nutrient_dict import nutrient_dict - -# recommendation method -from noms.analyze import generate_recommendations -from noms.analyze import recommend_removal \ No newline at end of file +name = "noms" +__version__ = "0.1.7" +__version_info__ = tuple(int(i) for i in __version__.split('.')) + +# client object +from noms.client.main import Client +# food and meal objects +from noms.objects.food import Food, Meal +# csv reports +from noms.report import export_report, report + +# nutrient dict +from noms.objects.nutrient_dict import index_from_name +from noms.objects.nutrient_dict import nutrient_dict + +# recommendation method +from noms.analyze import generate_recommendations +from noms.analyze import recommend_removal diff --git a/noms/analyze.py b/analyze.py similarity index 77% rename from noms/analyze.py rename to analyze.py index 3a22e16..264e60f 100644 --- a/noms/analyze.py +++ b/analyze.py @@ -1,128 +1,138 @@ -from scipy.optimize import minimize -from .objects.food import Food, Meal -import copy -import sys -import os - -def norm_rda_deficit(norm_rda_arr): - """ Returns a modified list of nutrient dicts in which value represents - a fraction of how much a given nutrient has been satisfied. A value of - 0 represents full satisfaction, and 1 represents no satisfaction. """ - r_nut = copy.deepcopy(norm_rda_arr) - for ni, _ in enumerate(r_nut): - r_nut[ni]['value'] = 1 - r_nut[ni]['value'] - return r_nut - -def loss(meal, nutrient_dict, verbose=False): - if verbose: - print("Deficit breakdown of meal:") - deficit = norm_rda_deficit(meal.norm_rda(nutrient_dict)) - loss = 0; ni = 0 - for nut in deficit: - if verbose: - print("{nut:<20}: {val:>10} percent unmet".format(nut=nut['name'], val=round(nut['value'] * 100, 1))) - loss += nut['value'] ** 2 - ni += 1 - return loss - -def best_contributors(k, meal, suggestion, nutrient_dict, x): - """ - Returns the top nutrients that are being satisfied by a give suggestion - """ - sug_norm = suggestion.norm_rda(nutrient_dict) - sug_norm = copy.deepcopy(sug_norm) - nutrient_residuals = [] - req = norm_rda_deficit(meal.norm_rda(nutrient_dict)) - ni = 0 - for nut in sug_norm: - nut['value'] *= k - this_nutrient_required = req[ni]['value'] - if this_nutrient_required > 0: - resid = abs((this_nutrient_required ** 2) - ((nut['value']-this_nutrient_required) ** 2)) - nutrient_residuals.append(dict(value=resid, name=nut['name'])) - ni += 1 - return sorted(nutrient_residuals, key=lambda x: x['value'], reverse=True)[:x] - -def suggestion_loss(meal, suggestion, nutrient_dict, verbose=False): - """ - Minimizes the squared residual of each normed nutrient for a given - food suggestion and meal. These minimized values are then used to - find the best food recommendation for a given meal in the context - of a nutrient_dict - """ - def scaled_loss(k, *args): - sug_norm = args[1].norm_rda(args[2]) - sug_norm = copy.deepcopy(sug_norm) - loss = 0; ni = 0 - for nut in sug_norm: - nut['value'] *= k - this_nutrient_required = args[0][ni]['value'] - # don't track it as loss if it's superfluous - # i.e. its not required but there is no limit - if not (this_nutrient_required == 0 and nutrient_dict[ni]['limit'] == None): - '''if 'nickname' in nutrient_dict[ni].keys() and nutrient_dict[ni]['nickname'] == "Sugar": - loss += ((nut['value'] - this_nutrient_required) ** 2) * 3 - elif not nutrient_dict[ni]['group'] == "Proximates": - loss += (nut['value'] - this_nutrient_required) ** 2''' - #if not (nutrient_dict[ni]['name'] == "Sugars, total"): - loss += (nut['value'] - this_nutrient_required) ** 2 - ni += 1 - return loss - required_normed_nutrients = norm_rda_deficit(meal.norm_rda(nutrient_dict)) - sol = minimize(scaled_loss, 1, args=(required_normed_nutrients, suggestion, nutrient_dict), bounds=[(0.05, sys.maxsize)], tol=1e-2) - # this can be uncommented to display a graph showing the convergence to minimize loss - # as the mass of the given food is scaled - if verbose: - import matplotlib.pyplot as plt - xs = []; losses = [] - for x in range(0, 20): - inp = sol.x[0] * (x/10) - xs.append(inp) - losses.append(scaled_loss(inp, required_normed_nutrients, suggestion, nutrient_dict)) - plt.plot(xs, losses) - plt.title("Loss graph for {}".format(suggestion.desc["name"])) - plt.show() - return (scaled_loss(sol.x[0], required_normed_nutrients, suggestion, nutrient_dict), sol.x[0]) - -def generate_recommendations(meal, pantry, nutrient_dict, n, verbose=False): - """ - Gives the top n food recommendations to satisfy daily nutrition in - context of the current foods, available foods, and a nutrient_dict full - of RDIs. - Meal is a meal object representing the current day's meal - Pantry is an array of Food objects representing potential foods - (Note: pantry food objects must have a mass of 100g) - Nutrient Dict is a personalized list of RDIs and limits based on the user's preference - The function returns the loss of that recommendation, the index of the meal, - and the object being suggested itself. - """ - rec_data = [] - rec_index = 0; rec_loss = sys.maxsize; rec_optimum = 0 - for rec_i, _ in enumerate(pantry): - sug_obj = suggestion_loss(meal, pantry[rec_i], nutrient_dict, verbose) - cur_loss = sug_obj[0] - # print("name:", pantry[rec_i].desc['name'], "loss:", sug_obj[0], "k:", sug_obj[1]) - if cur_loss < rec_loss: - rec_loss = cur_loss - rec_index = rec_i - rec_optimum = sug_obj[1] - rec_data.append([copy.copy(cur_loss), copy.copy(rec_i), copy.deepcopy(sug_obj[1])]) - if verbose: - print(pantry[rec_i].desc['name'], cur_loss) - print("^" * 50) - rec_data.sort(key=lambda x: x[0]) - return rec_data[:n] - -def recommend_removal(meal, nutrient_dict): - assert type(meal) == Meal - o_loss = loss(meal, nutrient_dict) # calculate the original loss - losses = [] - for i in range(0, len(meal.foods)): - # compute the loss for the meal without each food - this_meal = Meal([food for j, food in enumerate(meal.foods) if j != i]) - losses.append(loss(this_meal, nutrient_dict)) - if min(losses) < o_loss: - return losses.index(min(losses)) # removing a certain food is beneficial - else: - return -1 # removing any certain food is detrimental - \ No newline at end of file +from scipy.optimize import minimize +from .objects.food import Food, Meal +import copy +import sys +import os + +def norm_rda_deficit(norm_rda_arr): + """ Returns a modified list of nutrient dicts in which value represents + a fraction of how much a given nutrient has been satisfied. A value of + 0 represents full satisfaction, and 1 represents no satisfaction. """ + r_nut = copy.deepcopy(norm_rda_arr) + for ni, _ in enumerate(r_nut): + r_nut[ni]['value'] = 1 - r_nut[ni]['value'] + return r_nut + +def loss(meal, nutrient_dict, verbose=False): + if verbose: + print("Deficit breakdown of meal:") + deficit = norm_rda_deficit(meal.norm_rda(nutrient_dict)) + loss = 0; ni = 0 + for nut in deficit: + if verbose: + print("{nut:<20}: {val:>10} percent unmet".format(nut=nut['name'], val=round(nut['value'] * 100, 1))) + loss += nut['value'] ** 2 + ni += 1 + return loss + +def assess_deficit(meal, nutrient_dict): + print("Deficit breakdown of meal:") + deficit = norm_rda_deficit(meal.norm_rda(nutrient_dict)) + loss = 0; ni = 0 + for nut in deficit: + #if nut['value'] != 0: + print("{nut:<20}: {val:>10} percent unmet".format(nut=nut['name'], val=round(nut['value'] * 100, 1))) + loss += nut['value'] ** 2 + ni += 1 + print("Loss: ", round(loss, 2)) + +def best_contributors(k, meal, suggestion, nutrient_dict, x): + """ + Returns the top nutrients that are being satisfied by a give suggestion + """ + sug_norm = suggestion.norm_rda(nutrient_dict) + sug_norm = copy.deepcopy(sug_norm) + nutrient_residuals = [] + req = norm_rda_deficit(meal.norm_rda(nutrient_dict)) + ni = 0 + for nut in sug_norm: + nut['value'] *= k + this_nutrient_required = req[ni]['value'] + if this_nutrient_required > 0: + resid = abs((this_nutrient_required ** 2) - ((nut['value']-this_nutrient_required) ** 2)) + nutrient_residuals.append(dict(value=resid, name=nut['name'])) + ni += 1 + return sorted(nutrient_residuals, key=lambda x: x['value'], reverse=True)[:x] + +def suggestion_loss(meal, suggestion, nutrient_dict): + """ + Minimizes the squared residual of each normed nutrient for a given + food suggestion and meal. These minimized values are then used to + find the best food recommendation for a given meal in the context + of a nutrient_dict + """ + def scaled_loss(k, *args): + _nutrient_dict = nutrient_dict()() + sug_norm = args[1].norm_rda(args[2]) + sug_norm = copy.deepcopy(sug_norm) + loss = 0; ni = 0 + for nut in sug_norm: + nut['value'] *= k + this_nutrient_required = args[0][ni]['value'] + # don't track it as loss if it's superfluous + # i.e. its not required but there is no limit + if not (this_nutrient_required == 0 and _nutrient_dict[ni]['limit'] == None): + '''if 'nickname' in nutrient_dict[ni].keys() and nutrient_dict[ni]['nickname'] == "Sugar": + loss += ((nut['value'] - this_nutrient_required) ** 2) * 3 + elif not nutrient_dict[ni]['group'] == "Proximates": + loss += (nut['value'] - this_nutrient_required) ** 2''' + #if not (nutrient_dict[ni]['name'] == "Sugars, total"): + loss += (nut['value'] - this_nutrient_required) ** 2 + ni += 1 + return loss + _nutrient_dict = nutrient_dict()() + required_normed_nutrients = norm_rda_deficit(meal.norm_rda(_nutrient_dict)) + sol = minimize(scaled_loss, 1, args=(required_normed_nutrients, suggestion, _nutrient_dict), bounds=[(0.05, sys.maxsize)], tol=1e-2) + """ + # this can be uncommented to display a graph showing the convergence to minimize loss + # as the mass of the given food is scaled + import matplotlib.pyplot as plt + xs = []; losses = [] + for x in range(0, 20): + inp = sol.x[0] * (x/10) + xs.append(inp) + losses.append(scaled_loss(inp, required_normed_nutrients, suggestion, nutrient_dict)) + plt.plot(xs, losses) + plt.show() + """ + return (scaled_loss(sol.x[0], required_normed_nutrients, suggestion, _nutrient_dict), sol.x[0]) + +def generate_recommendations(meal, pantry, nutrient_dict, n, verbose=False): + """ + Gives the top n food recommendations to satisfy daily nutrition in + context of the current foods, available foods, and a nutrient_dict full + of RDAs. + Meal is a meal object representing the current day's meal + Pantry is an array of Food objects representing potential foods + (Note: pantry food objects must have a mass of 100g) + Nutrient Dict is a personalized list of rdas and limits based on the user's preference + """ + rec_data = [] + rec_index = 0; rec_loss = sys.maxsize; rec_optimum = 0 + for rec_i, _ in enumerate(pantry): + sug_obj = suggestion_loss(meal, pantry[rec_i], nutrient_dict) + cur_loss = sug_obj[0] + # print("name:", pantry[rec_i].desc['name'], "loss:", sug_obj[0], "k:", sug_obj[1]) + if cur_loss < rec_loss: + rec_loss = cur_loss + rec_index = rec_i + rec_optimum = sug_obj[1] + rec_data.append([copy.copy(cur_loss), copy.copy(rec_i), copy.deepcopy(sug_obj[1])]) + if verbose: + print(pantry[rec_i].desc['name'], cur_loss) + print("^" * 50) + rec_data.sort(key=lambda x: x[0]) + return rec_data[:n] + +def recommend_removal(meal, nutrient_dict): + assert type(meal) == Meal + o_loss = loss(meal, nutrient_dict) # calculate the original loss + losses = [] + for i in range(0, len(meal.foods)): + # compute the loss for the meal without each food + this_meal = Meal([food for j, food in enumerate(meal.foods) if j != i]) + losses.append(loss(this_meal, nutrient_dict)) + if min(losses) < o_loss: + return losses.index(min(losses)) # removing a certain food is beneficial + else: + return -1 # removing any certain food is detrimental diff --git a/noms/objects/__init__.py b/client/__init__.py similarity index 100% rename from noms/objects/__init__.py rename to client/__init__.py diff --git a/client/dict_parse.py b/client/dict_parse.py new file mode 100644 index 0000000..3c26140 --- /dev/null +++ b/client/dict_parse.py @@ -0,0 +1,117 @@ +import operator +from ..objects.food import Food + +def search_parse(search_results): + """ Return a simplified version of the json object returned from the USDA API. + This deletes extraneous pieces of information that are not important for providing + context on the search results. + """ + if 'errors' in search_results.keys(): + return None + # Store the search term that was used to produce these results + search_term = search_results['foodSearchCriteria']['query'] + if search_results['foods'] == []: + return None + else: + return dict(search_term=search_term, items=search_results['foods']) + +def food_parse(food_results, nutrient_dict, values): + """ Return a simplified version of the json object returned from the USDA API. + This deletes extraneous pieces of information, including nutrients that are + not tracked. It also exchanges nutrient names for their more common names, or "nicknames", + as defined in noms.objects.nutrient_dict + """ + if len(food_results) == 0: + return None + food_arr = [] + tracked_nutrients = [] + nutrient_nicknames = [] + # nutrient_dict is a global variable; some of the + # assignments below alters it value across modules + # thus making a shallow copy it + nutrient_dict = nutrient_dict()() + for nutrient in nutrient_dict: + if "nickname" in nutrient.keys(): + nutrient_nicknames.append(nutrient["nickname"]) + + else: + nutrient_nicknames.append(None) + #nutrient['nutrient_id'] = str(nutrient['nutrient_id']) + tracked_nutrients.append(nutrient["nutrient_id"]) + # Iterate through each food and simplify names + f = 0 + for food in food_results: + # create a 'value' key and equate it to 'amount' + # to take into account the changes in the new + # api results + for nutrient in food["foodNutrients"]: + if 'amount' in nutrient.keys(): + nutrient['value'] = nutrient['amount'] + else: + nutrient['value'] = 0 + nutrient['nutrient']['number'] = float(nutrient['nutrient']['number']) + nutrient['name'] = nutrient['nutrient']['name'] + nutrient['unit'] = nutrient['nutrient']['unitName'] + nutrient['nutrient_id'] = nutrient['nutrient']['number'] + + # sort nutrients by id if not already + n_list = food["foodNutrients"] + n_list.sort(key=lambda x: x['nutrient']['number']) + # end sort + n = 0 + for nutrient in food["foodNutrients"]: + if n == len(tracked_nutrients): + break + # check if this is a nutrient we should record + if (nutrient["nutrient"]['number']) == (tracked_nutrients[n]): + potential_name = nutrient_nicknames[n] + if potential_name != None: + nutrient["nutrient"]["name"] = potential_name + n += 1 + + # check if the food doesn't contain a tracked nutrient + while n < len(tracked_nutrients) and (nutrient["nutrient"]["number"]) > tracked_nutrients[n]: + to_insert = nutrient_dict[n].copy() + to_insert.update(value=0) + to_insert["nutrient"] = {"number":to_insert["nutrient_id"]} + food["foodNutrients"].insert(n,to_insert) + n += 1 + while n < len(tracked_nutrients) and food["foodNutrients"][-1]["nutrient"]["number"] < tracked_nutrients[-1]: + to_insert = nutrient_dict[n].copy() + to_insert.update(value=0) + + to_insert["nutrient"] = {"number":to_insert["nutrient_id"]} + food["foodNutrients"].insert(n,to_insert) + n += 1 + n = 0 + n_to_del = [] + for nutrient in food["foodNutrients"]: + # check if this is a nutrient we should delete + if (nutrient["nutrient"]["number"]) not in tracked_nutrients: + n_to_del.append(n) + n += 1 + offset = 0 + for del_n in n_to_del: + del food["foodNutrients"][del_n - offset] + offset += 1 + # sort nutrients by id if not already + n_list = food["foodNutrients"] + n_list.sort(key=lambda x: x['nutrient']['number']) + # end sort + n = 0 + for nutrient in food["foodNutrients"]: + if nutrient_nicknames[n] != None: + nutrient["name"] = nutrient_nicknames[n] + nutrient["value"] = nutrient["value"] * (values[f]/100) + n += 1 + # deleting keys except that in keys_to_keep + keys_to_keep = ['nutrient_id', 'name', 'group', 'unit', 'value'] + for nutrient in food['foodNutrients']: + nutrient_copy = nutrient.copy() + for key in nutrient_copy: + if key not in keys_to_keep: + del nutrient[key] + + f += 1 + food_arr.append(Food(food)) + return food_arr diff --git a/noms/client/main.py b/client/main.py similarity index 76% rename from noms/client/main.py rename to client/main.py index 89ba910..15efdb5 100644 --- a/noms/client/main.py +++ b/client/main.py @@ -1,118 +1,113 @@ -import requests -import json -import copy -import operator -from itertools import islice -from .dict_parse import search_parse, food_parse -from ..objects.nutrient_dict import nutrient_dict - -class SearchResults(): - """ - An object returned by Client.search_query which stores a Python dictionary - containing all of the search result information. - """ - def __init__(self, json): - self.json = json - def __str__(self, max_entries=None): - r_str = "" - if self.json == None: - r_str += "There are no search results for this query\n" - else: - r_str +="="*112 + "\n" - r_str +="Search results for \'{}\' on USDA Standard Reference Database".format(self.json["search_term"]) + "\n" - r_str +="="*112 + "\n" - if max_entries == None: - max_entries = len(self.json["items"]) - if max_entries < len(self.json["items"]): - self.json["items"] = self.json["items"][:max_entries] - self.json["items"].sort(key=operator.itemgetter("group")) - r_str +="{name:<72} {group:^30} {id:>8}".format(name="Name",group="Group",id="ID") + "\n" - for item in self.json["items"]: - if len(item["name"]) > 70: - item["name"] = item["name"][:70] + ".." - if len(item["group"]) > 28: - item["group"] = item["group"][:28] + ".." - r_str +="{name:<72} {group:^30} {id:>8}".format(name=item["name"],group=item["group"],id=item["ndbno"]) + "\n" - r_str +="="*112 + "\n" - return r_str - -class Client: - """ - The Client class is used to interface with the USDA Standard Reference Database - API. It must be initialized with an API key. - """ - - url = 'https://api.nal.usda.gov/usda/ndb' - - def __init__(self, key): - """ - A Client instance must be initialized with a key from - data.gov. This is free to obtain, and you can request one - here: https://api.data.gov/signup/ - """ - self.key = key - - def call(self, params, url_suffix): - """ target_url could be: - https://api.nal.usda.gov/usda/ndb/V2/reports - https://api.nal.usda.gov/usda/ndb/search - depending on which service of the api is being used - """ - target_url = self.url + url_suffix - # add the key to the API call - call_params = dict(params, api_key=self.key) - response = json.loads(requests.get(url=target_url, params=call_params).text) - return response - - def search_query(self, name): - params = dict( - q=name, - ds='Standard Reference', - format='json' - ) - result = search_parse(self.call(params,'/search')) - if result == None: - return None - else: - return SearchResults(search_parse(self.call(params, '/search'))) - - def food_query(self, ids): - # allow for either a single id (ndbno) query, or a list of queries - if type(ids) == list: - if len(ids) > 25: - raise Exception("Too many Food ID arguments. API limits it to 25.") - params = dict(ndbno=ids) - params.update(dict(type='f', format='json')) - return_obj = self.call(params, '/V2/reports') - offset = 0 - if 'foods' not in return_obj: - print("See the following error: {}".format(return_obj)) - return None - for i in range(0, len(return_obj["foods"])): - if 'error' in return_obj["foods"][i-offset].keys(): - del return_obj["foods"][i-offset] - offset += 1 - return return_obj - - def get_foods(self, id_value_dict): - # If more than 25 words are being queried, split it up - if len(id_value_dict.keys()) > 25: - print("Must call the database {} times, this may take a couple moments. Status: {leng}/{leng}".format(len(id_value_dict.keys())//25+1,leng=len(id_value_dict.keys()))) - dict_copy = id_value_dict.copy() - food_obj = [] - while len(dict_copy.keys()) > 25: - current_dict = {} - items = islice(dict_copy.items(), 25) - current_dict.update(items) - call = self.food_query(current_dict.keys()) - food_obj += food_parse(call, nutrient_dict, list(current_dict.values())) - for key in current_dict.keys(): - del dict_copy[key] - print("Status: {}/{}".format(len(dict_copy.keys()), len(id_value_dict.keys()))) - call = self.food_query(dict_copy.keys()) - food_obj += food_parse(call, nutrient_dict, list(dict_copy.values())) - print("Complete!") - else: - food_obj = self.food_query(id_value_dict.keys()) - food_obj = food_parse(food_obj, nutrient_dict, list(id_value_dict.values())) - return food_obj +import requests +import json +import copy +import operator +from itertools import islice +from .dict_parse import search_parse, food_parse +from ..objects.nutrient_dict import nutrient_dict + +class SearchResults(): + """ + An object returned by Client.search_query which stores a Python dictionary + containing all of the search result information. + """ + def __init__(self, json): + self.json = json + def __str__(self, max_entries=None): + r_str = "" + if self.json == None: + r_str += "There are no search results for this query\n" + else: + r_str +="="*112 + "\n" + r_str +="Search results for \'{}\' on USDA Standard Reference Database".format(self.json["search_term"]) + "\n" + r_str +="="*112 + "\n" + if max_entries == None: + max_entries = len(self.json["items"]) + if max_entries < len(self.json["items"]): + self.json["items"] = self.json["items"][:max_entries] + self.json["items"].sort(key=operator.itemgetter("dataType")) + r_str +="{name:<72} {group:^30} {id:>8}".format(name="description",group="dataType",id="ID") + "\n" + for item in self.json["items"]: + if len(item["description"]) > 70: + item["description"] = item["description"][:70] + ".." + if len(item["dataType"]) > 28: + item["dataType"] = item["dataType"][:28] + ".." + r_str +="{name:<72} {group:^30} {id:>8}".format(name=item["description"],group=item["dataType"],id=item["fdcId"]) + "\n" + r_str +="="*112 + "\n" + return r_str + +class Client: + """ + The Client class is used to interface with the USDA Standard Reference Database + API. It must be initialized with an API key. + """ + + #url = 'https://api.nal.usda.gov/usda/ndb' + url = 'https://api.nal.usda.gov/fdc/v1/foods' + + + def __init__(self, key): + """ + A Client instance must be initialized with a key from + data.gov. This is free to obtain, and you can request one + here: https://api.data.gov/signup/ + """ + self.key = key + + def call(self, params, url_suffix): + """ target_url could be: + https://api.nal.usda.gov/usda/ndb/V2/reports + https://api.nal.usda.gov/usda/ndb/search + depending on which service of the api is being used + """ + target_url = self.url + url_suffix + # add the key to the API call + call_params = dict(params, api_key=self.key) + test_response = requests.get(url=target_url, params=call_params) + response = json.loads(requests.get(url=target_url, params=call_params).text) + return response + + def search_query(self, name): + params = dict( + query=name, + requireAllWords=True, + dataType=['Foundation', 'Survey (FNDDS)', 'SR Legacy'] + ) + return SearchResults(search_parse(self.call(params, '/search'))) + + def food_query(self, ids): + # allow for either a single id (fdcId) query, or a list of queries + if type(ids) == list: + if len(ids) > 25: + raise Exception("Too many Food ID arguments. API limits it to 25.") + params = dict(fdcIds=ids) + return_obj = self.call(params, '') + offset = 0 + + if 'error' in return_obj: + print("See the following error: {}".format(return_obj)) + exit() + return return_obj + + def get_foods(self, id_value_dict): + # If more than 25 words are being queried, split it up + if len(id_value_dict.keys()) > 25: + print("Must call the database {} times, this may take a couple moments. Status: {leng}/{leng}".format(len(id_value_dict.keys())//25+1,leng=len(id_value_dict.keys()))) + dict_copy = id_value_dict.copy() + food_obj = [] + while len(dict_copy.keys()) > 25: + current_dict = {} + items = islice(dict_copy.items(), 25) + current_dict.update(items) + call = self.food_query(current_dict.keys()) + food_obj += food_parse(call, nutrient_dict, list(current_dict.values())) + for key in current_dict.keys(): + del dict_copy[key] + print("Status: {}/{}".format(len(dict_copy.keys()), len(id_value_dict.keys()))) + call = self.food_query(dict_copy.keys()) + food_obj += food_parse(call, nutrient_dict, list(dict_copy.values())) + print("Complete!") + else: + food_obj = self.food_query(id_value_dict.keys()) + food_obj = food_parse(food_obj, nutrient_dict, list(id_value_dict.values())) + return food_obj diff --git a/food.py b/food.py new file mode 100644 index 0000000..dc05b85 --- /dev/null +++ b/food.py @@ -0,0 +1,29 @@ +from .client.main import Client +from .client.dict_parse import food_parse +from .objects.nutrient_dict import nutrient_dict +from .objects.food import Meal +from itertools import islice +import copy + +def foods(id_value_dict, client): + # call the client. If more than 25 words are being queried, split it up + if len(id_value_dict.keys()) > 25: + print("Must call the database {} times, this may take a couple moments. Status: {leng}/{leng}".format(len(id_value_dict.keys())//25+1,leng=len(id_value_dict.keys()))) + dict_copy = id_value_dict.copy() + food_obj = [] + while len(dict_copy.keys()) > 25: + current_dict = {} + items = islice(dict_copy.items(), 25) + current_dict.update(items) + call = client.food_query(current_dict.keys()) + food_obj += food_parse(call, nutrient_dict, list(current_dict.values())) + for key in current_dict.keys(): + del dict_copy[key] + print("Status: {}/{}".format(len(dict_copy.keys()), len(id_value_dict.keys()))) + call = client.food_query(dict_copy.keys()) + food_obj += food_parse(call, nutrient_dict, list(dict_copy.values())) + print("Complete!") + else: + food_obj = client.food_query(id_value_dict.keys()) + food_obj = food_parse(food_obj, nutrient_dict, list(id_value_dict.values())) + return food_obj \ No newline at end of file diff --git a/noms/client/dict_parse.py b/noms/client/dict_parse.py deleted file mode 100644 index c792134..0000000 --- a/noms/client/dict_parse.py +++ /dev/null @@ -1,102 +0,0 @@ -import operator -from ..objects.food import Food - -def search_parse(search_results): - """ Return a simplified version of the json object returned from the USDA API. - This deletes extraneous pieces of information that are not important for providing - context on the search results. - """ - if 'errors' in search_results.keys(): - return None - # Store the search term that was used to produce these results - search_term = search_results["list"]["q"] - # Store a list of dictionary items for each result of the search - items = [] - for item in search_results["list"]["item"]: - # Remove extraneous pieces of data - del item["ds"]; del item["manu"]; del item["offset"] - items.append(item) - return dict(search_term=search_term, items=items) - -def food_parse(food_results, nutrient_dict, values): - """ Return a simplified version of the json object returned from the USDA API. - This deletes extraneous pieces of information, including nutrients that are - not tracked. It also exchanges nutrient names for their more common names, or "nicknames", - as defined in noms.objects.nutrient_dict - """ - if len(food_results["foods"]) == 0: - return None - food_arr = [] - tracked_nutrients = [] - nutrient_nicknames = [] - for nutrient in nutrient_dict: - if "nickname" in nutrient.keys(): - nutrient_nicknames.append(nutrient["nickname"]) - else: - nutrient_nicknames.append(None) - tracked_nutrients.append(nutrient["nutrient_id"]) - food_results = food_results["foods"] - # Remove extraneous pieces of data in the food description - to_del = {'food':["sr", "type", "sources", "footnotes", "langual"], - 'desc':["sd", "sn", "cn", "manu", "nf", "cf", "ff", "pf", "r", "rd", "ru", "ds"], - # This current implementation deletes the measurement data from the database, - # this could be changed later to provide richer data and easy UX tools (select measure) - 'nutrients':["derivation", "sourcecode", "dp", "se"]} - # Iterate through each food and remove extra information, and simplify names - f = 0 - for food in food_results: - for del_item in to_del["food"]: - del food["food"][del_item] - for del_item in to_del["desc"]: - del food["food"]["desc"][del_item] - # sort nutrients by id if not already - n_list = food["food"]["nutrients"] - n_list.sort(key=operator.itemgetter("nutrient_id")) - # end sort - n = 0 - for nutrient in food["food"]["nutrients"]: - if n == len(tracked_nutrients): - break - # check if this is a nutrient we should record - if nutrient["nutrient_id"] == tracked_nutrients[n]: - potential_name = nutrient_nicknames[n] - if potential_name != None: - nutrient["name"] = potential_name - for del_item in to_del["nutrients"]: - del nutrient[del_item] - n += 1 - # check if the food doesn't contain a tracked nutrient - while n < len(tracked_nutrients) and nutrient["nutrient_id"] > tracked_nutrients[n]: - to_insert = nutrient_dict[n] - to_insert.update(value=0) - food["food"]["nutrients"].insert(n,to_insert) - n += 1 - while n < len(tracked_nutrients) and food["food"]["nutrients"][-1]["nutrient_id"] < tracked_nutrients[-1]: - to_insert = nutrient_dict[n] - to_insert.update(value=0) - food["food"]["nutrients"].insert(n,to_insert) - n += 1 - n = 0 - n_to_del = [] - for nutrient in food["food"]["nutrients"]: - # check if this is a nutrient we should delete - if nutrient["nutrient_id"] not in tracked_nutrients: - n_to_del.append(n) - n += 1 - offset = 0 - for del_n in n_to_del: - del food["food"]["nutrients"][del_n - offset] - offset += 1 - # sort nutrients by id if not already - n_list = food["food"]["nutrients"] - n_list.sort(key=operator.itemgetter("nutrient_id")) - # end sort - n = 0 - for nutrient in food["food"]["nutrients"]: - if nutrient_nicknames[n] != None: - food["food"]["nutrients"][n]["name"] = nutrient_nicknames[n] - nutrient["value"] = nutrient["value"] * (values[f]/100) - n += 1 - f += 1 - food_arr.append(Food(food)) - return food_arr \ No newline at end of file diff --git a/noms/objects/nutrient_dict.py b/noms/objects/nutrient_dict.py deleted file mode 100644 index 8cbe1b0..0000000 --- a/noms/objects/nutrient_dict.py +++ /dev/null @@ -1,158 +0,0 @@ -import json -import os -import codecs - -def index_from_name(name): - i = 0 - for nutrient in nutrient_dict: - if nutrient["name"] == name: - return i - if "nickname" in nutrient.keys(): - if nutrient["nickname"] == name: - return i - i += 1 - # if not found, return -1 - return -1 - -dir_path = os.path.dirname(os.path.realpath(__file__)) -nutrient_file = codecs.open("{}/nutrient_ids.json".format(dir_path), encoding="utf-8").read() -nutrient_dict = json.loads(nutrient_file) - -# PROFILE INFORMATION -tdee = 2000 #kcal per day -# What percent of daily caloric intake should each macro take? -protein_p = 0.25 -carb_p = 0.50 -fat_p = 0.25 -# Sugar should be no more than 10% of total calories -# US dietary guidelines: https://health.gov/dietaryguidelines/2015/guidelines/executive-summary/ -sugar_p = 0.10 - -# Assign RDAs, some dependent on gender or tdee -for item in nutrient_dict: - id = item["nutrient_id"] - # PROXIMATES - if id == 203: # Protein, g - item.update(rda=(tdee/4)*protein_p) - if id == 204: # Fat, g - item.update(rda=(tdee/9)*fat_p) - if id == 205: # Carbs, g - item.update(rda=(tdee/4)*carb_p) - if id == 207: # Ash, g - item.update(rda=None) - if id == 208: # Calories, kcal - item.update(rda=tdee) - if id == 221: # Alcohol, g note: 7 calories per gram - item.update(rda=None) - if id == 255: # Water, g - item.update(rda=2000) - # OTHER - if id == 262: # Caffeine, mg - item.update(rda=None) - item.update(limit=400) - if id == 263: # Theobromine, mg - item.update(rda=None) - item.update(limit=300) - # PROXIMATES - if id == 269: # Sugar, g - item.update(rda=None) - item.update(limit=(tdee/4)*sugar_p) - if id == 291: # Fiber, g note: 14 grams for every 1000 calories - item.update(rda=tdee*0.014) - # MINERALS - if id == 301: # Calcium, mg - item.update(rda=1000) - item.update(limit=2500) - if id == 303: # Iron, mg - item.update(rda=8) - item.update(limit=45) - if id == 304: # Magnesium, mg - item.update(rda=300) - item.update(limit=700) - if id == 305: # Phosphorus, mg - item.update(rda=700) - item.update(limit=4000) - if id == 306: # Potassium, mg - item.update(rda=1400) - item.update(limit=6000) - if id == 307: # Sodium, mg - item.update(rda=1000) - item.update(limit=2300) - if id == 309: # Zinc, mg - item.update(rda=12) - item.update(limit=100) - if id == 312: # Copper, mg - item.update(rda=0.9) - item.update(limit=10) - if id == 313: # Fluoride, ug - item.update(rda=400) - item.update(limit=10000) - if id == 315: # Manganese, mg - item.update(rda=1.8) - if id == 317: # Selenium, ug - item.update(rda=70) - item.update(limit=400) - # VITAMINS - if id == 318: # Vitamin A, IU - item.update(rda=900) - item.update(limit=20000) - if id == 323: # Vitamin E, mg - item.update(rda=15) - item.update(limit=1000) - if id == 324: # Vitamin D, IU - item.update(rda=1000) - item.update(limit=8000) - if id == 401: # Vitamin C, mg - item.update(rda=90) - item.update(limit=2000) - if id == 404: # Vitamin B-1, mg - item.update(rda=1.2) - if id == 405: # Vitamin B-2, mg - item.update(rda=1.3) - if id == 406: # Vitamin B-3, mg - item.update(rda=16) - if id == 410: # Vitamin B-5, mg - item.update(rda=4) - if id == 415: # Vitamin B-6, mg - item.update(rda=1.3) - item.update(limit=100) - if id == 417: # Vitamin B-9, ug - item.update(rda=400) - item.update(limit=1000) - if id == 418: # Vitamin B-12, mg - item.update(rda=2.4) - if id == 421: # Choline, mg - item.update(rda=550) - item.update(limit=3500) - if id == 430: # Vitamin K, ug - item.update(rda=120) - # LIPIDS - if id == 601: # Cholesterol, mg - item.update(rda=None) - item.update(limit=300) - if id == 605: # Trans Fat, g - item.update(rda=None) - # avoid more than 5% of fat calories from trans fat - item.update(limit=(tdee/9)*(fat_p*0.05)) - if id == 606: # Saturated Fat, g - item.update(rda=None) - item.update(limit=(tdee/9)*(fat_p*0.3)) - if id == 621: # DHA, g - item.update(rda=0.5) - if id == 629: # EPA, g - item.update(rda=0.5) - if id == 645: # Monounsaturated Fat, g - item.update(rda=(tdee/9)*(fat_p*.40)) - if id == 646: # Polyunsaturated Fat, g - item.update(rda=(tdee/9)*(fat_p*.30)) - if id == 851: # ALA, g - item.update(rda=0.6) - if "limit" not in item.keys(): - item.update(limit=None) - -# Round values to avoid long decimals in rda values -for item in nutrient_dict: - if item["rda"] != None: - item["rda"] = round(item["rda"], 2) - if item["limit"] != None: - item["limit"] = round(item["limit"], 2) \ No newline at end of file diff --git a/objects/__init__.py b/objects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/noms/objects/food.py b/objects/food.py similarity index 68% rename from noms/objects/food.py rename to objects/food.py index 8e12931..b5a539a 100644 --- a/noms/objects/food.py +++ b/objects/food.py @@ -1,64 +1,67 @@ -import copy -import operator -from .nutrient_dict import nutrient_dict, index_from_name - -def norm_rda(nutrient_array, nutrient_dict, disp=False): - r_nut = copy.deepcopy(nutrient_array) - for ni, _ in enumerate(nutrient_dict): - norm_val = 0 - if nutrient_dict[ni]['rda'] != None: - if r_nut[ni]['value'] < nutrient_dict[ni]['rda']: - # value is 5, rda is 15 - # norm value is 5/15 = 0.33 - r_nut[ni].update(to="rda") - norm_val = r_nut[ni]['value']/nutrient_dict[ni]['rda'] - else: - # value is 30, rda is 15 - # norm value is 1 - r_nut[ni].update(to="rda") - norm_val = 1 - if nutrient_dict[ni]['limit'] != None: - if r_nut[ni]['value'] > nutrient_dict[ni]['limit']: - r_nut[ni].update(to="limit") - norm_val = r_nut[ni]['value']/nutrient_dict[ni]['limit'] - elif nutrient_dict[ni]['rda'] == None: - if disp: - r_nut[ni].update(to="limit") - norm_val = r_nut[ni]['value']/nutrient_dict[ni]['limit'] - else: - norm_val = 1 - r_nut[ni].update(value=norm_val) - if 'measures' in r_nut[ni].keys(): - del r_nut[ni]['measures'] - del r_nut[ni]['unit'] - return r_nut - -class Food: - def __init__(self, data): - self.data = data - self.desc = data["food"]["desc"] - self.nutrients = data["food"]["nutrients"] - def norm_rda(self, nutrient_dict): - return norm_rda(self.nutrients, nutrient_dict) - -class Meal: - def __init__(self, foods): - self.foods = foods - self.nutrients = [] - for nutrient in foods[0].nutrients: - to_app = nutrient.copy() - to_app["value"] = 0 - self.nutrients.append(to_app) - for food in foods: - n = 0 - for nutrient in food.nutrients: - self.nutrients[n]["value"] += nutrient["value"] - n += 1 - for ni, _ in enumerate(self.nutrients): - self.nutrients[ni]["value"] = self.nutrients[ni]["value"] - def sort_by_top(self, n): - ni = index_from_name(n) - self.foods.sort(key=lambda f: f.nutrients[ni]["value"], reverse=True) - def norm_rda(self, nutrient_dict, disp=False): - return norm_rda(self.nutrients, nutrient_dict, disp) - +import copy +import operator +from .nutrient_dict import nutrient_dict, index_from_name + +def norm_rda(nutrient_array, nutrient_dict, disp=False): + r_nut = copy.deepcopy(nutrient_array) + if not isinstance(nutrient_dict, (list,)) : + _nutrient_dict = nutrient_dict()() + else: + _nutrient_dict = nutrient_dict + for ni, _ in enumerate(_nutrient_dict): + norm_val = 0 + if _nutrient_dict[ni]['rda'] != None: + if r_nut[ni]['value'] < _nutrient_dict[ni]['rda']: + # value is 5, rda is 15 + # norm value is 5/15 = 0.33 + r_nut[ni].update(to="rda") + norm_val = r_nut[ni]['value']/_nutrient_dict[ni]['rda'] + else: + # value is 30, rda is 15 + # norm value is 1 + r_nut[ni].update(to="rda") + norm_val = 1 + if _nutrient_dict[ni]['limit'] != None: + if r_nut[ni]['value'] > _nutrient_dict[ni]['limit']: + r_nut[ni].update(to="limit") + norm_val = r_nut[ni]['value']/_nutrient_dict[ni]['limit'] + elif _nutrient_dict[ni]['rda'] == None: + if disp: + r_nut[ni].update(to="limit") + norm_val = r_nut[ni]['value']/_nutrient_dict[ni]['limit'] + else: + norm_val = 1 + r_nut[ni].update(value=norm_val) + if 'measures' in r_nut[ni].keys(): + del r_nut[ni]['measures'] + del r_nut[ni]['unit'] + return r_nut + +class Food: + def __init__(self, data): + self.data = data + self.desc = data["description"] + self.nutrients = data["foodNutrients"] + def norm_rda(self, nutrient_dict): + return norm_rda(self.nutrients, nutrient_dict) + +class Meal: + def __init__(self, foods): + self.foods = foods + self.nutrients = [] + for nutrient in foods[0].nutrients: + to_app = nutrient.copy() + to_app["value"] = 0 + self.nutrients.append(to_app) + for food in foods: + n = 0 + for nutrient in food.nutrients: + self.nutrients[n]["value"] += nutrient["value"] + n += 1 + for ni, _ in enumerate(self.nutrients): + self.nutrients[ni]["value"] = self.nutrients[ni]["value"] + def sort_by_top(self, n): + ni = index_from_name(n) + self.foods.sort(key=lambda f: f.nutrients[ni]["value"], reverse=True) + def norm_rda(self, nutrient_dict, disp=False): + return norm_rda(self.nutrients, nutrient_dict, disp) diff --git a/objects/nutrient_dict.py b/objects/nutrient_dict.py new file mode 100644 index 0000000..adc8f03 --- /dev/null +++ b/objects/nutrient_dict.py @@ -0,0 +1,164 @@ +import json +import os +import codecs + +def index_from_name(name): + i = 0 + _nutrient_dict = nutrient_dict()() + for nutrient in _nutrient_dict: + if nutrient["name"] == name: + return i + if "nickname" in nutrient.keys(): + if nutrient["nickname"] == name: + return i + i += 1 + # if not found, return -1 + return -1 + +class nutrient_dict: + def __init__(self): + dir_path = os.path.dirname(os.path.realpath(__file__)) + nutrient_file = codecs.open("{}/nutrient_ids.json".format(dir_path), encoding="utf-8").read() + nutrient_dict = json.loads(nutrient_file) + + # PROFILE INFORMATION + tdee = 2000 #kcal per day + # What percent of daily caloric intake should each macro take? + protein_p = 0.25 + carb_p = 0.50 + fat_p = 0.25 + # Sugar should be no more than 10% of total calories + # US dietary guidelines: https://health.gov/dietaryguidelines/2015/guidelines/executive-summary/ + sugar_p = 0.10 + + # Assign RDAs, some dependent on gender or tdee + for item in nutrient_dict: + id = item["nutrient_id"] + # PROXIMATES + if id == 203: # Protein, g + item.update(rda=(tdee/4)*protein_p) + if id == 204: # Fat, g + item.update(rda=(tdee/9)*fat_p) + if id == 205: # Carbs, g + item.update(rda=(tdee/4)*carb_p) + if id == 207: # Ash, g + item.update(rda=None) + if id == 208: # Calories, kcal + item.update(rda=tdee) + if id == 221: # Alcohol, g note: 7 calories per gram + item.update(rda=None) + if id == 255: # Water, g + item.update(rda=2000) + # OTHER + if id == 262: # Caffeine, mg + item.update(rda=None) + item.update(limit=400) + if id == 263: # Theobromine, mg + item.update(rda=None) + item.update(limit=300) + # PROXIMATES + if id == 269: # Sugar, g + item.update(rda=None) + item.update(limit=(tdee/4)*sugar_p) + if id == 291: # Fiber, g note: 14 grams for every 1000 calories + item.update(rda=tdee*0.014) + # MINERALS + if id == 301: # Calcium, mg + item.update(rda=1000) + item.update(limit=2500) + if id == 303: # Iron, mg + item.update(rda=8) + item.update(limit=45) + if id == 304: # Magnesium, mg + item.update(rda=300) + item.update(limit=700) + if id == 305: # Phosphorus, mg + item.update(rda=700) + item.update(limit=4000) + if id == 306: # Potassium, mg + item.update(rda=1400) + item.update(limit=6000) + if id == 307: # Sodium, mg + item.update(rda=1000) + item.update(limit=2300) + if id == 309: # Zinc, mg + item.update(rda=12) + item.update(limit=100) + if id == 312: # Copper, mg + item.update(rda=0.9) + item.update(limit=10) + if id == 313: # Fluoride, ug + item.update(rda=400) + item.update(limit=10000) + if id == 315: # Manganese, mg + item.update(rda=1.8) + if id == 317: # Selenium, ug + item.update(rda=70) + item.update(limit=400) + # VITAMINS + if id == 318: # Vitamin A, IU + item.update(rda=900) + item.update(limit=20000) + if id == 323: # Vitamin E, mg + item.update(rda=15) + item.update(limit=1000) + if id == 324: # Vitamin D, IU + item.update(rda=1000) + item.update(limit=8000) + if id == 401: # Vitamin C, mg + item.update(rda=90) + item.update(limit=2000) + if id == 404: # Vitamin B-1, mg + item.update(rda=1.2) + if id == 405: # Vitamin B-2, mg + item.update(rda=1.3) + if id == 406: # Vitamin B-3, mg + item.update(rda=16) + if id == 410: # Vitamin B-5, mg + item.update(rda=4) + if id == 415: # Vitamin B-6, mg + item.update(rda=1.3) + item.update(limit=100) + if id == 417: # Vitamin B-9, ug + item.update(rda=400) + item.update(limit=1000) + if id == 418: # Vitamin B-12, mg + item.update(rda=2.4) + if id == 421: # Choline, mg + item.update(rda=550) + item.update(limit=3500) + if id == 430: # Vitamin K, ug + item.update(rda=120) + # LIPIDS + if id == 601: # Cholesterol, mg + item.update(rda=None) + item.update(limit=300) + if id == 605: # Trans Fat, g + item.update(rda=None) + # avoid more than 5% of fat calories from trans fat + item.update(limit=(tdee/9)*(fat_p*0.05)) + if id == 606: # Saturated Fat, g + item.update(rda=None) + item.update(limit=(tdee/9)*(fat_p*0.3)) + if id == 621: # DHA, g + item.update(rda=0.5) + if id == 629: # EPA, g + item.update(rda=0.5) + if id == 645: # Monounsaturated Fat, g + item.update(rda=(tdee/9)*(fat_p*.40)) + if id == 646: # Polyunsaturated Fat, g + item.update(rda=(tdee/9)*(fat_p*.30)) + if id == 851: # ALA, g + item.update(rda=0.6) + if "limit" not in item.keys(): + item.update(limit=None) + + # Round values to avoid long decimals in rda values + for item in nutrient_dict: + if item["rda"] != None: + item["rda"] = round(item["rda"], 2) + if item["limit"] != None: + item["limit"] = round(item["limit"], 2) + self.nutrient_dict = nutrient_dict + def __call__(self): + return self.nutrient_dict diff --git a/noms/objects/nutrient_ids.json b/objects/nutrient_ids.json similarity index 98% rename from noms/objects/nutrient_ids.json rename to objects/nutrient_ids.json index c4836e6..dca5b91 100644 --- a/noms/objects/nutrient_ids.json +++ b/objects/nutrient_ids.json @@ -1,46 +1,46 @@ -[ -{"nutrient_id": 203, "name": "Protein", "group": "Proximates", "unit": "g"}, -{"nutrient_id": 204, "name": "Total lipid (fat)", "group": "Proximates", "unit": "g", "nickname":"Fat"}, -{"nutrient_id": 205, "name": "Carbohydrate, by difference", "group": "Proximates", "unit": "g", "nickname":"Carbs"}, -{"nutrient_id": 208, "name": "Energy", "group": "Proximates", "unit": "kcal", "nickname":"Calories"}, -{"nutrient_id": 255, "name": "Water", "group": "Proximates", "unit": "g"}, -{"nutrient_id": 262, "name": "Caffeine", "group": "Other", "unit": "mg"}, -{"nutrient_id": 263, "name": "Theobromine", "group": "Other", "unit": "mg"}, -{"nutrient_id": 269, "name": "Sugars, total", "group": "Proximates", "unit": "g", "nickname":"Sugar"}, -{"nutrient_id": 291, "name": "Fiber, total dietary", "group": "Proximates", "unit": "g", "nickname":"Fiber"}, - -{"nutrient_id": 301, "name": "Calcium, Ca", "group": "Minerals", "unit": "mg", "nickname":"Calcium"}, -{"nutrient_id": 303, "name": "Iron, Fe", "group": "Minerals", "unit": "mg", "nickname":"Iron"}, -{"nutrient_id": 304, "name": "Magnesium, Mg", "group": "Minerals", "unit": "mg", "nickname":"Magnesium"}, -{"nutrient_id": 305, "name": "Phosphorus, P", "group": "Minerals", "unit": "mg", "nickname":"Phosphorus"}, -{"nutrient_id": 306, "name": "Potassium, K", "group": "Minerals", "unit": "mg", "nickname":"Potassium"}, -{"nutrient_id": 307, "name": "Sodium, Na", "group": "Minerals", "unit": "mg", "nickname":"Sodium"}, -{"nutrient_id": 309, "name": "Zinc, Zn", "group": "Minerals", "unit": "mg", "nickname":"Zinc"}, -{"nutrient_id": 312, "name": "Copper, Cu", "group": "Minerals", "unit": "mg", "nickname":"Copper"}, -{"nutrient_id": 313, "name": "Fluoride, F", "group": "Minerals", "unit": "µg", "nickname":"Fluoride"}, -{"nutrient_id": 315, "name": "Manganese, Mn", "group": "Minerals", "unit": "mg", "nickname":"Manganese"}, -{"nutrient_id": 317, "name": "Selenium, Se", "group": "Minerals", "unit": "µg", "nickname":"Selenium"}, - -{"nutrient_id": 318, "name": "Vitamin A, IU", "group": "Vitamins", "unit": "IU", "nickname":"Vitamin A"}, -{"nutrient_id": 323, "name": "Vitamin E (alpha-tocopherol)", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin E"}, -{"nutrient_id": 324, "name": "Vitamin D", "group": "Vitamins", "unit": "IU"}, -{"nutrient_id": 401, "name": "Vitamin C, total ascorbic acid", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin C"}, -{"nutrient_id": 404, "name": "Thiamin", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin B-1"}, -{"nutrient_id": 405, "name": "Riboflavin", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin B-2"}, -{"nutrient_id": 406, "name": "Niacin", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin B-3"}, -{"nutrient_id": 410, "name": "Pantothenic acid", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin B-5"}, -{"nutrient_id": 415, "name": "Vitamin B-6", "group": "Vitamins", "unit": "mg"}, -{"nutrient_id": 417, "name": "Folate, total", "group": "Vitamins", "unit": "µg", "nickname":"Vitamin B-9"}, -{"nutrient_id": 418, "name": "Vitamin B-12", "group": "Vitamins", "unit": "µg"}, -{"nutrient_id": 421, "name": "Choline, total", "group": "Vitamins", "unit": "mg", "nickname":"Choline"}, -{"nutrient_id": 430, "name": "Vitamin K (phylloquinone)", "group": "Vitamins", "unit": "µg", "nickname":"Vitamin K"}, - -{"nutrient_id": 601, "name": "Cholesterol", "group": "Lipids", "unit": "mg"}, -{"nutrient_id": 605, "name": "Fatty acids, total trans", "group": "Lipids", "unit": "g", "nickname":"Trans Fat"}, -{"nutrient_id": 606, "name": "Fatty acids, total saturated", "group": "Lipids", "unit": "g", "nickname":"Saturated Fat"}, -{"nutrient_id": 621, "name": "22:6 n-3 (DHA)", "group": "Lipids", "unit": "g", "nickname":"DHA"}, -{"nutrient_id": 629, "name": "20:5 n-3 (EPA)", "group": "Lipids", "unit": "g", "nickname":"EPA"}, -{"nutrient_id": 645, "name": "Fatty acids, total monounsaturated", "group": "Lipids", "unit": "g", "nickname":"Monounsaturated Fat"}, -{"nutrient_id": 646, "name": "Fatty acids, total polyunsaturated", "group": "Lipids", "unit": "g", "nickname":"Polyunsaturated Fat"}, -{"nutrient_id": 851, "name": "18:3 n-3 c,c,c (ALA)", "group": "Lipids", "unit": "g", "nickname":"ALA"} +[ +{"nutrient_id": 203, "name": "Protein", "group": "Proximates", "unit": "g"}, +{"nutrient_id": 204, "name": "Total lipid (fat)", "group": "Proximates", "unit": "g", "nickname":"Fat"}, +{"nutrient_id": 205, "name": "Carbohydrate, by difference", "group": "Proximates", "unit": "g", "nickname":"Carbs"}, +{"nutrient_id": 208, "name": "Energy", "group": "Proximates", "unit": "kcal", "nickname":"Calories"}, +{"nutrient_id": 255, "name": "Water", "group": "Proximates", "unit": "g"}, +{"nutrient_id": 262, "name": "Caffeine", "group": "Other", "unit": "mg"}, +{"nutrient_id": 263, "name": "Theobromine", "group": "Other", "unit": "mg"}, +{"nutrient_id": 269, "name": "Sugars, total", "group": "Proximates", "unit": "g", "nickname":"Sugar"}, +{"nutrient_id": 291, "name": "Fiber, total dietary", "group": "Proximates", "unit": "g", "nickname":"Fiber"}, + +{"nutrient_id": 301, "name": "Calcium, Ca", "group": "Minerals", "unit": "mg", "nickname":"Calcium"}, +{"nutrient_id": 303, "name": "Iron, Fe", "group": "Minerals", "unit": "mg", "nickname":"Iron"}, +{"nutrient_id": 304, "name": "Magnesium, Mg", "group": "Minerals", "unit": "mg", "nickname":"Magnesium"}, +{"nutrient_id": 305, "name": "Phosphorus, P", "group": "Minerals", "unit": "mg", "nickname":"Phosphorus"}, +{"nutrient_id": 306, "name": "Potassium, K", "group": "Minerals", "unit": "mg", "nickname":"Potassium"}, +{"nutrient_id": 307, "name": "Sodium, Na", "group": "Minerals", "unit": "mg", "nickname":"Sodium"}, +{"nutrient_id": 309, "name": "Zinc, Zn", "group": "Minerals", "unit": "mg", "nickname":"Zinc"}, +{"nutrient_id": 312, "name": "Copper, Cu", "group": "Minerals", "unit": "mg", "nickname":"Copper"}, +{"nutrient_id": 313, "name": "Fluoride, F", "group": "Minerals", "unit": "µg", "nickname":"Fluoride"}, +{"nutrient_id": 315, "name": "Manganese, Mn", "group": "Minerals", "unit": "mg", "nickname":"Manganese"}, +{"nutrient_id": 317, "name": "Selenium, Se", "group": "Minerals", "unit": "µg", "nickname":"Selenium"}, + +{"nutrient_id": 318, "name": "Vitamin A, IU", "group": "Vitamins", "unit": "IU", "nickname":"Vitamin A"}, +{"nutrient_id": 323, "name": "Vitamin E (alpha-tocopherol)", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin E"}, +{"nutrient_id": 324, "name": "Vitamin D", "group": "Vitamins", "unit": "IU"}, +{"nutrient_id": 401, "name": "Vitamin C, total ascorbic acid", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin C"}, +{"nutrient_id": 404, "name": "Thiamin", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin B-1"}, +{"nutrient_id": 405, "name": "Riboflavin", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin B-2"}, +{"nutrient_id": 406, "name": "Niacin", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin B-3"}, +{"nutrient_id": 410, "name": "Pantothenic acid", "group": "Vitamins", "unit": "mg", "nickname":"Vitamin B-5"}, +{"nutrient_id": 415, "name": "Vitamin B-6", "group": "Vitamins", "unit": "mg"}, +{"nutrient_id": 417, "name": "Folate, total", "group": "Vitamins", "unit": "µg", "nickname":"Vitamin B-9"}, +{"nutrient_id": 418, "name": "Vitamin B-12", "group": "Vitamins", "unit": "µg"}, +{"nutrient_id": 421, "name": "Choline, total", "group": "Vitamins", "unit": "mg", "nickname":"Choline"}, +{"nutrient_id": 430, "name": "Vitamin K (phylloquinone)", "group": "Vitamins", "unit": "µg", "nickname":"Vitamin K"}, + +{"nutrient_id": 601, "name": "Cholesterol", "group": "Lipids", "unit": "mg"}, +{"nutrient_id": 605, "name": "Fatty acids, total trans", "group": "Lipids", "unit": "g", "nickname":"Trans Fat"}, +{"nutrient_id": 606, "name": "Fatty acids, total saturated", "group": "Lipids", "unit": "g", "nickname":"Saturated Fat"}, +{"nutrient_id": 621, "name": "22:6 n-3 (DHA)", "group": "Lipids", "unit": "g", "nickname":"DHA"}, +{"nutrient_id": 629, "name": "20:5 n-3 (EPA)", "group": "Lipids", "unit": "g", "nickname":"EPA"}, +{"nutrient_id": 645, "name": "Fatty acids, total monounsaturated", "group": "Lipids", "unit": "g", "nickname":"Monounsaturated Fat"}, +{"nutrient_id": 646, "name": "Fatty acids, total polyunsaturated", "group": "Lipids", "unit": "g", "nickname":"Polyunsaturated Fat"}, +{"nutrient_id": 851, "name": "18:3 n-3 c,c,c (ALA)", "group": "Lipids", "unit": "g", "nickname":"ALA"} ] \ No newline at end of file diff --git a/noms/report.py b/report.py similarity index 86% rename from noms/report.py rename to report.py index de7a671..e274554 100644 --- a/noms/report.py +++ b/report.py @@ -1,53 +1,54 @@ -import csv -import sys -from .objects.nutrient_dict import * - -def report(meal): - report = [] - for i in range(0, len(meal.nutrients)): - name = meal.nutrients[i]["name"] - rda = nutrient_dict[i]["rda"] - limit = nutrient_dict[i]["limit"] - value = meal.nutrients[i]["value"] - unit = meal.nutrients[i]["unit"] - state = "" - if rda == None: - rda = 0 - if limit == None: - limit = sys.maxsize - if value < rda: - state = "deficient" - elif value > limit: - state = "excessive" - else: - state = "satisfactory" - if limit == sys.maxsize: - limit = None - report.append({"name":name, "rda":rda, "limit":limit, "value":value, "state":state, "unit":unit}) - return report - -def export_report(meal, path): - with open(path, "w",newline='') as csvfile: - writer = csv.writer(csvfile, delimiter=',') - # Write profile information - writer.writerow(['TDEE',None,tdee]) - writer.writerow(['Carb Ratio',carb_p,carb_p*tdee]) - writer.writerow(['Protein Ratio',protein_p,protein_p*tdee]) - writer.writerow(['Fat Ratio',fat_p,fat_p*tdee]) - writer.writerow(['']) - # Write nutritional information - writer.writerow(['Nutrient', 'RDA', 'Limit']) - for i in range(0, len(meal.nutrients)): - name = meal.nutrients[i]["name"] - row = [name] - if nutrient_dict[i]["rda"] == None: - row.append("None") - else: - row.append(nutrient_dict[i]["rda"]) - if nutrient_dict[i]["limit"] == None: - row.append("None") - else: - row.append(nutrient_dict[i]["limit"]) - row.append(meal.nutrients[i]["value"]) - i += 1 - writer.writerow(row) \ No newline at end of file +import csv +import sys +from .objects.nutrient_dict import nutrient_dict, index_from_name + +def report(meal): + report = [] + _nutrient_dict = nutrient_dict()() + for i in range(0, len(meal.nutrients)): + name = meal.nutrients[i]["name"] + rda = _nutrient_dict[i]["rda"] + limit = _nutrient_dict[i]["limit"] + value = meal.nutrients[i]["value"] + unit = meal.nutrients[i]["unit"] + state = "" + if rda == None: + rda = 0 + if limit == None: + limit = sys.maxsize + if value < rda: + state = "deficient" + elif value > limit: + state = "excessive" + else: + state = "satisfactory" + if limit == sys.maxsize: + limit = None + report.append({"name":name, "rda":rda, "limit":limit, "value":value, "state":state, "unit":unit}) + return report + +def export_report(meal, path): + with open(path, "w",newline='') as csvfile: + writer = csv.writer(csvfile, delimiter=',') + # Write profile information + writer.writerow(['TDEE',None,tdee]) + writer.writerow(['Carb Ratio',carb_p,carb_p*tdee]) + writer.writerow(['Protein Ratio',protein_p,protein_p*tdee]) + writer.writerow(['Fat Ratio',fat_p,fat_p*tdee]) + writer.writerow(['']) + # Write nutritional information + writer.writerow(['Nutrient', 'RDA', 'Limit']) + for i in range(0, len(meal.nutrients)): + name = meal.nutrients[i]["name"] + row = [name] + if nutrient_dict[i]["rda"] == None: + row.append("None") + else: + row.append(nutrient_dict[i]["rda"]) + if nutrient_dict[i]["limit"] == None: + row.append("None") + else: + row.append(nutrient_dict[i]["limit"]) + row.append(meal.nutrients[i]["value"]) + i += 1 + writer.writerow(row) diff --git a/search.py b/search.py new file mode 100644 index 0000000..682601b --- /dev/null +++ b/search.py @@ -0,0 +1,29 @@ +from .client.main import Client +from .client.dict_parse import search_parse +import operator + +def get_results(query, client): + search_obj = client.search_query(query) + search_obj = search_parse(search_obj) + return search_obj + +def print_results(search_obj, max_entries=None): + if search_obj == None: + print("There are no search results for this query") + else: + print("="*112) + print("Search results for \'{}\' on USDA Standard Reference Database".format(search_obj["search_term"])) + print("="*112) + if max_entries == None: + max_entries = len(search_obj["items"]) + if max_entries < len(search_obj["items"]): + search_obj["items"] = search_obj["items"][:max_entries] + search_obj["items"].sort(key=operator.itemgetter("group")) + print("{name:<72} {group:^30} {id:>8}".format(name="Name",group="Group",id="ID")) + for item in search_obj["items"]: + if len(item["name"]) > 70: + item["name"] = item["name"][:70] + ".." + if len(item["group"]) > 28: + item["group"] = item["group"][:28] + ".." + print("{name:<72} {group:^30} {id:>8}".format(name=item["name"],group=item["group"],id=item["ndbno"])) + print("="*112) \ No newline at end of file diff --git a/service/__init__.py b/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/food.py b/service/food.py new file mode 100644 index 0000000..2c6635e --- /dev/null +++ b/service/food.py @@ -0,0 +1,13 @@ +from ..client.main import Client +from ..client.dict_parse import food_parse +from ..objects.nutrient_dict import nutrient_dict +from ..objects.food import Meal + +def foods(id_value_dict, client): + food_obj = client.food_query(id_value_dict.keys()) + # Return list of food objects in context of nutrients being tracked + food_obj = food_parse(food_obj, nutrient_dict, list(id_value_dict.values())) + return food_obj + +def meal(foods): + return Meal(foods) \ No newline at end of file diff --git a/service/report.py b/service/report.py new file mode 100644 index 0000000..e8a4716 --- /dev/null +++ b/service/report.py @@ -0,0 +1,52 @@ +import csv +import sys +from ..objects.nutrient_dict import * + +def report(meal): + report = [] + for i in range(0, len(meal.nutrients)): + name = meal.nutrients[i]["name"] + rda = nutrient_dict[i]["rda"] + limit = nutrient_dict[i]["limit"] + value = meal.nutrients[i]["value"] + state = "" + if rda == None: + rda = 0 + if limit == None: + limit = sys.maxsize + if value < rda: + state = "deficient" + elif value > limit: + state = "excessive" + else: + state = "satisfactory" + if limit == sys.maxsize: + limit = None + report.append({"name":name, "rda":rda, "limit":limit, "value":value, "state":state}) + return report + +def export_report(meal, path): + csvfile = open(path, "w",newline='') + writer = csv.writer(csvfile, delimiter=',') + # Write profile information + writer.writerow(['TDEE',None,tdee]) + writer.writerow(['Carb Ratio',carb_p,carb_p*tdee]) + writer.writerow(['Protein Ratio',protein_p,protein_p*tdee]) + writer.writerow(['Fat Ratio',fat_p,fat_p*tdee]) + writer.writerow(['']) + # Write nutritional information + writer.writerow(['Nutrient', 'RDA', 'Limit']) + for i in range(0, len(meal.nutrients)): + name = meal.nutrients[i]["name"] + row = [name] + if nutrient_dict[i]["rda"] == None: + row.append("None") + else: + row.append(nutrient_dict[i]["rda"]) + if nutrient_dict[i]["limit"] == None: + row.append("None") + else: + row.append(nutrient_dict[i]["limit"]) + row.append(meal.nutrients[i]["value"]) + i += 1 + writer.writerow(row) diff --git a/service/search.py b/service/search.py new file mode 100644 index 0000000..9ad4fe1 --- /dev/null +++ b/service/search.py @@ -0,0 +1,26 @@ +from ..client.main import Client +from ..client.dict_parse import search_parse +import operator + +def get_results(query, client): + search_obj = client.search_query(query) + search_obj = search_parse(search_obj) + return search_obj + +def print_results(search_obj, max_entries=None): + print("="*112) + print("Search results for \'{}\' on USDA Standard Reference Database".format(search_obj["search_term"])) + print("="*112) + if max_entries == None: + max_entries = len(search_obj["items"]) + if max_entries < len(search_obj["items"]): + search_obj["items"] = search_obj["items"][:max_entries] + search_obj["items"].sort(key=operator.itemgetter("group")) + print("{name:<72} {group:^30} {id:>8}".format(name="Name",group="Group",id="ID")) + for item in search_obj["items"]: + if len(item["name"]) > 70: + item["name"] = item["name"][:70] + ".." + if len(item["group"]) > 28: + item["group"] = item["group"][:28] + ".." + print("{name:<72} {group:^30} {id:>8}".format(name=item["name"],group=item["group"],id=item["ndbno"])) + print("="*112) \ No newline at end of file diff --git a/test.py b/test.py index faebe9d..8f24070 100644 --- a/test.py +++ b/test.py @@ -14,45 +14,43 @@ def _search(): assert "items" in broc_search.json.keys() assert len(broc_search.json["items"]) > 5 uni_search = client.search_query("Unicorn meat") - assert uni_search == None + assert uni_search.json == None def _foods(): client = _client() - food_list = client.get_foods({'11090':100, '20041':500, '09120319':100}) - assert len(food_list) == 2 food_list = client.get_foods({ - '01001':20, - '01132':100, - '09037':80, - '15076':150, - '09201':140, - '14278':300, - '12006':20, - '20041':150, - '16057':50, - '11233':50, - '19904':10, - '14400':1000 # literally an entire liter of coke + '173410':20, # 01001':20, + '1100335':100, #'01132':100, + '1103883': 80, #'09037':80, + '175167':150, #15076':150, + '1102597':140, #'09201':140, + '1104292':300, #'14278':300, + '1100612':20, #'12006':20, + '1101628':150,#'20041':150, + '1100429':50, #'16057':50, + '1103116':50, #'11233':50, + '1104032':10,#'19904':10, + '1104331':1000 #14400':1000 # literally an entire liter of coke }) - assert len(food_list) == 11 + assert len(food_list) == 12 assert type(food_list[0]) == noms.Food - assert "name" in food_list[0].desc.keys() + assert type(food_list[0].desc) == type('') def _meal(): client = _client() food_list = client.get_foods({ - '01001':20, - '01132':100, - '09037':80, - '15076':150, - '09201':140, - '14278':300, - '12006':20, - '20041':150, - '16057':50, - '11233':50, - '19904':10, - '14400':1000 # literally an entire liter of coke + '173410':20, # 01001':20, + '1100335':100, #'01132':100, + '1103883': 80, #'09037':80, + '175167':150, #15076':150, + '1102597':140, #'09201':140, + '1104292':300, #'14278':300, + '1100612':20, #'12006':20, + '1101628':150,#'20041':150, + '1100429':50, #'16057':50, + '1103116':50, #'11233':50, + '1104032':10,#'19904':10, + '1104331':1000 #14400':1000 # literally an entire liter of coke }) meal = noms.Meal(food_list) assert type(meal) == noms.Meal @@ -61,90 +59,81 @@ def _meal(): def _report(meal): r = noms.report(meal) - assert len(r) == len(noms.nutrient_dict) + assert len(r) == len(noms.nutrient_dict()()) def _sort(meal): m = copy.deepcopy(meal) m.sort_by_top("Sugar") - assert m.foods[0].desc["ndbno"] == '14400' # the most sugar-dense food in the meal is coke + assert m.foods[0].data['fdcId'] == 1104331 # the most sugar-dense food in the meal is coke def _pantry(): client = _client() pantry_items = { - # DAIRY AND EGG - "01001":100, # butter, salted - "01145":100, # butter, without salt - "01079":100, # 2% milk - "01077":100, # milk, whole - "01086":100, # skim milk - "01132":100, # scrambled eggs - "01129":100, # hard boiled eggs - "01128":100, # fried egg - # MEAT - "15076":100, # atlantic salmon - "07935":100, # chicken breast oven-roasted - "13647":100, # steak - "05192":100, # turkey - # FRUIT - "09037":100, # avocado - "09316":100, # strawberries - "09050":100, # blueberry - "09302":100, # raspberry - "09500":100, # red delicious apple - "09040":100, # banana - "09150":100, # lemon - "09201":100, # oranges - "09132":100, # grapes - # PROCESSED - "21250":100, # hamburger - "21272":100, # pizza - "19088":100, # ice cream - "18249":100, # donut - # DRINK - "14400":100, # coke - "14429":100, # tap water - "14433":100, # bottled water - "09206":100, # orange juice - "14278":100, # brewed green tea - "14209":100, # coffee brewed with tap water - # (milk is included in dairy group) - # GRAIN - "12006":100, # chia - "12220":100, # flaxseed - "20137":100, # quinoa, cooked - "20006":100, # pearled barley - "20051":100, # white rice enriched cooked - "20041":100, # brown rice cooked - "12151":100, # pistachio - "19047":100, # pretzel - "12061":100, # almond - # LEGUME - "16057":100, # chickpeas - "16015":100, # black beans - "16043":100, # pinto beans - "16072":100, # lima beans - "16167":100, # peanut butter smooth - # VEGETABLE - "11124":100, # raw carrots - "11090":100, # broccoli - "11457":100, # spinach, raw - "11357":100, # baked potato - "11508":100, # baked sweet potato - "11530":100, # tomato, red, cooked - "11253":100, # lettuce - "11233":100, # kale - "11313":100, # peas - "11215":100, # garlic - # OTHER - "04053":100, # olive oil - "19904":100, # dark chocolate - "11238":100, # shiitake mushrooms - "19165":100, # cocoa powder - } + "1097517":100, # 2% milk + "1097878":100, # cocoa powder + "1100383":100, # lima beans + "1100549":100, # pistachio + "1100918":100, # ice cream + "1102594":100, # lemon + "1102710":100, # strawberries + "1102880":100, # baked potato + "1103193":100, # raw carrots + "1103261":100, # baked sweet potato + "1103645":100, # peas + "1103845":100, # garlic + "1103861":100, # olive oil + "1103883":100, # avocado + "1104493":100, # bottled water + "1105430":100, # red delicious apple + "168436":100, # shiitake mushrooms + "168917":100, # quinoa, cooked + "171890":100, # coffee brewed with tap water + "173410":100, # butter, salted + "173423":100, # fried egg + "173430":100, # butter, without salt + "174608":100, # chicken breast oven-roasted + "1097512":100, # milk, whole + "1097521":100, # skim milk + "1099608":100, # steak + "1099796":100, # hamburger + "1099888":100, # turkey + "1100335":100, # scrambled eggs + "1100393":100, # pinto beans + "1100410":100, # black beans + "1100429":100, # chickpeas + "1100555":100, # almond + "1100612":100, # chia + "1101112":100, # pizza + "1101628":100, # brown rice cooked + "1102597":100, # oranges + "1102702":100, # blueberry + "1102708":100, # raspberry + "1103116":100, # kale + "1103136":100, # spinach, raw + "1103183":100, # broccoli + "1103358":100, # lettuce + "1103860":100, # flaxseed + "1104032":100, # dark chocolate + "1104292":100, # brewed green tea + "1104331":100, # coke + "1104492":100, # tap water + "168880":100, # white rice enriched cooked + "170050":100, # tomato, red, cooked + "170285":100, # pearled barley + "171354":100, # orange juice + "171370":100, # pretzel + "173424":100, # hard boiled eggs + "173945":100, # banana + "174993":100, # donut + "175167":100, # atlantic salmon + "324860":100 # peanut butter smooth + } pantry_food = client.get_foods(pantry_items) + #import pickle + #pantry_food =pickle.load(open('pantry_foods_data.pkl', 'rb')) pantry = noms.Meal(pantry_food) assert type(pantry) == noms.Meal - assert pantry.nutrients[0]["name"] == noms.nutrient_dict[0]["name"] + assert pantry.nutrients[0]["name"] == noms.nutrient_dict()()[0]["name"] return pantry def _gen_recommendations(meal, pantry, verbose=False): @@ -158,18 +147,30 @@ def _gen_recommendations(meal, pantry, verbose=False): def _remove_recommendation(meal): result = noms.recommend_removal(meal, noms.nutrient_dict) # check that we are recommending the user not to have a liter of coke - assert meal.foods[result].desc["ndbno"] == "14400" + #assert meal.foods[result].desc["ndbno"] == "14400" + assert meal.foods[result].data['fdcId'] == 1104331 def test(): + print('testing noms.client .....') _client() + print('testing search ......') _search() + print('testing a list of foods .....') _foods() + print('testing meal object ....') meal = _meal() + #import pickle + #meal = pickle.load(open('mytestmeal.pkl', 'rb')) + print('testing reporting .....') _report(meal) + print('testing sorting .....') _sort(meal) + print('creating pantry .....') pantry = _pantry() + print('generating recommendation .....') _gen_recommendations(meal, pantry.foods) + print('removing recommendation ......') _remove_recommendation(meal) if __name__ == "__main__": - test() \ No newline at end of file + test()