Skip to content

Impliment BusinessWindowIndexer for non-fixed offsets #24

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion pandas/core/window/indexers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

import numpy as np

from pandas.tseries.offsets import DateOffset
from pandas.core.indexes.datetimes import DatetimeIndex

from pandas.tseries.frequencies import to_offset
from pandas.tseries.offsets import BusinessMixin, DateOffset

BeginEnd = Tuple[np.ndarray, np.ndarray]

Expand Down Expand Up @@ -231,3 +234,30 @@ def get_window_bounds(
end[i] -= 1

return start, end


class BusinessWindowIndexer(VariableWindowIndexer):
"""Calculate window bounds based on business frequencies."""

def __init__(self, index=None, offset=None, keys=None):
super().__init__(index, offset, keys)
self.offset = to_offset(self.offset)
if not isinstance(self.offset, BusinessMixin):
raise ValueError(
"BusinessWindowIndexer only supports Business frequencies."
)
if not isinstance(self.index, DatetimeIndex):
raise ValueError("BusinessWindowIndexer only supports DatetimeIndexes.")

def get_window_bounds(
self,
num_values: int = 0,
window_size: int = 0,
min_periods: Optional[int] = None,
center: Optional[bool] = None,
closed: Optional[str] = None,
win_type: Optional[str] = None,
) -> BeginEnd:
return super().get_window_bounds(
num_values, self.offset, min_periods, center, closed, win_type
)
19 changes: 12 additions & 7 deletions pandas/core/window/rolling.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ def _get_window_indexer(self, index_as_array):
-------
VariableWindowIndexer or FixedWindowIndexer
"""
if isinstance(self.window, BaseIndexer):
Copy link
Collaborator

Choose a reason for hiding this comment

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

so we ignore the arg when using a custom indexer? or should it still be passed (and can be ignored if needed inside the custom indexer?

imagine writing VariableIWindowIndexer and FixedWindowIndwxer as a BaseIndexer (which i think we should do at some point)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All custom indexers should subclass BaseIndexer so self.window should be the custom indexer which will accept arguments from mean (_apply).

VariableWindowIndexer and FixedWindowIndexer already subclass BaseIndexer

return self.window
if self.is_freq_type:
return VariableWindowIndexer(index=index_as_array)
return FixedWindowIndexer(index=index_as_array)
Expand Down Expand Up @@ -436,7 +438,14 @@ def _apply(
check_minp = _use_window

if window is None:
apply_window = self._get_window(**kwargs) # type: int
apply_window = self._get_window(**kwargs)
else:
apply_window = window

if isinstance(apply_window, BaseIndexer):
# This value isn't significant for subclasses of BaseIndexer
# but is passed along to other validation checks.
apply_window = 0

blocks, obj = self._create_blocks()
block_list = list(blocks)
Expand Down Expand Up @@ -898,12 +907,8 @@ def _pop_args(win_type, arg_names, kwargs):
# GH #15662. `False` makes symmetric window, rather than periodic.
return sig.get_window(win_type, window, False).astype(float)
elif isinstance(window, BaseIndexer):
return window.get_window_span(
win_type=self.win_type,
min_periods=self.min_periods,
center=self.center,
closed=self.closed,
)
# Defer calling `.get_window_bounds` until later?
return window

def _get_roll_func(
self, cfunc: Callable, check_minp: Callable, index: np.ndarray, **kwargs
Expand Down
12 changes: 11 additions & 1 deletion pandas/tests/window/test_custom_indexer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from pandas import Series
from pandas import Series, date_range
from pandas.core.window.indexers import BusinessWindowIndexer
import pandas.util.testing as tm


def test_custom_indexer_validates(
Expand All @@ -13,3 +15,11 @@ def test_custom_indexer_validates(
min_periods=min_periods,
closed=closed,
)


def test_rolling_business_day():
index = date_range("2019-09-06", "2019-09-13", freq="D")
s = Series(range(len(index)), index=index)
result = s.rolling(BusinessWindowIndexer(index=s.index, offset="B")).mean()
expected = Series([0, 1, 1.5, 2, 4, 5, 6, 7], index=index)
tm.assert_series_equal(result, expected)