Bag-Of-Receptive-Fields (BORF) turns time series into sparse bag-of-words features. Each signal is cut into sliding windows at many window sizes, word lengths and dilations, every window becomes a SAX word, and the output counts how often each word occurs. The features are meant for linear models and can be mapped back to the parts of the series they come from.
BORF handles univariate and multivariate series, variable lengths, missing values and irregular sampling, and is a scikit-learn transformer.
BORF is now available in the aeon library!
For a more customizable estimator and for XAI, continue below.
pip install "git+https://github.com/fspinna/borf"The original implementation for regularly sampled series is available as release v0.1.0.
import numpy as np
from sklearn.linear_model import RidgeClassifierCV
from sklearn.pipeline import make_pipeline
from fast_borf import BORF
rng = np.random.default_rng(0)
X = rng.standard_normal((100, 1, 200)).cumsum(axis=2) # (n_series, n_signals, n_timestamps)
y = rng.integers(0, 2, 100)
model = make_pipeline(BORF(n_jobs=-1), RidgeClassifierCV())
model.fit(X, y)See examples/ for complete scripts, and
notebooks/classification_tutorial.ipynb
for a step-by-step walkthrough of the process, classification and
explanations, on regular and irregular series (install with the notebooks
extra to run it).
- NumPy array of shape
(n_series, n_signals, n_timestamps). Use NaN for missing values and to pad shorter series. - Ragged awkward array with the same nesting, for series of different lengths.
- Irregular sampling: put the timestamps of each series in an extra last
channel and use
BORF(time_channel=True). Timestamps must be strictly increasing within each series. Without a time channel, observations are treated as evenly spaced.
| parameter | default | meaning |
|---|---|---|
min_window_size, max_window_size |
4, longest series | window sizes, powers of two in this range |
max_word_length |
8 | word lengths are the powers of two from 2 up to this |
alphabet_sizes |
(3,) |
SAX alphabet sizes |
min_dilation, max_dilation |
1, log2(longest series) | dilations, powers of two in this range |
complexity |
"quadratic" |
stride choice: "quadratic" (stride 1), "linear" (stride = word length) or "linear_logarithmic" |
configs |
None |
explicit list of configurations, instead of the parameters above |
min_window_to_signal_std_ratio |
0.0 | windows flatter than this fraction of the signal's standard deviation count as flat |
channel_groups |
None |
"all" or e.g. [[0, 1, 2], [3, 4, 5]]: count each word summed over the channels of each group (not yet supported by the explanations) |
vocabulary |
"fit" |
"fit": a column per word seen during fit; "full": every possible word, so the feature space does not depend on the data |
block_transformer |
None |
scikit-learn transformer applied separately to each configuration's columns |
time_channel |
False |
the last channel holds timestamps |
n_jobs |
1 | numba threads, -1 for all cores |
After fitting:
configs_: the configurations (window size, word length, alphabet size, dilation, stride), in the order of the output columns.config_slices_[i]: the output columns of configurationi.feature_index_: for every output column, its configuration index, signal index (group index withchannel_groups) and SAX word (as an integer in basealphabet_size).
block_transformer fits a copy of any scikit-learn transformer or pipeline on
each configuration's columns, for example to normalize or select features per
configuration:
from sklearn.feature_selection import SelectKBest, chi2
from sklearn.preprocessing import Normalizer
BORF(block_transformer=Normalizer())
BORF(block_transformer=SelectKBest(chi2, k=10)) # 10 words per configurationfeature_index_ and config_slices_ follow columns that the transformer keeps
or selects.
fast_borf.xai.BagOfReceptiveFields maps per-series feature importances back
onto the series. The importances can come from anything: SHAP values, or for a
linear model simply coefficient times feature value.
from fast_borf.xai import BagOfReceptiveFields
explainer = BagOfReceptiveFields(borf).build(X, y_true, y_pred, task="classification")
explainer.add_feature_importance(F) # (n_series, n_features), or (n_classes, n_series, n_features)
explainer.map_contained_feature_importance_to_saliency() # S_: importance per point
explainer.map_notcontained_feature_importance() # F_norm_: importance of absent words
field = explainer.receptive_fields_[explainer.F_avg_rank_argsort_[0]]
field.alignments_indices[i] # (occurrences, word_length, segment_size) points of each segment
field.mappings[i], field.alignments[i] # their values and timestampsReceptive fields are computed when first accessed. fast_borf.core exposes the
underlying steps (segment_means, discretize, window_positions,
panel_words) for custom analyses and plots. See
examples/explanation.py.
pip install -e ".[dev]"
pre-commit install
pytestThe tests in tests/test_reference.py compare against outputs saved from the
original implementation, so refactoring cannot silently change the features.