Repository: aeye-lab/pymovements
Pull request #422 (2023-05-25 14:59:23)

refactor(events): Refactor gaze event processing for input tuples

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/422?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 [(`d80420c`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/d80420c3ea0c2939dd2f9e1a9dcad9db1e09e46d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`e6abd2b`)](https://app.codecov.io/gh/aeye-lab/pymovements/pull/422?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      #422   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           55        55           
  Lines         1916      1908    -8     
  Branches       427       425    -2     
=========================================
- Hits          1916      1908    -8     
```


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


</details>

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


Patch:
diff --git a/src/pymovements/events/event_processing.py b/src/pymovements/events/event_processing.py
index addc2e7..2671ff6 100644
--- a/src/pymovements/events/event_processing.py
+++ b/src/pymovements/events/event_processing.py
@@ -164,31 +164,36 @@ class EventGazeProcessor:
             for property_name in self.event_properties
         }
 
+        # We need to create a new column here, which is a list of position tuples.
+        # For the intermediate time before the tuple will be the default format for
+        # positions, we create this column here and drop this column afterwards.
+        position_columns = tuple(gaze.position_columns[:2])
+        if position_columns:
+            position_component_expressions = [pl.col(component) for component in position_columns]
+            gaze.frame = gaze.frame.with_columns(
+                pl.concat_list(position_component_expressions)
+                .alias('position'),
+            )
+        velocity_columns = tuple(gaze.velocity_columns[:2])
+        if velocity_columns:
+            velocity_component_expressions = [pl.col(component) for component in velocity_columns]
+            gaze.frame = gaze.frame.with_columns(
+                pl.concat_list(velocity_component_expressions)
+                .alias('velocity'),
+            )
+
         property_kwargs: dict[str, dict[str, Any]] = {
             property_name: {} for property_name in property_expressions.keys()
         }
         for property_name, property_expression in property_expressions.items():
-            property_args = inspect.getfullargspec(property_expression).args
-            if 'velocity_columns' in property_args:
-                velocity_columns = tuple(gaze.velocity_columns[:2])
-                property_kwargs[property_name]['velocity_columns'] = velocity_columns
-
-            if 'position_columns' in property_args:
-                position_columns = tuple(gaze.position_columns[:2])
-                property_kwargs[property_name]['position_columns'] = position_columns
+            property_args = inspect.getfullargspec(property_expression).kwonlyargs
 
             if 'position_column' in property_args:
-                # We need to create a new column here, which is a list of position tuples.
-                # For the intermediate time before the tuple will be the default format for
-                # positions, we create this column here and drop this column afterwards.
-                position_columns = tuple(gaze.position_columns[:2])
-                component_expressions = [pl.col(component) for component in position_columns]
-                gaze.frame = gaze.frame.with_columns(
-                    pl.concat_list(component_expressions)
-                    .alias('position'),
-                )
                 property_kwargs[property_name]['position_column'] = 'position'
 
+            if 'velocity_column' in property_args:
+                property_kwargs[property_name]['velocity_column'] = 'velocity'
+
         result = (
             gaze.frame.join(events.frame, on=identifiers)
             .filter(pl.col('time').is_between(pl.col('onset'), pl.col('offset')))
@@ -198,20 +203,14 @@ class EventGazeProcessor:
                     property_expression(**property_kwargs[property_name])
                     .alias(property_name)
                     for property_name, property_expression in property_expressions.items()
-                    if 'position_column' not in property_kwargs[property_name]
-                ] + [
-                    property_expression(**property_kwargs[property_name])
-                    .alias(property_name)
-                    .first()  # Not sure why this is needed, an outer list is being created somehow.
-                    for property_name, property_expression in property_expressions.items()
-                    if 'position_column' in property_kwargs[property_name]
-
                 ],
             )
         )
 
-        # If we created the position tuple column we drop it again.
+        # If we created the position and velocity tuple columns, we drop it again.
         if 'position' in gaze.frame.columns:
             gaze.frame.drop_in_place('position')
+        if 'velocity' in gaze.frame.columns:
+            gaze.frame.drop_in_place('velocity')
 
         return result
diff --git a/src/pymovements/events/event_properties.py b/src/pymovements/events/event_properties.py
index 22c6e6b..19212f6 100644
--- a/src/pymovements/events/event_properties.py
+++ b/src/pymovements/events/event_properties.py
@@ -35,68 +35,78 @@ def register_event_property(function: Callable) -> Callable:
 
 
 @register_event_property
-def duration() -> pl.Expr:
-    """Duration of an event.
-
-    The duration is defined as the difference between offset time and onset time.
-    """
-    return pl.col('offset') - pl.col('onset')
-
-
-@register_event_property
-def peak_velocity(velocity_columns: tuple[str, str] = ('x_vel', 'y_vel')) -> pl.Expr:
-    """Peak velocity of an event.
+def amplitude(
+        *,
+        position_column: str = 'position',
+        n_components: int = 2,
+) -> pl.Expr:
+    """Amplitude of an event.
 
     Parameters
     ----------
-    velocity_columns
-        The column names of the pitch and yaw velocity components.
+    position_column
+        The column name of the position tuples.
+    n_components:
+        Number of positional components. Usually these are the two components yaw and pitch.
 
     Raises
     ------
-    TypeError
-        If velocity_columns not of type tuple, velocity_columns not of length 2, or elements of
-        velocity_columns not of type str.
+    ValueError
+        If number of components is not 2.
     """
-    _check_velocity_columns(velocity_columns)
+    _check_has_two_componenents(n_components)
 
-    x_velocity = pl.col(velocity_columns[0])
-    y_velocity = pl.col(velocity_columns[1])
+    x_position = pl.col(position_column).arr.get(0)
+    y_position = pl.col(position_column).arr.get(1)
 
-    return (x_velocity.pow(2) + y_velocity.pow(2)).sqrt().max()
+    return (
+        (x_position.max() - x_position.min()).pow(2)
+        + (y_position.max() - y_position.min()).pow(2)
+    ).sqrt()
 
 
 @register_event_property
-def dispersion(position_columns: tuple[str, str] = ('x_pos', 'y_pos')) -> pl.Expr:
+def dispersion(
+        *,
+        position_column: str = 'position',
+        n_components: int = 2,
+) -> pl.Expr:
     """Dispersion of an event.
 
     Parameters
     ----------
-    position_columns
-        The column names of the pitch and yaw position components.
+    position_column
+        The column name of the position tuples.
+    n_components:
+        Number of positional components. Usually these are the two components yaw and pitch.
 
     Raises
     ------
-    TypeError
-        If position_columns not of type tuple, position_columns not of length 2, or elements of
-        position_columns not of type str.
+    ValueError
+        If number of components is not 2.
     """
-    _check_position_columns(position_columns)
+    _check_has_two_componenents(n_components)
 
-    x_position = pl.col(position_columns[0])
-    y_position = pl.col(position_columns[1])
+    x_position = pl.col(position_column).arr.get(0)
+    y_position = pl.col(position_column).arr.get(1)
 
     return x_position.max() - x_position.min() + y_position.max() - y_position.min()
 
 
 @register_event_property
-def amplitude(position_columns: tuple[str, str] = ('x_pos', 'y_pos')) -> pl.Expr:
-    """Amplitude of an event.
+def disposition(
+        *,
+        position_column: str = 'position',
+        n_components: int = 2,
+) -> pl.Expr:
+    """Disposition of an event.
 
     Parameters
     ----------
-    position_columns
-        The column names of the pitch and yaw position components.
+    position_column
+        The column name of the position tuples.
+    n_components:
+        Number of positional components. Usually these are the two components yaw and pitch.
 
     Raises
     ------
@@ -104,46 +114,58 @@ def amplitude(position_columns: tuple[str, str] = ('x_pos', 'y_pos')) -> pl.Expr
         If position_columns not of type tuple, position_columns not of length 2, or elements of
         position_columns not of type str.
     """
-    _check_position_columns(position_columns)
+    _check_has_two_componenents(n_components)
 
-    x_position = pl.col(position_columns[0])
-    y_position = pl.col(position_columns[1])
+    x_position = pl.col(position_column).arr.get(0)
+    y_position = pl.col(position_column).arr.get(1)
 
     return (
-        (x_position.max() - x_position.min()).pow(2)
-        + (y_position.max() - y_position.min()).pow(2)
+        (x_position.head(n=1) - x_position.reverse().head(n=1)).pow(2)
+        + (y_position.head(n=1) - y_position.reverse().head(n=1)).pow(2)
     ).sqrt()
 
 
 @register_event_property
-def disposition(position_columns: tuple[str, str] = ('x_pos', 'y_pos')) -> pl.Expr:
-    """Disposition of an event.
+def duration() -> pl.Expr:
+    """Duration of an event.
+
+    The duration is defined as the difference between offset time and onset time.
+    """
+    return pl.col('offset') - pl.col('onset')
+
+
+@register_event_property
+def peak_velocity(
+        *,
+        velocity_column: str = 'velocity',
+        n_components: int = 2,
+) -> pl.Expr:
+    """Peak velocity of an event.
 
     Parameters
     ----------
-    position_columns
-        The column names of the pitch and yaw position components.
+    velocity_column
+        The column name of the velocity tuples.
+    n_components:
+        Number of positional components. Usually these are the two components yaw and pitch.
 
     Raises
     ------
-    TypeError
-        If position_columns not of type tuple, position_columns not of length 2, or elements of
-        position_columns not of type str.
+    ValueError
+        If number of components is not 2.
     """
-    _check_position_columns(position_columns)
+    _check_has_two_componenents(n_components)
 
-    x_position = pl.col(position_columns[0])
-    y_position = pl.col(position_columns[1])
+    x_velocity = pl.col(velocity_column).arr.get(0)
+    y_velocity = pl.col(velocity_column).arr.get(1)
 
-    return (
-        (x_position.head(n=1) - x_position.reverse().head(n=1)).pow(2)
-        + (y_position.head(n=1) - y_position.reverse().head(n=1)).pow(2)
-    ).sqrt()
+    return (x_velocity.pow(2) + y_velocity.pow(2)).sqrt().max()
 
 
 @register_event_property
 def position(
         method: str = 'mean',
+        *,
         position_column: str = 'position',
         n_components: int = 2,
 ) -> pl.Expr:
@@ -186,40 +208,11 @@ def position(
 
         component_expressions.append(expression_component)
 
-    return pl.concat_list(component_expressions)
-
-
-def _check_position_columns(position_columns: tuple[str, str]) -> None:
-    """Check if position_columns is of type tuple[str, str]."""
-    if not isinstance(position_columns, tuple):
-        raise TypeError(
-            'position_columns must be of type tuple[str, str]'
-            f' but is of type {type(position_columns).__name__}',
-        )
-    if len(position_columns) != 2:
-        raise TypeError(
-            f'position_columns must be of length of 2 but is of length {len(position_columns)}',
-        )
-    if not all(isinstance(velocity_column, str) for velocity_column in position_columns):
-        raise TypeError(
-            'position_columns must be of type tuple[str, str] but is '
-            f'tuple[{type(position_columns[0]).__name__}, {type(position_columns[1]).__name__}]',
-        )
+    # Not sure why first() is needed here, but an outer list is being created somehow.
+    return pl.concat_list(component_expressions).first()
 
 
-def _check_velocity_columns(velocity_columns: tuple[str, str]) -> None:
-    """Check if velocity_columns is of type tuple[str, str]."""
-    if not isinstance(velocity_columns, tuple):
-        raise TypeError(
-            'velocity_columns must be of type tuple[str, str]'
-            f' but is of type {type(velocity_columns).__name__}',
-        )
-    if len(velocity_columns) != 2:
-        raise TypeError(
-            f'velocity_columns must be of length of 2 but is of length {len(velocity_columns)}',
-        )
-    if not all(isinstance(velocity_column, str) for velocity_column in velocity_columns):
-        raise TypeError(
-            'velocity_columns must be of type tuple[str, str] but is '
-            f'tuple[{type(velocity_columns[0]).__name__}, {type(velocity_columns[1]).__name__}]',
-        )
+def _check_has_two_componenents(n_components: int) -> None:
+    """Check that number of componenents is two."""
+    if n_components != 2:
+        raise ValueError('data must have exactly two components')


Test patch:
diff --git a/tests/events/event_processing_test.py b/tests/events/event_processing_test.py
index c4f09e9..e99b66d 100644
--- a/tests/events/event_processing_test.py
+++ b/tests/events/event_processing_test.py
@@ -295,14 +295,54 @@ def test_event_gaze_processor_init_exceptions(args, kwargs, exception, msg_subst
             ),
             id='dispersion_single_event_complete_window',
         ),
+        pytest.param(
+            pl.from_dict(
+                {'subject_id': [1], 'onset': [0], 'offset': [10]},
+                schema={'subject_id': pl.Int64, 'onset': pl.Int64, 'offset': pl.Int64},
+            ),
+            pl.from_dict(
+                {
+                    'subject_id': np.ones(10),
+                    'time': np.arange(10),
+                    'x_vel': np.concatenate([np.arange(0.1, 1.1, 0.1)]),
+                    'y_vel': np.concatenate([np.arange(0.1, 1.1, 0.1)]),
+                },
+                schema={
+                    'subject_id': pl.Int64,
+                    'time': pl.Int64,
+                    'x_vel': pl.Float64,
+                    'y_vel': pl.Float64,
+                },
+            ),
+            {'event_properties': 'peak_velocity'},
+            {'identifiers': 'subject_id'},
+            pl.from_dict(
+                {
+                    'subject_id': [1],
+                    'name': [None],
+                    'onset': [0],
+                    'offset': [10],
+                    'peak_velocity': [np.sqrt(2)],
+                },
+                schema={
+                    'subject_id': pl.Int64,
+                    'name': pl.Utf8,
+                    'onset': pl.Int64,
+                    'offset': pl.Int64,
+                    'peak_velocity': pl.Float64,
+                },
+            ),
+            id='peak_velocity_single_event_complete_window',
+        ),
     ],
 )
 def test_event_gaze_processor_process_correct_result(
         event_df, gaze_df, init_kwargs, process_kwargs, expected_dataframe,
 ):
-    processor = EventGazeProcessor(**init_kwargs)
     events = EventDataFrame(event_df)
     gaze = GazeDataFrame(gaze_df)
+
+    processor = EventGazeProcessor(**init_kwargs)
     property_result = processor.process(events, gaze, **process_kwargs)
     assert_frame_equal(property_result, expected_dataframe)
 
diff --git a/tests/events/event_properties_test.py b/tests/events/event_properties_test.py
index d78e823..c97ce52 100644
--- a/tests/events/event_properties_test.py
+++ b/tests/events/event_properties_test.py
@@ -32,80 +32,10 @@ from pymovements.events.event_properties import EVENT_PROPERTIES
     [
         pytest.param(
             event_properties.peak_velocity,
-            {'velocity_columns': 'x_vel'},
-            TypeError,
-            ('velocity_columns', 'type', 'must', 'tuple', 'str'),
-            id='peak_velocity_str',
-        ),
-        pytest.param(
-            event_properties.peak_velocity,
-            {'velocity_columns': 1},
-            TypeError,
-            ('velocity_columns', 'type', 'must', 'tuple', 'int'),
-            id='peak_velocity_int',
-        ),
-        pytest.param(
-            event_properties.peak_velocity,
-            {'velocity_columns': ('x_vel', 'y_vel', 'z_vel')},
-            TypeError,
-            ('velocity_columns', 'length', 'must', '2', '3'),
-            id='peak_velocity_tuple_length_3',
-        ),
-        pytest.param(
-            event_properties.peak_velocity,
-            {'velocity_columns': ('x_vel', 1)},
-            TypeError,
-            ('velocity_columns', 'type', 'must', 'tuple', 'str', 'int'),
-            id='peak_velocity_tuple_of_int',
-        ),
-        pytest.param(
-            event_properties.dispersion,
-            {'position_columns': 'x_pos'},
-            TypeError,
-            ('position_columns', 'type', 'must', 'tuple', 'str'),
-            id='duration_str',
-        ),
-        pytest.param(
-            event_properties.dispersion,
-            {'position_columns': 1},
-            TypeError,
-            ('position_columns', 'type', 'must', 'tuple', 'int'),
-            id='duration_int',
-        ),
-        pytest.param(
-            event_properties.dispersion,
-            {'position_columns': ('x_pos', 'y_pos', 'z_pos')},
-            TypeError,
-            ('position_columns', 'length', 'must', '2', '3'),
-            id='duration_tuple_length_3',
-        ),
-        pytest.param(
-            event_properties.dispersion,
-            {'position_columns': ('x_pos', 1)},
-            TypeError,
-            ('position_columns', 'type', 'must', 'tuple', 'str', 'int'),
-            id='duration_tuple_of_int',
-        ),
-        pytest.param(
-            event_properties.amplitude,
-            {'position_columns': 1},
-            TypeError,
-            ('position_columns', 'type', 'must', 'tuple', 'int'),
-            id='amplitude_int',
-        ),
-        pytest.param(
-            event_properties.amplitude,
-            {'position_columns': ('x_pos', 'y_pos', 'z_pos')},
-            TypeError,
-            ('position_columns', 'length', 'must', '2', '3'),
-            id='amplitude_tuple_length_3',
-        ),
-        pytest.param(
-            event_properties.amplitude,
-            {'position_columns': ('x_pos', 1)},
-            TypeError,
-            ('position_columns', 'type', 'must', 'tuple', 'str', 'int'),
-            id='amplitude_tuple_of_int',
+            {'n_components': 3},
+            ValueError,
+            ('data must have exactly two components',),
+            id='peak_velocity_not_2_components_raise_value_error',
         ),
         pytest.param(
             event_properties.position,
@@ -146,51 +76,27 @@ def test_property_init_exceptions(event_property, init_kwargs, exception, msg_su
         ),
         pytest.param(
             event_properties.peak_velocity,
-            {'velocity_columns': ('x_vel', 'y_vel')},
-            pl.DataFrame(schema={'y_vel': pl.Int64}),
-            pl.exceptions.ColumnNotFoundError,
-            ('x_vel',),
-            id='peak_velocity_missing_x_vel_column',
-        ),
-        pytest.param(
-            event_properties.peak_velocity,
-            {'velocity_columns': ('x_vel', 'y_vel')},
-            pl.DataFrame(schema={'x_vel': pl.Int64}),
+            {'velocity_column': 'velocity'},
+            pl.DataFrame(schema={'_velocity': pl.Int64}),
             pl.exceptions.ColumnNotFoundError,
-            ('y_vel',),
-            id='peak_velocity_missing_y_vel_column',
+            ('velocity',),
+            id='peak_velocity_missing_velocity_column',
         ),
         pytest.param(
             event_properties.dispersion,
-            {'position_columns': ('x_pos', 'y_pos')},
-            pl.DataFrame(schema={'y_pos': pl.Int64}),
+            {'position_column': 'position'},
+            pl.DataFrame(schema={'_position': pl.Int64}),
             pl.exceptions.ColumnNotFoundError,
-            ('x_pos',),
-            id='dispersion_missing_x_pos_column',
-        ),
-        pytest.param(
-            event_properties.dispersion,
-            {'position_columns': ('x_pos', 'y_pos')},
-            pl.DataFrame(schema={'x_pos': pl.Int64}),
-            pl.exceptions.ColumnNotFoundError,
-            ('y_pos',),
-            id='dispersion_missing_y_pos_column',
+            ('position',),
+            id='dispersion_missing_position_column',
         ),
         pytest.param(
             event_properties.amplitude,
-            {'position_columns': ('x_pos', 'y_pos')},
-            pl.DataFrame(schema={'y_pos': pl.Int64}),
+            {'position_column': 'position'},
+            pl.DataFrame(schema={'_position': pl.Int64}),
             pl.exceptions.ColumnNotFoundError,
-            ('x_pos',),
-            id='amplitude_missing_x_pos_column',
-        ),
-        pytest.param(
-            event_properties.amplitude,
-            {'position_columns': ('x_pos', 'y_pos')},
-            pl.DataFrame(schema={'x_pos': pl.Int64}),
-            pl.exceptions.ColumnNotFoundError,
-            ('y_pos',),
-            id='amplitude_missing_y_pos_column',
+            ('position',),
+            id='amplitude_missing_position_column',
         ),
     ],
 )
@@ -241,8 +147,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.peak_velocity,
             {},
             pl.DataFrame(
-                {'x_vel': [0, 0], 'y_vel': [1, 1]},
-                schema={'x_vel': pl.Float64, 'y_vel': pl.Float64},
+                {'velocity': [[0, 0], [0, 1]]},
+                schema={'velocity': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'peak_velocity': [1]},
@@ -255,8 +161,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.dispersion,
             {},
             pl.DataFrame(
-                {'x_pos': [2], 'y_pos': [3]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[2, 3]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'dispersion': [0]},
@@ -269,8 +175,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.dispersion,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 2], 'y_pos': [0, 0]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 0], [2, 0]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'dispersion': [2]},
@@ -283,8 +189,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.dispersion,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 0], 'y_pos': [0, 3]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 0], [0, 3]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'dispersion': [3]},
@@ -297,8 +203,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.dispersion,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 2], 'y_pos': [0, 3]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 0], [2, 3]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'dispersion': [5]},
@@ -311,8 +217,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.disposition,
             {},
             pl.DataFrame(
-                {'x_pos': [1], 'y_pos': [1]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[2, 1]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'disposition': [0]},
@@ -325,8 +231,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.disposition,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 1], 'y_pos': [0, 0]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 0], [1, 0]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'disposition': [1]},
@@ -339,8 +245,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.disposition,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 0], 'y_pos': [0, 1]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 0], [0, 1]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'disposition': [1]},
@@ -353,8 +259,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.disposition,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 1], 'y_pos': [0, 1]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 0], [1, 1]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'disposition': [np.sqrt(2)]},
@@ -367,8 +273,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.disposition,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 1.1, 1], 'y_pos': [0, 0, 0]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 0], [1.1, 0], [1, 0]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'disposition': [1]},
@@ -381,8 +287,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.disposition,
             {},
             pl.DataFrame(
-                {'x_pos': [-1, 0, 1], 'y_pos': [0, 0, 0]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[-1, 0], [0, 0], [1, 0]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'disposition': [2]},
@@ -395,8 +301,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.amplitude,
             {},
             pl.DataFrame(
-                {'x_pos': [4], 'y_pos': [5]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[4, 5]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'amplitude': [0]},
@@ -409,8 +315,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.amplitude,
             {},
             pl.DataFrame(
-                {'x_pos': [2, 0], 'y_pos': [0, 0]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[2, 0], [0, 0]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'amplitude': [2]},
@@ -423,8 +329,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.amplitude,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 0], 'y_pos': [3, 0]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 3], [0, 0]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'amplitude': [3]},
@@ -436,8 +342,8 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             event_properties.amplitude,
             {},
             pl.DataFrame(
-                {'x_pos': [0, 1], 'y_pos': [0, 1]},
-                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+                {'position': [[0, 0], [1, 1]]},
+                schema={'position': pl.List(pl.Float64)},
             ),
             pl.DataFrame(
                 {'amplitude': [np.sqrt(2)]},