Search code examples
pythonflaskscipyflask-restfulscipy-optimize

Python - Scipy Optimize And Return Value In Flask-Restful


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')

Solution

  • TL;DR

    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()
    

    Verification

    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.

    What goes wrong?

    There are multiple problems in the initial code, mainly:

    • Casting problems, when API payload is sent through Flask. Added cast in reqparse;
    • Using GET method when it should use POST instead (you are sending data to your server before having a resource back).
    • A 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;
    • Returning an 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).