Offline Algorithms#
Offline (batch) algorithms process the entire signal at once and return a set of detected change points. changepoint-doctor provides 9 offline algorithms spanning exact, pruned, greedy, and non-parametric approaches.
PELT (Pruned Exact Linear Time)#
PELT [1] is the recommended default for most use cases. It uses dynamic programming with a pruning rule that achieves O(n) complexity when the number of change points grows linearly with n.
When to use: Strong default when n is large and the number of change points is unknown.
Complexity: O(n) expected with pruning; O(n^2) worst case.
import cpd
result = cpd.Pelt(model="l2", min_segment_len=2).fit(x).predict(pen="bic")
print(result.breakpoints)
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Cost model: |
|
|
|
Minimum segment length |
|
|
|
Candidate stride (increase to trade accuracy for speed) |
|
|
|
Hard cap on detected change points |
BinSeg (Binary Segmentation)#
Binary Segmentation [2] is a greedy top-down approach that recursively splits the signal at the point of maximum cost reduction.
When to use: Fast approximate segmentation; good when speed matters more than optimality.
Complexity: O(n log n) for k change points.
Masking risk: BinSeg can miss closely spaced changes (masking). If diagnostics indicate masking risk, prefer WBS.
result = cpd.Binseg(model="l2", min_segment_len=2).fit(x).predict(n_bkps=3)
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Cost model |
|
|
|
Minimum segment length |
|
|
|
Candidate stride |
|
|
|
Hard cap |
|
|
|
Maximum recursion depth |
FPOP (Functional Pruning Optimal Partitioning)#
FPOP [3] uses functional pruning for optimal partitioning with L2 cost. It provides strong pruning behavior and exact solutions.
When to use: You want optimal partitioning with strong pruning; L2 cost only.
Complexity: O(n) expected.
result = cpd.Fpop(min_segment_len=2).fit(x).predict(pen="bic")
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Minimum segment length |
|
|
|
Candidate stride |
|
|
|
Hard cap |
Note
FPOP is restricted to L2 cost. Specifying a different cost model will raise an error.
SegNeigh / DynP (Exact Dynamic Programming)#
Exact DP segmentation for a known number of change points k. Finds the globally optimal k-segmentation.
When to use: You know the exact number of change points and want guaranteed optimality.
Complexity: O(k * m^2) time, O(k * m) memory, where m is the effective candidate count.
result = cpd.detect_offline(
x,
detector="segneigh", # "dynp" is an alias
cost="l2",
constraints={"min_segment_len": 2},
stopping={"n_bkps": 3},
)
Sizing guide:
Use when k is known and m is modest
Increase
jumpand/ormin_segment_lento reduce m when runtime is too highPrefer PELT/FPOP when k is unknown or n is very large
WBS (Wild Binary Segmentation)#
WBS [4] draws random intervals and applies CUSUM-like tests within each, providing robustness against the masking effect that plagues standard BinSeg.
When to use: Closely spaced changes where BinSeg might mask weaker signals.
Complexity: O(M * n) where M is the number of random intervals.
result = cpd.detect_offline(
x,
pipeline={
"detector": {"kind": "wbs"},
"cost": "l2",
"stopping": {"pen": "bic"},
"constraints": {"min_segment_len": 2},
},
)
Note
WBS is available via detect_offline(pipeline=...) but not yet as a high-level Python class.
BottomUp#
Bottom-up segmentation starts with many short segments and iteratively merges adjacent segments with the lowest merge cost.
When to use: Complementary to BinSeg; can detect changes that top-down methods miss.
result = cpd.detect_offline(
x,
pipeline={
"detector": {"kind": "bottomup"},
"cost": "l2",
"stopping": {"pen": "bic"},
"constraints": {"min_segment_len": 2},
},
)
SlidingWindow#
Computes a local discrepancy score by sliding a window across the signal and comparing left/right sub-windows.
When to use: Quick visual scan of where changes might be; useful as a feature for downstream models.
result = cpd.detect_offline(
x,
pipeline={
"detector": {"kind": "sliding_window"},
"cost": "l2",
"stopping": {"pen": "bic"},
"constraints": {"min_segment_len": 2},
},
)
KernelCpd (experimental)#
Kernel-based change-point detection [5] uses RKHS embeddings for non-parametric detection. It can detect distributional changes beyond mean/variance shifts.
When to use: Non-parametric detection where the type of distributional change is unknown.
result = cpd.detect_offline(
x,
pipeline={
"detector": {"kind": "kernel_cpd"},
"cost": "kernel",
"stopping": {"n_bkps": 2},
"constraints": {"min_segment_len": 5},
},
)
Warning
KernelCpd is experimental. Requires the kernel feature flag.
GpCpd / ArgpCpd (experimental)#
Gaussian process change-point detection [6] uses GP marginal likelihood for change detection, modeling the signal as piecewise-stationary GP segments.
When to use: Smooth signals where GP models are appropriate and you need change detection that respects temporal correlation.
Warning
GpCpd and ArgpCpd are experimental. Requires the gp feature flag.
Stopping criteria#
All offline detectors support multiple stopping strategies:
Stopping |
Python usage |
When to use |
|---|---|---|
Known k ( |
|
You know the expected number of changes |
Manual penalty |
|
Tight operational control over sensitivity |
BIC [7] |
|
Good default for automatic model selection |
AIC [8] |
|
Less conservative than BIC; recovers weaker changes |
PenaltyPath |
|
Sweep multiple penalties in one pass |
Penalty selection guidance#
Scenario |
Recommended stopping |
|---|---|
Unknown number of changes, moderate noise |
|
Unknown number, want to find weaker changes |
|
Known number of changes |
|
Operational threshold tuning |
|
Exploratory analysis |
|
BIC/AIC complexity terms are cost-model-aware:
l2:params_per_segment=2(mean + residual variance proxy)normal:params_per_segment=3(mean + variance + residual term)normal_full_cov: model-aware complexity1 + d + d(d+1)/2