Repository: aeye-lab/pymovements
Pull request #419 (2023-05-18 17:22:20)

feat(events): Add fixation location event property
since I had the property locally, first attempt at an implmentation for the package. feedback welcome

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/419?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 [(`44bd0c0`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/44bd0c0bdb8f2837594e3f83436ad23cdb2ad5df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`d01379e`)](https://app.codecov.io/gh/aeye-lab/pymovements/pull/419?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      #419   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           55        55           
  Lines         1896      1918   +22     
  Branches       419       427    +8     
=========================================
+ Hits          1896      1918   +22     
```


| [Impacted Files](https://app.codecov.io/gh/aeye-lab/pymovements/pull/419?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\_properties.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/419?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/419?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).

dkrako: Thanks again for your work, looks great! 

But I think you have missed the review comment about merging the two functions using a new `component` argument.

Also, move the merged function above the two private module functions `_check_position_columns` and `_check_velocity_columns`.
SiQube: the column names stay the same then unfortunately, can you give me some pointers how I can change the column names for x/y? 
dkrako: I'm not completely sure what you mean. Do you mean the output or the input columns?

You can get the input column by using the `component` argument as the index of the `position_columns` tuple.
The output columns will be then the same as the input columns actually.

But maybe you refer to the output columns in some test cases?
These are assigned using the `alias()` method, see for example here:
https://github.com/aeye-lab/pymovements/blob/9e175a3313eb604951e7ecf0659b70f63bc5bb6d/tests/events/event_properties_test.py#L536

I am still thinking about how to integrate this well with the coming `apply()` method so don't worry about the output column name for now.
SiQube: I've added it  -- but now we have just one column in the `result_df`, namely `fixation_centroid` and I don't know how to adjust it given the component.
dkrako: LGTM!

The issue with the output column names will be resolved by the implementation of the apply method.

Patch:
diff --git a/src/pymovements/events/event_properties.py b/src/pymovements/events/event_properties.py
index ac2526b..f8d8b12 100644
--- a/src/pymovements/events/event_properties.py
+++ b/src/pymovements/events/event_properties.py
@@ -141,6 +141,65 @@ def disposition(position_columns: tuple[str, str] = ('x_pos', 'y_pos')) -> pl.Ex
     ).sqrt()
 
 
+@register_event_property
+def fixation_centroid(
+        component: str = 'x',
+        method: str = 'mean',
+        position_columns: tuple[str, str] = ('x_pos', 'y_pos'),
+) -> pl.Expr:
+    """Calculate a fixations x centroid.
+
+    Parameters
+    ----------
+    component
+        Specify the component of which to calculate the  centroid.
+        Supported components: 'x', 'y'.
+        Defaults to 'x'.
+    method
+        Specify the type of centroid to be extracted, supported methods: 'mean', 'median'.
+        Defaults to 'mean'.
+    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.
+    ValueError
+        If component is not one of the supported methods.
+    ValueError
+        If method is not one of the supported methods.
+    """
+    _check_position_columns(position_columns)
+
+    component_to_pos = {
+        'x': 0,
+        'y': 1,
+    }
+
+    if component not in component_to_pos:
+        raise ValueError(
+            f'Component {component} not supported. '
+            f"Please choose one of the following: ['x', 'y'].",
+        )
+
+    position = pl.col(position_columns[component_to_pos[component]])
+    if method not in ['mean', 'median']:
+        raise ValueError(
+            f'Method {method} not supported. '
+            f"Please choose one of the following: ['mean', 'median'].",
+        )
+
+    if method == 'mean':
+        centroid = position.mean()
+
+    if method == 'median':
+        centroid = position.median()
+
+    return centroid
+
+
 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):


Test patch:
diff --git a/tests/events/event_properties_test.py b/tests/events/event_properties_test.py
index 6b65f7a..dad58b9 100644
--- a/tests/events/event_properties_test.py
+++ b/tests/events/event_properties_test.py
@@ -107,6 +107,20 @@ from pymovements.events.event_properties import EVENT_PROPERTIES
             ('position_columns', 'type', 'must', 'tuple', 'str', 'int'),
             id='amplitude_tuple_of_int',
         ),
+        pytest.param(
+            event_properties.fixation_centroid,
+            {'method': 'foo'},
+            ValueError,
+            ('method', 'foo', 'not', 'supported', 'mean', 'median'),
+            id='fixation_centroid_value_error_method',
+        ),
+        pytest.param(
+            event_properties.fixation_centroid,
+            {'component': 'foo'},
+            ValueError,
+            ('component', 'foo', 'not', 'supported', 'x', 'y'),
+            id='fixation_centroid_value_error_component',
+        ),
     ],
 )
 def test_property_init_exceptions(event_property, init_kwargs, exception, msg_substrings):
@@ -425,7 +439,6 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             ),
             id='amplitude_two_samples_y_move',
         ),
-
         pytest.param(
             event_properties.amplitude,
             {},
@@ -439,6 +452,84 @@ def test_property_exceptions(event_property, init_kwargs, input_df, exception, m
             ),
             id='amplitude_two_samples_xy_move',
         ),
+        pytest.param(
+            event_properties.fixation_centroid,
+            {'component': 'x'},
+            pl.DataFrame(
+                {'x_pos': [0, 1], 'y_pos': [0, 0]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'fixation_centroid': [0.5]},
+                schema={'fixation_centroid': pl.Float64},
+            ),
+            id='fixation_centroid_x_two_samples',
+        ),
+        pytest.param(
+            event_properties.fixation_centroid,
+            {'component': 'x'},
+            pl.DataFrame(
+                {'x_pos': [0, 1, 3], 'y_pos': [0, 0, 0]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'fixation_centroid': [1.333333]},
+                schema={'fixation_centroid': pl.Float64},
+            ),
+            id='fixation_centroid_x_three_samples_float',
+        ),
+        pytest.param(
+            event_properties.fixation_centroid,
+            {'method': 'median', 'component': 'x'},
+            pl.DataFrame(
+                {'x_pos': [0, 1, 3], 'y_pos': [0, 2, 3]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'fixation_centroid': [1]},
+                schema={'fixation_centroid': pl.Float64},
+            ),
+            id='fixation_centroid_x_median',
+        ),
+        pytest.param(
+            event_properties.fixation_centroid,
+            {'component': 'y'},
+            pl.DataFrame(
+                {'x_pos': [0, 1], 'y_pos': [0, 5]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'fixation_centroid': [2.5]},
+                schema={'fixation_centroid': pl.Float64},
+            ),
+            id='fixation_centroid_y_two_samples',
+        ),
+        pytest.param(
+            event_properties.fixation_centroid,
+            {'component': 'y'},
+            pl.DataFrame(
+                {'x_pos': [0, 0, 0], 'y_pos': [0, 2, 4]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'fixation_centroid': [2]},
+                schema={'fixation_centroid': pl.Float64},
+            ),
+            id='fixation_centroid_y_three_samples_float',
+        ),
+        pytest.param(
+            event_properties.fixation_centroid,
+            {'method': 'median', 'component': 'y'},
+            pl.DataFrame(
+                {'x_pos': [0, 1, 3], 'y_pos': [0, 2, 3]},
+                schema={'x_pos': pl.Float64, 'y_pos': pl.Float64},
+            ),
+            pl.DataFrame(
+                {'fixation_centroid': [2]},
+                schema={'fixation_centroid': pl.Float64},
+            ),
+            id='fixation_centroid_y_median',
+        ),
     ],
 )
 def test_property_has_expected_result(event_property, init_kwargs, input_df, expected_df):
@@ -455,6 +546,11 @@ def test_property_has_expected_result(event_property, init_kwargs, input_df, exp
         pytest.param(event_properties.dispersion, 'dispersion', id='dispersion'),
         pytest.param(event_properties.amplitude, 'amplitude', id='amplitude'),
         pytest.param(event_properties.disposition, 'disposition', id='disposition'),
+        pytest.param(
+            event_properties.fixation_centroid,
+            'fixation_centroid',
+            id='fixation_centroid',
+        ),
     ],
 )
 def test_property_registered(property_function, property_function_name):