Repository: aeye-lab/pymovements
Pull request #434 (2023-06-05 10:56:33)

feat(gaze): Add merging component columns into tuple columns
## Description
This PR implements the merging component columns into a single tuple column.

On initializing a dataframe we can now parse lists of column names for the specific type of channel:

```python
gaze = GazeDataFrame(
    data=df,
    experiment=experiment,
    pixel_columns = ['x_pix', 'y_pix'],
    position_columns = ['x_dva', 'y_dva'],
    velocity_columns = ['x_vel', 'y_vel'],
    acceleration_columns = ['x_acc', 'y_acc'],
)
```

This then results in a dataframe like this:

```
>>> gaze.frame
shape: (3, 4)
┌────────────┬────────────┬────────────┬──────────────┐
│ pixel      ┆ position   ┆ velocity   ┆ acceleration │
│ ---        ┆ ---        ┆ ---        ┆ ---          │
│ list[f64]  ┆ list[f64]  ┆ list[f64]  ┆ list[f64]    │
╞════════════╪════════════╪════════════╪══════════════╡
│ [0.1, 0.2] ┆ [1.1, 1.2] ┆ [3.1, 3.2] ┆ [5.1, 5.2]   │
│ [0.2, 0.4] ┆ [1.2, 1.4] ┆ [3.2, 3.4] ┆ [5.2, 5.4]   │
│ [0.3, 0.6] ┆ [1.3, 1.6] ┆ [3.3, 3.6] ┆ [5.3, 5.6]   │
└────────────┴────────────┴────────────┴──────────────┘
```

These names will be hardcoded in the `__init__()` for now.

When passing None as `*_columns` arguments (default value), there won't be the corresponding column in the initialized `GazeDataFrame`.

## Implemented changes

The PR is implemented as a silent enhancement. This way we can add required functionality one by one (e.g. `explode()`, integration into `DatasetDefinition`) without breaking everything.

Using the four new arguments is much more intuitive than what I have done earlier with the stuff around the `valid_columns` type of attributes in the `GazeDataFrame`. This PR now provides a better alternative with the named arguments, which will integrate nicely into the `DatasetDefinition` class.

- [x] Add method `GazeDataFrame.merge_component_columns_into_tuple_column()`
- [x] Add arguments `pixel_columns`, `position_columns`, `velocity_columns`, `acceleration_columns` to `GazeDataFrame.__init__()`
- [x] Add notes in docstring on how components are expected to be specified.

## Type of change

- [x] New feature (non-breaking change which adds functionality)
- [x] This change is or requires a documentation update

## How Has This Been Tested?

I have written tests exclusively for the `__init__()` method:

- [x] Test empty dataframe no schema
- [x] Test empty dataframe with schema no component columns
- [x] Test single row dataframe with two componenents for each argument
- [x] Test single row dataframe with six componenents for each argument

For each of the four arguments the following test cases are implemented:

- [x] Test two components (empty & single row, with & w/o unconsidered column )
- [x] Test four components (empty & single row, with & w/o unconsidered column )
- [x] Test six componenents (empty & single row, with & w/o unconsidered column )

