Repository: aeye-lab/pymovements (python)

Issue:
raise helpful error message if pos2vel() is applied without pix2deg()
## Description of the problem

```python
dataset = pm.Dataset('ToyDataset', path='data/ToyDataset')
dataset.download()
dataset.load()
dataset.pos2vel()
```
results in the cryptic error
```
Downloading http://github.com/aeye-lab/pymovements-toy-dataset/zipball/6cb5d663317bf418cec0c9abe1dde5085a8a8ebd/ to data/ToyDataset/downloads/pymovements-toy-dataset.zip
pymovements-toy-dataset.zip: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 3.06M/3.06M [00:05<00:00, 640kB/s]Checking integrity of pymovements-toy-dataset.zip
Extracting pymovements-toy-dataset.zip to data/ToyDataset/raw
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 20/20 [00:00<00:00, 43.67it/s]  0%|                                                                                                                                                                                     | 0/20 [00:00<?, ?it/s]Traceback (most recent call last):
  File "/home/user/lab/pymovements/t.py", line 7, in <module>
    dataset.pos2vel()
  File "/home/user/lab/pymovements/src/pymovements/dataset/dataset.py", line 355, in pos2vel
    gaze_df.pos2vel(method=method, **kwargs)
  File "/home/user/lab/pymovements/src/pymovements/gaze/gaze_dataframe.py", line 373, in pos2vel
    self.transform('pos2vel', method=method, **kwargs)
  File "/home/user/lab/pymovements/src/pymovements/gaze/gaze_dataframe.py", line 298, in transform
    [
  File "/home/user/lab/pymovements/src/pymovements/gaze/gaze_dataframe.py", line 299, in <listcomp>
    df.with_columns(transform_method(**kwargs))
  File "/home/user/.local/lib/python3.9/site-packages/polars/dataframe/frame.py", line 7631, in with_columns
    self.lazy()
  File "/home/user/.local/lib/python3.9/site-packages/polars/utils/deprecation.py", line 93, in wrapper
    return function(*args, **kwargs)
  File "/home/user/.local/lib/python3.9/site-packages/polars/lazyframe/frame.py", line 1695, in collect
    return wrap_df(ldf.collect())
exceptions.ColumnNotFoundError: position

Error originated just after this operation:
DF ["text_id", "page_id", "time", "stimuli_x"]; PROJECT */6 COLUMNS; SELECTION: "None"
```

## Description of a solution

If the `GazeDataFrame` has the `pixel` column available, it would be nice to suggest two solutions:

- applying `pix2deg()` beforehand
- taking use of the `position_column` and `velocity_column` keyword arguments from `pos2vel()`

## Minimum acceptance criteria

- [ ] raise error with helpful message somewhere before calling `pl.DataFrame.with_columns()` in  `GazeDataFrame.transform()`


Pull request:
raise helpful error message for transforms
resolves #499 and additionally covers both `pix2deg` and `pos2acc`

Patch:
diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py
index a95181e..5561b35 100644
--- a/src/pymovements/gaze/gaze_dataframe.py
+++ b/src/pymovements/gaze/gaze_dataframe.py
@@ -266,6 +266,33 @@ class GazeDataFrame:
                 _check_n_components(self.n_components)
                 kwargs['n_components'] = self.n_components
 
+            if transform_method.__name__ in {'pos2vel', 'pos2acc'}:
+                if 'position' not in self.frame.columns and 'position_column' not in kwargs:
+                    if 'pixel' in self.frame.columns:
+                        raise pl.exceptions.ColumnNotFoundError(
+                            "Neither 'position' is in the columns of the dataframe: "
+                            f'{self.frame.columns} nor is the position column specified. '
+                            "Since the dataframe has a 'pixel' column, consider running "
+                            f'pix2deg() before {transform_method.__name__}(). If you want '
+                            'to calculate pixel transformations, you can do so by using '
+                            f"{transform_method.__name__}(position_column='pixel'). "
+                            f'Available dataframe columns are {self.frame.columns}',
+                        )
+                    raise pl.exceptions.ColumnNotFoundError(
+                        "Neither 'position' is in the columns of the dataframe: "
+                        f'{self.frame.columns} nor is the position column specified. '
+                        f'Available dataframe columns are {self.frame.columns}',
+                    )
+            if transform_method.__name__ in {'pix2deg'}:
+                if 'pixel' not in self.frame.columns and 'pixel_column' not in kwargs:
+                    raise pl.exceptions.ColumnNotFoundError(
+                        "Neither 'position' is in the columns of the dataframe: "
+                        f'{self.frame.columns} nor is the pixel column specified. '
+                        'You can specify the pixel column via: '
+                        f'{transform_method.__name__}(pixel_column="name_of_your_pixel_column"). '
+                        f'Available dataframe columns are {self.frame.columns}',
+                    )
+
             if self.trial_columns is None:
                 self.frame = self.frame.with_columns(transform_method(**kwargs))
             else:


