Repository: sktime/sktime

Issue:
[BUG] test error in `RandomShapeletTransform`
Update: this is not sporadic but is also failing on `main`.

Unclear what is causing this, but it started recently, and no changes to the estimator have been made so far. Will temporarily address this by an emergency merge.

---
Sometimes, `RandomShapeletTransform` seems to create an empty list internally.

Traceback from https://github.com/alan-turing-institute/sktime/runs/7593226866?check_suite_focus=true

```
self = RandomShapeletTransform(batch_size=20, max_shapelets=5, n_shapelet_samples=50)
X = array([[[1.96618854, 1.33121008, 2.11716292, 2.99250424, 1.23532101,
         1.23533743, 3.0486872 , 2.23690912, 1.        , 2.01203443]]])
y = array([4, 3, 6, 8, 1, 2, 9, 7, 0, 5])

    def _fit(self, X, y=None):
        """Fit the shapelet transform to a specified X and y.
    
        Parameters
        ----------
        X: pandas DataFrame or np.ndarray
            The training input samples.
        y: array-like or list
            The class values for X.
    
        Returns
        -------
        self : RandomShapeletTransform
            This estimator.
        """
        # this is a few versions away currently, and heaps dont support the replacement
        warnings.simplefilter("ignore", category=NumbaPendingDeprecationWarning)
    
        self._n_jobs = check_n_jobs(self.n_jobs)
    
        self.classes_, self._class_counts = np.unique(y, return_counts=True)
        self.n_classes = self.classes_.shape[0]
        for index, classVal in enumerate(self.classes_):
            self._class_dictionary[classVal] = index
    
        le = preprocessing.LabelEncoder()
        y = le.fit_transform(y)
    
        self.n_instances, self.n_dims, self.series_length = X.shape
    
        if self.max_shapelets is None:
            self._max_shapelets = min(10 * self.n_instances, 1000)
    
        if self.max_shapelet_length is None:
            self._max_shapelet_length = self.series_length
    
        time_limit = self.time_limit_in_minutes * 60
        start_time = time.time()
        fit_time = 0
    
        max_shapelets_per_class = self._max_shapelets / self.n_classes
        shapelets = [[(-1.0, -1, -1, -1, -1, -1)] for _ in range(self.n_classes)]
        n_shapelets_extracted = 0
    
        if time_limit > 0:
            while (
                fit_time < time_limit
                and n_shapelets_extracted < self.contract_max_n_shapelet_samples
            ):
                candidate_shapelets = Parallel(n_jobs=self._n_jobs)(
                    delayed(self._extract_random_shapelet)(
                        X,
                        y,
                        n_shapelets_extracted + i,
                        shapelets,
                        max_shapelets_per_class,
                    )
                    for i in range(self._batch_size)
                )
    
                for i, heap in enumerate(shapelets):
                    RandomShapeletTransform._merge_shapelets(
                        heap,
                        candidate_shapelets,
                        max_shapelets_per_class,
                        i,
                    )
    
                if self.remove_self_similar:
                    for i, heap in enumerate(shapelets):
                        to_keep = (
                            RandomShapeletTransform._remove_self_similar_shapelets(heap)
                        )
                        shapelets[i] = [n for (n, b) in zip(heap, to_keep) if b]
    
                n_shapelets_extracted += self._batch_size
                fit_time = time.time() - start_time
        else:
            while n_shapelets_extracted < self._n_shapelet_samples:
                n_shapelets_to_extract = (
                    self._batch_size
                    if n_shapelets_extracted + self._batch_size
                    <= self._n_shapelet_samples
                    else self._n_shapelet_samples - n_shapelets_extracted
                )
    
                candidate_shapelets = Parallel(n_jobs=self._n_jobs)(
                    delayed(self._extract_random_shapelet)(
                        X,
                        y,
                        n_shapelets_extracted + i,
                        shapelets,
                        max_shapelets_per_class,
                    )
                    for i in range(n_shapelets_to_extract)
                )
    
                for i, heap in enumerate(shapelets):
                    RandomShapeletTransform._merge_shapelets(
                        heap,
                        candidate_shapelets,
                        max_shapelets_per_class,
                        i,
                    )
    
                if self.remove_self_similar:
                    for i, heap in enumerate(shapelets):
                        to_keep = (
                            RandomShapeletTransform._remove_self_similar_shapelets(heap)
                        )
                        shapelets[i] = [n for (n, b) in zip(heap, to_keep) if b]
    
                n_shapelets_extracted += n_shapelets_to_extract
    
        self.shapelets = [
            (
                s[0],
                s[1],
                s[2],
                s[3],
                s[4],
                self.classes_[s[5]],
                z_normalise_series(X[s[4], s[3], s[2] : s[2] + s[1]]),
            )
            for class_shapelets in shapelets
            for s in class_shapelets
            if s[0] > 0
        ]
        self.shapelets.sort(reverse=True, key=lambda s: (s[0], s[1], s[2], s[3], s[4]))
    
>       to_keep = RandomShapeletTransform._remove_identical_shapelets(self.shapelets)
E       ValueError: cannot compute fingerprint of empty list

X          = array([[[1.96618854, 1.33121008, 2.11716292, 2.99250424, 1.23532101,
         1.23533743, 3.0486872 , 2.23690912, 1.        , 2.01203443]]])
candidate_shapelets = [(0.0, 9, 0, 0, 0, 4), (0.0, 6, 1, 0, 0, 4), (0.0, 9, 0, 0, 0, 4), (0.0, 5, 3, 0, 0, 4), (0.0, 9, 0, 0, 0, 4), (0.0, 4, 2, 0, 0, 4), ...]
classVal   = 3.048687201442344
fit_time   = 0
heap       = [(-1.0, -1, -1, -1, -1, -1)]
i          = 9
index      = 9
le         = LabelEncoder()
max_shapelets_per_class = 0.5
n_shapelets_extracted = 50
n_shapelets_to_extract = 10
self       = RandomShapeletTransform(batch_size=20, max_shapelets=5, n_shapelet_samples=50)
shapelets  = [[(-1.0, -1, -1, -1, -1, -1)], [(-1.0, -1, -1, -1, -1, -1)], [(-1.0, -1, -1, -1, -1, -1)], [(-1.0, -1, -1, -1, -1, -1)], [(-1.0, -1, -1, -1, -1, -1)], [(-1.0, -1, -1, -1, -1, -1)], ...]
start_time = 1659204964.8932788
time_limit = 0.0
to_keep    = [True]
y          = array([4, 3, 6, 8, 1, 2, 9, 7, 0, 5])
```


Discussion:

Resolving patch:
diff --git a/sktime/transformations/panel/shapelet_transform.py b/sktime/transformations/panel/shapelet_transform.py
--- a/sktime/transformations/panel/shapelet_transform.py
+++ b/sktime/transformations/panel/shapelet_transform.py
@@ -1151,7 +1151,8 @@ def _fit(self, X, y=None):
 
         if self.max_shapelets is None:
             self._max_shapelets = min(10 * self.n_instances, 1000)
-
+        if self._max_shapelets < self.n_classes:
+            self._max_shapelets = self.n_classes
         if self.max_shapelet_length is None:
             self._max_shapelet_length = self.series_length
 


Test patch:
diff --git a/sktime/tests/_config.py b/sktime/tests/_config.py
--- a/sktime/tests/_config.py
+++ b/sktime/tests/_config.py
@@ -29,8 +29,6 @@
     "RandomIntervalClassifier",
     "MiniRocket",
     "MatrixProfileTransformer",
-    # RandomShapeletTransform is breaking with empty lists, see #3138
-    "RandomShapeletTransform",
 ]