saxpy 1.0.1.dev167__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. saxpy-2.0.0/.gitignore +27 -0
  2. saxpy-2.0.0/CHANGELOG.md +73 -0
  3. saxpy-2.0.0/PKG-INFO +380 -0
  4. saxpy-2.0.0/README.md +353 -0
  5. saxpy-2.0.0/pyproject.toml +77 -0
  6. saxpy-2.0.0/saxpy/__init__.py +11 -0
  7. saxpy-2.0.0/saxpy/alphabet.py +26 -0
  8. saxpy-2.0.0/saxpy/discord.py +99 -0
  9. {saxpy-1.0.1.dev167 → saxpy-2.0.0}/saxpy/distance.py +5 -4
  10. saxpy-2.0.0/saxpy/hotsax.py +206 -0
  11. saxpy-2.0.0/saxpy/paa.py +78 -0
  12. saxpy-2.0.0/saxpy/repair.py +408 -0
  13. saxpy-2.0.0/saxpy/rra.py +367 -0
  14. saxpy-2.0.0/saxpy/sax.py +292 -0
  15. saxpy-2.0.0/saxpy/saxvsm.py +320 -0
  16. saxpy-2.0.0/saxpy/saxvsm_optimize.py +345 -0
  17. {saxpy-1.0.1.dev167 → saxpy-2.0.0}/saxpy/strfunc.py +1 -1
  18. saxpy-2.0.0/saxpy/util.py +34 -0
  19. saxpy-2.0.0/saxpy/visit_registry.py +67 -0
  20. saxpy-2.0.0/saxpy/znorm.py +46 -0
  21. saxpy-1.0.1.dev167/.travis.yml +0 -23
  22. saxpy-1.0.1.dev167/AUTHORS +0 -1
  23. saxpy-1.0.1.dev167/ChangeLog +0 -169
  24. saxpy-1.0.1.dev167/MANIFEST.in +0 -1
  25. saxpy-1.0.1.dev167/PKG-INFO +0 -94
  26. saxpy-1.0.1.dev167/README.md +0 -187
  27. saxpy-1.0.1.dev167/README.rst +0 -76
  28. saxpy-1.0.1.dev167/conda.recipe/meta.yaml +0 -29
  29. saxpy-1.0.1.dev167/data/ecg0606_1.csv +0 -2299
  30. saxpy-1.0.1.dev167/data/insect.txt +0 -18667
  31. saxpy-1.0.1.dev167/jupyter/.ipynb_checkpoints/insect-checkpoint.ipynb +0 -158
  32. saxpy-1.0.1.dev167/jupyter/.ipynb_checkpoints/paa-checkpoint.ipynb +0 -393
  33. saxpy-1.0.1.dev167/jupyter/.ipynb_checkpoints/znorm-checkpoint.ipynb +0 -198
  34. saxpy-1.0.1.dev167/jupyter/discord.ipynb +0 -173
  35. saxpy-1.0.1.dev167/jupyter/distance.ipynb +0 -170
  36. saxpy-1.0.1.dev167/jupyter/hotsax.ipynb +0 -173
  37. saxpy-1.0.1.dev167/jupyter/insect.ipynb +0 -409
  38. saxpy-1.0.1.dev167/jupyter/paa.ipynb +0 -391
  39. saxpy-1.0.1.dev167/jupyter/sax.ipynb +0 -383
  40. saxpy-1.0.1.dev167/jupyter/str_func.ipynb +0 -128
  41. saxpy-1.0.1.dev167/jupyter/tinkah.ipynb +0 -145
  42. saxpy-1.0.1.dev167/jupyter/visit_registry.ipynb +0 -115
  43. saxpy-1.0.1.dev167/jupyter/znorm.ipynb +0 -198
  44. saxpy-1.0.1.dev167/requirements.txt +0 -4
  45. saxpy-1.0.1.dev167/saxpy/__init__.py +0 -5
  46. saxpy-1.0.1.dev167/saxpy/alphabet.py +0 -98
  47. saxpy-1.0.1.dev167/saxpy/discord.py +0 -83
  48. saxpy-1.0.1.dev167/saxpy/hotsax.py +0 -144
  49. saxpy-1.0.1.dev167/saxpy/paa.py +0 -29
  50. saxpy-1.0.1.dev167/saxpy/sax.py +0 -76
  51. saxpy-1.0.1.dev167/saxpy/visit_registry.py +0 -58
  52. saxpy-1.0.1.dev167/saxpy/znorm.py +0 -11
  53. saxpy-1.0.1.dev167/saxpy.egg-info/PKG-INFO +0 -94
  54. saxpy-1.0.1.dev167/saxpy.egg-info/SOURCES.txt +0 -57
  55. saxpy-1.0.1.dev167/saxpy.egg-info/dependency_links.txt +0 -1
  56. saxpy-1.0.1.dev167/saxpy.egg-info/not-zip-safe +0 -1
  57. saxpy-1.0.1.dev167/saxpy.egg-info/pbr.json +0 -1
  58. saxpy-1.0.1.dev167/saxpy.egg-info/requires.txt +0 -4
  59. saxpy-1.0.1.dev167/saxpy.egg-info/top_level.txt +0 -1
  60. saxpy-1.0.1.dev167/setup.cfg +0 -25
  61. saxpy-1.0.1.dev167/setup.py +0 -9
  62. saxpy-1.0.1.dev167/site/citation.bib +0 -20
  63. saxpy-1.0.1.dev167/tests/test_cuts.py +0 -8
  64. saxpy-1.0.1.dev167/tests/test_cuts0.py +0 -8
  65. saxpy-1.0.1.dev167/tests/test_discord_bruteforce.py +0 -11
  66. saxpy-1.0.1.dev167/tests/test_discord_hotsax.py +0 -16
  67. saxpy-1.0.1.dev167/tests/test_distance.py +0 -28
  68. saxpy-1.0.1.dev167/tests/test_paa.py +0 -36
  69. saxpy-1.0.1.dev167/tests/test_registry.py +0 -26
  70. saxpy-1.0.1.dev167/tests/test_sax_chunking.py +0 -28
  71. saxpy-1.0.1.dev167/tests/test_sax_window.py +0 -43
  72. saxpy-1.0.1.dev167/tests/test_str.py +0 -16
  73. saxpy-1.0.1.dev167/tests/test_ts2string.py +0 -29
  74. saxpy-1.0.1.dev167/tests/test_znorm.py +0 -22
  75. saxpy-1.0.1.dev167/tox.ini +0 -8
  76. {saxpy-1.0.1.dev167 → saxpy-2.0.0}/LICENSE +0 -0
