Project2

Spoonacular Food API Vignette

Nicole Levin, Shaoyu Wang 2022-10-12

Introduction

This document is a vignette that describes pulling data from an API and performing some exploratory data analysis based on those results. This vignette will utilize the Spoonacular food API. More information about this API can be found here. In this vignette, we will be building some functions to interact with the API and doing some exploratory data analysis based on some example search results.

Required Packages

The following packages are required for interacting with the functions in this vignette:

First, we will load the required packages

#Load libraries
library(httr)
library(dplyr)
library(jsonlite)
library(tidyverse)

Building Search Functions

There are three functions that have been created to allow the user to make a search from the API.

The first is called ingredient_search and it allows the user to select an ingredient and a number of results to return. The function will then provide an output data frame with many points of information for each of the recipes. The function works by first searching for recipes with the specified ingredient. The unique recipe ID numbers that the API uses for identifying these recipes are then plugged into a second search to get a combination of quantitative and categorical data including health scores, aggregate likes, whether a dish is vegetarian, whether it is gluten free, and a few others. The function takes two arguments, which are:

  1. ingredient - name of the ingredient
  2. number - maximum number of results to return

Full search code is as follows:

#User options for search. Can input: (1)ingredient for search, (2)Number of results
ingredient_search <- function(ingredient, number=10){
  ingredient <- tolower(ingredient)
  search_url <- "https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com/recipes/findByIngredients"
  queryString <- list(
  ingredients = ingredient,
  number = number)
search_result <- VERB("GET", search_url, add_headers('X-RapidAPI-Key' = '73a2057416msh633772bf8275edfp1b3775jsn7e44cc5cb98d', 'X-RapidAPI-Host' = 'spoonacular-recipe-food-nutrition-v1.p.rapidapi.com'), query = queryString, content_type("application/octet-stream"))
  parsed_search_result <- fromJSON(rawToChar(search_result$content))
  id_list <- paste(as.character(parsed_search_result$id), collapse=",")
bulk_url <- "https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com/recipes/informationBulk"
queryString <- list(ids = id_list)
bulk_search <- VERB("GET", bulk_url, add_headers('X-RapidAPI-Key' = '73a2057416msh633772bf8275edfp1b3775jsn7e44cc5cb98d', 'X-RapidAPI-Host' = 'spoonacular-recipe-food-nutrition-v1.p.rapidapi.com'), query = queryString, content_type("application/octet-stream"))
  parsed_bulk_search <- fromJSON(rawToChar(bulk_search$content))
  results_df <- as_tibble(parsed_bulk_search) %>% select(id, title, glutenFree, dairyFree, vegetarian, aggregateLikes, healthScore, weightWatcherSmartPoints, sourceName, pricePerServing, readyInMinutes)
  combined_results <- results_df %>% mutate(WW_category = if_else(weightWatcherSmartPoints <10, 1, if_else(weightWatcherSmartPoints <20, 2, 3)))
  combined_results$WW_category = factor(combined_results$WW_category, levels = c(1, 2, 3), labels = c("low", "medium", "high"))
return(combined_results)
}
#Run a test
ingredient_test <- ingredient_search(ingredient="cherry", number=50)
ingredient_test
## # A tibble: 50 × 12
##         id title         glutenFree dairyFree vegetarian aggregateLikes healthScore weightWatcherSm…
##      <int> <chr>         <lgl>      <lgl>     <lgl>               <int>       <int>            <int>
##  1  799316 Black Cherry… TRUE       TRUE      FALSE                  10           0                6
##  2   50161 Jell-o Mold   TRUE       FALSE     TRUE                    1           2                9
##  3  779475 Black Cherry… TRUE       TRUE      TRUE                  167           0                2
##  4   50105 Cherry Mojit… TRUE       TRUE      TRUE                   37           1                5
##  5   52755 Yellow Cherr… TRUE       TRUE      TRUE                   26           2               31
##  6 1065961 Cherry Prese… TRUE       TRUE      TRUE                    1           7               11
##  7  405122 Bing Cherry … TRUE       FALSE     TRUE                    0           2                6
##  8  525223 Dark Chocola… FALSE      TRUE      FALSE               26141           3               30
##  9  524954 Root Beer Fl… TRUE       TRUE      FALSE                2592           1                2
## 10  539027 Quick and Ea… TRUE       FALSE     TRUE                 1491           5               10
## # … with 40 more rows, and 4 more variables: sourceName <chr>, pricePerServing <dbl>,
## #   readyInMinutes <int>, WW_category <fct>

