Repository: aeye-lab/pymovements (python)

Issue:
raise helpful warning/exception if no columns specified but passed gaze initialized with sample dataframe
## Description of the problem
As #1015 has shown, it can be easy to miss that pixel/position/velocity/etc. columns need to be specified when initializing or loading a `GazeDataFrame`. Otherwise, the processing functions will raise this rather unhelpful exception:

```
AttributeError: n_components must be either 2, 4 or 6 but is None
```

## Description of a solution

1. A more helpful warning right at initialization. This could be something like: `GazeDataFrame includes samples in data, but no column names are specified. Please specify either pixel_columns, position_columns, velocity_columns or acceleration_columns to apply any transformations`.

2. A more helpful exception when applying transformations. This could something like: `no pixel_columns specified. Please specify pixel_columns during initialization to apply pix2deg()`

The wording of my proposed messages can surely be improved further.

Pull request:
feat: add warning and improve error message if no gaze components
…azeDataFrame
Resolves #1016


## Description

Please include a clear and concise description of the change and which issue is fixed.
Please also include excepted improvements, relevant motivation and context.
List any dependencies that are required for this change.

## Implemented changes

Insert a description of the changes implemented in the pull request.

- [ ] Change 1
- [ ] Change 2
- [ ] Change 3

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes.
Provide instructions so we can reproduce.
Please also list any relevant details for your test configuration

- [ ] Test A
- [ ] Test B

## Type of change

Remove irrelevant items:
- Bug fix
- New functionality
- Performance improvement
- New dataset
- Breaking change
- Documentation update

## Context

Resolves #

#### related issues:
- #

#### required by:
- #

#### requires:
- [ ] #

#### conflicts with:
- #

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
- [ ] I have checked my code and corrected any misspellings


Patch:
diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py
index fe231fc..ecd30e3 100644
--- a/src/pymovements/gaze/gaze_dataframe.py
+++ b/src/pymovements/gaze/gaze_dataframe.py
@@ -971,11 +971,14 @@ class GazeDataFrame:
         --------
         Let's initialize an example GazeDataFrame first:
         >>> gaze = pm.gaze.from_numpy(
-        ...     distance=np.concatenate([np.zeros(40), np.full(10, np.nan), np.ones(50)]),
+        ...     pixel=np.concatenate(
+        ...         [np.zeros((2, 40)), np.full((2, 10), np.nan), np.ones((2, 50))],
+        ...         axis=1,
+        ...     ),
         ... )
 
         You can calculate measures, for example the null ratio like this:
-        >>> gaze.measure_samples('null_ratio', column='distance')
+        >>> gaze.measure_samples('null_ratio', column='pixel')
         shape: (1, 1)
         ┌────────────┐
         │ null_ratio │
@@ -1242,6 +1245,16 @@ class GazeDataFrame:
     def _check_n_components(self) -> None:
         """Check that n_components is either 2, 4 or 6.
 
+        Ensure that the number of gaze components is valid.
+
+        Valid configurations are:
+            - 2 components: monocular data (e.g., x and y)
+            - 4 components: binocular data (e.g., x/y for left and right eye)
+            - 6 components: binocular + cyclopean data (x/y for left, right, and cyclopean eye)
+
+        If no valid gaze columns were specified (pixel, position, etc.), raise an error
+        with a helpful message to guide proper initialization.
+
         Raises
         ------
         AttributeError
@@ -1249,7 +1262,11 @@ class GazeDataFrame:
         """
         if self.n_components not in {2, 4, 6}:
             raise AttributeError(
-                f'n_components must be either 2, 4 or 6 but is {self.n_components}',
+                'Number of components required but no gaze components could be inferred.\n'
+                'This usually happens if you did not specify any column content'
+                ' and the content could not be autodetected from the column names. \n'
+                "Please specify 'pixel_columns', 'position_columns', 'velocity_columns'"
+                " or 'acceleration_columns' explicitly during initialization.",
             )
 
     def _check_component_columns(self, **kwargs: list[str]) -> None:
