event2vector 0.4.0__tar.gz → 0.5.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 (36) hide show
  1. event2vector-0.5.0/CHANGELOG.md +129 -0
  2. {event2vector-0.4.0 → event2vector-0.5.0}/CITATION.cff +14 -7
  3. event2vector-0.5.0/PKG-INFO +393 -0
  4. event2vector-0.5.0/README.md +357 -0
  5. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/__init__.py +1 -1
  6. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/metrics.py +36 -0
  7. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/models.py +296 -11
  8. event2vector-0.5.0/event2vector.egg-info/PKG-INFO +393 -0
  9. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector.egg-info/SOURCES.txt +3 -1
  10. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector.egg-info/requires.txt +1 -0
  11. {event2vector-0.4.0 → event2vector-0.5.0}/pyproject.toml +2 -1
  12. event2vector-0.5.0/tests/test_reconstruction.py +214 -0
  13. event2vector-0.5.0/tests/test_root_cause.py +128 -0
  14. event2vector-0.4.0/CHANGELOG.md +0 -64
  15. event2vector-0.4.0/PKG-INFO +0 -262
  16. event2vector-0.4.0/README.md +0 -227
  17. event2vector-0.4.0/event2vector.egg-info/PKG-INFO +0 -262
  18. {event2vector-0.4.0 → event2vector-0.5.0}/MANIFEST.in +0 -0
  19. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/baselines.py +0 -0
  20. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/data.py +0 -0
  21. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/datasets/__init__.py +0 -0
  22. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/datasets/arithmetic.py +0 -0
  23. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/datasets/brown.py +0 -0
  24. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/datasets/decay.py +0 -0
  25. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/datasets/groups.py +0 -0
  26. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/datasets/lifepath.py +0 -0
  27. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/datasets/realdata.py +0 -0
  28. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/eval/__init__.py +0 -0
  29. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/eval/figures.py +0 -0
  30. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/eval/harness.py +0 -0
  31. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector/geometries.py +0 -0
  32. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector.egg-info/dependency_links.txt +0 -0
  33. {event2vector-0.4.0 → event2vector-0.5.0}/event2vector.egg-info/top_level.txt +0 -0
  34. {event2vector-0.4.0 → event2vector-0.5.0}/setup.cfg +0 -0
  35. {event2vector-0.4.0 → event2vector-0.5.0}/tests/test_affine.py +0 -0
  36. {event2vector-0.4.0 → event2vector-0.5.0}/tests/test_device.py +0 -0
