I'm trying to build a REST API using Flask which returns value from Scipy's minimize
function. I am able to get a result but I want to expose it in an API call and this code is erroring:
import scipy.stats as sp
from scipy.optimize import minimize
from flask import Flask, g, Response
from flask_restful import Resource, Api, reqparse
import numpy as np
app = Flask(__name__)
api = Api(app)
class Minimize(Resource):
result = None
def _calculate_probability(self, spread, std_dev):
return sp.norm.sf(0.5, spread, scale=std_dev)
def _calculate_mse(self, std_dev):
spread_inputs = np.array(self.spreads)
model_probabilities = self._calculate_probability(spread_inputs, std_dev)
mse = np.sum((model_probabilities - self.expected_probabilities)**2) / len(spread_inputs)
return mse
def __init__(self, expected_probabilities, spreads, std_dev_guess):
self.std_dev_guess = std_dev_guess
self.spreads = spreads
self.expected_probabilities = expected_probabilities
def solve(self):
self.result = minimize(self._calculate_mse, self.std_dev_guess, method='BFGS')
def get(self):
return {'data': self.result}, 200
api.add_resource(Minimize, '/minimize')
I'm able to print an answer to the console:
spreads = [10.5, 9.5, 10, 8.5]
expected_probabilities = [0.8091, 0.7785, 0.7708, 0.7692]
minimizer = Minimize(expected_probabilities, spreads, 12.0)
minimizer.solve()
print(minimizer.get())
I get this:
probability-calculator_1 | ({'data': fun: 0.00018173060393236452
probability-calculator_1 | hess_inv: array([[1381.37379663]])
probability-calculator_1 | jac: array([-1.56055103e-06])
probability-calculator_1 | message: 'Optimization terminated successfully.'
probability-calculator_1 | nfev: 24
probability-calculator_1 | nit: 3
probability-calculator_1 | njev: 8
probability-calculator_1 | status: 0
probability-calculator_1 | success: True
probability-calculator_1 | x: array([11.70822653])}, 200)
But, when I do a GET request to localhost:5000/minimize
, this is the error response:
TypeError: __init__() missing 3 required positional arguments: 'expected_probabilities', 'spreads', and
'std_dev_guess'
How do I define the API call so it returns the printed answer?
EDIT: So I have added another class to try and get a response to a POST request.
class MinimisedError(Resource):
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('spread_inputs', action='append', required=True)
parser.add_argument('expected_probabilities',
action='append', required=True)
parser.add_argument('std_dev', required=True)
args = parser.parse_args()
minimizer = Minimize(args.spread_inputs,
args.expected_probabilities, float(args.std_dev))
minimizer.solve()
return {minimizer.get()}, 200
api.add_resource(MinimisedError, '/minimize')
When I try a POST with body
{
"expected_probabilities":[0.8091, 0.7785, 0.7708, 0.7692],
"spread_inputs":[10.5, 9.5, 10, 8.5],
"std_dev":12.0
}
I get this response:
numpy.core._exceptions.UFuncTypeError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('<U32'), dtype('<U32')) -> dtype('<U32')
Bellow a minimal complete verifiable example that solves your problem:
from http import HTTPStatus
import numpy as np
from scipy import stats, optimize
from flask import Flask
from flask_restful import Resource, Api, reqparse
app = Flask(__name__)
api = Api(app)
class OptimizeStdDev(Resource):
@staticmethod
def solve(spread, expected, stddev):
"""Solve a specific problem (staticmethod are stateless)"""
spread = np.array(spread)
expected = np.array(expected)
def mse(s):
estimated = stats.norm.sf(0.5, spread, scale=s)
mse = np.sum(np.power((estimated - expected), 2))/spread.size
return mse
optsol = optimize.minimize(mse, stddev, method='BFGS')
return optsol
def post(self):
"""Bind optimizer to POST endpoint"""
parser = reqparse.RequestParser()
parser.add_argument('spread', action='append', type=float, required=True)
parser.add_argument('expected', action='append', type=float, required=True)
parser.add_argument('stddev', type=float, required=True)
args = parser.parse_args()
opt = OptimizeStdDev.solve(**args)
# Convert OptimizeResult as a JSON serializable object:
res = {k: v.tolist() if isinstance(v, np.ndarray) else v for k, v in opt.items()}
return res, HTTPStatus.OK
api.add_resource(OptimizeStdDev, '/minimize')
def main():
app.run(debug=True)
if __name__ == "__main__":
main()
Let's check this MCVE actually solves your problem:
import requests
data = {
"expected": [0.8091, 0.7785, 0.7708, 0.7692],
"spread": [10.5, 9.5, 10, 8.5],
"stddev": 12.0
}
rep = requests.post("http://127.0.0.1:5000/minimize", json=data)
rep.json()
Returns the following JSON object:
{
"fun": 0.00018173060393236452,
"jac": [-1.5605510270688683e-06],
"hess_inv": [[1381.3737966283536]],
"nfev": 24,
"njev": 8,
"status": 0,
"success": True,
"message": "Optimization terminated successfully.",
"x": [11.708226529461706],
"nit": 3
}
Which complies with your expected output.
There are multiple problems in the initial code, mainly:
reqparse
;GET
method when it should use POST
instead (you are sending data to your server before having a resource back).Resource
class to store user input is not a good design when using Flask. Additionally, storing user input into a class breaks a major REST fundamental principle: statelessness. A class must be stateless with regards to any clients. Instead we may use @staticmethod
to insure statelessness and nested function with local variables (see solve
and mse
). This is why I totally refactored how your solver is implemented;scipy.optimize.optimize.OptimizeResult
solution object is not valid because it is not JSON serializable as this (neither numpy.ndarray
), instead we can map solution fields to a dict when returning the resource (see res
one-liner in post
method).