@@ -1564,6 +1581,18 @@ class GazeDataFrame:
             column_specifiers.append(acceleration_columns)
 
         self.n_components = self._infer_n_components(column_specifiers)
+        # Warning if contains data but no gaze-related columns were provided.
+        # This can lead to failure in downstream methods that rely on those columns
+        # (e.g., transformations).
+        if len(self.frame) > 1 and not self.n_components:
+            warnings.warn(
+                'GazeDataFrame contains data but no components could be inferred. \n'
+                'This usually happens if you did not specify any column content'
+                ' and the content could not be autodetected from the column names. \n'
+                "Please specify 'pixel_columns', 'position_columns', 'velocity_columns'"
+                " or 'acceleration_columns' explicitly during initialization."
+                ' Otherwise, transformation methods may fail.',
+            )
 
     def _init_time_column(
             self,


Test patch:
diff --git a/tests/unit/gaze/apply_test.py b/tests/unit/gaze/apply_test.py
index 6b02f0e..7629652 100644
--- a/tests/unit/gaze/apply_test.py
+++ b/tests/unit/gaze/apply_test.py
@@ -97,12 +97,14 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(0, 100),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(
                     name=['fixation', 'saccade'], onsets=[0, 50], offsets=[40, 100],
                 ),
             ),
             pm.gaze.from_numpy(
                 time=np.arange(0, 100),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(
                     name=['fixation', 'saccade', 'unclassified'],
                     onsets=[0, 50, 40],
diff --git a/tests/unit/gaze/detect_test.py b/tests/unit/gaze/detect_test.py
index 5faa43f..51b1c0f 100644
--- a/tests/unit/gaze/detect_test.py
+++ b/tests/unit/gaze/detect_test.py
@@ -721,6 +721,7 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(0, 100),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(name='fixation', onsets=[0], offsets=[100]),
             ),
             pm.EventDataFrame(name='fixation', onsets=[0], offsets=[100]),
@@ -732,6 +733,7 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(0, 100),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(name='fixation', onsets=[10], offsets=[100]),
             ),
             pm.EventDataFrame(
@@ -747,6 +749,7 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(0, 100),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(name='fixation', onsets=[0], offsets=[90]),
             ),
             pm.EventDataFrame(
@@ -762,6 +765,7 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(0, 100),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(name='fixation', onsets=[0, 50], offsets=[40, 100]),
             ),
             pm.EventDataFrame(
@@ -777,6 +781,7 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(0, 100),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(
                     name=['fixation', 'saccade'], onsets=[0, 50], offsets=[40, 100],
                 ),
@@ -795,6 +800,7 @@ from pymovements.synthetic import step_function
             pm.gaze.from_numpy(
                 trial=np.array([1] * 50 + [2] * 50),
                 time=np.arange(0, 100),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(
                     name='fixation', onsets=[0, 90], offsets=[10, 100], trials=[1, 2],
                 ),
@@ -814,6 +820,7 @@ from pymovements.synthetic import step_function
             pm.gaze.from_numpy(
                 trial=np.array([1] * 50 + [2] * 50),
                 time=np.concatenate((np.arange(0, 50), np.arange(0, 50))),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(
                     name='fixation', onsets=[0, 40], offsets=[10, 50], trials=[1, 2],
                 ),
@@ -837,6 +844,7 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(100, 200),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(
                     name=['fixation', 'saccade'], onsets=[0, 50], offsets=[40, 100],
                 ),
@@ -854,6 +862,7 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(0, 200),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(
                     name=['fixation', 'saccade'], onsets=[210, 250], offsets=[240, 300],
                 ),
@@ -871,6 +880,7 @@ from pymovements.synthetic import step_function
             {},
             pm.gaze.from_numpy(
                 time=np.arange(100, 200),
+                position=np.zeros((2, 100)),
                 events=pm.EventDataFrame(
                     name=['fixation', 'fixation'], onsets=[0, 120], offsets=[110, 220],
                 ),
diff --git a/tests/unit/gaze/gaze_aoi_mapping_test.py b/tests/unit/gaze/gaze_aoi_mapping_test.py
index 156ddcd..6914272 100644
--- a/tests/unit/gaze/gaze_aoi_mapping_test.py
+++ b/tests/unit/gaze/gaze_aoi_mapping_test.py
@@ -1070,6 +1070,7 @@ EXPECTED_DF = {
 }
 
 
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 @pytest.mark.parametrize(
     ('eye'),
     [
@@ -1079,6 +1080,7 @@ EXPECTED_DF = {
         'else',
     ],
 )
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 @pytest.mark.parametrize(
     ('aoi_column'),
     [
@@ -1086,6 +1088,7 @@ EXPECTED_DF = {
         'char',
     ],
 )
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 @pytest.mark.parametrize(
     ('gaze_type'),
     [
@@ -1122,6 +1125,7 @@ def test_gaze_to_aoi_mapping_char_width_height(eye, aoi_column, gaze_type):
     assert_frame_equal(gaze_df.frame, EXPECTED_DF[f'{aoi_column}_{eye}_{gaze_type}'])
 
 
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 @pytest.mark.parametrize(
     ('eye'),
     [
@@ -1131,6 +1135,7 @@ def test_gaze_to_aoi_mapping_char_width_height(eye, aoi_column, gaze_type):
         'else',
     ],
 )
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 @pytest.mark.parametrize(
     ('aoi_column'),
     [
@@ -1138,6 +1143,7 @@ def test_gaze_to_aoi_mapping_char_width_height(eye, aoi_column, gaze_type):
         'char',
     ],
 )
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 @pytest.mark.parametrize(
     ('gaze_type'),
     [
diff --git a/tests/unit/gaze/gaze_dataframe_unnest_test.py b/tests/unit/gaze/gaze_dataframe_unnest_test.py
index 4ae951a..192ccd9 100644
--- a/tests/unit/gaze/gaze_dataframe_unnest_test.py
+++ b/tests/unit/gaze/gaze_dataframe_unnest_test.py
@@ -438,6 +438,9 @@ def test_gaze_dataframe_unnest_has_expected_frame_multiple_unnest(
             Warning,
             'No columns to unnest. '
             'Please specify columns to unnest via the "input_columns" argument.',
+            marks=pytest.mark.filterwarnings(
+                'ignore:GazeDataFrame contains data but no.*:UserWarning',
+            ),
             id='df_single_row_two_components_unnest_all_default_values_no_cols_to_unnest',
         ),
     ],
@@ -460,7 +463,7 @@ def test_gaze_dataframe_unnest_errors(init_data, unnest_kwargs, exception, excep
             {'input_columns': 'pixel', 'output_suffixes': ['_x', '_y']},
             1,
             AttributeError,
-            'n_components must be either 2, 4 or 6 but is 1',
+            'Number of components required but no gaze components could be inferred.',
             id='df_single_row_two_pixel_component_invalid_number_of_components',
         ),
     ],
@@ -475,4 +478,4 @@ def test_gaze_dataframe_unnest_invalid_number_of_components(
         gaze.unnest(**unnest_kwargs)
 
     msg, = exc_info.value.args
-    assert msg == exception_msg
+    assert msg.startswith(exception_msg)
diff --git a/tests/unit/gaze/gaze_init_test.py b/tests/unit/gaze/gaze_init_test.py
index d34ac9f..c3780f1 100644
--- a/tests/unit/gaze/gaze_init_test.py
+++ b/tests/unit/gaze/gaze_init_test.py
@@ -1159,6 +1159,9 @@ from pymovements import GazeDataFrame
             },
             pl.from_dict({'time': [1.23]}, schema={'time': pl.Float64}),
             None,
+            marks=pytest.mark.filterwarnings(
+                'ignore:GazeDataFrame contains data but no.*:UserWarning',
+            ),
             id='df_single_row_time_column_dataset_definition',
         ),
 
@@ -1170,6 +1173,9 @@ from pymovements import GazeDataFrame
             },
             pl.from_dict({'time': [1.23]}, schema={'time': pl.Float64}),
             None,
+            marks=pytest.mark.filterwarnings(
+                'ignore:GazeDataFrame contains data but no.*:UserWarning',
+            ),
             id='df_single_row_time_column_overwrites_dataset_definition',
         ),
 
@@ -1180,6 +1186,9 @@ from pymovements import GazeDataFrame
             },
             pl.from_dict({'time': [1230]}, schema={'time': pl.Int64}),
             None,
+            marks=pytest.mark.filterwarnings(
+                'ignore:GazeDataFrame contains data but no.*:UserWarning',
+            ),
             id='df_single_row_time_unit_dataset_definition',
         ),
 
@@ -1191,6 +1200,9 @@ from pymovements import GazeDataFrame
             },
             pl.from_dict({'time': [4560]}, schema={'time': pl.Int64}),
             None,
+            marks=pytest.mark.filterwarnings(
+                'ignore:GazeDataFrame contains data but no.*:UserWarning',
+            ),
             id='df_single_row_time_unit_overwrites_dataset_definition',
         ),
 
@@ -1201,6 +1213,9 @@ from pymovements import GazeDataFrame
             },
             pl.from_dict({'distance': [1.23]}, schema={'distance': pl.Float64}),
             None,
