Repository: aeye-lab/pymovements
Pull request #440 (2023-06-06 16:00:09)

feat(gaze): Add GazeDataFrame.transform()
This is still a work in progress!

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?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 [(`71696e7`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/71696e71fe691c134826a85cef500ddd0f022bc9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`32df7c1`)](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?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      #440   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           55        55           
  Lines         2089      2146   +57     
  Branches       475       459   -16     
=========================================
+ Hits          2089      2146   +57     
```


| [Files Changed](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) | Coverage Δ | |
|---|---|---|
| [src/pymovements/gaze/transforms\_pl/\_\_init\_\_.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvdHJhbnNmb3Jtc19wbC9fX2luaXRfXy5weQ==) | `100.00% <ø> (ø)` | |
| [src/pymovements/gaze/transforms\_pl/pos2acc.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvdHJhbnNmb3Jtc19wbC9wb3MyYWNjLnB5) | `100.00% <ø> (ø)` | |
| [src/pymovements/gaze/gaze\_dataframe.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?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%> (ø)` | |
| [...rc/pymovements/gaze/transforms\_pl/center\_origin.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvdHJhbnNmb3Jtc19wbC9jZW50ZXJfb3JpZ2luLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/gaze/transforms\_pl/pix2deg.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvdHJhbnNmb3Jtc19wbC9waXgyZGVnLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/gaze/transforms\_pl/pos2vel.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvdHJhbnNmb3Jtc19wbC9wb3MydmVsLnB5) | `100.00% <100.00%> (ø)` | |
| [...c/pymovements/gaze/transforms\_pl/savitzky\_golay.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvdHJhbnNmb3Jtc19wbC9zYXZpdHpreV9nb2xheS5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/utils/checks.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL3V0aWxzL2NoZWNrcy5weQ==) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/440?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).   
:loudspeaker: Have feedback on the report? [Share it here](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).

dkrako: I have ignored one line for pylint for this error: [cell-var-from-loop](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/cell-var-from-loop.html)

caused by these lines:
https://github.com/aeye-lab/pymovements/blob/40f85a63bf188e264fc52fff4515ff881e754d44/src/pymovements/gaze/transforms_pl/pix2deg.py#L79-L86

But now I have read a bit further and found this reddit thread:
https://www.reddit.com/r/learnpython/comments/kvct0d/pylint_cell_variable_val_defined_in_loop_why/

I toyed around a bit and pylint has actually exposed a bug here I think.
I will have to revisit this next week.
dkrako: Wow pylint really exposed a bug here. I didn't know that was a potential problem in python.

If I understand correctly, the unexpected behavior comes from the delayed creation of the lambda function.
In the previous code snippet `component` would then always be the final value (`n_components - 1`).

As a very simple example, the output of this is crazy:

```python
>>> multipliers = [lambda x : i * x for i in range(4)]
>>> print([m(2) for m in multipliers])
[6, 6, 6, 6]
```

This is the fix for the problematic code of my last comment:

https://github.com/aeye-lab/pymovements/blob/a1dfad6ce83aac0226b97f71e864fbcc09716ce0/src/pymovements/gaze/transforms_pl/pix2deg.py#L81-L86

with

