Shapelet transform - interpretable time-series classification.

Classes

ShapeletTransformClassifier

class algorithms.timeseries.classification.shapelets.ShapeletTransformClassifier(TimeSeriesClassifier)

Classification by which short subsequences a series contains.

A shapelet is a short subsequence whose presence — or absence — separates the classes. Where MiniRocketClassifier is accurate but opaque and DTWNeighborsClassifier compares whole series, this method answers "what does the model actually look for?" with an exhibit: the fitted shapelets are real subsequences from the training data that you can plot next to a series and read.
Constructor
__init__(
    self,
    n_shapelets: int = 100,
    n_candidates: int = 1000,
    min_length: float = 0.05,
    max_length: float = 0.5,
    quality: str = 'f_stat',
    remove_similar: bool = True,
    estimator: Optional[Any] = None,
    random_state: Optional[int] = None,
)

Overview

  1. Sample candidate subsequences at random positions and lengths from
random training series.
  1. For every candidate, compute its distance to every training series —
the smallest z-normalised Euclidean distance to any window.
  1. Score each candidate by how well those distances separate the classes.
  2. Keep the best, discarding candidates that overlap an already-kept one.
  3. Represent each series by its distance to every kept shapelet, and fit a
classifier on that.

Theory

The distance from series X to shapelet S of length m is

d(X, S) = \min_{p} \frac{1}{\sqrt{m}} \left\| \hat{z}(X_{p:p+m}) - S \right\|_2

where \hat{z} z-normalises the window. Normalising each window makes the match invariant to local offset and scale — a shapelet found in a high-amplitude series still matches the same shape at low amplitude — and dividing by \sqrt{m} keeps shapelets of different lengths comparable.

Because the shapelet is z-normalised, the squared distance collapses to 2m - 2\langle X_{p:p+m}, S \rangle / \sigma_p, so the window normalisation never has to be materialised. The C++ kernel uses that, with running sums for \sigma_p.

Exhaustive shapelet search is O(n^2 L^4) and was the method's original obstacle. This class samples n_candidates instead, which is the standard modern remedy and costs little accuracy in practice.

Parameters

n_shapelets
int = 100
Number of shapelets to keep. Also the number of output features.
n_candidates
int = 1000
Candidates sampled before selection. More candidates means a better pool and proportionally more fitting time.
min_length
int or float = 0.05
Shortest candidate. A float is a fraction of the series length.
max_length
int or float = 0.5
Longest candidate. A float is a fraction of the series length.
quality
{'f_stat', 'information_gain'} = 'f_stat'
How candidates are scored. 'f_stat' is a one-way ANOVA on the distances — vectorised over all candidates and much faster; 'information_gain' is the classical criterion, :math:`O(n)` splits per candidate.
remove_similar
bool = True
Whether to discard a candidate that overlaps an already-kept shapelet from the same series. Without this the selection fills up with near duplicates of one strong pattern.
estimator
Classifier
Head fitted on the distance features. Defaults to LogisticRegression.
random_state
int
Seed for candidate sampling.

Attributes

shapelets_
list of np.ndarray
The kept shapelets, z-normalised. Plot these to see what the model looks for.
shapelet_info_
list of dict
For each shapelet: series (training row it came from), start, length, channel and quality.
estimator_
Classifier
The fitted head.
classes_
np.ndarray of shape (n_classes,)
Class labels seen during fit.

Notes

Complexity. Fitting is O(c \, n \, L \, m) for c candidates, n training series of length L and shapelet length m — the candidate scan dominates, and it runs in the shared C++ kernel tuiml._cpp_ext.timeseries.shapelet_distances. Transforming is O(k \, n \, L \, m) for k kept shapelets.

When to use. Choose shapelets when someone will ask why — clinical, industrial or regulatory settings where a prediction has to be defended. Expect to give up some accuracy against MINIROCKET for that; if nobody needs the explanation, MINIROCKET is faster and usually better. Shapelets also suit problems where the class is defined by a local pattern that can appear anywhere in the series, which is exactly what the min-over- windows distance looks for.

Multivariate panels are searched channel by channel, and each shapelet records the channel it came from, so the explanation stays specific.

References

Ye2009
Ye, L., & Keogh, E. (2009). Time Series Shapelets: A New Primitive for Data Mining. ACM SIGKDD, 947-956. :doi:`10.1145/1557019.1557122`
Hills2014
Hills, J., Lines, J., Baranauskas, E., Mapp, J., & Bagnall, A. (2014). Classification of Time Series by Shapelet Transformation. Data Mining and Knowledge Discovery, 28(4), 851-881. :doi:`10.1007/s10618-013-0322-1`
Bostrom2015
Bostrom, A., & Bagnall, A. (2015). Binary Shapelet Transform for Multiclass Time Series Classification. DaWaK, 257-269. :doi:`10.1007/978-3-319-22729-0_20`
python
>>> import numpy as np
>>> from tuiml.algorithms.timeseries.classification import ShapeletTransformClassifier
>>> rng = np.random.default_rng(0)
>>> # Class 1 hides a triangular spike somewhere in the noise; class 0 does not.
>>> X = rng.normal(0, 0.3, (60, 120))
>>> y = np.array([0, 1] * 30)
>>> spike = np.concatenate([np.linspace(0, 3, 8), np.linspace(3, 0, 8)])
>>> for i in np.flatnonzero(y == 1):
...     start = rng.integers(0, 100)
...     X[i, start:start + 16] += spike
>>> model = ShapeletTransformClassifier(
...     n_shapelets=10, n_candidates=200, random_state=0).fit(X, y)
>>> float((model.predict(X) == y).mean())
1.0

The fitted shapelets are real subsequences you can inspect and plot:

python
>>> len(model.shapelets_)
10
>>> sorted(model.shapelet_info_[0])
['channel', 'length', 'quality', 'series', 'start']

Methods

get_parameter_schema (cls) -> Dict[str, Dict[str, Any]]

Return JSON Schema for algorithm parameters.

get_capabilities (cls) -> List[str]

Return supported capabilities.

get_complexity (cls) -> str

Return complexity analysis.

get_references (cls) -> List[str]

Return academic citations.

fit (self, X: np.ndarray, y: np.ndarray) -> 'ShapeletTransformClassifier'

Search for shapelets and fit the head on the distance features.

Parameters
X
np.ndarray of shape (n_samples, n_timepoints) or (n_samples, n_channels, n_timepoints)
Training series.
y
np.ndarray of shape (n_samples,)
Training labels.
Returns
self
ShapeletTransformClassifier
The fitted classifier.
transform (self, X: np.ndarray) -> np.ndarray

Return the shapelet-distance features of a panel.

Parameters
X
np.ndarray of shape (n_samples, n_timepoints) or (n_samples, n_channels, n_timepoints)
Series to transform.
Returns
features
np.ndarray of shape (n_samples, n_shapelets)
Distance from each series to each kept shapelet.
predict (self, X: np.ndarray) -> np.ndarray

Classify each series.

Parameters
X
np.ndarray of shape (n_samples, n_timepoints) or (n_samples, n_channels, n_timepoints)
Series to classify.
Returns
y_pred
np.ndarray of shape (n_samples,)
Predicted labels.
predict_proba (self, X: np.ndarray) -> np.ndarray

Return class probabilities from the head.

Parameters
X
np.ndarray of shape (n_samples, n_timepoints) or (n_samples, n_channels, n_timepoints)
Series to classify.
Returns
proba
np.ndarray of shape (n_samples, n_classes)
Class probabilities as reported by the head.
__repr__ (self) -> str

Return a readable representation of the classifier.