Repository: aeye-lab/pymovements
Pull request #443 (2023-06-08 14:10:00)

feat(dataset)!: Integrate column specifications to Dataset
This will create the first breaking changes

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?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 [(`7c575bd`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/7c575bde944009307b496c6e463fb1c8900bceec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`2c49127`)](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?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      #443   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           55        55           
  Lines         2024      2089   +65     
  Branches       462       432   -30     
=========================================
+ Hits          2024      2089   +65     
```


| [Impacted Files](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) | Coverage Δ | |
|---|---|---|
| [src/pymovements/events/event\_processing.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2V2ZW50cy9ldmVudF9wcm9jZXNzaW5nLnB5) | `100.00% <ø> (ø)` | |
| [src/pymovements/dataset/dataset.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2RhdGFzZXQvZGF0YXNldC5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/dataset/dataset\_definition.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2RhdGFzZXQvZGF0YXNldF9kZWZpbml0aW9uLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/dataset/dataset\_files.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2RhdGFzZXQvZGF0YXNldF9maWxlcy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/datasets/gazebase.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2RhdGFzZXRzL2dhemViYXNlLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/datasets/gazebasevr.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2RhdGFzZXRzL2dhemViYXNldnIucHk=) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/datasets/judo1000.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2RhdGFzZXRzL2p1ZG8xMDAwLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/datasets/toy\_dataset.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2RhdGFzZXRzL3RveV9kYXRhc2V0LnB5) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/datasets/toy\_dataset\_eyelink.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2RhdGFzZXRzL3RveV9kYXRhc2V0X2V5ZWxpbmsucHk=) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/gaze/gaze\_dataframe.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?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%> (ø)` | |
| ... and [4 more](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/443?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/docs/source/tutorials/heatmap.ipynb b/docs/source/tutorials/heatmap.ipynb
index bbfcdff..feb3840 100644
--- a/docs/source/tutorials/heatmap.ipynb
+++ b/docs/source/tutorials/heatmap.ipynb
@@ -101,7 +101,7 @@
    "source": [
     "figure = pm.plotting.heatmap(\n",
     "    gaze=dataset.gaze[5],\n",
-    "    position_columns=('x_right_pix', 'y_right_pix'),\n",
+    "    position_column='pixel',\n",
     "    origin='upper',\n",
     "    show_cbar=True,\n",
     "    cbar_label='Time [s]',\n",
@@ -127,7 +127,7 @@
    "source": [
     "figure = pm.plotting.heatmap(\n",
     "    dataset.gaze[5],\n",
-    "    position_columns=('x_right_pix', 'y_right_pix'),\n",
+    "    position_column='pixel',\n",
     "    origin='upper',\n",
     "    show_cbar=True,\n",
     "    cbar_label='Time [s]',\n",
@@ -154,7 +154,7 @@
    "source": [
     "figure = pm.plotting.heatmap(\n",
     "    dataset.gaze[5],\n",
-    "    position_columns=('x_right_pix', 'y_right_pix'),\n",
+    "    position_column='pixel',\n",
     "    origin='upper',\n",
     "    show_cbar=True,\n",
     "    cbar_label='Time [s]',\n",
diff --git a/docs/source/tutorials/local-dataset.ipynb b/docs/source/tutorials/local-dataset.ipynb
index bbd4494..6e2cfe1 100644
--- a/docs/source/tutorials/local-dataset.ipynb
+++ b/docs/source/tutorials/local-dataset.ipynb
@@ -150,52 +150,45 @@
   },
   {
    "cell_type": "markdown",
-   "id": "7e32b202",
-   "metadata": {},
-   "source": [
-    "We can also adjust how the CSV files are read."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "id": "e039de9b",
+   "id": "a1f91fdd",
    "metadata": {},
    "source": [
-    "The `column_map` dictionary maps the original column names in the CSV files to the desired column names. Here the original column names are 'timestamp', **'x'**, and **'y'**, and the desired column names are **'time'**, **'x_pix'**, and **'y_pix'**, respectively."
+    "We can also adjust how the CSV files are read.\n",
+    "Here, we specify that the separator in the CSV files is a tab (**'\\t'**)."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "19d897a3",
+   "id": "14b40a9f",
    "metadata": {},
    "outputs": [],
    "source": [
-    "column_map = {\n",
-    "    'timestamp': 'time',\n",
-    "    'x': 'x_pix',\n",
-    "    'y': 'y_pix',\n",
+    "custom_read_kwargs = {\n",
+    "    'separator': '\\t',\n",
     "}"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "a1f91fdd",
+   "id": "e039de9b",
    "metadata": {},
    "source": [
-    "Here, we specify that the separator in the CSV files is a tab (**'\\t'**)."
+    "The `time_column` and `pixel_columns` arguments can be used to specify your columns according to their content.\n",
+    "This is needed for correctly applying all preprocessing methods.\n",
+    "\n",
+    "Depending on the content of your dataset, you can alternatively also provide `position_columns`, `velocity_columns` and `acceleration_columns`."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "14b40a9f",
+   "id": "19d897a3",
    "metadata": {},
    "outputs": [],
    "source": [
-    "custom_read_kwargs = {\n",
-    "    'separator': '\\t',\n",
-    "}"
+    "time_column = 'timestamp'\n",
+    "pixel_columns = ['x', 'y']"
    ]
   },
   {
@@ -226,8 +219,9 @@
     "    experiment=experiment,\n",
     "    filename_format=filename_format,\n",
     "    filename_format_dtypes=filename_format_dtypes,\n",
-    "    column_map=column_map,\n",
     "    custom_read_kwargs=custom_read_kwargs,\n",
+    "    time_column=time_column,\n",
+    "    pixel_columns=pixel_columns,\n",
     ")"
    ]
   },
diff --git a/src/pymovements/dataset/dataset.py b/src/pymovements/dataset/dataset.py
index b2f98bc..47e55a9 100644
--- a/src/pymovements/dataset/dataset.py
+++ b/src/pymovements/dataset/dataset.py
@@ -395,33 +395,67 @@ class Dataset:
         if isinstance(method, str):
             method = EventDetectionLibrary.get(method)
 
+        # this is just a work-around until merged columns are standard behavior
+        # https://github.com/aeye-lab/pymovements/pull/443
+        exploded_columns = {}
+        if 'position' in self.gaze[0].frame.columns:
+            exploded_columns_pos = [
+                'x_left_pos', 'y_left_pos',
+                'x_right_pos', 'y_right_pos',
+                'x_avg_pos', 'y_avg_pos',
+            ][:self.gaze[0].n_components]
+            exploded_columns['position'] = exploded_columns_pos
+        else:
+            raise pl.exceptions.ColumnNotFoundError(
+                f'Column \'position\' not found.'
+                f' Available columns are: {self.gaze[0].frame.columns}',
+            )
+
+        if 'velocity' in self.gaze[0].frame.columns:
+            exploded_columns_vel = [
+                'x_left_vel', 'y_left_vel',
+                'x_right_vel', 'y_right_vel',
+                'x_avg_vel', 'y_avg_vel',
+            ][:self.gaze[0].n_components]
+            exploded_columns['velocity'] = exploded_columns_vel
+        else:
+            raise pl.exceptions.ColumnNotFoundError(
+                f'Column \'velocity\' not found.'
+                f' Available columns are: {self.gaze[0].frame.columns}',
+            )
+
+        self.gaze[0].explode('position', exploded_columns['position'])
+        self.gaze[0].explode('velocity', exploded_columns['velocity'])
+
+        if (
+                isinstance(self.gaze[0].n_components, int)
+                and self.gaze[0].n_components < 4
+                and eye not in [None, 'auto']
+        ):
+            raise AttributeError()
+
         # Automatically infer eye to use for event detection.
         if eye == 'auto':
-            if 'x_right_pos' in self.gaze[0].columns:
+            if 'x_avg_pos' in self.gaze[0].columns:
+                eye = 'avg'
+            elif 'x_right_pos' in self.gaze[0].columns:
                 eye = 'right'
-            elif 'x_left_pos' in self.gaze[0].columns:
-                eye = 'left'
-            elif 'x_pos' in self.gaze[0].columns:
-                eye = None
             else:
-                raise AttributeError(
-                    'Either right or left eye columns must be present in gaze data frame.'
-                    f' Available columns are: {self.gaze[0].columns}',
-                )
-
-        if eye is None:
-            position_columns = ['x_pos', 'y_pos']
-            velocity_columns = ['x_vel', 'y_vel']
-        else:
-            position_columns = [f'x_{eye}_pos', f'y_{eye}_pos']
-            velocity_columns = [f'x_{eye}_vel', f'y_{eye}_vel']
+                eye = 'left'
 
-        if not set(position_columns).issubset(set(self.gaze[0].columns)):
-            raise AttributeError(
-                f'{eye} eye specified but required columns are not available in gaze dataframe.'
-                f' required columns: {position_columns}'
-                f', available columns: {self.gaze[0].columns}',
-            )
+        position_columns = [f'x_{eye}_pos', f'y_{eye}_pos']
+        velocity_columns = [f'x_{eye}_vel', f'y_{eye}_vel']
+
+        # this is just a work-around until merged columns are standard behavior
+        # https://github.com/aeye-lab/pymovements/pull/443
+        self.gaze[0].merge_component_columns_into_tuple_column(
+            input_columns=exploded_columns['position'],
+            output_column='position',
+        )
+        self.gaze[0].merge_component_columns_into_tuple_column(
+            input_columns=exploded_columns['velocity'],
+            output_column='velocity',
+        )
 
         disable_progressbar = not verbose
 
@@ -431,6 +465,10 @@ class Dataset:
         for file_id, (gaze_df, fileinfo_row) in tqdm(
                 enumerate(zip(self.gaze, self.fileinfo.to_dicts())), disable=disable_progressbar,
         ):
+            # this is just a work-around until merged columns are standard behavior
+            # https://github.com/aeye-lab/pymovements/pull/443
+            gaze_df.explode('position', exploded_columns['position'])
+            gaze_df.explode('velocity', exploded_columns['velocity'])
 
             positions = gaze_df.frame.select(position_columns).to_numpy()
             velocities = gaze_df.frame.select(velocity_columns).to_numpy()
@@ -453,6 +491,18 @@ class Dataset:
                 [self.events[file_id].frame, new_event_df.frame],
                 how='diagonal',
             )
+
+            # this is just a work-around until merged columns are standard behavior
+            # https://github.com/aeye-lab/pymovements/pull/443
+            gaze_df.merge_component_columns_into_tuple_column(
+                input_columns=exploded_columns['position'],
+                output_column='position',
+            )
+            gaze_df.merge_component_columns_into_tuple_column(
+                input_columns=exploded_columns['velocity'],
+                output_column='velocity',
+            )
+
         return self
 
     def detect(
diff --git a/src/pymovements/dataset/dataset_definition.py b/src/pymovements/dataset/dataset_definition.py
index b5da62e..a7017a8 100644
--- a/src/pymovements/dataset/dataset_definition.py
+++ b/src/pymovements/dataset/dataset_definition.py
@@ -62,6 +62,7 @@ class DatasetDefinition:
     custom_read_kwargs : dict[str, Any], optional
         If specified, these keyword arguments will be passed to the file reading function.
     """
+    # pylint: disable=too-many-instance-attributes
     name: str = '.'
 
     mirrors: tuple[str, ...] = field(default_factory=tuple)
@@ -78,6 +79,12 @@ class DatasetDefinition:
 
     column_map: dict[str, str] = field(default_factory=dict)
 
+    time_column: str | 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
+
     def __post_init__(self) -> None:
         if len(self.column_map) > 0:
             self.custom_read_kwargs['columns'] = list(self.column_map.keys())
diff --git a/src/pymovements/dataset/dataset_files.py b/src/pymovements/dataset/dataset_files.py
index 245773f..285ea56 100644
--- a/src/pymovements/dataset/dataset_files.py
+++ b/src/pymovements/dataset/dataset_files.py
@@ -64,7 +64,7 @@ def scan_dataset(definition: DatasetDefinition, paths: DatasetPaths) -> pl.DataF
         relative=True,
     )
 
-    if len(fileinfo_dicts) == 0:
+    if not fileinfo_dicts:
         print(definition.filename_format)
         print(curly_to_regex(definition.filename_format))
         raise RuntimeError(f'no matching files found in {paths.raw}')
@@ -207,20 +207,79 @@ def load_gaze_files(
                 extension=extension,
             )
 
-        gaze_df = load_gaze_file(
+        gaze_data = load_gaze_file(
             filepath=filepath,
             preprocessed=preprocessed,
             custom_read_kwargs=definition.custom_read_kwargs,
         )
 
         # Add fileinfo columns to dataframe.
-        gaze_df = add_fileinfo(
+        gaze_data = add_fileinfo(
             definition=definition,
-            df=gaze_df,
+            df=gaze_data,
             fileinfo=fileinfo_row,
         )
 
-        gaze_dfs.append(GazeDataFrame(gaze_df, experiment=definition.experiment))
+        if preprocessed and extension == 'feather':
+            # Preprocessed data already has tuple columns.
+            gaze_df = GazeDataFrame(
+                gaze_data,
+                experiment=definition.experiment,
+            )
+
+        elif preprocessed and extension == 'csv':
+            # Workaround for preprocessed column names
+            # https://github.com/aeye-lab/pymovements/pull/443
+            time_column = None
+            if 'time' in gaze_data.columns:
+                time_column = 'time'
+
+            pixel_columns: list[str] = list(
+                set(GazeDataFrame.valid_pixel_position_columns) & set(gaze_data.columns),
+            )
+            if not pixel_columns:
+                pixel_columns = None  # type: ignore
+
+            position_columns: list[str] = list(
+                set(GazeDataFrame.valid_position_columns) & set(gaze_data.columns),
+            )
+            if not position_columns:
+                position_columns = None  # type: ignore
+
+            velocity_columns: list[str] = list(
+                set(GazeDataFrame.valid_velocity_columns) & set(gaze_data.columns),
+            )
+            if not velocity_columns:
+                velocity_columns = None  # type: ignore
+
+            acceleration_columns: list[str] = list(
+                set(GazeDataFrame.valid_acceleration_columns) & set(gaze_data.columns),
+            )
+            if not acceleration_columns:
+                acceleration_columns = None  # type: ignore
+
+            gaze_df = GazeDataFrame(
+                gaze_data,
+                experiment=definition.experiment,
+                time_column=time_column,
+                pixel_columns=pixel_columns,
+                position_columns=position_columns,
+                velocity_columns=velocity_columns,
+                acceleration_columns=acceleration_columns,
+            )
+        else:
+            # Create GazeDataFrame
+            gaze_df = GazeDataFrame(
+                gaze_data,
+                experiment=definition.experiment,
+                time_column=definition.time_column,
+                pixel_columns=definition.pixel_columns,
+                position_columns=definition.position_columns,
+                velocity_columns=definition.velocity_columns,
+                acceleration_columns=definition.acceleration_columns,
+            )
+
+        gaze_dfs.append(gaze_df)
 
     return gaze_dfs
 
@@ -424,6 +483,46 @@ def save_preprocessed(
             extension=extension,
         )
 
+        if extension == 'csv':
+            # this is just a work-around until merged columns are standard behavior
+            # https://github.com/aeye-lab/pymovements/pull/443
+            exploded_columns = {}
+            if 'pixel' in gaze_df.frame.columns:
+                exploded_columns_pix = [
+                    'x_left_pix', 'y_left_pix',
+                    'x_right_pix', 'y_right_pix',
+                    'x_avg_pix', 'y_avg_pix',
+                ][:gaze_df.n_components]
+                gaze_df.explode('pixel', exploded_columns_pix)
+                exploded_columns['pixel'] = exploded_columns_pix
+
+            if 'position' in gaze_df.frame.columns:
+                exploded_columns_pos = [
+                    'x_left_pos', 'y_left_pos',
+                    'x_right_pos', 'y_right_pos',
+                    'x_avg_pos', 'y_avg_pos',
+                ][:gaze_df.n_components]
+                gaze_df.explode('position', exploded_columns_pos)
+                exploded_columns['position'] = exploded_columns_pos
+
+            if 'velocity' in gaze_df.frame.columns:
+                exploded_columns_vel = [
+                    'x_left_vel', 'y_left_vel',
+                    'x_right_vel', 'y_right_vel',
+                    'x_avg_vel', 'y_avg_vel',
+                ][:gaze_df.n_components]
+                gaze_df.explode('velocity', exploded_columns_vel)
+                exploded_columns['velocity'] = exploded_columns_vel
+
+            if 'acceleration' in gaze_df.frame.columns:
+                exploded_columns_vel = [
+                    'x_left_acc', 'y_left_acc',
+                    'x_right_acc', 'y_right_acc',
+                    'x_avg_acc', 'y_avg_acc',
+                ][:gaze_df.n_components]
+                gaze_df.explode('acceleration', exploded_columns_vel)
+                exploded_columns['acceleration'] = exploded_columns_vel
+
         gaze_df_out = gaze_df.frame.clone()
         for column in gaze_df.columns:
             if column in fileinfo.columns:
@@ -438,7 +537,7 @@ def save_preprocessed(
         elif extension == 'csv':
             gaze_df_out.write_csv(preprocessed_filepath)
         else:
-            valid_extensions = ['csv', 'feather', 'asc']
+            valid_extensions = ['csv', 'feather']
             raise ValueError(
                 f'unsupported file format "{extension}".'
                 f'Supported formats are: {valid_extensions}',
diff --git a/src/pymovements/datasets/gazebase.py b/src/pymovements/datasets/gazebase.py
index 92e5839..d61360c 100644
--- a/src/pymovements/datasets/gazebase.py
+++ b/src/pymovements/datasets/gazebase.py
@@ -137,11 +137,12 @@ class GazeBase(DatasetDefinition):
         },
     )
 
+    time_column: str = 'n'
+
+    position_columns: list[str] = field(default_factory=lambda: ['x', 'y'])
+
     column_map: dict[str, str] = field(
         default_factory=lambda: {
-            'n': 'time',
-            'x': 'x_left_pos',
-            'y': 'y_left_pos',
             'val': 'validity',
             'xT': 'x_target_pos',
             'yT': 'y_target_pos',
diff --git a/src/pymovements/datasets/gazebasevr.py b/src/pymovements/datasets/gazebasevr.py
index a25b999..888f2e2 100644
--- a/src/pymovements/datasets/gazebasevr.py
+++ b/src/pymovements/datasets/gazebasevr.py
@@ -138,15 +138,12 @@ class GazeBaseVR(DatasetDefinition):
         },
     )
 
+    time_column: str = 'n'
+
+    position_columns: list[str] = field(default_factory=lambda: ['lx', 'ly', 'rx', 'ry', 'x', 'y'])
+
     column_map: dict[str, str] = field(
         default_factory=lambda: {
-            'n': 'time',
-            'x': 'x_pos',
-            'y': 'y_pos',
-            'lx': 'x_left_pos',
-            'ly': 'y_left_pos',
-            'rx': 'x_right_pos',
-            'ry': 'y_right_pos',
             'xT': 'x_target_pos',
             'yT': 'y_target_pos',
         },
diff --git a/src/pymovements/datasets/judo1000.py b/src/pymovements/datasets/judo1000.py
index 0f002b9..b0ecc53 100644
--- a/src/pymovements/datasets/judo1000.py
+++ b/src/pymovements/datasets/judo1000.py
@@ -125,15 +125,17 @@ class JuDo1000(DatasetDefinition):
         },
     )
 
+    time_column: str = 'time'
+    pixel_columns: list[str] = field(
+        default_factory=lambda: [
+            'x_left', 'y_left', 'x_right', 'y_right',
+        ],
+    )
+
     column_map: dict[str, str] = field(
         default_factory=lambda: {
             'trialId': 'trial_id',
             'pointId': 'point_id',
-            'time': 'time',
-            'x_left': 'x_left_pix',
-            'y_left': 'y_left_pix',
-            'x_right': 'x_right_pix',
-            'y_right': 'y_right_pix',
         },
     )
 
diff --git a/src/pymovements/datasets/toy_dataset.py b/src/pymovements/datasets/toy_dataset.py
index 4a57bb4..480bb71 100644
--- a/src/pymovements/datasets/toy_dataset.py
+++ b/src/pymovements/datasets/toy_dataset.py
@@ -123,13 +123,11 @@ class ToyDataset(DatasetDefinition):
         },
     )
 
-    column_map: dict[str, str] = field(
-        default_factory=lambda: {
-            'timestamp': 'time',
-            'x': 'x_right_pix',
-            'y': 'y_right_pix',
-        },
-    )
+    time_column: str = 'timestamp'
+
+    pixel_columns: list[str] = field(default_factory=lambda: ['x', 'y'])
+
+    column_map: dict[str, str] = field(default_factory=lambda: {})
 
     custom_read_kwargs: dict[str, str] = field(
         default_factory=lambda: {
diff --git a/src/pymovements/datasets/toy_dataset_eyelink.py b/src/pymovements/datasets/toy_dataset_eyelink.py
index 4868cca..fb1b162 100644
--- a/src/pymovements/datasets/toy_dataset_eyelink.py
+++ b/src/pymovements/datasets/toy_dataset_eyelink.py
@@ -128,6 +128,10 @@ class ToyDatasetEyeLink(DatasetDefinition):
 
     column_map: dict[str, str] = field(default_factory=lambda: {})
 
+    time_column: str = 'time'
+
+    pixel_columns: list[str] = field(default_factory=lambda: ['x_pix', 'y_pix'])
+
     custom_read_kwargs: dict[str, Any] = field(
         default_factory=lambda: {
             'patterns': [
diff --git a/src/pymovements/events/event_processing.py b/src/pymovements/events/event_processing.py
index 50f0f4e..ff35c7b 100644
--- a/src/pymovements/events/event_processing.py
+++ b/src/pymovements/events/event_processing.py
@@ -191,24 +191,6 @@ class EventGazeProcessor:
             property_kwargs for _, property_kwargs in self.event_properties
         ]
 
-        # We need to create a new column here, which is a list of position tuples.
-        # For the intermediate time before the tuple will be the default format for
-        # positions, we create this column here and drop this column afterwards.
-        position_columns = tuple(gaze.position_columns[:2])
-        if position_columns:
-            position_component_expressions = [pl.col(component) for component in position_columns]
-            gaze.frame = gaze.frame.with_columns(
-                pl.concat_list(position_component_expressions)
-                .alias('position'),
-            )
-        velocity_columns = tuple(gaze.velocity_columns[:2])
-        if velocity_columns:
-            velocity_component_expressions = [pl.col(component) for component in velocity_columns]
-            gaze.frame = gaze.frame.with_columns(
-                pl.concat_list(velocity_component_expressions)
-                .alias('velocity'),
-            )
-
         for property_id, property_expression in enumerate(property_expressions):
             property_args = inspect.getfullargspec(property_expression).kwonlyargs
 
@@ -242,11 +224,4 @@ class EventGazeProcessor:
                 ],
             )
         )
-
-        # If we created the position and velocity tuple columns, we drop it again.
-        if 'position' in gaze.frame.columns:
-            gaze.frame.drop_in_place('position')
-        if 'velocity' in gaze.frame.columns:
-            gaze.frame.drop_in_place('velocity')
-
         return result
diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py
index ce9b66e..910c4e5 100644
--- a/src/pymovements/gaze/gaze_dataframe.py
+++ b/src/pymovements/gaze/gaze_dataframe.py
@@ -33,28 +33,40 @@ class GazeDataFrame:
     Each column is a channel in the gaze time series.
     """
 
-    _valid_pixel_position_columns = [
+    valid_pixel_position_columns = [
         'x_pix', 'y_pix',
         'x_left_pix', 'y_left_pix',
         'x_right_pix', 'y_right_pix',
+        '__x_pix__', '__y_pix__',
+        '__x_left_pix__', '__y_left_pix__',
+        '__x_right_pix__', '__y_right_pix__',
     ]
 
-    _valid_position_columns = [
+    valid_position_columns = [
         'x_pos', 'y_pos',
         'x_left_pos', 'y_left_pos',
         'x_right_pos', 'y_right_pos',
+        '__x_pos__', '__y_pos__',
+        '__x_left_pos__', '__y_left_pos__',
+        '__x_right_pos__', '__y_right_pos__',
     ]
 
-    _valid_velocity_columns = [
+    valid_velocity_columns = [
         'x_vel', 'y_vel',
         'x_left_vel', 'y_left_vel',
         'x_right_vel', 'y_right_vel',
+        '__x_vel__', '__y_vel__',
+        '__x_left_vel__', '__y_left_vel__',
+        '__x_right_vel__', '__y_right_vel__',
     ]
 
-    _valid_acceleration_columns = [
+    valid_acceleration_columns = [
         'x_acc', 'y_acc',
         'x_left_acc', 'y_left_acc',
         'x_right_acc', 'y_right_acc',
+        '__x_acc__', '__y_acc__',
+        '__x_left_acc__', '__y_left_acc__',
+        '__x_right_acc__', '__y_right_acc__',
     ]
 
     def __init__(
@@ -62,6 +74,7 @@ class GazeDataFrame:
             data: pl.DataFrame | None = None,
             experiment: Experiment | None = None,
             *,
+            time_column: str | None = None,
             pixel_columns: list[str] | None = None,
             position_columns: list[str] | None = None,
             velocity_columns: list[str] | None = None,
@@ -75,6 +88,8 @@ class GazeDataFrame:
             A dataframe to be transformed to a polars dataframe.
         experiment : Experiment
             The experiment definition.
+        time_column:
+            The name if the timestamp column in the input data frame.
         pixel_columns:
             The name of the pixel position columns in the input data frame.
         position_columns:
@@ -147,6 +162,10 @@ class GazeDataFrame:
             data = data.clone()
         self.frame = data
 
+        if time_column is not None:
+            self.frame = self.frame.rename({time_column: 'time'})
+
+        n_components = None
         if pixel_columns is not None:
             _check_component_columns(
                 frame=self.frame,
@@ -156,6 +175,7 @@ class GazeDataFrame:
                 input_columns=pixel_columns,
                 output_column='pixel',
             )
+            n_components = len(pixel_columns)
 
         if position_columns is not None:
             _check_component_columns(
@@ -167,6 +187,7 @@ class GazeDataFrame:
                 input_columns=position_columns,
                 output_column='position',
             )
+            n_components = len(position_columns)
 
         if velocity_columns is not None:
             _check_component_columns(
@@ -178,6 +199,7 @@ class GazeDataFrame:
                 input_columns=velocity_columns,
                 output_column='velocity',
             )
+            n_components = len(velocity_columns)
 
         if acceleration_columns is not None:
             _check_component_columns(
@@ -189,7 +211,9 @@ class GazeDataFrame:
                 input_columns=acceleration_columns,
                 output_column='acceleration',
             )
+            n_components = len(acceleration_columns)
 
+        self.n_components = n_components
         self.experiment = experiment
 
     def pix2deg(self) -> None:
@@ -209,26 +233,41 @@ class GazeDataFrame:
         # mypy does not get that experiment now cannot be None anymore
         assert self.experiment is not None
 
-        pix_position_columns = self.pixel_position_columns
-        if not pix_position_columns:
-            raise AttributeError(
-                'No valid pixel position columns found.'
-                f' Valid pixel position columns are: {self._valid_pixel_position_columns}.'
-                f' Available columns are: {self.frame.columns}.',
+        # this is just a work-around until GazeDataFrame.transform() is implemented
+        if 'pixel' in self.frame.columns:
+            pixel_columns = [
+                '__x_left_pix__', '__y_left_pix__',
+                '__x_right_pix__', '__y_right_pix__',
+                '__x_avg_pix__', '__y_avg_pix__',
+            ][:self.n_components]
+            self.explode('pixel', pixel_columns)
+        else:
+            raise pl.exceptions.ColumnNotFoundError(
+                f'Column \'pixel\' not found. Available columns are: {self.frame.columns}',
             )
 
-        dva_position_columns = self._pixel_to_dva_position_columns(pix_position_columns)
-
-        pixel_positions = self.frame.select(pix_position_columns)
+        pixel_positions = self.frame.select(pixel_columns)
         dva_positions = self.experiment.screen.pix2deg(pixel_positions.to_numpy())
 
+        dva_columns = self._pixel_to_dva_position_columns(pixel_columns)
         self.frame = self.frame.with_columns(
             [
                 pl.Series(name=dva_column_name, values=dva_positions[:, dva_column_id])
-                for dva_column_id, dva_column_name in enumerate(dva_position_columns)
+                for dva_column_id, dva_column_name in enumerate(dva_columns)
             ],
         )
 
+        self.merge_component_columns_into_tuple_column(
+            input_columns=dva_columns,
+            output_column='position',
+        )
+
+        # this is just a work-around until merged columns are standard behavior
+        self.merge_component_columns_into_tuple_column(
+            input_columns=pixel_columns,
+            output_column='pixel',
+        )
+
     def pos2acc(
             self,
             window_length: int = 7,
@@ -263,17 +302,20 @@ class GazeDataFrame:
         # mypy does not get that experiment now cannot be None anymore
         assert self.experiment is not None
 
-        position_columns = self.position_columns
-        if not position_columns:
-            raise AttributeError(
-                'No valid position columns found.'
-                f' Valid position columns are: {self._valid_position_columns}.'
-                f' Available columns are: {self.frame.columns}.',
+        # this is just a work-around until merged columns are standard behavior
+        if 'position' in self.frame.columns:
+            position_columns = [
+                '__x_left_pos__', '__y_left_pos__',
+                '__x_right_pos__', '__y_right_pos__',
+                '__x_avg_pos__', '__y_avg_pos__',
+            ][:self.n_components]
+            self.explode('position', position_columns)
+        else:
+            raise pl.exceptions.ColumnNotFoundError(
+                f'Column \'position\' not found. Available columns are: {self.frame.columns}',
             )
-        acceleration_columns = self._position_to_acceleration_columns(position_columns)
 
         positions = self.frame.select(position_columns)
-
         acceleration = pos2acc(
             positions.to_numpy(),
             sampling_rate=self.experiment.sampling_rate,
@@ -283,6 +325,7 @@ class GazeDataFrame:
             cval=cval,
         )
 
+        acceleration_columns = self._position_to_acceleration_columns(position_columns)
         self.frame = self.frame.with_columns(
             [
                 pl.Series(name=velocity_column_name, values=acceleration[:, column_id])
@@ -290,6 +333,17 @@ class GazeDataFrame:
             ],
         )
 
+        self.merge_component_columns_into_tuple_column(
+            input_columns=acceleration_columns,
+            output_column='acceleration',
+        )
+
+        # this is just a work-around until merged columns are standard behavior
+        self.merge_component_columns_into_tuple_column(
+            input_columns=position_columns,
+            output_column='position',
+        )
+
     def pos2vel(self, method: str = 'smooth', **kwargs: int | float | str) -> None:
         """Compute gaze velocity in dva/s from dva position coordinates.
 
@@ -314,19 +368,23 @@ class GazeDataFrame:
         # mypy does not get that experiment now cannot be None anymore
         assert self.experiment is not None
 
-        position_columns = self.position_columns
-        if not position_columns:
-            raise AttributeError(
-                'No valid position columns found.'
-                f' Valid position columns are: {self._valid_position_columns}.'
-                f' Available columns are: {self.frame.columns}.',
+        # this is just a work-around until merged columns are standard behavior
+        if 'position' in self.frame.columns:
+            position_columns = [
+                '__x_left_pos__', '__y_left_pos__',
+                '__x_right_pos__', '__y_right_pos__',
+                '__x_avg_pos__', '__y_avg_pos__',
+            ][:self.n_components]
+            self.explode('position', position_columns)
+        else:
+            raise pl.exceptions.ColumnNotFoundError(
+                f'Column \'position\' not found. Available columns are: {self.frame.columns}',
             )
-        velocity_columns = self._position_to_velocity_columns(position_columns)
 
         positions = self.frame.select(position_columns)
-
         velocities = self.experiment.pos2vel(positions.to_numpy(), method=method, **kwargs)
 
+        velocity_columns = self._position_to_velocity_columns(position_columns)
         self.frame = self.frame.with_columns(
             [
                 pl.Series(name=velocity_column_name, values=velocities[:, column_id])
@@ -334,6 +392,17 @@ class GazeDataFrame:
             ],
         )
 
+        self.merge_component_columns_into_tuple_column(
+            input_columns=velocity_columns,
+            output_column='velocity',
+        )
+
+        # this is just a work-around until merged columns are standard behavior
+        self.merge_component_columns_into_tuple_column(
+            input_columns=position_columns,
+            output_column='position',
+        )
+
     @property
     def schema(self) -> pl.type_aliases.SchemaDict:
         """Schema of event dataframe."""
@@ -344,30 +413,6 @@ class GazeDataFrame:
         """List of column names."""
         return self.frame.columns
 
-    @property
-    def acceleration_columns(self) -> list[str]:
-        """Acceleration columns (in degrees of visual angle per second^2) of dataframe."""
-        acceleration_columns = list(set(self._valid_acceleration_columns) & set(self.frame.columns))
-        return acceleration_columns
-
-    @property
-    def velocity_columns(self) -> list[str]:
-        """Velocity columns (in degrees of visual angle per second) of dataframe."""
-        velocity_columns = list(set(self._valid_velocity_columns) & set(self.frame.columns))
-        return velocity_columns
-
-    @property
-    def pixel_position_columns(self) -> list[str]:
-        """Pixel position columns for this dataset."""
-        pixel_position_columns = set(self._valid_pixel_position_columns) & set(self.frame.columns)
-        return list(pixel_position_columns)
-
-    @property
-    def position_columns(self) -> list[str]:
-        """Position columns (in degrees of visual angle) for this dataset."""
-        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],
@@ -416,33 +461,30 @@ class GazeDataFrame:
     def _pixel_to_dva_position_columns(columns: list[str]) -> list[str]:
         """Get corresponding dva position columns from pixel position columns."""
         return [
-            column.replace('_pix', '_pos')
-            for column in columns
-            if column.endswith('_pix')
+            column.replace('pixel', 'position').replace('pix', 'pos')
+            for column in columns if 'pix' in column
         ]
 
     @staticmethod
     def _position_to_acceleration_columns(columns: list[str]) -> list[str]:
         """Get corresponding acceleration columns from dva position columns."""
         return [
-            column.replace('_pos', '_acc')
-            for column in columns
-            if column.endswith('_pos')
+            column.replace('position', 'acceleration').replace('pos', 'acc')
+            for column in columns if 'pos' in column
         ]
 
     @staticmethod
     def _position_to_velocity_columns(columns: list[str]) -> list[str]:
         """Get corresponding velocity columns from dva position columns."""
         return [
-            column.replace('_pos', '_vel')
-            for column in columns
-            if column.endswith('_pos')
+            column.replace('position', 'velocity').replace('pos', 'vel')
+            for column in columns if 'pos' in column
         ]
 
     def _check_experiment(self) -> None:
         """Check if experiment attribute has been set."""
         if self.experiment is None:
-            raise AttributeError('experiment must be specified for this method to work.')
+            raise AttributeError('experiment must be specified for this method to work')
 
 
 def _check_component_columns(
diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py
index bef72cc..2650eb2 100644
--- a/src/pymovements/gaze/transforms.py
+++ b/src/pymovements/gaze/transforms.py
@@ -121,9 +121,9 @@ def pix2deg(
             'Number of dimensions of arr must be either 0, 1 or 2'
             f' (arr.ndim: {arr.ndim})',
         )
-    if arr.ndim == 2 and arr.shape[-1] not in [1, 2, 4]:
+    if arr.ndim == 2 and arr.shape[-1] not in [1, 2, 4, 6]:
         raise ValueError(
-            'Last coord dimension must have length 1, 2 or 4.'
+            'Last coord dimension must have length 1, 2, 4 or 6.'
             f' (arr.shape: {arr.shape})',
         )
 
@@ -148,6 +148,16 @@ def pix2deg(
         screen_px = np.tile(screen_px, 2)
         screen_cm = np.tile(screen_cm, 2)
 
+    if arr.ndim != 0 and arr.shape[-1] == 6:
+        if screen_px.shape != (2,):
+            raise ValueError('arr is 6-dimensional, but screen_px is not 2-dimensional')
+        if screen_cm.shape != (2,):
+            raise ValueError('arr is 6-dimensional, but screen_cm is not 2-dimensional')
+
+        # We have binocular and cyclopian data. Triple tile screen parameters.
+        screen_px = np.tile(screen_px, 3)
+        screen_cm = np.tile(screen_cm, 3)
+
     # Compute eye-to-screen-distance in pixels.
     distance_px = distance_cm * (screen_px / screen_cm)
 
diff --git a/src/pymovements/plotting/heatmap.py b/src/pymovements/plotting/heatmap.py
index ddc58fe..173aa14 100644
--- a/src/pymovements/plotting/heatmap.py
+++ b/src/pymovements/plotting/heatmap.py
@@ -31,7 +31,7 @@ from pymovements.gaze import GazeDataFrame
 
 def heatmap(
         gaze: GazeDataFrame,
-        position_columns: tuple[str, str] = ('x_pix', 'y_pix'),
+        position_column: str = 'pixel',
         gridsize: tuple[int, int] = (10, 10),
         cmap: colors.Colormap | str = 'jet',
         interpolation: str = 'gaussian',
@@ -55,8 +55,8 @@ def heatmap(
     ----------
     gaze : GazeDataFrame
         A GazeDataFrame object.
-    position_columns : tuple[str, str], optional
-        The column names of the x and y position data
+    position_column : str, optional
+        The column name of the x and y position data
     gridsize : tuple[int, int], optional
         The number of bins in the x and y dimensions.
     cmap : colors.Colormap | str, optional
@@ -97,8 +97,8 @@ def heatmap(
     """
 
     # Extract x and y positions from the gaze dataframe
-    x = gaze.frame[position_columns[0]]
-    y = gaze.frame[position_columns[1]]
+    x = gaze.frame[position_column].list.get(0).to_numpy()
+    y = gaze.frame[position_column].list.get(1).to_numpy()
 
     # Check if experiment properties are available
     if not gaze.experiment:
@@ -111,12 +111,15 @@ def heatmap(
     screen = gaze.experiment.screen
 
     # Use screen properties to define the grid or degrees of visual angle
-    if 'pix' in position_columns[0] and 'pix' in position_columns[1]:
+    if position_column == 'pixel':
         xmin, xmax = 0, screen.width_px
         ymin, ymax = 0, screen.height_px
-    else:
+    elif position_column == 'position':
         xmin, xmax = int(screen.x_min_dva), int(screen.x_max_dva)
         ymin, ymax = int(screen.y_min_dva), int(screen.y_max_dva)
+    else:
+        xmin, xmax = int(x.min()), int(x.max())
+        ymin, ymax = int(y.min()), int(y.max())
 
     # Define the grid and bin the gaze data
     x_bins = np.linspace(xmin, xmax, num=gridsize[0]).astype(int)
diff --git a/src/pymovements/plotting/traceplot.py b/src/pymovements/plotting/traceplot.py
index 7c67dc0..a3fcf8c 100644
--- a/src/pymovements/plotting/traceplot.py
+++ b/src/pymovements/plotting/traceplot.py
@@ -82,8 +82,7 @@ DEFAULT_SEGMENTDATA_TWOSLOPE = {
 
 def traceplot(
         gaze: GazeDataFrame,
-        x: str,
-        y: str,
+        position_column: str = 'pixel',
         cval: np.ndarray | None = None,  # pragma: no cover
         cmap: colors.Colormap | None = None,
         cmap_norm: colors.Normalize | str | None = None,
@@ -104,10 +103,8 @@ def traceplot(
     ----------
     gaze: GazeDataFrame
         The GazeDataFrame to plot.
-    x: str
-        Column name of x-coordinates
-    y: str
-        Column name of y-coordinates
+    position_column : str, optional
+        The column name of the x and y position data
     cval: np.ndarray
         line color values.
     cmap: matplotlib.colors.Colormap, optional
@@ -139,8 +136,8 @@ def traceplot(
         If length of x and y coordinates do not match or if ``cmap_norm`` is unknown.
 
     """
-    x_signal = gaze.frame[x]
-    y_signal = gaze.frame[y]
+    x_signal = gaze.frame[position_column].list.get(0)
+    y_signal = gaze.frame[position_column].list.get(1)
 
     n = len(x_signal)
 
diff --git a/src/pymovements/plotting/tsplot.py b/src/pymovements/plotting/tsplot.py
index 31cb163..d0ce9de 100644
--- a/src/pymovements/plotting/tsplot.py
+++ b/src/pymovements/plotting/tsplot.py
@@ -26,6 +26,7 @@ import math
 
 import matplotlib.pyplot as plt
 import numpy as np
+import polars as pl
 
 from pymovements.gaze import GazeDataFrame
 
@@ -92,7 +93,7 @@ def tsplot(
         If array has more than two dimensions.
     """
     if channels is None:
-        channels = gaze.frame.columns
+        channels = [c for c in gaze.frame.columns if gaze.frame[c].dtype != pl.List]
 
     arr = gaze.frame[channels].to_numpy().transpose()
 


Test patch:
diff --git a/tests/dataset/dataset_test.py b/tests/dataset/dataset_test.py
index f51e6cb..57e7d08 100644
--- a/tests/dataset/dataset_test.py
+++ b/tests/dataset/dataset_test.py
@@ -20,7 +20,6 @@
 """Test all functionality in pymovements.dataset.dataset."""
 import os
 import shutil
-import unittest
 from pathlib import Path
 from unittest.mock import Mock
 
@@ -54,7 +53,7 @@ def create_preprocessed_gaze_files_from_fileinfo(gaze_dfs, fileinfo, rootpath):
 
         for key in fileinfo_row.keys():
             if key in gaze_df.columns:
-                gaze_df = gaze_df.drop(key)
+                gaze_df = gaze_df.frame.drop(key)
 
         gaze_df.write_ipc(rootpath / filepath)
 
@@ -105,6 +104,35 @@ def mock_toy(rootpath, raw_fileformat, eyes):
                     'y_right_pix': pl.Float64,
                 },
             )
+            pixel_columns = ['x_left_pix', 'y_left_pix', 'x_right_pix', 'y_right_pix']
+
+        elif eyes == 'both+avg':
+            gaze_df = pl.from_dict(
+                {
+                    'subject_id': fileinfo_row['subject_id'],
+                    'time': np.arange(1000),
+                    'x_left_pix': np.zeros(1000),
+                    'y_left_pix': np.zeros(1000),
+                    'x_right_pix': np.zeros(1000),
+                    'y_right_pix': np.zeros(1000),
+                    'x_avg_pix': np.zeros(1000),
+                    'y_avg_pix': np.zeros(1000),
+                },
+                schema={
+                    'subject_id': pl.Int64,
+                    'time': pl.Int64,
+                    'x_left_pix': pl.Float64,
+                    'y_left_pix': pl.Float64,
+                    'x_right_pix': pl.Float64,
+                    'y_right_pix': pl.Float64,
+                    'x_avg_pix': pl.Float64,
+                    'y_avg_pix': pl.Float64,
+                },
+            )
+            pixel_columns = [
+                'x_left_pix', 'y_left_pix', 'x_right_pix', 'y_right_pix', 'x_avg_pix', 'y_avg_pix',
+            ]
+
         elif eyes == 'left':
             gaze_df = pl.from_dict(
                 {
@@ -120,6 +148,7 @@ def mock_toy(rootpath, raw_fileformat, eyes):
                     'y_left_pix': pl.Float64,
                 },
             )
+            pixel_columns = ['x_left_pix', 'y_left_pix']
         elif eyes == 'right':
             gaze_df = pl.from_dict(
                 {
@@ -135,6 +164,7 @@ def mock_toy(rootpath, raw_fileformat, eyes):
                     'y_right_pix': pl.Float64,
                 },
             )
+            pixel_columns = ['x_right_pix', 'y_right_pix']
         elif eyes == 'none':
             gaze_df = pl.from_dict(
                 {
@@ -150,6 +180,7 @@ def mock_toy(rootpath, raw_fileformat, eyes):
                     'y_pix': pl.Float64,
                 },
             )
+            pixel_columns = ['x_pix', 'y_pix']
         else:
             raise ValueError(f'invalid value for eyes: {eyes}')
 
@@ -157,42 +188,42 @@ def mock_toy(rootpath, raw_fileformat, eyes):
 
     create_raw_gaze_files_from_fileinfo(gaze_dfs, fileinfo, rootpath / 'raw')
 
+    # Create GazeDataFrames for passing as ground truth
+    gaze_dfs = [
+        pm.GazeDataFrame(gaze_df, pixel_columns=pixel_columns)
+        for gaze_df in gaze_dfs
+    ]
+
     preprocessed_gaze_dfs = []
     for fileinfo_row in fileinfo.to_dicts():
-        gaze_df = pl.from_dict(
-            {
-                'subject_id': fileinfo_row['subject_id'],
-                'time': np.arange(1000),
-                'x_left_pix': np.zeros(1000),
-                'y_left_pix': np.zeros(1000),
-                'x_right_pix': np.zeros(1000),
-                'y_right_pix': np.zeros(1000),
-                'x_left_pos': np.zeros(1000),
-                'y_left_pos': np.zeros(1000),
-                'x_right_pos': np.zeros(1000),
-                'y_right_pos': np.zeros(1000),
-                'x_left_vel': np.zeros(1000),
-                'y_left_vel': np.zeros(1000),
-                'x_right_vel': np.zeros(1000),
-                'y_right_vel': np.zeros(1000),
-            },
-            schema={
-                'subject_id': pl.Int64,
-                'time': pl.Int64,
-                'x_left_pix': pl.Float64,
-                'y_left_pix': pl.Float64,
-                'x_right_pix': pl.Float64,
-                'y_right_pix': pl.Float64,
-                'x_left_pos': pl.Float64,
-                'y_left_pos': pl.Float64,
-                'x_right_pos': pl.Float64,
-                'y_right_pos': pl.Float64,
-                'x_left_vel': pl.Float64,
-                'y_left_vel': pl.Float64,
-                'x_right_vel': pl.Float64,
-                'y_right_vel': pl.Float64,
-            },
+        position_columns = [pixel_column.replace('pix', 'pos') for pixel_column in pixel_columns]
+        velocity_columns = [pixel_column.replace('pix', 'vel') for pixel_column in pixel_columns]
+        acceleration_columns = [
+            pixel_column.replace('pix', 'acc') for pixel_column in pixel_columns
+        ]
+
+        gaze_data = {
+            'subject_id': fileinfo_row['subject_id'],
+            'time': np.arange(1000),
+        }
+        gaze_schema = {
+            'subject_id': pl.Int64,
+            'time': pl.Int64,
+        }
+
+        for column in pixel_columns + position_columns + velocity_columns + acceleration_columns:
+            gaze_data[column] = np.zeros(1000)
+            gaze_schema[column] = pl.Float64
+
+        # Create GazeDataFrames for passing as ground truth
+        gaze_df = pm.GazeDataFrame(
+            pl.from_dict(gaze_data, schema=gaze_schema),
+            pixel_columns=pixel_columns,
+            position_columns=position_columns,
+            velocity_columns=velocity_columns,
+            acceleration_columns=acceleration_columns,
         )
+
         preprocessed_gaze_dfs.append(gaze_df)
 
     create_preprocessed_gaze_files_from_fileinfo(
@@ -233,6 +264,8 @@ def mock_toy(rootpath, raw_fileformat, eyes):
         ),
         filename_format=r'{subject_id:d}.' + raw_fileformat,
         filename_format_dtypes={'subject_id': pl.Int64},
+        time_column='time',
+        pixel_columns=pixel_columns,
     )
 
     return {
@@ -248,12 +281,17 @@ def mock_toy(rootpath, raw_fileformat, eyes):
     }
 
 
-@pytest.fixture(name='dataset_configuration', params=['ToyMono', 'ToyBino', 'ToyLeft', 'ToyRight'])
+@pytest.fixture(
+    name='dataset_configuration',
+    params=['ToyMono', 'ToyBino', 'ToyLeft', 'ToyRight', 'ToyBino+Avg'],
+)
 def fixture_dataset(request, tmp_path):
     rootpath = tmp_path
 
     if request.param == 'ToyBino':
         dataset_dict = mock_toy(rootpath, raw_fileformat='csv', eyes='both')
+    elif request.param == 'ToyBino+Avg':
+        dataset_dict = mock_toy(rootpath, raw_fileformat='csv', eyes='both+avg')
     elif request.param == 'ToyMono':
         dataset_dict = mock_toy(rootpath, raw_fileformat='csv', eyes='none')
     elif request.param == 'ToyLeft':
@@ -282,7 +320,7 @@ def test_load_correct_raw_gaze_dfs(dataset_configuration):
 
     expected_gaze_dfs = dataset_configuration['raw_gaze_dfs']
     for result_gaze_df, expected_gaze_df in zip(dataset.gaze, expected_gaze_dfs):
-        assert_frame_equal(result_gaze_df.frame, expected_gaze_df)
+        assert_frame_equal(result_gaze_df.frame, expected_gaze_df.frame)
 
 
 def test_load_gaze_has_position_columns(dataset_configuration):
@@ -290,7 +328,7 @@ def test_load_gaze_has_position_columns(dataset_configuration):
     dataset.load(preprocessed=True)
 
     for result_gaze_df in dataset.gaze:
-        assert result_gaze_df.position_columns
+        assert 'position' in result_gaze_df.columns
 
 
 def test_load_correct_preprocessed_gaze_dfs(dataset_configuration):
@@ -299,7 +337,7 @@ def test_load_correct_preprocessed_gaze_dfs(dataset_configuration):
 
     expected_gaze_dfs = dataset_configuration['preprocessed_gaze_dfs']
     for result_gaze_df, expected_gaze_df in zip(dataset.gaze, expected_gaze_dfs):
-        assert_frame_equal(result_gaze_df.frame, expected_gaze_df)
+        assert_frame_equal(result_gaze_df.frame, expected_gaze_df.frame)
 
 
 def test_load_correct_event_dfs(dataset_configuration):
@@ -397,6 +435,7 @@ def test_save_gaze_exceptions(init_kwargs, save_kwargs, exception, dataset_confi
         dataset.load()
         dataset.pix2deg()
         dataset.pos2vel()
+        dataset.pos2acc()
         dataset.save_preprocessed(**save_kwargs)
 
 
@@ -488,13 +527,7 @@ def test_pix2deg(dataset_configuration):
 
     dataset.pix2deg()
 
-    dva_schema = {}
-    for column_name in original_schema.keys():
-        if column_name.endswith('_pix'):
-            dva_column_name = column_name.replace('_pix', '_pos')
-            dva_schema[dva_column_name] = original_schema[column_name]
-    expected_schema = {**original_schema, **dva_schema}
-
+    expected_schema = {**original_schema, 'position': pl.List(pl.Float64)}
     for result_gaze_df in dataset.gaze:
         print(result_gaze_df.schema)
         print(expected_schema)
@@ -510,13 +543,7 @@ def test_pos2acc(dataset_configuration):
 
     dataset.pos2acc()
 
-    acc_schema = {}
-    for column_name in original_schema.keys():
-        if column_name.endswith('_pix'):
-            acc_column_name = column_name.replace('_pix', '_acc')
-            acc_schema[acc_column_name] = original_schema[column_name]
-    expected_schema = {**original_schema, **acc_schema}
-
+    expected_schema = {**original_schema, 'acceleration': pl.List(pl.Float64)}
     for result_gaze_df in dataset.gaze:
         assert result_gaze_df.schema == expected_schema
 
@@ -530,13 +557,7 @@ def test_pos2vel(dataset_configuration):
 
     dataset.pos2vel()
 
-    vel_schema = {}
-    for column_name in original_schema.keys():
-        if column_name.endswith('_pix'):
-            vel_column_name = column_name.replace('_pix', '_vel')
-            vel_schema[vel_column_name] = original_schema[column_name]
-    expected_schema = {**original_schema, **vel_schema}
-
+    expected_schema = {**original_schema, 'velocity': pl.List(pl.Float64)}
     for result_gaze_df in dataset.gaze:
         assert result_gaze_df.schema == expected_schema
 
@@ -602,14 +623,6 @@ def test_detect_events_auto_eye(detect_event_kwargs, dataset_configuration):
             },
             id='right',
         ),
-        pytest.param(
-            {
-                'method': pm.events.microsaccades,
-                'threshold': 1,
-                'eye': 'eye',
-            },
-            id='eye',
-        ),
     ],
 )
 def test_detect_events_explicit_eye(detect_event_kwargs, dataset_configuration):
@@ -621,9 +634,8 @@ def test_detect_events_explicit_eye(detect_event_kwargs, dataset_configuration):
     dataset_eyes = dataset_configuration['eyes']
 
     exception = None
-    if dataset_eyes != detect_event_kwargs['eye']:
-        if dataset_eyes != 'both' or detect_event_kwargs['eye'] == 'eye':
-            exception = AttributeError
+    if not dataset_eyes.startswith('both') and detect_event_kwargs['eye'] is not None:
+        exception = AttributeError
 
     if exception is None:
         dataset.detect_events(**detect_event_kwargs)
@@ -726,35 +738,72 @@ def test_detect_events_alias(dataset_configuration, detect_kwargs, monkeypatch):
     mock.assert_called_with(**detect_kwargs)
 
 
+@pytest.mark.parametrize('dataset_configuration', ['ToyMono'], indirect=['dataset_configuration'])
 def test_detect_events_attribute_error(dataset_configuration):
     dataset = pm.Dataset(**dataset_configuration['init_kwargs'])
     dataset.load()
-
-    try:
-        dataset.gaze[0] = dataset.gaze[0].drop('x_left_pos')
-    except BaseException:
-        pass
-
-    try:
-        dataset.gaze[0] = dataset.gaze[0].drop('x_right_pos')
-    except BaseException:
-        pass
-
-    try:
-        dataset.gaze[0] = dataset.gaze[0].drop('x_eye_pos')
-    except BaseException:
-        pass
+    dataset.pix2deg()
+    dataset.pos2vel()
 
     detect_event_kwargs = {
         'method': pm.events.microsaccades,
         'threshold': 1,
-        'eye': 'auto',
+        'eye': 'right',
     }
 
     with pytest.raises(AttributeError):
         dataset.detect_events(**detect_event_kwargs)
 
 
+@pytest.mark.parametrize('dataset_configuration', ['ToyMono'], indirect=['dataset_configuration'])
+@pytest.mark.parametrize(
+    ('rename_arg', 'detect_event_kwargs', 'expected_message'),
+    [
+        pytest.param(
+            {'position': 'custom_position'},
+            {
+                'method': pm.events.microsaccades,
+                'threshold': 1,
+                'eye': 'right',
+            },
+            (
+                "Column 'position' not found. Available columns are: "
+                "['subject_id', 'time', 'pixel', 'velocity', 'custom_position']"
+            ),
+            id='no_position',
+        ),
+        pytest.param(
+            {'velocity': 'custom_velocity'},
+            {
+                'method': pm.events.microsaccades,
+                'threshold': 1,
+                'eye': 'right',
+            },
+            (
+                "Column 'velocity' not found. Available columns are: "
+                "['subject_id', 'time', 'pixel', 'custom_velocity', 'position']"
+            ),
+            id='no_velocity',
+        ),
+    ],
+)
+def test_detect_events_raises_column_not_found_error(
+        dataset_configuration, rename_arg, detect_event_kwargs, expected_message,
+):
+    dataset = pm.Dataset(**dataset_configuration['init_kwargs'])
+    dataset.load()
+    dataset.pix2deg()
+    dataset.pos2vel()
+
+    dataset.gaze[0].frame = dataset.gaze[0].frame.rename(rename_arg)
+
+    with pytest.raises(pl.exceptions.ColumnNotFoundError) as excinfo:
+        dataset.detect_events(**detect_event_kwargs)
+
+    msg, = excinfo.value.args
+    assert msg == expected_message
+
+
 @pytest.mark.parametrize(
     ('events_init', 'events_expected'),
     [
@@ -883,6 +932,7 @@ def test_load_previously_saved_events_gaze(
     dataset.load()
     dataset.pix2deg()
     dataset.pos2vel()
+    dataset.pos2acc()
     dataset.detect_events(**detect_event_kwargs)
 
     # We must not overwrite the original variable as it's needed in the end.
@@ -917,11 +967,14 @@ def test_load_previously_saved_events_gaze(
         ),
     ],
 )
-def test_save_preprocessed(preprocessed_dirname, expected_save_dirpath, dataset_configuration):
+def test_save_preprocessed_directory_exists(
+        preprocessed_dirname, expected_save_dirpath, dataset_configuration,
+):
     dataset = pm.Dataset(**dataset_configuration['init_kwargs'])
     dataset.load()
     dataset.pix2deg()
     dataset.pos2vel()
+    dataset.pos2acc()
 
     if preprocessed_dirname is None:
         preprocessed_dirname = 'preprocessed'
@@ -934,6 +987,31 @@ def test_save_preprocessed(preprocessed_dirname, expected_save_dirpath, dataset_
     )
 
 
+def test_save_preprocessed_no_tuple_columns(dataset_configuration):
+    dataset = pm.Dataset(**dataset_configuration['init_kwargs'])
+    dataset.load()
+    dataset.pix2deg()
+    dataset.pos2vel()
+    dataset.pos2acc()
+
+    # This is not implemented yet
+    # dataset.gaze[0].explode(['pixel', 'position', 'velocity', 'acceleration'])
+    dataset.gaze[0].frame = dataset.gaze[0].frame.rename({'time': 'ttt'})
+    dataset.gaze[0].frame = dataset.gaze[0].frame.drop(
+        ['pixel', 'position', 'velocity', 'acceleration'],
+    )
+
+    preprocessed_dirname = 'preprocessed-test'
+    shutil.rmtree(dataset.path / Path(preprocessed_dirname), ignore_errors=True)
+    shutil.rmtree(dataset.path / Path(preprocessed_dirname), ignore_errors=True)
+    dataset.save_preprocessed(preprocessed_dirname, extension='csv')
+    dataset.load_gaze_files(True, preprocessed_dirname, extension='csv')
+
+    assert (dataset.path / preprocessed_dirname).is_dir(), (
+        f'data was not written to {dataset.path / Path(preprocessed_dirname)}'
+    )
+
+
 @pytest.mark.parametrize(
     ('expected_save_preprocessed_path', 'expected_save_events_path', 'save_kwargs'),
     [
@@ -1027,6 +1105,7 @@ def test_save_files_have_correct_extension(
     dataset.load()
     dataset.pix2deg()
     dataset.pos2vel()
+    dataset.pos2acc()
 
     detect_events_kwargs = {'method': pm.events.microsaccades, 'threshold': 1, 'eye': 'auto'}
     dataset.detect_events(**detect_events_kwargs)
@@ -1185,11 +1264,8 @@ def test_velocity_columns(dataset_configuration):
     dataset = pm.Dataset(**dataset_configuration['init_kwargs'])
     dataset.load(preprocessed=True)
 
-    expected_velocity_columns = ['x_left_vel', 'y_left_vel', 'x_right_vel', 'y_right_vel']
-    case = unittest.TestCase()
-
     for gaze_df in dataset.gaze:
-        case.assertCountEqual(gaze_df.velocity_columns, expected_velocity_columns)
+        assert 'velocity' in gaze_df.columns
 
 
 @pytest.mark.parametrize(
diff --git a/tests/events/event_processing_test.py b/tests/events/event_processing_test.py
index 8d4ec32..15cc7bd 100644
--- a/tests/events/event_processing_test.py
+++ b/tests/events/event_processing_test.py
@@ -143,19 +143,22 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                 {'subject_id': [1], 'onset': [0], 'offset': [10]},
                 schema={'subject_id': pl.Int64, 'onset': pl.Int64, 'offset': pl.Int64},
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(10),
-                    'time': np.arange(10),
-                    'x_vel': np.ones(10),
-                    'y_vel': np.zeros(10),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_vel': pl.Float64,
-                    'y_vel': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(10),
+                        'time': np.arange(10),
+                        'x_vel': np.ones(10),
+                        'y_vel': np.zeros(10),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_vel': pl.Float64,
+                        'y_vel': pl.Float64,
+                    },
+                ),
+                velocity_columns=['x_vel', 'y_vel'],
             ),
             {'event_properties': 'peak_velocity'},
             {'identifiers': 'subject_id'},
@@ -182,19 +185,22 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                 {'subject_id': [1], 'onset': [0], 'offset': [10]},
                 schema={'subject_id': pl.Int64, 'onset': pl.Int64, 'offset': pl.Int64},
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(10),
-                    'time': np.arange(10),
-                    'x_vel': np.ones(10),
-                    'y_vel': np.zeros(10),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_vel': pl.Float64,
-                    'y_vel': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(10),
+                        'time': np.arange(10),
+                        'x_vel': np.ones(10),
+                        'y_vel': np.zeros(10),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_vel': pl.Float64,
+                        'y_vel': pl.Float64,
+                    },
+                ),
+                velocity_columns=['x_vel', 'y_vel'],
             ),
             {'event_properties': 'peak_velocity'},
             {'identifiers': 'subject_id'},
@@ -221,19 +227,22 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                 {'subject_id': [1], 'onset': [0], 'offset': [5]},
                 schema={'subject_id': pl.Int64, 'onset': pl.Int64, 'offset': pl.Int64},
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(10),
-                    'time': np.arange(10),
-                    'x_vel': np.concatenate([np.ones(5), np.zeros(5)]),
-                    'y_vel': np.zeros(10),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_vel': pl.Float64,
-                    'y_vel': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(10),
+                        'time': np.arange(10),
+                        'x_vel': np.concatenate([np.ones(5), np.zeros(5)]),
+                        'y_vel': np.zeros(10),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_vel': pl.Float64,
+                        'y_vel': pl.Float64,
+                    },
+                ),
+                velocity_columns=['x_vel', 'y_vel'],
             ),
             {'event_properties': 'peak_velocity'},
             {'identifiers': 'subject_id'},
@@ -260,19 +269,22 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                 {'subject_id': [1], 'onset': [0], 'offset': [10]},
                 schema={'subject_id': pl.Int64, 'onset': pl.Int64, 'offset': pl.Int64},
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(10),
-                    'time': np.arange(10),
-                    'x_pos': np.concatenate([np.ones(5), np.zeros(5)]),
-                    'y_pos': np.concatenate([np.zeros(5), np.ones(5)]),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_pos': pl.Float64,
-                    'y_pos': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(10),
+                        'time': np.arange(10),
+                        'x_pos': np.concatenate([np.ones(5), np.zeros(5)]),
+                        'y_pos': np.concatenate([np.zeros(5), np.ones(5)]),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_pos': pl.Float64,
+                        'y_pos': pl.Float64,
+                    },
+                ),
+                position_columns=['x_pos', 'y_pos'],
             ),
             {'event_properties': 'dispersion'},
             {'identifiers': 'subject_id'},
@@ -299,19 +311,22 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                 {'subject_id': [1], 'onset': [0], 'offset': [10]},
                 schema={'subject_id': pl.Int64, 'onset': pl.Int64, 'offset': pl.Int64},
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(10),
-                    'time': np.arange(10),
-                    'x_vel': np.concatenate([np.arange(0.1, 1.1, 0.1)]),
-                    'y_vel': np.concatenate([np.arange(0.1, 1.1, 0.1)]),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_vel': pl.Float64,
-                    'y_vel': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(10),
+                        'time': np.arange(10),
+                        'x_vel': np.concatenate([np.arange(0.1, 1.1, 0.1)]),
+                        'y_vel': np.concatenate([np.arange(0.1, 1.1, 0.1)]),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_vel': pl.Float64,
+                        'y_vel': pl.Float64,
+                    },
+                ),
+                velocity_columns=['x_vel', 'y_vel'],
             ),
             {'event_properties': 'peak_velocity'},
             {'identifiers': 'subject_id'},
@@ -340,19 +355,22 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                     'subject_id': pl.Int64, 'name': pl.Utf8, 'onset': pl.Int64, 'offset': pl.Int64,
                 },
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(100),
-                    'time': np.arange(100),
-                    'x_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
-                    'y_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_vel': pl.Float64,
-                    'y_vel': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(100),
+                        'time': np.arange(100),
+                        'x_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
+                        'y_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_vel': pl.Float64,
+                        'y_vel': pl.Float64,
+                    },
+                ),
+                velocity_columns=['x_vel', 'y_vel'],
             ),
             {'event_properties': 'peak_velocity'},
             {'identifiers': 'subject_id'},
@@ -381,19 +399,22 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                     'subject_id': pl.Int64, 'name': pl.Utf8, 'onset': pl.Int64, 'offset': pl.Int64,
                 },
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(100),
-                    'time': np.arange(100),
-                    'x_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
-                    'y_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_vel': pl.Float64,
-                    'y_vel': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(100),
+                        'time': np.arange(100),
+                        'x_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
+                        'y_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_vel': pl.Float64,
+                        'y_vel': pl.Float64,
+                    },
+                ),
+                velocity_columns=['x_vel', 'y_vel'],
             ),
             {'event_properties': 'peak_velocity'},
             {'identifiers': 'subject_id', 'name': 'A'},