The example search results are for the ingredient “cherry with a maximum number of results of 30. As the output shows, we can get a lot of information from a simple search.

The second function is called calorie_search and it allows the user to select a minimum number of calories, maximum number of calories, whether they want random results or ordered, and what number of results they want. This function has default values for all variables, so they do not all have to be specified for the search to execute. It works in a similar fashion, where the initial query gets the list of ID numbers to be used in a second query to get more data. In this case though, the initial nutrition search based on calories gives us some output variables like number of calories, grams of carbohydrates, grams of protein, and grams of fat that we can bring into the final data set too. This function takes in four arguments, which are, in order:

  1. calories_from: the minimum number of calories
  2. calories_to: the maximum number of calories
  3. number - the maximum number of results to return

The full code of the function is as follows:

#This function is a search by calories. User options are (1)Minimum calories, (2)Maximum calories, (3)Whether we want a random set of results or the first ones alphabetically, and a number of results. All of these have defaults, so the user can specify nothing and still get a result.
calorie_search <- function(calories_from=0, calories_to=5000, number=10){
  calories_from <- as.character(calories_from)
  calories_to <- as.character(calories_to)
  number <- as.character(number)
  search_url <- "https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com/recipes/findByNutrients"
    queryString <- list(minCalories = calories_from, maxCalories = calories_to, number=number)
  search_result <- VERB("GET", search_url, add_headers('X-RapidAPI-Key' = '73a2057416msh633772bf8275edfp1b3775jsn7e44cc5cb98d', 'X-RapidAPI-Host' = 'spoonacular-recipe-food-nutrition-v1.p.rapidapi.com'), query = queryString, content_type("application/octet-stream"))
  parsed_search_result <- fromJSON(rawToChar(search_result$content))
  id_list <- paste(as.character(parsed_search_result$id), collapse=",")
  bulk_url <- "https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com/recipes/informationBulk"
queryString <- list(ids = id_list)
bulk_search <- VERB("GET", bulk_url, add_headers('X-RapidAPI-Key' = '73a2057416msh633772bf8275edfp1b3775jsn7e44cc5cb98d', 'X-RapidAPI-Host' = 'spoonacular-recipe-food-nutrition-v1.p.rapidapi.com'), query = queryString, content_type("application/octet-stream"))
  parsed_bulk_search <- fromJSON(rawToChar(bulk_search$content))
  results_df <- as_tibble(parsed_bulk_search) %>% select(id, title, glutenFree, dairyFree, vegetarian, aggregateLikes, healthScore, weightWatcherSmartPoints, sourceName, pricePerServing, readyInMinutes)
  results_df <- results_df %>% mutate(WW_category = if_else(weightWatcherSmartPoints <10, 1, if_else(weightWatcherSmartPoints <20, 2, 3))) 
  results_df$WW_category = factor(results_df$WW_category, levels = c(1, 2, 3), labels = c("low", "medium", "high"))
  combined_results <- as_tibble(c(results_df, parsed_search_result[,5:8]))
  return(combined_results)
}