saxpy-2.0.0/.gitignore ADDED
@@ -0,0 +1,27 @@
1
+ .idea/workspace.xml
2
+ build/
3
+ dist/
4
+ _build/
5
+ _generate/
6
+ .cache*
7
+ .tox*
8
+ .eggs*
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+ .pytest_cache/
12
+ ChangeLog
13
+ *.so
14
+ *.py[cod]
15
+ *.egg-info
16
+ .coverage
17
+ .pypirc
18
+ uv.lock
19
+ jupyter/.ipynb*
20
+ /.venv/
21
+ /.vscode/
22
+ .idea/
23
+
24
+ # Internal benchmarking / research work product (harnesses, reports, profiling,
25
+ # cross-impl studies) — kept local, never published with the OSS library.
26
+ benchmarks/
27
+ saxpy_benchmark_report.html
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ All notable changes to saxpy are documented here. This project adheres to
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [2.0.0] — 2026-06
7
+
8
+ First modern PyPI release. (The only artifact previously on PyPI was the legacy
9
+ `1.0.1.dev167` pbr snapshot; 2.0.0 supersedes it.) This release modernizes the
10
+ build, adds a grammar layer and a SAX-VSM classifier, and aligns results with the
11
+ sibling jMotif implementations (R and Java).
12
+
13
+ ### Added
14
+ - **SAX-VSM time-series classification.** A classifier layer in `saxpy.saxvsm`
15
+ (`load_ucr_data`, `train_tfidf`, `classify_series`, `classification_accuracy`)
16
+ built on the existing word-bag / TF·IDF primitives. Verified accuracy 1.0 on the
17
+ Cylinder-Bell-Funnel demo set.
18
+ - **DIRECT cross-validation parameter optimizer** (`saxpy.saxvsm_optimize`):
19
+ `optimize_parameters` selects window / PAA / alphabet by minimizing leave-out CV
20
+ error on the training set via `scipy.optimize.direct` (mirrors the jmotif-R and
21
+ Java DiRect recipes). Deterministic with `random_state`; `cv_error` exposes the
22
+ objective.
23
+ - **Configurable TF·IDF weighting.** `bags_to_tfidf(tf_scheme=...)` — `"log1p"`
24
+ (default, `ln(1 + tf)`) or `"smart"` (`1 + ln(tf)`).
25
+ - **Grammar layer.** RePair grammar inference (`saxpy.repair.str_to_repair_grammar`)
26
+ and RRA variable-length discord discovery (`saxpy.rra.find_discords_rra`), ported
27
+ from the C++/Java implementations.
28
+ - **Reproducibility.** Optional `random_state` for HOT-SAX, brute-force, and RRA
29
+ discord search, so results are reproducible across runs and across the
30
+ Python / R / Java implementations.
31
+ - `cosine_distance` — correctly-named accessor for the per-class cosine distance.
32
+
33
+ ### Changed
34
+ - **Cross-implementation alignment.** The TF·IDF weighting is standardized to
35
+ `log1p` (`ln(1 + tf) · ln(N / df)`) across saxpy, jmotif-R, and the Java
36
+ `sax-vsm_classic`; SAX-VSM is verified 3-way identical on CBF (900/900).
37
+ - **RRA discord search**: skip degenerate sub-`paa_size` zero-coverage intervals;
38
+ order candidates by rule frequency (matches GrammarViz).
39
+ - **Packaging & tooling**: migrated from pbr to Hatchling, adopted `uv` +
40
+ `pyproject.toml`, ruff + mypy gates, and a 3.10/3.11/3.12 × linux/macOS/windows CI
41
+ matrix. The version is single-sourced from `pyproject.toml`.
42
+ - **Minimum Python is now 3.10.**
43
+
44
+ ### Performance
45
+ - Brute-force discord search: vectorized the nearest-neighbour distance (one
46
+ numpy pass per candidate instead of a per-neighbour pure-Python early-abandon
47
+ loop), ~90× faster on a full ECG series with identical discords. It now
48
+ computes every candidate's exact NN, so it no longer depends on visit order
49
+ (`random_state` is retained but inert).
50
+ - RePair: bucketed (count-indexed) priority queue (Larsson–Moffat), ~2.8× faster on
51
+ the Python side.
52
+ - RRA: cached reference z-norm + lightweight 1-D z-norm (~3–6× faster).
53
+ - PAA: vectorized (`reshape` / `repeat`-and-mean).
54
+
55
+ ### Fixed
56
+ - Adversarial-audit hardening across the SAX / discord / VSM core (divide-by-zero
57
+ guards, on-breakpoint symbol canonicalization, PAA boundary and up-sampling
58
+ validation, deterministic lowest-index discord tie-break, and more).
59
+ - RRA `_paa2` float-boundary correctness, and z-normalized discord distance to match
60
+ HOT-SAX / the paper.
61
+ - Packaging hygiene: an explicit sdist allowlist, so the published source
62
+ distribution ships only the package and user-facing docs (no dev/internal files
63
+ or test datasets).
64
+
65
+ ### Deprecated
66
+ - `cosine_similarity` is a misnomer — it returns cosine *distance* (`1 - cosine`),
67
+ not similarity. It is kept as an alias for `cosine_distance` and may be removed in
68
+ a future release.
69
+
70
+ ### Docs
71
+ - README rewritten as the user-facing tutorial: runnable examples across the full
72
+ stack, a SAX-VSM section, the cross-implementation alignment notes, and algorithm
73
+ performance tables.
saxpy-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,380 @@
1
+ Metadata-Version: 2.4
2
+ Name: saxpy
3
+ Version: 2.0.0
4
+ Summary: SAX, HOT-SAX, and SAX-VSM implementations for time series symbolic discretization in Python
5
+ Project-URL: Homepage, https://github.com/seninp/saxpy
6
+ Project-URL: Repository, https://github.com/seninp/saxpy
7
+ Project-URL: Changelog, https://github.com/seninp/saxpy/blob/master/CHANGELOG.md
8
+ Author-email: Pavel Senin <seninp@gmail.com>
9
+ License-Expression: GPL-2.0-only
10
+ License-File: LICENSE
11
+ Keywords: data-mining,discord,discretization,hot-sax,motif,sax,time-series
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: numpy
24
+ Requires-Dist: scikit-learn
25
+ Requires-Dist: scipy
26
+ Description-Content-Type: text/markdown
27
+
28
+ Time series symbolic discretization with SAX
29
+ ====
30
+ [![Latest PyPI version](https://img.shields.io/pypi/v/saxpy.svg)](https://pypi.python.org/pypi/saxpy)
31
+ [![image](http://img.shields.io/:license-gpl2-green.svg)](http://www.gnu.org/licenses/gpl-2.0.html)
32
+
33
+
34
+ This code is released under [GPL v.2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) and implements in Python the SAX toolkit and the algorithms built on top of it:
35
+ * **Symbolic Aggregate approXimation (SAX)** -- discretizing a time series into a string, with z-Normalization and PAA [1]
36
+ * **HOT-SAX** -- an algorithm for the exact time series discord (anomaly) discovery [3]
37
+ * **RePair** -- a grammar-inference algorithm run over the SAX representation [7]
38
+ * **RRA (Rare Rule Anomaly)** -- a grammar-based, variable-length discord discovery algorithm built on RePair [8]
39
+ * **SAX-VSM** -- an algorithm for interpretable time series classification (and its discretization parameters optimization) [5]
40
+ * **SAX-ENERGY**, **SAX-REPEAT**, **SAX-INDEPENDENT** -- extensions of SAX to multi-dimensional time series [4]
41
+
42
+ Note that most of this functionality is also implemented in [R](https://github.com/jMotif/jmotif-R) (the `jmotif` package on CRAN) and in [Java](https://github.com/jMotif/SAX); the SAX-VSM classifier specifically also lives in the Java [sax-vsm_classic](https://github.com/jMotif/sax-vsm_classic) project, and the grammar-based anomaly work in [GrammarViz](https://github.com/GrammarViz2/grammarviz2_src). These implementations are kept aligned -- see the next section.
43
+
44
+ #### Cross-implementation alignment
45
+
46
+ The Python (saxpy), R, and Java implementations are kept aligned: the SAX stack -- z-Normalization, PAA, Gaussian breakpoints, symbol assignment -- and the HOT-SAX / brute-force discord search produce the same results across all three to floating-point precision. The shared conventions are:
47
+
48
+ * z-Normalization uses the **population** standard deviation (divide by `n`), matching the Matrix Profile / MASS convention, so each window has empirical variance exactly 1 (the assumption behind SAX's equiprobable breakpoints);
49
+ * PAA uses **fractional** segment boundaries (a sample straddling a segment edge is split by overlap);
50
+ * a value falling exactly on a breakpoint maps to the symbol **above** the cut;
51
+ * all distance-based discord search compares **z-normalized** subsequences -- HOT-SAX, brute-force, *and* RRA key on shape, not amplitude; HOT-SAX and brute-force additionally break distance ties by the lowest index, so their results are reproducible regardless of search order.
52
+
53
+ RRA agrees with the others on the discord *region* (e.g. the ecg0606 anomaly at position 430), but its reported nearest-neighbour distance is a search-order-dependent *approximation*: the rarest-first search early-abandons as soon as a close-enough neighbour is found, so the exact distance value (not the position) can differ by a few percent between implementations whose random visit orders differ. This is inherent to the heuristic, not a convention gap.
54
+
55
+ **SAX-VSM** is aligned too. The TF\*IDF weight uses **log1p term frequency, `ln(1 + tf)`, and a natural-log IDF, `ln(N / df)`** in saxpy, jmotif-R, and the Java `sax-vsm_classic` (which previously used the SMART `1 + ln(tf)` / `log10` scheme). A cross-implementation accuracy study (CBF, Gun_Point, Coffee, Beef, OSULeaf, Adiac) found `log1p` ties or beats SMART at the tuned operating point on every dataset and wins more parameter points overall, so `log1p` is now canonical. Note that the IDF *base* (`ln` vs `log10`) is a uniform per-word factor that cancels in the cosine -- it never affects a classification, only the printed weight magnitudes; the TF nonlinearity is the only behavioural lever. On the shared Cylinder-Bell-Funnel set all three score identically (900/900 at window 60 / PAA 8 / alphabet 6, EXACT numerosity reduction).
56
+
57
+ #### Installation
58
+ saxpy is published on PyPI, install it with `pip` (or `uv pip`):
59
+
60
+ $ pip install saxpy
61
+
62
+ saxpy requires Python 3.10+ and depends on `numpy`, `scipy`, and `scikit-learn`. The example data used in this README (`data/ecg0606_1.csv`, `resources/data/cbf/`) ships in the source tree, so the file-reading examples below assume you are running from a repository clone; a `pip`-installed wheel contains the code only.
63
+
64
+ #### Development
65
+ The project uses [uv](https://docs.astral.sh/uv/) for environment management and packaging (PEP 621 / `pyproject.toml`, Hatchling build backend). From a clone:
66
+
67
+ $ uv sync # create the venv and install saxpy + dev tools
68
+ $ uv run pytest # run the test suite (unit tests + doctests)
69
+ $ uv run ruff check # lint
70
+ $ uv run ruff format # format
71
+ $ uv run mypy saxpy # type-check
72
+
73
+ The tests run across all supported interpreters via `uv run --python 3.10/3.11/3.12 pytest`; `uv build` produces a wheel + sdist in `dist/`; `uv run pre-commit install` enables the ruff + mypy hooks.
74
+
75
+ SAX in a nutshell
76
+ ------------
77
+ SAX transforms a sequence of rational numbers (i.e., a time series) into a sequence of letters (i.e., a string). An illustration of a 128-point time series converted into a word of 8 letters:
78
+
79
+ ![SAX in a nutshell](https://raw.githubusercontent.com/jMotif/SAX/master/src/resources/sax_transform.png)
80
+
81
+ As discretization is probably the most used transformation in data mining, SAX has been widely used throughout the field. Find more information about SAX at its authors' pages: [SAX overview by Jessica Lin](http://cs.gmu.edu/~jessica/sax.htm), [Eamonn Keogh's SAX page](http://www.cs.ucr.edu/~eamonn/SAX.htm), or at the [sax-vsm wiki page](http://jmotif.github.io/sax-vsm_site/morea/algorithm/SAX.html).
82
+
83
+ #### 1.0 Simple time series to SAX conversion
84
+ To convert a time series of an arbitrary length to SAX we first need to define the alphabet cuts. Saxpy retrieves cuts for a Normal alphabet (we use size 3 here) via `cuts_for_asize`:
85
+
86
+ from saxpy.alphabet import cuts_for_asize
87
+ cuts_for_asize(3)
88
+
89
+ which yields an array (the leading `-inf` is the lower sentinel, so the array has `alphabet_size` entries):
90
+
91
+ array([ -inf, -0.4307273, 0.4307273])
92
+
93
+ To convert a time series to letters we use `ts_to_string`, not forgetting to z-Normalize the input first:
94
+
95
+ import numpy as np
96
+ from saxpy.znorm import znorm
97
+ from saxpy.sax import ts_to_string
98
+
99
+ ts_to_string(znorm(np.array([-2, 0, 2, 0, -1])), cuts_for_asize(3))
100
+
101
+ which produces a string:
102
+
103
+ 'abcba'
104
+
105
+ #### 2.0 SAX conversion with PAA aggregation (i.e., "chunking")
106
+ In order to reduce dimensionality further, PAA (Piecewise Aggregate Approximation) is usually applied before SAX. PAA splits the series into equally-sized segments and averages the points within each, so the example above reduces from five points to three letters:
107
+
108
+ import numpy as np
109
+ from saxpy.znorm import znorm
110
+ from saxpy.paa import paa
111
+ from saxpy.sax import ts_to_string
112
+ from saxpy.alphabet import cuts_for_asize
113
+
114
+ dat = np.array([-2, 0, 2, 0, -1])
115
+ dat_paa_3 = paa(znorm(dat), 3)
116
+
117
+ ts_to_string(dat_paa_3, cuts_for_asize(3))
118
+
119
+ and a three-letter string is produced:
120
+
121
+ 'acb'
122
+
123
+ The two steps are also available as a single call, `sax_by_chunking(series, paa_size, alphabet_size, znorm_threshold=0.01)`, which z-Normalizes, PAA-reduces, and discretizes in one shot:
124
+
125
+ from saxpy.sax import sax_by_chunking
126
+
127
+ sax_by_chunking(np.array([-2., 0, 2, 0, -1, 1, 3, 1, 0]), paa_size=3, alphabet_size=3)
128
+ # 'bbc'
129
+
130
+ #### 3.0 SAX conversion via a sliding window
131
+ Typically, to investigate the structure of a time series -- to discover anomalous (i.e., discords) and recurrent (i.e., motifs) patterns -- we convert it to SAX via a sliding window. Saxpy implements this workflow in `sax_via_window`:
132
+
133
+ import numpy as np
134
+ from saxpy.sax import sax_via_window
135
+
136
+ dat = np.array([0., 0., 0., 0., 0., -0.270340178359072, -0.367828308500142,
137
+ 0.666980581124872, 1.87088147328446, 2.14548907684624,
138
+ -0.480859313143032, -0.72911654245842, -0.490308602315934,
139
+ -0.66152028906509, -0.221049033806403, 0.367003418871239,
140
+ 0.631073992586373, 0.0487728723414486, 0.762655178750436,
141
+ 0.78574757843331, 0.338239686422963, 0.784206454089066,
142
+ -2.14265084073625, 2.11325193044223, 0.186018356196443,
143
+ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.519132472499234,
144
+ -2.604783141655, -0.244519550114012, -1.6570790528784,
145
+ 3.34184602886343, 2.10361226260999, 1.9796808733979,
146
+ -0.822247322003058, 1.06850578033292, -0.678811824405992,
147
+ 0.804225748913681, 0.57363964388698, 0.437113583759113,
148
+ 0.437208643628268, 0.989892093383503, 1.76545983424176,
149
+ 0.119483882364649, -0.222311941138971, -0.74669456611669,
150
+ -0.0663660879732063, 0., 0., 0., 0., 0.,])
151
+
152
+ sax_via_window(dat, win_size=6, paa_size=3, alphabet_size=3, nr_strategy=None, znorm_threshold=0.01)
153
+
154
+ the result maps each SAX word to the list of start positions where it occurred:
155
+
156
+ defaultdict(list,
157
+ {'aac': [4, 10, 11, 30, 35],
158
+ 'abc': [12, 14, 36, 44],
159
+ 'acb': [5, 16, 21, 37, 43],
160
+ 'acc': [13, 52, 53, 54],
161
+ 'bac': [3, 19, 34, 45, 51],
162
+ 'bba': [31],
163
+ 'bbb': [15, 18, 20, 22, 25, 26, 27, 28, 29, 41, 42, 46],
164
+ 'bbc': [2],
165
+ 'bca': [6, 17, 32, 38, 47, 48],
166
+ 'caa': [8, 23, 24, 40],
167
+ 'cab': [9, 50],
168
+ 'cba': [7, 39, 49],
169
+ 'cbb': [33],
170
+ 'cca': [0, 1]})
171
+
172
+ `sax_via_window` is the hub almost everything downstream calls. Its full signature:
173
+
174
+ def sax_via_window(series, win_size, paa_size, alphabet_size=3,
175
+ nr_strategy='exact', znorm_threshold=0.01, sax_type='unidim')
176
+
177
+ `nr_strategy` is the numerosity reduction strategy (`'exact'`, `'mindist'`, or `None`) that collapses runs of the same/near-identical consecutive word. `sax_type` selects the per-window strategy: `'unidim'` (the default, for 1-D series) and the three multi-dimensional modes below.
178
+
179
+ #### 3.1 Multi-dimensional SAX (SAX-ENERGY / SAX-REPEAT / SAX-INDEPENDENT)
180
+ For a multi-dimensional series (an `n x d` array of `n` timestamps over `d` dimensions), `sax_type` selects how the dimensions are combined [4]:
181
+
182
+ * `'independent'` -- SAX-encode each dimension on its own and concatenate the per-dimension words;
183
+ * `'energy'` -- z-Normalize across dimensions and aggregate the per-dimension energy;
184
+ * `'repeat'` -- run standard SAX per dimension, then k-means-cluster the multi-dimensional words into the requested alphabet.
185
+
186
+ For example, on a 12-point, 2-dimensional series with the independent mode:
187
+
188
+ import numpy as np
189
+ from saxpy.sax import sax_via_window
190
+
191
+ mts = np.column_stack([np.sin(np.linspace(0, 3, 12)),
192
+ np.cos(np.linspace(0, 3, 12))])
193
+
194
+ sax_via_window(mts, win_size=6, paa_size=3, alphabet_size=3,
195
+ nr_strategy="none", znorm_threshold=0.01, sax_type="independent")
196
+ # {'abccba': [0, 1], 'acccba': [2], 'acbcba': [3], 'ccacba': [4], 'cbacba': [5, 6]}
197
+
198
+ each six-letter word is the 3-letter word of dimension 0 followed by that of dimension 1.
199
+
200
+ #### 4.0 Time series discord discovery with HOT-SAX
201
+ Saxpy implements the HOT-SAX discord discovery algorithm in `find_discords_hotsax`:
202
+
203
+ import numpy as np
204
+ from numpy import genfromtxt
205
+ from saxpy.hotsax import find_discords_hotsax
206
+
207
+ dd = genfromtxt("data/ecg0606_1.csv", delimiter=',')
208
+ find_discords_hotsax(dd)
209
+
210
+ which finds the anomalies as `(position, nearest-neighbour distance)` pairs:
211
+
212
+ [(430, np.float64(5.279080006171839)), (318, np.float64(4.175756357308695))]
213
+
214
+ The function is parameterized with the sliding window size, the number of discords desired, the PAA and alphabet sizes, and the z-Normalization threshold:
215
+
216
+ def find_discords_hotsax(series, win_size=100, num_discords=2, paa_size=3,
217
+ alphabet_size=3, znorm_threshold=0.01, sax_type='unidim')
218
+
219
+ For verifying discords or measuring the HOT-SAX speed-up, saxpy also ships a reference O(n²) brute-force search, `find_discords_brute_force(series, win_size, num_discords=2, znorm_threshold=0.01)`:
220
+
221
+ from saxpy.discord import find_discords_brute_force
222
+
223
+ find_discords_brute_force(dd[100:500], 100, 2)
224
+ # [(73, 6.198555329625454), (219, 5.563692399101613)]
225
+
226
+ Both searches take an optional `random_state` for a reproducible search trajectory; the discords themselves are deterministic (lowest-index tie-break) regardless of the seed.
227
+
228
+ #### 5.0 Grammar inference with RePair
229
+ Saxpy infers a RePair grammar from a space-delimited string of SAX words (or any tokens) via `str_to_repair_grammar`, which returns a dict mapping `rule_id` to a `RepairRule`. Rule 0 (R0) is the compressed top-level string:
230
+
231
+ from saxpy.repair import str_to_repair_grammar
232
+
233
+ g = str_to_repair_grammar("abc abc cba cba bac xxx abc abc cba cba bac")
234
+ g[0].rule_string # 'R4 xxx R4'
235
+ g[4].expanded_rule_string # 'abc abc cba cba bac'
236
+
237
+ RePair is **lossless** and the grammar is structurally equivalent across the R, Python, and Java implementations: decompressing R0 always reproduces the input, and R0 ends up with no repeated digram. RePair works by repeatedly replacing the *most frequent* digram with a new rule; when several digrams share the maximal frequency, *which one is replaced first* is an implementation detail (saxpy follows Python dict insertion order, the C++/R port follows `unordered_map` iteration, Java uses a priority queue). On tie-heavy inputs this can change the **rule numbering** and which equal-frequency pair gets factored -- so the rule ids and even the rule count may differ slightly between implementations -- but the compression is correct in all of them. Treat `rule_id` as implementation-local, not a cross-language identifier.
238
+
239
+ #### 6.0 Time series discord discovery with RRA
240
+ The Rare Rule Anomaly (RRA) algorithm builds a RePair grammar over the SAX representation, derives variable-length subsequences from the grammar rules, and searches them rarest-first. It is exposed as `find_discords_rra`:
241
+
242
+ import numpy as np
243
+ from numpy import genfromtxt
244
+ from saxpy.rra import find_discords_rra
245
+
246
+ dd = genfromtxt("data/ecg0606_1.csv", delimiter=',')
247
+ find_discords_rra(dd, win_size=100, num_discords=2)
248
+
249
+ which returns a list of `RRADiscord` records (variable-length, with start/end positions and the nearest-neighbour distance):
250
+
251
+ [RRADiscord(rule_id=76, start=1722, end=1870, length=148, nn_distance=0.05577536309246554),
252
+ RRADiscord(rule_id=35, start=430, end=531, length=101, nn_distance=0.05258209490111305)]
253
+
254
+ As noted in the alignment section, `nn_distance` is computed between **z-normalized** subsequences (so RRA keys on shape, like HOT-SAX), and because the rarest-first search early-abandons it is a search-order-dependent *approximation* of the true nearest-neighbour distance; the discord *positions* are stable. The signature:
255
+
256
+ def find_discords_rra(series, win_size, paa_size=3, alphabet_size=3,
257
+ nr_strategy="none", znorm_threshold=0.01, num_discords=2,
258
+ random_state=None)
259
+
260
+ #### 7.0 Time series classification with SAX-VSM
261
+ SAX-VSM turns each training class into a single "bag of words" of SAX patterns, weights those with TF\*IDF, and classifies a new series by the class whose weight vector is closest in cosine angle [5]. Saxpy ships the building blocks (`series_to_wordbag`, `manyseries_to_wordbag`, `bags_to_tfidf`, `cosine_distance`, `class_for_bag`) and, on top of them, a convenience layer (`load_ucr_data`, `train_tfidf`, `classify_series`, `classification_accuracy`).
262
+
263
+ Using the Cylinder-Bell-Funnel dataset that ships in the repository:
264
+
265
+ from saxpy.saxvsm import load_ucr_data, train_tfidf, classification_accuracy
266
+
267
+ # UCR/jMotif text format: class label in column 0, series values after.
268
+ train = load_ucr_data("resources/data/cbf/CBF_TRAIN") # {'1': [...], '2': [...], '3': [...]}
269
+ test = load_ucr_data("resources/data/cbf/CBF_TEST")
270
+
271
+ # build the per-class TF*IDF vectors, then score the test split
272
+ tfidf = train_tfidf(train, win_size=60, paa_size=8, alphabet_size=6,
273
+ nr_strategy="exact", znorm_threshold=0.01)
274
+ classification_accuracy(test, tfidf, 60, 8, 6, "exact", 0.01)
275
+ # 1.0 -- a perfect score on CBF at these parameters
276
+
277
+ `bags_to_tfidf` takes an optional `tf_scheme` (`"log1p"`, the default `ln(1 + tf)`, or `"smart"`, `1 + ln(tf)`); see the cross-implementation note above for why `log1p` is canonical. To classify a single series, use `classify_series(series, tfidf, win_size, paa_size, ...)`, which returns the predicted label (or `None` when every class ties).
278
+
279
+ #### 7.1 SAX-VSM discretization parameters optimization
280
+ The window / PAA / alphabet sizes are the hidden parameters of SAX-VSM, and the right choice is not obvious. saxpy can select them automatically by minimizing the leave-out cross-validation error on the *training* set with the DIRECT global optimizer (`scipy.optimize.direct`) -- the same recipe used by the Java DiRect sampler and the jmotif-R README:
281
+
282
+ import numpy as np
283
+ from saxpy.saxvsm_optimize import optimize_parameters
284
+
285
+ X = np.array([s for series in train.values() for s in series])
286
+ y = np.array([lbl for lbl, series in train.items() for _ in series])
287
+
288
+ optimize_parameters(X, y, max_iter=10, random_state=0)
289
+ # {'window_size': 65, 'paa_size': 12, 'alphabet_size': 4,
290
+ # 'cv_error': 0.0, 'nfev': 11, 'evaluations': [...]}
291
+
292
+ `optimize_parameters` is deterministic for a fixed `random_state`; the underlying CV objective is exposed separately as `cv_error(train_data, train_labels, params, ...)` if you want to score a single parameter point. Run independently, saxpy, jmotif-R, and Java each pick their own optimum and reach the same test accuracy on the datasets we checked (CBF, Coffee, Gun_Point, Beef).
293
+
294
+ #### 8.0 Time series motif discovery with EMMA
295
+ *Not yet implemented in saxpy.* EMMA (the complement to HOT-SAX -- recurrent motifs rather than discords) is available in the sibling [Java project](https://github.com/jMotif/SAX); a Python port is planned.
296
+
297
+ Algorithm performance
298
+ ------------
299
+ The tables below characterize the time and peak-memory behaviour of the saxpy algorithms as a function of series length. Each row is a **real, non-periodic signal** at that length (tiling/exact repetition makes the discord search pathological, so it is avoided), discretized with `win=100, paa=4, alphabet=4`, finding two discords. Measured on an AMD Ryzen 9 5950X (single core, Python 3.12) with a standalone profiling harness. Because each length is a different signal, read the columns as real-world cost at that scale, not a controlled same-signal scaling curve.
300
+
301
+ **Wall-clock (seconds, best of repeated runs):**
302
+
303
+ | n | dataset | SAX via window | HOT-SAX | brute-force | RePair | RRA |
304
+ |------:|------------|---------------:|--------:|------------:|-------:|------:|
305
+ | 2,299 | ecg0606 | 0.16 | 0.82 | 0.82 | 0.005 | 2.08 |
306
+ | 5,400 | stdb_308 | 0.38 | 9.36 | 4.28 | 0.009 | 18.94 |
307
+ | 21,600 | mitdbx_108 | 1.52 | 19.31 | 76.53 | 0.030 | 77.22 |
308
+ | 35,039 | dutch_power | 2.49 | 71.50 | 232.44 | 0.066 | 195.97 |
309
+
310
+ **Peak memory (MiB above baseline, via `tracemalloc`):**
311
+
312
+ | n | dataset | SAX via window | HOT-SAX | brute-force | RePair | RRA |
313
+ |------:|------------|---------------:|--------:|------------:|-------:|------:|
314
+ | 2,299 | ecg0606 | 0.1 | 3.7 | 3.9 | 0.2 | 1.2 |
315
+ | 5,400 | stdb_308 | 0.1 | 8.9 | 9.3 | 0.5 | 2.5 |
316
+ | 21,600 | mitdbx_108 | 0.3 | 35.9 | 37.8 | 1.3 | 9.6 |
317
+ | 35,039 | dutch_power | 0.6 | 58.4 | 61.8 | 2.6 | 16.4 |
318
+
319
+ A few things this makes concrete: `sax_via_window` and RePair are effectively linear and cheap (RePair runs over the SAX *word* string, not the raw series); the brute-force reference and HOT-SAX are the O(n²) distance searches (the vectorized brute-force is the exact reference -- HOT-SAX's heuristic ordering is what buys the speed-up on the smaller, structured series, while on a long noisy signal the two converge in cost); and RRA, being variable-length and grammar-driven, is the heaviest in pure Python at large n (the compiled R/Java RRA is faster in absolute terms -- this is the interpreter gap, not an algorithmic difference).
320
+
321
+ **SAX-VSM (Cylinder-Bell-Funnel, 30 train / 900 test):**
322
+
323
+ | operation | wall-clock | peak memory |
324
+ |-----------|-----------:|------------:|
325
+ | `train_tfidf` + `classification_accuracy` (all 900 test series) | 8.6 s | 0.5 MiB |
326
+ | `optimize_parameters` (`max_iter=10`, leave-out CV) | 86.0 s | 1.8 MiB |
327
+
328
+ Changes since the last release
329
+ ------------
330
+ saxpy 2.0.0 is the first modern PyPI release (the only prior artifact was the legacy `1.0.1.dev167` pbr snapshot). The full list is in [CHANGELOG.md](CHANGELOG.md); the highlights:
331
+
332
+ * **New: SAX-VSM classification** -- the classifier layer (§7.0) and a DIRECT cross-validation **parameter optimizer** (§7.1), verified identical to the R and Java implementations on CBF.
333
+ * **New: the grammar layer** -- RePair grammar inference (§5.0) and RRA variable-length discord discovery (§6.0), ported from the C++/Java.
334
+ * **New: reproducibility** -- an optional `random_state` on the HOT-SAX, brute-force, and RRA discord searches.
335
+ * **Cross-implementation alignment** -- the SAX stack, the discord distances, and the SAX-VSM `log1p` TF\*IDF are now aligned to R and Java (see the alignment section). Several outputs near a breakpoint differ from the 1.x line as a result.
336
+ * **Performance** -- a bucketed priority queue for RePair (~2.8×), a lighter RRA z-Norm path (~3--6×), and vectorized PAA.
337
+ * **Modernized packaging & tooling** -- pbr → Hatchling, `uv` + `pyproject.toml`, ruff + mypy gates, and a 3.10/3.11/3.12 × linux/macOS/windows CI matrix. The minimum Python is now 3.10.
338
+ * **Renamed** `cosine_similarity` → `cosine_distance` (it returns `1 - cosine`); the old name is kept as a deprecated alias.
339
+
340
+ ## References
341
+ [1] Lin, J., Keogh, E., Patel, P., and Lonardi, S.,
342
+ [*Finding Motifs in Time Series*](http://cs.gmu.edu/~jessica/Lin_motif.pdf),
343
+ The 2nd Workshop on Temporal Data Mining, the 8th ACM Int'l Conference on KDD (2002)
344
+
345
+ [2] Patel, P., Keogh, E., Lin, J., Lonardi, S.,
346
+ [*Mining Motifs in Massive Time Series Databases*](http://www.cs.gmu.edu/~jessica/publications/motif_icdm02.pdf),
347
+ In Proc. ICDM (2002)
348
+
349
+ [3] Keogh, E., Lin, J., Fu, A.,
350
+ [*HOT SAX: Efficiently finding the most unusual time series subsequence*](http://www.cs.ucr.edu/~eamonn/HOT%20SAX%20%20long-ver.pdf),
351
+ In Proc. ICDM (2005)
352
+
353
+ [4] Mohammad, Y., Nishida T.,
354
+ [*Robust learning from demonstrations using multidimensional SAX*](https://ieeexplore.ieee.org/document/6987960),
355
+ 2014 14th International Conference on Control, Automation and Systems (ICCAS 2014)
356
+
357
+ [5] Senin, P., and Malinchik, S.,
358
+ [*SAX-VSM: Interpretable Time Series Classification Using SAX and Vector Space Model*](https://csdl.ics.hawaii.edu/techreports/2013/13-05/13-05.pdf),
359
+ Data Mining (ICDM), 2013 IEEE 13th International Conference on, pp. 1175-1180 (2013)
360
+
361
+ [6] Salton, G., Wong, A., Yang, C.,
362
+ [*A vector space model for automatic indexing*](http://dl.acm.org/citation.cfm?id=361220),
363
+ Commun. ACM 18, 11, 613-620 (1975)
364
+
365
+ [7] Larsson, N.J., and Moffat, A.,
366
+ [*Offline dictionary-based compression*](https://ieeexplore.ieee.org/document/755679),
367
+ In Data Compression Conference (1999)
368
+
369
+ [8] Senin, P., Lin, J., Wang, X., Oates, T., Gandhi, S., Boedihardjo, A.P., Chen, C., Frankenstein, S.,
370
+ [*Time series anomaly discovery with grammar-based compression*](https://openproceedings.org/2015/conf/edbt/paper-155.pdf),
371
+ In Proc. of the International Conference on Extending Database Technology, EDBT (2015)
372
+
373
+ ## Citing this work
374
+
375
+ If you are using this implementation for your academic work, please cite our [GrammarViz 2.0 paper](http://link.springer.com/chapter/10.1007/978-3-662-44845-8_37):
376
+
377
+ [[Citation]](https://raw.githubusercontent.com/jMotif/SAX/master/citation.bib) Senin, P., Lin, J., Wang, X., Oates, T., Gandhi, S., Boedihardjo, A.P., Chen, C., Frankenstein, S., Lerner, M., [*GrammarViz 2.0: a tool for grammar-based pattern discovery in time series*](http://csdl.ics.hawaii.edu/techreports/2014/14-06/14-06.pdf), ECML/PKDD Conference (2014).
378
+
379
+ ## Made with Aloha!
380
+ ![Made with Aloha!](https://raw.githubusercontent.com/GrammarViz2/grammarviz2_src/master/src/resources/assets/aloha.jpg)