+            marks=pytest.mark.filterwarnings(
+                'ignore:GazeDataFrame contains data but no.*:UserWarning',
+            ),
             id='df_single_row_distance_column_dataset_definition',
         ),
 
@@ -1212,6 +1227,9 @@ from pymovements import GazeDataFrame
             },
             pl.from_dict({'distance': [1.23]}, schema={'distance': pl.Float64}),
             None,
+            marks=pytest.mark.filterwarnings(
+                'ignore:GazeDataFrame contains data but no.*:UserWarning',
+            ),
             id='df_single_row_distance_column_overwrites_dataset_definition',
         ),
 
@@ -1258,6 +1276,7 @@ def test_init_gaze_dataframe_has_expected_experiment(init_kwargs, expected_exper
     assert gaze.experiment == expected_experiment
 
 
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 @pytest.mark.parametrize(
     ('init_kwargs', 'expected_trial_columns'),
     [
@@ -1955,3 +1974,13 @@ def test_gaze_init_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
+
+
+def test_gaze_init_warnings():
+    with pytest.warns(UserWarning) as record:
+        GazeDataFrame(data=pl.from_dict({'a': [1, 2, 3]}))
+
+    expected_msg_prefix = 'GazeDataFrame contains data but no components could be inferred.'
+
+    assert len(record) == 1
+    assert record[0].message.args[0].startswith(expected_msg_prefix)
diff --git a/tests/unit/gaze/gaze_transform_test.py b/tests/unit/gaze/gaze_transform_test.py
index e20ad1f..451ffed 100644
--- a/tests/unit/gaze/gaze_transform_test.py
+++ b/tests/unit/gaze/gaze_transform_test.py
@@ -1009,7 +1009,7 @@ def test_gaze_dataframe_pix2deg_creates_position_column(data, experiment, pixel_
                 'experiment': pm.Experiment(1024, 768, 38, 30, 60, 'center', 1000),
             },
             AttributeError,
-            'n_components must be either 2, 4 or 6 but is None',
+            'Number of components required but no gaze components could be inferred.',
             id='no_column_components',
         ),
         pytest.param(
@@ -1069,7 +1069,7 @@ def test_gaze_dataframe_pix2deg_exceptions(init_kwargs, exception, expected_msg)
         gaze_df.pix2deg()
 
     msg, = excinfo.value.args
-    assert msg == expected_msg
+    assert msg.startswith(expected_msg)
 
 
 @pytest.mark.parametrize(
@@ -1110,7 +1110,7 @@ def test_gaze_dataframe_pix2deg_warnings(init_kwargs, warning, expected_msg):
                 'experiment': pm.Experiment(1024, 768, 38, 30, 60, 'center', 1000),
             },
             AttributeError,
-            'n_components must be either 2, 4 or 6 but is None',
+            'Number of components required but no gaze components could be inferred.',
             id='no_column_components',
         ),
         pytest.param(
@@ -1169,7 +1169,7 @@ def test_gaze_dataframe_deg2pix_exceptions(init_kwargs, exception, expected_msg)
         gaze_df.deg2pix()
 
     msg, = excinfo.value.args
-    assert msg == expected_msg
+    assert msg.startswith(expected_msg)
 
 
 @pytest.mark.parametrize(
@@ -1216,7 +1216,7 @@ def test_gaze_dataframe_pos2acc_creates_acceleration_column(data, experiment, po
                 'experiment': pm.Experiment(1024, 768, 38, 30, 60, 'center', 1000),
             },
             AttributeError,
-            'n_components must be either 2, 4 or 6 but is None',
+            'Number of components required but no gaze components could be inferred.',
             id='no_column_components',
         ),
         pytest.param(
@@ -1270,7 +1270,7 @@ def test_gaze_dataframe_pos2acc_exceptions(init_kwargs, exception, expected_msg)
         gaze_df.pos2acc()
 
     msg, = excinfo.value.args
-    assert msg == expected_msg
+    assert msg.startswith(expected_msg)
 
 
 @pytest.mark.parametrize(
@@ -1339,7 +1339,7 @@ def test_gaze_dataframe_clip_creates_new_column(experiment):
                 'experiment': pm.Experiment(1024, 768, 38, 30, 60, 'center', 1000),
             },
             AttributeError,
-            'n_components must be either 2, 4 or 6 but is None',
+            'Number of components required but no gaze components could be inferred.',
             id='no_column_components',
         ),
         pytest.param(
@@ -1393,7 +1393,7 @@ def test_gaze_dataframe_pos2vel_exceptions(init_kwargs, exception, expected_msg)
         gaze_df.pos2vel()
 
     msg, = excinfo.value.args
-    assert msg == expected_msg
+    assert msg.startswith(expected_msg)
 
 
 @pytest.mark.parametrize(
diff --git a/tests/unit/gaze/integration_numpy_test.py b/tests/unit/gaze/integration_numpy_test.py
index 31f23c3..87975dc 100644
--- a/tests/unit/gaze/integration_numpy_test.py
+++ b/tests/unit/gaze/integration_numpy_test.py
@@ -26,6 +26,7 @@ from polars.testing import assert_frame_equal
 import pymovements as pm
 
 
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 def test_from_numpy():
     array = np.array(
         [
diff --git a/tests/unit/gaze/integration_pandas_test.py b/tests/unit/gaze/integration_pandas_test.py
index 2fdb127..b4e8b66 100644
--- a/tests/unit/gaze/integration_pandas_test.py
+++ b/tests/unit/gaze/integration_pandas_test.py
@@ -26,6 +26,7 @@ from polars.testing import assert_frame_equal
 import pymovements as pm
 
 
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 def test_from_pandas_additional_time_column():
     pandas_df = pd.DataFrame(
         {
diff --git a/tests/unit/gaze/io/ipc_test.py b/tests/unit/gaze/io/ipc_test.py
index 8478f44..bef02a6 100644
--- a/tests/unit/gaze/io/ipc_test.py
+++ b/tests/unit/gaze/io/ipc_test.py
@@ -46,7 +46,10 @@ import pymovements as pm
                 'column_map': {'pixel': 'pixel_coordinates'},
             },
             (10, 2),
-            id='feather_bino_shape',
+            marks=pytest.mark.filterwarnings(
+                'ignore:GazeDataFrame contains data but no.*:UserWarning',
+            ),
+            id='feather_mono_shape_column_map',
         ),
     ],
 )
diff --git a/tests/unit/gaze/measure_test.py b/tests/unit/gaze/measure_test.py
index a73f3b6..713e66e 100644
--- a/tests/unit/gaze/measure_test.py
+++ b/tests/unit/gaze/measure_test.py
@@ -23,20 +23,26 @@ import polars as pl
 import pytest
 from polars.testing import assert_frame_equal
 
-import pymovements as pm
+from pymovements import GazeDataFrame
 
 
 def my_test_measure(column: str) -> pl.Expr:
     return pl.col(column).len().cast(pl.Int64).alias('my_measure')
 
 
+@pytest.mark.filterwarnings('ignore:GazeDataFrame contains data but no.*:UserWarning')
 @pytest.mark.parametrize(
-    ('gaze', 'method', 'kwargs', 'expected'),
+    ('gaze_init_kwargs', 'method', 'kwargs', 'expected'),
     [
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [1000, 1001, 1002, 1003]}, schema={'A': pl.Int64}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [1000, 1001, 1002, 1003],
+                    },
+                    schema={'A': pl.Int64},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [0.0]}),
@@ -44,9 +50,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [1000, None, None, 1003]}, schema={'A': pl.Int64}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [1000, None, None, 1003],
+                    }, schema={'A': pl.Int64},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [0.5]}),
@@ -54,9 +64,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [1.0, np.nan, np.nan, 1.3]}, schema={'A': pl.Float64}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [1.0, np.nan, np.nan, 1.3],
+                    }, schema={'A': pl.Float64},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [0.5]}),
@@ -64,9 +78,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [None, None, None, None]}, schema={'A': pl.Int64}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [None, None, None, None],
+                    }, schema={'A': pl.Int64},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [1.0]}),
@@ -74,9 +92,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [0.0, 0.1, 0.2, 0.3]}, schema={'A': pl.Float64}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [0.0, 0.1, 0.2, 0.3],
+                    }, schema={'A': pl.Float64},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [0.0]}),
@@ -84,9 +106,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [0.0, None, None, 0.3]}, schema={'A': pl.Float64}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [0.0, None, None, 0.3],
+                    }, schema={'A': pl.Float64},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [0.5]}),
@@ -94,9 +120,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [None, None, None, None]}, schema={'A': pl.Float64}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [None, None, None, None],
+                    }, schema={'A': pl.Float64},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [1.0]}),
@@ -104,9 +134,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': ['a', 'b', 'c', 'd']}, schema={'A': pl.Utf8}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': ['a', 'b', 'c', 'd'],
+                    }, schema={'A': pl.Utf8},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [0.0]}),
@@ -114,9 +148,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': ['a', None, None, 'd']}, schema={'A': pl.Utf8}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': ['a', None, None, 'd'],
+                    }, schema={'A': pl.Utf8},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [0.5]}),
@@ -124,9 +162,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [None, None, None, None]}, schema={'A': pl.Utf8}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [None, None, None, None],
+                    }, schema={'A': pl.Utf8},
+                ),
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'null_ratio': [1.0]}),
@@ -134,13 +176,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(
+            {
+                'data': pl.from_dict(
                     data={'t': [1000, 1001, 1002], 'x': [0.1, 0.2, 0.3], 'y': [0.1, 0.2, 0.3]},
                 ),
-                time_column='t',
-                pixel_columns=['x', 'y'],
-            ),
+                'time_column': 't',
+                'pixel_columns': ['x', 'y'],
+            },
             'null_ratio',
             {'column': 'pixel'},
             pl.DataFrame(data={'null_ratio': [0.0]}),
