bayesian-changepoint 1.1.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 (30) hide show
  1. bayesian_changepoint-1.1.0/LICENSE +21 -0
  2. bayesian_changepoint-1.1.0/PKG-INFO +587 -0
  3. bayesian_changepoint-1.1.0/README.md +540 -0
  4. bayesian_changepoint-1.1.0/bayesian_changepoint.egg-info/PKG-INFO +587 -0
  5. bayesian_changepoint-1.1.0/bayesian_changepoint.egg-info/SOURCES.txt +28 -0
  6. bayesian_changepoint-1.1.0/bayesian_changepoint.egg-info/dependency_links.txt +1 -0
  7. bayesian_changepoint-1.1.0/bayesian_changepoint.egg-info/requires.txt +25 -0
  8. bayesian_changepoint-1.1.0/bayesian_changepoint.egg-info/top_level.txt +1 -0
  9. bayesian_changepoint-1.1.0/bayesian_changepoint_detection/__init__.py +57 -0
  10. bayesian_changepoint-1.1.0/bayesian_changepoint_detection/bayesian_models.py +645 -0
  11. bayesian_changepoint-1.1.0/bayesian_changepoint_detection/device.py +146 -0
  12. bayesian_changepoint-1.1.0/bayesian_changepoint_detection/generate_data.py +429 -0
  13. bayesian_changepoint-1.1.0/bayesian_changepoint_detection/hazard_functions.py +74 -0
  14. bayesian_changepoint-1.1.0/bayesian_changepoint_detection/offline_likelihoods.py +657 -0
  15. bayesian_changepoint-1.1.0/bayesian_changepoint_detection/online_likelihoods.py +446 -0
  16. bayesian_changepoint-1.1.0/bayesian_changepoint_detection/priors.py +224 -0
  17. bayesian_changepoint-1.1.0/pyproject.toml +154 -0
  18. bayesian_changepoint-1.1.0/setup.cfg +4 -0
  19. bayesian_changepoint-1.1.0/tests/test_device.py +129 -0
  20. bayesian_changepoint-1.1.0/tests/test_docs_code_blocks.py +43 -0
  21. bayesian_changepoint-1.1.0/tests/test_integration.py +288 -0
  22. bayesian_changepoint-1.1.0/tests/test_offline_prior_recursion.py +198 -0
  23. bayesian_changepoint-1.1.0/tests/test_online_detection.py +259 -0
  24. bayesian_changepoint-1.1.0/tests/test_online_likelihood_values.py +207 -0
  25. bayesian_changepoint-1.1.0/tests/test_online_likelihoods.py +274 -0
  26. bayesian_changepoint-1.1.0/tests/test_pdf_rows_consistency.py +247 -0
  27. bayesian_changepoint-1.1.0/tests/test_priors.py +208 -0
  28. bayesian_changepoint-1.1.0/tests/test_regression_vs_numpy.py +177 -0
  29. bayesian_changepoint-1.1.0/tests/test_studentt_exactness.py +86 -0
  30. bayesian_changepoint-1.1.0/tests/test_viterbi.py +160 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Johannes Kulick
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,587 @@
1
+ Metadata-Version: 2.4
2
+ Name: bayesian-changepoint
3
+ Version: 1.1.0
4
+ Summary: Bayesian changepoint detection algorithms with PyTorch support
5
+ Author-email: Johannes Kulick <mail@johanneskulick.net>
6
+ Maintainer-email: Johannes Kulick <mail@johanneskulick.net>, Esteban Carisimo <el.murdo@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/hildensia/bayesian_changepoint_detection
9
+ Project-URL: Repository, https://github.com/hildensia/bayesian_changepoint_detection
10
+ Project-URL: Issues, https://github.com/hildensia/bayesian_changepoint_detection/issues
11
+ Keywords: bayesian,changepoint,detection,time-series,pytorch
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: torch>=2.0.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
29
+ Requires-Dist: numpy>=1.21.0; extra == "dev"
30
+ Requires-Dist: scipy>=1.7.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.6; extra == "dev"
32
+ Requires-Dist: pre-commit>=3.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
+ Requires-Dist: jupyter>=1.0.0; extra == "dev"
35
+ Requires-Dist: notebook>=6.4.0; extra == "dev"
36
+ Provides-Extra: plot
37
+ Requires-Dist: numpy>=1.21.0; extra == "plot"
38
+ Requires-Dist: matplotlib>=3.5.0; extra == "plot"
39
+ Requires-Dist: seaborn>=0.11.0; extra == "plot"
40
+ Provides-Extra: docs
41
+ Requires-Dist: sphinx>=5.0.0; extra == "docs"
42
+ Requires-Dist: sphinx-rtd-theme>=1.2.0; extra == "docs"
43
+ Requires-Dist: numpydoc>=1.5.0; extra == "docs"
44
+ Provides-Extra: gpu
45
+ Requires-Dist: torch[cuda]>=2.0.0; extra == "gpu"
46
+ Dynamic: license-file
47
+
48
+ # 📈 Bayesian Changepoint Detection
49
+
50
+ Find the points where a time series changes regime, with calibrated posterior
51
+ probabilities instead of a threshold. Online (Adams & MacKay 2007) and offline
52
+ (Fearnhead 2006) Bayesian changepoint detection on PyTorch tensors, with
53
+ conjugate Normal-Gamma and Normal-Wishart likelihoods for univariate and
54
+ multivariate series.
55
+
56
+ [![CI](https://github.com/hildensia/bayesian_changepoint_detection/actions/workflows/ci.yml/badge.svg)](https://github.com/hildensia/bayesian_changepoint_detection/actions/workflows/ci.yml)
57
+ [![PyPI](https://img.shields.io/pypi/v/bayesian-changepoint.svg)](https://pypi.org/project/bayesian-changepoint/)
58
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
59
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/LICENSE)
60
+
61
+ ## ✨ Features
62
+
63
+ - 🔭 **Online detection**: the run-length posterior after every observation (Adams & MacKay 2007), for streams and for measuring how quickly a change would have been noticed
64
+ - 🔍 **Offline detection**: the exact posterior probability of a changepoint at every position given the whole series (Fearnhead 2006)
65
+ - 🎯 **Calibrated outputs**: probabilities you can threshold, MAP segment starts, and the single most probable segmentation (`viterbi_changepoints`)
66
+ - 📐 **Conjugate likelihoods**: Student-t predictive for univariate data (unknown mean and variance), multivariate-t for vector data (unknown mean and covariance), independent-features and covariance-only variants
67
+ - 🧮 **Verified mathematics**: closed forms checked against `scipy` and against exhaustive enumeration of segmentations; every pinned number in the test suite says where it comes from
68
+ - ⚡ **Vectorized recursions**: both detectors are O(T²) with the inner work on tensors, not Python loops; 1 000 points offline in under 4 s on a laptop CPU
69
+ - 🖥️ **Runs where your tensors are**: CPU, CUDA or Apple MPS through one `device` argument, with measured guidance on when an accelerator is *not* worth it
70
+ - 🪶 **One dependency**: `torch`; NumPy, SciPy and Matplotlib are only needed for the tests and examples
71
+
72
+ ## 🚀 Quick Start
73
+
74
+ ### Installation
75
+
76
+ ```bash
77
+ pip install bayesian-changepoint
78
+ ```
79
+
80
+ Using [uv](https://docs.astral.sh/uv/):
81
+
82
+ ```bash
83
+ uv add bayesian-changepoint
84
+ ```
85
+
86
+ The import name is `bayesian_changepoint_detection`, whatever the
87
+ distribution is called:
88
+
89
+ ```python
90
+ import bayesian_changepoint_detection
91
+ ```
92
+
93
+ To run the examples and notebooks, add the `plot` extra
94
+ (`pip install "bayesian-changepoint[plot]"`, or `".[plot]"` from a clone).
95
+
96
+ #### Package names
97
+
98
+ `bayesian-changepoint` is the distribution name from 1.1.0 on. The same
99
+ project was published before as **`bayescd`** (0.4, April 2022) and, earlier,
100
+ as **`bayesian-changepoint-detection`** (0.2.dev1). Both are frozen at those
101
+ releases and neither gets updates; if you have one installed, replace it:
102
+
103
+ ```bash
104
+ pip uninstall bayescd bayesian-changepoint-detection
105
+ pip install bayesian-changepoint
106
+ ```
107
+
108
+ The code is the same project and lives in the same repository; only the name
109
+ on PyPI changed. 1.1.0 is a rewrite on PyTorch relative to 0.4 and changes
110
+ the online API relative to 1.0.x — the
111
+ [CHANGELOG](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/CHANGELOG.md)
112
+ lists every breaking change.
113
+
114
+ ### System Requirements
115
+
116
+ - Python 3.9 or higher
117
+ - PyTorch 2.0 or higher (installed automatically). For a CUDA build of
118
+ PyTorch, install it first following <https://pytorch.org/get-started/locally/>;
119
+ the CPU build is enough for everything in this README.
120
+
121
+ ## 📖 Usage
122
+
123
+ ### Online detection
124
+
125
+ The online detector processes the series one point at a time and keeps the
126
+ posterior over the *run length*, the number of observations since the last
127
+ change. Two helpers turn that posterior into changepoints.
128
+
129
+ ```python
130
+ from functools import partial
131
+
132
+ import torch
133
+
134
+ from bayesian_changepoint_detection import (
135
+ StudentT,
136
+ changepoint_probabilities,
137
+ constant_hazard,
138
+ get_map_changepoints,
139
+ online_changepoint_detection,
140
+ )
141
+
142
+ torch.manual_seed(42)
143
+ data = torch.cat([
144
+ torch.randn(50) + 0, # first segment: mean 0
145
+ torch.randn(50) + 3, # second segment: mean 3
146
+ torch.randn(50) + 0, # third segment: mean 0
147
+ ])
148
+
149
+ hazard = partial(constant_hazard, 250) # prior: one change every ~250 points
150
+ likelihood = StudentT(alpha=0.1, beta=0.01, kappa=1, mu=0) # unknown mean and variance
151
+
152
+ R, map_run_lengths = online_changepoint_detection(data, hazard, likelihood)
153
+
154
+ # Index of the first point of each new segment on the MAP run-length path.
155
+ # min_separation merges starts closer than that many points when the
156
+ # posterior hesitates between neighbours.
157
+ print(get_map_changepoints(R, min_separation=10)) # tensor([ 50, 100])
158
+
159
+ # Or a probability per position, judged `lag` observations later.
160
+ probs = changepoint_probabilities(R, lag=10) # probs[t] refers to data index t
161
+ print(torch.where(probs[1:] > 0.5)[0] + 1) # tensor([ 50, 100])
162
+ ```
163
+
164
+ `R[r, t]` is `P(run length = r | first t observations)`. Why not simply
165
+ threshold `R[0, :]`? Under a constant hazard the posterior probability of run
166
+ length 0 is the hazard rate at every step, whatever the data say; the
167
+ evidence for a change at `t` shows up in the *following* columns, as mass at
168
+ run length `k` in column `t + k`. `changepoint_probabilities` reads exactly
169
+ that. `viterbi_changepoints(data, hazard, likelihood)` returns the single
170
+ most probable run-length path instead, i.e. the MAP segmentation.
171
+
172
+ ### Offline detection
173
+
174
+ The offline detector sees the whole series and returns, for every position,
175
+ the posterior probability that a segment ends there. It is usually sharper
176
+ than the online detector; use it for retrospective analysis.
177
+
178
+ ```python
179
+ from bayesian_changepoint_detection import const_prior, offline_changepoint_detection
180
+ from bayesian_changepoint_detection.offline_likelihoods import StudentT as OfflineStudentT
181
+
182
+ prior = partial(const_prior, p=1 / (len(data) + 1)) # flat prior on segment length
183
+ Q, P, changepoint_log_probs = offline_changepoint_detection(data, prior, OfflineStudentT())
184
+
185
+ changepoint_probs = torch.exp(changepoint_log_probs).sum(0) # P(a segment ends at t)
186
+ print(torch.where(changepoint_probs > 0.5)[0]) # tensor([49, 99])
187
+ ```
188
+
189
+ The two detectors use different index conventions: online reports the first
190
+ point of the new segment (50), offline the last point of the old one (49).
191
+ See the [FAQ](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/README.md#-faq).
192
+
193
+ ### Multivariate data
194
+
195
+ Pass a `[T, d]` tensor and a multivariate likelihood; everything else is the
196
+ same.
197
+
198
+ ```python
199
+ from bayesian_changepoint_detection import MultivariateT
200
+
201
+ dims = 3
202
+ mv_data = torch.cat([
203
+ torch.randn(50, dims) + torch.tensor([0.0, 0.0, 0.0]),
204
+ torch.randn(50, dims) + torch.tensor([2.0, -1.0, 1.0]),
205
+ torch.randn(50, dims) + torch.tensor([0.0, 0.0, 0.0]),
206
+ ])
207
+
208
+ R, _ = online_changepoint_detection(mv_data, hazard, MultivariateT(dims=dims))
209
+ print(get_map_changepoints(R, min_separation=10)) # tensor([ 48, 100])
210
+ ```
211
+
212
+ The first start lands two points early on this draw: the lag-10 posterior
213
+ puts 0.53 on 48, 0.13 on 49 and 0.26 on 50, and the MAP path takes the
214
+ mode. Read `changepoint_probabilities` when the exact position matters.
215
+
216
+ ### Devices
217
+
218
+ Every likelihood and both detectors take a `device` argument. Selection is
219
+ automatic (CUDA, then MPS, then CPU); pass `device="cpu"` to the likelihood
220
+ and the detector to opt out. On a laptop the CPU is the faster choice for
221
+ the online detector (measured: 6–30x faster than MPS), and the offline
222
+ detector always runs on the CPU under MPS because it needs float64. How the
223
+ argument is resolved, what has been measured, how to time your own workload
224
+ and how much memory the tables need: [docs/devices.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/docs/devices.md).
225
+
226
+ ### API at a glance
227
+
228
+ | Function | Returns |
229
+ |---|---|
230
+ | `online_changepoint_detection(data, hazard, likelihood)` | `R` (run-length posterior, `[T+1, T+1]`) and the MAP run length after each point |
231
+ | `changepoint_probabilities(R, lag)` | `P(a new segment started at t)`, judged `lag` observations later |
232
+ | `get_map_changepoints(R, min_separation=1)` | indices where the MAP run-length path starts a new segment |
233
+ | `viterbi_changepoints(data, hazard, likelihood)` | the single most probable run-length path and its segment starts |
234
+ | `compute_run_length_posterior(data, hazard, likelihood)` | just `R`, for code that only wants the posterior |
235
+ | `offline_changepoint_detection(data, prior, likelihood)` | `Q` (log evidence), `P` (segment log likelihoods), `Pcp` (log probability of the j-th changepoint at t) |
236
+ | `constant_hazard(lam, r)` | hazard `1 / lam` for every run length |
237
+ | `const_prior`, `geometric_prior`, `negative_binomial_prior` | log prior on segment length for the offline detector |
238
+ | `online_likelihoods.StudentT`, `online_likelihoods.MultivariateT` | online conjugate models (Normal-Gamma, Normal-Wishart) |
239
+ | `offline_likelihoods.StudentT`, `MultivariateT`, `IndependentFeaturesLikelihood`, `FullCovarianceLikelihood` | offline segment marginal likelihoods |
240
+ | `get_device`, `get_device_info`, `to_tensor` | device helpers |
241
+
242
+ All public functions have NumPy-style docstrings with the formulas and the
243
+ paper they come from.
244
+
245
+ ## 🏗️ Architecture
246
+
247
+ ```text
248
+ bayesian_changepoint_detection/
249
+ ├── __init__.py # Public API and __version__ (from package metadata)
250
+ ├── bayesian_models.py # The two detectors, viterbi_changepoints, and the R helpers
251
+ ├── online_likelihoods.py # Online StudentT and MultivariateT: per-run-length predictive densities
252
+ ├── offline_likelihoods.py # Offline StudentT, MultivariateT, IndependentFeatures, FullCovariance: segment marginals
253
+ ├── priors.py # const_prior, geometric_prior, negative_binomial_prior (segment-length priors)
254
+ ├── hazard_functions.py # constant_hazard
255
+ ├── device.py # get_device, get_device_info, to_tensor, ensure_tensor
256
+ └── generate_data.py # Synthetic series with known changepoints, for tests and examples
257
+ ```
258
+
259
+ Supporting directories: `tests/` (the suite, see below), `examples/` (scripts
260
+ and two notebooks, run in CI), `docs/` (pages whose code blocks are executed
261
+ by the tests).
262
+
263
+ ## 🧪 Development
264
+
265
+ ### Setup Development Environment
266
+
267
+ ```bash
268
+ # Clone repository
269
+ git clone https://github.com/hildensia/bayesian_changepoint_detection.git
270
+ cd bayesian_changepoint_detection
271
+
272
+ # Install with development dependencies
273
+ uv venv && source .venv/bin/activate
274
+ uv pip install -e ".[dev]"
275
+ # ...or, without uv: python -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]"
276
+
277
+ # Install pre-commit hooks (ruff lint + format on staged files)
278
+ pre-commit install
279
+ ```
280
+
281
+ ### Running Tests
282
+
283
+ ```bash
284
+ # Run all tests (about 15 s on a CPU)
285
+ pytest
286
+
287
+ # Only the tests that check the mathematics against independent references
288
+ pytest -m math
289
+
290
+ # Only the tests that pin current behaviour (contracts, edge cases, devices, goldens)
291
+ pytest -m behaviour
292
+
293
+ # With coverage
294
+ pytest --cov=bayesian_changepoint_detection --cov-report=term-missing
295
+
296
+ # One file
297
+ pytest tests/test_online_detection.py -v
298
+ ```
299
+
300
+ Every test carries exactly one of the markers `math` and `behaviour`;
301
+ collection fails otherwise. Tests pass `device="cpu"` explicitly, because
302
+ device selection is automatic and the suite is much slower on an accelerator.
303
+ The Python blocks in this README and in `docs/` are executed as part of the
304
+ suite.
305
+
306
+ ### Code Quality
307
+
308
+ ```bash
309
+ # Lint with ruff
310
+ ruff check .
311
+
312
+ # Format code
313
+ ruff format .
314
+
315
+ # Type checking (configured, advisory: not enforced in CI)
316
+ mypy bayesian_changepoint_detection
317
+ ```
318
+
319
+ `ruff check` and `ruff format --check` are enforced in CI, together with the
320
+ test suite on Python 3.9–3.13, the example scripts, and a build job that
321
+ installs the wheel into a clean environment. See
322
+ [CONTRIBUTING.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/CONTRIBUTING.md) for the workflow and the review process.
323
+
324
+ ### Building
325
+
326
+ ```bash
327
+ # Build sdist and wheel
328
+ python -m build
329
+
330
+ # Check the metadata PyPI will see
331
+ twine check --strict dist/*
332
+ ```
333
+
334
+ ## 📊 Example Output
335
+
336
+ `examples/simple_example.py` runs both detectors on a 150-point series with
337
+ changes at 50 and 100 and saves a figure:
338
+
339
+ ```text
340
+ ============================================================
341
+ Bayesian Changepoint Detection - Simple Example
342
+ ============================================================
343
+ Generated data with 150 points
344
+ True changepoints at: [50, 100]
345
+ Running online changepoint detection...
346
+ ✓ Online detection completed
347
+ Segment starts on the MAP path: [50, 100]
348
+ Max lag-10 changepoint probability (t > 0): 0.9117
349
+ Running offline changepoint detection...
350
+ ✓ Offline detection completed
351
+ Max changepoint probability: 0.9322
352
+
353
+ Detected changepoints:
354
+ Online method: [50, 100]...
355
+ Offline method: [49, 99]...
356
+ Creating visualization...
357
+ ✓ Visualization saved as 'changepoint_detection_results.png'
358
+
359
+ ============================================================
360
+ ✅ Example completed successfully!
361
+ ============================================================
362
+ ```
363
+
364
+ Other scripts in `examples/`: `basic_usage.py` (400 points, four segments,
365
+ both detectors), `multivariate_example.py`, `gpu_acceleration.py` (device
366
+ selection and CPU/GPU comparison), `benchmark_offline.py` (offline timing at
367
+ several lengths), and the notebooks `Example_Code.ipynb` and
368
+ `Multivariate_Example.ipynb`. The scripts run in CI on every push.
369
+
370
+ ## ⚡ Performance
371
+
372
+ Both algorithms are O(T²) in the series length: the offline recursion is
373
+ vectorized per start point (one `pdf_rows` call gives the likelihood of every
374
+ segment starting there), the online recursion over run lengths at each step.
375
+ Memory is also O(T²): the run-length posterior `R` is `(T+1)²` float32, the
376
+ offline tables about `16 T²` bytes (see [docs/devices.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/docs/devices.md#memory)).
377
+
378
+ Measured on an Apple M-series laptop, CPU, 4 threads, PyTorch 2.14:
379
+
380
+ | Workload | Time |
381
+ |---|---|
382
+ | Offline `StudentT`, 1 000 points, `const_prior`, exact sum | 3.6 s (147 s before the vectorized likelihood of 1.1.0, same changepoints) |
383
+ | Online `StudentT`, 1 000 points | 0.16 s |
384
+ | Online `StudentT`, 5 000 points | 1.7 s |
385
+ | Online `MultivariateT`, 10-D, 1 000 points | 0.56 s |
386
+
387
+ Accelerators: see the FAQ; MPS is slower than the CPU on all of these, CUDA
388
+ is unmeasured (issue #43). Only measured numbers appear in this README.
389
+
390
+ ## ❓ FAQ
391
+
392
+ ### Which detector should I use, online or offline?
393
+
394
+ `online_changepoint_detection` (Adams & MacKay 2007) processes the series one
395
+ point at a time and, after each point, gives the posterior over how long the
396
+ current segment has lasted. Use it for streams, or when you want to know how
397
+ quickly a change would have been noticed. `offline_changepoint_detection`
398
+ (Fearnhead 2006) sees the whole series and returns the posterior probability
399
+ of a changepoint at each position, using data on both sides of it. Use it for
400
+ retrospective analysis; it is usually sharper. Both cost O(T²).
401
+
402
+ ### The two detectors report the same change at indices one apart. Why?
403
+
404
+ Different conventions, both documented in the docstrings:
405
+
406
+ - Online (`get_map_changepoints`, `changepoint_probabilities`,
407
+ `viterbi_changepoints`): the index of the **first point of the new
408
+ segment**. A series whose first 80 points come from one regime reports 80.
409
+ - Offline (`Pcp[j, t]`, and `torch.exp(Pcp).sum(0)[t]`): the probability that
410
+ a segment **ends at `t`**, i.e. the last point of the old regime. The same
411
+ series reports 79.
412
+
413
+ So `offline index + 1 == online index`.
414
+
415
+ ### Does the scale of my data matter? (issue #34)
416
+
417
+ Yes. The priors are on the mean and variance of the data, so their
418
+ hyperparameters have units, and rescaling the data without rescaling them
419
+ changes the model. For the univariate Normal-Gamma model (online `StudentT`
420
+ with `alpha, beta, kappa, mu`; offline `StudentT` with `alpha0, beta0,
421
+ kappa0, mu0`):
422
+
423
+ | parameter | meaning | units |
424
+ |---|---|---|
425
+ | `mu` | prior mean of a segment | data units |
426
+ | `kappa` | how many observations the prior mean is worth | none |
427
+ | `alpha` | half the number of observations the variance prior is worth | none |
428
+ | `beta` | `alpha` times the prior guess of the variance | data units² |
429
+
430
+ Multiplying the data by `c` is equivalent to using `mu * c` and `beta * c²`
431
+ with `kappa` and `alpha` unchanged. With `beta / alpha` far from the actual
432
+ within-segment variance, or `mu` far from the data, the first points of
433
+ every segment look surprising and the detector over- or under-reacts.
434
+
435
+ Practical choices: standardize the data (subtract a typical level, divide by
436
+ a typical within-segment standard deviation, ideally estimated on a
437
+ calibration window rather than on the whole series), or set `mu` to the
438
+ expected level and `beta = alpha * expected_variance`. The values in the
439
+ examples (`alpha=0.1, beta=0.01, kappa=1, mu=0`) encode "around zero,
440
+ variance about 0.1, but I am not sure": with `df = 2 * alpha = 0.2` the
441
+ predictive is extremely heavy-tailed, which is why they still work on
442
+ roughly unit-scale data.
443
+
444
+ The multivariate classes work the same way but parametrize the prior on
445
+ the covariance differently. Online `MultivariateT` takes `scale`, the
446
+ Wishart scale `W` on the *precision*: to encode a prior covariance `C` pass
447
+ `scale = inv(C) / dof` (default `I / dof`, unit prior covariance). Offline
448
+ `MultivariateT` takes `Psi0`, the inverse-Wishart scale on the *covariance*
449
+ side (`Psi0 = inv(W)`): the same prior covariance `C` is `Psi0 = dof0 * C`,
450
+ and the default `dof0 * I` is the same unit prior covariance as online.
451
+ `mu`/`mu0` are in data units in both.
452
+
453
+ ### How do I make the detector more or less sensitive? (issue #31)
454
+
455
+ In order of importance:
456
+
457
+ 1. **The hazard, i.e. the expected segment length.** `constant_hazard(lam)`
458
+ puts prior probability `1 / lam` on a change at every step. Larger `lam`
459
+ means fewer detections, more confidence needed, slightly longer delay;
460
+ smaller `lam` means more, earlier, and more false alarms. This is the main
461
+ knob and it is about the data, not the model: set it near the segment
462
+ length you expect.
463
+ 2. **How much you trust the prior versus the first points of a new segment.**
464
+ `kappa` (for the mean) and `alpha` (for the variance) act as pseudo-counts.
465
+ Small values let a few points establish a new regime quickly; larger values
466
+ make the detector wait for more evidence. `beta` and `mu` should describe
467
+ the data (previous question) rather than be used as sensitivity knobs.
468
+ 3. **How you read the output.** `changepoint_probabilities(R, lag)` trades
469
+ delay for confidence: a larger `lag` gives a more decisive probability,
470
+ `lag` observations later. `get_map_changepoints(R, min_separation=k)`
471
+ drops starts closer than `k` points to an earlier one, for when the
472
+ posterior hesitates between neighbouring points.
473
+
474
+ Offline, the equivalent of the hazard is the segment-length prior:
475
+ `const_prior(p=1/(T+1))` is the flat default; `geometric_prior(p=1/L)`
476
+ encodes an expected segment length `L`; `negative_binomial_prior` allows a
477
+ peaked length distribution. Leave `truncate` at its default: the sum is exact
478
+ and the legacy truncation can drop the dominant term.
479
+
480
+ ### My data are not normally distributed. Can I still use this? (issue #36)
481
+
482
+ Every likelihood here assumes that **within a segment** the observations are
483
+ independent and Gaussian, and it detects changes in the mean and/or the
484
+ (co)variance of that Gaussian:
485
+
486
+ | likelihood | within-segment model |
487
+ |---|---|
488
+ | online `StudentT`, offline `StudentT` | i.i.d. Normal, unknown mean and variance (Normal-Gamma prior) |
489
+ | online `MultivariateT` | i.i.d. multivariate Normal, unknown mean and covariance (Normal-Wishart) |
490
+ | offline `IndependentFeaturesLikelihood` | one Normal-Gamma model per dimension, independent |
491
+ | offline `MultivariateT` | i.i.d. multivariate Normal, unknown mean and covariance (Normal-Wishart) |
492
+ | offline `FullCovarianceLikelihood` | multivariate Normal with unknown covariance and **no mean parameter** (mean zero, Xuan & Murphy 2007): it detects covariance changes; segments that differ in mean are misread as scale changes, so use `MultivariateT` when means move |
493
+
494
+ When the data are not Gaussian the detector still runs, and the question is
495
+ what the misspecification does to it:
496
+
497
+ - **Heavy tails or outliers**: single extreme points look like the start of
498
+ a new segment. The Student-t predictive already tolerates some of this;
499
+ a larger `lam` or `kappa` helps, and so does a transform (log for positive,
500
+ right-skewed quantities such as latencies or prices).
501
+ - **Counts or bounded data**: a variance-stabilizing transform (square root
502
+ or Anscombe for counts, logit for proportions) usually gets you close
503
+ enough. A Poisson likelihood is on the roadmap (issue #23).
504
+ - **Autocorrelation or slow drift**: the model has no notion of dynamics
505
+ within a segment, so a drift is reported as a sequence of small changes.
506
+ Differencing, or modelling residuals from a trend, is the usual fix.
507
+ - **Changes in something other than mean or variance** (e.g. in
508
+ autocorrelation) are not detected.
509
+
510
+ In short: use it when "piecewise stationary with Gaussian-ish noise" is a
511
+ reasonable description after a transform, and check on a segment you trust
512
+ that the residuals look plausible.
513
+
514
+ ### Why is it slow on my laptop with a GPU?
515
+
516
+ Device selection is automatic and prefers CUDA or Apple MPS when present, but
517
+ the online recursion is a sequential loop over small tensors, and each step
518
+ on an accelerator pays a launch cost. Measured on an Apple M-series laptop
519
+ (PyTorch 2.14), CPU against MPS:
520
+
521
+ | workload | CPU | MPS |
522
+ |---|---|---|
523
+ | online `StudentT`, 1 000 points | 0.16 s | 2.5 s |
524
+ | online `StudentT`, 5 000 points | 1.7 s | 11 s |
525
+ | online `MultivariateT`, 10-D, 1 000 points | 0.56 s | 17 s |
526
+
527
+ The offline detector needs float64 and always runs on the CPU when MPS is
528
+ selected. Pass `device="cpu"` to both the likelihood and the detector unless
529
+ you have measured otherwise on your hardware; CUDA has not been benchmarked
530
+ (issue #43).
531
+
532
+ ## 🤝 Contributing
533
+
534
+ Contributions are welcome. Please see the [Contributing Guidelines](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/CONTRIBUTING.md)
535
+ for the development setup, the conventions (including the rule that a test
536
+ pinning a number says where the number comes from) and the review process.
537
+
538
+ 1. Fork the repository
539
+ 2. Create a feature branch (`git checkout -b feat/amazing-feature`)
540
+ 3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
541
+ 4. Push to the branch (`git push origin feat/amazing-feature`)
542
+ 5. Open a Pull Request
543
+
544
+ ### Project documentation
545
+
546
+ | Document | Contents |
547
+ | --- | --- |
548
+ | [CONTRIBUTING.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/CONTRIBUTING.md) | Development setup, conventions, releasing |
549
+ | [CHANGELOG.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/CHANGELOG.md) | Release history, including the numerical changes in 1.1.0 |
550
+ | [AGENTS.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/AGENTS.md) | Conventions for AI coding agents: the two `StudentT`s, index conventions, changing the math |
551
+ | [SECURITY.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/SECURITY.md) | How to report a vulnerability |
552
+ | [CODE_OF_CONDUCT.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/CODE_OF_CONDUCT.md) | Community standards |
553
+ | [docs/devices.md](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/docs/devices.md) | CPU, CUDA and MPS: device resolution, measurements, memory |
554
+
555
+ ## 📄 License
556
+
557
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/LICENSE) file for details.
558
+
559
+ ## 🔗 Related Resources
560
+
561
+ - Ryan P. Adams and David J. C. MacKay (2007). *Bayesian Online Changepoint Detection*. arXiv:0710.3742. <https://arxiv.org/abs/0710.3742> — the online algorithm.
562
+ - Paul Fearnhead (2006). *Exact and Efficient Bayesian Inference for Multiple Changepoint Problems*. Statistics and Computing 16(2), 203–213. <https://doi.org/10.1007/s11222-006-8450-8> — the offline algorithm.
563
+ - Xiang Xuan and Kevin Murphy (2007). *Modeling Changing Dependency Structure in Multivariate Time Series*. ICML 2007, 1055–1062. <https://doi.org/10.1145/1273496.1273629> — the multivariate likelihoods.
564
+ - Kevin P. Murphy (2007). *Conjugate Bayesian analysis of the Gaussian distribution*. Technical note. <https://www.cs.ubc.ca/~murphyk/Papers/bayesGauss.pdf> — the Normal-Gamma and Normal-Wishart closed forms used in the likelihoods.
565
+
566
+ ## 🙏 Acknowledgements
567
+
568
+ - **Johannes Kulick** wrote the original NumPy implementation (2014–2022), published as `bayesian-changepoint-detection` and then `bayescd`, and owns this repository.
569
+ - **Esteban Carisimo** did the PyTorch rewrite, the vectorized recursions, the verified likelihoods and the current maintenance.
570
+
571
+ ### Citation
572
+
573
+ If you use this library in your research, please cite it (GitHub's "Cite
574
+ this repository" button reads [CITATION.cff](https://github.com/hildensia/bayesian_changepoint_detection/blob/master/CITATION.cff)):
575
+
576
+ ```bibtex
577
+ @software{bayesian_changepoint_detection,
578
+ title = {Bayesian Changepoint Detection: A PyTorch Implementation},
579
+ author = {Kulick, Johannes and Carisimo, Esteban},
580
+ url = {https://github.com/hildensia/bayesian_changepoint_detection},
581
+ year = {2026},
582
+ version = {1.1.0}
583
+ }
584
+ ```
585
+
586
+ The algorithms are due to Adams & MacKay (2007) and Fearnhead (2006); please
587
+ cite those papers as well.