Repository: aeye-lab/pymovements
Pull request #334 (2023-04-26 13:02:16)

feat(gaze): Add gaze.from_numpy and gaze.from_pandas

codecov-commenter: ## [Codecov](https://codecov.io/gh/aeye-lab/pymovements/pull/334?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 [(`863d7d5`)](https://codecov.io/gh/aeye-lab/pymovements/commit/863d7d532fd027637bb7e54e070d7a42a978cb07?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`9f41154`)](https://codecov.io/gh/aeye-lab/pymovements/pull/334?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00%.

> :exclamation: Current head 9f41154 differs from pull request most recent head 7b840ab. Consider uploading reports for the commit 7b840ab to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #334   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           42        44    +2     
  Lines         1603      1624   +21     
  Branches       354       354           
=========================================
+ Hits          1603      1624   +21     
```


| [Impacted Files](https://codecov.io/gh/aeye-lab/pymovements/pull/334?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/\_\_init\_\_.py](https://codecov.io/gh/aeye-lab/pymovements/pull/334?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/gaze/from\_numpy.py](https://codecov.io/gh/aeye-lab/pymovements/pull/334?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvZnJvbV9udW1weS5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/pymovements/gaze/from\_pandas.py](https://codecov.io/gh/aeye-lab/pymovements/pull/334?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL2dhemUvZnJvbV9wYW5kYXMucHk=) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/aeye-lab/pymovements/pull/334?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/__init__.py b/src/pymovements/gaze/__init__.py
index 27cd934..44ae454 100644
--- a/src/pymovements/gaze/__init__.py
+++ b/src/pymovements/gaze/__init__.py
@@ -43,12 +43,16 @@
 """
 from pymovements.gaze import transforms
 from pymovements.gaze.experiment import Experiment
+from pymovements.gaze.from_numpy import from_numpy
+from pymovements.gaze.from_pandas import from_pandas
 from pymovements.gaze.gaze_dataframe import GazeDataFrame
 from pymovements.gaze.screen import Screen
 
 
 __all__ = [
     'Experiment',
+    'from_numpy',
+    'from_pandas',
     'GazeDataFrame',
     'Screen',
     'transforms',
diff --git a/src/pymovements/gaze/from_numpy.py b/src/pymovements/gaze/from_numpy.py
new file mode 100644
index 0000000..61a65d2
--- /dev/null
+++ b/src/pymovements/gaze/from_numpy.py
@@ -0,0 +1,56 @@
+# 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.
+"""Module to create a GazeDataFrame from a numpy array."""
+from __future__ import annotations
+
+import numpy as np
+import polars as pl
+from typing_extensions import Literal
+
+from pymovements.gaze.experiment import Experiment
+from pymovements.gaze.gaze_dataframe import GazeDataFrame
+
+
+def from_numpy(
+        data: np.ndarray,
+        schema: list[str],
+        experiment: Experiment | None = None,
+        orient: Literal['col', 'row'] = 'col',
+) -> GazeDataFrame:
+    """Construct a :py:class:`~pymovements.gaze.gaze_dataframe.GazeDataFrame`
+    from a numpy array.
+
+    Parameters
+    ----------
+    data:
+        Two-dimensional data represented as a numpy ndarray.
+    schema:
+        A list of column names.
+    orient:
+        Whether to interpret the two-dimensional data as columns or as rows.
+    experiment : Experiment
+        The experiment definition.
+
+    Returns
+    -------
+    py:class:`~pymovements.GazeDataFrame`
+    """
+    df = pl.from_numpy(data=data, schema=schema, orient=orient)
+    return GazeDataFrame(data=df, experiment=experiment)
diff --git a/src/pymovements/gaze/from_pandas.py b/src/pymovements/gaze/from_pandas.py
new file mode 100644
index 0000000..b144922
--- /dev/null
+++ b/src/pymovements/gaze/from_pandas.py
@@ -0,0 +1,49 @@
+# 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.
+"""Module to create a GazeDataFrame from a numpy array."""
+from __future__ import annotations
+
+import pandas as pd
+import polars as pl
+
+from pymovements.gaze.experiment import Experiment
+from pymovements.gaze.gaze_dataframe import GazeDataFrame
+
+
+def from_pandas(
+        data: pd.DataFrame,
+        experiment: Experiment | None = None,
+) -> GazeDataFrame:
+    """Construct a :py:class:`~pymovements.gaze.gaze_dataframe.GazeDataFrame`
+    from a pandas DataFrame.
+
+    Parameters
+    ----------
+    data:
+        Data represented as a pandas DataFrame.
+    experiment : Experiment
+        The experiment definition.
+
+    Returns
+    -------
+    py:class:`~pymovements.GazeDataFrame`
+    """
+    df = pl.from_pandas(data=data)
+    return GazeDataFrame(data=df, experiment=experiment)
diff --git a/tox.ini b/tox.ini
index 9191247..fa22f03 100644
--- a/tox.ini
+++ b/tox.ini
@@ -86,6 +86,7 @@ changedir = {toxinidir}
 deps =
     mypy
     .[test]
+    pandas-stubs
     types-all
 commands =
     mypy {toxinidir}/src


Test patch:
diff --git a/tests/gaze/from_numpy_test.py b/tests/gaze/from_numpy_test.py
new file mode 100644
index 0000000..1cefdc5
--- /dev/null
+++ b/tests/gaze/from_numpy_test.py
@@ -0,0 +1,55 @@
+# 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 from gaze.from_numpy."""
+import numpy as np
+
+import pymovements as pm
+
+
+def test_from_numpy():
+    array = np.array(
+        [
+            [0, 1, 2, 3],
+            [0, 1, 2, 3],
+            [0, 1, 2, 3],
+            [0, 1, 2, 3],
+        ],
+    )
+
+    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,
+    )
+
+    assert gaze.frame.shape == (4, 4)
+    assert gaze.columns == schema
diff --git a/tests/gaze/from_pandas_test.py b/tests/gaze/from_pandas_test.py
new file mode 100644
index 0000000..67bebb9
--- /dev/null
+++ b/tests/gaze/from_pandas_test.py
@@ -0,0 +1,52 @@
+# 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 from gaze.from_pandas."""
+import pandas as pd
+
+import pymovements as pm
+
+
+def test_from_pandas():
+    pandas_df = pd.DataFrame(
+        {
+            'x_pix': [0, 1, 2, 3],
+            'y_pix': [0, 1, 2, 3],
+            'x_pos': [0, 1, 2, 3],
+            'y_pos': [0, 1, 2, 3],
+        },
+    )
+
+    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,
+    )
+
+    assert gaze.frame.shape == (4, 4)
+    assert all(gaze.columns == pandas_df.columns)