Skip to content
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ array([ 50, 100, 150, 200])
import scipy.stats as st
from skchange.datasets import generate_piecewise_data
from skchange.detectors import SeededBinarySegmentation
from skchange.tuning import CalibratedDetector
from skchange.tuning import CalibratedDetectorFWER

# Change-free beta(2, 5) data used to calibrate the detection threshold.
X_calib = generate_piecewise_data(st.beta(2, 5), lengths=300, seed=0)
Expand All @@ -62,7 +62,7 @@ X = generate_piecewise_data(
seed=1,
)

cal = CalibratedDetector(
cal = CalibratedDetectorFWER(
SeededBinarySegmentation(),
level=0.05,
n_simulations=999,
Expand Down
4 changes: 2 additions & 2 deletions docs/source/api_reference/tuning.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Calibrated detector
:toctree: auto_generated/
:template: class.rst

CalibratedDetector
CalibratedDetectorFWER

Penalty calibration
-------------------
Expand All @@ -23,7 +23,7 @@ Penalty calibration
:toctree: auto_generated/
:template: functions.rst

calibrate_penalty_scale
calibrate_penalty_scale_fwer
penalty_curve
unpenalised_scores

Expand Down
18 changes: 8 additions & 10 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Welcome to skchange

Skchange provides fast and flexible changepoint detection algorithms within a
`scikit-learn <https://scikit-learn.org>`_-like API.
Users upgrading from 0.15.x should consult the
Users upgrading from version <0.17 should consult the
`migration guide <https://github.com/NorskRegnesentral/skchange/blob/main/MIGRATION_GUIDE.md>`_.

Installation
Expand All @@ -28,14 +28,13 @@ For better computational performance, it is recommended to install skchange with
Key features
------------

- **Theoretically grounded algorithms**: Fast exact and approximate search methods with solid statistical foundations.
- **High performance**: `Numba <https://numba.readthedocs.io>`_ is used extensively for computational speed.
- **Theoretical soundness**: Exact and approximate changepoint detection algorithms with solid statistical foundations.
- **Flexible**: Detectors are composed of modular costs or statistical tests. Browse the :doc:`api_reference/interval_scorers` for built-in options or see :doc:`developer_guide/extending` to implement your own.
- **Fast**: `Numba <https://numba.readthedocs.io>`_ is used extensively for computational speed.
- **Easy to use**: Familiar `scikit-learn <https://scikit-learn.org>`_ ``fit`` / ``predict`` API for both users and contributors.
- **Segment anomaly detection**: Detect intervals of anomalous behaviour in time series data.
- **High-dimensional data**: Algorithms covering settings where either few (sparse changes) or many features (dense changes) change simultaneously.
- **Automatic penalty calibration**: Data-driven utilities for calibrating the detection threshold to balance false alarms against missed detections.
- **Large scorer library**: A broad collection of built-in cost functions and statistical tests for a wide range of data distributions.
- **Easy to use**: Familiar ``fit`` / ``predict`` API for both users and contributors.
- **Easy to extend**: Inherit from base class templates to add custom costs and statistical tests for your dataset and problem.
- **High-dimensional data**: Algorithms suitable for high-dimensional data with an unknown number of changing features.
- **Automatic penalty calibration**: Data-driven utilities for calibrating the false alarm rate.

Mission
-------
Expand All @@ -57,8 +56,7 @@ Quick example
cps = MovingWindow(bandwidth=20).fit_predict(X)
# array([ 50, 100, 150, 200])

See the :doc:`user_guide/index` for more, or jump to the
:doc:`api_reference/index`.
See the :doc:`user_guide/index` for more, or jump to the :doc:`api_reference/index`.

Licence
-------
Expand Down
66 changes: 56 additions & 10 deletions docs/source/user_guide/change_detection/mean.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"source": [
"# Change in mean\n",
"\n",
"The most common change-detection problem is detecting shifts in the mean of a time series. Skchange offers several scorers for this feature; the two most useful entry points are `L2Cost` (paired with `PELT`) for exact optimisation, and `CUSUM` (paired with `SeededBinarySegmentation`) for a faster approximate search."
"The most common change-detection problem is detecting shifts in the mean of a time series. Skchange offers several scorers for this feature; the two most useful entry points are `L2Cost` paired with `PELT` for exact optimisation, and `CUSUM` paired with `SeededBinarySegmentation` or `MovingWindow` for a faster approximate search."
]
},
{
Expand Down Expand Up @@ -73,7 +73,7 @@
"source": [
"## `SeededBinarySegmentation` and `CUSUM`\n",
"\n",
"For long series, an exact `PELT` search can become expensive. `SeededBinarySegmentation` evaluates a change score on a pre-computed grid of intervals and picks the local maxima that exceed the penalty. Paired with the classical `CUSUM` statistic for a change in mean, it recovers the same changepoints at a fraction of the cost."
"For long series, an exact `PELT` search can become expensive. `SeededBinarySegmentation` evaluates a change score on a pre-computed grid of intervals and picks the local maxima that exceed the penalty. Paired with the classical `CUSUM` statistic for a change in mean, it recovers the same changepoints at a fraction of the time."
]
},
{
Expand All @@ -98,21 +98,67 @@
"id": "7",
"metadata": {},
"source": [
"Note the `min_subinterval_length=2`: with the default of 5, the two-sample-wide spike segment `[30, 35)` would fall through the grid.\n",
"Note the `min_subinterval_length=2`: with the default of 5, the two-sample-wide spike segment `[30, 35)` would fall through the grid."
]
},
{
"cell_type": "markdown",
"id": "8",
"metadata": {},
"source": [
"## `MovingWindow` and `CUSUM`\n",
"\n",
"`MovingWindow` slides one or more fixed-width windows across the series and evaluates a change score at the window's midpoint. The length from the midpoint to the boundaries of the window is called the *bandwidth*. Beyond `predict`, `MovingWindow` exposes `predict_scores`, which returns the penalised score at every candidate changepoint. This is a useful visual diagnostic that shows how much evidence there is for a change at each location and how sensitive that evidence is to the bandwidth."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import plotly.express as px\n",
"\n",
"## Which one to pick\n",
"from skchange.new_api.detectors import MovingWindow\n",
"\n",
"- **Use `PELT` + `L2Cost`** when you want an optimal segmentation of the whole series and the sample size is manageable. The output is the segmentation that minimises the penalised total squared error.\n",
"- **Use `SeededBinarySegmentation` + `CUSUM`** when speed matters or the series is very long. The output is a set of local maxima of the CUSUM statistic; not globally optimal, but usually very close and much faster to compute.\n",
"detector = MovingWindow(CUSUM(), bandwidth=[5, 15, 40], penalty_scale=1.0).fit(X)\n",
"changepoints = detector.predict(X)\n",
"scores, index = detector.predict_scores(X, return_index=True)\n",
"\n",
"For guidance on setting the penalty, see the [Penalties section of the concepts page](../concepts.ipynb#Penalties) and the tuning material referenced there."
"score_df = pd.DataFrame(\n",
" {\n",
" \"split\": index[\"splits\"],\n",
" \"bandwidth\": index[\"bws\"].astype(str),\n",
" \"penalised_score\": scores,\n",
" }\n",
")\n",
"fig = px.line(\n",
" score_df,\n",
" x=\"split\",\n",
" y=\"penalised_score\",\n",
" color=\"bandwidth\",\n",
" labels={\"split\": \"Candidate changepoint\", \"penalised_score\": \"Penalised score\"},\n",
")\n",
"fig.add_hline(y=0, line_dash=\"dash\", line_color=\"gray\")\n",
"for cp in changepoints:\n",
" fig.add_vline(x=int(cp), line_dash=\"dot\", line_color=\"red\")\n",
"fig.show()\n",
"print(changepoints)"
]
},
{
"cell_type": "markdown",
"id": "8",
"id": "10",
"metadata": {},
"source": []
"source": [
"Peaks above the dashed zero line mark candidate changepoints; `MovingWindow` keeps the local maxima. Small bandwidths react to short segments like the `[30, 35)` spike, while larger bandwidths smooth over noise and pinpoint sustained shifts more sharply. Comparing curves across bandwidths often reveals whether a detection is well-supported at multiple scales or only picked up at one.\n",
"\n",
"## Choosing a detector\n",
"\n",
"The choice of detector comes down to the usual trade-offs between exact optimisation and fast approximate search. See the [Detectors](../detectors/index.rst) section for a side-by-side comparison and per-detector notes. For guidance on setting the penalty, see the [Penalties calibration page](../tuning/penalty_calibration.ipynb)."
]
}
],
"metadata": {
Expand All @@ -131,7 +177,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.12"
"version": "3.13.14"
}
},
"nbformat": 4,
Expand Down
8 changes: 4 additions & 4 deletions docs/source/user_guide/concepts/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ Concepts
========