https://github.com/aeye-lab/pymovements/blob/a1dfad6ce83aac0226b97f71e864fbcc09716ce0/src/pymovements/gaze/transforms_pl/pix2deg.py#L91-L93


Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 4eb8adb..0893d70 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,7 +35,7 @@ dependencies = [
   "matplotlib>=3.0.0,<4",
   "numpy>=1.10.0,<2",
   "pandas>1.0.0,<3",
-  "polars>=0.18.0,<0.19.0",
+  "polars>=0.18.3,<0.19.0",
   "pyarrow>=11.0.0,<12",
   "scipy>=1.5.4,<2",
   "tqdm>=4.0.0,<5",
diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py
index 910c4e5..b0fef4b 100644
--- a/src/pymovements/gaze/gaze_dataframe.py
+++ b/src/pymovements/gaze/gaze_dataframe.py
@@ -20,8 +20,13 @@
 """Module for the GazeDataFrame."""
 from __future__ import annotations
 
+import inspect
+from collections.abc import Callable
+from typing import Any
+
 import polars as pl
 
+from pymovements.gaze import transforms_pl
 from pymovements.gaze.experiment import Experiment
 from pymovements.gaze.transforms import pos2acc
 
@@ -216,6 +221,62 @@ class GazeDataFrame:
         self.n_components = n_components
         self.experiment = experiment
 
+    def transform(
+            self,
+            transform_method: str | Callable[..., pl.Expr],
+            **kwargs: Any,
+    ) -> None:
+        """Apply transformation method."""
+        if isinstance(transform_method, str):
+            transform_method = transforms_pl.TransformLibrary.get(transform_method)
+
+        if transform_method.__name__ == 'downsample':
+            downsample_factor = kwargs.pop('factor')
+            self.frame = self.frame.select(
+                transforms_pl.downsample(
+                    factor=downsample_factor, **kwargs,
+                ),
+            )
+
+        else:
+            method_kwargs = inspect.getfullargspec(transform_method).kwonlyargs
+            if 'origin' in method_kwargs and 'origin' not in kwargs:
+                self._check_experiment()
+                assert self.experiment is not None
+                kwargs['origin'] = self.experiment.screen.origin
+
+            if 'screen_resolution' in method_kwargs and 'screen_resolution' not in kwargs:
+                self._check_experiment()
+                assert self.experiment is not None
+                kwargs['screen_resolution'] = (
+                    self.experiment.screen.width_px, self.experiment.screen.height_px,
+                )
+
+            if 'screen_size' in method_kwargs and 'screen_size' not in kwargs:
+                self._check_experiment()
+                assert self.experiment is not None
+                kwargs['screen_size'] = (
+                    self.experiment.screen.width_cm, self.experiment.screen.height_cm,
+                )
+
+            if 'distance' in method_kwargs and 'distance' not in kwargs:
+                self._check_experiment()
+                assert self.experiment is not None
+                kwargs['distance'] = self.experiment.screen.distance_cm
+
+            if 'sampling_rate' in method_kwargs and 'sampling_rate' not in kwargs:
+                self._check_experiment()
+                assert self.experiment is not None
+                kwargs['sampling_rate'] = self.experiment.sampling_rate
+
+            if 'n_components' in method_kwargs and 'n_components' not in kwargs:
+                try:
+                    kwargs['n_components'] = self.frame['pixel'].list.lengths()[0]
+                except pl.ColumnNotFoundError:
+                    kwargs['n_components'] = self.frame['position'].list.lengths()[0]
+
+            self.frame = self.frame.with_columns(transform_method(**kwargs))
+
     def pix2deg(self) -> None:
         """Compute gaze positions in degrees of visual angle from pixel position coordinates.
 
@@ -484,7 +545,7 @@ class GazeDataFrame:
     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 not be None for this method to work')
 
 
 def _check_component_columns(
diff --git a/src/pymovements/gaze/transforms_pl/__init__.py b/src/pymovements/gaze/transforms_pl/__init__.py
index 145c324..f63a227 100644
--- a/src/pymovements/gaze/transforms_pl/__init__.py
+++ b/src/pymovements/gaze/transforms_pl/__init__.py
@@ -21,26 +21,24 @@
 from pymovements.gaze.transforms_pl.center_origin import center_origin
 from pymovements.gaze.transforms_pl.downsample import downsample
 from pymovements.gaze.transforms_pl.norm import norm
-from pymovements.gaze.transforms_pl.pix2deg import helper as pix2deg_helper
+from pymovements.gaze.transforms_pl.pix2deg import _arctan2_helper
 from pymovements.gaze.transforms_pl.pix2deg import pix2deg
 from pymovements.gaze.transforms_pl.pos2acc import pos2acc
 from pymovements.gaze.transforms_pl.pos2vel import pos2vel
-from pymovements.gaze.transforms_pl.savitzky_golay import helper as savitzky_golay_helper
 from pymovements.gaze.transforms_pl.savitzky_golay import savitzky_golay
 from pymovements.gaze.transforms_pl.transforms_library import register_transform
 from pymovements.gaze.transforms_pl.transforms_library import TransformLibrary
 
 
 __all__ = [
+    '_arctan2_helper',
     'center_origin',
     'downsample',
     'norm',
     'pix2deg',
-    'pix2deg_helper',
     'pos2acc',
     'pos2vel',
     'register_transform',
     'savitzky_golay',
-    'savitzky_golay_helper',
     'TransformLibrary',
 ]
diff --git a/src/pymovements/gaze/transforms_pl/center_origin.py b/src/pymovements/gaze/transforms_pl/center_origin.py
index c70a5ee..5ebb152 100644
--- a/src/pymovements/gaze/transforms_pl/center_origin.py
+++ b/src/pymovements/gaze/transforms_pl/center_origin.py
@@ -28,9 +28,11 @@ from pymovements.gaze.transforms_pl.transforms_library import register_transform
 @register_transform
 def center_origin(
         *,
-        screen_px: int,
+        screen_resolution: tuple[int, int],
         origin: str,
-        pixel_column: str,
+        n_components: int,
+        pixel_column: str = 'pixel',
+        output_column: str | None = None,
 ) -> pl.Expr:
     """Center pixel data.
 
@@ -38,18 +40,24 @@ def center_origin(
 
     Parameters
     ----------
-    screen_px:
-        Size of screen in pixels.
+    screen_resolution:
+        Pixel screen resolution as tuple (width, height).
     origin:
         The location of the pixel origin. Supported values: ``center``, ``lower left``
+    n_components:
+        Number of components in input column.
     pixel_column:
-        Name of the column with pixel data.
+        Name of the input column with pixel data.
+    output_column:
+        Name of the output column with centered pixel data.
     """
+    if output_column is None:
+        output_column = pixel_column
 
     if origin == 'center':
-        centered_pixels = pl.col(pixel_column)
+        origin_offset = (0.0, 0.0)
     elif origin == 'lower left':
-        centered_pixels = pl.col(pixel_column) - ((screen_px - 1) / 2)
+        origin_offset = ((screen_resolution[0] - 1) / 2, (screen_resolution[1] - 1) / 2)
     else:
         supported_origins = ['center', 'lower left']
         raise ValueError(
@@ -57,5 +65,10 @@ def center_origin(
             f' Valid values are: {supported_origins}',
         )
 
-    # If the sequence is empty, just forward sequence.
-    return pl.when(pl.all().len() == 0).then(pl.all()).otherwise(centered_pixels)
+    centered_pixels = pl.concat_list(
+        [
+            pl.col(pixel_column).list.get(component) - origin_offset[component % 2]
+            for component in range(n_components)
+        ],
+    ).alias(output_column)
+    return centered_pixels
diff --git a/src/pymovements/gaze/transforms_pl/pix2deg.py b/src/pymovements/gaze/transforms_pl/pix2deg.py
index 9c2c980..711f3d0 100644
--- a/src/pymovements/gaze/transforms_pl/pix2deg.py
+++ b/src/pymovements/gaze/transforms_pl/pix2deg.py
@@ -20,8 +20,7 @@
 """Module for py:func:`pymovements.gaze.transforms.pix2deg`"""
 from __future__ import annotations
 
-from functools import partial
-from typing import Any
+from collections.abc import Callable
 
 import numpy as np
 import polars as pl
@@ -31,57 +30,110 @@ from pymovements.gaze.transforms_pl.transforms_library import register_transform
 from pymovements.utils import checks
 
 
-def helper(s: Any, distance_px: float) -> pl.Series:
-    """This function is a workaround to get complete coverage by testing this function
-    explicitly."""
-    return pl.Series(np.arctan2(s[0], distance_px))
-
-
 @register_transform
 def pix2deg(
         *,
-        screen_px: int,
-        screen_cm: float,
-        distance_cm: float,
+        screen_resolution: tuple[int, int],
+        screen_size: tuple[float, float],
+        distance: float,
         origin: str,
-        pixel_column: str,
-        position_column: str,
+        n_components: int,
+        pixel_column: str = 'pixel',
+        position_column: str = 'position',
 ) -> pl.Expr:
     """Converts pixel screen coordinates to degrees of visual angle.
 
     Parameters
     ----------
-    screen_px:
-        Size of screen in pixels.
-    screen_cm:
-        Size of screen in centimeters.
-    distance_cm:
+    screen_resolution:
+        Pixel screen resolution as tuple (width, height).
+    screen_size:
+        Screen size in centimeters as tuple (width, height).
+    distance:
         Eye-to-screen distance in centimeters
     origin:
         The location of the pixel origin. Supported values: ``center``, ``lower left``. See also
         py:func:`~pymovements.gaze.transform.center_origin` for more information.
+    n_components:
+        Number of components in input column.
     pixel_column:
         The input pixel column name.
     position_column:
         The output position column name.
     """
-    _check_screen_scalar(screen_px=screen_px, screen_cm=screen_cm, distance_cm=distance_cm)
+    _check_screen_resolution(screen_resolution)
+    _check_screen_size(screen_size)
+    _check_distance(distance)
 
-    centered_pixels = center_origin(screen_px=screen_px, origin=origin, pixel_column=pixel_column)
+    centered_pixels = center_origin(
+        screen_resolution=screen_resolution,
+        origin=origin,
+        n_components=n_components,
+        pixel_column=pixel_column,
+    )
 
     # Compute eye-to-screen-distance in pixel units.
-    distance_px = distance_cm * (screen_px / screen_cm)
+    distance_pixels = tuple(
+        distance * (screen_px / screen_cm)
+        for screen_px, screen_cm in zip(screen_resolution, screen_size)
+    )
 
-    # Compute positions as radians using arctan2.
-    radians = pl.map([centered_pixels], partial(helper, distance_px=distance_px))
+    degree_components = [
+        centered_pixels.list.get(component).map(
+            _arctan2_helper(distance_pixels[component % 2]),
+        ) * (180 / np.pi)
+        for component in range(n_components)
+    ]
 
-    # 180 / pi transforms radians to degrees.
-    degrees = radians * (180 / np.pi)
+    return pl.concat_list(list(degree_components)).alias(position_column)
 
-    return degrees.alias(position_column)
 
+def _arctan2_helper(distance: float) -> Callable:
+    """Returns single-argument lambda function with fixed second argument."""
+    return lambda s: np.arctan2(s, distance)
 
-def _check_screen_scalar(**kwargs: Any) -> None:
+
+def _check_distance(distance: float) -> None:
     """Check if all screen values are scalars and are greather than zero."""
-    checks.check_is_scalar(**kwargs)
-    checks.check_is_greater_than_zero(**kwargs)
+    checks.check_is_scalar(distance=distance)
+    checks.check_is_greater_than_zero(distance=distance)
+
+
+def _check_screen_resolution(screen_resolution: tuple[int, int]) -> None:
+    """Check screen resolution value."""
+    if screen_resolution is None:
+        raise TypeError('screen_resolution must not be None')
+
+    if not isinstance(screen_resolution, (tuple, list)):
+        raise TypeError(
+            'screen_resolution must be of type tuple[int, int],'
+            f' but is of type {type(screen_resolution).__name__}',
+        )
+
+    if len(screen_resolution) != 2:
+        raise ValueError(
+            f'screen_resolution must have length of 2, but is of length {len(screen_resolution)}',
+        )
+
+    for element in screen_resolution:
+        checks.check_is_scalar(screen_resolution=element)
+        checks.check_is_greater_than_zero(screen_resolution=element)
+
+
+def _check_screen_size(screen_size: tuple[float, float]) -> None:
+    """Check screen size value."""
+    if screen_size is None:
+        raise TypeError('screen_size must not be None')
+
+    if not isinstance(screen_size, (tuple, list)):
+        raise TypeError(
+            'screen_size must be of type tuple[int, int],'
+            f' but is of type {type(screen_size).__name__}',
+        )
+
+    if len(screen_size) != 2:
+        raise ValueError(f'screen_size must have length of 2, but is of length {len(screen_size)}')
+
+    for element in screen_size:
+        checks.check_is_scalar(screen_size=element)
+        checks.check_is_greater_than_zero(screen_size=element)
diff --git a/src/pymovements/gaze/transforms_pl/pos2acc.py b/src/pymovements/gaze/transforms_pl/pos2acc.py
index f1aadeb..0f8acd2 100644
--- a/src/pymovements/gaze/transforms_pl/pos2acc.py
+++ b/src/pymovements/gaze/transforms_pl/pos2acc.py
@@ -30,15 +30,15 @@ from pymovements.gaze.transforms_pl.transforms_library import register_transform
 def pos2acc(
         *,
         sampling_rate: float,
-        window_length: int,
-        degree: int,
+        n_components: int,
+        degree: int = 2,
+        window_length: int = 7,
         padding: str | float | int | None = 'nearest',
+        position_column: str = 'position',
+        acceleration_column: str = 'acceleration',
 ) -> pl.Expr:
     """Compute acceleration data from positional data.
 
-    The method which is used for calculation is
-    :py:func:`~pymovements.gaze.transforms.savitzky_golay`.
-
     Parameters
     ----------
     sampling_rate:
@@ -49,6 +49,12 @@ def pos2acc(
         The window size to use.
     padding:
         The padding to use.
+    n_components:
+        Number of components in input column.
+    position_column:
+        The input position column name.
+    acceleration_column:
+        The output acceleration column name.
     """
     return savitzky_golay(
         window_length=window_length,
@@ -56,4 +62,7 @@ def pos2acc(
         sampling_rate=sampling_rate,
         padding=padding,
         derivative=2,
+        n_components=n_components,
+        input_column=position_column,
+        output_column=acceleration_column,
     )
diff --git a/src/pymovements/gaze/transforms_pl/pos2vel.py b/src/pymovements/gaze/transforms_pl/pos2vel.py
index 895b9bf..47cdd15 100644
--- a/src/pymovements/gaze/transforms_pl/pos2vel.py
+++ b/src/pymovements/gaze/transforms_pl/pos2vel.py
@@ -31,22 +31,15 @@ def pos2vel(
         *,
         sampling_rate: float,
         method: str,
+        n_components: int,
         degree: int | None = None,
         window_length: int | None = None,
         padding: str | float | int | None = 'nearest',
+        position_column: str = 'position',
+        velocity_column: str = 'velocity',
 ) -> pl.Expr:
     """Compute velocitiy data from positional data.
 
-    There are three methods available for velocity calculation:
-    * ``savitzky_golay``: velocity is calculated by a polynomial of fixed degree and window length.
-    See :py:func:`~pymovements.gaze.transforms.savitzky_golay` for further details.
-    * ``smooth``: velocity is calculated from the difference of the mean values
-    of the subsequent two samples and the preceding two samples
-    * ``neighbors``: velocity is calculated from difference of the subsequent
-    sample and the preceding sample
-    * ``preceding``: velocity is calculated from the difference of the current
-    sample to the preceding sample
-
     Parameters
     ----------
     sampling_rate:
@@ -62,25 +55,62 @@ def pos2vel(
     padding:
         The padding to use.  This has only an effect if using ``savitzky_golay`` as calculation
         method.
-    """
+    n_components:
+        Number of components in input column.
+    position_column:
+        The input position column name.
+    velocity_column:
+        The output velocity column name.
 
-    if method == 'neighbors':
-        preceding_neighbor = pl.all().shift(periods=1)
-        succeeding_neighbor = pl.all().shift(periods=-1)
-        return (succeeding_neighbor - preceding_neighbor) * (sampling_rate / 2)
+    Notes
+    -----
+    There are three methods available for velocity calculation:
 
+    * ``savitzky_golay``: velocity is calculated by a polynomial of fixed degree and window length.
+      See :py:func:`~pymovements.gaze.transforms.savitzky_golay` for further details.
+    * ``five_point``: velocity is calculated from the difference of the mean values
+      of the subsequent two samples and the preceding two samples
+    * ``neighbors``: velocity is calculated from difference of the subsequent
+      sample and the preceding sample
+    * ``preceding``: velocity is calculated from the difference of the current
+      sample to the preceding sample
+    """
     if method == 'preceding':
-        return pl.all().diff(n=1, null_behavior='ignore') * sampling_rate
+        return pl.concat_list(
+            [
+                pl.col(position_column).list.get(component)
+                .diff(n=1, null_behavior='ignore') * sampling_rate
+                for component in range(n_components)
+            ],
+        ).alias(velocity_column)
+
+    if method == 'neighbors':
+        return pl.concat_list(
+            [
+                (
+                    pl.col(position_column).shift(periods=-1).list.get(component)
+                    - pl.col(position_column).shift(periods=1).list.get(component)
+                ) * (sampling_rate / 2)
+                for component in range(n_components)
+            ],
+        ).alias(velocity_column)
 
-    if method == 'smooth':
+    if method in {'fivepoint', 'smooth'}:
         # Center of window is period 0 and will be filled.
         # mean(arr_-2, arr_-1) and mean(arr_1, arr_2) needs division by two
         # window is now 3 samples long (arr_-1.5, arr_0, arr_1+5)
         # we therefore need a divison by three, all in all it's a division by 6
-        return (
-            pl.all().shift(periods=-2) + pl.all().shift(periods=-1)
-            - pl.all().shift(periods=1) - pl.all().shift(periods=2)
-        ) * (sampling_rate / 6)
+        return pl.concat_list(
+            [
+                (
+                    pl.col(position_column).shift(periods=-2).list.get(component)
+                    + pl.col(position_column).shift(periods=-1).list.get(component)
+                    - pl.col(position_column).shift(periods=1).list.get(component)
+                    - pl.col(position_column).shift(periods=2).list.get(component)
+                ) * (sampling_rate / 6)
+                for component in range(n_components)
+            ],
+        ).alias(velocity_column)
 
     if method == 'savitzky_golay':
         if window_length is None:
@@ -94,9 +124,12 @@ def pos2vel(
             sampling_rate=sampling_rate,
             padding=padding,
             derivative=1,
+            n_components=n_components,
+            input_column=position_column,
+            output_column=velocity_column,
         )
 
-    supported_methods = ['preceding', 'neighbors', 'smooth', 'savitzky_golay']
+    supported_methods = ['preceding', 'neighbors', 'fivepoint', 'smooth', 'savitzky_golay']
     raise ValueError(
         f"Unknown method '{method}'. Supported methods are: {supported_methods}",
     )
diff --git a/src/pymovements/gaze/transforms_pl/savitzky_golay.py b/src/pymovements/gaze/transforms_pl/savitzky_golay.py
index e153c1c..625b98f 100644
--- a/src/pymovements/gaze/transforms_pl/savitzky_golay.py
+++ b/src/pymovements/gaze/transforms_pl/savitzky_golay.py
@@ -20,7 +20,6 @@
 """Module for py:func:`pymovements.gaze.transforms.savitzky_golay`"""
 from __future__ import annotations
 
-from collections.abc import Callable
 from functools import partial
 from typing import Any
 
@@ -31,25 +30,27 @@ from pymovements.gaze.transforms_pl.transforms_library import register_transform
 from pymovements.utils import checks
 
 
-def helper(s: Any, func: Callable) -> Any:
-    """This function is a workaround to get complete coverage by testing this function
-    explicitly."""
-    return func(x=s[0])
-
-
 @register_transform
 def savitzky_golay(
         *,
         window_length: int,
         degree: int,
+        sampling_rate: float,
+        n_components: int,
+        input_column: str,
+        output_column: str | None = None,
         derivative: int = 0,
-        sampling_rate: float = 1.0,
         padding: str | float | int | None = 'nearest',
 ) -> pl.Expr:
     """Apply a 1-D Savitzky-Golay filter to a column. :cite:p:`SavitzkyGolay1964`
 
     Parameters
     ----------
+    sampling_rate : float, optional
+        The spacing of the samples to which the filter will be applied.
+        This is only used if deriv > 0. Default is 1.0.
+    n_components:
+        Number of components in input column.
     window_length : int
         The length of the filter window (i.e., the number of coefficients).
         If `padding` is ``None``, `window_length` must be less than or equal
@@ -61,9 +62,6 @@ def savitzky_golay(
         The order of the derivative to compute. This must be a
         nonnegative integer. The default is 0, which means to filter
         the data without differentiating.
-    sampling_rate : float, optional
-        The spacing of the samples to which the filter will be applied.
-        This is only used if deriv > 0. Default is 1.0.
     padding : str or float, optional
         Must be either ``None``, a scalar or one of the strings ``mirror``, ``nearest`` or ``wrap``.
         This determines the type of extension to use for the padded signal to
@@ -73,6 +71,10 @@ def savitzky_golay(
         evaluate the last ``window_length // 2`` output values.
         When passing a scalar value, data will be padded using the passed value.
         See the Notes for more details on the padding methods ``mirror``, ``nearest`` or ``wrap``.
+    input_column:
+        The input column name.
+    output_column:
+        The output column name.
 
     Returns
     -------
@@ -82,31 +84,42 @@ def savitzky_golay(
     Notes
     -----
     Details on the `padding` options:
+
     * ``None``: No padding extension is used.
     * scalar value (int or float): The padding extension contains the specified scalar value.
     * ``mirror``: Repeats the values at the edges in reverse order. The value closest to the edge is
-    not included.
+      not included.
     * ``nearest``: The padding extension contains the nearest input value.
     * ``wrap``: The padding extension contains the values from the other end of the array.
 
-    For example, if the input is ``[1, 2, 3, 4, 5, 6, 7, 8]``, and
-    `window_length` is 7, the following shows the padded data for
+    Given the input is ``[1, 2, 3, 4, 5, 6, 7, 8]``, and
+    `window_length` is 7, the following table shows the padded data for
     the various ``padding`` options:
 
-    mode        |   Ext   |         Input          |   Ext
-    ------------+---------+------------------------+---------
-    None        | -  -  - | 1  2  3  4  5  6  7  8 | -  -  -
-    0           | 0  0  0 | 1  2  3  4  5  6  7  8 | 0  0  0
-    1           | 1  1  1 | 1  2  3  4  5  6  7  8 | 1  1  1
-    ``nearest`` | 1  1  1 | 1  2  3  4  5  6  7  8 | 8  8  8
-    ``mirror``  | 4  3  2 | 1  2  3  4  5  6  7  8 | 7  6  5
-    ``wrap``    | 6  7  8 | 1  2  3  4  5  6  7  8 | 1  2  3
+    +-------------+-------------+----------------------------+-------------+
+    | mode        |   padding   |           input            |   padding   |
+    +=============+=============+============================+=============+
+    | ``None``    | ``-  -  -`` | ``1  2  3  4  5  6  7  8`` | ``-  -  -`` |
+    +-------------+-------------+----------------------------+-------------+
+    | ``0``       | ``0  0  0`` | ``1  2  3  4  5  6  7  8`` | ``0  0  0`` |
+    +-------------+-------------+----------------------------+-------------+
+    | ``1``       | ``1  1  1`` | ``1  2  3  4  5  6  7  8`` | ``1  1  1`` |
+    +-------------+-------------+----------------------------+-------------+
+    | ``nearest`` | ``1  1  1`` | ``1  2  3  4  5  6  7  8`` | ``8  8  8`` |
+    +-------------+-------------+----------------------------+-------------+
+    | ``mirror``  | ``4  3  2`` | ``1  2  3  4  5  6  7  8`` | ``7  6  5`` |
+    +-------------+-------------+----------------------------+-------------+
+    | ``wrap``    | ``6  7  8`` | ``1  2  3  4  5  6  7  8`` | ``1  2  3`` |
+    +-------------+-------------+----------------------------+-------------+
     """
     _check_window_length(window_length=window_length)
     _check_degree(degree=degree, window_length=window_length)
     _check_derivative(derivative=derivative)
     _check_padding(padding=padding)
 
+    if output_column is None:
+        output_column = input_column
+
     delta = 1 / sampling_rate
 
     constant_value = 0.0
@@ -130,18 +143,27 @@ def savitzky_golay(
     # If the sequence is empty, don't use apply but forward sequence.
     return pl.when(pl.all().len() == 0).then(pl.all()).otherwise(
         # Use explode to transform array to pl.Series
-        pl.apply('*', partial(helper, func=func)).list.explode(),
-    )
+        # pl.apply(input_column, partial(helper, func=func)).list.explode(),
+        pl.concat_list(
+            [
+                # pl.map([pl.col(input_column).list.get(component)], func)
+                pl.col(input_column).list.get(component).map(func).list.explode()
+                for component in range(n_components)
+            ],
+        ),
+    ).alias(output_column)
 
 
 def _check_window_length(window_length: Any) -> None:
     """Check that window length is an integer and greater than zero."""
+    checks.check_is_not_none(window_length=window_length)
     checks.check_is_int(window_length=window_length)
     checks.check_is_greater_than_zero(degree=window_length)
 
 
 def _check_degree(degree: Any, window_length: int) -> None:
     """Check that polynomial degree is an integer, greater than zero and less than window_length."""
+    checks.check_is_not_none(degree=degree)
     checks.check_is_int(degree=degree)
     checks.check_is_greater_than_zero(degree=degree)
 
diff --git a/src/pymovements/utils/checks.py b/src/pymovements/utils/checks.py
index e114947..053adfb 100644
--- a/src/pymovements/utils/checks.py
+++ b/src/pymovements/utils/checks.py
@@ -188,6 +188,24 @@ def check_is_length_matching(**kwargs: Sized) -> None:
         raise ValueError(f'The sequences "{key_1}" and "{key_2}" must be of equal length.')
 
 
+def check_is_not_none(**kwargs: Any) -> None:
+    """Check if all passed values are None.
+
+    Parameters
+    ----------
+    kwargs
+        Keyword argument dictionary.
+
+    Raises
+    ------
+    TypeError
+        If any of the passed arguments are not None.
+    """
+    for key, value in kwargs.items():
+        if value is None:
+            raise TypeError(f"'{key}' must not be None")
+
+
 def check_is_int(**kwargs: Any) -> None:
     """Check if all passed values are of type `int`.
 


Test patch:
diff --git a/tests/gaze/gaze_dataframe_test.py b/tests/gaze/gaze_dataframe_test.py
index f7b0243..73d4039 100644
--- a/tests/gaze/gaze_dataframe_test.py
+++ b/tests/gaze/gaze_dataframe_test.py
@@ -231,7 +231,7 @@ def test_gaze_dataframe_pix2deg_has_correct_columns(
         pytest.param(
             {'data': pl.DataFrame(schema={'x_pix': pl.Float64, 'y_pix': pl.Float64})},
             AttributeError,
-            'experiment must be specified for this method to work',
+            'experiment must not be None for this method to work',
             id='no_experiment',
         ),
     ],
@@ -324,7 +324,7 @@ def test_gaze_dataframe_pos2acc_has_correct_columns(
         pytest.param(
             {'data': pl.DataFrame(schema={'x_pos': pl.Float64, 'y_pos': pl.Float64})},
             AttributeError,
-            'experiment must be specified for this method to work',
+            'experiment must not be None for this method to work',
             id='no_experiment',
         ),
     ],
@@ -416,7 +416,7 @@ def test_gaze_dataframe_pos2vel_has_correct_columns(
         pytest.param(
             {'data': pl.DataFrame(schema={'x_pos': pl.Float64, 'y_pos': pl.Float64})},
             AttributeError,
-            'experiment must be specified for this method to work',
+            'experiment must not be None for this method to work',
             id='no_dva_pos_columns',
         ),
     ],
diff --git a/tests/gaze/gaze_transform_test.py b/tests/gaze/gaze_transform_test.py
new file mode 100644
index 0000000..ce812fd
--- /dev/null
+++ b/tests/gaze/gaze_transform_test.py
@@ -0,0 +1,428 @@
+# Copyright (c) 2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Test GazeDataFrame transform method."""
+import numpy as np
+import polars as pl
+import pytest
+from polars.testing import assert_frame_equal
+
+import pymovements as pm
+
+
+@pytest.mark.parametrize(
+    ('gaze_init_kwargs', 'transform_method', 'transform_kwargs', 'expected'),
+    [
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': np.arange(1000, 1100, 1),
+                        'x_pix': np.arange(0, 10, 0.1),
+                        'y_pix': np.arange(20, 30, 0.1),
+                    },
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+            },
+            pm.gaze.transforms_pl.downsample, {'factor': 1},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': np.arange(1000, 1100, 1),
+                        'x_pix': np.arange(0, 10, 0.1),
+                        'y_pix': np.arange(20, 30, 0.1),
+                    },
+                ),
+                pixel_columns=['x_pix', 'y_pix'],
+            ),
+            id='downsample_factor_1_method_pass',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': np.arange(1000, 1100, 1),
+                        'x_pix': np.arange(0, 10, 0.1),
+                        'y_pix': np.arange(20, 30, 0.1),
+                    },
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+            },
+            'downsample', {'factor': 1},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': np.arange(1000, 1100, 1),
+                        'x_pix': np.arange(0, 10, 0.1),
+                        'y_pix': np.arange(20, 30, 0.1),
+                    },
+                ),
+                pixel_columns=['x_pix', 'y_pix'],
+            ),
+            id='downsample_factor_1',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': np.arange(1000, 1010, 1),
+                        'x_pix': np.arange(0, 1, 0.1),
+                        'y_pix': np.arange(20, 21, 0.1),
+                    },
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+            },
+            'downsample', {'factor': 2},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': np.arange(1000, 1010, 2),
+                        'x_pix': np.arange(0, 1, 0.2),
+                        'y_pix': [
+                            20.0, 20.200000000000003, 20.400000000000006,
+                            20.60000000000001, 20.80000000000001,
+                        ],
+                    },
+                ),
+                pixel_columns=['x_pix', 'y_pix'],
+            ),
+            id='downsample_factor_2',
+        ),
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': [1000],
+                        'x_pix': [(100 - 1) / 2],
+                        'y_pix': [0.0],
+                    },
+                ),
+                'experiment': pm.Experiment(
+                    sampling_rate=1000,
+                    screen_width_px=100,
+                    screen_height_px=100,
+                    screen_width_cm=100,
+                    screen_height_cm=100,
+                    distance_cm=100,
+                    origin='center',
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+            },
+            'center_origin', {},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': [1000],
+                        'x_pix': [(100 - 1) / 2],
+                        'y_pix': [0.0],
+                    },
+                ),
+                pixel_columns=['x_pix', 'y_pix'],
+            ),
+            id='center_origin_center',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': [1000, 1000],
+                        'x_pix': [(100 - 1) / 2, (100 - 1) / 2],
+                        'y_pix': [0.0, 0.0],
+                    },
+                ),
+                'experiment': pm.Experiment(
+                    sampling_rate=1000,
+                    screen_width_px=100,
+                    screen_height_px=100,
+                    screen_width_cm=100,
+                    screen_height_cm=100,
+                    distance_cm=100,
+                    origin='lower left',
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+            },
+            'center_origin', {},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': [1000, 1000],
+                        'x_pix': [0.0, 0.0],
+                        'y_pix': [-(100 - 1) / 2, -(100 - 1) / 2],
+                    },
+                ),
+                pixel_columns=['x_pix', 'y_pix'],
+            ),
+            id='center_origin_lower_left',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': [1000, 1000],
+                        'x_pix': [(100 - 1) / 2, (100 - 1) / 2],
+                        'y_pix': [0.0, 0.0],
+                    },
+                ),
+                'experiment': pm.Experiment(
+                    sampling_rate=1000,
+                    screen_width_px=100,
+                    screen_height_px=100,
+                    screen_width_cm=100,
+                    screen_height_cm=100,
+                    distance_cm=100,
+                    origin='center',
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+            },
+            'pix2deg', {},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': [1000, 1000],
+                        'x_pix': [49.5, 49.5],
+                        'y_pix': [0.0, 0.0],
+                        'x_dva': [26.335410003881348, 26.335410003881348],
+                        'y_dva': [0.0, 0.0],
+                    },
+                ),
+                pixel_columns=['x_pix', 'y_pix'],
+                position_columns=['x_dva', 'y_dva'],
+            ),
+            id='pix2deg_origin_center',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': [1000, 1000],
+                        'x_pix': [(100 - 1) / 2, (100 - 1) / 2],
+                        'y_pix': [0.0, 0.0],
+                    },
+                ),
+                'experiment': pm.Experiment(
+                    sampling_rate=1000,
+                    screen_width_px=100,
+                    screen_height_px=100,
+                    screen_width_cm=100,
+                    screen_height_cm=100,
+                    distance_cm=100,
+                    origin='center',
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+            },
+            'pix2deg', {'n_components': 2},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': [1000, 1000],
+                        'x_pix': [49.5, 49.5],
+                        'y_pix': [0.0, 0.0],
+                        'x_dva': [26.335410003881348, 26.335410003881348],
+                        'y_dva': [0.0, 0.0],
+                    },
+                ),
+                pixel_columns=['x_pix', 'y_pix'],
+                position_columns=['x_dva', 'y_dva'],
+            ),
+            id='pix2deg_origin_center_explicit_n_components',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': [1000],
+                        'x_pix': [(100 - 1) / 2],
+                        'y_pix': [0.0],
+                    },
+                ),
+                'experiment': pm.Experiment(
+                    sampling_rate=1000,
+                    screen_width_px=100,
+                    screen_height_px=100,
+                    screen_width_cm=100,
+                    screen_height_cm=100,
+                    distance_cm=100,
+                    origin='lower left',
+                ),
+                'pixel_columns': ['x_pix', 'y_pix'],
+            },
+            'pix2deg', {},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': [1000],
+                        'x_pix': [49.5],
+                        'y_pix': [0.0],
+                        'x_dva': [0.0],
+                        'y_dva': [-26.335410003881348],
+                    },
+                ),
+                pixel_columns=['x_pix', 'y_pix'],
+                position_columns=['x_dva', 'y_dva'],
+            ),
+            id='pix2deg_origin_lower_left',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': [1000, 1001, 1002],
+                        'x_dva': [1.0, 1.0, 1.0],
+                        'y_dva': [1.0, 1.1, 1.2],
+                    },
+                ),
+                'experiment': pm.Experiment(
+                    sampling_rate=1000,
+                    screen_width_px=100,
+                    screen_height_px=100,
+                    screen_width_cm=100,
+                    screen_height_cm=100,
+                    distance_cm=100,
+                    origin='lower left',
+                ),
+                'position_columns': ['x_dva', 'y_dva'],
+            },
+            'pos2vel', {'method': 'preceding'},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': [1000, 1001, 1002],
+                        'x_dva': [1.0, 1.0, 1.0],
+                        'y_dva': [1.0, 1.1, 1.2],
+                        'x_vel': [None, 0.0, 0.0],
+                        'y_vel': [None, 100.00000000000009, 99.99999999999987],
+                    },
+                ),
+                position_columns=['x_dva', 'y_dva'],
+                velocity_columns=['x_vel', 'y_vel'],
+            ),
+            id='pos2vel_preceding',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': [1000, 1001, 1002],
+                        'x_dva': [1.0, 1.0, 1.0],
+                        'y_dva': [1.0, 1.1, 1.2],
+                    },
+                ),
+                'experiment': pm.Experiment(
+                    sampling_rate=1000,
+                    screen_width_px=100,
+                    screen_height_px=100,
+                    screen_width_cm=100,
+                    screen_height_cm=100,
+                    distance_cm=100,
+                    origin='lower left',
+                ),
+                'position_columns': ['x_dva', 'y_dva'],
+            },
+            'pos2vel', {'method': 'neighbors'},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': [1000, 1001, 1002],
+                        'x_dva': [1.0, 1.0, 1.0],
+                        'y_dva': [1.0, 1.1, 1.2],
+                        'x_vel': [None, 0.0, None],
+                        'y_vel': [None, 99.99999999999997, None],
+                    },
+                ),
+                position_columns=['x_dva', 'y_dva'],
+                velocity_columns=['x_vel', 'y_vel'],
+            ),
+            id='pos2vel_neighbors',
+        ),
+
+        pytest.param(
+            {
+                'data': pl.from_dict(
+                    {
+                        'time': [1000, 1001, 1002, 1003, 1004],
+                        'x_dva': [1.0, 1.0, 1.0, 1.0, 1.0],
+                        'y_dva': [1.0, 1.1, 1.2, 1.3, 1.4],
+                    },
+                ),
+                'experiment': pm.Experiment(
+                    sampling_rate=1000,
+                    screen_width_px=100,
+                    screen_height_px=100,
+                    screen_width_cm=100,
+                    screen_height_cm=100,
+                    distance_cm=100,
+                    origin='lower left',
+                ),
+                'position_columns': ['x_dva', 'y_dva'],
+            },
+            'pos2vel', {'method': 'fivepoint'},
+            pm.GazeDataFrame(
+                data=pl.from_dict(
+                    {
+                        'time': [1000, 1001, 1002, 1003, 1004],
+                        'x_dva': [1.0, 1.0, 1.0, 1.0, 1.0],
+                        'y_dva': [1.0, 1.1, 1.2, 1.3, 1.4],
+                        'x_vel': [None, None, 0.0, None, None],
+                        'y_vel': [None, None, 100.00000000000001, None, None],
+                    },
+                ),
+                position_columns=['x_dva', 'y_dva'],
+                velocity_columns=['x_vel', 'y_vel'],
+            ),
+            id='pos2vel_five_point',
+        ),
+    ],
+)
+def test_gaze_transform_expected_frame(
+        gaze_init_kwargs, transform_method, transform_kwargs, expected,
+):
+    gaze = pm.GazeDataFrame(**gaze_init_kwargs)
+    gaze.transform(transform_method, **transform_kwargs)
+
+    assert_frame_equal(gaze.frame, expected.frame)
+
+
+@pytest.mark.parametrize(
+    ('init_kwargs', 'exception', 'exception_msg'),
+    [
+        pytest.param(
+            {
+                'data': pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+                'pixel_columns': 1,
+            },
+            TypeError,
+            'pixel_columns must be of type list, but is of type int',
+            id='pixel_columns_int',
+        ),
+    ],
+)
+def test_gaze_transform_raises_exception(init_kwargs, exception, exception_msg):
+    with pytest.raises(exception) as excinfo:
+        pm.GazeDataFrame(**init_kwargs)
+
+    msg, = excinfo.value.args
+    assert msg == exception_msg
diff --git a/tests/gaze/transforms/center_origin_test.py b/tests/gaze/transforms/center_origin_test.py
index dafeb06..f2d68e8 100644
--- a/tests/gaze/transforms/center_origin_test.py
+++ b/tests/gaze/transforms/center_origin_test.py
@@ -29,19 +29,22 @@ import pymovements as pm
     ('kwargs', 'exception', 'msg_substrings'),
     [
         pytest.param(
-            {'origin': 'center', 'pixel_column': 'pixel'},
+            {'origin': 'center', 'pixel_column': 'pixel', 'n_components': 2},
             TypeError,
-            ('screen_px', 'missing'),
-            id='no_screen_px_raises_type_error',
+            ('screen_resolution', 'missing'),
+            id='no_screen_resolution_raises_type_error',
         ),
         pytest.param(
-            {'screen_px': 100, 'pixel_column': 'pixel'},
+            {'screen_resolution': (100, 100), 'pixel_column': 'pixel', 'n_components': 2},
             TypeError,
             ('origin', 'missing'),
             id='no_origin_raises_type_error',
         ),
         pytest.param(
-            {'screen_px': 100, 'origin': 'foobar', 'pixel_column': 'pixel'},
+            {
+                'screen_resolution': (100, 100), 'origin': 'foobar', 'pixel_column': 'pixel',
+                'n_components': 2,
+            },
             ValueError,
             ('origin', 'invalid', 'foobar', 'valid', 'center', 'lower left'),
             id='invalid_origin_raises_value_error',
@@ -61,22 +64,41 @@ def test_center_origin_init_raises_error(kwargs, exception, msg_substrings):
     ('kwargs', 'series', 'expected_df'),
     [
         pytest.param(
-            {'screen_px': 100, 'origin': 'center', 'pixel_column': 'pixel'},
-            pl.Series('pixel', [], pl.Float64),
-            pl.Series('pixel', [], pl.Float64),
+            {
+                'screen_resolution': (100, 100), 'origin': 'center', 'pixel_column': 'pixel',
+                'n_components': 2,
+            },
+            pl.Series('pixel', [], pl.List(pl.Float64)),
+            pl.Series('pixel', [], pl.List(pl.Float64)),
             id='empty_series_returns_empty_series',
         ),
         pytest.param(
-            {'screen_px': 100, 'origin': 'center', 'pixel_column': 'pixel'},
-            pl.Series('pixel', [0], pl.Float64),
-            pl.Series('pixel', [0], pl.Float64),
-            id='zero_origin_center_returns_0',
+            {
+                'screen_resolution': (100, 100), 'origin': 'center',
+                'pixel_column': 'pixel', 'output_column': 'centered',
+                'n_components': 2,
+            },
+            pl.Series('pixel', [], pl.List(pl.Float64)),
+            pl.Series('centered', [], pl.List(pl.Float64)),
+            id='empty_series_returns_empty_series_with_output_column',
         ),
         pytest.param(
-            {'screen_px': 100, 'origin': 'lower left', 'pixel_column': 'pixel'},
-            pl.Series('pixel', [(100 - 1) / 2], pl.Float64),
-            pl.Series('pixel', [0], pl.Float64),
-            id='center_pixel_origin_lowerleft_returns_0',
+            {
+                'screen_resolution': (100, 100), 'origin': 'center', 'pixel_column': 'pixel',
+                'n_components': 2,
+            },
+            pl.Series('pixel', [[0, (100 - 1) / 2]], pl.List(pl.Float64)),
+            pl.Series('pixel', [[0, 49.5]], pl.List(pl.Float64)),
+            id='origin_center',
+        ),
+        pytest.param(
+            {
+                'screen_resolution': (100, 100), 'origin': 'lower left', 'pixel_column': 'pixel',
+                'n_components': 2,
+            },
+            pl.Series('pixel', [[0, (100 - 1) / 2]], pl.List(pl.Float64)),
+            pl.Series('pixel', [[-49.5, 0]], pl.List(pl.Float64)),
+            id='origin_lowerleft',
         ),
     ],
 )
diff --git a/tests/gaze/transforms/norm_test.py b/tests/gaze/transforms/norm_test.py
index 174b06a..f92059d 100644
--- a/tests/gaze/transforms/norm_test.py
+++ b/tests/gaze/transforms/norm_test.py
@@ -52,7 +52,7 @@ import pymovements as pm
                 pl.Series('x', [1, 1], pl.Float64),
                 pl.Series('y', [1, 1], pl.Float64),
             ]),