@@ -422,19 +443,22 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                     'subject_id': pl.Int64, 'name': pl.Utf8, 'onset': pl.Int64, 'offset': pl.Int64,
                 },
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(100),
-                    'time': np.arange(100),
-                    'x_pos': np.concatenate([np.ones(11), np.zeros(69), 2 * np.ones(20)]),
-                    'y_pos': np.concatenate([np.ones(11), np.zeros(69), 2 * np.ones(20)]),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_pos': pl.Float64,
-                    'y_pos': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(100),
+                        'time': np.arange(100),
+                        'x_pos': np.concatenate([np.ones(11), np.zeros(69), 2 * np.ones(20)]),
+                        'y_pos': np.concatenate([np.ones(11), np.zeros(69), 2 * np.ones(20)]),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_pos': pl.Float64,
+                        'y_pos': pl.Float64,
+                    },
+                ),
+                position_columns=['x_pos', 'y_pos'],
             ),
             {'event_properties': 'location'},
             {'identifiers': 'subject_id'},
@@ -463,19 +487,26 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                     'subject_id': pl.Int64, 'name': pl.Utf8, 'onset': pl.Int64, 'offset': pl.Int64,
                 },
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(100),
-                    'time': np.arange(100),
-                    'x_pos': np.concatenate([np.ones(11), np.zeros(69), 2 * np.ones(19), [200]]),
-                    'y_pos': np.concatenate([np.ones(11), np.zeros(69), 2 * np.ones(19), [200]]),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_pos': pl.Float64,
-                    'y_pos': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(100),
+                        'time': np.arange(100),
+                        'x_pos': np.concatenate(
+                            [np.ones(11), np.zeros(69), 2 * np.ones(19), [200]],
+                        ),
+                        'y_pos': np.concatenate(
+                            [np.ones(11), np.zeros(69), 2 * np.ones(19), [200]],
+                        ),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_pos': pl.Float64,
+                        'y_pos': pl.Float64,
+                    },
+                ),
+                position_columns=['x_pos', 'y_pos'],
             ),
             {'event_properties': ('location', {'method': 'mean'})},
             {'identifiers': 'subject_id'},
