Changepoints#

Changepoint detection finds candidate behavioural boundaries in kinematic, audio, and spectral time series. Ethograph exposes detectors (binary masks aligned to the signal’s time axis), merging and snapping utilities, and feature generators that turn changepoints into model-ready inputs.

See Changepoints for the conceptual overview and the changepoint features section below for the representations used by downstream segmentation models.


Detection#

Detectors consume a 1-D signal and return a binary (0/1) mask of the same length. NaN boundaries in the input are always added as changepoints so that valid/invalid transitions are not lost.

ethograph.features.changepoints.find_peaks_binary(x, **kwargs)[source]#

scipy.signal.find_peaks + NaN boundaries -> binary mask.

ethograph.features.changepoints.find_troughs_binary(x, **kwargs)[source]#

Find troughs (local minima) + NaN boundaries -> binary mask.

ethograph.features.changepoints.find_nearest_turning_points_binary(x, threshold=1, max_value=None, prominence=0.5, distance=2, **kwargs)[source]#

Convert a 1D signal into a binary mask marking boundaries of peak regions.

Identifies peaks in the signal, then finds the nearest “turning points” (where the gradient is near zero) on either side of each peak. These turning points define the boundaries of peak regions. The result is a binary mask where 1 indicates a turning-point boundary.

The algorithm works in four steps:
  1. Compute the gradient of x and find indices where |gradient| < threshold, treating these as candidate turning points (near-stationary regions).

  2. Find peaks in x using scipy.signal.find_peaks with any additional kwargs.

  3. For each peak, select the closest turning point to its left and right.

  4. Add boundaries at NaN transitions in the original signal.

Parameters:
  • x – Input 1D signal.

  • threshold – Maximum absolute gradient value to qualify as a turning point. Lower values select only very flat regions. Default is 1.

  • max_value – If set, discard turning points where x exceeds this value. Useful for ignoring turning points on high plateaus.

  • **kwargs – Passed to scipy.signal.find_peaks (e.g. height, distance, prominence).

Returns:

Binary array of same length as x, with 1 at turning-point boundaries and NaN-transition boundaries, 0 elsewhere.

ethograph.features.changepoints.add_NaN_boundaries(arr, changepoints)[source]#

Merge NaN-transition boundaries with other changepoints -> binary mask.

To attach detectors to a xarray.Dataset or a pynapple object:

ethograph.io.dataset.add_changepoints_to_ds(ds, target_feature, changepoint_name, changepoint_func, **func_kwargs)[source]#

Detect changepoints in a feature and store them in the dataset.

Applies changepoint_func independently along every non-time dimension (e.g. per keypoint, per individual) using xarray.apply_ufunc() with vectorize=True. The result is an int8 binary array (1 = changepoint, 0 = not) stored as ds["{target_feature}_{changepoint_name}"].

Parameters:
  • ds (xarray.Dataset) – Trial dataset containing target_feature.

  • target_feature (str) – Name of the variable to run detection on (e.g. "speed").

  • changepoint_name (str) – Suffix for the output variable name. The stored variable will be called "{target_feature}_{changepoint_name}" (e.g. "speed_troughs").

  • changepoint_func (callable) – A function f(x, **kwargs) -> array[int8] that takes a 1-D numpy array and returns a same-length binary indicator.

  • **func_kwargs – Forwarded to changepoint_func.

Returns:

The input dataset with the changepoint variable added in place.

Return type:

xarray.Dataset

Examples

>>> import ethograph as eto
>>> from ethograph.features.changepoints import find_troughs_binary
>>> dt = eto.open("experiment.nc")
>>> ds = dt.itrial(0)
>>> ds = eto.add_changepoints_to_ds(
...     ds,
...     target_feature="speed",
...     changepoint_name="troughs",
...     changepoint_func=find_troughs_binary,
...     prominence=0.3,
... )
>>> ds["speed_troughs"]
<xarray.DataArray 'speed_troughs' (time: 9000, keypoints: 7)>
ethograph.io.pynapple.add_changepoints_to_nap(data, target_feature, changepoint_func, **func_kwargs)[source]#
Return type:

nap.TsGroup


Merging and time extraction#

Changepoints stored as multiple attrs["type"] == "changepoints" DataArrays can be merged into a single boolean mask, or converted directly to absolute times (seconds) for use by the GUI and correction pipeline.

ethograph.features.changepoints.merge_changepoints(ds)[source]#

Merge all changepoint variables in a dataset into a single boolean mask.

Combines every variable with attrs["type"] == "changepoints" using logical OR across all non-time dimensions. All changepoint variables must share the same target_feature attribute.

Parameters:

ds (xr.Dataset) – Dataset containing one or more changepoint variables.

Returns:

  • ds (xr.Dataset) – Copy of the input with a new "changepoints" DataArray (float 0/1) replacing the individual changepoint variables.

  • target_feature (str) – The shared target_feature attribute from the input variables.

Raises:

ValueError – If changepoint variables reference different target features.

ethograph.features.changepoints.extract_cp_times(ds, time, **cp_kwargs)[source]#

Extract merged changepoint times from dataset.

Replaces the inline pattern: merge_changepoints -> binary -> np.where -> times. Returns empty array if no CP variables exist.

Return type:

ndarray

ethograph.features.changepoints.snap_to_nearest_changepoint_time(t_clicked, ds, feature_sel, time, **ds_kwargs)[source]#

Snap a clicked time (seconds) to the nearest changepoint time.

Works entirely in the time domain — no index conversion needed. Combines kinematic, audio, and oscillation changepoints.

Return type:

float


Label correction#