-            pl.Series(None, [1.41421356, 1.41421356], pl.Float64),
+            pl.Series(None, [1.4142, 1.4142], pl.Float64),
             id='empty_series_returns_empty_series',
         ),
     ],
diff --git a/tests/gaze/transforms/pix2deg_test.py b/tests/gaze/transforms/pix2deg_test.py
index 81bc025..6b32ecf 100644
--- a/tests/gaze/transforms/pix2deg_test.py
+++ b/tests/gaze/transforms/pix2deg_test.py
@@ -31,38 +31,42 @@ import pymovements as pm
     [
         pytest.param(
             {
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_size': (100, 00),
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             TypeError,
-            ('screen_px', 'missing'),
-            id='no_screen_px_raises_type_error',
+            ('screen_resolution', 'missing'),
+            id='no_screen_resolution_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 100, 'distance_cm': 100, 'origin': 'center',
+                'screen_resolution': (100, 00), 'distance': 100, 'origin': 'center',
                 'pixel_column': 'pixel', 'position_column': 'position',
+                'n_components': 2,
             },
             TypeError,
-            ('screen_cm', 'missing'),
-            id='no_screen_cm_raises_type_error',
+            ('screen_size', 'missing'),
+            id='no_screen_size_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 100, 'screen_cm': 100, 'origin': 'center',
+                'screen_resolution': (100, 100), 'screen_size': (100, 100), 'origin': 'center',
                 'pixel_column': 'pixel', 'position_column': 'position',
+                'n_components': 2,
             },
             TypeError,
-            ('distance_cm', 'missing'),
-            id='no_distance_cm_raises_type_error',
+            ('distance', 'missing'),
+            id='no_distance_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 100, 'screen_cm': 100, 'distance_cm': 100,
+                'screen_resolution': (100, 100), 'screen_size': (100, 100), 'distance': 100,
                 'pixel_column': 'pixel', 'position_column': 'position',
+                'n_components': 2,
             },
             TypeError,
             ('origin', 'missing'),
