Repository: onnx/sklearn-onnx (python)

Issue:
QDA converter throws an error when used with string labels
`QuadraticDiscriminantAnalysis` converter crashes when using string labels. The shape calculator for the labels is also wrong. 

## Typerror with string labels
To reproduce the error, run the following code (shortened version of the first QDA [test](https://github.com/onnx/sklearn-onnx/blob/acfa6a47ea111ecb93774d62cde0dfbbd669c7ca/tests/test_sklearn_quadratic_discriminant_analysis_converter.py#L28) ):  
```python
import numpy as np
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import DoubleTensorType

X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
y = np.array(["a", "a", "a", "b", "b", "b"])

skl_model = QuadraticDiscriminantAnalysis()
skl_model.fit(X, y)

onnx_model = convert_sklearn(
    skl_model,
    "scikit-learn QDA",
    [("input", DoubleTensorType([None, X.shape[1]]))],
    target_opset=16,
)
```
Output: 
`TypeError: Unable to make a tensor name='classes' onnx_type=7 shape=[2] content-type=<class 'numpy.ndarray'>.`

Note: `LinearDiscriminantAnalysis` model does not fail on this case


## Wrong shape calculator 
The `QuadraticDiscriminantAnalysis`  shape calculator is  wrong in the following line:
https://github.com/onnx/sklearn-onnx/blob/acfa6a47ea111ecb93774d62cde0dfbbd669c7ca/skl2onnx/shape_calculators/quadratic_discriminant_analysis.py#L9
 The first output should have shape `[1, None] `and not `[1, N]`, where `N` is the number of labels. 

This leads to a warning during inference:
```python
import numpy as np
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import DoubleTensorType
import onnxruntime

X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
y = np.array([0, 0, 0, 1, 1, 1])

skl_model = QuadraticDiscriminantAnalysis()
skl_model.fit(X, y)

onnx_model = convert_sklearn(
    skl_model,
    "scikit-learn QDA",
    [("input", DoubleTensorType([None, X.shape[1]]))],
    target_opset=16,
    options={"zipmap": False},
)


sess = onnxruntime.InferenceSession(
    onnx_model.SerializeToString(), providers=["CPUExecutionProvider"]
)

sess.run(None, {"input": X.astype(np.double)})
```
Output:
`[W:onnxruntime:, execution_frame.cc:870 VerifyOutputSizes] Expected shape from model of {1,2} does not match actual shape of {1,6} for output label`

#### Package versions: 
```
python==3.9.21
numpy==1.24.3
sklearn==1.6.1
skl2onnx==1.18.0
onnxconverter_common==1.13.0
onnx==1.13.1
onnxruntime==1.19.2
```


Pull request:
Fix QDA converter crashing on string labels and incorrect shape calculation
Fixes #1168 

Patch:
diff --git a/skl2onnx/operator_converters/quadratic_discriminant_analysis.py b/skl2onnx/operator_converters/quadratic_discriminant_analysis.py
index d68bb92..30b17e3 100644
--- a/skl2onnx/operator_converters/quadratic_discriminant_analysis.py
+++ b/skl2onnx/operator_converters/quadratic_discriminant_analysis.py
@@ -31,6 +31,12 @@ def convert_quadratic_discriminant_analysis_classifier(
     model = operator.raw_operator
 
     n_classes = len(model.classes_)
+    if all(isinstance(i, str) for i in model.classes_):
+        class_type = onnx_proto.TensorProto.STRING
+        class_labels = [s.encode("utf-8") for s in model.classes_]
+    else:
+        class_type = onnx_proto.TensorProto.INT64
+        class_labels = [int(i) for i in model.classes_]
 
     proto_dtype = guess_proto_type(operator.inputs[0].type)
     if proto_dtype != onnx_proto.TensorProto.DOUBLE:
@@ -148,10 +154,7 @@ def convert_quadratic_discriminant_analysis_classifier(
     apply_argmax(scope, [decision_fun], [argmax_out], container, axis=1)
 
     classes = scope.get_unique_variable_name("classes")
-    container.add_initializer(
-        classes, onnx_proto.TensorProto.INT64, [n_classes], model.classes_
-    )
-
+    container.add_initializer(classes, class_type, [n_classes], class_labels)
     container.add_node(
         "ArrayFeatureExtractor",
         [classes, argmax_out],
diff --git a/skl2onnx/shape_calculators/quadratic_discriminant_analysis.py b/skl2onnx/shape_calculators/quadratic_discriminant_analysis.py
index 3dab73c..8a6ccf5 100644
--- a/skl2onnx/shape_calculators/quadratic_discriminant_analysis.py
+++ b/skl2onnx/shape_calculators/quadratic_discriminant_analysis.py
@@ -1,13 +1,19 @@
 # SPDX-License-Identifier: Apache-2.0
 
 from ..common._registration import register_shape_calculator
-from ..common.data_types import Int64TensorType
+from ..common.data_types import Int64TensorType, StringTensorType
 
 
 def calculate_quadratic_discriminant_analysis_shapes(operator):
-    N = len(operator.raw_operator.classes_)
-    operator.outputs[0].type = Int64TensorType([1, N])
-    operator.outputs[1].type.shape = [None, N]
+    classes = operator.raw_operator.classes_
+    if all((isinstance(s, str)) for s in classes):
+        label_tensor_type = StringTensorType
+    else:
+        label_tensor_type = Int64TensorType
+
+    n_clasess = len(classes)
+    operator.outputs[0].type = label_tensor_type([1, None])
+    operator.outputs[1].type.shape = [None, n_clasess]
 
 
 register_shape_calculator(


Test patch:
diff --git a/tests/test_sklearn_quadratic_discriminant_analysis_converter.py b/tests/test_sklearn_quadratic_discriminant_analysis_converter.py
index d9b7df6..0d0c74b 100644
--- a/tests/test_sklearn_quadratic_discriminant_analysis_converter.py
+++ b/tests/test_sklearn_quadratic_discriminant_analysis_converter.py
@@ -49,6 +49,36 @@ class TestQuadraticDiscriminantAnalysisConverter(unittest.TestCase):
             basename="SklearnQDA_2c2f_Float",
         )
 
+    @unittest.skipIf(
+        pv.Version(sklearn.__version__) < pv.Version("1.0"), reason="scikit-learn<1.0"
+    )
+    @unittest.skipIf(
+        pv.Version(onnx_version) < pv.Version("1.11"), reason="fails with onnx 1.10"
+    )
+    def test_model_qda_2c2f_float_string_labels(self):
+        # 2 classes, 2 features, string_labels
+        X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
+        y = np.array(["apple", "apple", "apple", "banana", "banana", "banana"])
+        X_test = np.array([[-0.8, -1], [0.8, 1]])
+
+        skl_model = QuadraticDiscriminantAnalysis()
+        skl_model.fit(X, y)
+
+        onnx_model = convert_sklearn(
+            skl_model,
+            "scikit-learn QDA",
+            [("input", FloatTensorType([None, X.shape[1]]))],
+            target_opset=TARGET_OPSET,
+        )
+
+        self.assertIsNotNone(onnx_model)
+        dump_data_and_model(
+            X_test.astype(np.float32),
+            skl_model,
+            onnx_model,
+            basename="SklearnQDA_2c2f_Float_String_labels",
+        )
+
     @unittest.skipIf(
         pv.Version(sklearn.__version__) < pv.Version("1.0"), reason="scikit-learn<1.0"
     )