Changelog¶
All notable changes to this project are documented here. The format follows Keep a Changelog, and the project uses Semantic Versioning.
[Unreleased]¶
[1.2.1] — 2026-09-23¶
No code change: the library behaves exactly as 1.2.0.
Changed¶
- Removed a maintainer's personal email address from the package metadata,
CODE_OF_CONDUCT.mdandSECURITY.md; private reports now start with an issue asking for a private channel.
[1.2.0] — 2026-09-23¶
Added¶
-
benchmarks/tcpd.py: detection quality on the Turing Change Point Dataset (van den Burg and Williams, 2020) with TCPDBench's protocol and metrics (F1 with margin 5 and covering, against five annotators;benchmarks/metrics.py, ported from TCPDBench and tested against its examples). The series are downloaded from a pinned TCPD commit and checked against TCPD's checksums, not copied into the repository. On the 30 univariate series with a published BOCPD score, the online detector's MAP segmentation reproduces TCPDBench's BOCPD (F1 0.694 against 0.696 with defaults, 0.887 against 0.890 tuned; identical F1 on 27 of the 31 series both score, which add the 2-Drun_log), and the offline detector scores higher untuned (F1 0.739, cover 0.664). Results inbenchmarks/results/2026-09-23-tcpd.json. -
benchmarks/performance.py: reproducible timings of the offline, online and streaming detectors against the released versions 0.4 (NumPy), 1.0.0 and 1.1.0 on the same series and parameters, each case in a fresh process, with every run scored against the true changepoints. Protocol inbenchmarks/README.md; the README's performance table comes frombenchmarks/results/2026-09-23-apple-m1-cpu.json(1.2.0 re-measured; the older versions from2026-09-22-apple-m1-cpu.json). It showed that 1.0.0's multivariate online detector misses almost every change (F1 0.04), that 0.4's multivariate online detector crashes, and that the offline changepoint table is O(T³). CI runs the quick suite on the current code so the harness cannot rot. -
negative_binomial_hazard(k, p, r)(issue #23): the hazard of negative binomial segment lengths,P(length = r + 1) / P(length >= r + 1), the online counterpart ofnegative_binomial_priorso both detectors can share one prior on segment length.k = 1isconstant_hazard(1 / p);k > 1makes short segments unlikely. Checked againstscipy.stats.nbinomand against the offline prior. segment_statistics(data, starts)and itsSegmentStatisticsresult: per-segment mean and standard deviation, and for each changepoint the change in mean (its direction) and a Welch z-score (issue #42).online_likelihoods.NormalKnownVarianceandoffline_likelihoods.NormalKnownVariance(issue #23): Normal observations with a known variance and a conjugate Normal prior on the mean, for mean changes at a known noise level (Murphy 2007, section 2). The offline marginal is checked againstscipy.stats.multivariate_normalwith the joint covariance and against the chain rule; the online predictive againstscipy.stats.norm.online_likelihoods.Poissonandoffline_likelihoods.Poissonfor count data (issue #23): Poisson counts with a conjugate Gamma prior on the rate, negative-binomial predictive online, closed-form segment marginal offline (Gelman et al., BDA3, section 2.6). Checked againstscipy.stats.nbinomand against the chain-rule product of predictives. The online class supportsmax_run_lengthinOnlineChangepointDetector.OnlineChangepointDetector, the online detector one observation at a time for streams of unknown length (issue #13):update(x),run_length_posterior,map_run_length,changepoint_probability(lag).max_run_lengthbounds memory and time per observation by dropping run lengths above the bound and renormalizing. Checked against the NumPy reference with and without the bound, and againstonline_changepoint_detection. Online likelihoods gainprune(n), driven by a_run_length_stateclass attribute.- Documentation site built with MkDocs (Material theme, API reference
generated from the docstrings by mkdocstrings), checked with
mkdocs build --strictin CI. Its pages include sections of the README, so the two stay in sync. Thedocsextra now installs MkDocs instead of Sphinx, which had no configuration in the repository. Published at https://estcarisimo.github.io/bayesian_changepoint_detection/.
Fixed¶
- The example notebooks run again:
Multivariate_Example.ipynbimportedIndepentFeaturesLikelihood(a typo forIndependentFeaturesLikelihood) and applied NumPy functions to tensors; both passed the deprecatedtruncate, andExample_Code.ipynbstill recommended it for speed. Their stored outputs (from much older versions) are cleared. CI now runs their code cells (examples/run_notebooks.py). to_tensor/ensure_tensorkeep float64 precision when nodtypeis given: float64 NumPy arrays, Python floats and float64 tensors stay float64 (float32 on MPS); integer and boolean input still becomes float32. Before, every non-tensor became float32, so a float64 NumPy series far from zero was quantized (steps of 8 near 1e8) before the detectors' float64 or centered arithmetic could help; offline and online results for such input now equal those for the float64 tensor.to_tensor(t)on a float64 tensor now returns float64 rather than downcasting. Complex input keeps a complex dtype, so the detectors reject it ("data must be real") instead of silently dropping the imaginary part.- Online
StudentT,MultivariateTandNormalKnownVariancekeep their float32 state relative to the first observation. For data far from zero the state used to cancel: with unit noise at an offset of 1e6 the run-length posterior moved by up to 0.84 and three spurious changepoints appeared. With float64 input the posterior now matches a float64 reference to 3e-6 at offsets up to 1e8.muis a read-only property in data units; the state thatpruneslices is_mu_c. - Offline likelihoods stay exact for data far from zero (issue #55). The
prefix sums are now taken on data centered on its mean, and the
multivariate determinants use the matrix determinant lemma for the
rank-one mean term. Against exact rational arithmetic the error at a data
offset of 1e8 drops from 44 nats (
StudentT, prior mean at the data), 17 (MultivariateT), 16 (FullCovarianceLikelihood) and 1.6 (IndependentFeaturesLikelihood) to float64 rounding, out to offsets of 1e10. Results for ordinary data change only at the level of rounding, and run time is unchanged. - The three detectors validate their input in one place, before any work:
data must be
[T]or[T, D], non-empty, real, finite and, when the likelihood declaresdims, have that many components per observation. Violations raiseValueError. Before, a 0-d tensor raisedIndexError, complex data ran, and offlineMultivariateT(dims=3)silently accepted univariate data or a transposed[3, T]tensor (read as three observations of dimension T). A[D, T]tensor now gets a "pass data.T" hint.[T]data with adims=1multivariate likelihood is taken as[T, 1](the online detector used to fail on it). Valid inputs give the same results as before. -
Offline
MultivariateTno longer overwritesdims=Nonewith the first series' dimension; an explicitdimsthat disagrees with the data raisesValueErrorfrompdfandpdf_rowstoo. -
constant_hazard(lam, r)raisesValueErrorforlam < 1and for NaN. It used to accept any positivelam, so0 < lam < 1returned a hazard1 / lamabove 1, which is not a probability and made the online detector's growth probabilities negative.lam >= 1behaves as before.
Changed¶
- The offline changepoint table
Pcpis up to 19x faster and no longer cubic in practice: the part of each row's sum that does not depend on the changepoint index is built once, and rows stop once the probability of that many changepoints falls belowexp(-1000)(later rows are left-inf; their true values are smaller still, and 0 in float64 either way). Measured on an Apple M1 (CPU): 1 000 points 2.7 s to 0.53 s, 2 000 points 20 s to 2.1 s, 4 000 points 158 s to 8.3 s; 5-DMultivariateT, 2 000 points, 22 s to 3.0 s. Same changepoints in every case; computed entries agree with a direct evaluation of the recursion to 1e-12, andexp(Pcp)is unchanged. - The default device is the CPU.
get_device(None), and therefore every likelihood, prior, hazard and generator built withoutdevice, now returns the CPU; up to 1.1.0 it picked CUDA, then MPS, when present.get_device("auto")keeps the old choice.offline_changepoint_detectionnow follows the likelihood's device whendeviceis omitted, as the online detectors already did, so naming a device once on the likelihood is enough for both. Only where results are computed changes (CPU and accelerator results agree to float32 rounding, not bit for bit). On the hardware measured (Apple M-series), the CPU was 6-30x faster than MPS for the online detector, and the offline detector could not use MPS. Users who relied on automatic GPU placement should passdevice="auto"(or"cuda") to the likelihood. - The test marker
behaviouris nowbehavior(pytest -m behavior), under the new rule that the project's identifiers use American English. No library names changed; none used British spellings. - The release workflow uploads from the
pypiGitHub environment (testpypifor rehearsals), so each upload is listed under "Deployments" on the repository page. Nothing about the published package changes. - CI measures test coverage on every Python version, fails below 94%, and
shows the per-module table in the run summary. New tests for the synthetic
data generators took
generate_datafrom 42% to 97% coverage.
[1.1.0] — 2026-09-22¶
Changed (breaking)¶
online_changepoint_detectionnow returns(R, map_run_lengths). The second value is the most likely run length after each observation (argmaxof each column ofR, dtypelong), which is what versions before 1.0 returned asmaxes. Versions 1.0.x returned the un-normalizedR[0, t]under the namechangepoint_probs; that quantity scales with the data evidence and, once normalized, equals the hazard rate at every step under a constant hazard, so it could not detect changepoints (#41, #18). Code that thresholded it must switch toget_map_changepointsorchangepoint_probabilities.get_map_changepoints(R)now reports the data indices at which the MAP run-length path implies a new segment started. Itsthresholdargument is deprecated and ignored (it thresholdedR[0, :]); a newmin_separationargument merges nearby starts when the posterior flips between them.offline_likelihoods.MultivariateTdefault prior:Psi0(the covariance-side scale,inv(W)) is nowdof0 * I, unit prior covariance, the same prior as the online class; it wasI,dof0times tighter (#75). Changepoint locations on the multivariate test series are unchanged; a spurious 0.33 bump on the 2-D regression series drops to 0.07. ExplicitPsi0arguments are unaffected.online_likelihoods.MultivariateTdefault prior: the Wishart scale is nowI / dof, giving unit prior covariance as the documentation always claimed. The previousIencoded a prior covariance ofI / dof. Explicitscalearguments are unaffected.offline_likelihoods.StudentTgains keyword-only prior hyperparametersalpha0,beta0,kappa0,mu0(defaults reproduce the previous prior).- Distribution renamed to
bayesian-changepoint(pip install bayesian-changepoint). The import name is unchanged:import bayesian_changepoint_detection. The same project was published asbayescdup to 0.4 (April 2022) and asbayesian-changepoint-detectionbefore that (0.2.dev1); both stay frozen at those releases. Earlier drafts of this changelog and of the packaging metadata calledbayesian-changepoint-detectionan unrelated project; that was wrong, it is this project's own older distribution.__version__reads the new distribution's metadata and falls back tobayescdfor an older install.setup.py,setup.cfg,requirements.txtanduv.lockwere removed;pyproject.tomlis the single source of metadata (license as an SPDX expression withlicense-files, per PEP 639; setuptools >= 77).numpyandscipymoved to thedevextra,matplotlib/seaborn/numpyto a newplotextra; the library itself depends only ontorch. Python 3.8 dropped.
Added¶
viterbi_changepointsandcompute_run_length_posteriorare exported from the package.viterbi_changepointsis now a vectorized max-product pass that returns the most probable run-length path (the MAP segmentation under the BOCPD model), verified against an exhaustive search over segmentations; it runs in the same time as the forward pass (about 40x faster than before on 160 points). The previous version summed over predecessors in the changepoint transition, so its path scores were neither the forward pass nor Viterbi, and it had no tests. Input validation as for the other detectors.changepoint_probabilities(R, lag):P(a new segment started at t)judgedlagobservations later, i.e.R[lag, t + lag], the quantity the original notebook plotted asR[Nw, Nw:].- Offline likelihoods expose
pdf_rows(data, t), returning the log marginal likelihood of every segment starting attin one vectorized call, andsetup(data)to precompute sufficient statistics. - Likelihood models can be moved between devices with
.to(device). - ruff for linting and formatting (
[tool.ruff]inpyproject.toml, alintCI job,.pre-commit-config.yaml); black, isort and flake8 are no longer used and.flake8is gone. The whole tree was formatted once. - CI job
examples: runs every example script headless with theplotextra installed, so examples cannot drift from the API unnoticed. - CI workflow (GitHub Actions) running the test suite on Python 3.9–3.13,
plus a
buildjob: sdist and wheel,twine check --strict, and a smoke test that installs the wheel into a clean environment and runs both detectors. AGENTS.mdwith repository conventions.CHANGELOG.md(this file).- README FAQ: online vs offline, the one-index difference between their conventions, data scaling and prior units (#34), sensitivity (#31), non-Gaussian data (#36), CPU vs accelerator.
- Test kind markers
math(checked against an independent reference) andbehaviour(pins current behaviour); every test carries exactly one andtests/conftest.pyfails collection otherwise.pytest -m mathruns the 87 tests that would fail if the mathematics were wrong. - Releases publish to PyPI with Trusted Publishing (OpenID Connect):
cd.ymlbuilds the sdist and wheel, checks the version against the tag, uploads withpypa/gh-action-pypi-publishand attaches both files to the GitHub release. No PyPI token or password is stored in the repository; the 2022 username/password secrets are no longer used. A manual run of the workflow can upload to TestPyPI instead. - README rewritten: what the library computes, a working install,
online/offline/multivariate
examples whose printed outputs are the real ones and which run in the test
suite, how to read
R, an API table, the package layout, development commands, captured example output, measured performance only, the FAQ, and full references. Gone: three installation methods and a troubleshooting section, the "Migration from v0.4" section (the old module path it showed does not exist), and the remaining unmeasured GPU claims. docs/devices.md: how thedeviceargument is resolved by each detector, the MPS-to-CPU fallback of the offline detector, what has been measured, how to time your own workload, and the memory footprint of the run-length and offline tables. It replacesdocs/gpu_offline_detection_guide.mdanddocs/gpu_online_detection_guide.md(2 500 lines written for the PyTorch port and never executed: they unpacked the removedchangepoint_probsoutput, thresholded it at 0.5, linked to a guide that did not exist, and recommended GPUs for series above 1 000 points on the strength of speedups nobody had measured; #56). A test executes every Python block underdocs/so the prose cannot drift from the API again.maintainersinpyproject.tomllists both maintainers.CONTRIBUTING.md(setup, conventions, themasterrule, releasing) and an explicit.github/dependabot.yml(GitHub Actions and pip, weekly, grouped; pip usesincrease-if-necessaryso lower bounds are not bumped needlessly).- Community health files:
CODE_OF_CONDUCT.md(Contributor Covenant 2.1),SECURITY.md,CITATION.cff,.github/CODEOWNERS, issue templates and a pull request template.
Fixed¶
offline_changepoint_detectionevaluates the sum over segment ends in full;truncatenow defaults to-infand is deprecated. The legacy rule (cut at the first term 40 nats below the running sum, inherited from the NumPy original) assumed the terms decay after a peak, but for a start inside a segment they dip and then rise to the true end; with multivariate likelihoods the cut discarded that dominant term and returned changepoint "probabilities" around 1e31 (10-D example with three changes:MultivariateTandIndependentFeaturesLikelihoodboth). Since the segment likelihoods are computed for every end in one vectorized call, truncation saved no work either. Univariate results are unchanged.- Offline recursion (
offline_changepoint_detection) now indexes the segment-length prior correctly (Fearnhead 2006, eq. 2): the first changepoint row evaluatedgat length minus one, the later rows pairedgwith the wrong segment length, and the tail term1 - Gincluded a "length 0" term. None of this was visible withconst_prior; with the geometric and negative binomial priors the changepoint posterior was wrong. Verified against an exhaustive enumeration of all segmentations (tests/test_offline_prior_recursion.py). Withconst_priorthe log evidence moves by about 0.01 and changepoint locations do not move. geometric_priorwas off by one ((1-p)^t pinstead of the documented(1-p)^(t-1) p) and raised at length 0, which made it unusable with the offline detector;negative_binomial_priorhadpand1 - pswapped, sok = 1did not reduce to the geometric prior. Both now matchscipy.stats.geom/scipy.stats.nbinom; impossible lengths return-inf.online_changepoint_detectionraises aValueErrorfor empty input and forNaN/Infvalues, like the offline detector andviterbi_changepoints; it used to return a 1x1 posterior, or a non-finiteRwhose MAP run length is 0 at every step.offline_changepoint_detectionraises aValueErrorfor empty input, forNaN/Infvalues, and for a length prior whose mass on lengths1..T-1exceeds 1 (e.g.const_prior(p=0.25)on more than five points), instead of returningnanevidence and all-zero changepoint probabilities.- Offline
StudentTnow evaluates the exact Normal-Gamma marginal likelihood of a segment (Murphy 2007, eqs. 95–97) instead of scoring each point under the posterior of the whole segment. Offline detection is 70–150x faster (1000 points: about 3 minutes to under 2 seconds on CPU) and recovers changepoints the previous code missed (#47). - The offline recursion runs in float64 (float32 on MPS, with a CPU fallback for the recursion itself).
- Online
MultivariateTnow keeps the inverse Wishart scaleT = W^{-1}as its state (scale_inv, Murphy 2007 eq. 255) and evaluates the predictive through a Cholesky factor ofT; no matrix is inverted per step. The previous update invertedW + 1e-6 Iand the result back every step, and on a shrinkingWthat regularizer compounds: on 2-D standard normal data (numpy.random.default_rng(0)), after 500 stationary points the posterior scale was 7.6% off and the log predictive 0.06 nats off, after 3000 points 67% and 1.0 nats (#59).scaleis still available as a property (inv(scale_inv)). About 1.5-2x faster in 10-D. - Online
MultivariateTpredictive used the Wishart scaleWwhereW^{-1}belongs (Murphy 2007, eq. 258), so the predictive covariance shrank with every observation and the run-length posterior collapsed to run lengths 1–3 even on stationary data. Verified against an independent NumPy/scipy implementation of the recursion. constant_hazardreturned a tensor on the auto-selected device instead of the caller's, andonline_changepoint_detectiondid not move the model and data to one device; both crashed on machines with MPS or CUDA even withdevice="cpu".IndependentFeaturesLikelihoodandFullCovarianceLikelihoodproducednanfor univariate input (zero-variance length-one segments).examples/example.pyremoved: it still used the pre-1.0 NumPy module layout (offline_changepoint_detectionmodule,const_prior(l=...)) and had not run since the PyTorch rewrite;basic_usage.pyandsimple_example.pycover the same ground..idea/(IDE settings) and two example figures were tracked at the repository root; removed, and root-level*.pngis now ignored.- Root-level
test.pywas never collected by pytest; its tests now live intests/test_online_detection.pywith the pre-1.0 assertions restored.
[1.0.0] — 2025-11-06¶
- PyTorch rewrite of the library (#46). Not published to PyPI.
[0.4] — 2022-04-05¶
- Last NumPy release, published to PyPI as
bayescd. (Earlier releases used the distribution namebayesian-changepoint-detection.)