From d930231011e6ba4b109b6c5cd5aa9f09250f7b03 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Tue, 12 Sep 2023 16:30:27 +0200 Subject: [PATCH 01/22] First implementation of smooth method --- src/pymovements/gaze/transforms.py | 105 +++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index 5fc61ffe3..8a28db825 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -542,6 +542,111 @@ def savitzky_golay( ).alias(output_column) +@register_transform +def smooth( + *, + method: str, + window_length: int, + n_components: int, + degree: int | None = None, + column: str = 'position', + padding: str | float | int | None = 'nearest', +) -> pl.Expr: + """ + Smooth data in a column. + + Parameters + ---------- + method: + The method to use for smoothing. + window_length + For ``moving_average`` this is the window size to calculate the mean of the subsequent + samples. For ``savitzky_golay`` this is the window size to use for the polynomial fit. + For ``exponential_moving_average`` this is the span parameter. + column + The input column name to which the smoothing is applied. + n_components: + Number of components in input column. + degree: + The degree of the polynomial to use. This has only an effect if using ``savitzky_golay`` as + smoothing method. + padding: + The padding to use. This has only an effect if using ``savitzky_golay`` as smoothing + method. + + Returns + ------- + polars.Expr + The respective polars expression. + + Notes + ----- + There following methods are available for smoothing: + + * ``savitzky_golay``: Smooth data by applying a Savitzky-Golay filter. + See :py:func:`~pymovements.gaze.transforms.savitzky_golay` for further details. + * ``moving_average``: Smooth data by calculating the mean of the subsequent samples. + Each smoothed sample is calculated by the mean of the samples in the window around the sample. + * ``exponential_moving_average``: Smooth data by + """ + + if method == 'moving_average': + pad_func = partial( + np.pad, + pad_width=window_length // 2, + mode=padding + ) + + return pl.concat_list( + [ + pl.col(column).list.get(component).map(pad_func).rolling_mean( + window_size=window_length, + center=True + ) + for component in range(n_components) + ], + ).alias(column) + + if method == 'exponential_moving_average': + pad_func = partial( + np.pad, + pad_width=window_length // 2, + mode=padding + ) + + return pl.concat_list( + [ + pl.col(column).list.get(component).map(pad_func).ewm_mean( + span=window_length, + adjust=False, + min_periods=window_length, + ).mean() + for component in range(n_components) + ], + ).alias(column) + + if method == 'savitzky_golay': + if degree is None: + raise TypeError("'degree' must not be none for method 'savitzky_golay'") + + return savitzky_golay( + window_length=window_length, + degree=degree, + sampling_rate=1, + padding=padding, + derivative=0, + n_components=n_components, + input_column=column, + output_column=None, + ) + + supported_methods = ['moving_average', 'savitzky_golay'] + + raise ValueError( + f"Unknown method '{method}'. Supported methods are: {supported_methods}", + ) + + def _check_window_length(window_length: Any) -> None: """Check that window length is an integer and greater than zero.""" checks.check_is_not_none(window_length=window_length) From 8180f119eb85e77b0d26dc65eaac0029c180d349 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Tue, 12 Sep 2023 19:40:09 +0200 Subject: [PATCH 02/22] * Docs for padding * Adjusted padding --- src/pymovements/gaze/transforms.py | 135 ++++++++++++++++++++--------- 1 file changed, 94 insertions(+), 41 deletions(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index 8a28db825..3a6d31546 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -31,7 +31,6 @@ from pymovements.utils import checks - TransformMethod = TypeVar('TransformMethod', bound=Callable[..., pl.Expr]) @@ -392,10 +391,10 @@ def pos2vel( return pl.concat_list( [ ( - pl.col(position_column).shift(periods=-2).list.get(component) - + pl.col(position_column).shift(periods=-1).list.get(component) - - pl.col(position_column).shift(periods=1).list.get(component) - - pl.col(position_column).shift(periods=2).list.get(component) + pl.col(position_column).shift(periods=-2).list.get(component) + + pl.col(position_column).shift(periods=-1).list.get(component) + - pl.col(position_column).shift(periods=1).list.get(component) + - pl.col(position_column).shift(periods=2).list.get(component) ) * (sampling_rate / 6) for component in range(n_components) ], @@ -558,7 +557,7 @@ def smooth( Parameters ---------- method: - The method to use for smoothing. + The method to use for smoothing. See Notes for more details. window_length For ``moving_average`` this is the window size to calculate the mean of the subsequent samples. For ``savitzky_golay`` this is the window size to use for the polynomial fit. @@ -571,8 +570,12 @@ def smooth( The degree of the polynomial to use. This has only an effect if using ``savitzky_golay`` as smoothing method. padding: - The padding to use. This has only an effect if using ``savitzky_golay`` as smoothing - method. + Must be either ``None``, a scalar or one of the strings ``mirror``, ``nearest`` or ``wrap``. + This determines the type of extension to use for the padded signal to + which the filter is applied. + When passing ``None``, no extension padding is used. + When passing a scalar value, data will be padded using the passed value. + See the Notes for more details on the padding methods. Returns ------- @@ -588,42 +591,87 @@ def smooth( * ``moving_average``: Smooth data by calculating the mean of the subsequent samples. Each smoothed sample is calculated by the mean of the samples in the window around the sample. * ``exponential_moving_average``: Smooth data by - """ - if method == 'moving_average': - pad_func = partial( - np.pad, - pad_width=window_length // 2, - mode=padding - ) + Details on the `padding` options: - return pl.concat_list( - [ - pl.col(column).list.get(component).map(pad_func).rolling_mean( - window_size=window_length, - center=True - ) - for component in range(n_components) - ], - ).alias(column) + * ``None``: No padding extension is used. + * scalar value (int or float): The padding extension contains the specified scalar value. + * ``mirror``: Repeats the values at the edges in reverse order. The value closest to the edge is + not included. + * ``nearest``: The padding extension contains the nearest input value. + * ``wrap``: The padding extension contains the values from the other end of the array. - if method == 'exponential_moving_average': - pad_func = partial( - np.pad, - pad_width=window_length // 2, - mode=padding - ) + Given the input is ``[1, 2, 3, 4, 5, 6, 7, 8]``, and + `window_length` is 7, the following table shows the padded data for + the various ``padding`` options: - return pl.concat_list( - [ - pl.col(column).list.get(component).map(pad_func).ewm_mean( - span=window_length, - adjust=False, - min_periods=window_length, - ).mean() - for component in range(n_components) - ], - ).alias(column) + +-------------+-------------+----------------------------+-------------+ + | mode | padding | input | padding | + +=============+=============+============================+=============+ + | ``None`` | ``- - -`` | ``1 2 3 4 5 6 7 8`` | ``- - -`` | + +-------------+-------------+----------------------------+-------------+ + | ``0`` | ``0 0 0`` | ``1 2 3 4 5 6 7 8`` | ``0 0 0`` | + +-------------+-------------+----------------------------+-------------+ + | ``1`` | ``1 1 1`` | ``1 2 3 4 5 6 7 8`` | ``1 1 1`` | + +-------------+-------------+----------------------------+-------------+ + | ``nearest`` | ``1 1 1`` | ``1 2 3 4 5 6 7 8`` | ``8 8 8`` | + +-------------+-------------+----------------------------+-------------+ + | ``mirror`` | ``4 3 2`` | ``1 2 3 4 5 6 7 8`` | ``7 6 5`` | + +-------------+-------------+----------------------------+-------------+ + | ``wrap`` | ``6 7 8`` | ``1 2 3 4 5 6 7 8`` | ``1 2 3`` | + +-------------+-------------+----------------------------+-------------+ + + """ + + _check_window_length(window_length=window_length) + _check_padding(padding=padding) + + if method in {'moving_average', 'exponential_moving_average'}: + pad_kwargs = {} + pad_func = _identity + + if isinstance(padding, (int, float)): + pad_kwargs['constant_values'] = padding + padding = 'constant' + elif padding == 'nearest': + # option 'nearest' is called 'edge' for np.pad + padding = 'edge' + elif padding == 'mirror': + # option 'mirror' is called 'reflect' for np.pad + padding = 'reflect' + + if padding is not None: + pad_kwargs['mode'] = padding + pad_kwargs['pad_width'] = window_length // 2 + + pad_func = partial( + np.pad, + **pad_kwargs + ) + + if method == 'moving_average': + + return pl.concat_list( + [ + pl.col(column).list.get(component).map(pad_func).rolling_mean( + window_size=window_length, + center=True + ) + for component in range(n_components) + ], + ).alias(column) + + if method == 'exponential_moving_average': + return pl.concat_list( + [ + pl.col(column).list.get(component).map(pad_func).ewm_mean( + span=window_length, + adjust=False, + min_periods=window_length, + ).mean() + for component in range(n_components) + ], + ).alias(column) if method == 'savitzky_golay': if degree is None: @@ -640,13 +688,18 @@ def smooth( output_column=None, ) - supported_methods = ['moving_average', 'savitzky_golay'] + supported_methods = ['moving_average', 'exponential_moving_average', 'savitzky_golay'] raise ValueError( f"Unknown method '{method}'. Supported methods are: {supported_methods}", ) +def _identity(x: Any): + """Identity function as placeholder for None as padding""" + return x + + def _check_window_length(window_length: Any) -> None: """Check that window length is an integer and greater than zero.""" checks.check_is_not_none(window_length=window_length) From d97555b492373912fdb7f934991e55e5104d3b56 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Tue, 12 Sep 2023 19:41:07 +0200 Subject: [PATCH 03/22] First Test for smooth method --- tests/gaze/transforms/smooth_test.py | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/gaze/transforms/smooth_test.py diff --git a/tests/gaze/transforms/smooth_test.py b/tests/gaze/transforms/smooth_test.py new file mode 100644 index 000000000..2ff4252d6 --- /dev/null +++ b/tests/gaze/transforms/smooth_test.py @@ -0,0 +1,52 @@ +# Copyright (c) 2022-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 pymovements.gaze.transforms.smooth.""" + +import polars as pl +import pytest +from polars.testing import assert_frame_equal + +import pymovements as pm + +@pytest.mark.parametrize( + "kwargs, series, expected_df", + [ + ( + { + "method": "moving_average", + "n_components": 1, + "window_length": 3, + "padding": 'wrap', + + }, + pl.Series('position', [[1.,1.],[2.,2.],[3.,3.]], pl.List(pl.Float64)), + pl.Series('position', [[1,1],[2,2],[3, 3]], pl.List(pl.Float64)), + ) + ] +) +def test_smooth_returns(kwargs, series, expected_df): + """Test if smooth returns the expected dataframe.""" + df = series.to_frame() + + result_df = df.select( + pm.gaze.transforms.smooth(**kwargs), + ) + + assert_frame_equal(result_df, expected_df.to_frame()) From 6451ea4dfed94a73bffced17689a70c1f7f455e0 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Thu, 14 Sep 2023 12:21:17 +0200 Subject: [PATCH 04/22] Fix padding functionality --- src/pymovements/gaze/transforms.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index 3a6d31546..1cd1ac11b 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -627,7 +627,7 @@ def smooth( _check_padding(padding=padding) if method in {'moving_average', 'exponential_moving_average'}: - pad_kwargs = {} + pad_kwargs = {"pad_width": 0} pad_func = _identity if isinstance(padding, (int, float)): @@ -653,10 +653,10 @@ def smooth( return pl.concat_list( [ - pl.col(column).list.get(component).map(pad_func).rolling_mean( - window_size=window_length, - center=True - ) + pl.col(column).list.get(component).map(pad_func).list.explode() + .rolling_mean(window_size=window_length,center=True) + .shift(periods=pad_kwargs['pad_width']) + .slice(pad_kwargs['pad_width'] * 2) for component in range(n_components) ], ).alias(column) @@ -668,7 +668,8 @@ def smooth( span=window_length, adjust=False, min_periods=window_length, - ).mean() + ).shift(periods=pad_kwargs['pad_width']) + .slice(pad_kwargs['pad_width'] * 2) for component in range(n_components) ], ).alias(column) From 816e64e9023687e25dcd3714b552d4a83eeabe27 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Thu, 14 Sep 2023 12:53:18 +0200 Subject: [PATCH 05/22] Fix exponential moving average --- src/pymovements/gaze/transforms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index 1cd1ac11b..c98d130cb 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -664,7 +664,8 @@ def smooth( if method == 'exponential_moving_average': return pl.concat_list( [ - pl.col(column).list.get(component).map(pad_func).ewm_mean( + pl.col(column).list.get(component).map(pad_func).list.explode() + .ewm_mean( span=window_length, adjust=False, min_periods=window_length, From 19bb655273a7f9f57ddfc57a2595c3269f18042e Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Thu, 14 Sep 2023 13:31:08 +0200 Subject: [PATCH 06/22] Add _identity return value tyoe --- src/pymovements/gaze/transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index c98d130cb..0cd0f1a9b 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -697,7 +697,7 @@ def smooth( ) -def _identity(x: Any): +def _identity(x: Any) -> Any: """Identity function as placeholder for None as padding""" return x From b408d09a500212c4f74cce3e97185be67f049db4 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Thu, 14 Sep 2023 13:32:02 +0200 Subject: [PATCH 07/22] Add some tests --- tests/gaze/transforms/smooth_test.py | 121 +++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 6 deletions(-) diff --git a/tests/gaze/transforms/smooth_test.py b/tests/gaze/transforms/smooth_test.py index 2ff4252d6..0eb8edf33 100644 --- a/tests/gaze/transforms/smooth_test.py +++ b/tests/gaze/transforms/smooth_test.py @@ -25,19 +25,84 @@ import pymovements as pm + @pytest.mark.parametrize( "kwargs, series, expected_df", [ - ( + pytest.param( { "method": "moving_average", - "n_components": 1, + "n_components": 2, + "window_length": 1, + }, + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + id="moving_average_window_length_1_returns_same_series", + ), + pytest.param( + { + "method": "moving_average", + "n_components": 2, "window_length": 3, - "padding": 'wrap', - + "padding": None + }, + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[None, None], [2., 2.], [None, None]], pl.List(pl.Float64)), + id="moving_average_window_length_3_no_padding", + ), + pytest.param( + { + "method": "moving_average", + "n_components": 2, + "window_length": 3, + "padding": 0.0 + }, + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[1., 1.], [2., 2.], [5 / 3, 5 / 3]], pl.List(pl.Float64)), + id="moving_average_window_length_3_constant_padding", + ), + pytest.param( + { + "method": "moving_average", + "n_components": 2, + "window_length": 3, + "padding": 'nearest' + }, + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[4 / 3, 4 / 3], [2., 2.], [8 / 3, 8 / 3]], pl.List(pl.Float64)), + id="moving_average_window_length_3_nearest_padding", + ), + pytest.param( + { + "method": "moving_average", + "n_components": 2, + "window_length": 3, + "padding": 'mirror' + }, + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[5 / 3, 5 / 3], [2., 2.], [7 / 3, 7 / 3]], pl.List(pl.Float64)), + id="moving_average_window_length_3_mirror_padding", + ), + pytest.param( + { + "method": "exponential_moving_average", + "n_components": 2, + "window_length": 1, }, - pl.Series('position', [[1.,1.],[2.,2.],[3.,3.]], pl.List(pl.Float64)), - pl.Series('position', [[1,1],[2,2],[3, 3]], pl.List(pl.Float64)), + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + id="exponential_moving_average_window_length_1_returns_same_series", + ), + pytest.param( + { + "method": "savitzky_golay", + "n_components": 2, + "window_length": 2, + "degree": 1, + }, + pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[1.5, 1.5], [2.5, 2.5], [3., 3.]], pl.List(pl.Float64)), + id="savitzky_golay_window_length_2_degree_1_returns", ) ] ) @@ -50,3 +115,47 @@ def test_smooth_returns(kwargs, series, expected_df): ) assert_frame_equal(result_df, expected_df.to_frame()) + + +@pytest.mark.parametrize( + "kwargs, exception, msg_substrings", + [ + pytest.param( + { + "method": "invalid_method", + "n_components": 2, + "window_length": 3, + }, + ValueError, + "Unkown method 'invalid_method'. Supported methods are: ", + id="invalid_method_raises_value_error", + ), + pytest.param( + { + "method": "savitzky_golay", + "n_components": 2, + "window_length": 3, + "degree": None, + }, + TypeError, + "'degree' must not be none for method 'savitzky_golay'", + id="savitzky_golay_degree_none_raises_type_error", + ), + ] +) +def test_smooth_init_raises_error(kwargs, exception, msg_substrings): + """Test if smooth init raises the expected error.""" + with pytest.raises(exception) as excinfo: + pm.gaze.transforms.smooth(**kwargs) + + msg, = excinfo.value.args + for msg_substring in msg_substrings: + assert msg_substring.lower() in msg.lower() + + +def test_identity_returns_same_series(): + """Test if identity returns the same series.""" + series = pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)) + series_identity = pm.gaze.transforms._identity(series) + + assert_frame_equal(series.to_frame(), series_identity.to_frame()) From d5b789526c0494b1751e53852ac8dc56b1141e17 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Sep 2023 11:34:45 +0000 Subject: [PATCH 08/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pymovements/gaze/transforms.py | 14 ++-- tests/gaze/transforms/smooth_test.py | 95 ++++++++++++++-------------- 2 files changed, 54 insertions(+), 55 deletions(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index 0cd0f1a9b..14afe86ad 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -391,10 +391,10 @@ def pos2vel( return pl.concat_list( [ ( - pl.col(position_column).shift(periods=-2).list.get(component) - + pl.col(position_column).shift(periods=-1).list.get(component) - - pl.col(position_column).shift(periods=1).list.get(component) - - pl.col(position_column).shift(periods=2).list.get(component) + pl.col(position_column).shift(periods=-2).list.get(component) + + pl.col(position_column).shift(periods=-1).list.get(component) + - pl.col(position_column).shift(periods=1).list.get(component) + - pl.col(position_column).shift(periods=2).list.get(component) ) * (sampling_rate / 6) for component in range(n_components) ], @@ -627,7 +627,7 @@ def smooth( _check_padding(padding=padding) if method in {'moving_average', 'exponential_moving_average'}: - pad_kwargs = {"pad_width": 0} + pad_kwargs = {'pad_width': 0} pad_func = _identity if isinstance(padding, (int, float)): @@ -646,7 +646,7 @@ def smooth( pad_func = partial( np.pad, - **pad_kwargs + **pad_kwargs, ) if method == 'moving_average': @@ -654,7 +654,7 @@ def smooth( return pl.concat_list( [ pl.col(column).list.get(component).map(pad_func).list.explode() - .rolling_mean(window_size=window_length,center=True) + .rolling_mean(window_size=window_length, center=True) .shift(periods=pad_kwargs['pad_width']) .slice(pad_kwargs['pad_width'] * 2) for component in range(n_components) diff --git a/tests/gaze/transforms/smooth_test.py b/tests/gaze/transforms/smooth_test.py index 0eb8edf33..11d1aeeb4 100644 --- a/tests/gaze/transforms/smooth_test.py +++ b/tests/gaze/transforms/smooth_test.py @@ -18,7 +18,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Test pymovements.gaze.transforms.smooth.""" - import polars as pl import pytest from polars.testing import assert_frame_equal @@ -27,84 +26,84 @@ @pytest.mark.parametrize( - "kwargs, series, expected_df", + 'kwargs, series, expected_df', [ pytest.param( { - "method": "moving_average", - "n_components": 2, - "window_length": 1, + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 1, }, pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - id="moving_average_window_length_1_returns_same_series", + id='moving_average_window_length_1_returns_same_series', ), pytest.param( { - "method": "moving_average", - "n_components": 2, - "window_length": 3, - "padding": None + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': None, }, pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), pl.Series('position', [[None, None], [2., 2.], [None, None]], pl.List(pl.Float64)), - id="moving_average_window_length_3_no_padding", + id='moving_average_window_length_3_no_padding', ), pytest.param( { - "method": "moving_average", - "n_components": 2, - "window_length": 3, - "padding": 0.0 + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': 0.0, }, pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), pl.Series('position', [[1., 1.], [2., 2.], [5 / 3, 5 / 3]], pl.List(pl.Float64)), - id="moving_average_window_length_3_constant_padding", + id='moving_average_window_length_3_constant_padding', ), pytest.param( { - "method": "moving_average", - "n_components": 2, - "window_length": 3, - "padding": 'nearest' + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': 'nearest', }, pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), pl.Series('position', [[4 / 3, 4 / 3], [2., 2.], [8 / 3, 8 / 3]], pl.List(pl.Float64)), - id="moving_average_window_length_3_nearest_padding", + id='moving_average_window_length_3_nearest_padding', ), pytest.param( { - "method": "moving_average", - "n_components": 2, - "window_length": 3, - "padding": 'mirror' + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': 'mirror', }, pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), pl.Series('position', [[5 / 3, 5 / 3], [2., 2.], [7 / 3, 7 / 3]], pl.List(pl.Float64)), - id="moving_average_window_length_3_mirror_padding", + id='moving_average_window_length_3_mirror_padding', ), pytest.param( { - "method": "exponential_moving_average", - "n_components": 2, - "window_length": 1, + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 1, }, pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - id="exponential_moving_average_window_length_1_returns_same_series", + id='exponential_moving_average_window_length_1_returns_same_series', ), pytest.param( { - "method": "savitzky_golay", - "n_components": 2, - "window_length": 2, - "degree": 1, + 'method': 'savitzky_golay', + 'n_components': 2, + 'window_length': 2, + 'degree': 1, }, pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), pl.Series('position', [[1.5, 1.5], [2.5, 2.5], [3., 3.]], pl.List(pl.Float64)), - id="savitzky_golay_window_length_2_degree_1_returns", - ) - ] + id='savitzky_golay_window_length_2_degree_1_returns', + ), + ], ) def test_smooth_returns(kwargs, series, expected_df): """Test if smooth returns the expected dataframe.""" @@ -118,30 +117,30 @@ def test_smooth_returns(kwargs, series, expected_df): @pytest.mark.parametrize( - "kwargs, exception, msg_substrings", + 'kwargs, exception, msg_substrings', [ pytest.param( { - "method": "invalid_method", - "n_components": 2, - "window_length": 3, + 'method': 'invalid_method', + 'n_components': 2, + 'window_length': 3, }, ValueError, "Unkown method 'invalid_method'. Supported methods are: ", - id="invalid_method_raises_value_error", + id='invalid_method_raises_value_error', ), pytest.param( { - "method": "savitzky_golay", - "n_components": 2, - "window_length": 3, - "degree": None, + 'method': 'savitzky_golay', + 'n_components': 2, + 'window_length': 3, + 'degree': None, }, TypeError, "'degree' must not be none for method 'savitzky_golay'", - id="savitzky_golay_degree_none_raises_type_error", + id='savitzky_golay_degree_none_raises_type_error', ), - ] + ], ) def test_smooth_init_raises_error(kwargs, exception, msg_substrings): """Test if smooth init raises the expected error.""" From 5a7f63ecc378286f7d79c4686c8c4bd7541314b2 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Thu, 14 Sep 2023 16:59:29 +0200 Subject: [PATCH 09/22] Fix mypy typing error --- src/pymovements/gaze/transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index 14afe86ad..eaac1a0ed 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -627,7 +627,7 @@ def smooth( _check_padding(padding=padding) if method in {'moving_average', 'exponential_moving_average'}: - pad_kwargs = {'pad_width': 0} + pad_kwargs : dict[str, Any] = {'pad_width': 0.0} pad_func = _identity if isinstance(padding, (int, float)): From b345aae9816762f2f55ac36d07b043723f87604d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Sep 2023 15:00:44 +0000 Subject: [PATCH 10/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pymovements/gaze/transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index eaac1a0ed..5667724f5 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -627,7 +627,7 @@ def smooth( _check_padding(padding=padding) if method in {'moving_average', 'exponential_moving_average'}: - pad_kwargs : dict[str, Any] = {'pad_width': 0.0} + pad_kwargs: dict[str, Any] = {'pad_width': 0.0} pad_func = _identity if isinstance(padding, (int, float)): From 8c6eceb988f112ea00c4ab2efeb4527cb783c1ac Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Thu, 14 Sep 2023 17:05:06 +0200 Subject: [PATCH 11/22] * Fix No blank lines allowed after function docstring * Fix First line should end with a period (not 'g') --- src/pymovements/gaze/transforms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index eaac1a0ed..acf144c11 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -622,7 +622,6 @@ def smooth( +-------------+-------------+----------------------------+-------------+ """ - _check_window_length(window_length=window_length) _check_padding(padding=padding) @@ -698,7 +697,7 @@ def smooth( def _identity(x: Any) -> Any: - """Identity function as placeholder for None as padding""" + """Identity function as placeholder for None as padding.""" return x From f74a75ffb683073bc2094c704b3003a41ea3d909 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Thu, 21 Sep 2023 11:16:10 +0200 Subject: [PATCH 12/22] * minor fix pad_width type * use rounded up half of window_length as padding length * add tests for all smoothing methods --- src/pymovements/gaze/transforms.py | 6 +- tests/gaze/transforms/smooth_test.py | 245 +++++++++++++++++++++++++-- 2 files changed, 232 insertions(+), 19 deletions(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index 69daa1f12..e395e2a80 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -568,7 +568,7 @@ def smooth( Number of components in input column. degree: The degree of the polynomial to use. This has only an effect if using ``savitzky_golay`` as - smoothing method. + smoothing method. `degree` must be less than `window_length`. padding: Must be either ``None``, a scalar or one of the strings ``mirror``, ``nearest`` or ``wrap``. This determines the type of extension to use for the padded signal to @@ -626,7 +626,7 @@ def smooth( _check_padding(padding=padding) if method in {'moving_average', 'exponential_moving_average'}: - pad_kwargs: dict[str, Any] = {'pad_width': 0.0} + pad_kwargs: dict[str, Any] = {'pad_width': 0} pad_func = _identity if isinstance(padding, (int, float)): @@ -641,7 +641,7 @@ def smooth( if padding is not None: pad_kwargs['mode'] = padding - pad_kwargs['pad_width'] = window_length // 2 + pad_kwargs['pad_width'] = np.ceil(window_length / 2).astype(int) pad_func = partial( np.pad, diff --git a/tests/gaze/transforms/smooth_test.py b/tests/gaze/transforms/smooth_test.py index 11d1aeeb4..fe1fcd6ef 100644 --- a/tests/gaze/transforms/smooth_test.py +++ b/tests/gaze/transforms/smooth_test.py @@ -28,16 +28,75 @@ @pytest.mark.parametrize( 'kwargs, series, expected_df', [ + # Method: moving_average pytest.param( { 'method': 'moving_average', 'n_components': 2, 'window_length': 1, + 'padding': 'nearest', }, - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), id='moving_average_window_length_1_returns_same_series', ), + pytest.param( + { + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': None, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[None, None], [1/2, 1/2], [1/2, 1/2]], pl.List(pl.Float64)), + id='moving_average_window_length_2_no_padding', + ), + pytest.param( + { + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': 0.0, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1/2, 1/2], [1/2, 1/2]], pl.List(pl.Float64)), + id='moving_average_window_length_2_constant_padding_0', + ), + pytest.param( + { + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': 1.0, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series( + 'position', [[1 / 2, 1 / 2], [1 / 2, 1 / 2], [1 / 2, 1 / 2]], pl.List(pl.Float64) + ), + id='moving_average_window_length_2_constant_padding_1', + ), + pytest.param( + { + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': 'nearest', + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1/2, 1/2], [1/2, 1/2]], pl.List(pl.Float64)), + id='moving_average_window_length_2_nearest_padding', + ), + pytest.param( + { + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': 'mirror', + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[1 / 2, 1/2], [1/2, 1/2], [1/2, 1/2]], pl.List(pl.Float64)), + id='moving_average_window_length_2_mirror_padding', + ), pytest.param( { 'method': 'moving_average', @@ -45,8 +104,8 @@ 'window_length': 3, 'padding': None, }, - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - pl.Series('position', [[None, None], [2., 2.], [None, None]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[None, None], [1/3, 1/3], [None, None]], pl.List(pl.Float64)), id='moving_average_window_length_3_no_padding', ), pytest.param( @@ -56,9 +115,22 @@ 'window_length': 3, 'padding': 0.0, }, - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - pl.Series('position', [[1., 1.], [2., 2.], [5 / 3, 5 / 3]], pl.List(pl.Float64)), - id='moving_average_window_length_3_constant_padding', + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[1/3, 1/3], [1/3, 1/3], [1/3, 1/3]], pl.List(pl.Float64)), + id='moving_average_window_length_3_constant_padding_0', + ), + pytest.param( + { + 'method': 'moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': 1.0, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series( + 'position', [[2 / 3, 2 / 3], [1 / 3, 1 / 3], [2 / 3, 2 / 3]], pl.List(pl.Float64) + ), + id='moving_average_window_length_3_constant_padding_1', ), pytest.param( { @@ -67,8 +139,8 @@ 'window_length': 3, 'padding': 'nearest', }, - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - pl.Series('position', [[4 / 3, 4 / 3], [2., 2.], [8 / 3, 8 / 3]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[1/3, 1/3], [1/3, 1/3], [1/3, 1/3]], pl.List(pl.Float64)), id='moving_average_window_length_3_nearest_padding', ), pytest.param( @@ -78,20 +150,139 @@ 'window_length': 3, 'padding': 'mirror', }, - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - pl.Series('position', [[5 / 3, 5 / 3], [2., 2.], [7 / 3, 7 / 3]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[2/3, 2/3], [1/3, 1/3], [2/3, 2/3]], pl.List(pl.Float64)), id='moving_average_window_length_3_mirror_padding', ), + # Method: exponential_moving_average pytest.param( { 'method': 'exponential_moving_average', 'n_components': 2, 'window_length': 1, + 'padding': 'nearest', }, - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), id='exponential_moving_average_window_length_1_returns_same_series', ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': None, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[None, None], [2/3, 2/3], [2/9, 2/9]], pl.List(pl.Float64)), + id='exponential_moving_average_window_length_2_no_padding', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': 0.0, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [2/3, 2/3], [2/9, 2/9]], pl.List(pl.Float64)), + id='exponential_moving_average_window_length_2_constant_padding_0', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': 1.0, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series( + 'position', [[1 / 3, 1 / 3], [7 / 9, 7 / 9], [7 / 27, 7 / 27]], pl.List(pl.Float64) + ), + id='exponential_moving_average_window_length_2_constant_padding_1', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': 'nearest', + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [2/3, 2/3], [2/9, 2/9]], pl.List(pl.Float64)), + id='exponential_moving_average_window_length_2_nearest_padding', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 2, + 'padding': 'mirror', + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series( + 'position', [[1 / 3, 1 / 3], [7 / 9, 7 / 9], [7 / 27, 7 / 27]], pl.List(pl.Float64) + ), + id='exponential_moving_average_window_length_2_mirror_padding', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': None, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[None, None], [None, None], [0.25, 0.25]], pl.List(pl.Float64)), + id='exponential_moving_average_window_length_3_no_padding', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': 0.0, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [0.5, 0.5], [0.25, 0.25]], pl.List(pl.Float64)), + id='exponential_moving_average_window_length_3_constant_padding_0', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': 1.0, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0.5, 0.5], [0.75, 0.75], [0.375, 0.375]], pl.List(pl.Float64)), + id='exponential_moving_average_window_length_3_constant_padding_1', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': 'nearest', + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [0.5, 0.5], [0.25, 0.25]], pl.List(pl.Float64)), + id='exponential_moving_average_window_length_3_nearest_padding', + ), + pytest.param( + { + 'method': 'exponential_moving_average', + 'n_components': 2, + 'window_length': 3, + 'padding': 'mirror', + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series( + 'position', [[0.25, 0.25], [0.625, 0.625], [0.3125, 0.3125]], pl.List(pl.Float64) + ), + id='exponential_moving_average_window_length_3_mirror_padding', + ), + # Method: savitzky_golay pytest.param( { 'method': 'savitzky_golay', @@ -99,9 +290,31 @@ 'window_length': 2, 'degree': 1, }, - pl.Series('position', [[1., 1.], [2., 2.], [3., 3.]], pl.List(pl.Float64)), - pl.Series('position', [[1.5, 1.5], [2.5, 2.5], [3., 3.]], pl.List(pl.Float64)), - id='savitzky_golay_window_length_2_degree_1_returns', + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0.5, 0.5], [0.5, 0.5], [0., 0.]], pl.List(pl.Float64)), + id='savitzky_golay_window_length_2_degree_1_returns_mean_of_window', + ), + pytest.param( + { + 'method': 'savitzky_golay', + 'n_components': 2, + 'window_length': 3, + 'degree': 1, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[1/3, 1/3], [1/3, 1/3], [1/3, 1/3]], pl.List(pl.Float64)), + id='savitzky_golay_window_length_3_degree_1_returns_mean_of_window', + ), + pytest.param( + { + 'method': 'savitzky_golay', + 'n_components': 2, + 'window_length': 3, + 'degree': 2, + }, + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0.,0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + id='savitzky_golay_window_length_3_degree_2_returns', ), ], ) From 288f087ffa000e4e90d8c93b6b699d4d3dd81164 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 09:17:05 +0000 Subject: [PATCH 13/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/gaze/transforms/smooth_test.py | 44 ++++++++++++++++------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/tests/gaze/transforms/smooth_test.py b/tests/gaze/transforms/smooth_test.py index fe1fcd6ef..d2fed8c57 100644 --- a/tests/gaze/transforms/smooth_test.py +++ b/tests/gaze/transforms/smooth_test.py @@ -48,7 +48,8 @@ 'padding': None, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[None, None], [1/2, 1/2], [1/2, 1/2]], pl.List(pl.Float64)), + pl.Series('position', [[None, None], [1 / 2, 1 / 2], + [1 / 2, 1 / 2]], pl.List(pl.Float64)), id='moving_average_window_length_2_no_padding', ), pytest.param( @@ -59,7 +60,7 @@ 'padding': 0.0, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[0., 0.], [1/2, 1/2], [1/2, 1/2]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1 / 2, 1 / 2], [1 / 2, 1 / 2]], pl.List(pl.Float64)), id='moving_average_window_length_2_constant_padding_0', ), pytest.param( @@ -71,7 +72,7 @@ }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), pl.Series( - 'position', [[1 / 2, 1 / 2], [1 / 2, 1 / 2], [1 / 2, 1 / 2]], pl.List(pl.Float64) + 'position', [[1 / 2, 1 / 2], [1 / 2, 1 / 2], [1 / 2, 1 / 2]], pl.List(pl.Float64), ), id='moving_average_window_length_2_constant_padding_1', ), @@ -83,7 +84,7 @@ 'padding': 'nearest', }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[0., 0.], [1/2, 1/2], [1/2, 1/2]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1 / 2, 1 / 2], [1 / 2, 1 / 2]], pl.List(pl.Float64)), id='moving_average_window_length_2_nearest_padding', ), pytest.param( @@ -94,7 +95,8 @@ 'padding': 'mirror', }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[1 / 2, 1/2], [1/2, 1/2], [1/2, 1/2]], pl.List(pl.Float64)), + pl.Series('position', [[1 / 2, 1 / 2], [1 / 2, 1 / 2], + [1 / 2, 1 / 2]], pl.List(pl.Float64)), id='moving_average_window_length_2_mirror_padding', ), pytest.param( @@ -105,7 +107,8 @@ 'padding': None, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[None, None], [1/3, 1/3], [None, None]], pl.List(pl.Float64)), + pl.Series('position', [[None, None], [1 / 3, 1 / 3], + [None, None]], pl.List(pl.Float64)), id='moving_average_window_length_3_no_padding', ), pytest.param( @@ -116,7 +119,8 @@ 'padding': 0.0, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[1/3, 1/3], [1/3, 1/3], [1/3, 1/3]], pl.List(pl.Float64)), + pl.Series('position', [[1 / 3, 1 / 3], [1 / 3, 1 / 3], + [1 / 3, 1 / 3]], pl.List(pl.Float64)), id='moving_average_window_length_3_constant_padding_0', ), pytest.param( @@ -128,7 +132,7 @@ }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), pl.Series( - 'position', [[2 / 3, 2 / 3], [1 / 3, 1 / 3], [2 / 3, 2 / 3]], pl.List(pl.Float64) + 'position', [[2 / 3, 2 / 3], [1 / 3, 1 / 3], [2 / 3, 2 / 3]], pl.List(pl.Float64), ), id='moving_average_window_length_3_constant_padding_1', ), @@ -140,7 +144,8 @@ 'padding': 'nearest', }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[1/3, 1/3], [1/3, 1/3], [1/3, 1/3]], pl.List(pl.Float64)), + pl.Series('position', [[1 / 3, 1 / 3], [1 / 3, 1 / 3], + [1 / 3, 1 / 3]], pl.List(pl.Float64)), id='moving_average_window_length_3_nearest_padding', ), pytest.param( @@ -151,7 +156,8 @@ 'padding': 'mirror', }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[2/3, 2/3], [1/3, 1/3], [2/3, 2/3]], pl.List(pl.Float64)), + pl.Series('position', [[2 / 3, 2 / 3], [1 / 3, 1 / 3], + [2 / 3, 2 / 3]], pl.List(pl.Float64)), id='moving_average_window_length_3_mirror_padding', ), # Method: exponential_moving_average @@ -174,7 +180,8 @@ 'padding': None, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[None, None], [2/3, 2/3], [2/9, 2/9]], pl.List(pl.Float64)), + pl.Series('position', [[None, None], [2 / 3, 2 / 3], + [2 / 9, 2 / 9]], pl.List(pl.Float64)), id='exponential_moving_average_window_length_2_no_padding', ), pytest.param( @@ -185,7 +192,7 @@ 'padding': 0.0, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[0., 0.], [2/3, 2/3], [2/9, 2/9]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [2 / 3, 2 / 3], [2 / 9, 2 / 9]], pl.List(pl.Float64)), id='exponential_moving_average_window_length_2_constant_padding_0', ), pytest.param( @@ -197,7 +204,7 @@ }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), pl.Series( - 'position', [[1 / 3, 1 / 3], [7 / 9, 7 / 9], [7 / 27, 7 / 27]], pl.List(pl.Float64) + 'position', [[1 / 3, 1 / 3], [7 / 9, 7 / 9], [7 / 27, 7 / 27]], pl.List(pl.Float64), ), id='exponential_moving_average_window_length_2_constant_padding_1', ), @@ -209,7 +216,7 @@ 'padding': 'nearest', }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[0., 0.], [2/3, 2/3], [2/9, 2/9]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [2 / 3, 2 / 3], [2 / 9, 2 / 9]], pl.List(pl.Float64)), id='exponential_moving_average_window_length_2_nearest_padding', ), pytest.param( @@ -221,7 +228,7 @@ }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), pl.Series( - 'position', [[1 / 3, 1 / 3], [7 / 9, 7 / 9], [7 / 27, 7 / 27]], pl.List(pl.Float64) + 'position', [[1 / 3, 1 / 3], [7 / 9, 7 / 9], [7 / 27, 7 / 27]], pl.List(pl.Float64), ), id='exponential_moving_average_window_length_2_mirror_padding', ), @@ -278,7 +285,7 @@ }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), pl.Series( - 'position', [[0.25, 0.25], [0.625, 0.625], [0.3125, 0.3125]], pl.List(pl.Float64) + 'position', [[0.25, 0.25], [0.625, 0.625], [0.3125, 0.3125]], pl.List(pl.Float64), ), id='exponential_moving_average_window_length_3_mirror_padding', ), @@ -302,7 +309,8 @@ 'degree': 1, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[1/3, 1/3], [1/3, 1/3], [1/3, 1/3]], pl.List(pl.Float64)), + pl.Series('position', [[1 / 3, 1 / 3], [1 / 3, 1 / 3], + [1 / 3, 1 / 3]], pl.List(pl.Float64)), id='savitzky_golay_window_length_3_degree_1_returns_mean_of_window', ), pytest.param( @@ -313,7 +321,7 @@ 'degree': 2, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[0.,0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), + pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), id='savitzky_golay_window_length_3_degree_2_returns', ), ], From 8457c300a80ba787f0e6847f294f8572b7be843f Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Thu, 21 Sep 2023 14:15:03 +0200 Subject: [PATCH 14/22] * add elif; try fix coverage exponential_moving_average --- src/pymovements/gaze/transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index e395e2a80..e392e2115 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -660,7 +660,7 @@ def smooth( ], ).alias(column) - if method == 'exponential_moving_average': + elif method == 'exponential_moving_average': return pl.concat_list( [ pl.col(column).list.get(component).map(pad_func).list.explode() From bf0be39ce86cf34bf40486ed28a6e9ba2fca2d02 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 12:16:02 +0000 Subject: [PATCH 15/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/gaze/transforms/smooth_test.py | 64 +++++++++++++++++++++------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/tests/gaze/transforms/smooth_test.py b/tests/gaze/transforms/smooth_test.py index d2fed8c57..4d1d34674 100644 --- a/tests/gaze/transforms/smooth_test.py +++ b/tests/gaze/transforms/smooth_test.py @@ -48,8 +48,12 @@ 'padding': None, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[None, None], [1 / 2, 1 / 2], - [1 / 2, 1 / 2]], pl.List(pl.Float64)), + pl.Series( + 'position', [ + [None, None], [1 / 2, 1 / 2], + [1 / 2, 1 / 2], + ], pl.List(pl.Float64), + ), id='moving_average_window_length_2_no_padding', ), pytest.param( @@ -95,8 +99,12 @@ 'padding': 'mirror', }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[1 / 2, 1 / 2], [1 / 2, 1 / 2], - [1 / 2, 1 / 2]], pl.List(pl.Float64)), + pl.Series( + 'position', [ + [1 / 2, 1 / 2], [1 / 2, 1 / 2], + [1 / 2, 1 / 2], + ], pl.List(pl.Float64), + ), id='moving_average_window_length_2_mirror_padding', ), pytest.param( @@ -107,8 +115,12 @@ 'padding': None, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[None, None], [1 / 3, 1 / 3], - [None, None]], pl.List(pl.Float64)), + pl.Series( + 'position', [ + [None, None], [1 / 3, 1 / 3], + [None, None], + ], pl.List(pl.Float64), + ), id='moving_average_window_length_3_no_padding', ), pytest.param( @@ -119,8 +131,12 @@ 'padding': 0.0, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[1 / 3, 1 / 3], [1 / 3, 1 / 3], - [1 / 3, 1 / 3]], pl.List(pl.Float64)), + pl.Series( + 'position', [ + [1 / 3, 1 / 3], [1 / 3, 1 / 3], + [1 / 3, 1 / 3], + ], pl.List(pl.Float64), + ), id='moving_average_window_length_3_constant_padding_0', ), pytest.param( @@ -144,8 +160,12 @@ 'padding': 'nearest', }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[1 / 3, 1 / 3], [1 / 3, 1 / 3], - [1 / 3, 1 / 3]], pl.List(pl.Float64)), + pl.Series( + 'position', [ + [1 / 3, 1 / 3], [1 / 3, 1 / 3], + [1 / 3, 1 / 3], + ], pl.List(pl.Float64), + ), id='moving_average_window_length_3_nearest_padding', ), pytest.param( @@ -156,8 +176,12 @@ 'padding': 'mirror', }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[2 / 3, 2 / 3], [1 / 3, 1 / 3], - [2 / 3, 2 / 3]], pl.List(pl.Float64)), + pl.Series( + 'position', [ + [2 / 3, 2 / 3], [1 / 3, 1 / 3], + [2 / 3, 2 / 3], + ], pl.List(pl.Float64), + ), id='moving_average_window_length_3_mirror_padding', ), # Method: exponential_moving_average @@ -180,8 +204,12 @@ 'padding': None, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[None, None], [2 / 3, 2 / 3], - [2 / 9, 2 / 9]], pl.List(pl.Float64)), + pl.Series( + 'position', [ + [None, None], [2 / 3, 2 / 3], + [2 / 9, 2 / 9], + ], pl.List(pl.Float64), + ), id='exponential_moving_average_window_length_2_no_padding', ), pytest.param( @@ -309,8 +337,12 @@ 'degree': 1, }, pl.Series('position', [[0., 0.], [1., 1.], [0., 0.]], pl.List(pl.Float64)), - pl.Series('position', [[1 / 3, 1 / 3], [1 / 3, 1 / 3], - [1 / 3, 1 / 3]], pl.List(pl.Float64)), + pl.Series( + 'position', [ + [1 / 3, 1 / 3], [1 / 3, 1 / 3], + [1 / 3, 1 / 3], + ], pl.List(pl.Float64), + ), id='savitzky_golay_window_length_3_degree_1_returns_mean_of_window', ), pytest.param( From 3613e69daa821e24fd62aa6050a5d6947108c2b3 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Fri, 22 Sep 2023 08:19:32 +0200 Subject: [PATCH 16/22] * Fix coverage --- src/pymovements/gaze/transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index e392e2115..a0bcc5f37 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -660,7 +660,7 @@ def smooth( ], ).alias(column) - elif method == 'exponential_moving_average': + else: return pl.concat_list( [ pl.col(column).list.get(component).map(pad_func).list.explode() From 590be440244754eed8545fce522c57e07f0549b3 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Fri, 22 Sep 2023 08:23:15 +0200 Subject: [PATCH 17/22] * Remove unnecessary "else" after "return" --- src/pymovements/gaze/transforms.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index a0bcc5f37..6cb7c5c90 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -660,19 +660,18 @@ def smooth( ], ).alias(column) - else: - return pl.concat_list( - [ - pl.col(column).list.get(component).map(pad_func).list.explode() - .ewm_mean( - span=window_length, - adjust=False, - min_periods=window_length, - ).shift(periods=pad_kwargs['pad_width']) - .slice(pad_kwargs['pad_width'] * 2) - for component in range(n_components) - ], - ).alias(column) + return pl.concat_list( + [ + pl.col(column).list.get(component).map(pad_func).list.explode() + .ewm_mean( + span=window_length, + adjust=False, + min_periods=window_length, + ).shift(periods=pad_kwargs['pad_width']) + .slice(pad_kwargs['pad_width'] * 2) + for component in range(n_components) + ], + ).alias(column) if method == 'savitzky_golay': if degree is None: From d5a6ddc81a7c3ff30ebafa741780f0649da59ddc Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Fri, 22 Sep 2023 10:08:03 +0200 Subject: [PATCH 18/22] * add smooth method to gaze dataframe + tests --- src/pymovements/gaze/gaze_dataframe.py | 25 ++++++ tests/gaze/gaze_transform_test.py | 116 +++++++++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py index 36f0f20f3..2fdc8d2cb 100644 --- a/src/pymovements/gaze/gaze_dataframe.py +++ b/src/pymovements/gaze/gaze_dataframe.py @@ -405,6 +405,31 @@ def pos2vel( """ self.transform('pos2vel', method=method, **kwargs) + + def smooth( + self, + column: str = 'position', + method: str = 'savitzky_golay', + degree: int = 2, + window_length: int = 7, + padding: str | float | int | None = 'nearest', + **kwargs: int | float | str, + ) -> None: + """ Smooth data in a column. + + See :func:`~transforms.smooth()` for details and description of parameters. + + """ + self.transform( + 'smooth', + column=column, + method=method, + degree=degree, + window_length=window_length, + padding=padding, + **kwargs, + ) + def detect( self, method: Callable[..., pm.EventDataFrame] | str, diff --git a/tests/gaze/gaze_transform_test.py b/tests/gaze/gaze_transform_test.py index f92ee5c5b..8ec8b039b 100644 --- a/tests/gaze/gaze_transform_test.py +++ b/tests/gaze/gaze_transform_test.py @@ -439,6 +439,50 @@ def fixture_experiment(): ), id='pos2vel_preceding_trialize_single_column_str', ), + pytest.param( + { + 'data': pl.from_dict( + { + 'x_dva': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + 'y_dva': [2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + }, + ), + 'position_columns': ['x_dva', 'y_dva'], + }, + 'smooth', {'method': 'moving_average', 'window_length': 3}, + pm.GazeDataFrame( + data=pl.from_dict( + { + 'x_dva': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + 'y_dva': [2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + }, + ), + position_columns=['x_dva', 'y_dva'], + ), + id='smooth', + ), + pytest.param( + { + 'data': pl.from_dict( + { + 'x_dva': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + 'y_dva': [2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + }, + ), + 'position_columns': ['x_dva', 'y_dva'], + }, + pm.gaze.transforms.smooth, {'method': 'moving_average', 'window_length': 3}, + pm.GazeDataFrame( + data=pl.from_dict( + { + 'x_dva': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + 'y_dva': [2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + }, + ), + position_columns=['x_dva', 'y_dva'], + ), + id='smooth_method_pass', + ), ], ) @@ -750,3 +794,75 @@ def test_gaze_dataframe_pos2vel_exceptions(init_kwargs, exception, expected_msg) msg, = excinfo.value.args assert msg == expected_msg + + +@pytest.mark.parametrize( + ('gaze_init_kwargs', 'kwargs', 'expected'), + [ + pytest.param( + { + 'data': pl.from_dict( + { + 'x_pix': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'y_pix': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'x_dva': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'y_dva': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + }, + ), + 'pixel_columns': ['x_pix', 'y_pix'], + 'position_columns': ['x_dva', 'y_dva'], + }, + {'method': 'moving_average', 'column': 'pixel', 'window_length': 3}, + pm.GazeDataFrame( + data=pl.from_dict( + { + + 'x_pix': [1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3], + 'y_pix': [1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3], + 'x_dva': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'y_dva': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + }, + ), + pixel_columns=['x_pix', 'y_pix'], + position_columns=['x_dva', 'y_dva'], + ), + id='pixel', + ), + pytest.param( + { + 'data': pl.from_dict( + { + 'x_pix': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'y_pix': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'x_dva': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'y_dva': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + }, + ), + 'pixel_columns': ['x_pix', 'y_pix'], + 'position_columns': ['x_dva', 'y_dva'], + }, + {'method': 'moving_average', 'column': 'position', 'window_length': 3}, + pm.GazeDataFrame( + data=pl.from_dict( + { + + 'x_pix': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'y_pix': [0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + 'x_dva': [1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3], + 'y_dva': [1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3], + }, + ), + pixel_columns=['x_pix', 'y_pix'], + position_columns=['x_dva', 'y_dva'], + ), + id='position', + ), + ], +) +def test_gaze_dataframe_smooth_expected_column( + gaze_init_kwargs, kwargs, expected, +): + gaze = pm.GazeDataFrame(**gaze_init_kwargs) + gaze.smooth(**kwargs) + + assert_frame_equal(gaze.frame, expected.frame) From aeeaa3502598d1c744fb46493fd2f19ecd816c13 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 Sep 2023 08:09:16 +0000 Subject: [PATCH 19/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pymovements/gaze/gaze_dataframe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py index 2fdc8d2cb..390210b87 100644 --- a/src/pymovements/gaze/gaze_dataframe.py +++ b/src/pymovements/gaze/gaze_dataframe.py @@ -405,7 +405,6 @@ def pos2vel( """ self.transform('pos2vel', method=method, **kwargs) - def smooth( self, column: str = 'position', From 3d925db9f0530d315af6c552e39d578320e11d0a Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Fri, 22 Sep 2023 10:13:01 +0200 Subject: [PATCH 20/22] * fix no whitespaces allowed surrounding docstring text --- src/pymovements/gaze/gaze_dataframe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py index 390210b87..329f1d1ed 100644 --- a/src/pymovements/gaze/gaze_dataframe.py +++ b/src/pymovements/gaze/gaze_dataframe.py @@ -417,7 +417,6 @@ def smooth( """ Smooth data in a column. See :func:`~transforms.smooth()` for details and description of parameters. - """ self.transform( 'smooth', From a6c73bd7be8f6b08913f8956d6fdbd8f864915b3 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Fri, 22 Sep 2023 10:14:49 +0200 Subject: [PATCH 21/22] * fix no whitespaces allowed surrounding docstring text --- src/pymovements/gaze/gaze_dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py index 329f1d1ed..f6f49154a 100644 --- a/src/pymovements/gaze/gaze_dataframe.py +++ b/src/pymovements/gaze/gaze_dataframe.py @@ -414,7 +414,7 @@ def smooth( padding: str | float | int | None = 'nearest', **kwargs: int | float | str, ) -> None: - """ Smooth data in a column. + """Smooth data in a column. See :func:`~transforms.smooth()` for details and description of parameters. """ From 12bd2df1b4316811ee0459b3f09bbfd4eaac1400 Mon Sep 17 00:00:00 2001 From: Jakob Chwastek Date: Mon, 25 Sep 2023 13:33:32 +0200 Subject: [PATCH 22/22] Complete docs --- src/pymovements/gaze/gaze_dataframe.py | 29 +++++++++++++++++++++++--- src/pymovements/gaze/transforms.py | 6 +++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/pymovements/gaze/gaze_dataframe.py b/src/pymovements/gaze/gaze_dataframe.py index f6f49154a..598369712 100644 --- a/src/pymovements/gaze/gaze_dataframe.py +++ b/src/pymovements/gaze/gaze_dataframe.py @@ -407,16 +407,39 @@ def pos2vel( def smooth( self, - column: str = 'position', method: str = 'savitzky_golay', - degree: int = 2, window_length: int = 7, + degree: int = 2, + column: str = 'position', padding: str | float | int | None = 'nearest', **kwargs: int | float | str, ) -> None: """Smooth data in a column. - See :func:`~transforms.smooth()` for details and description of parameters. + Parameters + ---------- + method: + The method to use for smoothing. Choose from ``savitzky_golay``, ``moving_average``, + ``exponential_moving_average``. See :func:`~transforms.smooth()` for details. + window_length: + For ``moving_average`` this is the window size to calculate the mean of the subsequent + samples. For ``savitzky_golay`` this is the window size to use for the polynomial fit. + For ``exponential_moving_average`` this is the span parameter. + degree: + The degree of the polynomial to use. This has only an effect if using + ``savitzky_golay`` as smoothing method. `degree` must be less than `window_length`. + column: + The input column name to which the smoothing is applied. + padding: + Must be either ``None``, a scalar or one of the strings + ``mirror``, ``nearest`` or ``wrap``. + This determines the type of extension to use for the padded signal to + which the filter is applied. + When passing ``None``, no extension padding is used. + When passing a scalar value, data will be padded using the passed value. + See :func:`~transforms.smooth()` for details on the padding methods. + **kwargs: + Additional keyword arguments to be passed to the :func:`~transforms.smooth()` method. """ self.transform( 'smooth', diff --git a/src/pymovements/gaze/transforms.py b/src/pymovements/gaze/transforms.py index 93eee6066..91a1a4396 100644 --- a/src/pymovements/gaze/transforms.py +++ b/src/pymovements/gaze/transforms.py @@ -578,13 +578,13 @@ def smooth( For ``moving_average`` this is the window size to calculate the mean of the subsequent samples. For ``savitzky_golay`` this is the window size to use for the polynomial fit. For ``exponential_moving_average`` this is the span parameter. - column - The input column name to which the smoothing is applied. n_components: Number of components in input column. degree: The degree of the polynomial to use. This has only an effect if using ``savitzky_golay`` as smoothing method. `degree` must be less than `window_length`. + column: + The input column name to which the smoothing is applied. padding: Must be either ``None``, a scalar or one of the strings ``mirror``, ``nearest`` or ``wrap``. This determines the type of extension to use for the padded signal to @@ -606,7 +606,7 @@ def smooth( See :py:func:`~pymovements.gaze.transforms.savitzky_golay` for further details. * ``moving_average``: Smooth data by calculating the mean of the subsequent samples. Each smoothed sample is calculated by the mean of the samples in the window around the sample. - * ``exponential_moving_average``: Smooth data by + * ``exponential_moving_average``: Smooth data by exponentially weighted moving average. Details on the `padding` options: