The Canonical Time-series Characteristics (catch22) transform

Catch22[1] is a collection of 22 time series features extracted from the 7000+ present in the hctsa [2][3] toolbox. A hierarchical clustering was performed on the correlation matrix of features that performed better than random chance to remove redundancy. These clusters were sorted by balanced accuracy using a decision tree classifier and a single feature was selected from the 22 clusters formed, taking into account balanced accuracy results, computational efficiency and interpretability. More about the individual features of catch22 can be learned in the Gitbook of the original creators.

In this notebook, we will demonstrate how to use aeon’s catch22 transformer on the ItalyPowerDemand univariate and BasicMotions multivariate datasets. We will go through the parameters of catch22 and how changing the default values may change results. Catch22 has also been used inside of classification.

1. Transformation

Catch22 is a feature based transformer that extracts 22 features from a time series. The input data can be both univariate and multivariate, without the need to reshape the data. It is most commonly used for interpretability of each time series data. Additionally, as the data of a time series will be reduced to 22 data values, it will increase computational efficiency of machine learning tasks such as clustering, classification, etc.

1.1 Import Data and Catch22

[1]:
import numpy as np

from aeon.datasets import load_basic_motions, load_italy_power_demand
from aeon.transformations.collection.feature_based import Catch22

1.2 Load Data

[2]:
IPD_X_train, IPD_y_train = load_italy_power_demand(split="train")
IPD_X_test, IPD_y_test = load_italy_power_demand(split="test")

print(
    "Italy Power Demand (Univariate): ",
    IPD_X_train.shape,
    IPD_y_train.shape,
    IPD_X_test.shape,
    IPD_y_test.shape,
)

BM_X_train, BM_y_train = load_basic_motions(split="train")
BM_X_test, BM_y_test = load_basic_motions(split="test")

print(
    "Load Basic Motions (Multivarite): ",
    BM_X_train.shape,
    BM_y_train.shape,
    BM_X_test.shape,
    BM_y_test.shape,
)
Italy Power Demand (Univariate):  (67, 1, 24) (67,) (1029, 1, 24) (1029,)
Load Basic Motions (Multivarite):  (40, 6, 100) (40,) (40, 6, 100) (40,)

1.3 Transform the Data

Univariate

[3]:
c22_uv = Catch22()
c22_uv.fit(IPD_X_train)
transformed_data_uv = c22_uv.transform(IPD_X_train)
print(transformed_data_uv.shape)
(67, 22)

Multivariate

Because it’s a multivariate dataset, the feature vector will be of size 22 times the number of channels (in this case 6, so 132 features).

[4]:
c22_mv = Catch22()
c22_mv.fit(BM_X_train)
transformed_data_mv = c22_mv.transform(BM_X_train)
print(transformed_data_mv.shape)
(40, 132)

2. Parameters

aeon’s catch22 includes a number of options that are not present in the original catch22 implementation, which we will talk about in section 2.4. A few of the parameters are shown below with examples, specifically the ones that affect the output. More can be found in catch22’s documentation.

2.1 Features

Catch22 extracts 22 distinct features from a time series. Sometimes you may not need all the features extracted by catch22, instead you may only need some very specific features. By defining a list of feature names, only those specified features will be extracted. The order of these features does matter, as that will be the order of the output. Both the full feature names from hctsa (e.g. "DN_HistogramMode_5") and the short names used in the catch22 paper (e.g. "mode_5") are accepted, as are integer feature indices. aeon’s catch22 documentation lists the 22 features under both names.

[5]:
features_long = ["DN_HistogramMode_5", "CO_f1ecac", "FC_LocalSimple_mean3_stderr"]
features_short = ["mode_5", "acf_timescale", "forecast_error"]

c22_long = Catch22(features=features_long)
c22_long.fit(IPD_X_train)
transformed_data_long = c22_long.transform(IPD_X_train)
print(transformed_data_long.shape)

c22_short = Catch22(features=features_short)
c22_short.fit(IPD_X_train, IPD_y_train)
transformed_data_short = c22_short.transform(IPD_X_train)
print(transformed_data_short.shape)
(67, 3)
(67, 3)

2.2 Catch24

Catch24 extracts 24 features from a time series. The 24 features consist of the 22 features from catch22 with the addition of the mean and standard deviation of the time series. In certain tasks, catch24 may outperform catch22. For example in [4], catch24 significally outperformed catch22 in cross-domain anomaly detection.

Catch22 extracts the most important features for machine learning tasks and therefore is more widely used.

[6]:
c24 = Catch22(catch24=True)
data_c24 = c24.fit_transform(IPD_X_train)
print(data_c24.shape)
(67, 24)

2.3 Replace NaNs

You may find that some time series cannot extract certain features from it. This may happen when division by zero occurs, or the input value is zero. Simply, it means we cannot extract the feature from the time series. However, we may still want a number for calculations and therefore ‘replace_nans’ allows us to replace NaN with zero.

