diff --git a/doc/source/user_guide/computation.rst b/doc/source/user_guide/computation.rst index b9f0683697ba6..45d15f29fcce8 100644 --- a/doc/source/user_guide/computation.rst +++ b/doc/source/user_guide/computation.rst @@ -451,6 +451,10 @@ The list of recognized types are the `scipy.signal window functions * ``slepian`` (needs width) * ``exponential`` (needs tau). +.. versionadded:: 1.2.0 + +All Scipy window types, concurrent with your installed version, are recognized ``win_types``. + .. ipython:: python ser = pd.Series(np.random.randn(10), index=pd.date_range("1/1/2000", periods=10)) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index 83614d7a9628b..c6edf4eb0e88e 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -226,6 +226,7 @@ Other enhancements - :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`) - :class:`DatetimeIndex` and :class:`Series` with ``datetime64`` or ``datetime64tz`` dtypes now support ``std`` (:issue:`37436`) +- :class:`Window` now supports all Scipy window types in ``win_type`` with flexible keyword argument support (:issue:`34556`) .. _whatsnew_120.api_breaking.python: diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index bfc31021a8f87..a976350a419fe 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -873,30 +873,14 @@ class Window(BaseWindow): To learn more about the offsets & frequency strings, please see `this link `__. - The recognized win_types are: - - * ``boxcar`` - * ``triang`` - * ``blackman`` - * ``hamming`` - * ``bartlett`` - * ``parzen`` - * ``bohman`` - * ``blackmanharris`` - * ``nuttall`` - * ``barthann`` - * ``kaiser`` (needs parameter: beta) - * ``gaussian`` (needs parameter: std) - * ``general_gaussian`` (needs parameters: power, width) - * ``slepian`` (needs parameter: width) - * ``exponential`` (needs parameter: tau), center is set to None. - - If ``win_type=None`` all points are evenly weighted. To learn more about - different window types see `scipy.signal window functions + If ``win_type=None``, all points are evenly weighted; otherwise, ``win_type`` + can accept a string of any `scipy.signal window function `__. - Certain window types require additional parameters to be passed. Please see - the third example below on how to add the additional parameters. + Certain Scipy window types require additional parameters to be passed + in the aggregation function. The additional parameters must match + the keywords specified in the Scipy window type method signature. + Please see the third example below on how to add the additional parameters. Examples -------- @@ -1000,71 +984,22 @@ def _constructor(self): def validate(self): super().validate() - window = self.window - if isinstance(window, BaseIndexer): + if isinstance(self.window, BaseIndexer): raise NotImplementedError( "BaseIndexer subclasses not implemented with win_types." ) - elif isinstance(window, (list, tuple, np.ndarray)): - pass - elif is_integer(window): - if window <= 0: + elif is_integer(self.window): + if self.window <= 0: raise ValueError("window must be > 0 ") - import_optional_dependency( - "scipy", extra="Scipy is required to generate window weight." + sig = import_optional_dependency( + "scipy.signal", extra="Scipy is required to generate window weight." ) - import scipy.signal as sig - if not isinstance(self.win_type, str): raise ValueError(f"Invalid win_type {self.win_type}") if getattr(sig, self.win_type, None) is None: raise ValueError(f"Invalid win_type {self.win_type}") else: - raise ValueError(f"Invalid window {window}") - - def _get_win_type(self, kwargs: Dict[str, Any]) -> Union[str, Tuple]: - """ - Extract arguments for the window type, provide validation for it - and return the validated window type. - - Parameters - ---------- - kwargs : dict - - Returns - ------- - win_type : str, or tuple - """ - # the below may pop from kwargs - def _validate_win_type(win_type, kwargs): - arg_map = { - "kaiser": ["beta"], - "gaussian": ["std"], - "general_gaussian": ["power", "width"], - "slepian": ["width"], - "exponential": ["tau"], - } - - if win_type in arg_map: - win_args = _pop_args(win_type, arg_map[win_type], kwargs) - if win_type == "exponential": - # exponential window requires the first arg (center) - # to be set to None (necessary for symmetric window) - win_args.insert(0, None) - - return tuple([win_type] + win_args) - - return win_type - - def _pop_args(win_type, arg_names, kwargs): - all_args = [] - for n in arg_names: - if n not in kwargs: - raise ValueError(f"{win_type} window requires {n}") - all_args.append(kwargs.pop(n)) - return all_args - - return _validate_win_type(self.win_type, kwargs) + raise ValueError(f"Invalid window {self.window}") def _center_window(self, result: np.ndarray, offset: int) -> np.ndarray: """ @@ -1079,31 +1014,6 @@ def _center_window(self, result: np.ndarray, offset: int) -> np.ndarray: result = np.copy(result[tuple(lead_indexer)]) return result - def _get_window_weights( - self, win_type: Optional[Union[str, Tuple]] = None - ) -> np.ndarray: - """ - Get the window, weights. - - Parameters - ---------- - win_type : str, or tuple - type of window to create - - Returns - ------- - window : ndarray - the window, weights - """ - window = self.window - if isinstance(window, (list, tuple, np.ndarray)): - return com.asarray_tuplesafe(window).astype(float) - elif is_integer(window): - import scipy.signal as sig - - # GH #15662. `False` makes symmetric window, rather than periodic. - return sig.get_window(win_type, window, False).astype(float) - def _apply( self, func: Callable[[np.ndarray, int, int], np.ndarray], @@ -1124,14 +1034,17 @@ def _apply( whether to cache a numba compiled function. Only available for numba enabled methods (so far only apply) **kwargs - additional arguments for rolling function and window function + additional arguments for scipy windows if necessary Returns ------- y : type of input """ - win_type = self._get_win_type(kwargs) - window = self._get_window_weights(win_type=win_type) + signal = import_optional_dependency( + "scipy.signal", extra="Scipy is required to generate window weight." + ) + assert self.win_type is not None # for mypy + window = getattr(signal, self.win_type)(self.window, **kwargs) offset = (len(window) - 1) // 2 if self.center else 0 def homogeneous_func(values: np.ndarray): diff --git a/pandas/tests/window/moments/test_moments_rolling.py b/pandas/tests/window/moments/test_moments_rolling.py index 2f622c2bc3e60..39b3a9a630760 100644 --- a/pandas/tests/window/moments/test_moments_rolling.py +++ b/pandas/tests/window/moments/test_moments_rolling.py @@ -431,7 +431,7 @@ def test_cmov_window_special(win_types_special): kwds = { "kaiser": {"beta": 1.0}, "gaussian": {"std": 1.0}, - "general_gaussian": {"power": 2.0, "width": 2.0}, + "general_gaussian": {"p": 2.0, "sig": 2.0}, "exponential": {"tau": 10}, } @@ -503,7 +503,7 @@ def test_cmov_window_special_linear_range(win_types_special): kwds = { "kaiser": {"beta": 1.0}, "gaussian": {"std": 1.0}, - "general_gaussian": {"power": 2.0, "width": 2.0}, + "general_gaussian": {"p": 2.0, "sig": 2.0}, "slepian": {"width": 0.5}, "exponential": {"tau": 10}, } diff --git a/pandas/tests/window/test_window.py b/pandas/tests/window/test_window.py index a3fff3122f80a..eab62b3383283 100644 --- a/pandas/tests/window/test_window.py +++ b/pandas/tests/window/test_window.py @@ -6,7 +6,6 @@ import pandas as pd from pandas import Series -from pandas.core.window import Window @td.skip_if_no_scipy @@ -50,7 +49,7 @@ def test_constructor_with_win_type(which, win_types): @pytest.mark.parametrize("method", ["sum", "mean"]) def test_numpy_compat(method): # see gh-12811 - w = Window(Series([2, 4, 6]), window=[0, 2]) + w = Series([2, 4, 6]).rolling(window=2) msg = "numpy operations are not valid with window objects" @@ -75,3 +74,11 @@ def test_agg_function_support(arg): with pytest.raises(AttributeError, match=msg): roll.agg({"A": arg}) + + +@td.skip_if_no_scipy +def test_invalid_scipy_arg(): + # This error is raised by scipy + msg = r"boxcar\(\) got an unexpected" + with pytest.raises(TypeError, match=msg): + Series(range(3)).rolling(1, win_type="boxcar").mean(foo="bar")