Repository: aeye-lab/pymovements
Pull request #651 (2024-01-10 12:11:17)

feat: add trial_columns to from_numpy() and from_pandas()
Somehow they were missing before from the implementation.

signatures are updated to:
```python
def from_numpy(
        data: np.ndarray | None = None,
        experiment: Experiment | None = None,
        events: EventDataFrame | None = None,
        *,
        trial: np.ndarray | None = None,
        time: np.ndarray | None = None,
        pixel: np.ndarray | None = None,
        position: np.ndarray | None = None,
        velocity: np.ndarray | None = None,
        acceleration: np.ndarray | None = None,
        distance: np.ndarray | None = None,
        schema: list[str] | None = None,
        orient: Literal['col', 'row'] = 'col',
        trial_columns: str | list[str] | None = None,
        time_column: str | 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,
        distance_column: str | None = None,
) -> GazeDataFrame:
```
and
```python
def from_pandas(
        data: pd.DataFrame,
        experiment: Experiment | None = None,
        events: EventDataFrame | None = None,
        *,
        trial_columns: str | list[str] | None = None,
        time_column: str | 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,
        distance_column: str | None = None,
) -> GazeDataFrame:
```

needs #650 to be merged first for pydocstyle ignores



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/651?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`b10da5d`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/b10da5d18725191a98e9d46a450981214753a133?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`471f9fa`)](https://app.codecov.io/gh/aeye-lab/pymovements/pull/651?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      #651   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           54        54           
  Lines         2356      2357    +1     
  Branches       588       588           
=========================================
+ Hits          2356      2357    +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/651?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).   
:loudspeaker: Have feedback on the report? [Share it here](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/integration.py b/src/pymovements/gaze/integration.py
index b7f9735..bdd5087 100644
--- a/src/pymovements/gaze/integration.py
+++ b/src/pymovements/gaze/integration.py
@@ -37,6 +37,7 @@ def from_numpy(
         experiment: Experiment | None = None,
         events: EventDataFrame | None = None,
         *,
+        trial: np.ndarray | None = None,
         time: np.ndarray | None = None,
         pixel: np.ndarray | None = None,
         position: np.ndarray | None = None,
@@ -45,6 +46,7 @@ def from_numpy(
         distance: np.ndarray | None = None,
         schema: list[str] | None = None,
         orient: Literal['col', 'row'] = 'col',
+        trial_columns: str | list[str] | None = None,
         time_column: str | None = None,
         pixel_columns: list[str] | None = None,
         position_columns: list[str] | None = None,
@@ -72,6 +74,8 @@ def from_numpy(
         The experiment definition. (default: None)
     events: EventDataFrame | None
         A dataframe of events in the gaze signal. (default: None)
+    trial: np.ndarray | None
+        Array of trial identifiers for each timestep. (default: None)
     time: np.ndarray | None
         Array of timestamps. (default: None)
     pixel: np.ndarray | None
@@ -88,6 +92,11 @@ def from_numpy(
         A list of column names. (default: None)
     orient: Literal['col', 'row']
         Whether to interpret the two-dimensional data as columns or as rows. (default: 'col')
+    trial_columns: str | list[str] | None
+        The name of the trial columns in the input data frame. If the list is empty or None,
+        the input data frame is assumed to contain only one trial. If the list is not empty,
+        the input data frame is assumed to contain multiple trials and the transformation
+        methods will be applied to each trial separately. (default: None)
     time_column: str | None
         The name of the timestamp column in the input data frame. (default: None)
     pixel_columns: list[str] | None
@@ -216,6 +225,7 @@ def from_numpy(
             data=df,
             experiment=experiment,
             events=events,
+            trial_columns=trial_columns,
             time_column=time_column,
             pixel_columns=pixel_columns,
             position_columns=position_columns,
@@ -227,6 +237,12 @@ def from_numpy(
     # Initialize with an empty DataFrame, as every column specifier could be None.
     dfs: list[pl.DataFrame] = [pl.DataFrame()]
 
+    trial_columns = None
+    if trial is not None:
+        df = pl.from_numpy(data=trial, schema=['trial'], orient=orient)
+        dfs.append(df)
+        trial_columns = 'trial'
+
     time_column = None
     if time is not None:
         df = pl.from_numpy(data=time, schema=['time'], orient=orient)
@@ -270,6 +286,7 @@ def from_numpy(
         experiment=experiment,
         events=events,
         time_column=time_column,
+        trial_columns=trial_columns,
         pixel_columns=pixel_columns,
         position_columns=position_columns,
         velocity_columns=velocity_columns,
@@ -283,6 +300,7 @@ def from_pandas(
         experiment: Experiment | None = None,
         events: EventDataFrame | None = None,
         *,
+        trial_columns: str | list[str] | None = None,
         time_column: str | None = None,
         pixel_columns: list[str] | None = None,
         position_columns: list[str] | None = None,
@@ -300,6 +318,11 @@ def from_pandas(
         The experiment definition. (default: None)
     events: EventDataFrame | None
         A dataframe of events in the gaze signal. (default: None)
+    trial_columns: str | list[str] | None
+        The name of the trial columns in the input data frame. If the list is empty or None,
+        the input data frame is assumed to contain only one trial. If the list is not empty,
+        the input data frame is assumed to contain multiple trials and the transformation
+        methods will be applied to each trial separately. (default: None)
     time_column: str | None
         The name of the timestamp column in the input data frame. (default: None)
     pixel_columns: list[str] | None
@@ -326,6 +349,7 @@ def from_pandas(
         data=df,
         experiment=experiment,
         events=events,
+        trial_columns=trial_columns,
         time_column=time_column,
         pixel_columns=pixel_columns,
         position_columns=position_columns,


Test patch:
diff --git a/tests/unit/gaze/integration_numpy_test.py b/tests/unit/gaze/integration_numpy_test.py
index b457882..707b1db 100644
--- a/tests/unit/gaze/integration_numpy_test.py
+++ b/tests/unit/gaze/integration_numpy_test.py
@@ -122,6 +122,56 @@ def test_from_numpy_with_schema():
     assert gaze.n_components == 2
 
 
+def test_from_numpy_with_trial_id():
+    array = np.array(
+        [
+            [1, 1, 2, 2],
+            [101, 102, 103, 104],
+            [0, 1, 2, 3],
+            [4, 5, 6, 7],
+        ],
+        dtype=np.float64,
+    )
+
+    schema = ['trial_id', 't', 'x_pix', 'y_pix']
+
+    experiment = pm.Experiment(
+        screen_width_px=1280,
+        screen_height_px=1024,
+        screen_width_cm=38,
+        screen_height_cm=30,
+        distance_cm=None,
+        origin='lower left',
+        sampling_rate=1000.0,
+    )
+
+    gaze = pm.gaze.from_numpy(
+        data=array,
+        schema=schema,
+        experiment=experiment,
+        trial_columns='trial_id',
+        time_column='t',
+        pixel_columns=['x_pix', 'y_pix'],
+    )
+
+    expected = pl.DataFrame(
+        {
+            'trial_id': [1, 1, 2, 2],
+            'time': [101, 102, 103, 104],
+            'pixel': [[0, 4], [1, 5], [2, 6], [3, 7]],
+        },
+        schema={
+            'trial_id': pl.Float64,
+            'time': pl.Float64,
+            'pixel': pl.List(pl.Float64),
+        },
+    )
+
+    assert_frame_equal(gaze.frame, expected)
+    assert gaze.n_components == 2
+    assert gaze.trial_columns == 'trial_id'
+
+
 def test_from_numpy_explicit_columns():
     time = np.array([101, 102, 103, 104], dtype=np.int64)
     distance = np.array([100, 100, 100, 100], dtype=np.float64)
@@ -173,6 +223,35 @@ def test_from_numpy_explicit_columns():
     assert gaze.n_components == 2
 
 
+def test_from_numpy_explicit_columns_with_trial():
+    trial = np.array([1, 1, 2, 2], dtype=np.int64)
+    time = np.array([101, 102, 103, 104], dtype=np.int64)
+    pixel = np.array([[0, 1, 2, 3], [4, 5, 6, 7]], dtype=np.int64)
+
+    gaze = pm.gaze.from_numpy(
+        trial=trial,
+        time=time,
+        pixel=pixel,
+    )
+
+    expected = pl.DataFrame(
+        {
+            'trial': [1, 1, 2, 2],
+            'time': [101, 102, 103, 104],
+            'pixel': [[0, 4], [1, 5], [2, 6], [3, 7]],
+        },
+        schema={
+            'trial': pl.Int64,
+            'time': pl.Int64,
+            'pixel': pl.List(pl.Int64),
+        },
+    )
+
+    assert_frame_equal(gaze.frame, expected)
+    assert gaze.n_components == 2
+    assert gaze.trial_columns == 'trial'
+
+
 def test_from_numpy_all_none():
     gaze = pm.gaze.from_numpy(
         data=None,
diff --git a/tests/unit/gaze/integration_pandas_test.py b/tests/unit/gaze/integration_pandas_test.py
index b76d005..e10abdf 100644
--- a/tests/unit/gaze/integration_pandas_test.py
+++ b/tests/unit/gaze/integration_pandas_test.py
@@ -50,6 +50,8 @@ def test_from_pandas():
 def test_from_pandas_explicit_columns():
     pandas_df = pd.DataFrame(
         {
+            't': [101, 102, 103, 104],
+            'd': [100, 100, 100, 100],
             'x_pix': [0, 1, 2, 3],
             'y_pix': [4, 5, 6, 7],
             'x_pos': [9, 8, 7, 6],
@@ -62,11 +64,15 @@ def test_from_pandas_explicit_columns():
     gaze = pm.gaze.from_pandas(
         data=pandas_df,
         experiment=experiment,
+        time_column='t',
+        distance_column='d',
         pixel_columns=['x_pix', 'y_pix'],
         position_columns=['x_pos', 'y_pos'],
     )
 
     expected = pl.DataFrame({
+        'time': [101, 102, 103, 104],
+        'distance': [100, 100, 100, 100],
         'pixel': [[0, 4], [1, 5], [2, 6], [3, 7]],
         'position': [[9, 5], [8, 4], [7, 3], [6, 2]],
     })
@@ -74,6 +80,36 @@ def test_from_pandas_explicit_columns():
     assert_frame_equal(gaze.frame, expected)
 
 
+def test_from_pandas_with_trial_columnms():
+    pandas_df = pd.DataFrame(
+        {
+            'trial_id': [1, 1, 2, 2],
+            't': [101, 102, 103, 104],
+            'x_pix': [0, 1, 2, 3],
+            'y_pix': [4, 5, 6, 7],
+        },
+    )
+
+    experiment = pm.Experiment(1280, 1024, 38, 30, 68, 'lower left', 1000.0)
+
+    gaze = pm.gaze.from_pandas(
+        data=pandas_df,
+        experiment=experiment,
+        trial_columns='trial_id',
+        time_column='t',
+        pixel_columns=['x_pix', 'y_pix'],
+    )
+
+    expected = pl.DataFrame({
+        'trial_id': [1, 1, 2, 2],
+        'time': [101, 102, 103, 104],
+        'pixel': [[0, 4], [1, 5], [2, 6], [3, 7]],
+    })
+
+    assert_frame_equal(gaze.frame, expected)
+    assert gaze.trial_columns == 'trial_id'
+
+
 @pytest.mark.parametrize(
     ('df', 'events'),
     [