[7]:
training_data = np.array([[0, 0, 0, 0, 0, 0]])

c22_nan = Catch22()
data_nan = c22_nan.fit_transform(training_data)
print(f"Data with NaN: {data_nan[0]}\n")

c22_no_nan = Catch22(replace_nans=True)
data_no_nan = c22_no_nan.fit_transform(training_data)
print("Data with no NaN: ", data_no_nan[0])
Data with NaN: [        nan         nan  6.          6.          0.          0.
  0.          1.          0.33333333  0.          0.                 nan
  1.          0.          0.          0.          1.         -0.
         nan         nan         nan  0.        ]

Data with no NaN:  [ 0.          0.          6.          6.          0.          0.
  0.          1.          0.33333333  0.          0.          0.
  1.          0.          0.          0.          1.         -0.
  0.          0.          0.          0.        ]

3. Performance

aeon’s catch22 is heavily optimised: it is around 3-6x faster than in previous aeon releases and 6-10x faster than the C pycatch22 package on typical collections. The gap grows with the number of cases. Briefly, the main optimisations are:

  • Shared intermediates. Several features need the same expensive quantities: the series minimum, maximum and mean, the Fourier power spectrum, and the autocorrelation with its first zero crossing. These are computed once per series and shared by every feature that uses them, where the reference implementation recomputes them per feature.

  • Batched FFTs. For equal-length collections, the FFTs behind the spectral and autocorrelation features are computed for the whole collection in single vectorised numpy calls rather than one series at a time.

  • A compiled dispatch loop. The loop over series and features is itself numba-compiled, so per-feature call overhead is negligible. This matters most for short series, such as the intervals extracted by interval-based forests like DrCIF, where catch22 extraction dominates runtime.

  • Faster feature algorithms. A few features were rewritten with better algorithms that produce the same output: the two outlier-timing features maintain an incremental running median instead of repeatedly rescanning, whiten_timescale stops at the first autocovariance sign change instead of computing a full autocorrelation, and the fluctuation-analysis features (rs_range, dfa) use closed-form least squares.

A quick illustration on the BasicMotions train set:

[9]:
import timeit

c22 = Catch22()
c22.fit_transform(BM_X_train)  # first call includes numba compilation

runtime = min(timeit.repeat(lambda: c22.fit_transform(BM_X_train), number=1, repeat=3))
print(f"22 features x 6 channels x 40 cases extracted in {runtime:.3f} s")
22 features x 6 channels x 40 cases extracted in 0.022 s

To put those optimisations in context, the table and plot below compare four implementations transforming random univariate collections of length 400 with an increasing number of cases: this version of aeon, the previous aeon implementation, the C pycatch22 package, and sktime’s catch22. Times are in seconds (best of 3) and the multiplier is the slowdown relative to aeon. These figures were generated offline with a benchmarking script, since running them requires the pycatch22 and sktime packages and a copy of the old aeon code.

n_cases

aeon

previous aeon

pycatch22 (C)

sktime

50

0.014

0.048 (3.4x)

0.123 (8.7x)

0.253 (17.8x)

100

0.030

0.097 (3.2x)

0.262 (8.6x)

0.590 (19.5x)

250

0.076

0.246 (3.2x)

0.876 (11.5x)

1.273 (16.7x)

500

0.168

0.485 (2.9x)

1.257 (7.5x)

2.539 (15.1x)

1000

0.292

0.954 (3.3x)

2.442 (8.4x)

4.927 (16.8x)

2000

0.591

1.919 (3.2x)

4.906 (8.3x)

10.318 (17.5x)

4000

1.198

3.849 (3.2x)

9.941 (8.3x)

19.922 (16.6x)

Runtime of four catch22 implementations for increasing collection size

When comparing outputs across packages, note that pycatch22 z-normalises each series internally while aeon computes on the data as given, so several feature values differ between the two unless you z-normalise the input yourself. sktime output differs from aeon and pycatch22 because of bugs we have fixed since the split.

4. References:

[1] Lubba, C. H., Sethi, S. S., Knaute, P., Schultz, S. R., Fulcher, B. D., & Jones, N. S. (2019). catch22: CAnonical Time-series CHaracteristics. Data Mining and Knowledge Discovery, 33(6), 1821-1852.

[2] Fulcher, B. D., & Jones, N. S. (2017). hctsa: A computational framework for automated time-series phenotyping using massive feature extraction. Cell systems, 5(5), 527-531.

[3] Fulcher, B. D., Little, M. A., & Jones, N. S. (2013). Highly comparative time-series analysis: the empirical structure of time series and their methods. Journal of the Royal Society Interface, 10(83), 20130048.

[4] Agrahari, R., Nicholson, M., Conran, C., Assem, H. and Kelleher, J.D., 2022. Assessing feature representations for instance-based cross-domain anomaly detection in cloud services univariate time series data. IoT, 3(1), pp.123-144.


Generated using nbsphinx. The Jupyter notebook can be found here.

binder