Repository: mlflow/mlflow

Issue:
REST API error with sklearn classifier
### System information
- **Have I written custom code (as opposed to using a stock example script provided in MLflow)**: I wrote a simple random forest classifier using sklearn and the iris dataset to test the mlflow workflow from scratch.
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: macOS 10.14.1
- **MLflow installed from (source or binary)**: source (pip install mlflow)
- **MLflow version (run ``mlflow --version``)**: 0.8.2
- **Python version**: 3.7
- **npm version (if running the dev UI):
- **Exact command to reproduce**:curl -X POST -H "Content-Type:application/json; format=pandas-split" --data '{"columns":["sepal_length_cm", "sepal_width_cm", "petal_length_cm", "petal_width_cm"],"data":[[5.3, 1.7, 3.5, 0.5]]}' http://127.0.0.1:1234/invocations

### Describe the problem
If sklearn classifier.predict() method returns a categorical variable, then there is a 500 internal server error returned after the above http request.

The error is from the following function in mlflow/utils/__init.py__:

def ndarray2list(ndarray):
    """
    Convert n-dimensional numpy array into nested lists and convert the elements types to native
    python so that the list is json-able using standard json library.
    :param ndarray: numpy array
    :return: list representation of the numpy array with element types convereted to native python
    """
    if len(ndarray.shape) <= 1:
        return [x.item() for x in ndarray]
    return [ndarray2list(ndarray[i, :]) for i in range(0, ndarray.shape[0])]

x.item() fails if the array returned from the sklearn classifier.predict() method contains strings, as in the case of the iris dataset (labels are 'setosa', etc). I corrected the error on my local install by changing the function as below:

def ndarray2list(ndarray):
    """
    Convert n-dimensional numpy array into nested lists and convert the elements types to native
    python so that the list is json-able using standard json library.
    :param ndarray: numpy array
    :return: list representation of the numpy array with element types convereted to native python
    """
    if len(ndarray.shape) <= 1:
        try:
            return [x.item() for x in ndarray]
        except:
            return [x for x in ndarray]
    return [ndarray2list(ndarray[i, :]) for i in range(0, ndarray.shape[0])]

### Source code / logs
server side error traceback:
[2019-02-19 10:57:20,668] ERROR in app: Exception on /invocations [POST]
Traceback (most recent call last):
  File "/anaconda3/lib/python3.7/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/anaconda3/lib/python3.7/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/anaconda3/lib/python3.7/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/anaconda3/lib/python3.7/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/anaconda3/lib/python3.7/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/anaconda3/lib/python3.7/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/anaconda3/lib/python3.7/site-packages/mlflow/server/handlers.py", line 68, in wrapper
    return func(*args, **kwargs)
  File "/anaconda3/lib/python3.7/site-packages/mlflow/pyfunc/scoring_server.py", line 185, in transformation
    predictions = get_jsonable_obj(raw_predictions, pandas_orient="records")
  File "/anaconda3/lib/python3.7/site-packages/mlflow/utils/__init__.py", line 34, in get_jsonable_obj
    return ndarray2list(data)
  File "/anaconda3/lib/python3.7/site-packages/mlflow/utils/__init__.py", line 20, in ndarray2list
    return [x.item() for x in ndarray]
  File "/anaconda3/lib/python3.7/site-packages/mlflow/utils/__init__.py", line 20, in <listcomp>
    return [x.item() for x in ndarray]
AttributeError: 'str' object has no attribute 'item'


Discussion:
@tomasatdatabricks Looks like `ndarray` has a `tolist()` method. Would that be appropriate to use here instead?
Yes it looks like it would be. 

Resolving patch:
diff --git a/mlflow/utils/__init__.py b/mlflow/utils/__init__.py
--- a/mlflow/utils/__init__.py
+++ b/mlflow/utils/__init__.py
@@ -9,18 +9,6 @@
                                                   micro=version_info.micro)
 
 
-def ndarray2list(ndarray):
-    """
-    Convert n-dimensional numpy array into nested lists and convert the elements types to native
-    python so that the list is json-able using standard json library.
-    :param ndarray: numpy array
-    :return: list representation of the numpy array with element types convereted to native python
-    """
-    if len(ndarray.shape) <= 1:
-        return [x.item() for x in ndarray]
-    return [ndarray2list(ndarray[i, :]) for i in range(0, ndarray.shape[0])]
-
-
 def get_jsonable_obj(data, pandas_orient="records"):
     """Attempt to make the data json-able via standard library.
     Look for some commonly used types that are not jsonable and convert them into json-able ones.
@@ -31,7 +19,7 @@ def get_jsonable_obj(data, pandas_orient="records"):
                           dictionary using this Pandas serialization orientation.
     """
     if isinstance(data, np.ndarray):
-        return ndarray2list(data)
+        return data.tolist()
     if isinstance(data, pd.DataFrame):
         return data.to_dict(orient=pandas_orient)
     if isinstance(data, pd.Series):


Test patch:
diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py
--- a/tests/utils/test_utils.py
+++ b/tests/utils/test_utils.py
@@ -1,3 +1,4 @@
+import numpy as np
 import pytest
 
 from mlflow.utils import get_unique_resource_id
@@ -15,3 +16,14 @@ def test_get_unique_resource_id_with_invalid_max_length_throws_exception():
 
     with pytest.raises(ValueError):
         get_unique_resource_id(max_length=0)
+
+
+def test_get_jsonnable_obj():
+    import json
+    from mlflow.utils import get_jsonable_obj
+    from mlflow.utils.rest_utils import NumpyEncoder
+    py_ary = [["a", "b", "c"], ["e", "f", "g"]]
+    np_ary = get_jsonable_obj(np.array(py_ary))
+    assert json.dumps(py_ary, cls=NumpyEncoder) == json.dumps(np_ary, cls=NumpyEncoder)
+    np_ary = get_jsonable_obj(np.array(py_ary, dtype=type(str)))
+    assert json.dumps(py_ary, cls=NumpyEncoder) == json.dumps(np_ary, cls=NumpyEncoder)