diff --git a/doc/source/user_guide/computation.rst b/doc/source/user_guide/computation.rst index b24020848b363..b9f0683697ba6 100644 --- a/doc/source/user_guide/computation.rst +++ b/doc/source/user_guide/computation.rst @@ -652,9 +652,9 @@ parameter: :header: "``closed``", "Description", "Default for" :widths: 20, 30, 30 - ``right``, close right endpoint, time-based windows + ``right``, close right endpoint, ``left``, close left endpoint, - ``both``, close both endpoints, fixed windows + ``both``, close both endpoints, ``neither``, open endpoints, For example, having the right endpoint open is useful in many problems that require that there is no contamination @@ -681,9 +681,6 @@ from present information back to past information. This allows the rolling windo df -Currently, this feature is only implemented for time-based windows. -For fixed windows, the closed parameter cannot be set and the rolling window will always have both endpoints closed. - .. _stats.iter_rolling_window: Iteration over window: diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index 28c86015fb7b6..99d2a1ee27265 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -222,6 +222,7 @@ Other enhancements - :meth:`DataFrame.plot` now recognizes ``xlabel`` and ``ylabel`` arguments for plots of type ``scatter`` and ``hexbin`` (:issue:`37001`) - :class:`DataFrame` now supports ``divmod`` operation (:issue:`37165`) - :meth:`DataFrame.to_parquet` now returns a ``bytes`` object when no ``path`` argument is passed (:issue:`37105`) +- :class:`Rolling` now supports the ``closed`` argument for fixed windows (:issue:`34315`) .. _whatsnew_120.api_breaking.python: diff --git a/pandas/_libs/window/indexers.pyx b/pandas/_libs/window/indexers.pyx index 9af1159a805ec..6a49a5bb34855 100644 --- a/pandas/_libs/window/indexers.pyx +++ b/pandas/_libs/window/indexers.pyx @@ -43,16 +43,14 @@ def calculate_variable_window_bounds( (ndarray[int64], ndarray[int64]) """ cdef: - bint left_closed = False - bint right_closed = False - int index_growth_sign = 1 + bint left_closed = False, right_closed = False ndarray[int64_t, ndim=1] start, end - int64_t start_bound, end_bound + int64_t start_bound, end_bound, index_growth_sign = 1 Py_ssize_t i, j - # if windows is variable, default is 'right', otherwise default is 'both' + # default is 'right' if closed is None: - closed = 'right' if index is not None else 'both' + closed = 'right' if closed in ['right', 'both']: right_closed = True diff --git a/pandas/core/window/indexers.py b/pandas/core/window/indexers.py index 71e77f97d8797..a8229257bb7bb 100644 --- a/pandas/core/window/indexers.py +++ b/pandas/core/window/indexers.py @@ -85,6 +85,10 @@ def get_window_bounds( end = np.arange(1 + offset, num_values + 1 + offset, dtype="int64") start = end - self.window_size + if closed in ["left", "both"]: + start -= 1 + if closed in ["left", "neither"]: + end -= 1 end = np.clip(end, 0, num_values) start = np.clip(start, 0, num_values) diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 1fcc47931e882..9136f9398799b 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -850,10 +850,11 @@ class Window(BaseWindow): axis : int or str, default 0 closed : str, default None Make the interval closed on the 'right', 'left', 'both' or - 'neither' endpoints. - For offset-based windows, it defaults to 'right'. - For fixed windows, defaults to 'both'. Remaining cases not implemented - for fixed windows. + 'neither' endpoints. Defaults to 'right'. + + .. versionchanged:: 1.2.0 + + The closed parameter with fixed windows is now supported. Returns ------- @@ -1976,11 +1977,6 @@ def validate(self): elif self.window < 0: raise ValueError("window must be non-negative") - if not self.is_datetimelike and self.closed is not None: - raise ValueError( - "closed only implemented for datetimelike and offset based windows" - ) - def _determine_window_length(self) -> Union[int, float]: """ Calculate freq for PeriodIndexes based on Index freq. Can not use diff --git a/pandas/tests/window/test_rolling.py b/pandas/tests/window/test_rolling.py index 048f7b8287176..9bba6d084f9c9 100644 --- a/pandas/tests/window/test_rolling.py +++ b/pandas/tests/window/test_rolling.py @@ -122,14 +122,37 @@ def test_numpy_compat(method): getattr(r, method)(dtype=np.float64) -def test_closed(): - df = DataFrame({"A": [0, 1, 2, 3, 4]}) - # closed only allowed for datetimelike +@pytest.mark.parametrize("closed", ["left", "right", "both", "neither"]) +def test_closed_fixed(closed, arithmetic_win_operators): + # GH 34315 + func_name = arithmetic_win_operators + df_fixed = DataFrame({"A": [0, 1, 2, 3, 4]}) + df_time = DataFrame({"A": [0, 1, 2, 3, 4]}, index=date_range("2020", periods=5)) - msg = "closed only implemented for datetimelike and offset based windows" + result = getattr(df_fixed.rolling(2, closed=closed, min_periods=1), func_name)() + expected = getattr(df_time.rolling("2D", closed=closed), func_name)().reset_index( + drop=True + ) - with pytest.raises(ValueError, match=msg): - df.rolling(window=3, closed="neither") + tm.assert_frame_equal(result, expected) + + +def test_closed_fixed_binary_col(): + # GH 34315 + data = [0, 1, 1, 0, 0, 1, 0, 1] + df = DataFrame( + {"binary_col": data}, + index=pd.date_range(start="2020-01-01", freq="min", periods=len(data)), + ) + + rolling = df.rolling(window=len(df), closed="left", min_periods=1) + result = rolling.mean() + expected = DataFrame( + [np.nan, 0, 0.5, 2 / 3, 0.5, 0.4, 0.5, 0.428571], + columns=["binary_col"], + index=pd.date_range(start="2020-01-01", freq="min", periods=len(data)), + ) + tm.assert_frame_equal(result, expected) @pytest.mark.parametrize("closed", ["neither", "left"])