In Skchange a **change detector** is composed of an **interval scorer** and a
**penalty**. A change detector is the object you use for detecting changes, an
interval scorer is the user-specified component that tells the detector what
**penalty**. A change detector is the object you use to search for changes in your data,
an interval scorer is the user-specified component that tells the detector what
distributional feature of the data to look for changes in, while the penalty
controls the number of detected events. This section introduces each of these concepts to give you a high-level
understanding of the library's design.
controls the number of detected events. This section introduces each of these concepts
to give you a high-level understanding of the library's design.

.. toctree::
:maxdepth: 2
Expand Down
85 changes: 51 additions & 34 deletions docs/source/user_guide/getting_started.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,56 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Penalty calibration"
"## Penalty calibration\n",
"The theoretically derived default penalties in Skchange assume that the data is\n",
"approximately Gaussian with unit variance in segments between two true changepoints.\n",
"This is rarely the case in practice, so calibrating the penalty to your data and\n",
"problem is often necessary.\n",
"Skchange provides several tools to automate or explore the calibration of the penalty."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### `CalibratedDetectorFWER`\n",
"This plays the same role as the ubiquitous `GridSearchCV` of scikit-learn. It wraps a detector and calibrates its penalty to control the family-wise error rate on `fit`, and delegates prediction to the calibrated detector."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import scipy.stats as st\n",
"\n",
"from skchange.datasets import generate_piecewise_data\n",
"from skchange.detectors import SeededBinarySegmentation\n",
"from skchange.tuning import CalibratedDetectorFWER\n",
"from skchange.utils.plotting import plot_detections\n",
"\n",
"# Change-free beta(2, 5) data used to calibrate the detection threshold.\n",
"X_train = generate_piecewise_data(st.beta(2, 5), lengths=300, seed=0)\n",
"# Test data with two changepoints where the beta shape changes.\n",
"X_test = generate_piecewise_data(\n",
" [st.beta(2, 5), st.beta(5, 2), st.beta(1, 10)],\n",
" lengths=100,\n",
" seed=1,\n",
")\n",
"\n",
"detector = CalibratedDetectorFWER(\n",
" SeededBinarySegmentation(),\n",
" level=0.05,\n",
" n_simulations=999,\n",
" random_state=0,\n",
")\n",
"detector.fit(X_train)\n",
"changepoints = detector.predict(X_test)\n",
"\n",
"plot_detections(X_test, changepoints=changepoints).show()\n",
"print(\"Calibrated penalty_scale:\", detector.penalty_scale_)\n",
"print(\"Detected changepoints: \", changepoints)"
]
},
{
Expand Down Expand Up @@ -336,38 +385,6 @@
" title = f\"Penalty curve with selected penalty scale = {selected_penalty_scale:.2f}\",\n",
").add_vline(x=selected_penalty_scale, line_dash=\"dash\", line_color=\"red\").show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Empirical scores distribution\n",
"Get the empirical distribution of the detector's internal interval scorer (cost, change score, saving or transient score) with the penalty parameter set to zero.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from skchange.tuning import unpenalised_scores\n",
"\n",
"# Generate data without any changepoints, to represent the null distribution.\n",
"X = generate_piecewise_normal_data(means=0, lengths=1000, seed=43)\n",
"\n",
"alpha = 0.01 # desired false positive rate\n",
"\n",
"scores = unpenalised_scores(detector, X)\n",
"selected_penalty = np.quantile(scores, 1 - alpha)\n",
"\n",
"px.histogram(\n",
" scores,\n",
" nbins=100,\n",
" labels={\"value\": \"score\"},\n",
" title=f\"Distribution of scores with selected penalty = {selected_penalty:.2f}\",\n",
").add_vline(x=selected_penalty, line_dash=\"dash\", line_color=\"red\").show()"
]
}
],
"metadata": {
Expand All @@ -386,7 +403,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.12"
"version": "3.13.14"
}
},
"nbformat": 4,
Expand Down
Loading