microsaccades#
- pymovements.events.detection.microsaccades(velocities: list[list[float]] | list[tuple[float, float]] | ndarray | Series, timesteps: list[int] | ndarray | Series | None = None, minimum_duration: int = 6, threshold: ndarray | tuple[float, float] | str = 'engbert2015', threshold_factor: float = 6, minimum_threshold: float = 1e-10, include_nan: bool = False, name: str = 'saccade') Events[source]#
Detect micro-saccades from velocity gaze sequence.
This algorithm has a noise-adaptive velocity threshold parameter, which can also be set explicitly.
The implementation and its default parameter values are based on the description from Engbert & Kliegl [Engbert and Kliegl, 2003] and is adopted from the Microsaccade Toolbox 0.9 originally implemented in R [Engbert et al., 2015].
- Parameters:
velocities (list[list[float]] | list[tuple[float, float]] | numpy.ndarray | polars.Series) – shape (N, 2) x and y velocities of N samples in chronological order
timesteps (list[int] | numpy.ndarray | polars.Series | None) – shape (N, ) Corresponding continuous 1D timestep time series. If None, sample based timesteps are assumed. (default: None)
minimum_duration (int) –
- Minimum saccade duration. The duration is specified in the units used in
timesteps. If
timestepsis None, thenminimum_durationis specified in numbers of samples. (default: 6)
- Minimum saccade duration. The duration is specified in the units used in
threshold (numpy.ndarray | tuple[float, float] | str) – If tuple of floats then use this as explicit elliptic threshold. If str, then use a data-driven velocity threshold method. See
compute_threshold()for a reference of valid methods. (default: ‘engbert2015’)threshold_factor (float) – factor for relative velocity threshold computation. (default: 6)
minimum_threshold (float) – minimal threshold value. Raises ValueError if calculated threshold is too low. (default: 1e-10)
include_nan (bool) – Indicator, whether we want to split events on missing/corrupt value (numpy.nan) (default: False)
name (str) – Name for detected events in Events. (default: ‘saccade’)
- Returns:
A dataframe with detected saccades as rows.
- Return type:
- Raises:
TypeError – If velocities is a polars Series and dtype not List
ValueError – If threshold value is below min_threshold value. If passed threshold is either not two-dimensional or not a supported method.
Examples
Create a synthetic velocity signal representing micro-saccades.
>>> import numpy as np >>> from pymovements.synthetic import step_function >>> from pymovements.gaze import from_numpy >>> velocities = step_function( ... length=200, steps=[2, 5, 9, 111, 150], ... values=[(0.5, 0.5), (11.0, 12.0), (0.2, 0.2), (10.0, 20.0), (0.1, 0.1)], ... start_value=(0., 0.), ... noise=0.001, ... ) >>> velocities.shape (200, 2)
Apply event detection algorithm on numpy array:
>>> microsaccades(velocities) shape: (1, 4) ┌─────────┬───────┬────────┬──────────┐ │ name ┆ onset ┆ offset ┆ duration │ │ --- ┆ --- ┆ --- ┆ --- │ │ str ┆ i64 ┆ i64 ┆ i64 │ ╞═════════╪═══════╪════════╪══════════╡ │ saccade ┆ 2 ┆ 199 ┆ 197 │ └─────────┴───────┴────────┴──────────┘
Run saccade detection with custom parameters:
>>> microsaccades(velocities, minimum_duration=10, threshold=0.1) shape: (1, 4) ┌─────────┬───────┬────────┬──────────┐ │ name ┆ onset ┆ offset ┆ duration │ │ --- ┆ --- ┆ --- ┆ --- │ │ str ┆ i64 ┆ i64 ┆ i64 │ ╞═════════╪═══════╪════════╪══════════╡ │ saccade ┆ 111 ┆ 149 ┆ 38 │ └─────────┴───────┴────────┴──────────┘
Polars series are also supported as input. Let’s create a nested position series from our numpy array:
>>> df = polars.from_numpy(velocities, schema=['x', 'y']) >>> velocity_series = df.select(polars.concat_list(('x', 'y')).alias('velocity'))['velocity'] >>> velocity_series shape: (200,) Series: 'velocity' [list[f64]] [ [-0.000628, 0.000055] [-0.000818, -0.000694] [0.50097, 0.498064] [0.500475, 0.500865] [0.498559, 0.499092] … [0.100042, 0.100217] [0.099627, 0.099812] [0.101374, 0.098537] [0.099669, 0.100079] [0.098491, 0.101448] ]
Apply event detection algorithm on polars series:
>>> microsaccades(velocity_series) shape: (1, 4) ┌─────────┬───────┬────────┬──────────┐ │ name ┆ onset ┆ offset ┆ duration │ │ --- ┆ --- ┆ --- ┆ --- │ │ str ┆ i64 ┆ i64 ┆ i64 │ ╞═════════╪═══════╪════════╪══════════╡ │ saccade ┆ 2 ┆ 199 ┆ 197 │ └─────────┴───────┴────────┴──────────┘
We can also apply the detection on a
Gazeobject.>>> from pymovements import Experiment >>> gaze = from_numpy( ... velocity=velocities.T, ... time=np.arange(len(velocities)), ... ) >>> gaze shape: (200, 2) ┌──────┬────────────────────────┐ │ time ┆ velocity │ │ --- ┆ --- │ │ i64 ┆ list[f64] │ ╞══════╪════════════════════════╡ │ 0 ┆ [-0.000628, 0.000055] │ │ 1 ┆ [-0.000818, -0.000694] │ │ 2 ┆ [0.50097, 0.498064] │ │ 3 ┆ [0.500475, 0.500865] │ │ 4 ┆ [0.498559, 0.499092] │ │ … ┆ … │ │ 195 ┆ [0.100042, 0.100217] │ │ 196 ┆ [0.099627, 0.099812] │ │ 197 ┆ [0.101374, 0.098537] │ │ 198 ┆ [0.099669, 0.100079] │ │ 199 ┆ [0.098491, 0.101448] │ └──────┴────────────────────────┘
Run saccade detection by using the
detect()method.>>> gaze.detect('microsaccades') >>> gaze.events shape: (1, 4) ┌─────────┬───────┬────────┬──────────┐ │ name ┆ onset ┆ offset ┆ duration │ │ --- ┆ --- ┆ --- ┆ --- │ │ str ┆ i64 ┆ i64 ┆ i64 │ ╞═════════╪═══════╪════════╪══════════╡ │ saccade ┆ 2 ┆ 199 ┆ 197 │ └─────────┴───────┴────────┴──────────┘
Passing parameters to
detect():>>> gaze.detect('microsaccades', minimum_duration=10, threshold=0.1, name='microsaccade') >>> gaze.events.filter_by_name('microsaccade') shape: (1, 4) ┌──────────────┬───────┬────────┬──────────┐ │ name ┆ onset ┆ offset ┆ duration │ │ --- ┆ --- ┆ --- ┆ --- │ │ str ┆ i64 ┆ i64 ┆ i64 │ ╞══════════════╪═══════╪════════╪══════════╡ │ microsaccade ┆ 111 ┆ 149 ┆ 38 │ └──────────────┴───────┴────────┴──────────┘