Skip to content

Commit 3d7f2c0

Browse files
leftysNico Cernek
authored and
Nico Cernek
committed
fix Rolling for multi-index and reversed index. (pandas-dev#28297)
1 parent bef9ef3 commit 3d7f2c0

File tree

3 files changed

+66
-15
lines changed

3 files changed

+66
-15
lines changed

doc/source/whatsnew/v1.0.0.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,9 @@ Plotting
398398
Groupby/resample/rolling
399399
^^^^^^^^^^^^^^^^^^^^^^^^
400400

401-
-
402401
- Bug in :meth:`DataFrame.rolling` not allowing for rolling over datetimes when ``axis=1`` (:issue: `28192`)
402+
- Bug in :meth:`DataFrame.rolling` not allowing rolling over multi-index levels (:issue: `15584`).
403+
- Bug in :meth:`DataFrame.rolling` not allowing rolling on monotonic decreasing time indexes (:issue: `19248`).
403404
- Bug in :meth:`DataFrame.groupby` not offering selection by column name when ``axis=1`` (:issue:`27614`)
404405
- Bug in :meth:`DataFrameGroupby.agg` not able to use lambda function with named aggregation (:issue:`27519`)
405406
- Bug in :meth:`DataFrame.groupby` losing column name information when grouping by a categorical column (:issue:`28787`)

pandas/core/window/rolling.py

+14-11
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(
7070
center: Optional[bool] = False,
7171
win_type: Optional[str] = None,
7272
axis: Axis = 0,
73-
on: Optional[str] = None,
73+
on: Optional[Union[str, Index]] = None,
7474
closed: Optional[str] = None,
7575
**kwargs
7676
):
@@ -126,7 +126,7 @@ def _create_blocks(self):
126126
obj = self._selected_obj
127127

128128
# filter out the on from the object
129-
if self.on is not None:
129+
if self.on is not None and not isinstance(self.on, Index):
130130
if obj.ndim == 2:
131131
obj = obj.reindex(columns=obj.columns.difference([self.on]), copy=False)
132132
blocks = obj._to_dict_of_blocks(copy=False).values()
@@ -637,10 +637,10 @@ class Window(_Window):
637637
Provide a window type. If ``None``, all points are evenly weighted.
638638
See the notes below for further information.
639639
on : str, optional
640-
For a DataFrame, a datetime-like column on which to calculate the rolling
641-
window, rather than the DataFrame's index. Provided integer column is
642-
ignored and excluded from result since an integer index is not used to
643-
calculate the rolling window.
640+
For a DataFrame, a datetime-like column or MultiIndex level on which
641+
to calculate the rolling window, rather than the DataFrame's index.
642+
Provided integer column is ignored and excluded from result since
643+
an integer index is not used to calculate the rolling window.
644644
axis : int or str, default 0
645645
closed : str, default None
646646
Make the interval closed on the 'right', 'left', 'both' or
@@ -1651,18 +1651,19 @@ def is_datetimelike(self):
16511651

16521652
@cache_readonly
16531653
def _on(self):
1654-
16551654
if self.on is None:
16561655
if self.axis == 0:
16571656
return self.obj.index
16581657
elif self.axis == 1:
16591658
return self.obj.columns
1659+
elif isinstance(self.on, Index):
1660+
return self.on
16601661
elif isinstance(self.obj, ABCDataFrame) and self.on in self.obj.columns:
16611662
return Index(self.obj[self.on])
16621663
else:
16631664
raise ValueError(
16641665
"invalid on specified as {0}, "
1665-
"must be a column (if DataFrame) "
1666+
"must be a column (of DataFrame), an Index "
16661667
"or None".format(self.on)
16671668
)
16681669

@@ -1706,10 +1707,12 @@ def validate(self):
17061707

17071708
def _validate_monotonic(self):
17081709
"""
1709-
Validate on is_monotonic.
1710+
Validate monotonic (increasing or decreasing).
17101711
"""
1711-
if not self._on.is_monotonic:
1712-
formatted = self.on or "index"
1712+
if not (self._on.is_monotonic_increasing or self._on.is_monotonic_decreasing):
1713+
formatted = self.on
1714+
if self.on is None:
1715+
formatted = "index"
17131716
raise ValueError("{0} must be monotonic".format(formatted))
17141717

17151718
def _validate_freq(self):

pandas/tests/window/test_timeseries_window.py

+50-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import numpy as np
22
import pytest
33

4-
from pandas import DataFrame, Index, Series, Timestamp, date_range, to_datetime
4+
from pandas import (
5+
DataFrame,
6+
Index,
7+
MultiIndex,
8+
Series,
9+
Timestamp,
10+
date_range,
11+
to_datetime,
12+
)
513
import pandas.util.testing as tm
614

715
import pandas.tseries.offsets as offsets
@@ -105,8 +113,16 @@ def test_monotonic_on(self):
105113
assert df.index.is_monotonic
106114
df.rolling("2s").sum()
107115

108-
# non-monotonic
109-
df.index = reversed(df.index.tolist())
116+
def test_non_monotonic_on(self):
117+
# GH 19248
118+
df = DataFrame(
119+
{"A": date_range("20130101", periods=5, freq="s"), "B": range(5)}
120+
)
121+
df = df.set_index("A")
122+
non_monotonic_index = df.index.to_list()
123+
non_monotonic_index[0] = non_monotonic_index[3]
124+
df.index = non_monotonic_index
125+
110126
assert not df.index.is_monotonic
111127

112128
with pytest.raises(ValueError):
@@ -690,3 +706,34 @@ def test_rolling_cov_offset(self):
690706

691707
expected2 = ss.rolling(3, min_periods=1).cov()
692708
tm.assert_series_equal(result, expected2)
709+
710+
def test_rolling_on_decreasing_index(self):
711+
# GH-19248
712+
index = [
713+
Timestamp("20190101 09:00:00"),
714+
Timestamp("20190101 09:00:02"),
715+
Timestamp("20190101 09:00:03"),
716+
Timestamp("20190101 09:00:05"),
717+
Timestamp("20190101 09:00:06"),
718+
]
719+
720+
df = DataFrame({"column": [3, 4, 4, 2, 1]}, index=reversed(index))
721+
result = df.rolling("2s").min()
722+
expected = DataFrame(
723+
{"column": [3.0, 3.0, 3.0, 2.0, 1.0]}, index=reversed(index)
724+
)
725+
tm.assert_frame_equal(result, expected)
726+
727+
def test_rolling_on_multi_index_level(self):
728+
# GH-15584
729+
df = DataFrame(
730+
{"column": range(6)},
731+
index=MultiIndex.from_product(
732+
[date_range("20190101", periods=3), range(2)], names=["date", "seq"]
733+
),
734+
)
735+
result = df.rolling("10d", on=df.index.get_level_values("date")).sum()
736+
expected = DataFrame(
737+
{"column": [0.0, 1.0, 3.0, 6.0, 10.0, 15.0]}, index=df.index
738+
)
739+
tm.assert_frame_equal(result, expected)

0 commit comments

Comments
 (0)