Snap interval-based labels to nearby changepoints. The full pipeline (correct_changepoints) runs purge → stitch → snap → purge; see Changepoint correction for parameter guidance.

ethograph.features.changepoints.correct_changepoints(df, cp_times, min_duration_s, stitch_gap_s, max_expansion_s, max_shrink_s, label_thresholds_s=None, do_purge=True, do_stitch=True, do_snap=True, do_purge_after=True)[source]#

Full interval-native correction pipeline.

Return type:

DataFrame

Steps:
  1. purge_short_intervals — pre-cleanup (do_purge)

  2. stitch_intervals — merge same-label across small gaps (do_stitch)

  3. snap_boundaries — snap to changepoint times (do_snap)

  4. purge_short_intervals — post-cleanup (do_purge_after)

ethograph.features.changepoints.correct_changepoints_automatic(df, min_duration_s=0.001, stitch_gap_s=0.0)[source]#

Lightweight cleanup used while manually creating labels.

Return type:

DataFrame

ethograph.features.changepoints.correct_changepoints_dense(labels, ds, all_params)[source]#

Correct dense label arrays using changepoints (legacy ML pipeline).

Operates on integer label arrays, not interval DataFrames. Use correct_changepoints() for the modern interval-native pipeline.

Parameters:
  • labels (array-like) – Dense integer label array of shape (T,).

  • ds (xr.Dataset) – Trial dataset containing changepoint variables.

  • all_params (dict) –

    Keys:

    • cp_kwargs: Selection kwargs forwarded to ds.sel().

    • min_label_length_s: Minimum label duration in seconds.

    • stitch_gap_len_s: Maximum gap to stitch in seconds.

    • label_thresholds_s: Per-label minimum durations (dict).

    • changepoint_params: Dict with max_expansion_s and max_shrink_s.

    • fps: Frame rate used to convert seconds to sample counts.

Returns:

Corrected integer label array of the same shape as labels.

Return type:

np.ndarray


Changepoint features#

Once changepoints have been curated, they are converted into learnable features for action- and audio-segmentation models (transformers, MS-TCN, DLC2Action, ASFormer, …). Three complementary representations let the model pick up changepoint structure at different scales:

Feature

Example (changepoints at t=4 and t=8)

Binary changepoints — exact positions

0 0 0 0 1 0 0 0 1 0 0 0 0 0

Smooth changepoints — proximity to nearest changepoint

0 0 0 .3 1 .3 0 .3 1 .3 0 0 0 0

Segment IDs — unique ID per inter-changepoint region

0 0 0 0 1 1 1 1 2 2 2 2 2 2

Smooth changepoints use a Laplacian kernel centred at each changepoint index \(i\):

\[\text{smooth}(t) = \sum_i \exp\!\left(-\frac{|t - i|}{\sigma}\right)\]

Laplacian peaks are narrow (so they pinpoint the changepoint) but have long tails (so they remain visible from far away). Passing several sigmas — e.g. [0.5, 3, 5] — yields a multi-scale view.

Weighted variants emphasise changepoints where a target signal x (typically speed) is low, via \(\exp(-x / (\bar{x} + \epsilon))\). This helps models distinguish speed minima before/after movements from minima occurring within a movement.

ethograph.features.changepoints.more_changepoint_features(changepoint_binary, targ_feat_vals, sigmas, distribution='laplacian')[source]#

Create changepoint-based features from a binary changepoint array.

Generates three complementary representations that help a model learn changepoint locations:

  • Binary changepoints — exact changepoint positions (0/1 mask). Example: 0 0 0 0 1 0 0 0 1 0 0 0 0 0

  • Smooth changepoints — proximity to the nearest changepoint, rendered as Laplacian (or Gaussian) peaks centred at each changepoint index. Example: 0 0 0 .3 1 .3 0 .3 1 .3 0 0 0 0

  • Segment IDs — unique identifier for each contiguous region between changepoints (normalised to [0, 1]). Example: 0 0 0 0 1 1 1 1 2 2 2 2 2 2

Smooth changepoints use a Laplacian kernel by default:

\[\text{smooth}(t) = \sum_i \exp\!\left(-\frac{|t - i|}{\sigma}\right)\]

where \(i\) are the changepoint indices and \(\sigma\) controls the peak width. Laplacians have a narrow peak that points directly at the changepoint while their long tails remain visible from far away. Passing multiple sigmas (e.g. [0.5, 3, 5]) yields features at several scales.

Additionally, weighted versions emphasise changepoints where the target feature (e.g. speed) is low, by multiplying the smooth features with \(\exp(-x / (\bar{x} + \epsilon))\). This helps the model distinguish speed minima before/after movements (low speed) from minima occurring within different movements.

Parameters:
  • changepoint_binary (ndarray) – Binary (0/1) array marking changepoint locations.

  • targ_feat_vals (ndarray) – Target feature values (e.g. speed) used to weight the smooth changepoint features.

  • sigmas (List[float]) – Kernel widths for the smooth changepoint representation.

  • distribution (Literal['gaussian', 'laplacian']) – "laplacian" (default) or "gaussian" kernel.

Return type:

ndarray

Returns:

2D array of shape (T, 1 + 2 * len(sigmas) + 1) stacking the binary mask, one smooth feature per sigma, their weighted (speed- emphasised, z-normalised) counterparts, and the normalised segment IDs.


Storage format#

Kinematic changepoints are stored as binary (int8) DataArrays sharing their feature’s time axis, tagged with attrs["type"] = "changepoints" and attrs["target_feature"]. Audio changepoints, which would be prohibitively large at audio sample rates, are stored instead as audio_cp_onsets / audio_cp_offsets float pairs. See Kinematic changepoints and Audio changepoints for examples.