Exceptions for each of the four arguments:
- [x] Test int, string and list[int] raise TypeError
- [x] Test empty list raises ValueError
- [x] Test 1, 3, 5 & 7 components raise VaueError
- [x] Test different datatypes in componenents raise TypeError
- [x] Test missing column raises pl.ColumnNotFoundError

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/434?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) Report
Patch coverage: **`100.00`**% and no project coverage change.
> Comparison is base [(`a1feb63`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/a1feb630d7a2527ac1a93b5aca14ff24a1879c35?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`cc869a0`)](https://app.codecov.io/gh/aeye-lab/pymovements/pull/434?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #434   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           56        56           
  Lines         1995      2025   +30     
  Branches       446       460   +14     
=========================================
+ Hits          1995      2025   +30     
```


| [Impacted Files](https://app.codecov.io/gh/aeye-lab/pymovements/pull/434?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) | Coverage Δ | |
|---|---|---|
| [src/pymovements/gaze/gaze\_dataframe.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/434?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvZ2F6ZV9kYXRhZnJhbWUucHk=) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/434?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).


Patch:
diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py
index c825ee3..abcae61 100644
--- a/src/pymovements/gaze/gaze_dataframe.py
+++ b/src/pymovements/gaze/gaze_dataframe.py
@@ -61,6 +61,11 @@ class GazeDataFrame:
             self,
             data: pl.DataFrame | None = None,
             experiment: Experiment | None = None,
+            *,
+            pixel_columns: list[str] | None = None,
+            position_columns: list[str] | None = None,
+            velocity_columns: list[str] | None = None,
+            acceleration_columns: list[str] | None = None,
     ):
         """Initialize a :py:class:`pymovements.gaze.gaze_dataframe.GazeDataFrame`.
 
@@ -70,11 +75,120 @@ class GazeDataFrame:
             A dataframe to be transformed to a polars dataframe.
         experiment : Experiment
             The experiment definition.
+        pixel_columns:
+            The name of the pixel position columns in the input data frame.
+        position_columns:
+            The name of the dva position columns in the input data frame.
+        velocity_columns:
+            The name of the dva velocity columns in the input data frame.
+        acceleration_columns:
+            The name of the dva acceleration columns in the input data frame.
+
+        Notes
+        -----
+        About using the arguments ``pixel_columns``, ``position_columns``, ``velocity_columns``,
+        and ``acceleration_columns``:
+
+        By passing a list of columns as any of these arguments, these columns will be merged into a
+        single column with the corresponding name , e.g. using `pixel_columns` will merge the
+        respective columns into the column `pixel`.
+
+        The supported number of component columns with the expected order are:
+
+        * two columns: monocular data; expected order: x-component, y-component
+        * four columns: binocular data; expected order: x-component left eye, y-component left eye,
+            x-component right eye, y-component right eye,
+        * six columns: binocular data with additional cyclopian data; expected order: x-component
+            left eye, y-component left eye, x-component right eye, y-component right eye,
+            x-component cyclopian eye, y-component cyclopian eye,
+
+
+        Examples
+        --------
+
+        First let's create an example `DataFrame` with three columns:
+        the timestamp ``t`` and ``x`` and ``y`` for the pixel position.
+
+        >>> df = pl.from_dict(
+        ...     data={'t': [1000, 1001, 1002], 'x': [0.1, 0.2, 0.3], 'y': [0.1, 0.2, 0.3]},
+        ... )
+        >>> df
+        shape: (3, 3)
+        ┌──────┬─────┬─────┐
+        │ t    ┆ x   ┆ y   │
+        │ ---  ┆ --- ┆ --- │
+        │ i64  ┆ f64 ┆ f64 │
+        ╞══════╪═════╪═════╡
+        │ 1000 ┆ 0.1 ┆ 0.1 │
+        │ 1001 ┆ 0.2 ┆ 0.2 │
+        │ 1002 ┆ 0.3 ┆ 0.3 │
+        └──────┴─────┴─────┘
+
+        We can now initialize our ``GazeDataFrame`` by specyfing the names of the pixel position
+        columns.
+        >>> gaze = GazeDataFrame(data=df, pixel_columns=['x', 'y'])
+        >>> gaze.frame
+        shape: (3, 2)
+        ┌──────┬────────────┐
+        │ t    ┆ pixel      │
+        │ ---  ┆ ---        │
+        │ i64  ┆ list[f64]  │
+        ╞══════╪════════════╡
+        │ 1000 ┆ [0.1, 0.1] │
+        │ 1001 ┆ [0.2, 0.2] │
+        │ 1002 ┆ [0.3, 0.3] │
+        └──────┴────────────┘
+
         """
         if data is None:
             data = pl.DataFrame()
+        else:
+            data = data.clone()
+        self.frame = data
+
+        if pixel_columns is not None:
+            _check_component_columns(
+                frame=self.frame,
+                pixel_columns=pixel_columns,
+            )
+            self.merge_component_columns_into_tuple_column(
+                input_columns=pixel_columns,
+                output_column='pixel',
+            )
+
+        if position_columns is not None:
+            _check_component_columns(
+                frame=self.frame,
+                position_columns=position_columns,
+            )
+
+            self.merge_component_columns_into_tuple_column(
+                input_columns=position_columns,
+                output_column='position',
+            )
+
+        if velocity_columns is not None:
+            _check_component_columns(
+                frame=self.frame,
+                velocity_columns=velocity_columns,
+            )
+
+            self.merge_component_columns_into_tuple_column(
+                input_columns=velocity_columns,
+                output_column='velocity',
+            )
+
+        if acceleration_columns is not None:
+            _check_component_columns(
+                frame=self.frame,
+                acceleration_columns=acceleration_columns,
+            )
+
+            self.merge_component_columns_into_tuple_column(
+                input_columns=acceleration_columns,
+                output_column='acceleration',
+            )
 
-        self.frame = data.clone()
         self.experiment = experiment
 
     def pix2deg(self) -> None:
@@ -253,6 +367,27 @@ class GazeDataFrame:
         position_columns = set(self._valid_position_columns) & set(self.frame.columns)
         return list(position_columns)
 
+    def merge_component_columns_into_tuple_column(
+            self,
+            input_columns: list[str],
+            output_column: str,
+    ) -> None:
+        """Merge component columns into a single tuple columns.
+
+        Input component columns will be dropped.
+
+        Parameters
+        ----------
+        input_columns:
+            Names of input columns to be merged into a single tuple column.
+        output_column:
+            Name of the resulting tuple column.
+        """
+        self.frame = self.frame.with_columns(
+            pl.concat_list([pl.col(component) for component in input_columns])
+            .alias(output_column),
+        ).drop(input_columns)
+
     @staticmethod
     def _pixel_to_dva_position_columns(columns: list[str]) -> list[str]:
         """Get corresponding dva position columns from pixel position columns."""
@@ -284,3 +419,39 @@ class GazeDataFrame:
         """Check if experiment attribute has been set."""
         if self.experiment is None:
             raise AttributeError('experiment must be specified for this method to work.')
+
+
+def _check_component_columns(
+        frame: pl.DataFrame,
+        **kwargs: list[str],
+) -> None:
+    """Check if component columns are in valid format."""
+    for component_type, columns in kwargs.items():
+        if not isinstance(columns, list):
+            raise TypeError(
+                f'{component_type} must be of type list, but is of type {type(columns).__name__}',
+            )
+
+        for column in columns:
+            if not isinstance(column, str):
+                raise TypeError(
+                    f'all elements in {component_type} must be of type str, '
+                    f'but one of the elements is of type {type(column).__name__}',
+                )
+
+        if len(columns) not in [2, 4, 6]:
+            raise ValueError(
+                f'{component_type} must contain either 2, 4 or 6 columns, but has {len(columns)}',
+            )
+
+        for column in columns:
+            if column not in frame.columns:
+                raise pl.exceptions.ColumnNotFoundError(
+                    f'column {column} from {component_type} is not available in dataframe',
+                )
+
+        if len(set(frame[columns].dtypes)) != 1:
+            types_list = sorted([str(t) for t in set(frame[columns].dtypes)])
+            raise ValueError(
+                f'all columns in {component_type} must be of same type, but types are {types_list}',
+            )


Test patch:
diff --git a/tests/gaze/gaze_dataframe_init_test.py b/tests/gaze/gaze_dataframe_init_test.py
new file mode 100644
index 0000000..12b9091
--- /dev/null
+++ b/tests/gaze/gaze_dataframe_init_test.py
@@ -0,0 +1,1152 @@
+# Copyright (c) 2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Test GazeDataFrame initialization."""
+import polars as pl
+import pytest
+from polars.testing import assert_frame_equal
+
+from pymovements.gaze.gaze_dataframe import GazeDataFrame
+
+
+@pytest.mark.parametrize(
+    ('init_kwargs', 'expected_frame'),
+    [
+        pytest.param(
+            {
+                'data': pl.DataFrame(),
+            },
+            pl.DataFrame(),
+            id='empty_df_no_schema',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'abc': pl.Int64}),
+            },
+            pl.DataFrame(schema={'abc': pl.Int64}),
+            id='empty_df_with_schema_no_component_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'pixel_columns': ['x', 'y'],
+            },
+            pl.DataFrame(schema={'pixel': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_two_pixel_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'abc': pl.Int64, 'x': pl.Float64, 'y': pl.Float64}),
+                'pixel_columns': ['x', 'y'],
+            },
+            pl.DataFrame(schema={'abc': pl.Int64, 'pixel': pl.List(pl.Float64)}),
+            id='empty_df_with_three_column_schema_two_pixel_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr': pl.Float64, 'yr': pl.Float64, 'xl': pl.Float64, 'yl': pl.Float64,
+                    },
+                ),
+                'pixel_columns': ['xr', 'yr', 'xl', 'yl'],
+            },
+            pl.DataFrame(schema={'pixel': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_four_pixel_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'x_right': pl.Float64, 'y_right': pl.Float64,
+                        'x_left': pl.Float64, 'y_left': pl.Float64,
+                        'x_avg': pl.Float64, 'y_avg': pl.Float64,
+                    },
+                ),
+                'pixel_columns': [
+                    'x_right', 'y_right', 'x_left', 'y_left', 'x_avg', 'y_avg',
+                ],
+            },
+            pl.DataFrame(schema={'pixel': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_six_pixel_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'x': [1.23], 'y': [4.56]}, schema={'x': pl.Float64, 'y': pl.Float64},
+                ),
+                'pixel_columns': ['x', 'y'],
+            },
+            pl.from_dict(
+                {'pixel': [[1.23, 4.56]]},
+                schema={'pixel': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_two_pixel_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'abc': [1], 'x': [1.23], 'y': [4.56]},
+                    schema={'abc': pl.Int64, 'x': pl.Float64, 'y': pl.Float64},
+                ),
+                'pixel_columns': ['x', 'y'],
+            },
+            pl.from_dict(
+                {'abc': [1], 'pixel': [[1.23, 4.56]]},
+                schema={'abc': pl.Int64, 'pixel': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_three_columns_two_pixel_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'xl': [1.2], 'yl': [3.4], 'xr': [5.6], 'yr': [7.8]},
+                    schema={'xl': pl.Float64, 'yl': pl.Float64, 'xr': pl.Float64, 'yr': pl.Float64},
+                ),
+                'pixel_columns': ['xl', 'yl', 'xr', 'yr'],
+            },
+            pl.from_dict(
+                {'pixel': [[1.2, 3.4, 5.6, 7.8]]},
+                schema={'pixel': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_four_pixel_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'x_right': [0.1], 'y_right': [0.2],
+                        'x_left': [0.3], 'y_left': [0.4],
+                        'x_avg': [0.5], 'y_avg': [0.6],
+                    },
+                    schema={
+                        'x_right': pl.Float64, 'y_right': pl.Float64,
+                        'x_left': pl.Float64, 'y_left': pl.Float64,
+                        'x_avg': pl.Float64, 'y_avg': pl.Float64,
+                    },
+                ),
+                'pixel_columns': [
+                    'x_right', 'y_right', 'x_left', 'y_left', 'x_avg', 'y_avg',
+                ],
+            },
+            pl.from_dict(
+                {'pixel': [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]},
+                schema={'pixel': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_six_pixel_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'position_columns': ['x', 'y'],
+            },
+            pl.DataFrame(schema={'position': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_two_position_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'abc': pl.Int64, 'x': pl.Float64, 'y': pl.Float64}),
+                'position_columns': ['x', 'y'],
+            },
+            pl.DataFrame(schema={'abc': pl.Int64, 'position': pl.List(pl.Float64)}),
+            id='empty_df_with_three_column_schema_two_position_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr': pl.Float64, 'yr': pl.Float64, 'xl': pl.Float64, 'yl': pl.Float64,
+                    },
+                ),
+                'position_columns': ['xr', 'yr', 'xl', 'yl'],
+            },
+            pl.DataFrame(schema={'position': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_four_position_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'x_right': pl.Float64, 'y_right': pl.Float64,
+                        'x_left': pl.Float64, 'y_left': pl.Float64,
+                        'x_avg': pl.Float64, 'y_avg': pl.Float64,
+                    },
+                ),
+                'position_columns': [
+                    'x_right', 'y_right', 'x_left', 'y_left', 'x_avg', 'y_avg',
+                ],
+            },
+            pl.DataFrame(schema={'position': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_six_position_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'x': [1.23], 'y': [4.56]}, schema={'x': pl.Float64, 'y': pl.Float64},
+                ),
+                'position_columns': ['x', 'y'],
+            },
+            pl.from_dict(
+                {'position': [[1.23, 4.56]]},
+                schema={'position': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_two_position_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'abc': [1], 'x': [1.23], 'y': [4.56]},
+                    schema={'abc': pl.Int64, 'x': pl.Float64, 'y': pl.Float64},
+                ),
+                'position_columns': ['x', 'y'],
+            },
+            pl.from_dict(
+                {'abc': [1], 'position': [[1.23, 4.56]]},
+                schema={'abc': pl.Int64, 'position': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_three_columns_two_position_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'xl': [1.2], 'yl': [3.4], 'xr': [5.6], 'yr': [7.8]},
+                    schema={'xl': pl.Float64, 'yl': pl.Float64, 'xr': pl.Float64, 'yr': pl.Float64},
+                ),
+                'position_columns': ['xl', 'yl', 'xr', 'yr'],
+            },
+            pl.from_dict(
+                {'position': [[1.2, 3.4, 5.6, 7.8]]},
+                schema={'position': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_four_position_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'x_right': [0.1], 'y_right': [0.2],
+                        'x_left': [0.3], 'y_left': [0.4],
+                        'x_avg': [0.5], 'y_avg': [0.6],
+                    },
+                    schema={
+                        'x_right': pl.Float64, 'y_right': pl.Float64,
+                        'x_left': pl.Float64, 'y_left': pl.Float64,
+                        'x_avg': pl.Float64, 'y_avg': pl.Float64,
+                    },
+                ),
+                'position_columns': [
+                    'x_right', 'y_right', 'x_left', 'y_left', 'x_avg', 'y_avg',
+                ],
+            },
+            pl.from_dict(
+                {'position': [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]},
+                schema={'position': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_six_position_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_vel': pl.Float64, 'y_vel': pl.Float64}),
+                'velocity_columns': ['x_vel', 'y_vel'],
+            },
+            pl.DataFrame(schema={'velocity': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_two_velocity_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'abc': pl.Int64, 'x_vel': pl.Float64, 'y_vel': pl.Float64,
+                    },
+                ),
+                'velocity_columns': ['x_vel', 'y_vel'],
+            },
+            pl.DataFrame(schema={'abc': pl.Int64, 'velocity': pl.List(pl.Float64)}),
+            id='empty_df_with_three_column_schema_two_velocity_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr_vel': pl.Float64, 'yr_vel': pl.Float64,
+                        'xl_vel': pl.Float64, 'yl_vel': pl.Float64,
+                    },
+                ),
+                'velocity_columns': ['xr_vel', 'yr_vel', 'xl_vel', 'yl_vel'],
+            },
+            pl.DataFrame(schema={'velocity': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_four_velocity_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'x_right_vel': pl.Float64, 'y_right_vel': pl.Float64,
+                        'x_left_vel': pl.Float64, 'y_left_vel': pl.Float64,
+                        'x_avg_vel': pl.Float64, 'y_avg_vel': pl.Float64,
+                    },
+                ),
+                'velocity_columns': [
+                    'x_right_vel', 'y_right_vel',
+                    'x_left_vel', 'y_left_vel',
+                    'x_avg_vel', 'y_avg_vel',
+                ],
+            },
+            pl.DataFrame(schema={'velocity': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_six_velocity_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'x_vel': [1.23], 'y_vel': [4.56]},
+                    schema={'x_vel': pl.Float64, 'y_vel': pl.Float64},
+                ),
+                'velocity_columns': ['x_vel', 'y_vel'],
+            },
+            pl.from_dict(
+                {'velocity': [[1.23, 4.56]]},
+                schema={'velocity': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_two_velocity_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'abc': [1], 'x_vel': [1.23], 'y_vel': [4.56]},
+                    schema={'abc': pl.Int64, 'x_vel': pl.Float64, 'y_vel': pl.Float64},
+                ),
+                'velocity_columns': ['x_vel', 'y_vel'],
+            },
+            pl.from_dict(
+                {'abc': [1], 'velocity': [[1.23, 4.56]]},
+                schema={'abc': pl.Int64, 'velocity': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_three_columns_two_velocity_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'xl_vel': [1.2], 'yl_vel': [3.4],
+                        'xr_vel': [5.6], 'yr_vel': [7.8],
+                    },
+                    schema={
+                        'xl_vel': pl.Float64, 'yl_vel': pl.Float64,
+                        'xr_vel': pl.Float64, 'yr_vel': pl.Float64,
+                    },
+                ),
+                'velocity_columns': ['xl_vel', 'yl_vel', 'xr_vel', 'yr_vel'],
+            },
+            pl.from_dict(
+                {'velocity': [[1.2, 3.4, 5.6, 7.8]]},
+                schema={'velocity': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_four_velocity_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'x_right_vel': [0.1], 'y_right_vel': [0.2],
+                        'x_left_vel': [0.3], 'y_left_vel': [0.4],
+                        'x_avg_vel': [0.5], 'y_avg_vel': [0.6],
+                    },
+                    schema={
+                        'x_right_vel': pl.Float64, 'y_right_vel': pl.Float64,
+                        'x_left_vel': pl.Float64, 'y_left_vel': pl.Float64,
+                        'x_avg_vel': pl.Float64, 'y_avg_vel': pl.Float64,
+                    },
+                ),
+                'velocity_columns': [
+                    'x_right_vel', 'y_right_vel',
+                    'x_left_vel', 'y_left_vel',
+                    'x_avg_vel', 'y_avg_vel',
+                ],
+            },
+            pl.from_dict(
+                {'velocity': [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]},
+                schema={'velocity': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_six_velocity_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_acc': pl.Float64, 'y_acc': pl.Float64}),
+                'acceleration_columns': ['x_acc', 'y_acc'],
+            },
+            pl.DataFrame(schema={'acceleration': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_two_acceleration_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'abc': pl.Int64, 'x_acc': pl.Float64, 'y_acc': pl.Float64,
+                    },
+                ),
+                'acceleration_columns': ['x_acc', 'y_acc'],
+            },
+            pl.DataFrame(schema={'abc': pl.Int64, 'acceleration': pl.List(pl.Float64)}),
+            id='empty_df_with_three_column_schema_two_acceleration_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr_acc': pl.Float64, 'yr_acc': pl.Float64,
+                        'xl_acc': pl.Float64, 'yl_acc': pl.Float64,
+                    },
+                ),
+                'acceleration_columns': ['xr_acc', 'yr_acc', 'xl_acc', 'yl_acc'],
+            },
+            pl.DataFrame(schema={'acceleration': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_four_acceleration_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'x_right_acc': pl.Float64, 'y_right_acc': pl.Float64,
+                        'x_left_acc': pl.Float64, 'y_left_acc': pl.Float64,
+                        'x_avg_acc': pl.Float64, 'y_avg_acc': pl.Float64,
+                    },
+                ),
+                'acceleration_columns': [
+                    'x_right_acc', 'y_right_acc',
+                    'x_left_acc', 'y_left_acc',
+                    'x_avg_acc', 'y_avg_acc',
+                ],
+            },
+            pl.DataFrame(schema={'acceleration': pl.List(pl.Float64)}),
+            id='empty_df_with_schema_six_acceleration_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'x_acc': [1.23], 'y_acc': [4.56]},
+                    schema={'x_acc': pl.Float64, 'y_acc': pl.Float64},
+                ),
+                'acceleration_columns': ['x_acc', 'y_acc'],
+            },
+            pl.from_dict(
+                {'acceleration': [[1.23, 4.56]]},
+                schema={'acceleration': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_two_acceleration_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {'abc': [1], 'x_acc': [1.23], 'y_acc': [4.56]},
+                    schema={'abc': pl.Int64, 'x_acc': pl.Float64, 'y_acc': pl.Float64},
+                ),
+                'acceleration_columns': ['x_acc', 'y_acc'],
+            },
+            pl.from_dict(
+                {'abc': [1], 'acceleration': [[1.23, 4.56]]},
+                schema={'abc': pl.Int64, 'acceleration': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_three_columns_two_acceleration_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'xl_acc': [1.2], 'yl_acc': [3.4],
+                        'xr_acc': [5.6], 'yr_acc': [7.8],
+                    },
+                    schema={
+                        'xl_acc': pl.Float64, 'yl_acc': pl.Float64,
+                        'xr_acc': pl.Float64, 'yr_acc': pl.Float64,
+                    },
+                ),
+                'acceleration_columns': ['xl_acc', 'yl_acc', 'xr_acc', 'yr_acc'],
+            },
+            pl.from_dict(
+                {'acceleration': [[1.2, 3.4, 5.6, 7.8]]},
+                schema={'acceleration': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_four_acceleration_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'x_right_acc': [0.1], 'y_right_acc': [0.2],
+                        'x_left_acc': [0.3], 'y_left_acc': [0.4],
+                        'x_avg_acc': [0.5], 'y_avg_acc': [0.6],
+                    },
+                    schema={
+                        'x_right_acc': pl.Float64, 'y_right_acc': pl.Float64,
+                        'x_left_acc': pl.Float64, 'y_left_acc': pl.Float64,
+                        'x_avg_acc': pl.Float64, 'y_avg_acc': pl.Float64,
+                    },
+                ),
+                'acceleration_columns': [
+                    'x_right_acc', 'y_right_acc',
+                    'x_left_acc', 'y_left_acc',
+                    'x_avg_acc', 'y_avg_acc',
+                ],
+            },
+            pl.from_dict(
+                {'acceleration': [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]},
+                schema={'acceleration': pl.List(pl.Float64)},
+            ),
+            id='df_single_row_six_acceleration_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'x_pix': [0.1], 'y_pix': [0.2],
+                        'x_dva': [1.1], 'y_dva': [1.2],
+                        'x_vel': [3.1], 'y_vel': [3.2],
+                        'x_acc': [5.1], 'y_acc': [5.2],
+                    },
+                    schema={
+                        'x_pix': pl.Float64, 'y_pix': pl.Float64,
+                        'x_dva': pl.Float64, 'y_dva': pl.Float64,
+                        'x_vel': pl.Float64, 'y_vel': pl.Float64,
+                        'x_acc': pl.Float64, 'y_acc': pl.Float64,
+                    },
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+                'position_columns': ['x_dva', 'y_dva'],
+                'velocity_columns': ['x_vel', 'y_vel'],
+                'acceleration_columns': ['x_acc', 'y_acc'],
+            },
+            pl.from_dict(
+                {
+                    'pixel': [[0.1, 0.2]],
+                    'position': [[1.1, 1.2]],
+                    'velocity': [[3.1, 3.2]],
+                    'acceleration': [[5.1, 5.2]],
+                },
+                schema={
+                    'pixel': pl.List(pl.Float64),
+                    'position': pl.List(pl.Float64),
+                    'velocity': pl.List(pl.Float64),
+                    'acceleration': pl.List(pl.Float64),
+                },
+            ),
+            id='df_single_row_all_types_two_columns',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'x_right_pos_pix': [0.1], 'y_right_pos_pix': [0.2],
+                        'x_left_pos_pix': [0.3], 'y_left_pos_pix': [0.4],
+                        'x_avg_pos_pix': [0.5], 'y_avg_pos_pix': [0.6],
+                        'x_right_pos_dva': [1.1], 'y_right_pos_dva': [1.2],
+                        'x_left_pos_dva': [1.3], 'y_left_pos_dva': [1.4],
+                        'x_avg_pos_dva': [1.5], 'y_avg_pos_dva': [1.6],
+                        'x_right_vel_dva': [3.1], 'y_right_vel_dva': [3.2],
+                        'x_left_vel_dva': [3.3], 'y_left_vel_dva': [3.4],
+                        'x_avg_vel_dva': [3.5], 'y_avg_vel_dva': [3.6],
+                        'x_right_acc_dva': [5.1], 'y_right_acc_dva': [5.2],
+                        'x_left_acc_dva': [5.3], 'y_left_acc_dva': [5.4],
+                        'x_avg_acc_dva': [5.5], 'y_avg_acc_dva': [5.6],
+                    },
+                    schema={
+                        'x_right_pos_pix': pl.Float64, 'y_right_pos_pix': pl.Float64,
+                        'x_left_pos_pix': pl.Float64, 'y_left_pos_pix': pl.Float64,
+                        'x_avg_pos_pix': pl.Float64, 'y_avg_pos_pix': pl.Float64,
+                        'x_right_pos_dva': pl.Float64, 'y_right_pos_dva': pl.Float64,
+                        'x_left_pos_dva': pl.Float64, 'y_left_pos_dva': pl.Float64,
+                        'x_avg_pos_dva': pl.Float64, 'y_avg_pos_dva': pl.Float64,
+                        'x_right_vel_dva': pl.Float64, 'y_right_vel_dva': pl.Float64,
+                        'x_left_vel_dva': pl.Float64, 'y_left_vel_dva': pl.Float64,
+                        'x_avg_vel_dva': pl.Float64, 'y_avg_vel_dva': pl.Float64,
+                        'x_right_acc_dva': pl.Float64, 'y_right_acc_dva': pl.Float64,
+                        'x_left_acc_dva': pl.Float64, 'y_left_acc_dva': pl.Float64,
+                        'x_avg_acc_dva': pl.Float64, 'y_avg_acc_dva': pl.Float64,
+                    },
+                ),
+                'pixel_columns': [
+                    'x_right_pos_pix', 'y_right_pos_pix',
+                    'x_left_pos_pix', 'y_left_pos_pix',
+                    'x_avg_pos_pix', 'y_avg_pos_pix',
+                ],
+                'position_columns': [
+                    'x_right_pos_dva', 'y_right_pos_dva',
+                    'x_left_pos_dva', 'y_left_pos_dva',
+                    'x_avg_pos_dva', 'y_avg_pos_dva',
+                ],
+                'velocity_columns': [
+                    'x_right_vel_dva', 'y_right_vel_dva',
+                    'x_left_vel_dva', 'y_left_vel_dva',
+                    'x_avg_vel_dva', 'y_avg_vel_dva',
+                ],
+                'acceleration_columns': [
+                    'x_right_acc_dva', 'y_right_acc_dva',
+                    'x_left_acc_dva', 'y_left_acc_dva',
+                    'x_avg_acc_dva', 'y_avg_acc_dva',
+                ],
+            },
+            pl.from_dict(
+                {
+                    'pixel': [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]],
+                    'position': [[1.1, 1.2, 1.3, 1.4, 1.5, 1.6]],
+                    'velocity': [[3.1, 3.2, 3.3, 3.4, 3.5, 3.6]],
+                    'acceleration': [[5.1, 5.2, 5.3, 5.4, 5.5, 5.6]],
+                },
+                schema={
+                    'pixel': pl.List(pl.Float64),
+                    'position': pl.List(pl.Float64),
+                    'velocity': pl.List(pl.Float64),
+                    'acceleration': pl.List(pl.Float64),
+                },
+            ),
+            id='df_single_row_all_types_six_columns',
+        ),
+    ],
+)
+def test_init_gaze_dataframe_has_expected_frame(init_kwargs, expected_frame):
+    gaze = GazeDataFrame(**init_kwargs)
+    assert_frame_equal(gaze.frame, expected_frame)
+
+
+@pytest.mark.parametrize(
+    ('init_kwargs', 'exception', 'exception_msg'),
+    [
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'pixel_columns': 1,
+            },
+            TypeError,
+            'pixel_columns must be of type list, but is of type int',
+            id='pixel_columns_int',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'pixel_columns': 'x',
+            },
+            TypeError,
+            'pixel_columns must be of type list, but is of type str',
+            id='pixel_columns_str',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'pixel_columns': [],
+            },
+            ValueError,
+            'pixel_columns must contain either 2, 4 or 6 columns, but has 0',
+            id='pixel_columns_empty_list',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'pixel_columns': [0, 1],
+            },
+            TypeError,
+            'all elements in pixel_columns must be of type str,'
+            ' but one of the elements is of type int',
+            id='pixel_columns_list_elements_not_string',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'pixel_columns': ['x'],
+            },
+            ValueError,
+            'pixel_columns must contain either 2, 4 or 6 columns, but has 1',
+            id='pixel_columns_list_of_one',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr': pl.Float64, 'yr': pl.Float64, 'xl': pl.Float64, 'yl': pl.Float64,
+                    },
+                ),
+                'pixel_columns': ['xr', 'xl', 'yl'],
+            },
+            ValueError,
+            'pixel_columns must contain either 2, 4 or 6 columns, but has 3',
+            id='pixel_columns_list_of_three',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr': pl.Float64, 'yr': pl.Float64,
+                        'xl': pl.Float64, 'yl': pl.Float64,
+                        'xa': pl.Float64, 'ya': pl.Float64,
+                    },
+                ),
+                'pixel_columns': ['xr', 'yr', 'xl', 'yl', 'xa'],
+            },
+            ValueError,
+            'pixel_columns must contain either 2, 4 or 6 columns, but has 5',
+            id='pixel_columns_list_of_five',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'abc': pl.Int64,
+                        'xr': pl.Float64, 'yr': pl.Float64,
+                        'xl': pl.Float64, 'yl': pl.Float64,
+                        'xa': pl.Float64, 'ya': pl.Float64,
+                    },
+                ),
+                'pixel_columns': ['xr', 'yr', 'xl', 'yl', 'xa', 'ya', 'abc'],
+            },
+            ValueError,
+            'pixel_columns must contain either 2, 4 or 6 columns, but has 7',
+            id='pixel_columns_list_of_seven',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Int64}),
+                'pixel_columns': ['x', 'y'],
+            },
+            ValueError,
+            'all columns in pixel_columns must be of same type, but types are'
+            " ['Float64', 'Int64']",
+            id='pixel_columns_different_type',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64}),
+                'pixel_columns': ['x', 'y'],
+            },
+            pl.exceptions.ColumnNotFoundError,
+            'column y from pixel_columns is not available in dataframe',
+            id='pixel_columns_missing_column',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'position_columns': 1,
+            },
+            TypeError,
+            'position_columns must be of type list, but is of type int',
+            id='position_columns_int',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'position_columns': 'x',
+            },
+            TypeError,
+            'position_columns must be of type list, but is of type str',
+            id='position_columns_str',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'position_columns': [],
+            },
+            ValueError,
+            'position_columns must contain either 2, 4 or 6 columns, but has 0',
+            id='position_columns_empty_list',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'position_columns': [0, 1],
+            },
+            TypeError,
+            'all elements in position_columns must be of type str,'
+            ' but one of the elements is of type int',
+            id='position_columns_list_elements_not_string',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'position_columns': ['x'],
+            },
+            ValueError,
+            'position_columns must contain either 2, 4 or 6 columns, but has 1',
+            id='position_columns_list_of_one',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr': pl.Float64, 'yr': pl.Float64, 'xl': pl.Float64, 'yl': pl.Float64,
+                    },
+                ),
+                'position_columns': ['xr', 'xl', 'yl'],
+            },
+            ValueError,
+            'position_columns must contain either 2, 4 or 6 columns, but has 3',
+            id='position_columns_list_of_three',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr': pl.Float64, 'yr': pl.Float64,
+                        'xl': pl.Float64, 'yl': pl.Float64,
+                        'xa': pl.Float64, 'ya': pl.Float64,
+                    },
+                ),
+                'position_columns': ['xr', 'yr', 'xl', 'yl', 'xa'],
+            },
+            ValueError,
+            'position_columns must contain either 2, 4 or 6 columns, but has 5',
+            id='position_columns_list_of_five',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'abc': pl.Int64,
+                        'xr': pl.Float64, 'yr': pl.Float64,
+                        'xl': pl.Float64, 'yl': pl.Float64,
+                        'xa': pl.Float64, 'ya': pl.Float64,
+                    },
+                ),
+                'position_columns': ['xr', 'yr', 'xl', 'yl', 'xa', 'ya', 'abc'],
+            },
+            ValueError,
+            'position_columns must contain either 2, 4 or 6 columns, but has 7',
+            id='position_columns_list_of_seven',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Int64}),
+                'position_columns': ['x', 'y'],
+            },
+            ValueError,
+            'all columns in position_columns must be of same type, but types are'
+            " ['Float64', 'Int64']",
+            id='position_columns_different_type',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64}),
+                'position_columns': ['x', 'y'],
+            },
+            pl.exceptions.ColumnNotFoundError,
+            'column y from position_columns is not available in dataframe',
+            id='position_columns_missing_column',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_vel': pl.Float64, 'y_vel': pl.Float64}),
+                'velocity_columns': 1,
+            },
+            TypeError,
+            'velocity_columns must be of type list, but is of type int',
+            id='velocity_columns_int',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_vel': pl.Float64, 'y_vel': pl.Float64}),
+                'velocity_columns': 'x_vel',
+            },
+            TypeError,
+            'velocity_columns must be of type list, but is of type str',
+            id='velocity_columns_str',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_vel': pl.Float64, 'y_vel': pl.Float64}),
+                'velocity_columns': [],
+            },
+            ValueError,
+            'velocity_columns must contain either 2, 4 or 6 columns, but has 0',
+            id='velocity_columns_empty_list',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_vel': pl.Float64, 'y_vel': pl.Float64}),
+                'velocity_columns': [0, 1],
+            },
+            TypeError,
+            'all elements in velocity_columns must be of type str,'
+            ' but one of the elements is of type int',
+            id='velocity_columns_list_elements_not_string',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_vel': pl.Float64, 'y_vel': pl.Float64}),
+                'velocity_columns': ['x_vel'],
+            },
+            ValueError,
+            'velocity_columns must contain either 2, 4 or 6 columns, but has 1',
+            id='velocity_columns_list_of_one',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr_vel': pl.Float64, 'yr_vel': pl.Float64,
+                        'xl_vel': pl.Float64, 'yl_vel': pl.Float64,
+                    },
+                ),
+                'velocity_columns': ['xr_vel', 'xl_vel', 'yl_vel'],
+            },
+            ValueError,
+            'velocity_columns must contain either 2, 4 or 6 columns, but has 3',
+            id='velocity_columns_list_of_three',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr_vel': pl.Float64, 'yr_vel': pl.Float64,
+                        'xl_vel': pl.Float64, 'yl_vel': pl.Float64,
+                        'xa_vel': pl.Float64, 'ya_vel': pl.Float64,
+                    },
+                ),
+                'velocity_columns': ['xr_vel', 'yr_vel', 'xl_vel', 'yl_vel', 'xa_vel'],
+            },
+            ValueError,
+            'velocity_columns must contain either 2, 4 or 6 columns, but has 5',
+            id='velocity_columns_list_of_five',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'abc': pl.Int64,
+                        'xr_vel': pl.Float64, 'yr_vel': pl.Float64,
+                        'xl_vel': pl.Float64, 'yl_vel': pl.Float64,
+                        'xa_vel': pl.Float64, 'ya_vel': pl.Float64,
+                    },
+                ),
+                'velocity_columns': [
+                    'xr_vel', 'yr_vel', 'xl_vel', 'yl_vel', 'xa_vel', 'ya_vel', 'abc',
+                ],
+            },
+            ValueError,
+            'velocity_columns must contain either 2, 4 or 6 columns, but has 7',
+            id='velocity_columns_list_of_seven',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_vel': pl.Float64, 'y_vel': pl.Int64}),
+                'velocity_columns': ['x_vel', 'y_vel'],
+            },
+            ValueError,
+            'all columns in velocity_columns must be of same type, but types are'
+            " ['Float64', 'Int64']",
+            id='velocity_columns_different_type',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_vel': pl.Float64}),
+                'velocity_columns': ['x_vel', 'y_vel'],
+            },
+            pl.exceptions.ColumnNotFoundError,
+            'column y_vel from velocity_columns is not available in dataframe',
+            id='velocity_columns_missing_column',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_acc': pl.Float64, 'y_acc': pl.Float64}),
+                'acceleration_columns': 1,
+            },
+            TypeError,
+            'acceleration_columns must be of type list, but is of type int',
+            id='acceleration_columns_int',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_acc': pl.Float64, 'y_acc': pl.Float64}),
+                'acceleration_columns': 'x_acc',
+            },
+            TypeError,
+            'acceleration_columns must be of type list, but is of type str',
+            id='acceleration_columns_str',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_acc': pl.Float64, 'y_acc': pl.Float64}),
+                'acceleration_columns': [],
+            },
+            ValueError,
+            'acceleration_columns must contain either 2, 4 or 6 columns, but has 0',
+            id='acceleration_columns_empty_list',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_acc': pl.Float64, 'y_acc': pl.Float64}),
+                'acceleration_columns': [0, 1],
+            },
+            TypeError,
+            'all elements in acceleration_columns must be of type str,'
+            ' but one of the elements is of type int',
+            id='acceleration_columns_list_elements_not_string',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_acc': pl.Float64, 'y_acc': pl.Float64}),
+                'acceleration_columns': ['x_acc'],
+            },
+            ValueError,
+            'acceleration_columns must contain either 2, 4 or 6 columns, but has 1',
+            id='acceleration_columns_list_of_one',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr_acc': pl.Float64, 'yr_acc': pl.Float64,
+                        'xl_acc': pl.Float64, 'yl_acc': pl.Float64,
+                    },
+                ),
+                'acceleration_columns': ['xr_acc', 'xl_acc', 'yl_acc'],
+            },
+            ValueError,
+            'acceleration_columns must contain either 2, 4 or 6 columns, but has 3',
+            id='acceleration_columns_list_of_three',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'xr_acc': pl.Float64, 'yr_acc': pl.Float64,
+                        'xl_acc': pl.Float64, 'yl_acc': pl.Float64,
+                        'xa_acc': pl.Float64, 'ya_acc': pl.Float64,
+                    },
+                ),
+                'acceleration_columns': ['xr_acc', 'yr_acc', 'xl_acc', 'yl_acc', 'xa_acc'],
+            },
+            ValueError,
+            'acceleration_columns must contain either 2, 4 or 6 columns, but has 5',
+            id='acceleration_columns_list_of_five',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(
+                    schema={
+                        'abc': pl.Int64,
+                        'xr_acc': pl.Float64, 'yr_acc': pl.Float64,
+                        'xl_acc': pl.Float64, 'yl_acc': pl.Float64,
+                        'xa_acc': pl.Float64, 'ya_acc': pl.Float64,
+                    },
+                ),
+                'acceleration_columns': [
+                    'xr_acc', 'yr_acc', 'xl_acc', 'yl_acc', 'xa_acc', 'ya_acc', 'abc',
+                ],
+            },
+            ValueError,
+            'acceleration_columns must contain either 2, 4 or 6 columns, but has 7',
+            id='acceleration_columns_list_of_seven',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_acc': pl.Float64, 'y_acc': pl.Int64}),
+                'acceleration_columns': ['x_acc', 'y_acc'],
+            },
+            ValueError,
+            'all columns in acceleration_columns must be of same type, but types are'
+            " ['Float64', 'Int64']",
+            id='acceleration_columns_different_type',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x_acc': pl.Float64}),
+                'acceleration_columns': ['x_acc', 'y_acc'],
+            },
+            pl.exceptions.ColumnNotFoundError,
+            'column y_acc from acceleration_columns is not available in dataframe',
+            id='acceleration_columns_missing_column',
+        ),
+    ],
+)
+def test_event_dataframe_init_exceptions(init_kwargs, exception, exception_msg):
+    with pytest.raises(exception) as excinfo:
+        GazeDataFrame(**init_kwargs)
+
+    msg, = excinfo.value.args
+    assert msg == exception_msg