Repository: aeye-lab/pymovements
Pull request #441 (2023-06-07 12:48:16)

feat(gaze): Add explode() method to GazeDataFrame
This adds very rudimentary functionality to explode a list column into columns for each component.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/441?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 [(`b370d51`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/b370d51a82b4065ea0a9b1703022ad78551b2eb0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`f7073d0`)](https://app.codecov.io/gh/aeye-lab/pymovements/pull/441?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      #441   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           55        55           
  Lines         2022      2024    +2     
  Branches       461       462    +1     
=========================================
+ Hits          2022      2024    +2     
```


| [Impacted Files](https://app.codecov.io/gh/aeye-lab/pymovements/pull/441?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/gaze\_dataframe.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/441?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%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/441?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/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py
index b3b3861..ce9b66e 100644
--- a/src/pymovements/gaze/gaze_dataframe.py
+++ b/src/pymovements/gaze/gaze_dataframe.py
@@ -389,6 +389,29 @@ class GazeDataFrame:
             .alias(output_column),
         ).drop(input_columns)
 
+    def explode(
+            self,
+            column: str,
+            output_columns: list[str],
+    ) -> None:
+        """Explode a column of type ``pl.List`` into one column for each list component.
+
+        The input column will be dropped.
+
+        Parameters
+        ----------
+        column:
+            Name of input columns to be exploded into several component columns.
+        output_columns:
+            Name of the resulting tuple columns.
+        """
+        self.frame = self.frame.with_columns(
+            [
+                pl.col(column).list.get(component_id).alias(output_column)
+                for component_id, output_column in enumerate(output_columns)
+            ],
+        ).drop(column)
+
     @staticmethod
     def _pixel_to_dva_position_columns(columns: list[str]) -> list[str]:
         """Get corresponding dva position columns from pixel position columns."""


Test patch:
diff --git a/tests/gaze/gaze_dataframe_explode_test.py b/tests/gaze/gaze_dataframe_explode_test.py
new file mode 100644
index 0000000..e43d345
--- /dev/null
+++ b/tests/gaze/gaze_dataframe_explode_test.py
@@ -0,0 +1,101 @@
+# 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.explode()."""
+import polars as pl
+import pytest
+from polars.testing import assert_frame_equal
+
+import pymovements as pm
+
+
+@pytest.mark.parametrize(
+    ('init_data', 'explode_kwargs', 'expected'),
+    [
+        pytest.param(
+            pl.DataFrame(schema={'pixel': pl.List(pl.Float64)}),
+            {'column': 'pixel', 'output_columns': ['x', 'y']},
+            pl.DataFrame(schema={'x': pl.Float64, 'y': pl.Float64}),
+            id='empty_df_with_schema_two_pixel_columns',
+        ),
+
+        pytest.param(
+            pl.DataFrame(schema={'abc': pl.Int64, 'pixel': pl.List(pl.Float64)}),
+            {'column': 'pixel', 'output_columns': ['x', 'y']},
+            pl.DataFrame(schema={'abc': pl.Int64, 'x': pl.Float64, 'y': pl.Float64}),
+            id='empty_df_with_three_column_schema_two_pixel_columns',
+        ),
+
+        pytest.param(
+            pl.DataFrame(schema={'pixel': pl.List(pl.Float64)}),
+            {'column': 'pixel', 'output_columns': ['xl', 'yl', 'xr', 'yr']},
+            pl.DataFrame(
+                schema={
+                    'xl': pl.Float64, 'yl': pl.Float64, 'xr': pl.Float64, 'yr': pl.Float64,
+                },
+            ),
+            id='empty_df_with_schema_four_pixel_columns',
+        ),
+
+        pytest.param(
+            pl.DataFrame(schema={'pixel': pl.List(pl.Float64)}),
+            {'column': 'pixel', 'output_columns': ['xl', 'yl', 'xr', 'yr', 'xa', 'ya']},
+            pl.DataFrame(
+                schema={
+                    'xl': pl.Float64, 'yl': pl.Float64,
+                    'xr': pl.Float64, 'yr': pl.Float64,
+                    'xa': pl.Float64, 'ya': pl.Float64,
+                },
+            ),
+            id='empty_df_with_schema_six_pixel_columns',
+        ),
+
+        pytest.param(
+            pl.DataFrame({'pixel': [[1.23, 4.56]]}),
+            {'column': 'pixel', 'output_columns': ['x', 'y']},
+            pl.DataFrame({'x': [1.23], 'y': [4.56]}),
+            id='df_single_row_two_pixel_columns',
+        ),
+
+        pytest.param(
+            pl.DataFrame({'abc': [1], 'pixel': [[1.23, 4.56]]}),
+            {'column': 'pixel', 'output_columns': ['x', 'y']},
+            pl.DataFrame({'abc': [1], 'x': [1.23], 'y': [4.56]}),
+            id='df_single_row_three_columns_two_pixel_columns',
+        ),
+
+        pytest.param(
+            pl.DataFrame({'pixel': [[1.2, 3.4, 5.6, 7.8]]}),
+            {'column': 'pixel', 'output_columns': ['xl', 'yl', 'xr', 'yr']},
+            pl.DataFrame({'xl': [1.2], 'yl': [3.4], 'xr': [5.6], 'yr': [7.8]}),
+            id='df_single_row_four_pixel_columns',
+        ),
+
+        pytest.param(
+            pl.DataFrame({'pixel': [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]}),
+            {'column': 'pixel', 'output_columns': ['xl', 'yl', 'xr', 'yr', 'xa', 'ya']},
+            pl.DataFrame({'xl': [.1], 'yl': [.2], 'xr': [.3], 'yr': [.4], 'xa': [.5], 'ya': [.6]}),
+            id='df_single_row_six_pixel_columns',
+        ),
+    ],
+)
+def test_gaze_dataframe_explode_has_expected_frame(init_data, explode_kwargs, expected):
+    gaze = pm.GazeDataFrame(init_data)
+    gaze.explode(**explode_kwargs)
+    assert_frame_equal(gaze.frame, expected)