@@ -148,15 +190,15 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(
+            {
+                'data': pl.from_dict(
                     data={
                         't': [1000, 1001, 1002], 'x': [None, None, None], 'y': [None, None, None],
                     },
                 ),
-                time_column='t',
-                pixel_columns=['x', 'y'],
-            ),
+                'time_column': 't',
+                'pixel_columns': ['x', 'y'],
+            },
             'null_ratio',
             {'column': 'pixel'},
             pl.DataFrame(data={'null_ratio': [1.0]}),
@@ -164,15 +206,15 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(
+            {
+                'data': pl.from_dict(
                     data={
                         't': [1000, 1001], 'x': [0.1, None], 'y': [0.2, None],
                     },
                 ),
-                time_column='t',
-                pixel_columns=['x', 'y'],
-            ),
+                'time_column': 't',
+                'pixel_columns': ['x', 'y'],
+            },
             'null_ratio',
             {'column': 'pixel'},
             pl.DataFrame(data={'null_ratio': [0.5]}),
@@ -180,15 +222,15 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.DataFrame(
+            {
+                'data': pl.DataFrame(
                     data={
                         't': [1000, 1001], 'x': [0.1, np.nan], 'y': [0.2, np.nan],
                     },
                 ),
-                time_column='t',
-                pixel_columns=['x', 'y'],
-            ),
+                'time_column': 't',
+                'pixel_columns': ['x', 'y'],
+            },
             'null_ratio',
             {'column': 'pixel'},
             pl.DataFrame(data={'null_ratio': [0.5]}),
