diff --git a/pandas/_libs/window/indexers.pyx b/pandas/_libs/window/indexers.pyx index 9af1159a805ec..ae67f5909eb56 100644 --- a/pandas/_libs/window/indexers.pyx +++ b/pandas/_libs/window/indexers.pyx @@ -11,7 +11,7 @@ def calculate_variable_window_bounds( int64_t num_values, int64_t window_size, object min_periods, # unused but here to match get_window_bounds signature - object center, # unused but here to match get_window_bounds signature + object center, object closed, const int64_t[:] index ): @@ -30,7 +30,7 @@ def calculate_variable_window_bounds( ignored, exists for compatibility center : object - ignored, exists for compatibility + center the rolling window on the current observation closed : str string of side of the window that should be closed @@ -45,6 +45,7 @@ def calculate_variable_window_bounds( cdef: bint left_closed = False bint right_closed = False + bint center_window = False int index_growth_sign = 1 ndarray[int64_t, ndim=1] start, end int64_t start_bound, end_bound @@ -62,6 +63,8 @@ def calculate_variable_window_bounds( if index[num_values - 1] < index[0]: index_growth_sign = -1 + if center: + center_window = True start = np.empty(num_values, dtype='int64') start.fill(-1) @@ -76,14 +79,27 @@ def calculate_variable_window_bounds( # right endpoint is open else: end[0] = 0 + if center_window: + for j in range(0, num_values+1): + if (index[j] == index[0] + index_growth_sign*window_size/2 and + right_closed): + end[0] = j+1 + break + elif index[j] >= index[0] + index_growth_sign * window_size/2: + end[0] = j + break with nogil: # start is start of slice interval (including) # end is end of slice interval (not including) for i in range(1, num_values): - end_bound = index[i] - start_bound = index[i] - index_growth_sign * window_size + if center_window: + end_bound = index[i] + index_growth_sign * window_size/2 + start_bound = index[i] - index_growth_sign * window_size/2 + else: + end_bound = index[i] + start_bound = index[i] - index_growth_sign * window_size # left endpoint is closed if left_closed: @@ -97,14 +113,27 @@ def calculate_variable_window_bounds( start[i] = j break + # for centered window advance the end bound until we are + # outside the constraint + if center_window: + for j in range(end[i - 1], num_values+1): + if j == num_values: + end[i] = j + elif ((index[j] - end_bound) * index_growth_sign == 0 and + right_closed): + end[i] = j+1 + break + elif (index[j] - end_bound) * index_growth_sign >= 0: + end[i] = j + break # end bound is previous end # or current index - if (index[end[i - 1]] - end_bound) * index_growth_sign <= 0: + elif (index[end[i - 1]] - end_bound) * index_growth_sign <= 0: end[i] = i + 1 else: end[i] = end[i - 1] # right endpoint is open - if not right_closed: + if not right_closed and not center_window: end[i] -= 1 return start, end diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 558c0eeb0ea65..0b087bebe4e32 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -462,7 +462,9 @@ def _get_window_indexer(self, window: int) -> BaseIndexer: if isinstance(self.window, BaseIndexer): return self.window if self.is_freq_type: - return VariableWindowIndexer(index_array=self._on.asi8, window_size=window) + return VariableWindowIndexer( + index_array=self._on.asi8, window_size=window, center=self.center + ) return FixedWindowIndexer(window_size=window) def _apply_series(self, homogeneous_func: Callable[..., ArrayLike]) -> "Series": @@ -470,7 +472,6 @@ def _apply_series(self, homogeneous_func: Callable[..., ArrayLike]) -> "Series": Series version of _apply_blockwise """ _, obj = self._create_blocks(self._selected_obj) - try: values = self._prep_values(obj.values) except (TypeError, NotImplementedError) as err: @@ -554,7 +555,14 @@ def homogeneous_func(values: np.ndarray): if values.size == 0: return values.copy() - offset = calculate_center_offset(window) if center else 0 + offset = ( + calculate_center_offset(window) + if center + and not isinstance( + self._get_window_indexer(window), VariableWindowIndexer + ) + else 0 + ) additional_nans = np.array([np.nan] * offset) if not is_weighted: @@ -597,7 +605,9 @@ def calc(x): if use_numba_cache: NUMBA_FUNC_CACHE[(kwargs["original_func"], "rolling_apply")] = func - if center: + if center and not isinstance( + self._get_window_indexer(window), VariableWindowIndexer + ): result = self._center_window(result, window) return result @@ -1950,15 +1960,13 @@ def validate(self): if (self.obj.empty or self.is_datetimelike) and isinstance( self.window, (str, BaseOffset, timedelta) ): - self._validate_monotonic() freq = self._validate_freq() - # we don't allow center - if self.center: + # we don't allow center for offset based windows + if self.center and self.obj.empty: raise NotImplementedError( - "center is not implemented for " - "datetimelike and offset based windows" + "center is not implemented for offset based windows" ) # this will raise ValueError on non-fixed freqs