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:
Compute the gradient of x and find indices where |gradient| < threshold, treating these as candidate turning points (near-stationary regions).
Find peaks in x using scipy.signal.find_peaks with any additional kwargs.
For each peak, select the closest turning point to its left and right.
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()withvectorize=True. The result is anint8binary array (1 = changepoint, 0 = not) stored asds["{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:
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, keypoint: 7)>
- ethograph.io.pynapple.add_changepoints_to_nap(data, target_feature, changepoint_func, **func_kwargs)[source]#
Detect changepoints in a pynapple time series and return them as a TsGroup.
Applies changepoint_func independently to each unit or column in data. Returns a
nap.TsGroupwhere each unit contains the changepoint timestamps for one source series. Source metadata (label, feature name, type) is stored as group metadata columns; if data is itself aTsGroup, its metadata columns are forwarded as well.- Parameters:
data (nap.Tsd | nap.TsdFrame | nap.TsGroup) – Input time series. For a
Tsda single unit is produced; for aTsdFrameone unit per column; for aTsGroupone unit per neuron/unit.target_feature (str) – Human-readable label recorded in the output metadata (e.g.
"speed").changepoint_func (callable) – A function
f(x, **kwargs) -> arraythat accepts a 1-D numpy array of values and returns an array of changepoint times (or a binary indicator of the same length).**func_kwargs – Forwarded to changepoint_func.
- Returns:
One unit per input series, containing changepoint timestamps.
- Return type:
nap.TsGroup
Examples
>>> import ethograph as eto >>> from ethograph.features.changepoints import find_troughs_binary >>> data = eto.load_nap_data("experiment.nwb") >>> cp_group = eto.add_changepoints_to_nap( ... data["speed"], ... target_feature="speed", ... changepoint_func=find_troughs_binary, ... prominence=0.3, ... )
Merging and time extraction#
Changepoints stored as kind="changepoint_feature" masks can be merged into
a single boolean mask, or read as times (seconds) at a given keypoint /
individual selection. changepoint_fired is the one reading the GUI has:
the lineplot draws it and a click snaps to it, so drawn and snapped agree.
- ethograph.features.changepoints.merge_changepoints(ds, vars=None, keep_dims=None)[source]#
Merge changepoint variables in a dataset into a single boolean mask.
Combines every raw changepoint mask (
attrs["changepoint_mask"]; seeethograph.io.schema.is_changepoint()) using logical OR across all non-time dimensions — the smooth expansions of a mask are ordinary features and are not merged. All masks must share the sametarget_featureattribute.- Parameters:
ds (xr.Dataset) – Dataset containing one or more changepoint variables.
vars (sequence of str, optional) – Which changepoint masks to merge; default merges every one
changepoint_vars()finds. Naming a variable that is not a changepoint mask is an error.keep_dims (sequence of str, optional) – Dims to leave standing instead of ORing across — typically the individual dim, so one animal’s changepoints do not leak into another’s. Default collapses every non-time dim.
- Returns:
ds (xr.Dataset) – Copy of the input with a new
"changepoints"DataArray (float 0/1) replacing the merged changepoint variables.target_feature (str) – The shared
target_featureattribute from the input variables.
- Raises:
ValueError – If changepoint variables reference different target features.
- ethograph.features.changepoints.changepoint_fired(mask, selections=None)[source]#
Where mask fires at selections: a boolean
(T,)array on the mask’s own time axis.This is the one reading of a changepoint mask the GUI has. The lineplot draws it (
XarrayLoader.select) and a click snaps to it (XarrayLoader.get_cp_times), so what is drawn and what is snapped to are the same set by construction.A selection key that is a dim of the mask pins that dim —
.selwhere the dim has a coordinate,.iselwhere it does not (the rule ofethograph.utils.xr_utils.sel_valid()). Every other key is ignored, and every non-time dim left free is OR’d across.- Return type:
- ethograph.features.changepoints.changepoint_mask_times(mask, selections=None)[source]#
Times at which mask fires at selections, read off the mask’s own time coordinate.
- Return type:
- ethograph.features.changepoints.dataset_changepoint_times(ds, feature=None, selections=None)[source]#
Sorted, unique times of every changepoint mask in ds — of feature only when given.
Masks that target different features are simply unioned; there is no merge step to refuse them. Empty when nothing matches.
- Return type:
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:
- Steps:
purge_short_intervals — pre-cleanup (do_purge)
stitch_intervals — merge same-label across small gaps (do_stitch)
snap_boundaries — snap to changepoint times (do_snap)
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:
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 |
|
Smooth changepoints — proximity to nearest changepoint |
|
Segment IDs — unique ID per inter-changepoint region |
|
Smooth changepoints use a Laplacian kernel centred at each changepoint index \(i\):
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, sigmas, distribution='laplacian', horizon=None, scale=None, max_length=None)[source]#
Create changepoint-based features from a binary changepoint array.
Four column groups, in this order (see
CP_TRANSFORMS):Binary — the exact changepoint positions (0/1 mask). Example:
0 0 0 0 1 0 0 0 1 0 0 0 0 0Proximity — one column per sigma: a Laplacian (or Gaussian) kernel centred at each changepoint, summed. An isolated changepoint reads 1 at its frame; overlapping kernels add, so a cluster of candidates reads above 1. Nothing is normalised per trial. Example (one sigma):
0 0 0 .3 1 .3 0 .3 1 .3 0 0 0 0Offset — two columns: samples since the previous changepoint and until the next, each clipped at horizon and scaled to
[0, 1]. A frame reads which side of its nearest candidate it is on, which the symmetric kernels cannot say; before the first / after the last changepoint the column is saturated (no candidate in reach). Example (horizon 4): since1 1 1 1 0 .25 .5 .75 0 .25 .5 .75 1 1, until1 .75 .5 .25 0 .75 .5 .25 0 1 1 1 1 1Length — one column: the length of the candidate segment the frame sits in (cut at every changepoint and at the trial’s ends), as
log1p(length) / log1p(max_length), clipped at 1. The offsets already resolve anything shorter than two horizons; this is the long range, where a fragment-prone short segment and a whole bout of rest should not read alike.
Proximity uses a Laplacian kernel by default:
\[\text{prox}(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.scale multiplies every proximity column by \(\exp(-x / \bar{x})\) of that signal, emphasising changepoints where it is low — with speed, the troughs before and after a movement rather than a dip inside one; with an amplitude envelope, the silences between calls. Frames where scale is NaN read 0.
- Parameters:
changepoint_binary (np.ndarray) – Binary (0/1) array marking changepoint locations.
sigmas (Sequence[float]) – Kernel widths (samples) for the proximity columns.
distribution (Literal[‘gaussian’, ‘laplacian’]) –
"laplacian"(default) or"gaussian"kernel.horizon (float | None) – Reach of the offset columns, in samples;
Noneisdefault_horizon().scale (np.ndarray | None) – Optional signal on the same time axis that scales the proximity columns.
max_length (float | None) – Where the length column saturates, in samples;
NoneisLENGTH_HORIZONShorizons.
- Returns:
the binary mask, one proximity column per sigma,
since,until, thenlength.- Return type:
2D array of shape
(T, 1 + len(sigmas) + 3)
Storage format#
Kinematic changepoints are stored as binary (int8) DataArrays sharing
their feature’s time axis, tagged with kind = "changepoint_feature" (plus the legacy 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.