@@ -70,121 +74,187 @@ import pymovements as pm
         ),
         pytest.param(
             {
-                'screen_px': None,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': None,
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             TypeError,
-            ('screen_px', 'None', 'float', 'int'),
-            id='none_screen_px_raises_type_error',
+            ('screen_resolution must not be None'),
+            id='none_screen_resolution_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': None,
-                'distance_cm': 100,
+                'screen_resolution': (100, 100),
+                'screen_size': None,
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             TypeError,
-            ('screen_cm', 'None', 'float', 'int'),
-            id='none_screen_cm_raises_type_error',
+            ('screen_size must not be None'),
+            id='none_screen_size_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': None,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': None,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             TypeError,
-            ('distance_cm', 'None', 'float', 'int'),
-            id='none_distance_cm_raises_type_error',
+            ('distance', 'None', 'float', 'int'),
+            id='none_distance_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 0,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': (0, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             ValueError,
-            ('screen_px', 'must be greater than zero', '0'),
-            id='zero_screen_px_raises_type_error',
+            ('screen_resolution', 'must be greater than zero', '0'),
+            id='zero_screen_resolution_zero',
         ),
         pytest.param(
             {
-                'screen_px': 100, 'screen_cm': 0, 'distance_cm': 100, 'origin': 'center',
-                'pixel_column': 'pixel', 'position_column': 'position',
+                'screen_resolution': 1,
+                'screen_size': (100, 100),
+                'distance': 100,
+                'origin': 'center',
+                'pixel_column': 'pixel',
+                'position_column': 'position',
+                'n_components': 2,
+            },
+            TypeError,
+            ('screen_resolution', 'must be of type tuple[int, int]', 'type int'),
+            id='screen_resolution_int_scalar',
+        ),
+        pytest.param(
+            {
+                'screen_resolution': (100, 100, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
+                'origin': 'center',
+                'pixel_column': 'pixel',
+                'position_column': 'position',
+                'n_components': 2,
             },
             ValueError,
-            ('screen_cm', 'must be greater than zero', '0'),
-            id='zero_screen_cm_raises_type_error',
+            ('screen_resolution must have length of 2, but is of length 3',),
+            id='screen_resolution_3-tuple',
         ),
         pytest.param(
             {
-                'screen_px': 100, 'screen_cm': 100, 'distance_cm': 0, 'origin': 'center',
+                'screen_resolution': (100, 100), 'screen_size': (0, 100), 'distance': 100,
+                'origin': 'center', 'pixel_column': 'pixel', 'position_column': 'position',
+                'n_components': 2,
+            },
+            ValueError,
+            ('screen_size', 'must be greater than zero', '0'),
+            id='zero_screen_size_raises_type_error',
+        ),
+        pytest.param(
+            {
+                'screen_resolution': (100, 100),
+                'screen_size': 1,
+                'distance': 100,
+                'origin': 'center',
+                'pixel_column': 'pixel',
+                'position_column': 'position',
+                'n_components': 2,
+            },
+            TypeError,
+            ('screen_size', 'must be of type tuple[int, int]', 'type int'),
+            id='screen_size_int_scalar',
+        ),
+        pytest.param(
+            {
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100, 100),
+                'distance': 100,
+                'origin': 'center',
+                'pixel_column': 'pixel',
+                'position_column': 'position',
+                'n_components': 2,
+            },
+            ValueError,
+            ('screen_size must have length of 2, but is of length 3',),
+            id='screen_size_3-tuple',
+        ),
+        pytest.param(
+            {
+                'screen_resolution': (100, 100), 'screen_size': (100, 100), 'distance': 0,
+                'origin': 'center', 'n_components': 2,
                 'pixel_column': 'pixel', 'position_column': 'position',
             },
             ValueError,
-            ('distance_cm', 'must be greater than zero', '0'),
-            id='zero_distance_cm_raises_type_error',
+            ('distance', 'must be greater than zero', '0'),
+            id='zero_distance_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': -1,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': (-1, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             ValueError,
-            ('screen_px', 'must be greater than zero', '-1'),
-            id='negative_screen_px_raises_type_error',
+            ('screen_resolution', 'must be greater than zero', '-1'),
+            id='negative_screen_resolution_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': -1,
-                'distance_cm': 100,
+                'screen_resolution': (100, 100),
+                'screen_size': (-1, 100),
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             ValueError,
-            ('screen_cm', 'must be greater than zero', '-1'),
-            id='negative_screen_cm_raises_type_error',
+            ('screen_size', 'must be greater than zero', '-1'),
+            id='negative_screen_size_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': -1,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': -1,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             ValueError,
-            ('distance_cm', 'must be greater than zero', '-1'),
-            id='negative_distance_cm_raises_type_error',
+            ('distance', 'must be greater than zero', '-1'),
+            id='negative_distance_raises_type_error',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'foobar',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
             ValueError,
             ('origin', 'invalid', 'foobar', 'valid', 'center', 'lower left'),
@@ -206,9 +276,9 @@ def test_pix2deg_init_raises_error(kwargs, exception, msg_substrings):
     [
         pytest.param(
             {
-                'screen_px': 100, 'screen_cm': 100, 'distance_cm': 100, 'origin': 'center',
-                'pixel_column': 'aaa',
-                'position_column': 'bbb',
+                'screen_resolution': (100, 100), 'screen_size': (100, 100), 'distance': 100,
+                'origin': 'center', 'pixel_column': 'aaa', 'position_column': 'bbb',
+                'n_components': 2,
             },
             pl.Series('ccc', [0], pl.Float64),
             pl.exceptions.ColumnNotFoundError,
@@ -235,160 +305,200 @@ def test_pix2deg_raises_error(kwargs, series, exception, msg_substrings):
     [
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [], pl.Float64),
-            pl.Series('position', [], pl.Float64),
-            id='empty_series_returns_empty_series',
+            pl.Series('pixel', [], pl.List(pl.Float64)),
+            pl.Series('position', [], pl.List(pl.Float64)),
+            id='empty_series',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [0], pl.Float64),
-            pl.Series('position', [0], pl.Float64),
+            pl.Series('pixel', [[0, 0]], pl.List(pl.Float64)),
+            pl.Series('position', [[0, 0]], pl.List(pl.Float64)),
             id='zero_origin_center_returns_0',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'lower left',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [(100 - 1) / 2], pl.Float64),
-            pl.Series('position', [0], pl.Float64),
+            pl.Series('pixel', [[0, (100 - 1) / 2]], pl.List(pl.Float64)),
+            pl.Series('position', [[-26.3354, 0]], pl.List(pl.Float64)),
             id='center_pixel_origin_lowerleft_returns_0',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 50,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 50,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [50], pl.Float64),
-            pl.Series('position', [45], pl.Float64),
+            pl.Series('pixel', [[0, 50]], pl.List(pl.Float64)),
+            pl.Series('position', [[0, 45]], pl.List(pl.Float64)),
             id='isosceles_triangle_origin_center_returns_45',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 50,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 50,
                 'origin': 'lower left',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [100 - 0.5], pl.Float64),
-            pl.Series('position', [45], pl.Float64),
+            pl.Series('pixel', [[0, 100 - 0.5]], pl.List(pl.Float64)),
+            pl.Series('position', [[-44.7120, 45]], pl.List(pl.Float64)),
             id='isosceles_triangle_origin_lowerleft_returns_45',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 50,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 50,
                 'origin': 'lower left',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [-0.5], pl.Float64),
