Metadata-Version: 2.4
Name: fast-borf
Version: 0.2.1
Summary: Bag-Of-Receptive-Fields (BORF) features for regular and irregular time series
Author: Francesco Spinnato
License-Expression: MIT
Project-URL: Repository, https://github.com/fspinna/borf
Project-URL: Paper, https://doi.org/10.1109/ACCESS.2024.3464743
Project-URL: Paper (irregular time series), https://arxiv.org/abs/2609.39268
Project-URL: Issues, https://github.com/fspinna/borf/issues
Keywords: time series,time series classification,irregular time series,explainable AI,interpretability,SAX,bag of words,scikit-learn
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: awkward>=2.6
Requires-Dist: numba>=0.60
Requires-Dist: numpy>=1.26
Requires-Dist: scikit-learn>=1.6
Requires-Dist: scipy>=1.13
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Provides-Extra: notebooks
Requires-Dist: aeon>=1.0; extra == "notebooks"
Requires-Dist: jupyter; extra == "notebooks"
Requires-Dist: matplotlib>=3.9; extra == "notebooks"
Requires-Dist: pyrregular>=0.1.10; extra == "notebooks"
Requires-Dist: seaborn>=0.13; extra == "notebooks"
Requires-Dist: shap>=0.48; extra == "notebooks"
Provides-Extra: dev
Requires-Dist: fast-borf[test]; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
Dynamic: license-file

# fast-borf

[![PyPI](https://img.shields.io/pypi/v/fast-borf)](https://pypi.org/project/fast-borf/)
[![Tests](https://github.com/fspinna/borf/actions/workflows/tests.yml/badge.svg)](https://github.com/fspinna/borf/actions/workflows/tests.yml)
[![IEEE Access](https://img.shields.io/badge/IEEE%20Access-10.1109%2FACCESS.2024.3464743-blue)](https://doi.org/10.1109/ACCESS.2024.3464743)
[![arXiv](https://img.shields.io/badge/arXiv-2609.39268-b31b1b)](https://arxiv.org/abs/2609.39268)
[![License: MIT](https://img.shields.io/badge/License-MIT-green)](LICENSE)

Official implementation of **Bag-Of-Receptive-Fields (BORF)**, from the papers
[*Fast, Interpretable, and Deterministic Time Series Classification With a
Bag-of-Receptive-Fields*](https://doi.org/10.1109/ACCESS.2024.3464743) (IEEE
Access, 2024) and [*A Time-Aware Bag-of-Receptive-Fields for Interpretable
Irregular Time Series Classification*](https://arxiv.org/abs/2609.39268)
(arXiv, 2026).

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. Compared
with the [aeon](#borf-in-aeon) version, this package adds explanations
(saliency maps and receptive fields), irregularly sampled series, channel
groups, blockwise transformers and a fixed vocabulary option.

## Installation

```bash
pip install fast-borf
```

The original implementation for regularly sampled series is available as
release [v0.1.0](https://github.com/fspinna/borf/releases/tag/v0.1.0).

## Quick start

```python
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/`](examples) for complete scripts, and
[`notebooks/classification_tutorial.ipynb`](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).

## Input formats

- **NumPy array** of shape `(n_series, n_signals, n_timestamps)`. Use NaN for
  missing values and to pad shorter series.
- **Ragged [awkward](https://awkward-array.org) 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.

## Main parameters

| 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 |

## Inspecting the features

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 configuration `i`.
- `feature_index_`: for every output column, its configuration index, signal
  index (group index with `channel_groups`) and SAX word (as an integer in
  base `alphabet_size`).

## Blockwise processing

`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:

```python
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 configuration
```

`feature_index_` and `config_slices_` follow columns that the transformer keeps
or selects.

## Explanations

`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.

```python
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 timestamps
```

Receptive 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`](examples/explanation.py).

## BORF in aeon

A BORF transformer for regularly sampled series is also available in the
[aeon](https://www.aeon-toolkit.org/en/stable/api_reference/auto_generated/aeon.transformations.collection.dictionary_based.BORF.html)
toolkit.

## Development

```bash
pip install -e ".[dev]"
pre-commit install
pytest
```

The tests in `tests/test_reference.py` compare against outputs saved from the
original implementation, so refactoring cannot silently change the features.

## Citation

If you use BORF, please cite:

```bibtex
@article{spinnato2024borf,
  title   = {Fast, Interpretable, and Deterministic Time Series Classification With a Bag-of-Receptive-Fields},
  author  = {Spinnato, Francesco and Guidotti, Riccardo and Monreale, Anna and Nanni, Mirco},
  journal = {IEEE Access},
  volume  = {12},
  pages   = {137893--137912},
  year    = {2024},
  doi     = {10.1109/ACCESS.2024.3464743}
}
```

For irregularly sampled series (`time_channel=True`), please also cite:

```bibtex
@misc{spinnato2026timeawareborf,
  title         = {A Time-Aware Bag-of-Receptive-Fields for Interpretable Irregular Time Series Classification},
  author        = {Spinnato, Francesco},
  year          = {2026},
  eprint        = {2609.39268},
  archivePrefix = {arXiv},
  primaryClass  = {cs.LG},
  url           = {https://arxiv.org/abs/2609.39268}
}
```
