Repository: sktime/sktime

Issue:
[BUG] BOSS classifier class names truncated during prediction
**Describe the bug**
When calling `predict_proba` on IndividualBOSS classifier, sktime returns KeyError

**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
-->

```
import pandas as pd
import numpy as np
x = pd.Series(np.arange(1.0,10.0))
nested_X = pd.DataFrame({
  "nested_X":[x, x, x]
})
y = pd.Series(['01A', '02A', '01A'], dtype="string", name="classes")

from sktime.transformations.panel.padder import PaddingTransformer
from sktime.classification.dictionary_based import IndividualBOSS

iboss = IndividualBOSS()
iboss.fit(nested_X, y)
iboss.predict_proba(nested_X[0:1])
```

** Output behavior **
```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[5], line 1
----> 1 iboss.predict_proba(nested_X[0:1])

File c:\Users\anaconda3\envs\env\lib\site-packages\sktime\classification\base.py:262, in BaseClassifier.predict_proba(self, X)
    259     return self._single_class_y_pred(X, method="predict_proba")
    261 # call internal _predict_proba
--> 262 return self._predict_proba(X)

File c:\Users\anaconda3\envs\env\lib\site-packages\sktime\classification\base.py:572, in BaseClassifier._predict_proba(self, X)
    570 dists = np.zeros((n_pred, self.n_classes_))
    571 for i in range(n_pred):
--> 572     dists[i, self._class_dictionary[preds[i]]] = 1
    574 return dists

KeyError: '0'
```

**Expected behavior**
It should return 1 of the 2 unique classes instead of '0'