@@ -504,19 +535,26 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
                     'subject_id': pl.Int64, 'name': pl.Utf8, 'onset': pl.Int64, 'offset': pl.Int64,
                 },
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(100),
-                    'time': np.arange(100),
-                    'x_pos': np.concatenate([np.ones(11), np.zeros(69), 2 * np.ones(19), [200]]),
-                    'y_pos': np.concatenate([np.ones(11), np.zeros(69), 2 * np.ones(19), [200]]),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_pos': pl.Float64,
-                    'y_pos': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(100),
+                        'time': np.arange(100),
+                        'x_pos': np.concatenate(
+                            [np.ones(11), np.zeros(69), 2 * np.ones(19), [200]],
+                        ),
+                        'y_pos': np.concatenate(
+                            [np.ones(11), np.zeros(69), 2 * np.ones(19), [200]],
+                        ),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_pos': pl.Float64,
+                        'y_pos': pl.Float64,
+                    },
+                ),
+                position_columns=['x_pos', 'y_pos'],
             ),
             {'event_properties': ('location', {'method': 'median'})},
             {'identifiers': 'subject_id'},
@@ -544,10 +582,8 @@ def test_event_gaze_processor_process_correct_result(
         event_df, gaze_df, init_kwargs, process_kwargs, expected_dataframe,
 ):
     events = EventDataFrame(event_df)