#Run a test
calorie_test <- calorie_search(calories_from= 100, calories_to = 500, number=50)
calorie_test
## # A tibble: 50 × 16
##        id title          glutenFree dairyFree vegetarian aggregateLikes healthScore weightWatcherSm…
##     <int> <chr>          <lgl>      <lgl>     <lgl>               <int>       <int>            <int>
##  1  17281 Spicy Tuna Ta… TRUE       TRUE      FALSE                   5          71               10
##  2  30338 Spanish-Style… TRUE       TRUE      FALSE                   0         100                2
##  3  51843 Lemon-blueber… FALSE      FALSE     TRUE                   25           2               11
##  4  58965 Mango & Raspb… FALSE      FALSE     TRUE                   30           1               19
##  5  66226 Grapefruit Po… FALSE      FALSE     TRUE                    0           0               12
##  6  70450 Apple Pie Flo… TRUE       FALSE     FALSE                 668           1               19
##  7  90629 Baked Apples … TRUE       FALSE     TRUE                    0           0                3
##  8  94853 Cantaloupe an… TRUE       FALSE     TRUE                    0          22                2
##  9 105012 Grilled Zucch… TRUE       FALSE     FALSE                   1          11                8
## 10 109236 Baby Artichok… TRUE       TRUE      FALSE                   0           5                5
## # … with 40 more rows, and 8 more variables: sourceName <chr>, pricePerServing <dbl>,
## #   readyInMinutes <int>, WW_category <fct>, calories <int>, protein <chr>, fat <chr>, carbs <chr>

The ingredient_search and calorie_search functions above output a variable of “WW_category” This variable did not exist in the source data and had to be created using the Weight Watchers Smart Points values. The categories utilized are:

  1. Low: less than 10
  2. Medium: 10 to less than 20
  3. High: 20 or greater

“if_else” statements are used to create the new variable based on this categorization.

search_by_2_nutrs

The third function is called search_by_2_nutrs, and it allows the user to search by two nutrients, minimum and maximum levels for both nutrients, and a maximum number of results to return. The nutrient options are grams of carbohydrates, grams of protein, and grams of fat. This function takes in seven arguments, which are, in order:

  1. first nutrient - carbohydrates, protein, or fat
  2. first_from - minimum for first nutrient (in grams)
  3. first_to - minimum for first nutrient (in grams)
  4. sec_nutrient - grams of carbohydrates, grams of protein, or grams of fat
  5. sec_from - minimum for second nutrient (in grams)
  6. sec_to - maximmum for second nutrient (in grams)
  7. number maximum number of results to return

For example, if the user likes fitness, this function can use to search the nutrients with low calories but high protein. The full code of the function is as follows:

#This function searches based on minimum and maximum levels of two nutrients.
search_by_2_nutrs <- function(first_nutrient, first_from=0, first_to=5000, sec_nutrient, sec_from=0, sec_to=5000, number=10){
  base<-"https://api.spoonacular.com/recipes/findByNutrients"
  call2 <- paste(base,"?min",first_nutrient,"=",first_from,"&max",first_nutrient,"=", first_to,
                 "&min",sec_nutrient,"=",sec_from,"&max",sec_nutrient,"=", sec_to,
                 "&number=",number,"&apiKey=90c7af330c5f45a4a1b709068daed452",sep="")
  get_2_nutrs <- GET(call2)
  get_2_nutrs_text <- content(get_2_nutrs, "text")
  get_2_nutrs_json <- fromJSON(get_2_nutrs_text, flatten = TRUE)
  get_2_nutrs_tb <- as_tibble(get_2_nutrs_json) %>%
    select("id","title","calories","protein","fat","carbs")
  return(get_2_nutrs_tb)
}

Exploratory Data Analysis

Now that we have set up our functions and run some test searches, we can do some exploratory data analysis of the results. First, let’s look at some contingency tables and bar plots of Weight Watcher points categories, grouped by vegetarian to see if vegetarian results tend to be lower point values.

#Create contingency table from calorie test results
table(calorie_test$WW_category, calorie_test$vegetarian, dnn=c("WW Category", "vegetarian"))
##            vegetarian
## WW Category FALSE TRUE
##      low       23   12
##      medium     8    6
##      high       1    0
#Create contingency table for ingredient test results
table(ingredient_test$WW_category, ingredient_test$vegetarian, dnn=c("WW Category", "vegetarian"))
##            vegetarian
## WW Category FALSE TRUE
##      low       15   23
##      medium     3    4
##      high       2    3
#Plot my tests as bar plots of Weight Watchers category grouped by vegetarian.
g <- ggplot(calorie_test, aes(x=WW_category))
g + geom_bar(aes(fill=vegetarian), position="dodge") +
  labs(title="Bar plot of WW Points for 100-500 Calorie Search", x="WW Smart Points Category")

