Repository: aeye-lab/pymovements
Pull request #272 (2023-03-10 11:58:56)

Datasets: Parse "-32768.00" as null value in ToyDataset


Patch:
diff --git a/src/pymovements/datasets/toy_dataset.py b/src/pymovements/datasets/toy_dataset.py
index a006bf4..af9d9b9 100644
--- a/src/pymovements/datasets/toy_dataset.py
+++ b/src/pymovements/datasets/toy_dataset.py
@@ -89,6 +89,7 @@ class ToyDataset(PublicDataset):
         'sep': '\t',
         'columns': list(_column_map.keys()),
         'new_columns': list(_column_map.values()),
+        'null_values': '-32768.00',
     }
 
     def __init__(
diff --git a/src/pymovements/events/event_properties.py b/src/pymovements/events/event_properties.py
index ddee9ae..ac2526b 100644
--- a/src/pymovements/events/event_properties.py
+++ b/src/pymovements/events/event_properties.py
@@ -58,25 +58,12 @@ def peak_velocity(velocity_columns: tuple[str, str] = ('x_vel', 'y_vel')) -> pl.
         If velocity_columns not of type tuple, velocity_columns not of length 2, or elements of
         velocity_columns not of type 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__}]',
-        )
+    _check_velocity_columns(velocity_columns)
 
-    x_velocity = velocity_columns[0]
-    y_velocity = velocity_columns[1]
+    x_velocity = pl.col(velocity_columns[0])
+    y_velocity = pl.col(velocity_columns[1])
 
-    return (pl.col(x_velocity).pow(2) + pl.col(y_velocity).pow(2)).sqrt().max()
+    return (x_velocity.pow(2) + y_velocity.pow(2)).sqrt().max()
 
 
 @register_event_property
@@ -94,6 +81,68 @@ def dispersion(position_columns: tuple[str, str] = ('x_pos', 'y_pos')) -> pl.Exp
         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)
