Skip to content

Commit 2e341ab

Browse files
authored
Center rolling window for time offset (#38780)
1 parent 29b7b61 commit 2e341ab

File tree

6 files changed

+313
-86
lines changed

6 files changed

+313
-86
lines changed

doc/source/user_guide/window.rst

+12
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ By default the labels are set to the right edge of the window, but a
157157
s.rolling(window=5, center=True).mean()
158158
159159
160+
This can also be applied to datetime-like indices.
161+
.. versionadded:: 1.3
162+
.. ipython:: python
163+
164+
df = pd.DataFrame(
165+
{"A": [0, 1, 2, 3, 4]}, index=pd.date_range("2020", periods=5, freq="1D")
166+
)
167+
df
168+
df.rolling("2D", center=False).mean()
169+
df.rolling("2D", center=True).mean()
170+
171+
160172
.. _window.endpoints:
161173

162174
Rolling window endpoints

doc/source/whatsnew/v1.3.0.rst

+17
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,23 @@ a copy will no longer be made (:issue:`32960`)
159159
The default behavior when not passing ``copy`` will remain unchanged, i.e.
160160
a copy will be made.
161161

162+
Centered Datetime-Like Rolling Windows
163+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
164+
165+
When performing rolling calculations on :class:`DataFrame` and :class:`Series`
166+
objects with a datetime-like index, a centered datetime-like window can now be
167+
used (:issue:`38780`).
168+
For example:
169+
170+
.. ipython:: python
171+
172+
df = pd.DataFrame(
173+
{"A": [0, 1, 2, 3, 4]}, index=pd.date_range("2020", periods=5, freq="1D")
174+
)
175+
df
176+
df.rolling("2D", center=True).mean()
177+
178+
162179
.. _whatsnew_130.enhancements.other:
163180

164181
Other enhancements

pandas/_libs/window/indexers.pyx

+35-8
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def calculate_variable_window_bounds(
1414
int64_t num_values,
1515
int64_t window_size,
1616
object min_periods, # unused but here to match get_window_bounds signature
17-
object center, # unused but here to match get_window_bounds signature
17+
bint center,
1818
object closed,
1919
const int64_t[:] index
2020
):
@@ -32,8 +32,8 @@ def calculate_variable_window_bounds(
3232
min_periods : object
3333
ignored, exists for compatibility
3434
35-
center : object
36-
ignored, exists for compatibility
35+
center : bint
36+
center the rolling window on the current observation
3737
3838
closed : str
3939
string of side of the window that should be closed
@@ -46,7 +46,8 @@ def calculate_variable_window_bounds(
4646
(ndarray[int64], ndarray[int64])
4747
"""
4848
cdef:
49-
bint left_closed = False, right_closed = False
49+
bint left_closed = False
50+
bint right_closed = False
5051
ndarray[int64_t, ndim=1] start, end
5152
int64_t start_bound, end_bound, index_growth_sign = 1
5253
Py_ssize_t i, j
@@ -77,14 +78,27 @@ def calculate_variable_window_bounds(
7778
# right endpoint is open
7879
else:
7980
end[0] = 0
81+
if center:
82+
for j in range(0, num_values + 1):
83+
if (index[j] == index[0] + index_growth_sign * window_size / 2 and
84+
right_closed):
85+
end[0] = j + 1
86+
break
87+
elif index[j] >= index[0] + index_growth_sign * window_size / 2:
88+
end[0] = j
89+
break
8090

8191
with nogil:
8292

8393
# start is start of slice interval (including)
8494
# end is end of slice interval (not including)
8595
for i in range(1, num_values):
86-
end_bound = index[i]
87-
start_bound = index[i] - index_growth_sign * window_size
96+
if center:
97+
end_bound = index[i] + index_growth_sign * window_size / 2
98+
start_bound = index[i] - index_growth_sign * window_size / 2
99+
else:
100+
end_bound = index[i]
101+
start_bound = index[i] - index_growth_sign * window_size
88102

89103
# left endpoint is closed
90104
if left_closed:
@@ -98,14 +112,27 @@ def calculate_variable_window_bounds(
98112
start[i] = j
99113
break
100114

115+
# for centered window advance the end bound until we are
116+
# outside the constraint
117+
if center:
118+
for j in range(end[i - 1], num_values + 1):
119+
if j == num_values:
120+
end[i] = j
121+
elif ((index[j] - end_bound) * index_growth_sign == 0 and
122+
right_closed):
123+
end[i] = j + 1
124+
break
125+
elif (index[j] - end_bound) * index_growth_sign >= 0:
126+
end[i] = j
127+
break
101128
# end bound is previous end
102129
# or current index
103-
if (index[end[i - 1]] - end_bound) * index_growth_sign <= 0:
130+
elif (index[end[i - 1]] - end_bound) * index_growth_sign <= 0:
104131
end[i] = i + 1
105132
else:
106133
end[i] = end[i - 1]
107134

108135
# right endpoint is open
109-
if not right_closed:
136+
if not right_closed and not center:
110137
end[i] -= 1
111138
return start, end

pandas/core/window/rolling.py

+3-8
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,9 @@ def _get_window_indexer(self) -> BaseIndexer:
367367
return self.window
368368
if self._win_freq_i8 is not None:
369369
return VariableWindowIndexer(
370-
index_array=self._index_array, window_size=self._win_freq_i8
370+
index_array=self._index_array,
371+
window_size=self._win_freq_i8,
372+
center=self.center,
371373
)
372374
return FixedWindowIndexer(window_size=self.window)
373375

@@ -1456,13 +1458,6 @@ def validate(self):
14561458

14571459
self._validate_monotonic()
14581460

1459-
# we don't allow center
1460-
if self.center:
1461-
raise NotImplementedError(
1462-
"center is not implemented for "
1463-
"datetimelike and offset based windows"
1464-
)
1465-
14661461
# this will raise ValueError on non-fixed freqs
14671462
try:
14681463
freq = to_offset(self.window)

0 commit comments

Comments
 (0)