Skip to content

Commit 2755500

Browse files
committed
BUG: Fixes to FixedForwardWindowIndexer and GroupbyIndexer (pandas-dev#43267)
1 parent beb7c48 commit 2755500

File tree

8 files changed

+203
-28
lines changed

8 files changed

+203
-28
lines changed

doc/source/whatsnew/v1.4.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ Groupby/resample/rolling
355355
- Bug in :meth:`pandas.DataFrame.ewm`, where non-float64 dtypes were silently failing (:issue:`42452`)
356356
- Bug in :meth:`pandas.DataFrame.rolling` operation along rows (``axis=1``) incorrectly omits columns containing ``float16`` and ``float32`` (:issue:`41779`)
357357
- Bug in :meth:`Resampler.aggregate` did not allow the use of Named Aggregation (:issue:`32803`)
358-
-
358+
- Bug in :class:`GroupbyIndexer` and :class:`FixedForwardWindowIndexer` leading to segfaults and incorrect windows (:issue:`43267`)
359359

360360
Reshaping
361361
^^^^^^^^^

pandas/core/indexers/objects.py

+20-20
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ def get_window_bounds(
9393

9494
end = np.clip(end, 0, num_values)
9595
start = np.clip(start, 0, num_values)
96-
9796
return start, end
9897

9998

@@ -266,9 +265,8 @@ def get_window_bounds(
266265
)
267266

268267
start = np.arange(num_values, dtype="int64")
269-
end_s = start[: -self.window_size] + self.window_size
270-
end_e = np.full(self.window_size, num_values, dtype="int64")
271-
end = np.concatenate([end_s, end_e])
268+
end = start + self.window_size
269+
end[end > num_values] = num_values
272270

273271
return start, end
274272

@@ -280,7 +278,7 @@ def __init__(
280278
self,
281279
index_array: np.ndarray | None = None,
282280
window_size: int = 0,
283-
groupby_indicies: dict | None = None,
281+
groupby_indices: dict | None = None,
284282
window_indexer: type[BaseIndexer] = BaseIndexer,
285283
indexer_kwargs: dict | None = None,
286284
**kwargs,
@@ -294,7 +292,7 @@ def __init__(
294292
the groups
295293
window_size : int
296294
window size during the windowing operation
297-
groupby_indicies : dict or None
295+
groupby_indices : dict or None
298296
dict of {group label: [positional index of rows belonging to the group]}
299297
window_indexer : BaseIndexer
300298
BaseIndexer class determining the start and end bounds of each group
@@ -303,11 +301,13 @@ def __init__(
303301
**kwargs :
304302
keyword arguments that will be available when get_window_bounds is called
305303
"""
306-
self.groupby_indicies = groupby_indicies or {}
304+
self.groupby_indices = groupby_indices or {}
307305
self.window_indexer = window_indexer
308-
self.indexer_kwargs = indexer_kwargs or {}
306+
self.indexer_kwargs = indexer_kwargs.copy() if indexer_kwargs else {}
309307
super().__init__(
310-
index_array, self.indexer_kwargs.pop("window_size", window_size), **kwargs
308+
index_array=index_array,
309+
window_size=self.indexer_kwargs.pop("window_size", window_size),
310+
**kwargs,
311311
)
312312

313313
@Appender(get_window_bounds_doc)
@@ -323,8 +323,8 @@ def get_window_bounds(
323323
# 3) Append the window bounds in group order
324324
start_arrays = []
325325
end_arrays = []
326-
window_indicies_start = 0
327-
for key, indices in self.groupby_indicies.items():
326+
window_indices_start = 0
327+
for key, indices in self.groupby_indices.items():
328328
index_array: np.ndarray | None
329329

330330
if self.index_array is not None:
@@ -341,18 +341,18 @@ def get_window_bounds(
341341
)
342342
start = start.astype(np.int64)
343343
end = end.astype(np.int64)
344-
# Cannot use groupby_indicies as they might not be monotonic with the object
344+
# Cannot use groupby_indices as they might not be monotonic with the object
345345
# we're rolling over
346-
window_indicies = np.arange(
347-
window_indicies_start, window_indicies_start + len(indices)
346+
window_indices = np.arange(
347+
window_indices_start, window_indices_start + len(indices)
348348
)
349-
window_indicies_start += len(indices)
349+
window_indices_start += len(indices)
350350
# Extend as we'll be slicing window like [start, end)
351-
window_indicies = np.append(
352-
window_indicies, [window_indicies[-1] + 1]
353-
).astype(np.int64)
354-
start_arrays.append(window_indicies.take(ensure_platform_int(start)))
355-
end_arrays.append(window_indicies.take(ensure_platform_int(end)))
351+
window_indices = np.append(window_indices, [window_indices[-1] + 1]).astype(
352+
np.int64
353+
)
354+
start_arrays.append(window_indices.take(ensure_platform_int(start)))
355+
end_arrays.append(window_indices.take(ensure_platform_int(end)))
356356
start = np.concatenate(start_arrays)
357357
end = np.concatenate(end_arrays)
358358
return start, end

pandas/core/window/ewm.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ def _get_window_indexer(self) -> GroupbyIndexer:
725725
GroupbyIndexer
726726
"""
727727
window_indexer = GroupbyIndexer(
728-
groupby_indicies=self._grouper.indices,
728+
groupby_indices=self._grouper.indices,
729729
window_indexer=ExponentialMovingWindowIndexer,
730730
)
731731
return window_indexer

pandas/core/window/expanding.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,7 @@ def _get_window_indexer(self) -> GroupbyIndexer:
684684
GroupbyIndexer
685685
"""
686686
window_indexer = GroupbyIndexer(
687-
groupby_indicies=self._grouper.indices,
687+
groupby_indices=self._grouper.indices,
688688
window_indexer=ExpandingIndexer,
689689
)
690690
return window_indexer

pandas/core/window/rolling.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def __iter__(self):
299299
center=self.center,
300300
closed=self.closed,
301301
)
302+
302303
# From get_window_bounds, those two should be equal in length of array
303304
assert len(start) == len(end)
304305

@@ -529,6 +530,7 @@ def _apply(
529530
y : type of input
530531
"""
531532
window_indexer = self._get_window_indexer()
533+
532534
min_periods = (
533535
self.min_periods
534536
if self.min_periods is not None
@@ -542,12 +544,16 @@ def homogeneous_func(values: np.ndarray):
542544
return values.copy()
543545

544546
def calc(x):
547+
545548
start, end = window_indexer.get_window_bounds(
546549
num_values=len(x),
547550
min_periods=min_periods,
548551
center=self.center,
549552
closed=self.closed,
550553
)
554+
555+
# From get_window_bounds, those two should be equal in length of array
556+
assert len(start) == len(end)
551557
return func(x, start, end, min_periods, *numba_args)
552558

553559
with np.errstate(all="ignore"):
@@ -1429,6 +1435,10 @@ def cov_func(x, y):
14291435
center=self.center,
14301436
closed=self.closed,
14311437
)
1438+
1439+
# From get_window_bounds, those two should be equal in length of array
1440+
assert len(start) == len(end)
1441+
14321442
with np.errstate(all="ignore"):
14331443
mean_x_y = window_aggregations.roll_mean(
14341444
x_array * y_array, start, end, min_periods
@@ -1468,6 +1478,10 @@ def corr_func(x, y):
14681478
center=self.center,
14691479
closed=self.closed,
14701480
)
1481+
1482+
# From get_window_bounds, those two should be equal in length of array
1483+
assert len(start) == len(end)
1484+
14711485
with np.errstate(all="ignore"):
14721486
mean_x_y = window_aggregations.roll_mean(
14731487
x_array * y_array, start, end, min_periods
@@ -2341,26 +2355,29 @@ def _get_window_indexer(self) -> GroupbyIndexer:
23412355
rolling_indexer: type[BaseIndexer]
23422356
indexer_kwargs: dict[str, Any] | None = None
23432357
index_array = self._index_array
2358+
23442359
if isinstance(self.window, BaseIndexer):
23452360
rolling_indexer = type(self.window)
2346-
indexer_kwargs = self.window.__dict__
2361+
indexer_kwargs = self.window.__dict__.copy()
23472362
assert isinstance(indexer_kwargs, dict) # for mypy
23482363
# We'll be using the index of each group later
23492364
indexer_kwargs.pop("index_array", None)
2350-
window = 0
2365+
window = self.window
23512366
elif self._win_freq_i8 is not None:
23522367
rolling_indexer = VariableWindowIndexer
23532368
window = self._win_freq_i8
23542369
else:
23552370
rolling_indexer = FixedWindowIndexer
23562371
window = self.window
2372+
23572373
window_indexer = GroupbyIndexer(
23582374
index_array=index_array,
23592375
window_size=window,
2360-
groupby_indicies=self._grouper.indices,
2376+
groupby_indices=self._grouper.indices,
23612377
window_indexer=rolling_indexer,
23622378
indexer_kwargs=indexer_kwargs,
23632379
)
2380+
23642381
return window_indexer
23652382

23662383
def _validate_monotonic(self):

pandas/tests/groupby/test_missing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def test_min_count(func, min_count, value):
144144
tm.assert_frame_equal(result, expected)
145145

146146

147-
def test_indicies_with_missing():
147+
def test_indices_with_missing():
148148
# GH 9304
149149
df = DataFrame({"a": [1, 1, np.nan], "b": [2, 3, 4], "c": [5, 6, 7]})
150150
g = df.groupby(["a", "b"])

pandas/tests/window/test_base_indexer.py

+49
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414
from pandas.core.indexers.objects import (
1515
ExpandingIndexer,
16+
FixedWindowIndexer,
1617
VariableOffsetWindowIndexer,
1718
)
1819

@@ -293,3 +294,51 @@ def get_window_bounds(self, num_values, min_periods, center, closed):
293294
result = getattr(df.rolling(indexer), func)(*args)
294295
expected = DataFrame({"values": values})
295296
tm.assert_frame_equal(result, expected)
297+
298+
299+
@pytest.mark.parametrize(
300+
"indexer_class", [FixedWindowIndexer, FixedForwardWindowIndexer, ExpandingIndexer]
301+
)
302+
@pytest.mark.parametrize("window_size", [1, 2, 12])
303+
@pytest.mark.parametrize(
304+
"df",
305+
[
306+
DataFrame({"a": [1, 1], "b": [0, 1]}),
307+
DataFrame({"a": [1, 2], "b": [0, 1]}),
308+
DataFrame({"b": [np.nan, 1, 2, np.nan] + list(range(4, 16))}).assign(a=1),
309+
],
310+
)
311+
def test_indexers_are_reusable_after_groupby_rolling(indexer_class, window_size, df):
312+
# GH 43267
313+
num_trials = 3
314+
indexer = indexer_class(window_size=window_size)
315+
original_window_size = indexer.window_size
316+
for i in range(num_trials):
317+
df.groupby("a")["b"].rolling(window=indexer, min_periods=1).mean()
318+
assert indexer.window_size == original_window_size
319+
320+
321+
@pytest.mark.parametrize(
322+
"window_size, num_values, expected_start, expected_end",
323+
[
324+
(1, 1, [0], [1]),
325+
(1, 2, [0, 1], [1, 2]),
326+
(2, 1, [0], [1]),
327+
(2, 2, [0, 1], [2, 2]),
328+
(5, 12, range(12), list(range(5, 12)) + [12] * 5),
329+
(12, 5, range(5), [5] * 5),
330+
(0, 0, np.array([], dtype="int64"), np.array([], dtype="int64")),
331+
(1, 0, np.array([], dtype="int64"), np.array([], dtype="int64")),
332+
(0, 1, [0], [0]),
333+
],
334+
)
335+
def test_fixed_forward_indexer_bounds(
336+
window_size, num_values, expected_start, expected_end
337+
):
338+
# GH 43267
339+
indexer = FixedForwardWindowIndexer(window_size=window_size)
340+
start, end = indexer.get_window_bounds(num_values=num_values)
341+
342+
tm.assert_equal(start, np.array(expected_start, dtype='int64'))
343+
tm.assert_equal(end, np.array(expected_end, dtype='int64'))
344+
assert len(start) == len(end)

0 commit comments

Comments
 (0)