Skip to content

Commit 499b5ef

Browse files
authored
ENH: Support all Scipy window types in rolling(..., win_type) (#37204)
1 parent 2b93155 commit 499b5ef

File tree

5 files changed

+34
-109
lines changed

5 files changed

+34
-109
lines changed

doc/source/user_guide/computation.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,10 @@ The list of recognized types are the `scipy.signal window functions
451451
* ``slepian`` (needs width)
452452
* ``exponential`` (needs tau).
453453

454+
.. versionadded:: 1.2.0
455+
456+
All Scipy window types, concurrent with your installed version, are recognized ``win_types``.
457+
454458
.. ipython:: python
455459
456460
ser = pd.Series(np.random.randn(10), index=pd.date_range("1/1/2000", periods=10))

doc/source/whatsnew/v1.2.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ Other enhancements
227227
- :meth:`DataFrame.to_parquet` now returns a ``bytes`` object when no ``path`` argument is passed (:issue:`37105`)
228228
- :class:`Rolling` now supports the ``closed`` argument for fixed windows (:issue:`34315`)
229229
- :class:`DatetimeIndex` and :class:`Series` with ``datetime64`` or ``datetime64tz`` dtypes now support ``std`` (:issue:`37436`)
230+
- :class:`Window` now supports all Scipy window types in ``win_type`` with flexible keyword argument support (:issue:`34556`)
230231

231232
.. _whatsnew_120.api_breaking.python:
232233

pandas/core/window/rolling.py

Lines changed: 18 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -873,30 +873,14 @@ class Window(BaseWindow):
873873
To learn more about the offsets & frequency strings, please see `this link
874874
<https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases>`__.
875875
876-
The recognized win_types are:
877-
878-
* ``boxcar``
879-
* ``triang``
880-
* ``blackman``
881-
* ``hamming``
882-
* ``bartlett``
883-
* ``parzen``
884-
* ``bohman``
885-
* ``blackmanharris``
886-
* ``nuttall``
887-
* ``barthann``
888-
* ``kaiser`` (needs parameter: beta)
889-
* ``gaussian`` (needs parameter: std)
890-
* ``general_gaussian`` (needs parameters: power, width)
891-
* ``slepian`` (needs parameter: width)
892-
* ``exponential`` (needs parameter: tau), center is set to None.
893-
894-
If ``win_type=None`` all points are evenly weighted. To learn more about
895-
different window types see `scipy.signal window functions
876+
If ``win_type=None``, all points are evenly weighted; otherwise, ``win_type``
877+
can accept a string of any `scipy.signal window function
896878
<https://docs.scipy.org/doc/scipy/reference/signal.windows.html#module-scipy.signal.windows>`__.
897879
898-
Certain window types require additional parameters to be passed. Please see
899-
the third example below on how to add the additional parameters.
880+
Certain Scipy window types require additional parameters to be passed
881+
in the aggregation function. The additional parameters must match
882+
the keywords specified in the Scipy window type method signature.
883+
Please see the third example below on how to add the additional parameters.
900884
901885
Examples
902886
--------
@@ -1000,71 +984,22 @@ def _constructor(self):
1000984
def validate(self):
1001985
super().validate()
1002986

1003-
window = self.window
1004-
if isinstance(window, BaseIndexer):
987+
if isinstance(self.window, BaseIndexer):
1005988
raise NotImplementedError(
1006989
"BaseIndexer subclasses not implemented with win_types."
1007990
)
1008-
elif isinstance(window, (list, tuple, np.ndarray)):
1009-
pass
1010-
elif is_integer(window):
1011-
if window <= 0:
991+
elif is_integer(self.window):
992+
if self.window <= 0:
1012993
raise ValueError("window must be > 0 ")
1013-
import_optional_dependency(
1014-
"scipy", extra="Scipy is required to generate window weight."
994+
sig = import_optional_dependency(
995+
"scipy.signal", extra="Scipy is required to generate window weight."
1015996
)
1016-
import scipy.signal as sig
1017-
1018997
if not isinstance(self.win_type, str):
1019998
raise ValueError(f"Invalid win_type {self.win_type}")
1020999
if getattr(sig, self.win_type, None) is None:
10211000
raise ValueError(f"Invalid win_type {self.win_type}")
10221001
else:
1023-
raise ValueError(f"Invalid window {window}")
1024-
1025-
def _get_win_type(self, kwargs: Dict[str, Any]) -> Union[str, Tuple]:
1026-
"""
1027-
Extract arguments for the window type, provide validation for it
1028-
and return the validated window type.
1029-
1030-
Parameters
1031-
----------
1032-
kwargs : dict
1033-
1034-
Returns
1035-
-------
1036-
win_type : str, or tuple
1037-
"""
1038-
# the below may pop from kwargs
1039-
def _validate_win_type(win_type, kwargs):
1040-
arg_map = {
1041-
"kaiser": ["beta"],
1042-
"gaussian": ["std"],
1043-
"general_gaussian": ["power", "width"],
1044-
"slepian": ["width"],
1045-
"exponential": ["tau"],
1046-
}
1047-
1048-
if win_type in arg_map:
1049-
win_args = _pop_args(win_type, arg_map[win_type], kwargs)
1050-
if win_type == "exponential":
1051-
# exponential window requires the first arg (center)
1052-
# to be set to None (necessary for symmetric window)
1053-
win_args.insert(0, None)
1054-
1055-
return tuple([win_type] + win_args)
1056-
1057-
return win_type
1058-
1059-
def _pop_args(win_type, arg_names, kwargs):
1060-
all_args = []
1061-
for n in arg_names:
1062-
if n not in kwargs:
1063-
raise ValueError(f"{win_type} window requires {n}")
1064-
all_args.append(kwargs.pop(n))
1065-
return all_args
1066-
1067-
return _validate_win_type(self.win_type, kwargs)
1002+
raise ValueError(f"Invalid window {self.window}")
10681003

10691004
def _center_window(self, result: np.ndarray, offset: int) -> np.ndarray:
10701005
"""
@@ -1079,31 +1014,6 @@ def _center_window(self, result: np.ndarray, offset: int) -> np.ndarray:
10791014
result = np.copy(result[tuple(lead_indexer)])
10801015
return result
10811016

1082-
def _get_window_weights(
1083-
self, win_type: Optional[Union[str, Tuple]] = None
1084-
) -> np.ndarray:
1085-
"""
1086-
Get the window, weights.
1087-
1088-
Parameters
1089-
----------
1090-
win_type : str, or tuple
1091-
type of window to create
1092-
1093-
Returns
1094-
-------
1095-
window : ndarray
1096-
the window, weights
1097-
"""
1098-
window = self.window
1099-
if isinstance(window, (list, tuple, np.ndarray)):
1100-
return com.asarray_tuplesafe(window).astype(float)
1101-
elif is_integer(window):
1102-
import scipy.signal as sig
1103-
1104-
# GH #15662. `False` makes symmetric window, rather than periodic.
1105-
return sig.get_window(win_type, window, False).astype(float)
1106-
11071017
def _apply(
11081018
self,
11091019
func: Callable[[np.ndarray, int, int], np.ndarray],
@@ -1124,14 +1034,17 @@ def _apply(
11241034
whether to cache a numba compiled function. Only available for numba
11251035
enabled methods (so far only apply)
11261036
**kwargs
1127-
additional arguments for rolling function and window function
1037+
additional arguments for scipy windows if necessary
11281038
11291039
Returns
11301040
-------
11311041
y : type of input
11321042
"""
1133-
win_type = self._get_win_type(kwargs)
1134-
window = self._get_window_weights(win_type=win_type)
1043+
signal = import_optional_dependency(
1044+
"scipy.signal", extra="Scipy is required to generate window weight."
1045+
)
1046+
assert self.win_type is not None # for mypy
1047+
window = getattr(signal, self.win_type)(self.window, **kwargs)
11351048
offset = (len(window) - 1) // 2 if self.center else 0
11361049

11371050
def homogeneous_func(values: np.ndarray):

pandas/tests/window/moments/test_moments_rolling.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ def test_cmov_window_special(win_types_special):
431431
kwds = {
432432
"kaiser": {"beta": 1.0},
433433
"gaussian": {"std": 1.0},
434-
"general_gaussian": {"power": 2.0, "width": 2.0},
434+
"general_gaussian": {"p": 2.0, "sig": 2.0},
435435
"exponential": {"tau": 10},
436436
}
437437

@@ -503,7 +503,7 @@ def test_cmov_window_special_linear_range(win_types_special):
503503
kwds = {
504504
"kaiser": {"beta": 1.0},
505505
"gaussian": {"std": 1.0},
506-
"general_gaussian": {"power": 2.0, "width": 2.0},
506+
"general_gaussian": {"p": 2.0, "sig": 2.0},
507507
"slepian": {"width": 0.5},
508508
"exponential": {"tau": 10},
509509
}

pandas/tests/window/test_window.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import pandas as pd
88
from pandas import Series
9-
from pandas.core.window import Window
109

1110

1211
@td.skip_if_no_scipy
@@ -50,7 +49,7 @@ def test_constructor_with_win_type(which, win_types):
5049
@pytest.mark.parametrize("method", ["sum", "mean"])
5150
def test_numpy_compat(method):
5251
# see gh-12811
53-
w = Window(Series([2, 4, 6]), window=[0, 2])
52+
w = Series([2, 4, 6]).rolling(window=2)
5453

5554
msg = "numpy operations are not valid with window objects"
5655

@@ -75,3 +74,11 @@ def test_agg_function_support(arg):
7574

7675
with pytest.raises(AttributeError, match=msg):
7776
roll.agg({"A": arg})
77+
78+
79+
@td.skip_if_no_scipy
80+
def test_invalid_scipy_arg():
81+
# This error is raised by scipy
82+
msg = r"boxcar\(\) got an unexpected"
83+
with pytest.raises(TypeError, match=msg):
84+
Series(range(3)).rolling(1, win_type="boxcar").mean(foo="bar")

0 commit comments

Comments
 (0)