-    gaze = GazeDataFrame(gaze_df)
-
     processor = EventGazeProcessor(**init_kwargs)
-    property_result = processor.process(events, gaze, **process_kwargs)
+    property_result = processor.process(events, gaze_df, **process_kwargs)
     assert_frame_equal(property_result, expected_dataframe)
 
 
@@ -559,19 +595,22 @@ def test_event_gaze_processor_process_correct_result(
                 {'subject_id': [1], 'onset': [0], 'offset': [10]},
                 schema={'subject_id': pl.Int64, 'onset': pl.Int64, 'offset': pl.Int64},
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(10),
-                    'time': np.arange(10),
-                    'x_vel': np.ones(10),
-                    'y_vel': np.zeros(10),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_vel': pl.Float64,
-                    'y_vel': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(10),
+                        'time': np.arange(10),
+                        'x_vel': np.ones(10),
+                        'y_vel': np.zeros(10),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_vel': pl.Float64,
+                        'y_vel': pl.Float64,
+                    },
+                ),
+                velocity_columns=['x_vel', 'y_vel'],
             ),
             {'event_properties': 'peak_velocity'},
             {'identifiers': []},
@@ -587,19 +626,22 @@ def test_event_gaze_processor_process_correct_result(
                     'subject_id': pl.Int64, 'name': pl.Utf8, 'onset': pl.Int64, 'offset': pl.Int64,
                 },
             ),
