Repository: kserve/kserve

Issue:
 inference request fails when sending with less number of features than the total model features for KServe 0.11 lightgbm
/kind bug

**What steps did you take and what happened:**
[A clear and concise description of what the bug is.]

In 0.10 LightGBM we are able to send a dictionary of features that is less than the total number of features used. 

In 0.11 we are getting the following errors
```
 {“error”:“The number of features in data (1) is not the same as it was in training data (30).\nYou can set ``predict_disable_shape_check=true`
```



**What did you expect to happen:**
One suspicion is that in 0.11 we are passing the feature names and seems predict is able to pad zero for the missing features

https://github.com/kserve/kserve/blob/release-0.10/python/lgbserver/lgbserver/model.py#L58


However in 0.11 we are no longer passing the feature names
https://github.com/kserve/kserve/blob/release-0.11/python/lgbserver/lgbserver/model.py


**What's the InferenceService yaml:**
[To help us debug please run `kubectl get isvc $name -n $namespace -oyaml` and paste the output]

**Anything else you would like to add:**
[Miscellaneous information that will assist in solving the issue.]


**Environment:**

- Istio Version:
- Knative Version:
- KServe Version:
- Kubeflow version:
- Cloud Environment:[k8s_istio/istio_dex/gcp_basic_auth/gcp_iap/aws/aws_cognito/ibm]
- Minikube/Kind version:
- Kubernetes version: (use `kubectl version`):
- OS (e.g. from `/etc/os-release`):



Discussion:

Resolving patch:
diff --git a/python/kserve/kserve/utils/utils.py b/python/kserve/kserve/utils/utils.py
--- a/python/kserve/kserve/utils/utils.py
+++ b/python/kserve/kserve/utils/utils.py
@@ -87,11 +87,11 @@ def cpu_count():
 def is_structured_cloudevent(body: Dict) -> bool:
     """Returns True if the JSON request body resembles a structured CloudEvent"""
     return "time" in body \
-           and "type" in body \
-           and "source" in body \
-           and "id" in body \
-           and "specversion" in body \
-           and "data" in body
+        and "type" in body \
+        and "source" in body \
+        and "id" in body \
+        and "specversion" in body \
+        and "data" in body
 
 
 def create_response_cloudevent(model_name: str, response: Dict, req_attributes: Dict,
@@ -138,35 +138,18 @@ def to_headers(context: ServicerContext) -> Dict[str, str]:
     return headers
 
 
-def get_predict_input(payload: Union[Dict, InferRequest]) -> Union[np.ndarray, pd.DataFrame]:
+def get_predict_input(payload: Union[Dict, InferRequest], columns: List = None) -> Union[np.ndarray, pd.DataFrame]:
     if isinstance(payload, Dict):
         instances = payload["inputs"] if "inputs" in payload else payload["instances"]
         if len(instances) == 0:
             return np.array(instances)
-        if isinstance(instances[0], Dict):
+        if isinstance(instances[0], Dict) or (
+                isinstance(instances[0], List) and len(instances[0]) != 0 and isinstance(instances[0][0], Dict)):
             dfs = []
             for input in instances:
-                dfs.append(pd.DataFrame(input))
+                dfs.append(pd.DataFrame(input, columns=columns))
             inputs = pd.concat(dfs, axis=0)
             return inputs
-
-        # Handles the following input format
-        # {'inputs': [
-        #     [{'sepal_width_(cm)': 3.5},
-        #      {'petal_length_(cm)': 1.4},
-        #      {'petal_width_(cm)': 0.2},
-        #      {'sepal_length_(cm)': 5.1}]
-        # ]}
-        elif isinstance(instances[0], List) and len(instances[0]) != 0 and isinstance(instances[0][0], Dict):
-            data: Dict[str, List] = {}
-            for instance in instances:
-                for item in instance:
-                    for key, val in item.items():
-                        if key in data:
-                            data[key].append(val)
-                        else:
-                            data[key] = [val]
-            return pd.DataFrame(data)
         else:
             return np.array(instances)
 
diff --git a/python/lgbserver/lgbserver/model.py b/python/lgbserver/lgbserver/model.py
--- a/python/lgbserver/lgbserver/model.py
+++ b/python/lgbserver/lgbserver/model.py
@@ -58,7 +58,7 @@ def load(self) -> bool:
 
     def predict(self, payload: Union[Dict, InferRequest], headers: Dict[str, str] = None) -> Union[Dict, InferResponse]:
         try:
-            instances = get_predict_input(payload)
+            instances = get_predict_input(payload, columns=self._booster.feature_name())
             result = self._booster.predict(instances)
             return get_predict_response(payload, result, self.name)
         except Exception as e:


Test patch:
diff --git a/python/lgbserver/lgbserver/test_model.py b/python/lgbserver/lgbserver/test_model.py
--- a/python/lgbserver/lgbserver/test_model.py
+++ b/python/lgbserver/lgbserver/test_model.py
@@ -14,10 +14,11 @@
 
 import lightgbm as lgb
 import os
+
+import numpy
 from sklearn.datasets import load_iris
 from lgbserver import LightGBMModel
 import pandas as pd
-import numpy
 from kserve.protocol.infer_type import InferInput, InferRequest
 
 model_dir = os.path.join(os.path.dirname(__file__), "example_model", "model")
@@ -46,20 +47,30 @@ def test_model():
                'petal_width_(cm)': {0: 0.2}, 'sepal_length_(cm)': {0: 5.1}}
 
     response = model.predict({"inputs": [request, request]})
-    assert numpy.argmax(response["predictions"][0]) == 2
+    assert numpy.argmax(response["predictions"][0]) == 0
 
     response = model.predict({"instances": [request, request]})
-    assert numpy.argmax(response["predictions"][0]) == 2
+    assert numpy.argmax(response["predictions"][0]) == 0
 
     request = [
         {'sepal_width_(cm)': 3.5}, {'petal_length_(cm)': 1.4},
         {'petal_width_(cm)': 0.2}, {'sepal_length_(cm)': 5.1}
     ]
     response = model.predict({"inputs": [request, request]})
-    assert numpy.argmax(response["predictions"][0]) == 2
+    assert numpy.argmax(response["predictions"][0]) == 0
+
+    response = model.predict({"instances": [request, request]})
+    assert numpy.argmax(response["predictions"][0]) == 0
+
+    request = [
+        {'sepal_width_(cm)': 3.5}, {'petal_length_(cm)': 1.4},
+        {'petal_width_(cm)': 0.2}
+    ]
+    response = model.predict({"inputs": [request, request]})
+    assert numpy.argmax(response["predictions"][0]) == 0
 
     response = model.predict({"instances": [request, request]})
-    assert numpy.argmax(response["predictions"][0]) == 2
+    assert numpy.argmax(response["predictions"][0]) == 0
 
     # test v2 handler
     infer_input = InferInput(name="input-0", shape=[2, 4], datatype="FP32",
diff --git a/test/e2e/data/iris_input_v5.json b/test/e2e/data/iris_input_v5.json
new file mode 100644
--- /dev/null
+++ b/test/e2e/data/iris_input_v5.json
@@ -0,0 +1,9 @@
+{
+    "inputs": [
+        [
+            {"sepal_length_(cm)": 5.1},
+            {"sepal_width_(cm)": 3.5},
+            {"petal_length_(cm)": 1.4}
+        ]
+    ]
+}
diff --git a/test/e2e/predictor/test_lightgbm.py b/test/e2e/predictor/test_lightgbm.py
--- a/test/e2e/predictor/test_lightgbm.py
+++ b/test/e2e/predictor/test_lightgbm.py
@@ -15,6 +15,7 @@
 import json
 import os
 
+import numpy
 import pytest
 from kubernetes import client
 from kubernetes.client import V1ContainerPort, V1ResourceRequirements
@@ -90,10 +91,13 @@ def test_lightgbm_runtime_kserve():
     kserve_client.wait_isvc_ready(service_name, namespace=KSERVE_TEST_NAMESPACE)
 
     res = predict(service_name, "./data/iris_input_v3.json")
-    assert res["predictions"][0][0] > 0.5
+    assert numpy.argmax(res["predictions"][0]) == 0
 
     res = predict(service_name, "./data/iris_input_v4.json")
-    assert res["predictions"][0][0] > 0.5
+    assert numpy.argmax(res["predictions"][0]) == 0
+
+    res = predict(service_name, "./data/iris_input_v5.json")
+    assert numpy.argmax(res["predictions"][0]) == 0
     kserve_client.delete(service_name, KSERVE_TEST_NAMESPACE)