@@ -0,0 +1,129 @@
1
+ # Changelog
2
+
3
+ All notable changes to `event2vector` are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.5.0] - 2026-10-21
8
+
9
+ The Euclidean model gets a reconstruction objective that acts on whole histories
10
+ and a set of root-cause analysis methods built on the exact decomposition of its
11
+ state. Other geometries are unchanged; the API is backward compatible.
12
+
13
+ ### Added
14
+
15
+ - Root-cause analysis methods on `Event2Vec`: `attribute` (exact per-event
16
+ logit contributions for Euclidean, leave-one-out otherwise), `counterfactual`,
17
+ `influence_matrix`, `event_impact`, and for Euclidean `decompose` and
18
+ `explain_difference` (event counts recovered from states by non-negative least
19
+ squares).
20
+ - `metrics.transition_graph_auc`, which scores the influence matrix against a
21
+ known transition graph.
22
+ - `lambda_difference` (experimental, off by default): a difference-reconstruction
23
+ term for the Euclidean model. Random pairs of histories in a padded batch must
24
+ have a state difference that decodes (with the same scale-invariant form) to
25
+ the events that differ. In our experiments it lowers the reconstruction error
26
+ and improves the decoding of sums, but does not improve the decoding of
27
+ differences; it is kept as an option for further work.
28
+ - `max_norm=None` disables the Euclidean norm clipping, so the state is the exact
29
+ sum of its events.
30
+
31
+ ### Changed
32
+
33
+ - The Euclidean reconstruction loss is upgraded to a history-level objective.
34
+ It reconstructs the normalized event histogram `c/n` of each history from its
35
+ additive embedding `Eᵀ c/n`, with the event-embedding matrix as a tied decoder:
36
+ `‖α* E Eᵀ c/n − c/n‖²`, where `α* = ⟨E Eᵀ c, c⟩ / ‖E Eᵀ c‖²` is the
37
+ closed-form best scale (no trainable parameter), so the loss does not compete
38
+ with prediction for the embedding norm. The embedding is rebuilt without dropout
39
+ or clipping so the gradient reaches `E`. The previous step-level term
40
+ `‖(h_t − e_t) − h_{t-1}‖²` is satisfied by construction of the additive update
41
+ (it acted only when `max_norm` clipping fired). The other geometries are
42
+ unchanged.
43
+
44
+ ### Fixed
45
+
46
+ - An infinite `max_norm` previously produced NaN gradients (`0 · inf` in the
47
+ backward pass of the clip).
48
+
49
+ ## [0.4.1] - 2026-10-15
50
+
51
+ Documentation and metadata only; the code is unchanged from 0.4.0.
52
+
53
+ ### Changed
54
+
55
+ - The paper is published in the Proceedings of Machine Learning Research
56
+ (vol. 282): the README citation, the "Paper" link and `CITATION.cff` (GitHub's
57
+ "Cite this repository" and Zenodo) now point to
58
+ [PMLR](https://proceedings.mlr.press/v282/sulc26a.html); arXiv is kept as the
59
+ preprint.
60
+ - Rewritten README: quickstart, how training works, the geometries with a figure
61
+ of the same events composed by each geometry, a parameter and method
62
+ reference, devices, datasets, examples, citation, and the event2vec logo.
63
+ - The Brown corpus example's extra requirements are documented
64
+ (`pip install "event2vector[viz,manifold,nlp]" nltk`).
65
+
66
+ ### Added
67
+
68
+ - `images/make_readme_figures.py`, which redraws the README's logo and geometry
69
+ figure from the library's own model classes.
70
+
71
+ ## [0.4.0] - 2026-10-15
72
+
73
+ ### Added
74
+
75
+ - `geometry="affine"` (`AffineModel`): each event is an invertible affine map
76
+ `x -> exp(M_e) x + v_e` applied on the left of an affine state, so an event can
77
+ act on the history (shrink, stretch or rotate it) instead of only adding to it.
78
+ With `M_e = 0`, the initialisation, the point is exactly the Euclidean sum. The
79
+ step is recovered exactly by the reconstruction loss, and subsequences compose
80
+ into one affine map:
81
+ - `AffineModel.point(state)` returns the plotted point `b` from a state;
82
+ - `AffineModel.compose(event_ids)` returns one `(A, v)` for a subsequence;
83
+ - `AffineModel.override_strength(event_ids)` returns the singular values of its
84
+ linear part.
85
+ - Estimator options `affine_readout` (`"point"` or `"full"`) and
86
+ `affine_max_log_scale` (bounds how much one event can stretch or shrink the state).
87
+ - Tests for the affine geometry (`tests/test_affine.py`).
88
+
89
+ ### Changed
90
+
91
+ - The MPS-safe `matrix_exp` is now a shared helper used by both the `rotation` and
92
+ `affine` geometries, and automatic device selection falls back to the CPU for
93
+ `affine` as it already did for `rotation`.
94
+
95
+ ## [0.3.1] - 2026-10-11
96
+
97
+ ### Fixed
98
+
99
+ - `Event2Vec(geometry="rotation")` no longer crashes on Apple Silicon with
100
+ `NotImplementedError: The operator 'aten::linalg_matrix_exp' is not currently
101
+ implemented for the MPS device`. `RotationModel` now computes the matrix
102
+ exponential on the CPU when its input is on MPS and moves the result back, so
103
+ `device="mps"`, a bare `RotationModel` and a `ProductModel` with a rotation
104
+ factor all work.
105
+ - The project links on PyPI pointed to `github.com/sulcantonin/event2vector`,
106
+ which does not exist; they now point to
107
+ [`github.com/sulcantonin/event2vec_public`](https://github.com/sulcantonin/event2vec_public).
108
+
109
+ ### Changed
110
+
111
+ - When the device is chosen automatically and MPS would be picked, the
112
+ `rotation` geometry runs on the CPU instead and emits a `RuntimeWarning`. On
113
+ MPS every step round-trips through the CPU, which made training roughly 8–14×
114
+ slower than on the CPU alone for this model. Pass `device="mps"` to use MPS
115
+ anyway. CUDA and the other geometries are unaffected.
116
+
117
+ ### Added
118
+
119
+ - Tests for the automatic device selection (`tests/test_device.py`).
120
+ - This changelog, linked from the PyPI project page.
121
+
122
+ ## [0.3.0] - 2026-07-21
123
+
124
+ Earlier releases are listed on [PyPI](https://pypi.org/project/event2vector/#history).
125
+
126
+ [0.4.1]: https://pypi.org/project/event2vector/0.4.1/
127
+ [0.4.0]: https://pypi.org/project/event2vector/0.4.0/
128
+ [0.3.1]: https://pypi.org/project/event2vector/0.3.1/
129
+ [0.3.0]: https://pypi.org/project/event2vector/0.3.0/
@@ -1,12 +1,13 @@
1
1
  cff-version: 1.2.0
2
- message: "If you use event2vector, please cite the paper below."
2
+ message: "If you use event2vector, please cite the paper below (PMLR vol. 282)."
3
3
  title: "event2vector: geometric embeddings for event sequences"
4
4
  type: software
5
5
  authors:
6
6
  - family-names: Sulc
7
7
  given-names: Antonin
8
- version: 0.4.0
9
- date-released: 2026-10-15
8
+ version: 0.5.0
9
+ doi: 10.5281/zenodo.23399003
10
+ date-released: 2026-10-20
10
11
  license: MIT
11
12
  repository-code: "https://github.com/sulcantonin/event2vec_public"
12
13
  url: "https://event2vec.com"
@@ -16,11 +17,17 @@ keywords:
16
17
  - representation-learning
17
18
  - geometric deep learning
18
19
  preferred-citation:
19
- type: article
20
+ type: conference-paper
20
21
  title: "Event2Vec: A Geometric Approach to Learning Composable Representations of Event Sequences"
21
22
  authors:
22
23
  - family-names: Sulc
23
24
  given-names: Antonin
24
- journal: "arXiv preprint arXiv:2509.12188"
25
- year: 2025
26
- url: "https://arxiv.org/abs/2509.12188"
25
+ collection-title: "Proceedings of the 4th (2025) and 3rd (2024) NeurIPS Workshops on Symmetry and Geometry in Neural Representations"
26
+ collection-type: proceedings
27
+ volume: 282
28
+ start: 492
29
+ end: 512
30
+ year: 2026
31
+ publisher:
32
+ name: PMLR
33
+ url: "https://proceedings.mlr.press/v282/sulc26a.html"
@@ -0,0 +1,393 @@
1
+ Metadata-Version: 2.4
2
+ Name: event2vector
3
+ Version: 0.5.0
4
+ Summary: Scikit-learn-style geometric embeddings for event sequences.
5
+ Author: Antonin Sulc
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sulcantonin/event2vec_public
8
+ Project-URL: Repository, https://github.com/sulcantonin/event2vec_public
9
+ Project-URL: Issues, https://github.com/sulcantonin/event2vec_public/issues
10
+ Project-URL: Changelog, https://github.com/sulcantonin/event2vec_public/blob/main/CHANGELOG.md
11
+ Keywords: sequence-embedding,event-sequences,representation-learning,word2vec,scikit-learn,time-series
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Classifier: Intended Audience :: Science/Research
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: numpy>=1.21
19
+ Requires-Dist: torch>=1.13
20
+ Requires-Dist: scikit-learn>=1.0
21
+ Requires-Dist: scipy>=1.5
22
+ Requires-Dist: tqdm>=4.0
23
+ Provides-Extra: viz
24
+ Requires-Dist: matplotlib; extra == "viz"
25
+ Requires-Dist: seaborn; extra == "viz"
26
+ Provides-Extra: manifold
27
+ Requires-Dist: openTSNE; extra == "manifold"
28
+ Provides-Extra: nlp
29
+ Requires-Dist: gensim; extra == "nlp"
30
+ Provides-Extra: hyperbolic
31
+ Requires-Dist: geoopt>=0.5; extra == "hyperbolic"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest; extra == "dev"
34
+ Requires-Dist: black; extra == "dev"
35
+ Requires-Dist: ruff; extra == "dev"
36
+
37
+ <div align="center">
38
+
39
+ <a href="https://event2vec.com">
40
+ <picture>
41
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sulcantonin/event2vec_public/main/images/logo-dark.svg">
42
+ <img alt="event2vec" src="https://raw.githubusercontent.com/sulcantonin/event2vec_public/main/images/logo-light.svg" width="300">
43
+ </picture>
44
+ </a>
45
+
46
+ **Interpretable, composable embeddings for sequences of discrete events.**
47
+
48
+ [![PyPI version](https://badge.fury.io/py/event2vector.svg)](https://badge.fury.io/py/event2vector)
49
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23399003.svg)](https://doi.org/10.5281/zenodo.23399003)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
52
+ [![PMLR](https://img.shields.io/badge/PMLR-vol.%20282-1f4e79.svg)](https://proceedings.mlr.press/v282/sulc26a.html)
53
+ [![arXiv](https://img.shields.io/badge/arXiv-2509.12188-b31b1b.svg)](https://arxiv.org/abs/2509.12188)
54
+
55
+ **[event2vec.com](https://event2vec.com)** · [Paper](https://proceedings.mlr.press/v282/sulc26a.html) · [Changelog](https://github.com/sulcantonin/event2vec_public/blob/main/CHANGELOG.md) · [Issues](https://github.com/sulcantonin/event2vec_public/issues)
56
+
57
+ ![Event2Vector teaser](https://raw.githubusercontent.com/sulcantonin/event2vec_public/main/images/teaser.png)
58
+
59
+ </div>
60
+
61
+ ## What it is
62
+
63
+ Event2Vector learns one vector per event type and represents a whole sequence as
64
+ the **composition of its events**. In the default Euclidean geometry that
65
+ composition is a plain sum, so a sequence's embedding is *exactly* the sum of its
66
+ events' vectors. This makes the representation interpretable by construction:
67
+
68
+ - **Decomposition**: every coordinate of a sequence is attributable to the events in it.
69
+ - **Composition**: sub-sequences combine by vector arithmetic.
70
+ - **Counterfactuals**: removing an event from a sequence is one subtraction.
71
+
72
+ Other geometries (hyperbolic, spherical, rotation, Heisenberg, affine, product)
73
+ keep this compositional structure while capturing hierarchy, cycles or order. All
74
+ of them sit behind one scikit-learn-style estimator.
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ pip install event2vector
80
+ ```
81
+
82
+ From source:
83
+
84
+ ```bash
85
+ git clone https://github.com/sulcantonin/event2vec_public.git
86
+ cd event2vec_public
87
+ pip install .
88
+ ```
89
+
90
+ Requires Python 3.9+ and PyTorch 1.13+.
91
+
92
+ ## Quickstart
93
+
94
+ Sequences are lists of integer event ids (`0 … num_event_types - 1`) and can have
95
+ different lengths.
96
+
97
+ ```python
98
+ import numpy as np
99
+ from event2vector import Event2Vec
100
+
101
+ vocab = ["start", "login", "search", "buy", "logout"]
102
+ sequences = [[0, 1, 2, 3, 4], [0, 1, 2, 2, 4], [0, 2, 3, 4], [0, 1, 4]] * 50
103
+
104
+ model = Event2Vec(
105
+ num_event_types=len(vocab),
106
+ embedding_dim=8,
107
+ geometry="euclidean",
108
+ pad_sequences=True, # vectorized batches of variable-length sequences
109
+ num_epochs=20,
110
+ random_state=0,
111
+ )
112
+ model.fit(sequences)
113
+
114
+ embeddings = model.transform(sequences) # (n_sequences, 8)
115
+
116
+ # A sequence is exactly the sum of its event vectors:
117
+ event_vectors = model.model.embedding.weight.detach().cpu().numpy()
118
+ assert np.allclose(embeddings[0], event_vectors[sequences[0]].sum(axis=0), atol=1e-5)
119
+
120
+ # gensim-style nearest events to an event or a whole sequence
121
+ print(model.most_similar(positive=3, topn=2)) # [(event_id, similarity), ...]
122
+ print(model.most_similar(positive=[[0, 1, 2]], topn=2))
123
+ ```
124
+
125
+ ## How it works
126
+
127
+ A sequence is read one event at a time. The state starts at the identity and each
128
+ event updates it, `h_t = h_{t-1} ⊕ e_t`, where `⊕` is the geometry's composition
129
+ (vector addition in the Euclidean case). Training minimizes three terms at every
130
+ step:
131
+
132
+ | term | weight | what it enforces |
133
+ |---|---|---|
134
+ | next-event prediction | 1 | a linear decoder predicts event `t+1` from `h_t` (cross-entropy) |
135
+ | reconstruction | `lambda_reconstruction` | Euclidean: the history's event counts can be read back from its embedding with the tied decoder `E`, `‖α* E Eᵀ c/n − c/n‖²` with the closed-form best scale `α*`, so it does not fix the embedding norm; other geometries: the step `e_t` is recovered from `h_{t-1}` and `h_t` |
136
+ | consistency | `lambda_consistency` | two dropout passes over the same step agree |
137
+
138
+ There is no attention and no hidden nonlinearity in the state update, which is
139
+ what keeps every embedding a readable composition of its events. See the
140
+ [paper](https://proceedings.mlr.press/v282/sulc26a.html) for details.
141
+
142
+ ## Geometries
143
+
144
+ Choose the composition with `geometry=`. Every geometry uses the same estimator
145
+ and, apart from `euclidean`, a generic left-cancellation reconstruction loss.
146
+
147
+ | `geometry` | update | best for |
148
+ |---|---|---|
149
+ | `euclidean` | vector addition | order-invariant bag of events, exact decomposition |
150
+ | `hyperbolic` | Möbius addition (curvature `curvature > 0`) | hierarchical, tree-like structure |
151
+ | `stereographic` | κ-gyroaddition (signed `kappa`) | one curvature dial: `kappa < 0` hyperbolic, `0` Euclidean, `> 0` spherical |
152
+ | `spherical` | κ-gyroaddition (`kappa > 0`) | cyclic / periodic structure |
153
+ | `rotation` | left `SO(n)` product | non-commutative orientation (order matters) |
154
+ | `heisenberg` | Heisenberg group product | order-dependent signed (Lévy) area |
155
+ | `affine` | `x ↦ exp(M_e) x + v_e` on the left | events that act on the history (shrink, stretch, rotate it), not only add to it |
156
+ | `product` | per-factor update | two structure types at once, e.g. hyperbolic × spherical |
157
+
158
+ ![The same three events composed by each geometry](https://raw.githubusercontent.com/sulcantonin/event2vec_public/main/images/geometries.png)
159
+
160
+ *The same three events composed by each geometry, in order a·b·c (blue) and c·b·a
161
+ (red). The Euclidean sum ends in the same place either way; where the endpoints
162
+ separate, the geometry remembers order. Drawn by the library's own model classes
163
+ with `python images/make_readme_figures.py`.*
164
+
165
+ ```python
166
+ Event2Vec(num_event_types=len(vocab), geometry="hyperbolic", curvature=1.0)
167
+ Event2Vec(num_event_types=len(vocab), geometry="stereographic", kappa=-1.0)
168
+ Event2Vec(num_event_types=len(vocab), geometry="rotation", rotation_n=3)
169
+ Event2Vec(num_event_types=len(vocab), geometry="product", product_kappas=(-1.0, 1.0))
170
+ ```
171
+
172
+ Hyperbolic embeddings live in the Poincaré ball; use the matching operations on them:
173
+
174
+ ```python
175
+ from event2vector import HyperbolicUtils
176
+
177
+ hyp = Event2Vec(num_event_types=len(vocab), geometry="hyperbolic", curvature=1.0,
178
+ pad_sequences=True, num_epochs=20).fit(sequences)
179
+ z = hyp.transform(sequences, as_numpy=False)
180
+ combined = HyperbolicUtils.mobius_add(z[0:1], z[1:2], hyp.curvature)
181
+ distance = HyperbolicUtils.poincare_dist_sq(z[0:1], z[1:2], hyp.curvature).sqrt()
182
+ ```
183
+
184
+ ### Affine geometry
185
+
186
+ In the additive geometries an event always pushes the state the same way. With
187
+ `geometry="affine"` each event is an invertible affine map `x ↦ exp(M_e) x + v_e`,
188
+ so the same event contributes differently depending on what came before, while
189
+ the model remains a group:
190
+
191
+ - with every `M_e = 0` (the initialization) the result is exactly the Euclidean sum,
192
+ so training starts from the additive model;
193
+ - every step is invertible, and the reconstruction loss recovers it exactly;
194
+ - any sub-sequence composes into a single affine map, so its effect on *any*
195
+ history is one `A x + v`.
196
+
197
+ The state is the whole map `[vec(A), b]`; the point to plot is `b`, the image of the
198
+ origin. The singular values of a sub-sequence's `A` measure how strongly it
199
+ overrides what came before: `1` passes the history through, values below `1`
200
+ erase it along that direction.
201
+
202
+ ```python
203
+ aff = Event2Vec(num_event_types=len(vocab), geometry="affine", embedding_dim=2,
204
+ pad_sequences=True, num_epochs=20).fit(sequences)
205
+
206
+ states = aff.transform(sequences, as_numpy=False) # [vec(A), b] per sequence
207
+ points = aff.model.point(states) # (n_sequences, 2), ready to plot
208
+
209
+ A, v = aff.model.compose([1, 2, 3]) # one map for a sub-sequence
210
+ print(aff.model.override_strength([1, 2, 3])) # its singular values
211
+ ```
212
+
213
+ `affine_readout="full"` lets the decoder read `A` as well as `b`, and
214
+ `affine_max_log_scale` bounds how strongly a single event can stretch or shrink
215
+ the state.
216
+
217
+ ## API reference
218
+
219
+ ### `Event2Vec` parameters
220
+
221
+ | parameter | default | meaning |
222
+ |---|---|---|
223
+ | `num_event_types` | required | size of the event vocabulary |
224
+ | `embedding_dim` | `64` | dimension of the event vectors (the plotted point for `affine`) |
225
+ | `geometry` | `"euclidean"` | see [Geometries](#geometries) |
226
+ | `curvature` | `1.0` | curvature `c > 0` for `hyperbolic` |
227
+ | `kappa` | `None` | signed curvature for `stereographic` / `spherical` |
228
+ | `rotation_n` | `3` | `n` of `SO(n)` for `rotation` |
229
+ | `heisenberg_m` | `1` | the state is the `(2m+1)`-dimensional Heisenberg group |
230
+ | `affine_readout` | `"point"` | `"point"` or `"full"`: what the decoder reads for `affine` |
231
+ | `affine_max_log_scale` | `None` | bound on how much one event can stretch the `affine` state |
232
+ | `product_kappas` | `(-1.0, 1.0)` | one curvature per factor for `product` |
233
+ | `dropout_p` | `0.2` | dropout on each step's increment |
234
+ | `max_norm` | `10.0` | norm clipping of the Euclidean state; `None` disables it, so the state is the exact sum of its events |
235
+ | `lambda_reconstruction` | `0.2` | weight of the reconstruction loss |
236
+ | `lambda_consistency` | `0.2` | weight of the consistency loss |
237
+ | `lambda_difference` | `0.0` | weight of the difference-reconstruction loss (Euclidean, needs `pad_sequences=True`): the difference of two histories' states must decode to the events that differ |
238
+ | `learning_rate` | `1e-3` | AdamW learning rate |
239
+ | `batch_size` | `32` | sequences per batch |
240
+ | `num_epochs` | `50` | training epochs |
241
+ | `pad_sequences` | `False` | vectorized padded batches (much faster on large corpora) |
242
+ | `use_gpu` / `device` | `True` / `None` | automatic CUDA / MPS / CPU choice, or an explicit device |
243
+ | `random_state` | `None` | seed for reproducible runs |
244
+ | `deterministic` | `False` | request deterministic kernels |
245
+ | `riemannian` | `False` | Riemannian Adam for `hyperbolic` (needs `geoopt`) |
246
+
247
+ ### Methods
248
+
249
+ | method | returns |
250
+ |---|---|
251
+ | `fit(sequences, epochs=None, verbose=False)` | the fitted estimator; per-epoch losses in `training_loss_` |
252
+ | `transform(sequences, as_numpy=True)` | one state per sequence (NumPy array or tensor) |
253
+ | `fit_transform(sequences, ...)` | `fit`, then `transform` on the same sequences |
254
+ | `most_similar(positive, negative=None, topn=10)` | nearest event ids to events or whole sequences, with similarities |
255
+ | `attribute(sequence, target=None)` | each event's contribution to the logit of the next event `target` |
256
+ | `counterfactual(sequence, remove)` | next-event probabilities with and without the events at positions `remove` |
257
+ | `influence_matrix()` | `A[j, i]`: how much event `i` alone raises the logit of next event `j` |
258
+ | `event_impact()` | how strongly one occurrence of each event moves the next-event distribution |
259
+ | `decompose(sequence_or_state)` | event counts recovered from a state vector (Euclidean) |
260
+ | `explain_difference(sequence_a, sequence_b)` | signed counts of the events that differ, read from the two states (Euclidean) |
261
+
262
+ The underlying PyTorch module is `model.model` (event vectors in
263
+ `model.model.embedding.weight`).
264
+
265
+ ### Root-cause analysis
266
+
267
+ In the Euclidean geometry the state is an exact weighted sum of the event
268
+ embeddings, `h_T = Σ_i w_i e_{s_i}`, and the decoder is linear. The weights `w_i`
269
+ are the products of the later norm-clipping factors (all `1` with
270
+ `max_norm=None`), so recent events weigh more once clipping fires. Each prediction
271
+ therefore splits exactly into per-event terms, which the methods above expose:
272
+
273
+ ```python
274
+ est = Event2Vec(num_event_types=len(vocab), pad_sequences=True).fit(sequences)
275
+
276
+ out = est.attribute(history, target=vocab["job_loss"])
277
+ out["bias"] + out["contributions"].sum() # equals out["logit"]: one term per event
278
+
279
+ est.counterfactual(history, remove=[2])["delta"] # change in next-event probabilities
280
+ A = est.influence_matrix() # (V, V) event -> next-event influence
281
+ est.event_impact() # one score per event
282
+ est.decompose(history, round_counts=True) # which events a state contains
283
+ est.explain_difference(failing_run, healthy_run) # +: more in the failing run
284
+ ```
285
+
286
+ | method | geometries | exact when |
287
+ |---|---|---|
288
+ | `attribute` | all | Euclidean: always (the terms sum to the logit). Others: leave-one-out differences, which do not sum to the logit |
289
+ | `counterfactual` | all | always (the edited sequence is re-encoded) |
290
+ | `influence_matrix`, `event_impact` | all | Euclidean with `max_norm=None`: `A = W_dec Eᵀ` and every sequence's logits are `bias + A @ counts` |
291
+ | `decompose`, `explain_difference` | Euclidean | `max_norm=None` and few enough distinct events for the embeddings to separate them; with clipping they estimate the clip-weighted counts |
292
+
293
+ `event2vector.metrics.transition_graph_auc(est, graph, vocab)` scores
294
+ `influence_matrix()` against a known transition graph (ROC AUC over all ordered
295
+ event pairs), e.g. `make_lifepath()["graph"]`.
296
+
297
+ These methods explain the model's predictions. They reflect associations learned
298
+ from the data, not causal effects, and the sum-based ones cannot tell event
299
+ orders apart when clipping does not fire.
300
+
301
+ ### Devices
302
+
303
+ By default the estimator uses CUDA, then Apple MPS, then the CPU. On Apple
304
+ Silicon the `rotation` and `affine` geometries default to the CPU, because MPS has
305
+ no `matrix_exp` kernel and the CPU is faster for them; pass `device="mps"` to
306
+ override. Use `use_gpu=False` to force the CPU, or `device="cuda:0"` to pick a
307
+ device.
308
+
309
+ ## Datasets, metrics and baselines
310
+
311
+ The package ships domains with *exact ground truth* for the additive structure,
312
+ so interpretability can be measured rather than only visualized. Datasets live in
313
+ `event2vector.datasets` and return a uniform record (`sequences`,
314
+ `processed_sequences`, `vocab`, `labels`, `graph`):
315
+
316
+ - `make_decay()`: radioactive decay chains; each event is an exact `(ΔZ, ΔN)`, so
317
+ additivity coincides with conservation laws (metric: `operator_recovery`).
318
+ - `make_group(name)`: Cayley-graph walks on abelian and non-abelian groups; the
319
+ additive endpoint provably equals the abelianization (metric: `abelianization_gap`).
320
+ - `make_lifepath()`, `load_brown()`: a controlled synthetic workbench and real
321
+ part-of-speech sequences.
322
+ - `load_gdelt()`, `load_sec_8k()`, `load_synthea()`, …: real-world event streams
323
+ (each needs a public data download; see the loader's docstring).
324
+
325
+ Metrics are in `event2vector.metrics`, a seeded evaluation harness in
326
+ `event2vector.eval`, and GRU / LSTM / Transformer / Word2Vec baselines in
327
+ `event2vector.baselines`.
328
+
329
+ ## Examples
330
+
331
+ A minimal end-to-end run (loss, embeddings, nearest events):
332
+
333
+ ```bash
334
+ python3 examples/minimal_example.py
335
+ ```
336
+
337
+ Part-of-speech sequences from the Brown corpus (the scripts need a few extra
338
+ packages: NLTK for the corpus, t-SNE and Word2Vec for the comparison plots):
339
+
340
+ ```bash
341
+ pip install "event2vector[viz,manifold,nlp]" nltk
342
+ python3 -m examples.prepare_brown_data
343
+ python3 -m examples.train_brown_data
344
+ python3 -m examples.visualize_brown_corpus
345
+ ```
346
+
347
+ Applications:
348
+
349
+ - [The Geometry of Groceries](https://sulcantonin.substack.com/p/the-geometry-of-groceries)
350
+ - [The Geometry of Language Families](https://sulcantonin.substack.com/p/the-geometry-of-language-families)
351
+
352
+ ## Development
353
+
354
+ ```bash
355
+ pip install -e . pytest
356
+ pytest tests/ -q
357
+ ```
358
+
359
+ Changes are listed in [CHANGELOG.md](https://github.com/sulcantonin/event2vec_public/blob/main/CHANGELOG.md).
360
+ Bug reports and questions are welcome in the
361
+ [issue tracker](https://github.com/sulcantonin/event2vec_public/issues).
362
+
363
+ ## Citation
364
+
365
+ If you use Event2Vector, please cite the paper, published in the
366
+ [Proceedings of Machine Learning Research](https://proceedings.mlr.press/v282/sulc26a.html) (vol. 282, NeurIPS Workshops
367
+ on Symmetry and Geometry in Neural Representations):
368
+
369
+ ```bibtex
370
+ @InProceedings{pmlr-v282-sulc26a,
371
+ title = {Event2Vec: A Geometric Approach to Learning Composable Representations of Event Sequences},
372
+ author = {Sulc, Antonin},
373
+ booktitle = {Proceedings of the 4th (2025) and 3rd (2024) NeurIPS Workshops on Symmetry and Geometry in Neural Representations},
374
+ pages = {492--512},
375
+ year = {2026},
376
+ editor = {Acosta, Francisco and Azeglio, Simone and Tolooshams, Bahareh and van de Geijn, Chase and Shewmake, Christian and Sanborn, Sophia and Miolane, Nina},
377
+ volume = {282},
378
+ series = {Proceedings of Machine Learning Research},
379
+ publisher = {PMLR},
380
+ url = {https://proceedings.mlr.press/v282/sulc26a.html}
381
+ }
382
+ ```
383
+
384
+ A preprint is on [arXiv:2509.12188](https://arxiv.org/abs/2509.12188).
385
+
386
+ To cite the software itself, use its Zenodo DOI
387
+ [10.5281/zenodo.23399003](https://doi.org/10.5281/zenodo.23399003), which always
388
+ resolves to the latest version (each release also has its own DOI). GitHub's
389
+ "Cite this repository" button reads the same metadata from `CITATION.cff`.
390
+
391
+ ## License
392
+
393
+ MIT. See the license badge above.