-            pl.from_dict(
-                {
-                    'subject_id': np.ones(100),
-                    'time': np.arange(100),
-                    'x_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
-                    'y_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
-                },
-                schema={
-                    'subject_id': pl.Int64,
-                    'time': pl.Int64,
-                    'x_vel': pl.Float64,
-                    'y_vel': pl.Float64,
-                },
+            GazeDataFrame(
+                pl.from_dict(
+                    {
+                        'subject_id': np.ones(100),
+                        'time': np.arange(100),
+                        'x_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
+                        'y_vel': np.concatenate([np.ones(10), np.zeros(70), 2 * np.ones(20)]),
+                    },
+                    schema={
+                        'subject_id': pl.Int64,
+                        'time': pl.Int64,
+                        'x_vel': pl.Float64,
+                        'y_vel': pl.Float64,
+                    },
+                ),
+                velocity_columns=['x_vel', 'y_vel'],
             ),
             {'event_properties': 'peak_velocity'},
             {'identifiers': 'subject_id', 'name': 'cde'},
@@ -614,10 +656,9 @@ def test_event_processor_process_exceptions(
 ):
     processor = EventGazeProcessor(**init_kwargs)
     events = EventDataFrame(event_df)
-    gaze = GazeDataFrame(gaze_df)
 
     with pytest.raises(exception) as excinfo:
-        processor.process(events, gaze, **process_kwargs)
+        processor.process(events, gaze_df, **process_kwargs)
 
     msg, = excinfo.value.args
     for msg_substring in msg_substrings:
diff --git a/tests/gaze/gaze_dataframe_test.py b/tests/gaze/gaze_dataframe_test.py
index 5d73ec6..f7b0243 100644
--- a/tests/gaze/gaze_dataframe_test.py
+++ b/tests/gaze/gaze_dataframe_test.py
@@ -32,23 +32,26 @@ def fixture_experiment():
 
 
 @pytest.mark.parametrize(
-    ('init_df', 'expected_velocity_columns'),
+    ('init_arg'),
     [
         pytest.param(
             None,
-            [],
-            id='no_data',
+            id='None',
         ),
         pytest.param(
             pl.DataFrame(),
-            [],
-            id='no_columns',
-        ),
-        pytest.param(
-            pl.DataFrame(schema={'abc': pl.Int64}),
-            [],
-            id='no_velocity_columns',
+            id='no_eye_velocity_columns',
         ),
+    ],
+)
+def test_gaze_dataframe_init(init_arg):
+    gaze_df = GazeDataFrame(init_arg)
+    assert isinstance(gaze_df.frame, pl.DataFrame)
+
+
+@pytest.mark.parametrize(
+    ('init_df', 'velocity_columns'),
+    [
         pytest.param(
             pl.DataFrame(schema={'x_vel': pl.Float64, 'y_vel': pl.Float64}),
             ['x_vel', 'y_vel'],
@@ -81,25 +84,15 @@ def fixture_experiment():
         ),
     ],
 )
-def test_gaze_dataframe_velocity_columns(init_df, expected_velocity_columns):
-    gaze_df = GazeDataFrame(init_df)
+def test_gaze_dataframe_velocity_columns(init_df, velocity_columns):
+    gaze_df = GazeDataFrame(init_df, velocity_columns=velocity_columns)
 
-    assert set(gaze_df.velocity_columns) == set(expected_velocity_columns)
+    assert 'velocity' in gaze_df.columns
 
 
 @pytest.mark.parametrize(
-    ('init_df', 'expected_pixel_columns'),
+    ('init_df', 'pixel_columns'),
     [
-        pytest.param(
-            pl.DataFrame(),
-            [],
-            id='no_columns',
-        ),
-        pytest.param(
-            pl.DataFrame(schema={'abc': pl.Int64}),
-            [],
-            id='no_pix_pos_columns',
-        ),
         pytest.param(
             pl.DataFrame(schema={'x_pix': pl.Float64, 'y_pix': pl.Float64}),
             ['x_pix', 'y_pix'],
@@ -132,25 +125,15 @@ def test_gaze_dataframe_velocity_columns(init_df, expected_velocity_columns):
         ),
     ],
 )
-def test_gaze_dataframe_pixel_position_columns(init_df, expected_pixel_columns):
-    gaze_df = GazeDataFrame(init_df)
+def test_gaze_dataframe_pixel_position_columns(init_df, pixel_columns):
+    gaze_df = GazeDataFrame(init_df, pixel_columns=pixel_columns)
 
-    assert set(gaze_df.pixel_position_columns) == set(expected_pixel_columns)
+    assert 'pixel' in gaze_df.columns
 
 
 @pytest.mark.parametrize(
-    ('init_df', 'expected_position_columns'),
+    ('init_df', 'position_columns'),
     [
-        pytest.param(
-            pl.DataFrame(),
-            [],
-            id='no_columns',
-        ),
-        pytest.param(
-            pl.DataFrame(schema={'abc': pl.Int64}),
-            [],
-            id='no_pos_columns',
-        ),
         pytest.param(
             pl.DataFrame(schema={'x_pos': pl.Float64, 'y_pos': pl.Float64}),
             ['x_pos', 'y_pos'],
@@ -183,33 +166,33 @@ def test_gaze_dataframe_pixel_position_columns(init_df, expected_pixel_columns):
         ),
     ],
 )
-def test_gaze_dataframe_position_columns(init_df, expected_position_columns):
-    gaze_df = GazeDataFrame(init_df)
+def test_gaze_dataframe_position_columns(init_df, position_columns):
+    gaze_df = GazeDataFrame(init_df, position_columns=position_columns)
 
-    assert set(gaze_df.position_columns) == set(expected_position_columns)
+    assert 'position' in gaze_df.columns
 
 
 @pytest.mark.parametrize(
-    ('init_df', 'expected_position_columns'),
+    ('init_df', 'pixel_columns'),
     [
         pytest.param(
             pl.DataFrame(schema={'x_pix': pl.Float64, 'y_pix': pl.Float64}),
-            ['x_pos', 'y_pos'],
+            ['x_pix', 'y_pix'],
             id='no_eye_pos_columns',
         ),
         pytest.param(
             pl.DataFrame(schema={'abc': pl.Int64, 'x_pix': pl.Float64, 'y_pix': pl.Float64}),
-            ['x_pos', 'y_pos'],
+            ['x_pix', 'y_pix'],
             id='no_eye_pos_columns_with_other_columns',
         ),
         pytest.param(
             pl.DataFrame(schema={'x_right_pix': pl.Float64, 'y_right_pix': pl.Float64}),
-            ['x_right_pos', 'y_right_pos'],
+            ['x_right_pix', 'y_right_pix'],
             id='right_eye_pos_columns',
         ),
         pytest.param(
             pl.DataFrame(schema={'x_left_pix': pl.Float64, 'y_left_pix': pl.Float64}),
-            ['x_left_pos', 'y_left_pos'],
+            ['x_left_pix', 'y_left_pix'],
             id='left_eye_pos_columns',
         ),
         pytest.param(
@@ -219,58 +202,59 @@ def test_gaze_dataframe_position_columns(init_df, expected_position_columns):
                     'x_right_pix': pl.Float64, 'y_right_pix': pl.Float64,
                 },
             ),
-            ['x_left_pos', 'y_left_pos', 'x_right_pos', 'y_right_pos'],
+            ['x_left_pix', 'y_left_pix', 'x_right_pix', 'y_right_pix'],
             id='both_eyes_pos_columns',
         ),
     ],
 )
 def test_gaze_dataframe_pix2deg_has_correct_columns(
-        init_df, expected_position_columns, experiment_fixture,
+        init_df, pixel_columns, experiment_fixture,
 ):