@@ -196,15 +238,15 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(
+            {
+                'data': pl.from_dict(
                     data={
                         't': [1000, 1001], 'x': [0.1, None], 'y': [0.2, None],
                     },
                 ),
-                time_column='t',
-                position_columns=['x', 'y'],
-            ),
+                'time_column': 't',
+                'position_columns': ['x', 'y'],
+            },
             'null_ratio',
             {'column': 'position'},
             pl.DataFrame(data={'null_ratio': [0.5]}),
@@ -212,15 +254,15 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(
+            {
+                'data': pl.from_dict(
                     data={
                         't': [1000, 1001], 'x': [0.1, None], 'y': [0.2, None],
                     },
                 ),
-                time_column='t',
-                velocity_columns=['x', 'y'],
-            ),
+                'time_column': 't',
+                'velocity_columns': ['x', 'y'],
+            },
             'null_ratio',
             {'column': 'velocity'},
             pl.DataFrame(data={'null_ratio': [0.5]}),
@@ -228,13 +270,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(
+            {
+                'data': pl.from_dict(
                     data={'A': [1000, 1001, 1002, 1003], 'trial': [1, 1, 1, 1]},
                     schema={'A': pl.Int64, 'trial': pl.Int64},
                 ),
-                trial_columns='trial',
-            ),
+                'trial_columns': 'trial',
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'trial': [1], 'null_ratio': [0.0]}),
@@ -242,13 +284,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(
+            {
+                'data': pl.from_dict(
                     data={'A': [1000, 1001, None, None], 'trial': [1, 1, 2, 2]},
                     schema={'A': pl.Int64, 'trial': pl.Int64},
                 ),
-                trial_columns='trial',
-            ),
+                'trial_columns': 'trial',
+            },
             'null_ratio',
             {'column': 'A'},
             pl.DataFrame(data={'trial': [1, 2], 'null_ratio': [0.0, 1.0]}),
@@ -256,9 +298,13 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
 
         pytest.param(
-            pm.GazeDataFrame(
-                data=pl.from_dict(data={'A': [1000, 1001, 1002, 1003]}, schema={'A': pl.Int64}),
-            ),
+            {
+                'data': pl.from_dict(
+                    data={
+                        'A': [1000, 1001, 1002, 1003],
+                    }, schema={'A': pl.Int64},
+                ),
+            },
             my_test_measure,
             {'column': 'A'},
             pl.DataFrame(data={'my_measure': [4]}),
@@ -266,6 +312,7 @@ def my_test_measure(column: str) -> pl.Expr:
         ),
     ],
 )
-def test_measure_samples(gaze, method, kwargs, expected):
+def test_measure_samples(gaze_init_kwargs, method, kwargs, expected):
+    gaze = GazeDataFrame(**gaze_init_kwargs)
     df = gaze.measure_samples(method, **kwargs)
     assert_frame_equal(df, expected)