-            pl.Series('position', [-45], pl.Float64),
+            pl.Series('pixel', [[0, -0.5]], pl.List(pl.Float64)),
+            pl.Series('position', [[-44.7120, -45]], pl.List(pl.Float64)),
             id='isosceles_triangle_left_origin_lowerleft_returns_neg45',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [50], pl.Float64),
-            pl.Series('position', [26.565], pl.Float64),
+            pl.Series('pixel', [[0, 50]], pl.List(pl.Float64)),
+            pl.Series('position', [[0, 26.5650]], pl.List(pl.Float64)),
             id='ankathet_half_origin_center_returns_26.565',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 100,
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 100,
                 'origin': 'lower left',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [100 - 0.5], pl.Float64),
-            pl.Series('position', [26.565], pl.Float64),
+            pl.Series('pixel', [[0, 100 - 0.5]], pl.List(pl.Float64)),
+            pl.Series('position', [[-26.3354, 26.5650]], pl.List(pl.Float64)),
             id='ankathet_half_origin_lowerleft_returns_26.565',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 50 / np.sqrt(3),
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 50 / np.sqrt(3),
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [50], pl.Float64),
-            pl.Series('position', [60], pl.Float64),
+            pl.Series('pixel', [[0, 50]], pl.List(pl.Float64)),
+            pl.Series('position', [[0, 60]], pl.List(pl.Float64)),
             id='ankathet_sqrt3_origin_center_returns_60',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 50 / np.sqrt(3),
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 50 / np.sqrt(3),
                 'origin': 'lower left',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [100 - 0.5], pl.Float64),
