Beginner’s guide to deploying H2o models as APIs using FLASK | by Dr. Varshita Sher | Towards Data Science

As a data scientist, most of our time is spent on data preprocessing (or data wrangling), model training, hyperparameter tuning, model validation, etc. However, the number of hats donned by data scientists is ever-increasing, and thus, many industries have now started looking for people who can turn their ML models into an API.
What does this even mean? It means:
You built an awesome ML model with superb accuracy.
You now want others to be able to make use of your model for making predictions (because you are nice or because it is profitable or both) without them needing to see your entire code. This is where an API comes in..
APIs allow us to build a connection so that outsiders can request data from server, send new information to the server, make changes to existing data on server and remove existing data on server.
To explain it in simpler terms (for the purpose of this article), an API is an interface that lets you access and manipulate data (either from a database or some other data source) in the backend when a user requests the data from the frontend. Flask is one of the most popular python frameworks to write data science APIs in the backend.
To test your APIs, you can run the backend server locally which by default runs on the localhost URL (http://127.0.0.1). This URL is followed by a port, say 5000, so that entire URL looks like http://127.0.0.1:5000 . The port can be changed in Flask settings so that if you want to run multiple local servers you can run them on http://127.0.0.1:5000, http://127.0.0.1:5001, and so on.
We will be building a machine learning model using H2o AutoML (uses very few lines of code but generates high-performing models). Our model will be able to predict whether a loan application would be approved or declined based on certain features. Following this, we will be creating our first API to which we would send a query with the client’s age, car type, loan amount, etc and it shall return the status of the application — approved or declined.
Note: I will be using Jupyter Notebook to build and test the H2o model and Spyder to write the Flask code. Finally, I will be accessing my API from within Jupyter as well.
Before we begin writing simple APIs for models, I wanted to give a brief taste of how it feels like to write an even simpler HelloWorld API (one that takes no input from the user). Let’s get started in Spyder...
Here is some boilerplate code that we are going to be using (and later making modifications to):
from flask import Flaskfrom flask_restful import Resource, Apiapp = Flask(__name__)api = Api(app)class HelloWorld(Resource):    def get(self):        return {'hello': 'world'}api.add_resource(HelloWorld, '/')if __name__ == '__main__':    app.run(debug=True, port = 12345)
Few things to note:
return here is of type dict i.e. a key-value pair (key is hello; value is world).
debug = True enables code reloading and better error messages. Never use debug mode in a production environment!
important to add whatever resource you create (such as HelloWorld) to the API using api.add_resource.
the port is explicitly specified as port = 12345 since sometimes the default port can be in use by some other application and can throw an error.
Finally, save the file as medium_tut.py and run it using the green Run button on top (or F5 on Mac). The output in the console pane on the right should look something like this:
As you can see the API is running on local server http://127.0.0.1:12345/.Now open Jupyter Notebook and type the following code to access the API we just wrote.
from requests import getimport requestsurl = 'http://127.0.0.1:12345/'get(url).json()
Kudos on writing your first ever API. :-)
If you notice carefully, the return type of our get function inHelloWorld was dict and thus we use .json() in the code above to parse the outputs of that dictionary.
We will be modifying our previous code in medium_tut.py file to accept one parameter (more specifically a number) from the user’s end and return its square to the user.
from flask import Flaskfrom flask_restful import Resource, Api, reqparseapp = Flask(__name__)api = Api(app)# argument parsingparser = reqparse.RequestParser()parser.add_argument('num1')class HelloWorld(Resource):    def get(self):        return {'hello': 'world'}class PrintSquare(Resource):    def get(self):        # use parser and find the user's input        args = parser.parse_args()                user_query = float(args['num1'])        return {'ans': user_query * user_query}api.add_resource(HelloWorld, '/hello')api.add_resource(PrintSquare, '/sq')if __name__ == '__main__':    app.run(debug=True, port = 12345)
Assessing the modifications we made:
we have added the user inputs (in our case a single number) to the parser using parser.add_argument().
we now have a new class PrintSquare for returning the square of a number.
since we have multiple classes now, and we do not want to confuse the API as to which function it should run when the user makes a request, we had to update our add_resource for HelloWorld by adding '/hello' as the endpoint to it. So now, HelloWorld would be running onhttp://127.0.0.1:12345/hello instead of http://127.0.0.1:12345/
Let’s check if we can access our API, back to Jupyter Notebook:
# note the URL has been updates, i.e. the 'sq' endpoint has been added to iturl = 'http://127.0.0.1:12345/sq'# we have one input parameter 'num1'params = {'num1': '2'}# outputting the response from APIresponse = requests.get(url, params)response.json()
Updating the medium_tut.py file on Spyder to accept two numbers as inputs from the user and return its sum.
from flask import Flaskfrom flask_restful import Resource, Api, reqparseapp = Flask(__name__)api = Api(app)# argument parsingparser = reqparse.RequestParser()parser.add_argument('num1')parser.add_argument('num2')class HelloWorld(Resource):    def get(self):        return {'hello': 'world'}class PrintSquare(Resource):    def get(self):        # use parser and find the user's input        args = parser.parse_args()                user_query = float(args['num1'])        return {'ans': user_query * user_query}class PrintSum(Resource):    def get(self):        # use parser and find the user's inputs        args = parser.parse_args()        num1 = float(args['num1'])        num2 = float(args['num2'])        return {'ans': num1 + num2}api.add_resource(HelloWorld, '/hello')api.add_resource(PrintSquare, '/sq')api.add_resource(PrintSum, '/sum')if __name__ == '__main__':    app.run(debug=True, port = 12345)
And finally checking to see if the API works in Jupyter Notebook:
url = 'http://127.0.0.1:12345/sum'# we have two input parameters - 'num1' & 'num2params = {'num1': '2', 'num2': '5'}response = requests.get(url, params)response.json()
And voila, you are all set to create APIs for your own ML models.
Tip: Go ahead and create a new directory on Desktop (or elsewhere), name it H2o_API. This is where we will be saving the Jupyter Notebook containing code to train our model. Later on, when we create the API script (in Spyder), it will also be saved in the same directory. The whole logic behind doing so is that when we create and download the model locally, it would be accessible by our Flask script.
Our dataset has 5 predictor features:
age_group: (Older, Young, Middle-Aged)
car_type: categorical (Convertible, Saloon, SUV)
loanamount: float
deposit: float
area: categorical (rural, urban)
and one outcome variable i.e. application_outcome which can take two values: approved or declined.
This is how our data looks like:
#Categorical Columns - enum#Numerical Columns - realcol_dict = {'age_group' : 'enum',             'car_type' : 'enum',             'loanamount' : 'real',             'deposit' : 'real',             'area' : 'enum',           'application_outcome': 'enum'}train_h2o = h2o.H2OFrame(train, column_types = col_dict)test_h2o = h2o.H2OFrame(test, column_types = col_dict)
Automated machine learning (AutoML) is the process of automating the end-to-end process of applying machine learning to real-world problems. It can be easily performed using H2o’s AutoML interface (I will be talking in-depth about it in my next article). In a nutshell, it provides a leaderboard with top-performing models, obtained from automatic training and tuning of many models.
For now, let’s load it into our Jupyter notebook:
from h2o.automl import H2OAutoMLaml = H2OAutoML(max_models = 5, max_runtime_secs=100, seed = 1)
Setting the predictor and response features:
# setting predictor and response featuresx = ['age_group', 'car_type', 'loanamount', 'deposit', 'area']y = 'application_outcome'
The training process can be achieved simply using a single line of code in H2o.
# Trainingaml.train(x=x, y='application_outcome', training_frame=train_h2o, validation_frame=test_h2o)
The best model (based on default metric AUC) is stored in aml.leader:
aml.leader
As you can see, even without much tinkering, we have obtained a model with AUC = 0.87 on the validation set (not bad at all)! Also, note that this model’s name is StackedEnsemble_AllModels_AutoML_20200619_172405. The name of your model would be similar to this, minus the part after last underscore i.e. 172405.
# download the model built above to your local machinemy_local_model = h2o.download_model(aml.leader, path="/Users/UserName/Desktop/H2o_API")
If you check the name of this saved model in your directory, it would look something like StackedEnsemble_AllModels_AutoML_20200619_******.
Creating a new python script pred_API.py to write our Flask script.
from flask import Flaskfrom flask_restful import Resource, Api, reqparseapp = Flask(__name__)api = Api(app)import h2oimport pandas as pdh2o.init()## load trained modelmodel_path = 'StackedEnsemble_AllModels_AutoML_20200619_******'uploaded_model = h2o.load_model(model_path)
# argument parsingparser = reqparse.RequestParser(bundle_errors=True) # if there are 2 errors, both of their msgs will be printedparser.add_argument('age_group', choices = ('Young', 'Older', 'Middle-Aged'), help = 'Bad Choice: {error_msg}. Valid choices are Young, Older, Middle-Aged')parser.add_argument('car_type', choices = ('SUV', 'Saloon', 'Convertible'), help = 'Bad Choice: {error_msg}. Valid choices are SUV, Saloon, Convertible')parser.add_argument('loanamount')parser.add_argument('deposit')parser.add_argument('area', choices = ('urban', 'rural'), help = 'Bad Choice: {error_msg}. Valid choices are urban, rural')
While adding the values for categorical features to the parser (such as age_group and car_type), we set the choices as a tuple containing all possible values that the feature can take. In case a value other than those mentioned in choices are encountered, it will throw an error message specified in help.
Finally, we set bundle_errors = True so that in the case of two errors, both the error messages are printed. For instance, we have deliberately set wrong values for age_group as ‘Olderr’ and car_type as ‘SUVv’ in the example below:
#Categorical Columns - enum#Numerical Columns - realcol_dict = {'age_group' : 'enum',             'car_type' : 'enum',             'loanamount' : 'real',             'deposit' : 'real',             'area' : 'enum',           'application_outcome': 'enum'}# prepare empty test data frame to be fed to the modeldata = {}# results dictitem_dict = {}class LoanPred(Resource):    def get(self):         args = parser.parse_args()         age = args['age_group']         car_type = args['car_type']         loanamount = float(args['loanamount'])         deposit = float(args['deposit'])         area = args['area']         application_outcome = 'declined' #setting as default to declined (can set it as 'approved' as well, doesn't matter)                  # put key:value pairs in empty dict called data         data['age_group'] = age         data['car_type'] = car_type         data['loanamount'] = [loanamount]         data['deposit'] = [deposit]         data['area'] = area         data['application_outcome'] = application_outcome                  # creating dataframe from dict         testing = pd.DataFrame(data)                  # converting pandas to h2o dataframe         test = h2o.H2OFrame(testing, column_types = col_dict)                 # making predictions         pred_ans = uploaded_model.predict(test).as_data_frame()                  # put key:value pairs in empty dict called item_dict         item_dict['Prediction'] = pred_ans.predict.values[0]         item_dict['Approved'] = pred_ans.approved.values[0]         item_dict['Declined'] = pred_ans.declined.values[0]                  return{'ans': item_dict}         api.add_resource(LoanPred, '/')if __name__ == '__main__':    app.run(debug=True, port= 1234)
We first take all input parameters and use them to create a pandas data frame called testing. This data frame must be converted into an H2o Frame called test before it can be fed to the H2o model for prediction.
One important thing to note is that the output from model.predict() is an H2o frame and currently, Spyder ipython console doesn’t show the h2o data frame correctly (you can follow the issue here on Github). Thus, we need to explicitly convert the output from model.predict() to a pandas data frame using as_data_frame(). The output pred_ans is a data frame containing three columns —
final prediction (approved or declined),
probability of application being approved,
probability of application being declined.
We store all three values as key-value pairs in item_dict.
And finally all the code in pred_API.py in one place:
from flask import Flaskfrom flask_restful import Resource, Api, reqparseapp = Flask(__name__)api = Api(app)import h2oimport pandas as pdh2o.init()## load saved modelmodel_path = 'StackedEnsemble_AllModels_AutoML_20200619_******'uploaded_model = h2o.load_model(model_path)# argument parsingparser = reqparse.RequestParser(bundle_errors=True) #if there are 2 errors, both's msg will be printedparser.add_argument('age_group', choices = ('Young', 'Older', 'Middle-Aged'), help = 'Bad Choice: {error_msg}. Valid choices are Young, Older, Middle-Aged')parser.add_argument('car_type', choices = ('SUV', 'Saloon', 'Convertible'), help = 'Bad Choice: {error_msg}. Valid choices are SUV, Saloon, Convertible')parser.add_argument('loanamount')parser.add_argument('deposit')parser.add_argument('area', choices = ('urban', 'rural'), help = 'Bad Choice: {error_msg}. Valid choices are urban, rural')#Categorical Columns - enum#Numerical Columns - realcol_dict = {'age_group' : 'enum',             'car_type' : 'enum',             'loanamount' : 'real',             'deposit' : 'real',             'area' : 'enum',           'application_outcome': 'enum'}#prepare empty test data frame to be fed to the modeldata = {}# results dictitem_dict = {}class LoanPred(Resource):    def get(self):         args = parser.parse_args()         age = args['age_group']         car_type = args['car_type']         loanamount = float(args['loanamount'])         deposit = float(args['deposit'])         area = args['area']         application_outcome = 'declined'                   # put key:value pairs in empty dict called data         data['age_group'] = age         data['car_type'] = car_type         data['loanamount'] = [loanamount]         data['deposit'] = [deposit]         data['area'] = area         data['application_outcome'] = application_outcome                  # creating dataframe from dict         testing = pd.DataFrame(data)                  # converting pandas to h2o dataframe         test = h2o.H2OFrame(testing, column_types = col_dict)                 # making predictions         pred_ans = uploaded_model.predict(test).as_data_frame()                  # put key:value pairs in empty dict called item_dict         item_dict['Prediction'] = pred_ans.predict.values[0]         item_dict['Approved'] = pred_ans.approved.values[0]         item_dict['Declined'] = pred_ans.declined.values[0]                  return{'ans': item_dict}         api.add_resource(LoanPred, '/')if __name__ == '__main__':    app.run(debug=True, port= 1234)
In Jupyter Notebook:
# Normal API call with all inputs in correct formaturl = 'http://127.0.0.1:1234/'params = {'age_group': 'Young', 'car_type': 'SUV', 'loanamount': '12342', 'deposit': '2360', 'area': 'rural' }response = requests.get(url, params)response.json()
And there we go.. We have successfully managed to make an API call and retrieve predictions from our model trained in H2o. Now all you have to do is grab a friend who can turn this API output in some fancy web-application.
This was a warm introduction to the concept of creating APIs for ML models. Hopefully, that wasn’t too scary and the descriptions were detailed enough to help you understand the nitty-gritty involved. All the code can be found on Github.
In the next part, we will be learning how to use Flask and Bigquery APIs to extract data from BigQuery datasets based on user query parameters.
Until next time :-)