Skip to content

ENH: Support closed for fixed windows in rolling #37207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Oct 22, 2020
7 changes: 2 additions & 5 deletions doc/source/user_guide/computation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
10 changes: 4 additions & 6 deletions pandas/_libs/window/indexers.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pandas/core/window/indexers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 5 additions & 9 deletions pandas/core/window/rolling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need a blank line here


The closed parameter with fixed windows is now supported.

Returns
-------
Expand Down Expand Up @@ -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
Expand Down
35 changes: 29 additions & 6 deletions pandas/tests/window/test_rolling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down