-    gaze_df = GazeDataFrame(init_df, experiment=experiment_fixture)
+    gaze_df = GazeDataFrame(init_df, experiment=experiment_fixture, pixel_columns=pixel_columns)
     gaze_df.pix2deg()
 
-    assert set(gaze_df.position_columns) == set(expected_position_columns)
+    assert 'position' in gaze_df.columns
 
 
 @pytest.mark.parametrize(
-    ('init_kwargs', 'exception', 'msg_substrings'),
+    ('init_kwargs', 'exception', 'expected_msg'),
     [
         pytest.param(
             {
                 'data': pl.DataFrame(schema={'x_foo': pl.Float64, 'y_foo': pl.Float64}),
                 'experiment': Experiment(1024, 768, 38, 30, 60, 'center', 1000),
             },
-            AttributeError, ('pixel', 'position', 'columns', 'valid', 'x_pix', 'x_foo'),
-            id='no_pix_pos_columns',
+            pl.exceptions.ColumnNotFoundError,
+            'Column \'pixel\' not found. Available columns are: [\'x_foo\', \'y_foo\']',
+            id='no_pixel_column',
         ),
         pytest.param(
             {'data': pl.DataFrame(schema={'x_pix': pl.Float64, 'y_pix': pl.Float64})},
-            AttributeError, ('experiment', 'must'),
-            id='no_pix_pos_columns',
+            AttributeError,
+            'experiment must be specified for this method to work',
+            id='no_experiment',
         ),
     ],
 )