-            pl.Series('position', [60], pl.Float64),
+            pl.Series('pixel', [[0, 100 - 0.5]], pl.List(pl.Float64)),
+            pl.Series('position', [[-59.75, 60]], pl.List(pl.Float64)),
             id='ankathet_sqrt3_origin_lowerleft_returns_60',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 50 * np.sqrt(3),
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 50 * np.sqrt(3),
                 'origin': 'center',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [50], pl.Float64),
-            pl.Series('position', [30], pl.Float64),
+            pl.Series('pixel', [[0, 50]], pl.List(pl.Float64)),
+            pl.Series('position', [[0, 30]], pl.List(pl.Float64)),
             id='opposite_sqrt3_origin_center_returns_30',
         ),
         pytest.param(
             {
-                'screen_px': 100,
-                'screen_cm': 100,
-                'distance_cm': 50 * np.sqrt(3),
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 100),
+                'distance': 50 * np.sqrt(3),
                 'origin': 'lower left',
                 'pixel_column': 'pixel',
                 'position_column': 'position',
+                'n_components': 2,
             },
-            pl.Series('pixel', [100 - 0.5], pl.Float64),
-            pl.Series('position', [30], pl.Float64),
+            pl.Series('pixel', [[-0.5, 100 - 0.5]], pl.List(pl.Float64)),
+            pl.Series('position', [[-30, 30]], pl.List(pl.Float64)),
             id='opposite_sqrt3_origin_lowerleft_returns_30',
         ),
+        pytest.param(
+            {
+                'screen_resolution': (100, 200),
+                'screen_size': (100, 100),
+                'distance': 100,
+                'origin': 'center',
+                'pixel_column': 'pixel',
+                'position_column': 'position',
+                'n_components': 2,
+            },
+            pl.Series('pixel', [[100, 100]], pl.List(pl.Float64)),
+            pl.Series('position', [[45, 26.5650]], pl.List(pl.Float64)),
+            id='screen_resolution_different_values',
+        ),
+        pytest.param(
+            {
+                'screen_resolution': (100, 100),
+                'screen_size': (100, 200),
+                'distance': 100,
+                'origin': 'center',
+                'pixel_column': 'pixel',
+                'position_column': 'position',
+                'n_components': 2,
+            },
+            pl.Series('pixel', [[100, 100]], pl.List(pl.Float64)),
+            pl.Series('position', [[45, 63.4349]], pl.List(pl.Float64)),
+            id='screen_size_different_values',
+        ),
     ],
 )
 def test_pix2deg_returns(kwargs, series, expected_df):
@@ -400,10 +510,9 @@ def test_pix2deg_returns(kwargs, series, expected_df):
     assert_frame_equal(result_df, expected_df.to_frame())
 
 
-def test_pix2deg_helper():
-    s = [[0]]
-    distance_cm = 100
-    expected = [0.0]
+def test__arctan2_helper():
+    x1 = 2
+    x2 = 100
 
-    result = pm.gaze.transforms_pl.pix2deg_helper(s, distance_cm)
-    assert all(result == expected)
+    f = pm.gaze.transforms_pl._arctan2_helper(x2)
+    assert np.allclose(f(x1), np.arctan2(x1, x2))
diff --git a/tests/gaze/transforms/pos2acc_test.py b/tests/gaze/transforms/pos2acc_test.py
index 0c232b5..3fa5de2 100644
--- a/tests/gaze/transforms/pos2acc_test.py
+++ b/tests/gaze/transforms/pos2acc_test.py
@@ -29,43 +29,31 @@ import pymovements as pm
     ('kwargs', 'exception', 'msg_substrings'),
     [
         pytest.param(
-            {'window_length': 1, 'sampling_rate': 1},
-            TypeError,
-            ('degree', 'missing'),
-            id='no_degree_raises_type_error',
-        ),
-        pytest.param(
-            {'degree': 1, 'sampling_rate': 1},
-            TypeError,
-            ('window_length', 'missing'),
-            id='no_window_length_raises_type_error',
-        ),
-        pytest.param(
-            {'window_length': 1, 'degree': 0, 'sampling_rate': 1},
+            {'window_length': 1, 'degree': 0, 'sampling_rate': 1, 'n_components': 2},
             ValueError,
             ('degree', 'must', 'greater than zero'),
             id='degree_zero_raises_type_error',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1.0, 'sampling_rate': 1},
+            {'window_length': 3, 'degree': 1.0, 'sampling_rate': 1, 'n_components': 2},
             TypeError,
             ('degree', "must be of type 'int'", "is of type 'float'"),
             id='degree_float_raises_type_error',
         ),
         pytest.param(
-            {'window_length': 1, 'degree': 1, 'sampling_rate': 1},
+            {'window_length': 1, 'degree': 1, 'sampling_rate': 1, 'n_components': 2},
             ValueError,
             ("'degree' must be less than 'window_length'"),
             id='degree_equal_window_size_raises_value_error',
         ),
         pytest.param(
-            {'window_length': 1, 'degree': 2, 'sampling_rate': 1},
+            {'window_length': 1, 'degree': 2, 'sampling_rate': 1, 'n_components': 2},
             ValueError,
             ("'degree' must be less than 'window_length'"),
             id='degree_greater_than_window_size_raises_value_error',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': [], 'sampling_rate': 1},
+            {'window_length': 3, 'degree': 1, 'padding': [], 'sampling_rate': 1, 'n_components': 2},
             TypeError,
             (
                 "'padding' must be of type 'str', 'int', 'float' or None",
@@ -74,7 +62,10 @@ import pymovements as pm
             id='invalid_padding_raises_value_error',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': 'foobar', 'sampling_rate': 1},
+            {
+                'window_length': 3, 'degree': 1, 'padding': 'foobar', 'sampling_rate': 1,
+                'n_components': 2,
+            },
             ValueError,
             (
                 'padding', 'invalid', 'foobar',
@@ -97,9 +88,12 @@ def test_pos2acc_init_raises_error(kwargs, exception, msg_substrings):
     ('kwargs', 'series', 'exception', 'msg_substrings'),
     [
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': None, 'sampling_rate': 1},
-            pl.Series('A', [1], pl.Float64),
-            pl.exceptions.PolarsPanicError,
+            {
+                'window_length': 3, 'degree': 1, 'padding': None, 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.exceptions.ComputeError,
             ('',),
             id='no_padding_input_shorter_than_window_length_raises_panicexception',
         ),
@@ -121,69 +115,96 @@ def test_pos2acc_raises_error(kwargs, series, exception, msg_substrings):
     ('kwargs', 'series', 'expected_df'),
     [
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'sampling_rate': 1},
-            pl.Series('A', [], pl.Float64),
-            pl.Series('A', [], pl.Float64),
+            {'window_length': 3, 'degree': 1, 'sampling_rate': 1, 'n_components': 2},
+            pl.Series('position', [], pl.List(pl.Float64)),
+            pl.Series('acceleration', [], pl.List(pl.Float64)),
             id='empty_series_returns_empty_series',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'sampling_rate': 1},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            {'window_length': 3, 'degree': 1, 'sampling_rate': 1, 'n_components': 2},
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': 'mirror', 'sampling_rate': 1},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'padding': 'mirror', 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero_mirror_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': 'nearest', 'sampling_rate': 1},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'padding': 'nearest', 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero_wrap_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': 'wrap', 'sampling_rate': 1},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'padding': 'wrap', 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero_wrap_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': 1, 'sampling_rate': 1},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'padding': 1, 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero_equal_scalar_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': 'nearest', 'sampling_rate': 1},
-            pl.Series('A', [1, 1], pl.Float64),
-            pl.Series('A', [0, 0], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'padding': 'nearest', 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[0, 0], [0, 0]], pl.List(pl.Float64)),
             id='two_equal_elements_results_zero_nearest_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': 'nearest', 'sampling_rate': 1},
-            pl.Series('A', [1, 1], pl.Float64),
-            pl.Series('A', [0, 0], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'padding': 'nearest', 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[0, 0], [0, 0]], pl.List(pl.Float64)),
             id='two_equal_elements_differentation_nearest_padding_result_zero',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'padding': 'nearest', 'sampling_rate': 1},
-            pl.Series('A', [1, 2], pl.Float64),
-            pl.Series('A', [0, 0], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'padding': 'nearest', 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [2, 2]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[0, 0], [0, 0]], pl.List(pl.Float64)),
             id='two_elements_1_2_double_differentation_nearest_padding_result_zero',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 2, 'padding': None, 'sampling_rate': 1},
-            pl.Series('A', [1, 4, 9], pl.Float64),
-            pl.Series('A', [2, 2, 2], pl.Float64),
+            {
+                'window_length': 3, 'degree': 2, 'padding': None, 'sampling_rate': 1,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [4, 4], [9, 9]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[2, 2], [2, 2], [2, 2]], pl.List(pl.Float64)),
             id='three_elements_1_4_9_double_differentation_none_padding_result_two',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 2, 'padding': None, 'sampling_rate': 10},
