Repository: aeye-lab/pymovements
Pull request #444 (2023-06-08 18:00:27)

feat(gaze): Add column merge arguments to integration functions
This adds the new GazeDataFrame functionality to merge columns into position, velocity, etc, to the integration functions for numpy and pandas

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


| [Impacted Files](https://app.codecov.io/gh/aeye-lab/pymovements/pull/444?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/integration.py](https://app.codecov.io/gh/aeye-lab/pymovements/pull/444?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvaW50ZWdyYXRpb24ucHk=) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/444?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: I personally would like to enforce the used order, so the cases you named are not supported and should raise an exception in the future. This issue is rather complicated and currently has low priority, so I would like to delegate adding these additional test cases.
Feel free to add a PR enforcing component order.

Regarding the second point, I think it makes more sense to handle this in another PR, as what you seem to be talking about  is extending the interface of `GazeDataFrame.__init__()` or `GazeDataFrame.merge_component_columns_into_tuple_column()`, which is out of scope of this PR.
Feel free to add a PR for your feature request.

Patch:
diff --git a/src/pymovements/gaze/integration.py b/src/pymovements/gaze/integration.py
index 5203fd9..611808d 100644
--- a/src/pymovements/gaze/integration.py
+++ b/src/pymovements/gaze/integration.py
@@ -34,6 +34,10 @@ def from_numpy(
         schema: list[str],
         experiment: Experiment | None = None,
         orient: Literal['col', 'row'] = 'col',
+        pixel_columns: list[str] | None = None,
+        position_columns: list[str] | None = None,
+        velocity_columns: list[str] | None = None,
+        acceleration_columns: list[str] | None = None,
 ) -> GazeDataFrame:
     """Construct a :py:class:`~pymovements.gaze.gaze_dataframe.GazeDataFrame`
     from a numpy array.
@@ -48,18 +52,37 @@ def from_numpy(
         Whether to interpret the two-dimensional data as columns or as rows.
     experiment : Experiment
         The experiment definition.
+    pixel_columns:
+        The name of the pixel position columns in the input data frame.
+    position_columns:
+        The name of the dva position columns in the input data frame.
+    velocity_columns:
+        The name of the dva velocity columns in the input data frame.
+    acceleration_columns:
+        The name of the dva acceleration columns in the input data frame.
 
     Returns
     -------
     py:class:`~pymovements.GazeDataFrame`
     """
     df = pl.from_numpy(data=data, schema=schema, orient=orient)
-    return GazeDataFrame(data=df, experiment=experiment)
+    return GazeDataFrame(
+        data=df,
+        experiment=experiment,
+        pixel_columns=pixel_columns,
+        position_columns=position_columns,
+        velocity_columns=velocity_columns,
+        acceleration_columns=acceleration_columns,
+    )
 
 
 def from_pandas(
         data: pd.DataFrame,
         experiment: Experiment | None = None,
+        pixel_columns: list[str] | None = None,
+        position_columns: list[str] | None = None,
+        velocity_columns: list[str] | None = None,
+        acceleration_columns: list[str] | None = None,
 ) -> GazeDataFrame:
     """Construct a :py:class:`~pymovements.gaze.gaze_dataframe.GazeDataFrame`
     from a pandas DataFrame.
@@ -70,10 +93,25 @@ def from_pandas(
         Data represented as a pandas DataFrame.
     experiment : Experiment
         The experiment definition.
+    pixel_columns:
+        The name of the pixel position columns in the input data frame.
+    position_columns:
+        The name of the dva position columns in the input data frame.
+    velocity_columns:
+        The name of the dva velocity columns in the input data frame.
+    acceleration_columns:
+        The name of the dva acceleration columns in the input data frame.
 
     Returns
     -------
     py:class:`~pymovements.GazeDataFrame`
     """
     df = pl.from_pandas(data=data)
-    return GazeDataFrame(data=df, experiment=experiment)
+    return GazeDataFrame(
+        data=df,
+        experiment=experiment,
+        pixel_columns=pixel_columns,
+        position_columns=position_columns,
+        velocity_columns=velocity_columns,
+        acceleration_columns=acceleration_columns,
+    )


Test patch:
diff --git a/tests/gaze/integration_numpy_test.py b/tests/gaze/integration_numpy_test.py
index 1cefdc5..3e5b2c3 100644
--- a/tests/gaze/integration_numpy_test.py
+++ b/tests/gaze/integration_numpy_test.py
@@ -19,6 +19,8 @@
 # SOFTWARE.
 """Test from gaze.from_numpy."""
 import numpy as np
+import polars as pl
+from polars.testing import assert_frame_equal
 
 import pymovements as pm
 
@@ -53,3 +55,42 @@ def test_from_numpy():
 
     assert gaze.frame.shape == (4, 4)
     assert gaze.columns == schema
+
+
+def test_from_pandas_explicit_columns():
+    array = np.array(
+        [
+            [0, 1, 2, 3],
+            [4, 5, 6, 7],
+            [9, 8, 7, 6],
+            [5, 4, 3, 2],
+        ],
+        dtype=np.int64,
+    )
+
+    schema = ['x_pix', 'y_pix', 'x_pos', 'y_pos']
+
+    experiment = pm.Experiment(
+        screen_width_px=1280,
+        screen_height_px=1024,
+        screen_width_cm=38,
+        screen_height_cm=30,
+        distance_cm=68,
+        origin='lower left',
+        sampling_rate=1000.0,
+    )
+
+    gaze = pm.gaze.from_numpy(
+        data=array,
+        schema=schema,
+        experiment=experiment,
+        pixel_columns=['x_pix', 'y_pix'],
+        position_columns=['x_pos', 'y_pos'],
+    )
+
+    expected = pl.DataFrame({
+        'pixel': [[0, 4], [1, 5], [2, 6], [3, 7]],
+        'position': [[9, 5], [8, 4], [7, 3], [6, 2]],
+    })
+
+    assert_frame_equal(gaze.frame, expected)
diff --git a/tests/gaze/integration_pandas_test.py b/tests/gaze/integration_pandas_test.py
index 67bebb9..2d1dc28 100644
--- a/tests/gaze/integration_pandas_test.py
+++ b/tests/gaze/integration_pandas_test.py
@@ -19,6 +19,8 @@
 # SOFTWARE.
 """Test from gaze.from_pandas."""
 import pandas as pd
+import polars as pl
+from polars.testing import assert_frame_equal
 
 import pymovements as pm
 
@@ -50,3 +52,38 @@ def test_from_pandas():
 
     assert gaze.frame.shape == (4, 4)
     assert all(gaze.columns == pandas_df.columns)
+
+
+def test_from_pandas_explicit_columns():
+    pandas_df = pd.DataFrame(
+        {
+            'x_pix': [0, 1, 2, 3],
+            'y_pix': [4, 5, 6, 7],
+            'x_pos': [9, 8, 7, 6],
+            'y_pos': [5, 4, 3, 2],
+        },
+    )
+
+    experiment = pm.Experiment(
+        screen_width_px=1280,
+        screen_height_px=1024,
+        screen_width_cm=38,
+        screen_height_cm=30,
+        distance_cm=68,
+        origin='lower left',
+        sampling_rate=1000.0,
+    )
+
+    gaze = pm.gaze.from_pandas(
+        data=pandas_df,
+        experiment=experiment,
+        pixel_columns=['x_pix', 'y_pix'],
+        position_columns=['x_pos', 'y_pos'],
+    )
+
+    expected = pl.DataFrame({
+        'pixel': [[0, 4], [1, 5], [2, 6], [3, 7]],
+        'position': [[9, 5], [8, 4], [7, 3], [6, 2]],
+    })
+
+    assert_frame_equal(gaze.frame, expected)