-
-
Notifications
You must be signed in to change notification settings - Fork 18.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: Add numba engine for rolling apply #30151
Changes from 13 commits
3b9bff8
9a302bf
0e9a600
36a77ed
dbb2a9b
f0e9a4d
1250aee
4e7fd1a
cb976cf
45420bb
17851cf
20767ca
9619f8d
66fa69c
b8908ea
135f2ad
34a5687
6da8199
123f77e
54e74d1
04d3530
4bbf587
f849bc7
0c30e48
c4c952e
8645976
987c916
b775684
2e04e60
9b20ff5
0c14033
c7106dc
1640085
2846faf
5a645c0
6bac000
6f1c73f
a890337
0a9071c
9d8d40b
84c3491
a429206
5826ad9
cf7571b
4bc9787
18eed60
f715b55
6a765bf
af3fe50
eb7b5e1
f7dfcf4
a42a960
d019830
29d145f
248149c
a3da51e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ | |
"xlrd": "1.1.0", | ||
"xlwt": "1.2.0", | ||
"xlsxwriter": "0.9.8", | ||
"numba": "0.46.0", | ||
} | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from typing import Callable, Dict, Optional, Tuple | ||
|
||
import numpy as np | ||
|
||
from pandas.compat._optional import import_optional_dependency | ||
|
||
|
||
def _generate_numba_apply_func( | ||
args: Tuple, kwargs: Dict, func: Callable, engine_kwargs: Optional[Dict] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add some comments here |
||
): | ||
numba = import_optional_dependency("numba") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add a doc-string that says what this function does (the parameters are already documented elsewhere, maybe just mention that) |
||
|
||
if engine_kwargs is None: | ||
engine_kwargs = {"nopython": True, "nogil": False, "parallel": False} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can just
as you set the defaults below |
||
|
||
nopython = engine_kwargs.get("nopython", True) | ||
nogil = engine_kwargs.get("nogil", False) | ||
parallel = engine_kwargs.get("parallel", False) | ||
|
||
if kwargs and nopython: | ||
raise ValueError( | ||
"numba does not support kwargs with nopython=True: " | ||
"https://github.com/numba/numba/issues/2916" | ||
) | ||
|
||
if parallel: | ||
loop_range = numba.prange | ||
else: | ||
loop_range = range | ||
|
||
def make_rolling_apply(func): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you move make_rolling_apply to module scope (out of this function)? also don't you need to actually assign to the cache? (on a miss) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cache assignment happens here https://github.com/pandas-dev/pandas/pull/30151/files#diff-0de5c5d9abfcdd141e83701eaaec4358R541 (the function needs to run on some data first) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then i wouldn’t even check the cache here ; that must be at the higher level There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay sure thing. I'll move it up then. |
||
""" | ||
1. jit the user's function | ||
2. Return a rolling apply function with the jitted function inline | ||
|
||
Configurations specified in engine_kwargs apply to both the user's | ||
function _AND_ the rolling apply function. | ||
""" | ||
|
||
@numba.generated_jit(nopython=nopython) | ||
def numba_func(window, *_args): | ||
if getattr(np, func.__name__, False) is func: | ||
|
||
def impl(window, *_args): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe move this func and the return outside of the if statements? AFAICT is duplicated |
||
return func(window, *_args) | ||
|
||
return impl | ||
else: | ||
jf = numba.jit(func, nopython=nopython) | ||
|
||
def impl(window, *_args): | ||
return jf(window, *_args) | ||
|
||
return impl | ||
|
||
@numba.jit(nopython=nopython, nogil=nogil, parallel=parallel) | ||
def roll_apply( | ||
values: np.ndarray, | ||
begin: np.ndarray, | ||
end: np.ndarray, | ||
minimum_periods: int, | ||
): | ||
result = np.empty(len(begin)) | ||
for i in loop_range(len(result)): | ||
start = begin[i] | ||
stop = end[i] | ||
window = values[start:stop] | ||
count_nan = np.sum(np.isnan(window)) | ||
if len(window) - count_nan >= minimum_periods: | ||
result[i] = numba_func(window, *args) | ||
else: | ||
result[i] = np.nan | ||
return result | ||
|
||
return roll_apply | ||
|
||
return make_rolling_apply(func) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -342,3 +342,31 @@ def test_multiple_agg_funcs(self, func, window_size, expected_vals): | |
) | ||
|
||
tm.assert_frame_equal(result, expected) | ||
|
||
|
||
class TestEngine: | ||
jreback marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this class would be more logically placed in |
||
def test_invalid_engine(self): | ||
with pytest.raises( | ||
ValueError, match="engine must be either 'numba' or 'cython'" | ||
): | ||
Series(range(1)).rolling(1).apply(lambda x: x, engine="foo") | ||
|
||
def test_invalid_engine_kwargs_cython(self): | ||
with pytest.raises( | ||
ValueError, match="cython engine does not accept engine_kwargs" | ||
): | ||
Series(range(1)).rolling(1).apply( | ||
lambda x: x, engine="cython", engine_kwargs={"nopython": False} | ||
) | ||
|
||
def test_invalid_raw_numba(self): | ||
with pytest.raises( | ||
ValueError, match="raw must be `True` when using the numba engine" | ||
): | ||
Series(range(1)).rolling(1).apply(lambda x: x, raw=False, engine="numba") | ||
|
||
def test_invalid_kwargs_nopython(self): | ||
with pytest.raises(ValueError, match="numba does not support kwargs with"): | ||
Series(range(1)).rolling(1).apply( | ||
lambda x: x, kwargs={"a": 1}, engine="numba", raw=True | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok with this being non-private (e.g. generate_numba_apply_func), unless its only used in this module