g <- ggplot(ingredient_test, aes(x=WW_category))
g + geom_bar(aes(fill=vegetarian), position="dodge") +
  labs(title="Bar plot of WW Points for Ingredient=Cherry", x="WW Smart Points Category")

The most noticeable result here is that we get many more vegetarian results with our fruit ingredient choice vs. a calorie-based search.

Next, let’s see if anything seems to trend with aggregate likes. Possibly results with higher calories tend to get more likes since they would tend to have more sugar or fat to make them taste good. Possibly higher health scores tend to get more likes because of their perceived health halo.

#Create scatterplots with aggregate likes as the response variable.
g <- ggplot(calorie_test, aes(x=calories, y=aggregateLikes))
g + geom_point() +
  labs(title="Aggregate Likes vs Calories from 100-500 Calorie Search Results", x="Calories", y="Aggregate Likes")

g <- ggplot(ingredient_test, aes(x=healthScore, y=aggregateLikes))
g + geom_point() + 
  labs(title="Aggregate Likes vs Calories from Cherry Ingredient Search Results", x="Health Score", y="Aggregate Likes")

The most notable result in these plots is that not very many of these recipes get very many likes. There are a few clear outliers with many more likes than the rest.

We are now interested in looking at whether there is a difference in the spread of health score results for vegetarian and non-vegetarian recipes utilizing some box plots.

#Next box plot of calories or health score broken out by vegetarian and not
g <- ggplot(calorie_test, aes(x=vegetarian, y=calories))
g + geom_boxplot()+
  labs(title =  "Boxplot of calories for 100-500 calorie search")

g <- ggplot(ingredient_test, aes(x=vegetarian, y=healthScore))
g + geom_boxplot() + 
  labs(title =  "Boxplot of health scores for cherry ingredient search")

The plots show a smaller spread of the calorie results for vegetarian recipes and some high outliers in the health scores for some of the vegetarian results with cherry as an ingredient.

One more thing that we could look at are the histogram distributions of calories for the calorie search and health score for the ingredient search results to see if any interesting distributions arise.

#Histogram for calories based on results from the calorie search
g <- ggplot(calorie_test, aes(x=calories))
g + geom_histogram(bins=10, color = "black", fill = "lightblue",
size = 1) + 
  labs(title="Histogram of calories for recipes from 100-500 calories")

#Histogram for health score based on results from the ingredient search
g <- ggplot(ingredient_test, aes(x=healthScore))
g + geom_histogram(bins=10, color = "black", fill = "lightblue",
size = 1) + 
  labs(title="Histogram of health scores for recipes with cherry as an ingredient")

The histograms show the results are skewed to the right in both distributions with peaks at relatively low numbers of calories and health scores.

Moreover, we can search any two nutrients from calories, protein, fat, and carbohydrates, to see what recipes what we would like to choose. Let’s say we are fitness enthusiasts, and we hope to select food that is low in calories but high in protein. We can search for a calorie range of 0-1500 and a protein range of 30-100. The full code using search_by_2_nutrs is as follows:

cal_prot_test <- search_by_2_nutrs(first_nutrient="Calories", first_from=0, first_to=1500,
                                  sec_nutrient="Protein", sec_from=30, sec_to=100, number=100)
cal_prot_test$protein <- as.numeric(substr(cal_prot_test$protein,1, nchar(cal_prot_test$protein)-1))
cal_prot_test$fat <- as.numeric(substr(cal_prot_test$fat,1, nchar(cal_prot_test$fat)-1))
cal_prot_test$carbs <- as.numeric(substr(cal_prot_test$carbs,1, nchar(cal_prot_test$carbs)-1))
cal_prot_test
## # A tibble: 100 × 6
##        id title                                     calories protein   fat carbs
##     <int> <chr>                                        <int>   <dbl> <dbl> <dbl>
##  1  32579 Tuna Spaghetti With Fava Beans                 721      44    28    78
##  2 511728 Pasta Margherita                               809      34    34    90
##  3 631754 Lemony Zucchini Fritters                       473      30    24    36
##  4 631912 A Post Thanksgiving “sopa De Tortilla”         444      36    26    19
##  5 632300 Amazing Chicken Burgers                        361      32    18    17
##  6 632822 Asian Coleslaw with Baked Salmon               508      40    27    32
##  7 633088 Authentic Jamaican Curry Chicken               547      69    19    25
##  8 633352 Bacon Wrapped Tofu Tacos                       580      30    36    33
##  9 633624 Baked Fried Chicken With Cauliflower Mash      662      66    29    33
## 10 633754 Baked Ratatouille                             1028      32    69    82
## # … with 90 more rows

