Websites like Allrecipes, Epicurious and Yummly aggregate millions of recipes from around the web and compile them into a centralized resource that allows users to search for and compare recipes. Some of the recipes pulled onto these sites come with lots of details, such as difficulty level, course, cuisine, total cost of ingredients, cook time, flavor profile, cuisine, etc. Others contain only a list of ingredients and instructions for preparation.

Since extra details about recipes allow users to search and filter more effectively, many websites have developed algorithms to predict the characteristics of recipes that contain missing information. Below is an overview of the steps I took to replicate the process of using machine learning to infer unknown recipe attributes.

Step 1: Get the data

I obtained access to the Yummly Recipe API, which contains data on over 2 million recipes. The API has very good documentation, and academic access can be requested if you're using it for educational purposes. It allows you to set a variety of search parameters, such as allowed_course, excluded_course, allowed_cuisine and excluded_cuisine. Although there was no apparent limit to the maximum number of responses you could request per API call, I found that batches of 500 worked best. Below is some of the code I used to make an API call. It uses the requests library, which returns the response in JSON format if the retrieval was successful. Once the JSON file was retrieved from the API I converted it to a Python dictionary which I then converted to a pandas DataFrame.

Step 2: Store on AWS

Since I was only requesting 500 recipes at a time, I had to make a series of API calls over the course of about a week. I stored the data retrieved from each batch on a Postgres instance on Amazon's Relational Database Service. I used pandas' very handy to_sql function to add slices of data to the database I created. Then when I had all the data compiled in Postgres, I used the read_sql function to make queries directly into pandas.

Here's how I sent data to sql. The Postgres instance I created is no longer active.

from sqlalchemy import create_engine
import pandas as pd

#establish db connection
engine = create_engine('postgresql://treytrey3:113315th3@recipeproject3.czcsc2tr7kct.us-east-1.rds.amazonaws.com:5432/dsicapstone3')

#sample dataframe 
df = pd.read_csv('../ingredients_combined/ingredients_reduced.csv')

#name it and send to sql
name = 'ingredients'
df.to_sql(name, engine, flavor='postgres', if_exists='replace')

Step 3: Clean text data

I had to preprocess the text data before I could normalize it. This required the removal of non-alphanumeric characters, as well as converting the text from a string to a list of individual words without whitespace.

Step 3: Normalize text data

from nltk.stem import WordNetLemmatizer

I used NLTK's WordNetLemmatizer to normalize ingredient names. Lemmatization is a natural language processing (NLP) technique that allows you to ignore trivial differences between different forms of the same word. Stanford's Introduction to Information Retrieval is a good resource for learning about NLP. Here they discuss the difference between lemmatization and another technique called stemming:

Stemming usually refers to a crude heuristic process that chops off the ends of words in the hope of achieving this goal correctly most of the time, and often includes the removal of derivational affixes. Lemmatization usually refers to doing things properly with the use of a vocabulary and morphological analysis of words, normally aiming to remove inflectional endings only and to return the base or dictionary form of a word, which is known as the lemma.

For example, compare the output of the word "sausage" when passed through a lemmatizer and a stemmer:

from nltk.stem import WordNetLemmatizer, PorterStemmer

wordnet_lemmatizer = WordNetLemmatizer()
porter_stemmer = PorterStemmer()

print wordnet_lemmatizer.lemmatize('sausages')
print porter_stemmer.stem('sausages')
sausage
sausag

Step 4: Create Bag of Words

from sklearn.feature_extraction.text import TfidfVectorizer

The above scikit-learn "feature_extraction" package was also really important to the success of this project. It contains many common NLP-related tools. One that was especially useful was the TfidfVectorizer. Tf-idf stands for term frequency inverse document frequency. It's not complicated; it essentially weights the importance of words based on their frequency in a document.

So if you were going through recipes as I was, ingredients like onions and salt and pepper aren't very indicative of the cuisine of the recipe. But jalapeno or soy sauce or herring is more important, but also more uncommon. Tf-idf weighs rarer terms more heavily than common terms. Here is an excellent overview of the concept.

Step 5: Deploy Naive Bayes

Naive Bayes is highly effective at text classification tasks because it assumes independence between each pair of features. The Bag of Words representation also assumes independence between all the words in each document, acccounting for at least part of theis synergy. It also doesn't require much tuning at all, unlike other models like logistic regression. You mainly have to decide whether to run the Multinomial or Bernouilli flavor of the model. Since Naive Bayes is quite fast compared to other models, there's often time to run both and see how they compare.

Below is the accuracy score I generated from a Bernouilli Naive Bayes. Bernouilli performed slightly better than Multinomial Naive Bayes. Sometimes it's unclear why one model performs better than another, so it's always good to compare a few. Scikit-Learn docs postulate that Bernoulli may perform better than Multinomial Naive Bayes on datasets with shorter documents. This certainly makes sense in the current case, where each document was a short list of ingredients.

Accuracy Score: 97%