**Additional context**
I manage to find out that this is due to numpy behavior of changing `str` type to '<U1' which truncates any string to only length of 1.  The particular line of code is at [link](https://github.com/sktime/sktime/blob/8fe509628405eaeaa6cbc26351e946c4a8a54687/sktime/classification/dictionary_based/_boss.py#L619)

Suggest to change it to the following:

```
classes = np.zeros(test_bags.shape[0], dtype=(lambda dtype: "object" if dtype == str else dtype)(type(self._class_vals[0])))
```

Need advise here if I should submit a pull request? Not sure if there's any test cases to run through? Thanks. 

**Versions**
<details>

System:
    python: 3.8.15 (default, Nov 24 2022, 14:38:14) [MSC v.1916 64 bit (AMD64)]
executable: c:\Users\kyunomi\anaconda3\envs\cogpilot\python.exe
   machine: Windows-10-10.0.22621-SP0

Python dependencies:
          pip: 22.3.1
   setuptools: 65.5.0
      sklearn: 1.2.0
       sktime: 0.15.0
  statsmodels: 0.13.5
        numpy: 1.23.5
        scipy: 1.10.0
       pandas: 1.3.5
   matplotlib: 3.5.3
       joblib: 1.1.1
        numba: 0.56.4
     pmdarima: 2.0.1
      tsfresh: 0.20.0

</details>

<!-- Thanks for contributing! -->



Discussion:
I can confirm the bug (python 3.10, windows).

a pull request with a fix would be much appreciated!
Re testing, here is how to run the test suite for the estimator locally:
https://www.sktime.org/en/stable/developer_guide/add_estimators.html#how-to-test-interface-conformance

Although it should be noted that the tests already pass on `main` and did not detect this, so we may like to think about adding a test on dummy data that would cause the issue. This means the test coverage is probably too small here, case-wise.

Separately, a `pytest` based test for the fix would be appreciated, which we then could turn later into a data scenario or an extra test for all classifiers.
(if you are interested, the general framework is described here:
https://www.sktime.org/en/stable/developer_guide/testing_framework.html
and the data scenarios are in `utils._testing.scenarios_classification`, maybe it suffices to change column names in one scenario)

Resolving patch:
diff --git a/sktime/classification/dictionary_based/_boss.py b/sktime/classification/dictionary_based/_boss.py
--- a/sktime/classification/dictionary_based/_boss.py
+++ b/sktime/classification/dictionary_based/_boss.py
@@ -616,7 +616,11 @@ def _predict(self, X):
             Predicted class labels.
         """
         test_bags = self._transformer.transform(X)
-        classes = np.zeros(test_bags.shape[0], dtype=type(self._class_vals[0]))
+        data_type = type(self._class_vals[0])
+        if data_type == np.str_:
+            data_type = "object"
+
+        classes = np.zeros(test_bags.shape[0], dtype=data_type)
 
         if self._transformed_data.shape[1] > 0:
             distance_matrix = pairwise_distances(


Test patch:
diff --git a/sktime/classification/dictionary_based/tests/test_boss.py b/sktime/classification/dictionary_based/tests/test_boss.py
new file mode 100644
--- /dev/null
+++ b/sktime/classification/dictionary_based/tests/test_boss.py
@@ -0,0 +1,171 @@
+# -*- coding: utf-8 -*-
+"""BOSS test code."""
+import numpy as np
+import pytest
+
+from sktime.classification.dictionary_based import BOSSEnsemble, IndividualBOSS
+from sktime.datasets import load_unit_test
+
+
+@pytest.fixture
+def dataset():
+    """
+    Load unit_test train and test data set from sktime.
+
+    :return: tuple, (X_train, y_train, X_test, y_test).
+    """
+    X_train, y_train = load_unit_test(split="train")
+    X_test, y_test = load_unit_test(split="test")
+    return (X_train, y_train, X_test, y_test)
+
+
+def test_individual_boss_classes_string(dataset):
+    """Test of Individual Boss on unit test data with class dtype as STRING."""
+    # load unit test data
+    X_train, y_train, X_test, y_test = dataset
+
+    # change class
+    new_class = {"1": "Class1", "2": "Class2"}
+    y_train = np.array([new_class[y] for y in y_train])
+
+    # train iboss and predict X_test
+    iboss = IndividualBOSS()
+    iboss.fit(X_train, y_train)
+    y_pred = iboss.predict(X_test)
+
+    # assert class type and names
+    assert y_pred.dtype == object
+    assert set(y_pred) == set(y_train)
+
+
+def test_individual_boss_classes_integer(dataset):
+    """Test of Individual Boss on unit test data with class dtype as INTEGER."""
+    # load unit test data
+    X_train, y_train, X_test, y_test = dataset
+
+    # change class
+    new_class = {"1": 1, "2": 2}
+    y_train = np.array([new_class[y] for y in y_train])
+
+    # train iboss and predict X_test
+    iboss = IndividualBOSS()
+    iboss.fit(X_train, y_train)
+    y_pred = iboss.predict(X_test)
+
+    # assert class type and names
+    assert y_pred.dtype == int
+    assert set(y_pred) == set(y_train)
+
+
+def test_individual_boss_classes_float(dataset):
+    """Test of Individual Boss on unit test data with class dtype as FLOAT."""
+    # load unit test data
+    X_train, y_train, X_test, y_test = dataset
+
+    # change class
+    new_class = {"1": 1.0, "2": 2.0}
+    y_train = np.array([new_class[y] for y in y_train])
+
+    # train iboss and predict X_test
+    iboss = IndividualBOSS()
+    iboss.fit(X_train, y_train)
+    y_pred = iboss.predict(X_test)
+
+    # assert class type and names
+    assert y_pred.dtype == float
+    assert set(y_pred) == set(y_train)
+
+
+def test_individual_boss_classes_boolean(dataset):
+    """Test of Individual Boss on unit test data with class dtype as BOOLEAN."""
+    # load unit test data
+    X_train, y_train, X_test, y_test = dataset
+
+    # change class
+    new_class = {"1": True, "2": False}
+    y_train = np.array([new_class[y] for y in y_train])
+
+    # train iboss and predict X_test
+    iboss = IndividualBOSS()
+    iboss.fit(X_train, y_train)
+    y_pred = iboss.predict(X_test)
+
+    # assert class type and names
+    assert y_pred.dtype == bool
+    assert set(y_pred) == set(y_train)
+
+
+def test_boss_ensemble_classes_string(dataset):
+    """Test of Boss Ensemble on unit test data with class dtype as STRING."""
+    # load unit test data
+    X_train, y_train, X_test, y_test = dataset
+
+    # change class
+    new_class = {"1": "Class1", "2": "Class2"}
+    y_train = np.array([new_class[y] for y in y_train])
+
+    # train boss_ensemble and predict X_test
+    boss_ensemble = BOSSEnsemble()
+    boss_ensemble.fit(X_train, y_train)
+    y_pred = boss_ensemble.predict(X_test)
+
+    # assert class type and names
+    assert y_pred.dtype == "<U6"
+    assert set(y_pred) == set(y_train)
+
+
+def test_boss_ensemble_classes_integer(dataset):
+    """Test of Boss Ensemble on unit test data with class dtype as INTEGER."""
+    # load unit test data
+    X_train, y_train, X_test, y_test = dataset
+
+    # change class
+    new_class = {"1": 1, "2": 2}
+    y_train = np.array([new_class[y] for y in y_train])
+
+    # train boss_ensemble and predict X_test
+    boss_ensemble = BOSSEnsemble()
+    boss_ensemble.fit(X_train, y_train)
+    y_pred = boss_ensemble.predict(X_test)
+
+    # assert class type and names
+    assert y_pred.dtype == int
+    assert set(y_pred) == set(y_train)
+
+
+def test_boss_ensemble_classes_float(dataset):
+    """Test of Boss Ensemble on unit test data with class dtype as FLOAT."""
+    # load unit test data
+    X_train, y_train, X_test, y_test = dataset
+
+    # change class
+    new_class = {"1": 1.0, "2": 2.0}
+    y_train = np.array([new_class[y] for y in y_train])
+
+    # train boss_ensemble and predict X_test
+    boss_ensemble = BOSSEnsemble()
+    boss_ensemble.fit(X_train, y_train)
+    y_pred = boss_ensemble.predict(X_test)
+
+    # assert class type and names
+    assert y_pred.dtype == float
+    assert set(y_pred) == set(y_train)
+
+
+def test_boss_ensemble_classes_boolean(dataset):
+    """Test of Boss Ensemble on unit test data with class dtype as BOOLEAN."""
+    # load unit test data
+    X_train, y_train, X_test, y_test = dataset
+
+    # change class
+    new_class = {"1": True, "2": False}
+    y_train = np.array([new_class[y] for y in y_train])
+
+    # train boss_ensemble and predict X_test
+    boss_ensemble = BOSSEnsemble()
+    boss_ensemble.fit(X_train, y_train)
+    y_pred = boss_ensemble.predict(X_test)
+
+    # assert class type and names
+    assert y_pred.dtype == bool
+    assert set(y_pred) == set(y_train)