Let’s use scatter plots to see the relationship between calories and fat, calories and carbohydrates.

g <- ggplot(cal_prot_test, aes(x = fat, y = calories))
g + geom_point() + geom_smooth(method = lm, col = "Red") +
  labs(title = "Scatter plot of calories vs fat")

g <- ggplot(cal_prot_test, aes(x = carbs, y = calories))
g + geom_point() + geom_smooth(method = lm, col = "Red") +
  labs(title = "Scatter plot of calories vs carbohydrates")

According to the plots above, we can see that the calories vs. fat has a stronger slope than calories vs. carbohydrates. Therefore, fat has more effect on calories than carbohydrates.

We can continue to evaluate calories, fat, carbohydrates to see if there are interesting finds. Let’s compare fat to carbohydrates, in order to choose food based on fat and carbohydrate levels. First, we will add a new output variable calories_level. This variable did not exist in the source data and had to be created using the values of calories. The categories utilized are:

  1. Low: less than 400
  2. Medium: 400 to less than 800
  3. High: greater than 800
cal_prot_test <- cal_prot_test %>% 
  arrange(calories) %>% 
  mutate(calorie_level = ifelse(cal_prot_test$calories >= 800, "3",
                              ifelse(cal_prot_test$calories >= 400, "2",
                                     ifelse(cal_prot_test$calories >= 0, "1"))),
         calorie_level = factor(calorie_level,
                              level = c(1,2,3),
                              labels = c("low","medium","high")))
cal_prot_test <- cal_prot_test %>% select(calorie_level, calories, protein, fat, carbs, title)
cal_prot_test
## # A tibble: 100 × 6
##    calorie_level calories protein   fat carbs title                                                 
##    <fct>            <int>   <dbl> <dbl> <dbl> <chr>                                                 
##  1 medium             214      32     6     7 Steamed Plaice & Spinach Rolls                        
##  2 high               217      32     7     3 Mahi-Mahi With Lemon Caper Sauce                      
##  3 medium             233      44     5     2 Steamed Halibut                                       
##  4 medium             243      33     4    16 Brined Chicken Breast with Sautéed Onion Dipping Sauce
##  5 low                294      30    11    17 Grilled Buffalo Chicken Wraps                         
##  6 medium             337      36    12    23 Spice-Rubbed Lemon Barbecue Salmon                    
##  7 medium             340      38    11    22 Thai Street Vendor Salmon Skewers                     
##  8 medium             343      36    17    12 Blood Orange Sesame Salmon                            
##  9 medium             347      52     6    18 Chicken Fingers                                       
## 10 high               351      31    15    27 Gluten Free Dairy Free Sugar Free Chinese Chicken Sal…
## # … with 90 more rows

Secondly, we’ll add other two output variables fat_level and carbs_level. Both fat_level and carbs_level categories utilized are:

  1. Low: less than 40
  2. Medium: 40 to less than 80
  3. High: greater than 80
cal_prot_test <- cal_prot_test %>% arrange(fat) %>% 
  mutate(fat_level = ifelse(cal_prot_test$fat >= 80, "3",
                            ifelse(cal_prot_test$fat >= 40,"2",
                                   ifelse(cal_prot_test$fat >= 0, "1"))),
         fat_level = factor(fat_level,
                            level = c(1,2,3),
                            labels = c("low","medium","high")))
cal_prot_test <- cal_prot_test %>% arrange(carbs) %>%          
  mutate(carb_level = ifelse(cal_prot_test$carbs >= 80, "3",
                              ifelse(cal_prot_test$carbs >= 40, "2",
                                     ifelse(cal_prot_test$carbs >= 0, "1"))),
         carb_level = factor(carb_level,
                              level = c(1,2,3),
                              labels = c("low","medium","high")))