-            pl.Series('A', [1, 4, 9], pl.Float64),
-            pl.Series('A', [200, 200, 200], pl.Float64),
+            {
+                'window_length': 3, 'degree': 2, 'padding': None, 'sampling_rate': 10,
+                'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [4, 4], [9, 9]], pl.List(pl.Float64)),
+            pl.Series('acceleration', [[200, 200], [200, 200], [200, 200]], pl.List(pl.Float64)),
             id='three_elements_1_4_9_double_differentation_sampling_rate_10_no_padding_result_200',
         ),
     ],
diff --git a/tests/gaze/transforms/pos2vel_test.py b/tests/gaze/transforms/pos2vel_test.py
index 1b98ee9..0921e09 100644
--- a/tests/gaze/transforms/pos2vel_test.py
+++ b/tests/gaze/transforms/pos2vel_test.py
@@ -29,37 +29,49 @@ import pymovements as pm
     ('kwargs', 'exception', 'msg_substrings'),
     [
         pytest.param(
-            {'method': 'savitzky_golay', 'window_length': 1, 'sampling_rate': 1},
+            {'method': 'savitzky_golay', 'window_length': 1, 'sampling_rate': 1, 'n_components': 2},
             TypeError,
             ('degree', 'must not be None', "method 'savitzky_golay'"),
             id='no_degree_raises_type_error',
         ),
         pytest.param(
-            {'method': 'savitzky_golay', 'degree': 1, 'sampling_rate': 1},
+            {'method': 'savitzky_golay', 'degree': 1, 'sampling_rate': 1, 'n_components': 2},
             TypeError,
             ('window_length', 'must not be None', "method 'savitzky_golay'"),
             id='no_window_length_raises_type_error',
         ),
         pytest.param(
-            {'method': 'savitzky_golay', 'window_length': 1, 'degree': 0, 'sampling_rate': 1},
+            {
+                'method': 'savitzky_golay', 'window_length': 1,
+                'degree': 0, 'sampling_rate': 1, 'n_components': 2,
+            },
             ValueError,
             ('degree', 'must', 'greater than zero'),
             id='degree_zero_raises_type_error',
         ),
         pytest.param(
-            {'method': 'savitzky_golay', 'window_length': 3, 'degree': 1.0, 'sampling_rate': 1},
+            {
+                'method': 'savitzky_golay', 'window_length': 3,
+                'degree': 1.0, 'sampling_rate': 1, 'n_components': 2,
+            },
             TypeError,
             ('degree', "must be of type 'int'", "is of type 'float'"),
             id='degree_float_raises_type_error',
         ),
         pytest.param(
-            {'method': 'savitzky_golay', 'window_length': 1, 'degree': 1, 'sampling_rate': 1},
+            {
+                'method': 'savitzky_golay', 'window_length': 1,
+                'degree': 1, 'sampling_rate': 1, 'n_components': 2,
+            },
             ValueError,
             ("'degree' must be less than 'window_length'"),
             id='degree_equal_window_size_raises_value_error',
         ),
         pytest.param(
-            {'method': 'savitzky_golay', 'window_length': 1, 'degree': 2, 'sampling_rate': 1},
+            {
+                'method': 'savitzky_golay', 'window_length': 1,
+                'degree': 2, 'sampling_rate': 1, 'n_components': 2,
+            },
             ValueError,
             ("'degree' must be less than 'window_length'"),
             id='degree_greater_than_window_size_raises_value_error',
@@ -71,6 +83,7 @@ import pymovements as pm
                 'degree': 1,
                 'padding': [],
                 'sampling_rate': 1,
+                'n_components': 2,
             },
             TypeError,
             (
@@ -82,7 +95,7 @@ import pymovements as pm
         pytest.param(
             {
                 'method': 'savitzky_golay', 'window_length': 3,
-                'degree': 1, 'padding': 'foobar', 'sampling_rate': 1,
+                'degree': 1, 'padding': 'foobar', 'sampling_rate': 1, 'n_components': 2,
             },
             ValueError,
             (
@@ -92,7 +105,10 @@ import pymovements as pm
             id='invalid_padding_raises_value_error',
         ),
         pytest.param(
-            {'method': 'foobar', 'window_length': 3, 'degree': 1, 'sampling_rate': 1},
+            {
+                'method': 'foobar', 'window_length': 3, 'degree': 1, 'sampling_rate': 1,
+                'n_components': 2,
+            },
             ValueError,
             (
                 'unknown', 'method', "'foobar'", 'supported methods', 'preceding', 'neighbors',
@@ -117,10 +133,10 @@ def test_pos2vel_init_raises_error(kwargs, exception, msg_substrings):
         pytest.param(
             {
                 'method': 'savitzky_golay', 'window_length': 3,
-                'degree': 1, 'padding': None, 'sampling_rate': 1,
+                'degree': 1, 'padding': None, 'sampling_rate': 1, 'n_components': 2,
             },
-            pl.Series('A', [1], pl.Float64),
-            pl.exceptions.PolarsPanicError,
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.exceptions.ComputeError,
             ('',),
             id='no_padding_input_shorter_than_window_length_raises_panicexception',
         ),
@@ -142,10 +158,13 @@ def test_pos2vel_raises_error(kwargs, series, exception, msg_substrings):
     ('kwargs', 'series', 'expected_df'),
     [
         pytest.param(
-            {'method': 'savitzky_golay', 'window_length': 3, 'degree': 1, 'sampling_rate': 1},
-            pl.Series('A', [], pl.Float64),
-            pl.Series('A', [], pl.Float64),
-            id='empty_series_returns_empty_series',
+            {
+                'method': 'savitzky_golay', 'window_length': 3, 'degree': 1,
+                'sampling_rate': 1, 'n_components': 2,
+            },
+            pl.Series('position', [], pl.List(pl.Float64)),
+            pl.Series('velocity', [], pl.List(pl.Float64)),
+            id='empty_series_raises_compute_error',
         ),
         pytest.param(
             {
@@ -154,9 +173,10 @@ def test_pos2vel_raises_error(kwargs, series, exception, msg_substrings):
                 'degree': 1,
                 'sampling_rate': 1,
                 'padding': 'nearest',
+                'n_components': 2,
             },
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_nearest_padding_results_zero',
         ),
         pytest.param(
@@ -166,9 +186,10 @@ def test_pos2vel_raises_error(kwargs, series, exception, msg_substrings):
                 'degree': 1,
                 'padding': 'mirror',
                 'sampling_rate': 1,
+                'n_components': 2,
             },
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero_mirror_padding',
         ),
         pytest.param(
@@ -178,9 +199,10 @@ def test_pos2vel_raises_error(kwargs, series, exception, msg_substrings):
                 'degree': 1,
                 'padding': 'nearest',
                 'sampling_rate': 1,
+                'n_components': 2,
             },
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero_wrap_padding',
         ),
         pytest.param(
@@ -190,9 +212,10 @@ def test_pos2vel_raises_error(kwargs, series, exception, msg_substrings):
                 'degree': 1,
                 'padding': 'wrap',
                 'sampling_rate': 1,
+                'n_components': 2,
             },
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero_wrap_padding',
         ),
         pytest.param(
@@ -202,9 +225,10 @@ def test_pos2vel_raises_error(kwargs, series, exception, msg_substrings):
                 'degree': 1,
                 'padding': 1,
                 'sampling_rate': 1,
+                'n_components': 2,
             },
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [0], pl.Float64),
+            pl.Series('position', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[0, 0]], pl.List(pl.Float64)),
             id='single_element_results_zero_equal_scalar_padding',
         ),
         pytest.param(
@@ -214,9 +238,10 @@ def test_pos2vel_raises_error(kwargs, series, exception, msg_substrings):
                 'degree': 1,
                 'padding': 'nearest',
                 'sampling_rate': 1,
+                'n_components': 2,
             },
-            pl.Series('A', [1, 1], pl.Float64),
-            pl.Series('A', [0, 0], pl.Float64),
+            pl.Series('position', [[1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[0, 0], [0, 0]], pl.List(pl.Float64)),
             id='two_equal_elements_results_zero_nearest_padding',
         ),
         pytest.param(
@@ -226,81 +251,119 @@ def test_pos2vel_raises_error(kwargs, series, exception, msg_substrings):
                 'degree': 1,
                 'padding': 'nearest',
                 'sampling_rate': 1,
+                'n_components': 2,
             },