-def test_gaze_dataframe_pix2deg_exceptions(init_kwargs, exception, msg_substrings):
+def test_gaze_dataframe_pix2deg_exceptions(init_kwargs, exception, expected_msg):
     gaze_df = GazeDataFrame(**init_kwargs)
 
     with pytest.raises(exception) as excinfo:
         gaze_df.pix2deg()
 
     msg, = excinfo.value.args
-    for msg_substring in msg_substrings:
-        assert msg_substring.lower() in msg.lower()
+    assert msg == expected_msg
 
 
 @pytest.mark.parametrize(
-    ('init_df', 'expected_acceleration_columns'),
+    ('init_df', 'pixel_columns'),
     [
         pytest.param(
             pl.DataFrame(
                 {'x_pix': np.arange(100), 'y_pix': np.arange(100)},
                 schema={'x_pix': pl.Float64, 'y_pix': pl.Float64},
             ),
-            ['x_acc', 'y_acc'],
+            ['x_pix', 'y_pix'],
             id='no_eye_pos_acc_columns',
         ),
         pytest.param(
@@ -278,7 +262,7 @@ def test_gaze_dataframe_pix2deg_exceptions(init_kwargs, exception, msg_substring
                 {'abc': np.arange(100), 'x_pix': np.arange(100), 'y_pix': np.arange(100)},
                 schema={'abc': pl.Int64, 'x_pix': pl.Float64, 'y_pix': pl.Float64},
             ),
-            ['x_acc', 'y_acc'],
+            ['x_pix', 'y_pix'],
             id='no_eye_pos_acc_columns',
         ),
         pytest.param(
@@ -286,7 +270,7 @@ def test_gaze_dataframe_pix2deg_exceptions(init_kwargs, exception, msg_substring
                 {'x_right_pix': np.arange(100), 'y_right_pix': np.arange(100)},
                 schema={'x_right_pix': pl.Float64, 'y_right_pix': pl.Float64},
             ),
-            ['x_right_acc', 'y_right_acc'],
+            ['x_right_pix', 'y_right_pix'],
             id='right_eye_pos_acc_columns',
         ),
         pytest.param(
@@ -294,7 +278,7 @@ def test_gaze_dataframe_pix2deg_exceptions(init_kwargs, exception, msg_substring
                 {'x_left_pix': np.arange(100), 'y_left_pix': np.arange(100)},
                 schema={'x_left_pix': pl.Float64, 'y_left_pix': pl.Float64},
             ),
-            ['x_left_acc', 'y_left_acc'],
+            ['x_left_pix', 'y_left_pix'],
             id='left_eye_pos_acc_columns',
         ),
         pytest.param(
@@ -309,60 +293,61 @@ def test_gaze_dataframe_pix2deg_exceptions(init_kwargs, exception, msg_substring
                 },
             ),
             [
-                'x_left_acc', 'y_left_acc', 'x_right_acc', 'y_right_acc',
+                'x_left_pix', 'y_left_pix', 'x_right_pix', 'y_right_pix',
             ],
             id='both_eyes_pos_acc_columns',
         ),
     ],
 )
 def test_gaze_dataframe_pos2acc_has_correct_columns(
-        init_df, expected_acceleration_columns, experiment_fixture,
+        init_df, pixel_columns, experiment_fixture,
 ):
-    gaze_df = GazeDataFrame(init_df, experiment=experiment_fixture)
+    gaze_df = GazeDataFrame(init_df, experiment=experiment_fixture, pixel_columns=pixel_columns)
     gaze_df.pix2deg()
     gaze_df.pos2acc()
 
-    assert set(gaze_df.acceleration_columns) == set(expected_acceleration_columns)
+    assert 'acceleration' in gaze_df.columns
 
 
 @pytest.mark.parametrize(
-    ('init_kwargs', 'exception', 'msg_substrings'),
+    ('init_kwargs', 'exception', 'expected_msg'),
     [
         pytest.param(
             {
-                'data': pl.DataFrame(schema={'x_pix': pl.Float64, 'y_pix': pl.Float64}),
+                'data': pl.DataFrame(schema={'x_pos': pl.Float64, 'y_pos': pl.Float64}),
                 'experiment': Experiment(1024, 768, 38, 30, 60, 'center', 1000),
             },
-            AttributeError, ('position', 'columns', 'valid', 'x_pos', 'x_pix'),
-            id='no_dva_pos_columns',
+            pl.exceptions.ColumnNotFoundError,
+            "Column 'position' not found. Available columns are: ['x_pos', 'y_pos']",
+            id='no_pixel_column',
         ),
         pytest.param(
             {'data': pl.DataFrame(schema={'x_pos': pl.Float64, 'y_pos': pl.Float64})},
-            AttributeError, ('experiment', 'must'),
-            id='no_dva_pos_columns',
+            AttributeError,
+            'experiment must be specified for this method to work',
+            id='no_experiment',
         ),
     ],
 )
-def test_gaze_dataframe_pos2acc_exceptions(init_kwargs, exception, msg_substrings):
+def test_gaze_dataframe_pos2acc_exceptions(init_kwargs, exception, expected_msg):
     gaze_df = GazeDataFrame(**init_kwargs)
 
     with pytest.raises(exception) as excinfo:
         gaze_df.pos2acc()
 
     msg, = excinfo.value.args
-    for msg_substring in msg_substrings:
-        assert msg_substring.lower() in msg.lower()
+    assert msg == expected_msg
 
 
 @pytest.mark.parametrize(
-    ('init_df', 'expected_velocity_columns'),
+    ('init_df', 'pixel_columns'),
     [
         pytest.param(
             pl.DataFrame(
                 {'x_pix': np.arange(100), 'y_pix': np.arange(100)},
                 schema={'x_pix': pl.Float64, 'y_pix': pl.Float64},
             ),
-            ['x_vel', 'y_vel'],
+            ['x_pix', 'y_pix'],
             id='no_eye_pos_vel_columns',
         ),
         pytest.param(
@@ -370,7 +355,7 @@ def test_gaze_dataframe_pos2acc_exceptions(init_kwargs, exception, msg_substring
                 {'abc': np.arange(100), 'x_pix': np.arange(100), 'y_pix': np.arange(100)},
                 schema={'abc': pl.Int64, 'x_pix': pl.Float64, 'y_pix': pl.Float64},
             ),
-            ['x_vel', 'y_vel'],
+            ['x_pix', 'y_pix'],
             id='no_eye_pos_vel_columns',
         ),
         pytest.param(
@@ -378,7 +363,7 @@ def test_gaze_dataframe_pos2acc_exceptions(init_kwargs, exception, msg_substring
                 {'x_right_pix': np.arange(100), 'y_right_pix': np.arange(100)},
                 schema={'x_right_pix': pl.Float64, 'y_right_pix': pl.Float64},
             ),
-            ['x_right_vel', 'y_right_vel'],
+            ['x_right_pix', 'y_right_pix'],
             id='right_eye_pos_vel_columns',
         ),
         pytest.param(
@@ -386,7 +371,7 @@ def test_gaze_dataframe_pos2acc_exceptions(init_kwargs, exception, msg_substring
                 {'x_left_pix': np.arange(100), 'y_left_pix': np.arange(100)},
                 schema={'x_left_pix': pl.Float64, 'y_left_pix': pl.Float64},
             ),
-            ['x_left_vel', 'y_left_vel'],
+            ['x_left_pix', 'y_left_pix'],
             id='left_eye_pos_vel_columns',
         ),
         pytest.param(
@@ -401,46 +386,46 @@ def test_gaze_dataframe_pos2acc_exceptions(init_kwargs, exception, msg_substring
                 },
             ),
             [
-                'x_left_vel', 'y_left_vel', 'x_right_vel', 'y_right_vel',
+                'x_left_pix', 'y_left_pix', 'x_right_pix', 'y_right_pix',
             ],
             id='both_eyes_pos_vel_columns',
         ),
     ],
 )
 def test_gaze_dataframe_pos2vel_has_correct_columns(
-        init_df, expected_velocity_columns, experiment_fixture,
+        init_df, pixel_columns, experiment_fixture,
 ):
-    gaze_df = GazeDataFrame(init_df, experiment=experiment_fixture)
+    gaze_df = GazeDataFrame(init_df, experiment=experiment_fixture, pixel_columns=pixel_columns)
     gaze_df.pix2deg()
     gaze_df.pos2vel()
 
-    assert set(gaze_df.velocity_columns) == set(expected_velocity_columns)
+    assert 'velocity' in gaze_df.columns
 
 
 @pytest.mark.parametrize(
-    ('init_kwargs', 'exception', 'msg_substrings'),
+    ('init_kwargs', 'exception', 'expected_msg'),
     [
         pytest.param(
             {
-                'data': pl.DataFrame(schema={'x_pix': pl.Float64, 'y_pix': pl.Float64}),
+                'data': pl.DataFrame(schema={'x_pos': pl.Float64, 'y_pos': pl.Float64}),
                 'experiment': Experiment(1024, 768, 38, 30, 60, 'center', 1000),
             },
-            AttributeError, ('position', 'columns', 'valid', 'x_pos', 'x_pix'),
-            id='no_dva_pos_columns',
+            pl.exceptions.ColumnNotFoundError,
+            "Column 'position' not found. Available columns are: ['x_pos', 'y_pos']",
         ),
         pytest.param(
             {'data': pl.DataFrame(schema={'x_pos': pl.Float64, 'y_pos': pl.Float64})},
-            AttributeError, ('experiment', 'must'),
+            AttributeError,
+            'experiment must be specified for this method to work',
             id='no_dva_pos_columns',
         ),
     ],
 )
-def test_gaze_dataframe_pos2vel_exceptions(init_kwargs, exception, msg_substrings):
+def test_gaze_dataframe_pos2vel_exceptions(init_kwargs, exception, expected_msg):
     gaze_df = GazeDataFrame(**init_kwargs)
 
     with pytest.raises(exception) as excinfo:
         gaze_df.pos2vel()
 
     msg, = excinfo.value.args
