From 5d37ab216ce1c25d4ab9abba1bdd82d7e6e2ee95 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Mon, 9 Sep 2019 23:59:21 -0700 Subject: [PATCH 1/3] Impliment Indexer for non-fixed offsets --- pandas/core/window/indexers.py | 32 +++++++++++++++++++++- pandas/core/window/rolling.py | 10 +++---- pandas/tests/window/test_custom_indexer.py | 8 +++++- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/pandas/core/window/indexers.py b/pandas/core/window/indexers.py index f9a2eead9cb87..662e50e4ffc23 100644 --- a/pandas/core/window/indexers.py +++ b/pandas/core/window/indexers.py @@ -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] @@ -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, offset, keys): + 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 + ) diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index ec300d5578881..8deb6f0cb1c76 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -395,6 +395,8 @@ def _get_window_indexer(self, index_as_array): ------- VariableWindowIndexer or FixedWindowIndexer """ + if isinstance(self.window, BaseIndexer): + return self.window if self.is_freq_type: return VariableWindowIndexer(index=index_as_array) return FixedWindowIndexer(index=index_as_array) @@ -898,12 +900,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 diff --git a/pandas/tests/window/test_custom_indexer.py b/pandas/tests/window/test_custom_indexer.py index 1d32d6619422a..c3b013f8288c7 100644 --- a/pandas/tests/window/test_custom_indexer.py +++ b/pandas/tests/window/test_custom_indexer.py @@ -1,4 +1,5 @@ -from pandas import Series +from pandas import Series, date_range +from pandas.core.window.indexers import BusinessWindowIndexer def test_custom_indexer_validates( @@ -13,3 +14,8 @@ def test_custom_indexer_validates( min_periods=min_periods, closed=closed, ) + + +def test_rolling_business_day(): + s = Series(range(10), index=date_range("2019-09-06", "2019-09-13", freq="D")) + result = s.rolling(BusinessWindowIndexer(index=s.index, offset="B")).mean() From d09663a901de787e74bf13f8ae4c8a6286690bdf Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Tue, 10 Sep 2019 23:25:44 -0700 Subject: [PATCH 2/3] Fix tests and apply_window in _apply --- pandas/core/window/indexers.py | 2 +- pandas/core/window/rolling.py | 9 ++++++++- pandas/tests/window/test_custom_indexer.py | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pandas/core/window/indexers.py b/pandas/core/window/indexers.py index 662e50e4ffc23..a81d712190f79 100644 --- a/pandas/core/window/indexers.py +++ b/pandas/core/window/indexers.py @@ -239,7 +239,7 @@ def get_window_bounds( class BusinessWindowIndexer(VariableWindowIndexer): """Calculate window bounds based on business frequencies.""" - def __init__(self, index, offset, keys): + 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): diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 8deb6f0cb1c76..0609bc8e1910a 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -438,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) diff --git a/pandas/tests/window/test_custom_indexer.py b/pandas/tests/window/test_custom_indexer.py index c3b013f8288c7..782bd98605677 100644 --- a/pandas/tests/window/test_custom_indexer.py +++ b/pandas/tests/window/test_custom_indexer.py @@ -17,5 +17,6 @@ def test_custom_indexer_validates( def test_rolling_business_day(): - s = Series(range(10), index=date_range("2019-09-06", "2019-09-13", freq="D")) + 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() From e3c4a861a19b6c60c2ee615f3e17a7ae618b9683 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Wed, 11 Sep 2019 23:21:48 -0700 Subject: [PATCH 3/3] Return expected values --- pandas/tests/window/test_custom_indexer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandas/tests/window/test_custom_indexer.py b/pandas/tests/window/test_custom_indexer.py index 782bd98605677..07b78fb8fc395 100644 --- a/pandas/tests/window/test_custom_indexer.py +++ b/pandas/tests/window/test_custom_indexer.py @@ -1,5 +1,6 @@ from pandas import Series, date_range from pandas.core.window.indexers import BusinessWindowIndexer +import pandas.util.testing as tm def test_custom_indexer_validates( @@ -20,3 +21,5 @@ 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)