-            pl.Series('A', [1, 1, 1], pl.Float64),
-            pl.Series('A', [0, 0, 0], pl.Float64),
+            pl.Series('position', [[1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[0, 0], [0, 0], [0, 0]], pl.List(pl.Float64)),
             id='two_equal_elements_differentation_none_padding_result_zero',
         ),
         pytest.param(
             {
                 'method': 'savitzky_golay', 'window_length': 3,
                 'degree': 1, 'padding': None, 'sampling_rate': 1,
+                'n_components': 2,
             },
-            pl.Series('A', [1, 2, 3], pl.Float64),
-            pl.Series('A', [1, 1, 1], pl.Float64),
+            pl.Series('position', [[1, 1], [2, 2], [3, 3]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
             id='two_elements_1_2_differentation_none_padding_result_one',
         ),
         pytest.param(
             {
                 'method': 'savitzky_golay', 'window_length': 3,
                 'degree': 1, 'padding': None, 'sampling_rate': 1000,
+                'n_components': 2,
             },
-            pl.Series('A', [1, 2, 3], pl.Float64),
-            pl.Series('A', [1000, 1000, 1000], pl.Float64),
+            pl.Series('position', [[1, 1], [2, 2], [3, 3]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[1000, 1000], [1000, 1000], [1000, 1000]], pl.List(pl.Float64)),
             id='three_elements_1_2_3_differentation_sampling_rate_1000_none_padding_result_1000',
         ),
         pytest.param(
-            {'method': 'preceding', 'sampling_rate': 1},
-            pl.Series('A', [1, 1, 1], pl.Float64),
-            pl.Series('A', [None, 0, 0], pl.Float64),
+            {
+                'method': 'preceding', 'sampling_rate': 1, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[None, None], [0, 0], [0, 0]], pl.List(pl.Float64)),
             id='three_equal_elements_method_preceding_results_zero',
         ),
         pytest.param(
-            {'method': 'preceding', 'sampling_rate': 1},
-            pl.Series('A', [1, 2, 3], pl.Float64),
-            pl.Series('A', [None, 1, 1], pl.Float64),
+            {
+                'method': 'preceding', 'sampling_rate': 1, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [2, 2], [3, 3]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[None, None], [1, 1], [1, 1]], pl.List(pl.Float64)),
             id='three_rising_elements_method_preceding_results_one',
         ),
         pytest.param(
-            {'method': 'preceding', 'sampling_rate': 1000},
-            pl.Series('A', [1, 2, 3], pl.Float64),
-            pl.Series('A', [None, 1000, 1000], pl.Float64),
+            {
+                'method': 'preceding', 'sampling_rate': 1000, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [2, 2], [3, 3]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[None, None], [1000, 1000], [1000, 1000]], pl.List(pl.Float64)),
             id='three_rising_elements_method_preceding_sampling_rate_1000_results_1000',
         ),
         pytest.param(
-            {'method': 'neighbors', 'sampling_rate': 1},
-            pl.Series('A', [1, 1, 1], pl.Float64),
-            pl.Series('A', [None, 0, None], pl.Float64),
+            {
+                'method': 'neighbors', 'sampling_rate': 1, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[None, None], [0, 0], [None, None]], pl.List(pl.Float64)),
             id='three_equal_elements_method_neighbors_results_zero',
         ),
         pytest.param(
-            {'method': 'neighbors', 'sampling_rate': 1},
-            pl.Series('A', [1, 2, 3], pl.Float64),
-            pl.Series('A', [None, 1, None], pl.Float64),
+            {
+                'method': 'neighbors', 'sampling_rate': 1, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [2, 2], [3, 3]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[None, None], [1, 1], [None, None]], pl.List(pl.Float64)),
             id='three_rising_elements_method_neighbors_results_one',
         ),
         pytest.param(
-            {'method': 'neighbors', 'sampling_rate': 1000},
-            pl.Series('A', [1, 2, 3], pl.Float64),
-            pl.Series('A', [None, 1000, None], pl.Float64),
+            {
+                'method': 'neighbors', 'sampling_rate': 1000, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [2, 2], [3, 3]], pl.List(pl.Float64)),
+            pl.Series('velocity', [[None, None], [1000, 1000], [None, None]], pl.List(pl.Float64)),
             id='three_rising_elements_method_neighbors_sampling_rate_1000_results_1000',
         ),
         pytest.param(
-            {'method': 'smooth', 'sampling_rate': 1},
-            pl.Series('A', [1, 1, 1, 1, 1], pl.Float64),
-            pl.Series('A', [None, None, 0, None, None], pl.Float64),
+            {
+                'method': 'smooth', 'sampling_rate': 1, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series(
+                'velocity', [
+                    [None, None], [None, None], [0, 0], [
+                        None, None,
+                    ], [None, None],
+                ], pl.List(pl.Float64),
+            ),
             id='five_equal_elements_method_smooth_results_zero',
         ),
         pytest.param(
-            {'method': 'smooth', 'sampling_rate': 1},
-            pl.Series('A', [1, 2, 3, 4, 5], pl.Float64),
-            pl.Series('A', [None, None, 1, None, None], pl.Float64),
+            {
+                'method': 'smooth', 'sampling_rate': 1, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]], pl.List(pl.Float64)),
+            pl.Series(
+                'velocity', [
+                    [None, None], [None, None], [1, 1], [
+                        None, None,
+                    ], [None, None],
+                ], pl.List(pl.Float64),
+            ),
             id='three_rising_elements_method_smooth_results_one',
         ),
         pytest.param(
-            {'method': 'smooth', 'sampling_rate': 1000},
-            pl.Series('A', [1, 2, 3, 4, 5], pl.Float64),
-            pl.Series('A', [None, None, 1000, None, None], pl.Float64),
+            {
+                'method': 'smooth', 'sampling_rate': 1000, 'n_components': 2,
+            },
+            pl.Series('position', [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]], pl.List(pl.Float64)),
+            pl.Series(
+                'velocity', [
+                    [None, None], [None, None], [1000, 1000],
+                    [None, None], [None, None],
+                ], pl.List(pl.Float64),
+            ),
             id='three_rising_elements_method_smooth_sampling_rate_1000_results_1000',
         ),
     ],
diff --git a/tests/gaze/transforms/savitzky_golay_test.py b/tests/gaze/transforms/savitzky_golay_test.py
index c376c8e..a7b0805 100644
--- a/tests/gaze/transforms/savitzky_golay_test.py
+++ b/tests/gaze/transforms/savitzky_golay_test.py
@@ -29,55 +29,81 @@ import pymovements as pm
     ('kwargs', 'exception', 'msg_substrings'),
     [
         pytest.param(
-            {'window_length': 1},
+            {
+                'window_length': 1, 'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
             TypeError,
             ('degree', 'missing'),
             id='no_degree_raises_type_error',
         ),
         pytest.param(
-            {'degree': 1},
+            {
+                'degree': 1, 'sampling_rate': 1,
+                'n_components': 2, 'input_column': 'A',
+            },
             TypeError,
             ('window_length', 'missing'),
             id='no_window_length_raises_type_error',
         ),
         pytest.param(
-            {'window_length': 1, 'degree': 0},
+            {
+                'window_length': 1, 'degree': 0, 'sampling_rate': 1,
+                'n_components': 2, 'input_column': 'A',
+            },
             ValueError,
             ('degree', 'must', 'greater than zero'),
             id='degree_zero_raises_type_error',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1.0},
+            {
+                'window_length': 3, 'degree': 1.0, 'sampling_rate': 1,
+                'n_components': 2, 'input_column': 'A',
+            },
             TypeError,
             ('degree', "must be of type 'int'", "is of type 'float'"),
             id='degree_float_raises_type_error',
         ),
         pytest.param(
-            {'window_length': 1, 'degree': 1},
+            {
+                'window_length': 1, 'degree': 1, 'sampling_rate': 1,
+                'n_components': 2, 'input_column': 'A',
+            },
             ValueError,
             ("'degree' must be less than 'window_length'"),
             id='degree_equal_window_size_raises_value_error',
         ),
         pytest.param(
-            {'window_length': 1, 'degree': 2},
+            {
+                'window_length': 1, 'degree': 2, 'sampling_rate': 1,
+                'n_components': 2, 'input_column': 'A',
+            },
             ValueError,
             ("'degree' must be less than 'window_length'"),
             id='degree_greater_than_window_size_raises_value_error',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': -1},
+            {
+                'window_length': 3, 'degree': 1, 'derivative': -1, 'sampling_rate': 1,
+                'n_components': 2, 'input_column': 'A',
+            },
             ValueError,
             ("'derivative' must not be negative", '-1'),
             id='derivative_negative_raises_value_error',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 1.0},
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 1.0, 'sampling_rate': 1,
+                'n_components': 2, 'input_column': 'A',
+            },
             TypeError,
             ("'derivative' must be of type 'int'", "is of type 'float'"),
             id='derivative_float_raises_value_error',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': []},
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': [],
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
             TypeError,
             (
                 "'padding' must be of type 'str', 'int', 'float' or None",
@@ -86,7 +112,10 @@ import pymovements as pm
             id='invalid_padding_raises_value_error',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 'foobar'},
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 'foobar',
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
             ValueError,
             (
                 'padding', 'invalid', 'foobar',
@@ -109,9 +138,12 @@ def test_savitzky_golay_init_raises_error(kwargs, exception, msg_substrings):
     ('kwargs', 'series', 'exception', 'msg_substrings'),
     [
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': None},
-            pl.Series('A', [1, 1], pl.Float64),
-            pl.exceptions.PolarsPanicError,
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': None,
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
+            pl.exceptions.ComputeError,
             ('',),
             id='no_padding_input_shorter_than_window_length_raises_panicexception',
         ),
@@ -133,63 +165,93 @@ def test_savitzky_golay_raises_error(kwargs, series, exception, msg_substrings):
     ('kwargs', 'series', 'expected_df'),
     [
         pytest.param(
-            {'window_length': 3, 'degree': 1},
-            pl.Series('A', [], pl.Float64),
-            pl.Series('A', [], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1,
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [], pl.List(pl.Float64)),
+            pl.Series('A', [], pl.List(pl.Float64)),
             id='empty_series_returns_empty_series',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [1], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0,
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
             id='single_element_stays_the_same',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 'mirror'},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [1], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 'mirror',
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
             id='single_element_stays_the_same_mirror_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 'nearest'},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [1], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 'nearest',
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
             id='single_element_stays_the_same_nearest_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 'wrap'},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [1], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 'wrap',
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
             id='single_element_stays_the_same_wrap_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 1},
-            pl.Series('A', [1], pl.Float64),
-            pl.Series('A', [1], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': 1,
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
+            pl.Series('A', [[1, 1]], pl.List(pl.Float64)),
             id='single_element_stays_the_same_scalar_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': None},
-            pl.Series('A', [1, 1, 1], pl.Float64),
-            pl.Series('A', [1, 1, 1], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 0, 'padding': None,
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series('A', [[1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
             id='three_elements_stay_the_same_none_padding',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 1, 'padding': None},
-            pl.Series('A', [1, 1, 1], pl.Float64),
-            pl.Series('A', [0, 0, 0], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 1, 'padding': None,
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
+            pl.Series('A', [[0, 0], [0, 0], [0, 0]], pl.List(pl.Float64)),
             id='three_equal_elements_differentation_none_padding_result_zero',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 1, 'derivative': 1, 'padding': None},
-            pl.Series('A', [1, 2, 3], pl.Float64),
-            pl.Series('A', [1, 1, 1], pl.Float64),
+            {
+                'window_length': 3, 'degree': 1, 'derivative': 1, 'padding': None,
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1], [2, 2], [3, 3]], pl.List(pl.Float64)),
+            pl.Series('A', [[1, 1], [1, 1], [1, 1]], pl.List(pl.Float64)),
             id='three_elements_1_2_3_differentation_none_padding_result_one',
         ),
         pytest.param(
-            {'window_length': 3, 'degree': 2, 'derivative': 2, 'padding': None},
-            pl.Series('A', [1, 4, 9], pl.Float64),
-            pl.Series('A', [2, 2, 2], pl.Float64),
+            {
+                'window_length': 3, 'degree': 2, 'derivative': 2, 'padding': None,
+                'sampling_rate': 1, 'n_components': 2, 'input_column': 'A',
+            },
+            pl.Series('A', [[1, 1], [4, 4], [9, 9]], pl.List(pl.Float64)),
+            pl.Series('A', [[2, 2], [2, 2], [2, 2]], pl.List(pl.Float64)),
             id='three_elements_1_4_9_double_differentation_none_padding_result_two',
         ),
     ],
@@ -201,14 +263,3 @@ def test_savitzky_golay_returns(kwargs, series, expected_df):
         pm.gaze.transforms_pl.savitzky_golay(**kwargs),
     )
     assert_frame_equal(result_df, expected_df.to_frame())
-
-
-def test_savitzky_golay_helper():
-    def func(x):
-        return x
-
-    x = [0]
-    expected = 0
-
-    result = pm.gaze.transforms_pl.savitzky_golay_helper(x, func)
-    assert result == expected
diff --git a/tests/utils/checks_test.py b/tests/utils/checks_test.py
index caffaca..6ed005e 100644
--- a/tests/utils/checks_test.py
+++ b/tests/utils/checks_test.py
@@ -184,3 +184,8 @@ def test_check_shapes_positions_velocities_raises_error(kwargs, expected_error,
 def test_check_two_kwargs_with_three_kwargs_raises_value_error() -> None:
     with pytest.raises(ValueError):
         checks.check_two_kwargs(a=1, b=2, c=3)
+
+
+def test_check_is_not_none_raises_type_error() -> None:
+    with pytest.raises(TypeError):
+        checks.check_is_not_none(a=1, b=None)