-    for msg_substring in msg_substrings:
-        assert msg_substring.lower() in msg.lower()
+    assert msg == expected_msg
diff --git a/tests/gaze/transforms_test.py b/tests/gaze/transforms_test.py
index 6281a1a..a0e134a 100644
--- a/tests/gaze/transforms_test.py
+++ b/tests/gaze/transforms_test.py
@@ -204,6 +204,28 @@ screen_cm_2d = [100, 100]
             ValueError,
             id='list_coords_4d_screen_cm_not_2d_raises_value_error',
         ),
+        pytest.param(
+            {
+                'arr': np.zeros((n_coords, 6)),
+                'screen_px': screen_px_1d,
+                'screen_cm': screen_px_2d,
+                'distance_cm': screen_cm_1d,
+                'origin': 'center',
+            },
+            ValueError,
+            id='list_coords_6d_screen_px_not_2d_raises_value_error',
+        ),
+        pytest.param(
+            {
+                'arr': np.zeros((n_coords, 6)),
+                'screen_px': screen_px_2d,
+                'screen_cm': screen_cm_1d,
+                'distance_cm': screen_cm_1d,
+                'origin': 'center',
+            },
+            ValueError,
+            id='list_coords_6d_screen_cm_not_2d_raises_value_error',
+        ),
         pytest.param(
             {
                 'arr': np.zeros((n_coords, 4)),
diff --git a/tests/plotting/heatmap_test.py b/tests/plotting/heatmap_test.py
index c96d85e..2553be0 100644
--- a/tests/plotting/heatmap_test.py
+++ b/tests/plotting/heatmap_test.py
@@ -38,29 +38,34 @@ def fixture_experiment():
     return Experiment(1024, 768, 38, 30, 60, 'center', 1000)
 
 
-# Fixture for x and y position columns
-@pytest.fixture(
-    name='column_names_fixture',
-    params=[('x_pos', 'y_pos'), ('x_pix', 'y_pix')],
-)
-def fixture_column_names(request):
-    return request.param
-
+@pytest.fixture(name='args', params=['pix', 'pos'])
+def args_fixture(experiment_fixture, request):
+    if request.param == 'pix':
+        column_names = ['x_pix', 'y_pix']
+        pixel_columns = column_names
+        position_columns = None
+    else:
+        column_names = ['x_pos', 'y_pos']
+        pixel_columns = None
+        position_columns = column_names
 
-@pytest.fixture(name='args')
-def args_fixture(experiment_fixture, column_names_fixture):
     # Init a dataframe with 2 columns and 100 rows
     df = pl.DataFrame(
         {
-            column_names_fixture[0]: np.arange(0, 100),
-            column_names_fixture[1]: np.arange(0, 100),
+            column_names[0]: np.arange(0, 100),
+            column_names[1]: np.arange(0, 100),
         },
     )
 
     # Init a GazeDataFrame
-    gdf = GazeDataFrame(data=df, experiment=experiment_fixture)
+    gdf = GazeDataFrame(
+        data=df,
+        experiment=experiment_fixture,
+        pixel_columns=pixel_columns,
+        position_columns=position_columns,
+    )
 
-    return {'gaze': gdf, 'position_columns': column_names_fixture}
+    return gdf, request.param
 
 
 @pytest.mark.parametrize(
@@ -118,7 +123,13 @@ def args_fixture(experiment_fixture, column_names_fixture):
 def test_heatmap_show(args, kwargs, monkeypatch):
     mock = Mock()
     monkeypatch.setattr(plt, 'show', mock)
-    heatmap(**args, **kwargs)
+
+    if args[1] == 'pix':
+        kwargs['position_column'] = 'pixel'
+    else:
+        kwargs['position_column'] = 'position'
+
+    heatmap(args[0], **kwargs)
     plt.close()
     mock.assert_called_once()
 
@@ -126,7 +137,30 @@ def test_heatmap_show(args, kwargs, monkeypatch):
 def test_heatmap_noshow(args, monkeypatch):
     mock = Mock()
     monkeypatch.setattr(plt, 'show', mock)
-    heatmap(**args, show=False)
+
+    if args[1] == 'pix':
+        position_column = 'pixel'
+    else:
+        position_column = 'position'
+
+    heatmap(args[0], position_column=position_column, show=False)
+    plt.close()
+    mock.assert_not_called()
+
+
+def test_heatmap_noshow_no_pixel_or_position_column(args, monkeypatch):
+    mock = Mock()
+    monkeypatch.setattr(plt, 'show', mock)
+
+    if args[1] == 'pix':
+        position_column = 'pixel'
+    else:
+        position_column = 'position'
+
+    gaze = args[0]
+    gaze.frame = gaze.frame.rename({position_column: 'custom_column'})
+
+    heatmap(gaze, position_column='custom_column', show=False)
     plt.close()
     mock.assert_not_called()
 
@@ -134,14 +168,27 @@ def test_heatmap_noshow(args, monkeypatch):
 def test_heatmap_save(args, monkeypatch, tmp_path):
     mock = Mock()
     monkeypatch.setattr(figure.Figure, 'savefig', mock)
-    heatmap(**args, show=False, savepath=str(tmp_path / 'test.svg'))
+
+    if args[1] == 'pix':
+        position_column = 'pixel'
+    else:
+        position_column = 'position'
+
+    heatmap(
+        args[0], position_column=position_column, show=False, savepath=str(tmp_path / 'test.svg'),
+    )
     plt.close()
     mock.assert_called_once()
 
 
 def test_heatmap_invalid_position_columns(args):
+    if args[1] == 'pix':
+        position_column = 'position'
+    else:
+        position_column = 'pixel'
+
     with pytest.raises(ColumnNotFoundError):
-        heatmap(gaze=args['gaze'], position_columns=('x_invalid', 'y_invalid'), show=False)
+        heatmap(gaze=args[0], position_column=position_column, show=False)
 
 
 def test_heatmap_no_experiment_property():
@@ -152,7 +199,7 @@ def test_heatmap_no_experiment_property():
         },
     )
 
-    gdf = GazeDataFrame(data=df, experiment=None)
+    gdf = GazeDataFrame(data=df, pixel_columns=['x_pix', 'y_pix'], experiment=None)
 
     with pytest.raises(ValueError):
         heatmap(gdf, show=False)
diff --git a/tests/plotting/traceplot_test.py b/tests/plotting/traceplot_test.py
index c99c273..0d1b2a3 100644
--- a/tests/plotting/traceplot_test.py
+++ b/tests/plotting/traceplot_test.py
@@ -29,7 +29,7 @@ from matplotlib import figure
 import pymovements as pm
 
 
-@pytest.fixture(name='gaze')
+@pytest.fixture(name='gaze', scope='session')
 def gaze_fixture():
     x = np.arange(-100, 100)
     y = np.arange(-100, 100)
@@ -49,6 +49,7 @@ def gaze_fixture():
         data=arr,
         schema=['x_pix', 'y_pix'],
         experiment=experiment,
+        pixel_columns=['x_pix', 'y_pix'],
     )
 
     gaze.pix2deg()
@@ -60,49 +61,52 @@ def gaze_fixture():
 @pytest.mark.parametrize(
     'kwargs',
     [
-        pytest.param({'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(-100, 100)}, id='cval_array'),
         pytest.param(
-            {'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(-100, 100), 'cmap_norm': 'twoslope'},
+            {'cval': np.arange(-100, 100)},
+            id='cval_array',
+        ),
+        pytest.param(
+            {'cval': np.arange(-100, 100), 'cmap_norm': 'twoslope'},
             id='cmap_norm_twoslope',
         ),
         pytest.param(
-            {'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(0, 200), 'cmap_norm': 'nonorm'},
+            {'cval': np.arange(0, 200), 'cmap_norm': 'nonorm'},
             id='cmap_norm_nonorm',
         ),
         pytest.param(
-            {'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(0, 200)},
+            {'cval': np.arange(0, 200)},
             id='cmap_norm_nonorm_implicit',
         ),
         pytest.param(
-            {'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(-100, 100), 'cmap_norm': 'normalize'},
+            {'cval': np.arange(-100, 100), 'cmap_norm': 'normalize'},
             id='cmap_norm_normalize',
         ),
         pytest.param(
-            {'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(0, 200), 'cmap_norm': 'linear'},
+            {'cval': np.arange(0, 200), 'cmap_norm': 'linear'},
             id='cmap_norm_linear',
         ),
         pytest.param(
-            {
-                'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(0, 200),
-                'cmap_norm': matplotlib.colors.NoNorm(),
-            },
+            {'cval': np.arange(0, 200), 'cmap_norm': matplotlib.colors.NoNorm()},
             id='cmap_norm_class',
         ),
         pytest.param(
-            {
-                'x': 'x_pix', 'y': 'y_pix',
-                'cmap': matplotlib.colors.LinearSegmentedColormap(name='test', segmentdata={}),
-            },
+            {'cmap': matplotlib.colors.LinearSegmentedColormap(name='test', segmentdata={})},
             id='cmap_class',
         ),
-        pytest.param({'x': 'x_pix', 'y': 'y_pix', 'cmap_segmentdata': {}}, id='cmap_segmentdata'),
-        pytest.param({'x': 'x_pix', 'y': 'y_pix', 'padding': 0.1}, id='padding'),
         pytest.param(
-            {'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(0, 200), 'show_cbar': True},
+            {'cmap_segmentdata': {}},
+            id='cmap_segmentdata',
+        ),
+        pytest.param(
+            {'padding': 0.1},
+            id='padding',
+        ),
+        pytest.param(
+            {'cval': np.arange(0, 200), 'show_cbar': True},
             id='show_cbar_true',
         ),
         pytest.param(
-            {'x': 'x_pix', 'y': 'y_pix', 'cval': np.arange(0, 200), 'show_cbar': False},
+            {'cval': np.arange(0, 200), 'show_cbar': False},
             id='show_cbar_false',
         ),
     ],
@@ -118,7 +122,7 @@ def test_traceplot_show(gaze, kwargs, monkeypatch):
 def test_traceplot_noshow(gaze, monkeypatch):
     mock = Mock()
     monkeypatch.setattr(plt, 'show', mock)
-    pm.plotting.traceplot(gaze=gaze, x='x_pix', y='y_pix', show=False)
+    pm.plotting.traceplot(gaze=gaze, show=False)
     plt.close()
     mock.assert_not_called()
 
@@ -128,8 +132,6 @@ def test_traceplot_save(gaze, monkeypatch, tmp_path):
     monkeypatch.setattr(figure.Figure, 'savefig', mock)
     pm.plotting.traceplot(
         gaze=gaze,
-        x='x_pix',
-        y='y_pix',
         show=False,
         savepath=str(
             tmp_path /
@@ -158,4 +160,4 @@ def test_traceplot_exceptions(gaze, kwargs, exception, monkeypatch):
     monkeypatch.setattr(plt, 'show', mock)
 
     with pytest.raises(exception):
-        pm.plotting.traceplot(gaze=gaze, x='x_pix', y='y_pix', **kwargs)
+        pm.plotting.traceplot(gaze=gaze, **kwargs)
diff --git a/tests/plotting/tsplot_test.py b/tests/plotting/tsplot_test.py
index 997ebec..f84d3fa 100644
--- a/tests/plotting/tsplot_test.py
+++ b/tests/plotting/tsplot_test.py
@@ -48,6 +48,7 @@ def gaze_fixture():
         data=arr,
         schema=['x_pix', 'y_pix'],
         experiment=experiment,
+        pixel_columns=['x_pix', 'y_pix'],
     )
 
     gaze.pix2deg()
@@ -80,6 +81,7 @@ def gaze_fixture():
 def test_tsplot_show(gaze, kwargs, monkeypatch):
     mock = Mock()
     monkeypatch.setattr(plt, 'show', mock)
+    gaze.explode('pixel', ['x_pix', 'y_pix'])
     pm.plotting.tsplot(gaze=gaze, **kwargs)
     plt.close()
     mock.assert_called_once()
@@ -88,6 +90,7 @@ def test_tsplot_show(gaze, kwargs, monkeypatch):
 def test_tsplot_noshow(gaze, monkeypatch):
     mock = Mock()
     monkeypatch.setattr(plt, 'show', mock)
+    gaze.explode('pixel', ['x_pix', 'y_pix'])
     pm.plotting.tsplot(gaze=gaze, show=False)
     plt.close()
     mock.assert_not_called()
@@ -96,6 +99,7 @@ def test_tsplot_noshow(gaze, monkeypatch):
 def test_tsplot_save(gaze, monkeypatch, tmp_path):
     mock = Mock()
     monkeypatch.setattr(figure.Figure, 'savefig', mock)
+    gaze.explode('pixel', ['x_pix', 'y_pix'])
     pm.plotting.tsplot(gaze=gaze, show=False, savepath=str(tmp_path / 'test.svg'))
     plt.close()
     mock.assert_called_once()