Test patch:
diff --git a/tests/gaze/gaze_transform_test.py b/tests/gaze/gaze_transform_test.py
index 24fea7b..f92ee5c 100644
--- a/tests/gaze/gaze_transform_test.py
+++ b/tests/gaze/gaze_transform_test.py
@@ -528,8 +528,10 @@ def test_gaze_dataframe_pix2deg_creates_position_column(data, experiment, pixel_
             },
             pl.exceptions.ColumnNotFoundError,
             (
-                'pixel\n\nError originated just after this operation:\nDF ["acceleration"]; '
-                'PROJECT */1 COLUMNS; SELECTION: "None"'
+                "Neither 'position' is in the columns of the dataframe: ['acceleration'] nor "
+                'is the pixel column specified. You can specify the pixel column via: '
+                'pix2deg(pixel_column="name_of_your_pixel_column"). Available dataframe '
+                "columns are ['acceleration']"
             ),
             id='no_pixel_column',
         ),
@@ -609,8 +611,25 @@ def test_gaze_dataframe_pos2acc_creates_acceleration_column(data, experiment, po
             },
             pl.exceptions.ColumnNotFoundError,
             (
-                'position\n\nError originated just after this operation:\nDF ["pixel"]; '
-                'PROJECT */1 COLUMNS; SELECTION: "None"'
+                "Neither 'position' is in the columns of the dataframe: ['pixel'] nor is the "
+                "position column specified. Since the dataframe has a 'pixel' column, "
+                'consider running pix2deg() before pos2acc(). If you want to calculate pixel '
+                "transformations, you can do so by using pos2acc(position_column='pixel'). "
+                "Available dataframe columns are ['pixel']"
+            ),
+            id='no_position_column',
+        ),
+        pytest.param(
+            {
+                'data': pl.from_dict({'x': [0.1], 'y': [0.2]}),
+                'experiment': pm.Experiment(1024, 768, 38, 30, 60, 'center', 1000),
+                'acceleration_columns': ['x', 'y'],
+            },
+            pl.exceptions.ColumnNotFoundError,
+            (
+                "Neither 'position' is in the columns of the dataframe: ['acceleration'] nor "
+                'is the position column specified. Available dataframe columns are '
+                "['acceleration']"
             ),
             id='no_position_column',
         ),
@@ -690,8 +709,25 @@ def test_gaze_dataframe_pos2vel_creates_velocity_column(data, experiment, positi
             },
             pl.exceptions.ColumnNotFoundError,
             (
-                'position\n\nError originated just after this operation:\nDF ["pixel"]; '
-                'PROJECT */1 COLUMNS; SELECTION: "None"'
+                "Neither 'position' is in the columns of the dataframe: ['pixel'] nor is the "
+                "position column specified. Since the dataframe has a 'pixel' column, "
+                'consider running pix2deg() before pos2vel(). If you want to calculate pixel '
+                "transformations, you can do so by using pos2vel(position_column='pixel'). "
+                "Available dataframe columns are ['pixel']"
+            ),
+            id='no_position_column',
+        ),
+        pytest.param(
+            {
+                'data': pl.from_dict({'x': [0.1], 'y': [0.2]}),
+                'experiment': pm.Experiment(1024, 768, 38, 30, 60, 'center', 1000),
+                'acceleration_columns': ['x', 'y'],
+            },
+            pl.exceptions.ColumnNotFoundError,
+            (
+                "Neither 'position' is in the columns of the dataframe: ['acceleration'] nor "
+                'is the position column specified. Available dataframe columns are '
+                "['acceleration']"
             ),
             id='no_position_column',
         ),