cal_prot_test <- cal_prot_test %>% 
  select(calorie_level, calories, fat_level, fat, carb_level, carbs, protein, title)
cal_prot_test
## # A tibble: 100 × 8
##    calorie_level calories fat_level   fat carb_level carbs protein title                            
##    <fct>            <int> <fct>     <dbl> <fct>      <dbl>   <dbl> <chr>                            
##  1 medium             891 medium       65 medium         0      71 Instant Pot Buffalo Garlic Butte…
##  2 high               417 low          30 low            1      35 Cheese Pork Chops                
##  3 medium             233 low           5 low            2      44 Steamed Halibut                  
##  4 high               217 low           7 medium         3      32 Mahi-Mahi With Lemon Caper Sauce 
##  5 low                388 low          20 low            3      48 Grilled Lemon Garlic Chicken     
##  6 low                399 low           4 medium         5      81 Super Easy Oven Baked Cod        
##  7 medium             454 medium       33 low            5      33 Fragrant Curried Chicken Salad   
##  8 medium             619 low          49 low            5      35 Pan-Roasted Swordfish with Chopp…
##  9 medium             426 low          31 low            6      31 Easy Slow Cooker Whole Chicken W…
## 10 medium             214 low           6 medium         7      32 Steamed Plaice & Spinach Rolls   
## # … with 90 more rows

Now, let’s use tables and bar plots to check the results.

table(cal_prot_test$calorie_level, cal_prot_test$fat_level, dnn=c("calorie_level", "fat_level"))
##              fat_level
## calorie_level low medium high
##        low     11      4    1
##        medium  45     15    2
##        high    20      1    1
table(cal_prot_test$calorie_level, cal_prot_test$carb_level, dnn=c("calorie_level", "carb_level"))
##              carb_level
## calorie_level low medium high
##        low      7      8    1
##        medium  32     24    6
##        high    14      7    1

Through the two contingency tables above, we can conclude that medium calorie level with low fat level have the most recipe options from our search, and medium calorie level with low carbohydrate level have the most recipes options from our search. In total, medium calorie level with low fat level have the most recipes.

calorie_mean_fat <- cal_prot_test %>%
  group_by(fat_level) %>%
  summarise_at(vars(calories), list(calorie_mean = mean))
calorie_mean_fat
## # A tibble: 3 × 2
##   fat_level calorie_mean
##   <fct>            <dbl>
## 1 low               532.
## 2 medium            819.
## 3 high             1132.
calorie_mean_carb <- cal_prot_test %>%
  group_by(carb_level) %>%
  summarise_at(vars(calories), list(calorie_mean = mean))
calorie_mean_carb
## # A tibble: 3 × 2
##   carb_level calorie_mean
##   <fct>             <dbl>
## 1 low                639.
## 2 medium             592.
## 3 high               556.

According to the tables above, we can find that, as the fat level increases, the calorie means for each level are increasing. For carbohydrates, we see the opposite, as the carbohydrate level increasing, the calorie means for each level are decreasing. That would be an interesting result to explore further in a deeper analysis.

g <- ggplot(cal_prot_test, aes(x = fat_level))
g + geom_bar(aes(fill = calorie_level), position = "dodge") +
  labs(title = "Bar plot of calorie_level vs fat_level")

g <- ggplot(cal_prot_test, aes(x = carb_level))
g + geom_bar(aes(fill = calorie_level), position = "dodge") +
  labs(title = "Bar plot of calorie_level vs carb_level")

From the bar plots above, we can find that for calorie level vs fat level, as fat is increasing, counts for all three calorie levels are decreasing. For calorie level vs carbohydrate level, as the carbohydrate level increases, the counts for medium and high calorie levels are decreasing, but the counts for low calorie level increase when moving from low carbohydrate level to medium carbohydrate level and then decrease from medium carbohydrate level to high carbohydrate level.

Conclusion

We hope that this vignette has given you some insight into working with APIs. In summary, what we did throughout this vignette was build some functions to allow for some flexible user defined searches, ran a few example searches, and did some exploratory data analysis with the results of those searches. Once the functions are built, there is nearly endless data that can be explored. Happy searching!