+
+    x_position = pl.col(position_columns[0])
+    y_position = pl.col(position_columns[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.
+
+    Parameters
+    ----------
+    position_columns
+        The column names of the pitch and yaw position components.
+
+    Raises
+    ------
+    TypeError
+        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)
+
+    x_position = pl.col(position_columns[0])
+    y_position = pl.col(position_columns[1])
+
+    return (
+        (x_position.max() - x_position.min()).pow(2)
+        + (y_position.max() - y_position.min()).pow(2)
+    ).sqrt()
+
+
+@register_event_property
+def disposition(position_columns: tuple[str, str] = ('x_pos', 'y_pos')) -> pl.Expr:
+    """Disposition of an event.
+
+    Parameters
+    ----------
+    position_columns
+        The column names of the pitch and yaw position components.
+
+    Raises
+    ------
+    TypeError
+        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)
+
+    x_position = pl.col(position_columns[0])
+    y_position = pl.col(position_columns[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()
+
+
+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]'
@@ -109,10 +158,20 @@ def dispersion(position_columns: tuple[str, str] = ('x_pos', 'y_pos')) -> pl.Exp
             f'tuple[{type(position_columns[0]).__name__}, {type(position_columns[1]).__name__}]',
         )
 
-    x_position = position_columns[0]
-    y_position = position_columns[1]
 
-    return (
-        (pl.col(x_position).max() - pl.col(x_position).min())
-        + (pl.col(y_position).max() - pl.col(y_position).min())
-    )
+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__}]',
+        )


Test patch:
diff --git a/tests/events/event_properties_test.py b/tests/events/event_properties_test.py
index 0b70b2c..6b65f7a 100644
--- a/tests/events/event_properties_test.py
+++ b/tests/events/event_properties_test.py
@@ -18,6 +18,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 """Test module pymovements.events.event_properties"""
+import numpy as np
 import polars as pl
 import pytest
 from polars.testing import assert_frame_equal
@@ -85,6 +86,27 @@ from pymovements.events.event_properties import EVENT_PROPERTIES
             ('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',
+        ),
     ],
 )
 def test_property_init_exceptions(event_property, init_kwargs, exception, msg_substrings):
@@ -115,21 +137,53 @@ def test_property_init_exceptions(event_property, init_kwargs, exception, msg_su
             ('onset',),
             id='duration_missing_onset_column',
         ),
+        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}),
             pl.exceptions.ColumnNotFoundError,
             ('y_vel',),
-            id='peak_velocity_missing_velocity_column',
+            id='peak_velocity_missing_y_vel_column',
         ),
         pytest.param(
             event_properties.dispersion,
             {'position_columns': ('x_pos', 'y_pos')},
+            pl.DataFrame(schema={'y_pos': 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',
+        ),
+        pytest.param(
+            event_properties.amplitude,
+            {'position_columns': ('x_pos', 'y_pos')},
+            pl.DataFrame(schema={'y_pos': 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='dispersion_missing_position_column',
+            id='amplitude_missing_y_pos_column',
         ),
     ],
 )
@@ -153,6 +207,7 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             pl.DataFrame(schema={'duration': pl.Int64}),
             id='empty_dataframe_results_in_empty_dataframe_with_correct_schema',
         ),
+
         pytest.param(
             event_properties.duration,
             {},
@@ -160,6 +215,7 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             pl.DataFrame({'duration': 1}, schema={'duration': pl.Int64}),
             id='single_event_duration',
         ),
+
         pytest.param(
             event_properties.duration,
             {},
@@ -173,6 +229,7 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             ),
             id='two_events_different_durations',
         ),
+
         pytest.param(
             event_properties.peak_velocity,
             {},
@@ -186,6 +243,49 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             ),
             id='single_event_peak_velocity',
         ),
+
+        pytest.param(
+            event_properties.dispersion,
+            {},
+            pl.DataFrame(
+                {'x_pos': [2], 'y_pos': [3]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'dispersion': [0]},
+                schema={'dispersion': pl.Float64},
+            ),
+            id='dispersion_one_sample',
+        ),
+
+        pytest.param(
+            event_properties.dispersion,
+            {},
+            pl.DataFrame(
+                {'x_pos': [0, 2], 'y_pos': [0, 0]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'dispersion': [2]},
+                schema={'dispersion': pl.Float64},
+            ),
+            id='dispersion_two_samples_x_move',
+        ),
+
+        pytest.param(
+            event_properties.dispersion,
+            {},
+            pl.DataFrame(
+                {'x_pos': [0, 0], 'y_pos': [0, 3]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'dispersion': [3]},
+                schema={'dispersion': pl.Float64},
+            ),
+            id='dispersion_two_samples_y_move',
+        ),
+
         pytest.param(
             event_properties.dispersion,
             {},
@@ -197,7 +297,147 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
                 {'dispersion': [5]},
                 schema={'dispersion': pl.Float64},
             ),
-            id='single_event_dispersion',
+            id='dispersion_two_samples_xy_move',
+        ),
+
+        pytest.param(
+            event_properties.disposition,
+            {},
+            pl.DataFrame(
+                {'x_pos': [1], 'y_pos': [1]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'disposition': [0]},
+                schema={'disposition': pl.Float64},
+            ),
+            id='disposition_single_sample',
+        ),
+
+        pytest.param(
+            event_properties.disposition,
+            {},
+            pl.DataFrame(
+                {'x_pos': [0, 1], 'y_pos': [0, 0]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'disposition': [1]},
+                schema={'disposition': pl.Float64},
+            ),
+            id='disposition_two_samples_x_move',
+        ),
+
+        pytest.param(
+            event_properties.disposition,
+            {},
+            pl.DataFrame(
+                {'x_pos': [0, 0], 'y_pos': [0, 1]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'disposition': [1]},
+                schema={'disposition': pl.Float64},
+            ),
+            id='disposition_two_samples_y_move',
+        ),
+
+        pytest.param(
+            event_properties.disposition,
+            {},
+            pl.DataFrame(
+                {'x_pos': [0, 1], 'y_pos': [0, 1]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'disposition': [np.sqrt(2)]},
+                schema={'disposition': pl.Float64},
+            ),
+            id='disposition_two_samples_xy_move',
+        ),
+
+        pytest.param(
+            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},
+            ),
+            pl.DataFrame(
+                {'disposition': [1]},
+                schema={'disposition': pl.Float64},
+            ),
+            id='disposition_three_samples_overshoot',
+        ),
+
+        pytest.param(
+            event_properties.disposition,
+            {},
+            pl.DataFrame(
+                {'x_pos': [-1, 0, 1], 'y_pos': [0, 0, 0]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'disposition': [2]},
+                schema={'disposition': pl.Float64},
+            ),
+            id='disposition_three_samples_negative',
+        ),
+
+        pytest.param(
+            event_properties.amplitude,
+            {},
+            pl.DataFrame(
+                {'x_pos': [4], 'y_pos': [5]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'amplitude': [0]},
+                schema={'amplitude': pl.Float64},
+            ),
+            id='amplitude_one_sample',
+        ),
+
+        pytest.param(
+            event_properties.amplitude,
+            {},
+            pl.DataFrame(
+                {'x_pos': [2, 0], 'y_pos': [0, 0]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'amplitude': [2]},
+                schema={'amplitude': pl.Float64},
+            ),
+            id='amplitude_two_samples_x_move',
+        ),
+
+        pytest.param(
+            event_properties.amplitude,
+            {},
+            pl.DataFrame(
+                {'x_pos': [0, 0], 'y_pos': [3, 0]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'amplitude': [3]},
+                schema={'amplitude': pl.Float64},
+            ),
+            id='amplitude_two_samples_y_move',
+        ),
+
+        pytest.param(
+            event_properties.amplitude,
+            {},
+            pl.DataFrame(
+                {'x_pos': [0, 1], 'y_pos': [0, 1]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'amplitude': [np.sqrt(2)]},
+                schema={'amplitude': pl.Float64},
+            ),
+            id='amplitude_two_samples_xy_move',
         ),
     ],
 )
@@ -212,8 +452,17 @@ def test_property_has_expected_result(event_property, init_kwargs, input_df, exp
     [
         pytest.param(event_properties.duration, 'duration', id='duration'),
         pytest.param(event_properties.peak_velocity, 'peak_velocity', id='peak_velocity'),
+        pytest.param(event_properties.dispersion, 'dispersion', id='dispersion'),
+        pytest.param(event_properties.amplitude, 'amplitude', id='amplitude'),
+        pytest.param(event_properties.disposition, 'disposition', id='disposition'),
     ],
 )
 def test_property_registered(property_function, property_function_name):
     assert property_function_name in EVENT_PROPERTIES
     assert EVENT_PROPERTIES[property_function_name] == property_function
+    assert EVENT_PROPERTIES[property_function_name].__name__ == property_function_name
+
+
+@pytest.mark.parametrize('property_function', EVENT_PROPERTIES.values())
+def test_property_returns_polars_expression(property_function):
+    assert isinstance(property_function(), pl.Expr)