scan-py documentation
Documentation for the scan-py change-point detection framework. This consists of the complete documentation with usage examples.
SCAN
- scan.detector.scan_cpd(x: Iterable[float], window_sizes: Sequence[int] | None = None, alpha: float = 0.05, n_boot: int = 400, vote_threshold: float = 0.5, min_window: int = 15, max_window: int | None = None, block_length: int | None = None, taper: str = 'tukey', ipm: str = 'wasserstein', tolerance_distance: int | None = None, random_state: int | None = None, n_jobs: int | None = -1, return_all: bool = True, *, change_type: str | None = None, batch_size: int = 32) ScanResult
Run SCAN / Ensemble SCAN and return a structured result.
- Parameters:
x – One-dimensional time series as input.
window_sizes – List of window sizes that we need to run the ensemble model. If omitted, set of windows are derived from
min_windowandmax_windowusing builtin_default_window_sizesfunction.alpha – Significance level for the hypothesis test.
n_boot – Number of tapered block bootstrap draws per local comparison.
vote_threshold – Minimum ensemble vote score required to retain a candidate change point.
min_window – Bounds to be used when
window_sizesis omitted.max_window
block_length – Optional bootstrap block length. If omitted, it is selected automatically using the
n^(1/3)rule.taper – Taper weights to be applied to tapered bootstrap blocks. Supported values are
"tukey"for Tukey’s weights and"none"for classical block bootstrap.ipm – IPM discrepancy measure used to detect changes. Currently, this supports
"wasserstein"distance.tolerance_distance – Optional maximum distance for grouping nearby candidates during ensemble voting. This defaults to the minimum window size in the
window_sizeslist.random_state – Optional For reproducibility.
n_jobs – Number of worker threads used for parallel processing. The default
-1uses all available CPU threads.return_all – When
False, omit per-window diagnostics.change_type – Explicit change type:
"mean","var", or"distribution".batch_size – Number of local comparisons batched per worker.
- Returns:
Structured result containing selected change points, vote scores, per-window diagnostics, parameters, and runtime metadata.
- Return type:
ScanResult
ScanResult output
The scan_cpd function returns a ScanResult object. This object collects
the main outputs of the SCAN procedure in one place, including the final
detected change points, detection scores, voting information, per-window
diagnostics, threshold values, input parameters, metadata, and the raw backend
output.
Attribute |
Type |
Description |
|---|---|---|
|
|
Final set of estimated change-point locations returned by SCAN framework.
This is the main output users usually need for downstream analysis or plotting.
|
|
|
Detection scores associated with candidate change points.
These help assess the relative strength of detected changes.
|
|
|
Raw vote count by candidate change point.
This is useful for understanding how stable a detection is across multiple window sizes.
|
|
|
Diagnostic information from each individual window size.
This helps inspect which window sizes contributed to each detection.
|
|
|
Bootstrap thresholds and local statistics keyed by window size.
These are important for reproducibility and for understanding the rejection rule.
|
|
|
Parameter values used in the call to
scan_cpd.This makes the result self-contained and easier to reproduce.
|
|
|
Additional run information, such as runtime and backend details.
This is useful for experiments, reporting, debugging, and reproducibility.
|
|
|
Segment metadata returned by the backend, when available.
This is useful for advanced inspection of backend segmentation output.
|
|
|
Raw output returned by the Rust/PyO3 backend before post-processing.
This is mainly useful for advanced users, debugging, or development.
|
The object returned by scan_cpd stores the main outputs of the detection
procedure as attributes. Each attribute can be accessed using the dot operator
(.), which makes it easy to inspect detected change points, detection
scores, voting information, run parameters, metadata, and per-window results.
Example
# Simulate the time series
T = 200_000 # Length of the time series
K = 100 # Number of change-points
min_seg_len = 1000 # Minimum distance between two change-points
seed = 2000 # Seed for reproducibility
x, true_cps, _, _ = simulate_time_series(
n=T,
n_cps=K,
min_seg_len=min_seg_len,
change_type="mean",
seed=seed,
)
# Detect change-points
from scan import choose_window_sizes
window_sizes = choose_window_sizes(
series_length=200_000,
n_windows=7,
seed=500,
)
- scan.detector.scan_single_window(x: Iterable[float], window_size: int, *, alpha: float = 0.05, n_boot: int = 400, block_length: int | None = None, taper: str = 'tukey', ipm: str = 'wasserstein', random_state: int | None = None, change_type: str | None = None, batch_size: int = 32) WindowResult
Run SCAN for one window size and return per-location diagnostics.
- Parameters:
x – One-dimensional time series to detect change-points.
window_size – Window size used for every local comparison.
alpha – Detection controls with the same meaning as in
scan_cpd().n_boot – Detection controls with the same meaning as in
scan_cpd().block_length – Detection controls with the same meaning as in
scan_cpd().taper – Detection controls with the same meaning as in
scan_cpd().ipm – Detection controls with the same meaning as in
scan_cpd().random_state – Detection controls with the same meaning as in
scan_cpd().change_type – Detection controls with the same meaning as in
scan_cpd().batch_size – Detection controls with the same meaning as in
scan_cpd().
- Returns:
Diagnostics for the supplied window size, including local statistics, thresholds, localized regions, and candidate change points.
- Return type:
WindowResult
WindowResult output
The scan_single_window function returns a WindowResult object. This
object contains the diagnostics for one scan window size.
Attribute |
Type |
Description |
|---|---|---|
|
|
Window size used by the local scan.
This identifies which scan scale produced the diagnostics.
|
|
|
Candidate change-points detected by the model.
This is useful for inspecting detections before ensemble voting.
|
|
|
Start indices of local windows evaluated by the backend.
These align each statistic and threshold with its local comparison.
|
|
|
Local discrepancy statistics aligned with
starts.These help inspect where the local scan found strong evidence of change.
|
|
|
Bootstrap thresholds aligned with
starts.These show the threshold each local statistic was compared against.
|
Example
import numpy as np
from scan import scan_single_window
x = np.r_[np.zeros(100), np.ones(100)]
window_result = scan_single_window(x, window_size=20, n_boot=100)
print(window_result.change_points)
Utils for SCAN
- scan.metrics.covering_metric(true_cps: Iterable[int], estimated_cps: Iterable[int], n: int) float
Compute a weighted segment-covering score in
[0, 1].Each true segment is compared with all estimated segments using intersection-over-union, then weighted by the true segment length uaing Jaccard distance between the segments.
- Parameters:
true_cps – Ground-truth split locations.
estimated_cps – Estimated split locations.
n – Total series length.
- Returns:
Weighted covering score, where 1 is perfect segment agreement.
- Return type:
float
Example
from scan import covering_metric
score = covering_metric(true_cps=[100, 200], estimated_cps=[98, 205], n=300)
print(score)
- scan.metrics.precision_recall_cpd(true_cps: Iterable[int], estimated_cps: Iterable[int], tolerance: int = 10) Tuple[float, float]
Return tolerant precision and recall.
Empty sets are handled in the usual change-point evaluation convention: precision is 1 when there are no estimates and no true points, and recall is 1 when there are no true points and no estimates.
Example
from scan import precision_recall_cpd
precision, recall = precision_recall_cpd([100, 200], [98, 205], tolerance=10)
print(precision, recall)
- scan.metrics.f1_score_cpd(true_cps: Iterable[int], estimated_cps: Iterable[int], tolerance: int = 10) float
Return tolerant F1 score for change-point estimates.
- Parameters:
true_cps – Ground-truth and estimated split locations.
estimated_cps – Ground-truth and estimated split locations.
tolerance – Maximum absolute distance allowed when matching points.
- Returns:
Harmonic mean of tolerant precision and recall.
- Return type:
float
Example
from scan import f1_score_cpd
f1 = f1_score_cpd([100, 200], [98, 205], tolerance=10)
print(f1)
- scan.simulator.choose_window_sizes(series_length: int, n_windows: int = 7, seed: int = 500) list[int]
Choose a reproducible set of safe scan window sizes.
- Parameters:
series_length – Length of the series that will be scanned.
n_windows – Maximum number of window sizes to return.
seed – Random seed used when sampling from candidate sizes.
- Returns:
Sorted scan window sizes.
- Return type:
list[int]
Example
from scan import choose_window_sizes
windows = choose_window_sizes(series_length=1000, n_windows=5, seed=123)
print(windows)
- scan.ensemble.ensemble_vote(window_results: Mapping[int, Sequence[int]], vote_threshold: float = 0.5, tolerance: int = 10) Tuple[List[int], Dict[int, float], Dict[int, int]]
Apply ensemble voting across window-specific candidate lists.
- Parameters:
window_results – Mapping from window size to candidate split locations.
vote_threshold – Minimum fraction of windows required to retain a cluster leader.
tolerance – Maximum gap used to cluster nearby candidates before voting.
- Returns:
Selected change points, normalized vote scores, and raw vote counts.
- Return type:
tuple[list[int], dict[int, float], dict[int, int]]
Example
from scan import ensemble_vote
selected, scores, votes = ensemble_vote({20: [100], 30: [102]}, tolerance=5)
print(selected, scores, votes)
- scan.ensemble.merge_change_points(change_points: Iterable[int], tolerance: int = 10) List[List[int]]
Cluster nearby candidate change points.
- Parameters:
change_points – Candidate split locations to group.
tolerance – Maximum gap between consecutive candidates in the same cluster.
- Returns:
Sorted clusters of nearby candidate locations.
- Return type:
list[list[int]]
Example
from scan import merge_change_points
clusters = merge_change_points([98, 100, 205], tolerance=5)
print(clusters)
- scan.bootstrap.adaptive_threshold(left: Iterable[float], right: Iterable[float], alpha: float = 0.05, n_boot: int = 400, block_length: int | None = None, taper: str = 'tukey', random_state: int | None = None) float
Compute a local bootstrap threshold for a two-window comparison.
The two windows are mean-centered and pooled before bootstrapping so the null distribution represents no local distributional change.
- Parameters:
left – Adjacent samples to compare.
right – Adjacent samples to compare.
alpha – Upper-tail probability for the returned threshold.
n_boot – Number of bootstrap samples used to approximate the null distribution.
block_length – Optional bootstrap block length.
taper – Taper shape passed to
tapered_block_bootstrap().random_state – Optional NumPy random seed.
- Returns:
Empirical
1 - alphaquantile of bootstrapped Wasserstein statistics.- Return type:
float
Example
import numpy as np
from scan import adaptive_threshold
rng = np.random.default_rng(123)
threshold = adaptive_threshold(rng.normal(size=50), rng.normal(size=50), n_boot=100)
print(threshold)
- scan.bootstrap.tapered_block_bootstrap(x: Iterable[float], sample_length: int, block_length: int | None = None, n_boot: int = 1, taper: str = 'tukey', random_state: int | None = None) ndarray
Generate tapered block bootstrap samples.
- Parameters:
x – One-dimensional source series used to draw overlapping blocks.
sample_length – Number of observations in each bootstrap replicate.
block_length – Length of each sampled block. When omitted, uses
n ** (1 / 3)with a minimum of 3 observations.n_boot – Number of bootstrap replicates to generate.
taper – Taper shape. Use
"tukey"for Hann-style tapering or"none"for rectangular blocks.random_state – Optional NumPy random seed for reproducible sampling.
- Returns:
Array with shape
(n_boot, sample_length)containing bootstrap replicates.- Return type:
numpy.ndarray
Example
import numpy as np
from scan import tapered_block_bootstrap
samples = tapered_block_bootstrap(np.arange(100), sample_length=50, n_boot=3)
print(samples.shape)
Plotting functions
- scan.plotting.plot_time_series(x: Iterable[float], change_points: Iterable[int] | None = None, true_change_points: Iterable[int] | None = None, index: Iterable[float] | None = None, x_label: str = 'Time', y_label: str = 'Value', title: str = 'Time series')
Plot a univariate time series with optional change-point markers.
- Parameters:
x – One-dimensional time series.
change_points – Optional detected split locations.
true_change_points – Optional ground-truth split locations.
index – Optional x-axis values. If omitted and
xhas anindexattribute, that index is used; otherwise integer positions are used.x_label – Plot labels.
y_label – Plot labels.
title – Plot labels.
- Returns:
Time-series plot with detected and true change-point markers when provided.
- Return type:
plotnine.ggplot
Example
from scan import plot_time_series
plot = plot_time_series([0.1, 0.2, 1.4, 1.6], change_points=[2])
print(plot)
- scan.plotting.plot_change_points(x: Iterable[float], result: ScanResult, true_change_points: Iterable[int] | None = None, index: Iterable[float] | None = None, x_label: str = 'Time', y_label: str = 'Series', title: str | None = None)
Plot a time series with detected and optional true change points.
Detected change points are taken from
result.change_points. True change points should be provided separately usingtrue_change_points.- Parameters:
x – One-dimensional time series to visualize.
result – SCAN result object from the
scan_cpdandscan_single_windowcontaining detected change points.true_change_points – Optional True change-points (or annotated).
index – Optional x-axis values.
x_label – Plot labels.
y_label – Plot labels.
title – Plot labels.
- Returns:
Time-series plot with detected and optional true change-point markers.
- Return type:
plotnine.ggplot
Example
import numpy as np
from scan import plot_change_points, scan_cpd
x = np.r_[np.zeros(100), np.ones(100)]
result = scan_cpd(x, window_sizes=[20], n_boot=50)
plot = plot_change_points(x, result)
print(plot)
- scan.plotting.plot_swal_curve(x: Iterable[float], x_label: str = 'Time series', y_label: str = 'Scaled Wasserstein statistic', title: str | None = None)
Plot the SWAL/Wasserstein localization curve for one change point.
- Parameters:
x – One-dimensional time series containing a single change point.
x_label – Plot labels.
y_label – Plot labels.
title – Plot labels.
- Returns:
Local refinement curve with the selected split marked.
- Return type:
plotnine.ggplot
Example
import numpy as np
from scan import plot_swal_curve
x = np.r_[np.zeros(50), np.ones(50)]
plot = plot_swal_curve(x)
print(plot)
- scan.plotting.plot_vote_scree(result: ScanResult, x_label: str = 'Voting threshold $(\\nu)$', y_label: str = 'Number of retained change points', title: str | None = None)
Plot number of retained change points versus voting threshold.
- Parameters:
result – SCAN result containing ensemble vote scores.
x_label – Plot labels.
y_label – Plot labels.
title – Plot labels.
- Returns:
Scree plot showing how many candidates remain as the vote threshold changes.
- Return type:
plotnine.ggplot
Example
import numpy as np
from scan import plot_vote_scree, scan_cpd
x = np.r_[np.zeros(100), np.ones(100)]
result = scan_cpd(x, window_sizes=[20, 30], n_boot=50)
plot = plot_vote_scree(result)
print(plot)
- scan.plotting.plot_window_votes(result: ScanResult, max_x_labels: int = 12, x_label_angle: int = 45, x_label: str = 'Candidate change point', y_label: str = 'Window votes', title: str | None = None)
Plot ensemble vote counts for candidate change points.
- Parameters:
result – SCAN result containing raw vote counts and scores.
max_x_labels – Maximum number of x-axis labels to display.
x_label_angle – Rotation angle for x-axis labels.
x_label – Plot labels.
y_label – Plot labels.
title – Plot labels.
- Returns:
Bar chart of candidate vote counts with the selected vote threshold.
- Return type:
plotnine.ggplot
Example
import numpy as np
from scan import plot_window_votes, scan_cpd
x = np.r_[np.zeros(100), np.ones(100)]
result = scan_cpd(x, window_sizes=[20, 30], n_boot=50)
plot = plot_window_votes(result)
print(plot)
- scan.plotting.plot_thresholds()
Placeholder for a future threshold diagnostics plot.
This function is exported for API continuity but is not implemented yet. It currently returns
None.
Example
from scan import plot_thresholds
plot = plot_thresholds()
print(plot)
Simulator
Functions for simulating univariate time series. This simulator generates AR, ARMA, and ARFIMA series with change points for use in change-point detection simulation studies.
- UnivariateSeriesSimulator.simulate_ar_series(rho: float, error_type: str, error_mean: float = 0.0, error_variance: float = 1.0, error_location: float = 0.0, error_scale: float = 0.03) ndarray
Simulate an AR(1) series with normal or Cauchy innovations.
- Parameters:
rho – Autoregressive coefficient.
error_type – Innovation family:
"normal"or"cauchy".error_mean – Mean and variance used for normal innovations.
error_variance – Mean and variance used for normal innovations.
error_location – Location and scale used for Cauchy innovations.
error_scale – Location and scale used for Cauchy innovations.
- Returns:
Simulated series of length
len_series.- Return type:
numpy.ndarray
Example
from scan import UnivariateSeriesSimulator
simulator = UnivariateSeriesSimulator(len_series=200, seed=123)
x = simulator.simulate_ar_series(rho=0.4, error_type="normal")
- UnivariateSeriesSimulator.simulate_ar_unif(error_variance: float = 1.0) ndarray
Simulate an AR process with random time-varying coefficients.
- Parameters:
error_variance – Variance of Gaussian innovations.
- Returns:
Simulated series with
rho_tdrawn uniformly from[0, 1].- Return type:
numpy.ndarray
Example
from scan import UnivariateSeriesSimulator
simulator = UnivariateSeriesSimulator(len_series=200, seed=123)
x = simulator.simulate_ar_unif(error_variance=1.0)
print(x[:5])
- UnivariateSeriesSimulator.simulate_arma(phi: Sequence[float] | float, theta: Sequence[float] | float, error_mean: float = 0.0, error_variance: float = 1.0) ndarray
Simulate an ARMA process using statsmodels when it is installed.
- Parameters:
phi – Autoregressive coefficients.
theta – Moving-average coefficients.
error_mean – Mean and variance of Gaussian innovations.
error_variance – Mean and variance of Gaussian innovations.
- Returns:
Simulated ARMA series.
- Return type:
numpy.ndarray
Example
from scan import UnivariateSeriesSimulator
simulator = UnivariateSeriesSimulator(len_series=200, seed=123)
x = simulator.simulate_arma(phi=0.5, theta=0.2)
print(x[:5])
- UnivariateSeriesSimulator.arfima_sim(d: float = 0.3, ar: Sequence[float] | None = None, ma: Sequence[float] | None = None, sigma: float = 1.0) ndarray
Simulate an ARFIMA(p,d,q) series.
- Parameters:
d – Fractional integration parameter.
ar – Optional autoregressive coefficients.
ma – Optional moving-average coefficients.
sigma – Innovation standard deviation.
- Returns:
Simulated ARFIMA series of length
len_series.- Return type:
numpy.ndarray
Example
from scan import UnivariateSeriesSimulator
simulator = UnivariateSeriesSimulator(len_series=500, seed=123)
x = simulator.arfima_sim(d=0.3, sigma=1.0)
print(x.shape)
- UnivariateSeriesSimulator.apply_random_shifts(series: Sequence[float], change_point_locations: Sequence[int], min_shift: float = 3.0, max_shift: float = 10.0, shifts: Sequence[float] | float | None = None, seed: int | None = None, change_type: str = 'mean', variance_multipliers: Sequence[float] | float | None = None, lognormal_mean: float = 0.0, lognormal_sigma: float | None = None, variance_center: str | float = 'pre_change', variance_reference: str = 'first_segment') dict[str, ndarray]
Apply mean and/or variance shifts at supplied split locations.
- Parameters:
series – Base one-dimensional series to transform.
change_point_locations – Split locations where segment parameters change.
min_shift – Range for randomly generated mean-shift magnitudes.
max_shift – Range for randomly generated mean-shift magnitudes.
shifts – Optional scalar or per-change mean shifts.
seed – Optional seed overriding the simulator seed for shift generation.
change_type – Shift type:
"mean","variance", or"distribution".variance_multipliers – Optional scalar or per-change variance multipliers.
lognormal_mean – Parameters used for random variance multipliers.
lognormal_sigma – Parameters used for random variance multipliers.
variance_center – Center used when rescaling segment variance.
variance_reference – Reference variance source for multipliers.
- Returns:
Original series, shifted series, change points, mean shifts, and variance multipliers.
- Return type:
dict[str, numpy.ndarray]
Example
from scan import UnivariateSeriesSimulator
simulator = UnivariateSeriesSimulator(len_series=300, seed=123)
base = simulator.simulate_ar_series(rho=0.0, error_type="normal")
shifted = simulator.apply_random_shifts(base, [100, 200], change_type="mean")
print(shifted["change_points"])
- UnivariateSeriesSimulator.select_change_point_locations(min_points: int, ratio: int = 100, min_first_cp: int = 30, n_cps: int | None = None) ndarray
Select sorted split locations with a minimum spacing constraint.
- Parameters:
min_points – Minimum distance between consecutive selected split locations.
ratio – Used to infer the number of change points when
n_cpsis not provided.min_first_cp – Earliest allowed split location.
n_cps – Optional explicit number of change points to select.
- Returns:
Sorted integer split locations.
- Return type:
numpy.ndarray
Example
from scan import UnivariateSeriesSimulator
simulator = UnivariateSeriesSimulator(len_series=500, seed=123)
cps = simulator.select_change_point_locations(min_points=80, n_cps=3)
print(cps)