Skip to content

Commit 5d520c6

Browse files
authored
PERF: Improve performance in rolling.mean(engine=numba) (#44176)
1 parent 5401aea commit 5d520c6

File tree

7 files changed

+133
-27
lines changed

7 files changed

+133
-27
lines changed

doc/source/whatsnew/v1.4.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ Performance improvements
426426
- :meth:`SparseArray.min` and :meth:`SparseArray.max` no longer require converting to a dense array (:issue:`43526`)
427427
- Indexing into a :class:`SparseArray` with a ``slice`` with ``step=1`` no longer requires converting to a dense array (:issue:`43777`)
428428
- Performance improvement in :meth:`SparseArray.take` with ``allow_fill=False`` (:issue:`43654`)
429-
- Performance improvement in :meth:`.Rolling.mean` and :meth:`.Expanding.mean` with ``engine="numba"`` (:issue:`43612`)
429+
- Performance improvement in :meth:`.Rolling.mean`, :meth:`.Expanding.mean`, :meth:`.Rolling.sum`, :meth:`.Expanding.sum` with ``engine="numba"`` (:issue:`43612`, :issue:`44176`)
430430
- Improved performance of :meth:`pandas.read_csv` with ``memory_map=True`` when file encoding is UTF-8 (:issue:`43787`)
431431
- Performance improvement in :meth:`RangeIndex.sort_values` overriding :meth:`Index.sort_values` (:issue:`43666`)
432432
- Performance improvement in :meth:`RangeIndex.insert` (:issue:`43988`)
+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from pandas.core._numba.kernels.mean_ import sliding_mean
2+
from pandas.core._numba.kernels.sum_ import sliding_sum
23

3-
__all__ = ["sliding_mean"]
4+
__all__ = ["sliding_mean", "sliding_sum"]

pandas/core/_numba/kernels/mean_.py

+2-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Numba 1D aggregation kernels that can be shared by
2+
Numba 1D mean kernels that can be shared by
33
* Dataframe / Series
44
* groupby
55
* rolling / expanding
@@ -11,20 +11,7 @@
1111
import numba
1212
import numpy as np
1313

14-
15-
@numba.jit(nopython=True, nogil=True, parallel=False)
16-
def is_monotonic_increasing(bounds: np.ndarray) -> bool:
17-
"""Check if int64 values are monotonically increasing."""
18-
n = len(bounds)
19-
if n < 2:
20-
return True
21-
prev = bounds[0]
22-
for i in range(1, n):
23-
cur = bounds[i]
24-
if cur < prev:
25-
return False
26-
prev = cur
27-
return True
14+
from pandas.core._numba.kernels.shared import is_monotonic_increasing
2815

2916

3017
@numba.jit(nopython=True, nogil=True, parallel=False)

pandas/core/_numba/kernels/shared.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import numba
2+
import numpy as np
3+
4+
5+
@numba.jit(numba.boolean(numba.int64[:]), nopython=True, nogil=True, parallel=False)
6+
def is_monotonic_increasing(bounds: np.ndarray) -> bool:
7+
"""Check if int64 values are monotonically increasing."""
8+
n = len(bounds)
9+
if n < 2:
10+
return True
11+
prev = bounds[0]
12+
for i in range(1, n):
13+
cur = bounds[i]
14+
if cur < prev:
15+
return False
16+
prev = cur
17+
return True

pandas/core/_numba/kernels/sum_.py

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Numba 1D sum kernels that can be shared by
3+
* Dataframe / Series
4+
* groupby
5+
* rolling / expanding
6+
7+
Mirrors pandas/_libs/window/aggregation.pyx
8+
"""
9+
from __future__ import annotations
10+
11+
import numba
12+
import numpy as np
13+
14+
from pandas.core._numba.kernels.shared import is_monotonic_increasing
15+
16+
17+
@numba.jit(nopython=True, nogil=True, parallel=False)
18+
def add_sum(
19+
val: float, nobs: int, sum_x: float, compensation: float
20+
) -> tuple[int, float, float]:
21+
if not np.isnan(val):
22+
nobs += 1
23+
y = val - compensation
24+
t = sum_x + y
25+
compensation = t - sum_x - y
26+
sum_x = t
27+
return nobs, sum_x, compensation
28+
29+
30+
@numba.jit(nopython=True, nogil=True, parallel=False)
31+
def remove_sum(
32+
val: float, nobs: int, sum_x: float, compensation: float
33+
) -> tuple[int, float, float]:
34+
if not np.isnan(val):
35+
nobs -= 1
36+
y = -val - compensation
37+
t = sum_x + y
38+
compensation = t - sum_x - y
39+
sum_x = t
40+
return nobs, sum_x, compensation
41+
42+
43+
@numba.jit(nopython=True, nogil=True, parallel=False)
44+
def sliding_sum(
45+
values: np.ndarray,
46+
start: np.ndarray,
47+
end: np.ndarray,
48+
min_periods: int,
49+
) -> np.ndarray:
50+
N = len(start)
51+
nobs = 0
52+
sum_x = 0.0
53+
compensation_add = 0.0
54+
compensation_remove = 0.0
55+
56+
is_monotonic_increasing_bounds = is_monotonic_increasing(
57+
start
58+
) and is_monotonic_increasing(end)
59+
60+
output = np.empty(N, dtype=np.float64)
61+
62+
for i in range(N):
63+
s = start[i]
64+
e = end[i]
65+
if i == 0 or not is_monotonic_increasing_bounds:
66+
for j in range(s, e):
67+
val = values[j]
68+
nobs, sum_x, compensation_add = add_sum(
69+
val, nobs, sum_x, compensation_add
70+
)
71+
else:
72+
for j in range(start[i - 1], s):
73+
val = values[j]
74+
nobs, sum_x, compensation_remove = remove_sum(
75+
val, nobs, sum_x, compensation_remove
76+
)
77+
78+
for j in range(end[i - 1], e):
79+
val = values[j]
80+
nobs, sum_x, compensation_add = add_sum(
81+
val, nobs, sum_x, compensation_add
82+
)
83+
84+
if nobs == 0 == nobs:
85+
result = 0.0
86+
elif nobs >= min_periods:
87+
result = sum_x
88+
else:
89+
result = np.nan
90+
91+
output[i] = result
92+
93+
if not is_monotonic_increasing_bounds:
94+
nobs = 0
95+
sum_x = 0.0
96+
compensation_remove = 0.0
97+
98+
return output

pandas/core/window/rolling.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -1345,15 +1345,16 @@ def sum(
13451345
if maybe_use_numba(engine):
13461346
if self.method == "table":
13471347
func = generate_manual_numpy_nan_agg_with_axis(np.nansum)
1348+
return self.apply(
1349+
func,
1350+
raw=True,
1351+
engine=engine,
1352+
engine_kwargs=engine_kwargs,
1353+
)
13481354
else:
1349-
func = np.nansum
1355+
from pandas.core._numba.kernels import sliding_sum
13501356

1351-
return self.apply(
1352-
func,
1353-
raw=True,
1354-
engine=engine,
1355-
engine_kwargs=engine_kwargs,
1356-
)
1357+
return self._numba_apply(sliding_sum, "rolling_sum", engine_kwargs)
13571358
window_func = window_aggregations.roll_sum
13581359
return self._apply(window_func, name="sum", **kwargs)
13591360

pandas/tests/window/test_numba.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,17 @@ def test_numba_vs_cython_rolling_methods(
5959
expected = getattr(roll, method)(engine="cython")
6060

6161
# Check the cache
62-
if method != "mean":
62+
if method not in ("mean", "sum"):
6363
assert (
6464
getattr(np, f"nan{method}"),
6565
"Rolling_apply_single",
6666
) in NUMBA_FUNC_CACHE
6767

6868
tm.assert_equal(result, expected)
6969

70-
@pytest.mark.parametrize("data", [DataFrame(np.eye(5)), Series(range(5))])
70+
@pytest.mark.parametrize(
71+
"data", [DataFrame(np.eye(5)), Series(range(5), name="foo")]
72+
)
7173
def test_numba_vs_cython_expanding_methods(
7274
self, data, nogil, parallel, nopython, arithmetic_numba_supported_operators
7375
):
@@ -82,7 +84,7 @@ def test_numba_vs_cython_expanding_methods(
8284
expected = getattr(expand, method)(engine="cython")
8385

8486
# Check the cache
85-
if method != "mean":
87+
if method not in ("mean", "sum"):
8688
assert (
8789
getattr(np, f"nan{method}"),
8890
"Expanding_apply_single",

0 commit comments

Comments
 (0)