idt#

pymovements.events.detection.idt(positions: list[list[float]] | list[tuple[float, float]] | ndarray | Series, timesteps: list[int] | ndarray | Series | None = None, minimum_duration: int = 100, dispersion_threshold: float = 1.0, include_nan: bool = False, name: str = 'fixation') Events[source]#

Fixation identification based on a dispersion threshold (I-DT).

The algorithm identifies fixations by grouping consecutive points within a maximum separation (dispersion) threshold and a minimum duration threshold. The algorithm uses a moving window to check the dispersion of the points in the window. If the dispersion is below the threshold, the window represents a fixation, and the window is expanded until the dispersion is above threshold.

The implementation and its default parameter values are based on the description and pseudocode from Salvucci and Goldberg [Salvucci and Goldberg, 2000].

Parameters:
  • positions (list[list[float]] | list[tuple[float, float]] | numpy.ndarray | polars.Series) – shape (N, 2) Continuous 2D position time series

  • 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 fixation duration. The duration is specified in the units used in timesteps. If timesteps is None, then minimum_duration is specified in numbers of samples. (default: 100)

  • dispersion_threshold (float) – Threshold for dispersion for a group of consecutive samples to be identified as fixation. (default: 1.0)

  • 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: ‘fixation’)

Returns:

A dataframe with detected fixations as rows.

Return type:

Events

Raises:
  • TypeError – If pixels is a polars Series and dtype not List If minimum_duration is not of type int or timesteps

  • ValueError – If positions is not shaped (N, 2) If dispersion_threshold is not greater than 0 If duration_threshold is not greater than 0

Examples

Create a synthetic step signal representing gaze segments.

>>> import numpy as np
>>> from pymovements.synthetic import step_function
>>> from pymovements.gaze import from_numpy
>>> positions = step_function(
...     length=200, steps=[2, 5, 9, 111, 150],
...     values=[(1., 2.), (2., 3.), (3., 4.), (1., 1.), (2., 2.)],
...     start_value=(0., 0.),
... )
>>> positions.shape
(200, 2)

Apply event detection algorithm on numpy array:

>>> idt(positions)
shape: (1, 4)
┌──────────┬───────┬────────┬──────────┐
│ name     ┆ onset ┆ offset ┆ duration │
│ ---      ┆ ---   ┆ ---    ┆ ---      │
│ str      ┆ i64   ┆ i64    ┆ i64      │
╞══════════╪═══════╪════════╪══════════╡
│ fixation ┆ 9     ┆ 111    ┆ 102      │
└──────────┴───────┴────────┴──────────┘

Run fixation detection with custom parameters:

>>> idt(positions, minimum_duration = 50, dispersion_threshold = 0.5)
shape: (2, 4)
┌──────────┬───────┬────────┬──────────┐
│ name     ┆ onset ┆ offset ┆ duration │
│ ---      ┆ ---   ┆ ---    ┆ ---      │
│ str      ┆ i64   ┆ i64    ┆ i64      │
╞══════════╪═══════╪════════╪══════════╡
│ fixation ┆ 9     ┆ 111    ┆ 102      │
│ fixation ┆ 150   ┆ 199    ┆ 49       │
└──────────┴───────┴────────┴──────────┘

Polars series are also supported as input. Let’s create a nested position series from our numpy array:

>>> df = polars.from_numpy(positions, schema=['x', 'y'])
>>> position_series = df.select(polars.concat_list(('x', 'y')).alias('position'))['position']
>>> position_series
shape: (200,)
Series: 'position' [list[f64]]
[
    [0.0, 0.0]
    [0.0, 0.0]
    [1.0, 2.0]
    [1.0, 2.0]
    [1.0, 2.0]

    [2.0, 2.0]
    [2.0, 2.0]
    [2.0, 2.0]
    [2.0, 2.0]
    [2.0, 2.0]
]

Apply event detection algorithm on polars series:

>>> idt(position_series)
shape: (1, 4)
┌──────────┬───────┬────────┬──────────┐
│ name     ┆ onset ┆ offset ┆ duration │
│ ---      ┆ ---   ┆ ---    ┆ ---      │
│ str      ┆ i64   ┆ i64    ┆ i64      │
╞══════════╪═══════╪════════╪══════════╡
│ fixation ┆ 9     ┆ 111    ┆ 102      │
└──────────┴───────┴────────┴──────────┘

We can also apply the detection on a Gaze object.

>>> from pymovements import Experiment
>>> gaze = from_numpy(
...    position=positions.T,
...    time=np.arange(len(positions)),
... )
>>> gaze
shape: (200, 2)
┌──────┬────────────┐
│ time ┆ position   │
│ ---  ┆ ---        │
│ i64  ┆ list[f64]  │
╞══════╪════════════╡
│ 0    ┆ [0.0, 0.0] │
│ 1    ┆ [0.0, 0.0] │
│ 2    ┆ [1.0, 2.0] │
│ 3    ┆ [1.0, 2.0] │
│ 4    ┆ [1.0, 2.0] │
│ …    ┆ …          │
│ 195  ┆ [2.0, 2.0] │
│ 196  ┆ [2.0, 2.0] │
│ 197  ┆ [2.0, 2.0] │
│ 198  ┆ [2.0, 2.0] │
│ 199  ┆ [2.0, 2.0] │
└──────┴────────────┘

Run fixation detection by using the detect() method.

>>> gaze.detect('idt')
>>> gaze.events
shape: (1, 4)
┌──────────┬───────┬────────┬──────────┐
│ name     ┆ onset ┆ offset ┆ duration │
│ ---      ┆ ---   ┆ ---    ┆ ---      │
│ str      ┆ i64   ┆ i64    ┆ i64      │
╞══════════╪═══════╪════════╪══════════╡
│ fixation ┆ 9     ┆ 111    ┆ 102      │
└──────────┴───────┴────────┴──────────┘

Passing parameters to detect():

>>> gaze.detect('idt', minimum_duration = 50, dispersion_threshold = 0.5, name='fixation_idt')
>>> gaze.events.filter_by_name('fixation_idt')
shape: (2, 4)
┌──────────────┬───────┬────────┬──────────┐
│ name         ┆ onset ┆ offset ┆ duration │
│ ---          ┆ ---   ┆ ---    ┆ ---      │
│ str          ┆ i64   ┆ i64    ┆ i64      │
╞══════════════╪═══════╪════════╪══════════╡
│ fixation_idt ┆ 9     ┆ 111    ┆ 102      │
│ fixation_idt ┆ 150   ┆ 199    ┆ 49       │
└──────────────┴───────┴────────┴──────────┘