diff --git a/src/pymovements/__init__.py b/src/pymovements/__init__.py index fd52b4827..d7591775a 100644 --- a/src/pymovements/__init__.py +++ b/src/pymovements/__init__.py @@ -21,6 +21,7 @@ from pymovements import _version from pymovements import datasets from pymovements import events +from pymovements import exceptions from pymovements import gaze from pymovements import plotting from pymovements import synthetic @@ -56,6 +57,7 @@ 'Screen', 'GazeDataFrame', + 'exceptions', 'plotting', 'synthetic', 'utils', diff --git a/src/pymovements/events/processing.py b/src/pymovements/events/processing.py index 15a51bf86..5f8b57970 100644 --- a/src/pymovements/events/processing.py +++ b/src/pymovements/events/processing.py @@ -26,10 +26,10 @@ import polars as pl +import pymovements as pm # pylint: disable=cyclic-import from pymovements.events.frame import EventDataFrame from pymovements.events.properties import EVENT_PROPERTIES from pymovements.exceptions import InvalidProperty -from pymovements.gaze.gaze_dataframe import GazeDataFrame class EventProcessor: @@ -140,7 +140,7 @@ def __init__( def process( self, events: EventDataFrame, - gaze: GazeDataFrame, + gaze: pm.GazeDataFrame, identifiers: str | list[str], name: str | None = None, ) -> pl.DataFrame: diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py index 5561b358c..3cfb3a11c 100644 --- a/src/pymovements/gaze/gaze_dataframe.py +++ b/src/pymovements/gaze/gaze_dataframe.py @@ -27,6 +27,7 @@ import polars as pl +import pymovements as pm # pylint: disable=cyclic-import from pymovements.gaze import transforms from pymovements.gaze.experiment import Experiment from pymovements.utils import checks @@ -79,6 +80,7 @@ def __init__( self, data: pl.DataFrame | None = None, experiment: Experiment | None = None, + events: pm.EventDataFrame | None = None, *, trial_columns: str | list[str] | None = None, time_column: str | None = None, @@ -95,6 +97,8 @@ def __init__( A dataframe to be transformed to a polars dataframe. experiment : Experiment The experiment definition. + events: EventDataFrame + A dataframe of events in the gaze signal. trial_columns: 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, @@ -214,6 +218,11 @@ def __init__( self.n_components = _infer_n_components(self.frame, column_specifiers) self.experiment = experiment + if events is None: + self.events = pm.EventDataFrame() + else: + self.events = events.copy() + def transform( self, transform_method: str | Callable[..., pl.Expr], diff --git a/src/pymovements/gaze/integration.py b/src/pymovements/gaze/integration.py index ec7b5f70a..0d3554ccf 100644 --- a/src/pymovements/gaze/integration.py +++ b/src/pymovements/gaze/integration.py @@ -26,6 +26,7 @@ import pandas as pd import polars as pl +from pymovements.events.frame import EventDataFrame from pymovements.gaze.experiment import Experiment from pymovements.gaze.gaze_dataframe import GazeDataFrame from pymovements.utils import checks @@ -33,13 +34,15 @@ def from_numpy( data: np.ndarray | None = None, + experiment: Experiment | None = None, + events: EventDataFrame | 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, schema: list[str] | None = None, - experiment: Experiment | None = None, orient: Literal['col', 'row'] = 'col', time_column: str | None = None, pixel_columns: list[str] | None = None, @@ -63,6 +66,10 @@ def from_numpy( ---------- data: Two-dimensional data represented as a numpy ndarray. + experiment : Experiment + The experiment definition. + events: EventDataFrame + A dataframe of events in the gaze signal. time: Array of timestamps. pixel: @@ -77,9 +84,6 @@ def from_numpy( A list of column names. orient: Whether to interpret the two-dimensional data as columns or as rows. - experiment : Experiment - The experiment definition. - time_column: str | None = None, time_column: The name of the timestamp column in the input data frame. pixel_columns: @@ -200,6 +204,7 @@ def from_numpy( return GazeDataFrame( data=df, experiment=experiment, + events=events, time_column=time_column, pixel_columns=pixel_columns, position_columns=position_columns, @@ -245,6 +250,7 @@ def from_numpy( return GazeDataFrame( data=df, experiment=experiment, + events=events, time_column=time_column, pixel_columns=pixel_columns, position_columns=position_columns, @@ -256,6 +262,8 @@ def from_numpy( def from_pandas( data: pd.DataFrame, experiment: Experiment | None = None, + events: EventDataFrame | None = None, + *, time_column: str | None = None, pixel_columns: list[str] | None = None, position_columns: list[str] | None = None, @@ -270,6 +278,8 @@ def from_pandas( Data represented as a pandas DataFrame. experiment : Experiment The experiment definition. + events: EventDataFrame + A dataframe of events in the gaze signal. time_column: The name of the timestamp column in the input data frame. pixel_columns: @@ -289,6 +299,7 @@ def from_pandas( return GazeDataFrame( data=df, experiment=experiment, + events=events, time_column=time_column, pixel_columns=pixel_columns, position_columns=position_columns, diff --git a/tests/gaze/gaze_init_test.py b/tests/gaze/gaze_init_test.py index f74b87c89..b0b510ee3 100644 --- a/tests/gaze/gaze_init_test.py +++ b/tests/gaze/gaze_init_test.py @@ -23,7 +23,7 @@ import pytest from polars.testing import assert_frame_equal -from pymovements.gaze.gaze_dataframe import GazeDataFrame +import pymovements as pm @pytest.mark.parametrize( @@ -704,7 +704,7 @@ ], ) def test_init_gaze_dataframe_has_expected_attrs(init_kwargs, expected_frame, expected_n_components): - gaze = GazeDataFrame(**init_kwargs) + gaze = pm.GazeDataFrame(**init_kwargs) assert_frame_equal(gaze.frame, expected_frame) assert gaze.n_components == expected_n_components @@ -1175,7 +1175,7 @@ def test_init_gaze_dataframe_has_expected_attrs(init_kwargs, expected_frame, exp ) def test_gaze_dataframe_init_exceptions(init_kwargs, exception, exception_msg): with pytest.raises(exception) as excinfo: - GazeDataFrame(**init_kwargs) + pm.GazeDataFrame(**init_kwargs) msg, = excinfo.value.args assert msg == exception_msg @@ -1187,9 +1187,71 @@ def test_gaze_copy_init_has_same_n_components(): Refers to issue #514. """ df_orig = pl.from_numpy(np.zeros((2, 1000)), orient='col', schema=['x', 'y']) - gaze = GazeDataFrame(df_orig, position_columns=['x', 'y']) + gaze = pm.GazeDataFrame(df_orig, position_columns=['x', 'y']) df_copy = gaze.frame.clone() - gaze_copy = GazeDataFrame(df_copy) + gaze_copy = pm.GazeDataFrame(df_copy) assert gaze.n_components == gaze_copy.n_components + + +@pytest.mark.parametrize( + ('events', 'init_kwargs'), + [ + pytest.param( + None, + { + 'data': pl.from_dict( + {'x': [1.23], 'y': [4.56]}, schema={'x': pl.Float64, 'y': pl.Float64}, + ), + 'position_columns': ['x', 'y'], + }, + id='data_with_no_events', + ), + + pytest.param( + pm.EventDataFrame(), + { + 'data': pl.from_dict( + {'x': [1.23], 'y': [4.56]}, schema={'x': pl.Float64, 'y': pl.Float64}, + ), + 'position_columns': ['x', 'y'], + }, + id='data_empty_events', + ), + + pytest.param( + pm.EventDataFrame(), + {}, + id='no_data_empty_events', + ), + + pytest.param( + pm.EventDataFrame(name='saccade', onsets=[0], offsets=[10]), + {}, + id='no_data_with_saccades', + ), + + pytest.param( + pm.EventDataFrame(name='fixation', onsets=[100], offsets=[910]), + { + 'data': pl.from_dict( + {'x': [1.23], 'y': [4.56]}, schema={'x': pl.Float64, 'y': pl.Float64}, + ), + 'position_columns': ['x', 'y'], + }, + id='data_with_fixations', + ), + ], +) +def test_gaze_init_events(events, init_kwargs): + if events is None: + expected_events = pm.EventDataFrame().frame + else: + expected_events = events.frame + + gaze = pm.GazeDataFrame(events=events, **init_kwargs) + + assert_frame_equal(gaze.events.frame, expected_events) + # We don't want the events point to the same reference. + assert gaze.events.frame is not expected_events diff --git a/tests/gaze/integration_numpy_test.py b/tests/gaze/integration_numpy_test.py index ae39855d2..ed61afedf 100644 --- a/tests/gaze/integration_numpy_test.py +++ b/tests/gaze/integration_numpy_test.py @@ -20,6 +20,7 @@ """Test from gaze.from_numpy.""" import numpy as np import polars as pl +import pytest from polars.testing import assert_frame_equal import pymovements as pm @@ -164,7 +165,7 @@ def test_from_numpy_explicit_columns(): assert gaze.n_components == 2 -def test_init_all_none(): +def test_from_numpy_all_none(): gaze = pm.gaze.from_numpy( data=None, schema=None, @@ -185,3 +186,41 @@ def test_init_all_none(): assert_frame_equal(gaze.frame, expected) assert gaze.n_components is None + + +@pytest.mark.parametrize( + 'events', + [ + pytest.param( + None, + id='events_none', + ), + + pytest.param( + pm.EventDataFrame(), + id='events_empty', + ), + + pytest.param( + pm.EventDataFrame(name='fixation', onsets=[123], offsets=[345]), + id='fixation', + ), + + pytest.param( + pm.EventDataFrame(name='saccade', onsets=[34123], offsets=[67345]), + id='saccade', + ), + + ], +) +def test_from_numpy_events(events): + if events is None: + expected_events = pm.EventDataFrame().frame + else: + expected_events = events.frame + + gaze = pm.gaze.from_numpy(events=events) + + assert_frame_equal(gaze.events.frame, expected_events) + # We don't want the events point to the same reference. + assert gaze.events.frame is not expected_events diff --git a/tests/gaze/integration_pandas_test.py b/tests/gaze/integration_pandas_test.py index 2d1dc2808..082c3b85b 100644 --- a/tests/gaze/integration_pandas_test.py +++ b/tests/gaze/integration_pandas_test.py @@ -20,6 +20,7 @@ """Test from gaze.from_pandas.""" import pandas as pd import polars as pl +import pytest from polars.testing import assert_frame_equal import pymovements as pm @@ -35,15 +36,7 @@ def test_from_pandas(): }, ) - 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, - ) + experiment = pm.Experiment(1280, 1024, 38, 30, 68, 'lower left', 1000.0) gaze = pm.gaze.from_pandas( data=pandas_df, @@ -64,15 +57,7 @@ def test_from_pandas_explicit_columns(): }, ) - 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, - ) + experiment = pm.Experiment(1280, 1024, 38, 30, 68, 'lower left', 1000.0) gaze = pm.gaze.from_pandas( data=pandas_df, @@ -87,3 +72,45 @@ def test_from_pandas_explicit_columns(): }) assert_frame_equal(gaze.frame, expected) + + +@pytest.mark.parametrize( + ('df', 'events'), + [ + pytest.param( + pd.DataFrame(), + None, + id='events_none', + ), + + pytest.param( + pd.DataFrame(), + pm.EventDataFrame(), + id='events_empty', + ), + + pytest.param( + pd.DataFrame(), + pm.EventDataFrame(name='fixation', onsets=[123], offsets=[345]), + id='fixation', + ), + + pytest.param( + pd.DataFrame(), + pm.EventDataFrame(name='saccade', onsets=[34123], offsets=[67345]), + id='saccade', + ), + + ], +) +def test_from_pandas_events(df, events): + if events is None: + expected_events = pm.EventDataFrame().frame + else: + expected_events = events.frame + + gaze = pm.gaze.from_pandas(data=df, events=events) + + assert_frame_equal(gaze.events.frame, expected_events) + # We don't want the events point to the same reference. + assert gaze.events.frame is not expected_events