Skip to content

Commit 9f82e19

Browse files
dcherianpre-commit-ci[bot]max-sixty
authored
Add engine="numbagg" (#72)
* Add engine="numbagg" * Fix. * fix CI * Add nanlen * fix env * Add numbagg * Bettter numbagg benchmarks? * Update ci/environment.yml * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * cleanup * Error on dtype specified * Don't shadow sum, mean, sum_of_squares * more skip * Fix backup npg aggregations * xfail nanmean bool * ignore numbagg for mypy * Add to upstream-dev CI * Add to optional dependencies * Fix bool reductions * fix mypy ignore * reintroduce engines * Update docstring * Update docs. * Support more aggregations * More aggregations * back to nancount * Add any, all * promote in nanstd too * Add ddof in anticipation of numbagg/numbagg#138 * Add more benchmarks * reorder benchmark table * Fix numba compilation setup? * More benchmarks * Rework benchmarks * small docstring update * ignore asv typing * fix type ignoring * Guard against numbagg failures * Use released numbagg --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maximilian Roos <[email protected]>
1 parent 68b122e commit 9f82e19

File tree

14 files changed

+271
-32
lines changed

14 files changed

+271
-32
lines changed

asv_bench/asv.conf.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
// followed by the pip installed packages).
7373
//
7474
"matrix": {
75+
"numbagg": [""],
7576
"numpy_groupies": [""],
7677
"numpy": [""],
7778
"pandas": [""],

asv_bench/benchmarks/reduce.py

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,86 @@
11
import numpy as np
2-
import numpy_groupies as npg
32
import pandas as pd
3+
from asv_runner.benchmarks.mark import parameterize, skip_for_params
44

55
import flox
6+
import flox.aggregations
67

7-
from . import parameterized
8+
N = 3000
9+
funcs = ["sum", "nansum", "mean", "nanmean", "max", "nanmax", "var", "count", "all"]
10+
engines = ["flox", "numpy", "numbagg"]
11+
expected_groups = {
12+
"None": None,
13+
"RangeIndex": pd.RangeIndex(5),
14+
"bins": pd.IntervalIndex.from_breaks([1, 2, 4]),
15+
}
16+
expected_names = tuple(expected_groups)
817

9-
N = 1000
10-
funcs = ["sum", "nansum", "mean", "nanmean", "max", "var", "nanvar", "count"]
11-
engines = ["flox", "numpy"]
12-
expected_groups = [None, pd.IntervalIndex.from_breaks([1, 2, 4])]
18+
NUMBAGG_FUNCS = ["nansum", "nanmean", "nanmax", "count", "all"]
19+
20+
numbagg_skip = [
21+
(func, expected_names[0], "numbagg") for func in funcs if func not in NUMBAGG_FUNCS
22+
] + [(func, expected_names[1], "numbagg") for func in funcs if func not in NUMBAGG_FUNCS]
23+
24+
25+
def setup_jit():
26+
# pre-compile jitted funcs
27+
labels = np.ones((N), dtype=int)
28+
array1 = np.ones((N), dtype=float)
29+
array2 = np.ones((N, N), dtype=float)
30+
31+
if "numba" in engines:
32+
for func in funcs:
33+
method = getattr(flox.aggregate_npg, func)
34+
method(labels, array1, engine="numba")
35+
if "numbagg" in engines:
36+
for func in set(NUMBAGG_FUNCS) & set(funcs):
37+
flox.groupby_reduce(array1, labels, func=func, engine="numbagg")
38+
flox.groupby_reduce(array2, labels, func=func, engine="numbagg")
1339

1440

1541
class ChunkReduce:
1642
"""Time the core reduction function."""
1743

44+
min_run_count = 5
45+
warmup_time = 1
46+
1847
def setup(self, *args, **kwargs):
19-
# pre-compile jitted funcs
20-
if "numba" in engines:
21-
for func in funcs:
22-
npg.aggregate_numba.aggregate(
23-
np.ones((100,), dtype=int), np.ones((100,), dtype=int), func=func
24-
)
2548
raise NotImplementedError
2649

27-
@parameterized("func, engine, expected_groups", [funcs, engines, expected_groups])
28-
def time_reduce(self, func, engine, expected_groups):
50+
@skip_for_params(numbagg_skip)
51+
@parameterize({"func": funcs, "expected_name": expected_names, "engine": engines})
52+
def time_reduce(self, func, expected_name, engine):
2953
flox.groupby_reduce(
3054
self.array,
3155
self.labels,
3256
func=func,
3357
engine=engine,
3458
axis=self.axis,
35-
expected_groups=expected_groups,
59+
expected_groups=expected_groups[expected_name],
3660
)
3761

38-
@parameterized("func, engine, expected_groups", [funcs, engines, expected_groups])
39-
def peakmem_reduce(self, func, engine, expected_groups):
62+
@parameterize({"func": ["nansum", "nanmean", "nanmax", "count"], "engine": engines})
63+
def time_reduce_bare(self, func, engine):
64+
flox.aggregations.generic_aggregate(
65+
self.labels,
66+
self.array,
67+
axis=-1,
68+
size=5,
69+
func=func,
70+
engine=engine,
71+
fill_value=0,
72+
)
73+
74+
@skip_for_params(numbagg_skip)
75+
@parameterize({"func": funcs, "expected_name": expected_names, "engine": engines})
76+
def peakmem_reduce(self, func, expected_name, engine):
4077
flox.groupby_reduce(
4178
self.array,
4279
self.labels,
4380
func=func,
4481
engine=engine,
4582
axis=self.axis,
46-
expected_groups=expected_groups,
83+
expected_groups=expected_groups[expected_name],
4784
)
4885

4986

@@ -52,17 +89,45 @@ def setup(self, *args, **kwargs):
5289
self.array = np.ones((N,))
5390
self.labels = np.repeat(np.arange(5), repeats=N // 5)
5491
self.axis = -1
92+
if "numbagg" in args:
93+
setup_jit()
94+
95+
96+
class ChunkReduce1DUnsorted(ChunkReduce):
97+
def setup(self, *args, **kwargs):
98+
self.array = np.ones((N,))
99+
self.labels = np.random.permutation(np.repeat(np.arange(5), repeats=N // 5))
100+
self.axis = -1
101+
setup_jit()
55102

56103

57104
class ChunkReduce2D(ChunkReduce):
58105
def setup(self, *args, **kwargs):
59106
self.array = np.ones((N, N))
60107
self.labels = np.repeat(np.arange(N // 5), repeats=5)
61108
self.axis = -1
109+
setup_jit()
110+
111+
112+
class ChunkReduce2DUnsorted(ChunkReduce):
113+
def setup(self, *args, **kwargs):
114+
self.array = np.ones((N, N))
115+
self.labels = np.random.permutation(np.repeat(np.arange(N // 5), repeats=5))
116+
self.axis = -1
117+
setup_jit()
62118

63119

64120
class ChunkReduce2DAllAxes(ChunkReduce):
65121
def setup(self, *args, **kwargs):
66122
self.array = np.ones((N, N))
67123
self.labels = np.repeat(np.arange(N // 5), repeats=5)
68124
self.axis = None
125+
setup_jit()
126+
127+
128+
class ChunkReduce2DAllAxesUnsorted(ChunkReduce):
129+
def setup(self, *args, **kwargs):
130+
self.array = np.ones((N, N))
131+
self.labels = np.random.permutation(np.repeat(np.arange(N // 5), repeats=5))
132+
self.axis = None
133+
setup_jit()

ci/environment.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ dependencies:
2323
- toolz
2424
- numba
2525
- scipy
26+
- pip:
27+
- numbagg>=0.3

ci/upstream-dev-env.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ dependencies:
1818
- git+https://github.com/pandas-dev/pandas
1919
- git+https://github.com/dask/dask
2020
- git+https://github.com/ml31415/numpy-groupies
21+
- git+https://github.com/numbagg/numbagg

docs/source/engines.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@
99
1. `engine="numba"` wraps `numpy_groupies.aggregate_numba`. This uses `numba` kernels for the core aggregation.
1010
1. `engine="flox"` uses the `ufunc.reduceat` method after first argsorting the array so that all group members occur sequentially. This was copied from
1111
a [gist by Stephan Hoyer](https://gist.github.com/shoyer/f538ac78ae904c936844)
12+
1. `engine="numbagg"` uses the reductions available in [`numbagg.grouped`](https://github.com/numbagg/numbagg/blob/main/numbagg/grouped.py)
13+
from the [numbagg](https://github.com/numbagg/numbagg) project.
1214

1315
See [](arrays) for more details.
1416

1517
## Tradeoffs
1618

17-
For the common case of reducing a nD array by a 1D array of group labels (e.g. `groupby("time.month")`), `engine="flox"` *can* be faster.
19+
For the common case of reducing a nD array by a 1D array of group labels (e.g. `groupby("time.month")`), `engine="numbagg"` is almost always faster, and `engine="flox"` *can* be faster.
1820

1921
The reason is that `numpy_groupies` converts all groupby problems to a 1D problem, this can involve [some overhead](https://github.com/ml31415/numpy-groupies/pull/46).
2022
It is possible to optimize this a bit in `flox` or `numpy_groupies`, but the work has not been done yet.
2123
The advantage of `engine="numpy"` is that it tends to work for more array types, since it appears to be more common to implement `np.bincount`, and not `np.add.reduceat`.
2224

2325
```{tip}
24-
Other potential engines we could add are [`numbagg`](https://github.com/numbagg/numbagg) ([stalled PR here](https://github.com/xarray-contrib/flox/pull/72)) and [`datashader`](https://github.com/xarray-contrib/flox/issues/142).
25-
Both use numba for high-performance aggregations. Contributions or discussion is very welcome!
26+
One other potential engine we could add is [`datashader`](https://github.com/xarray-contrib/flox/issues/142).
27+
Contributions or discussion is very welcome!
2628
```

flox/aggregate_numbagg.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from functools import partial
2+
3+
import numbagg
4+
import numbagg.grouped
5+
import numpy as np
6+
7+
8+
def _numbagg_wrapper(
9+
group_idx,
10+
array,
11+
*,
12+
axis=-1,
13+
func="sum",
14+
size=None,
15+
fill_value=None,
16+
dtype=None,
17+
numbagg_func=None,
18+
):
19+
return numbagg_func(
20+
array,
21+
group_idx,
22+
axis=axis,
23+
num_labels=size,
24+
# The following are unsupported
25+
# fill_value=fill_value,
26+
# dtype=dtype,
27+
)
28+
29+
30+
def nansum(group_idx, array, *, axis=-1, size=None, fill_value=None, dtype=None):
31+
if np.issubdtype(array.dtype, np.bool_):
32+
array = array.astype(np.in64)
33+
return numbagg.grouped.group_nansum(
34+
array,
35+
group_idx,
36+
axis=axis,
37+
num_labels=size,
38+
# fill_value=fill_value,
39+
# dtype=dtype,
40+
)
41+
42+
43+
def nanmean(group_idx, array, *, axis=-1, size=None, fill_value=None, dtype=None):
44+
if np.issubdtype(array.dtype, np.int_):
45+
array = array.astype(np.float64)
46+
return numbagg.grouped.group_nanmean(
47+
array,
48+
group_idx,
49+
axis=axis,
50+
num_labels=size,
51+
# fill_value=fill_value,
52+
# dtype=dtype,
53+
)
54+
55+
56+
def nanvar(group_idx, array, *, axis=-1, size=None, fill_value=None, dtype=None, ddof=0):
57+
assert ddof != 0
58+
if np.issubdtype(array.dtype, np.int_):
59+
array = array.astype(np.float64)
60+
return numbagg.grouped.group_nanvar(
61+
array,
62+
group_idx,
63+
axis=axis,
64+
num_labels=size,
65+
# ddof=0,
66+
# fill_value=fill_value,
67+
# dtype=dtype,
68+
)
69+
70+
71+
def nanstd(group_idx, array, *, axis=-1, size=None, fill_value=None, dtype=None, ddof=0):
72+
assert ddof != 0
73+
if np.issubdtype(array.dtype, np.int_):
74+
array = array.astype(np.float64)
75+
return numbagg.grouped.group_nanstd(
76+
array,
77+
group_idx,
78+
axis=axis,
79+
num_labels=size,
80+
# ddof=0,
81+
# fill_value=fill_value,
82+
# dtype=dtype,
83+
)
84+
85+
86+
nansum_of_squares = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nansum_of_squares)
87+
nanlen = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nancount)
88+
nanprod = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanprod)
89+
nanfirst = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanfirst)
90+
nanlast = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanlast)
91+
# nanargmax = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanargmax)
92+
# nanargmin = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanargmin)
93+
nanmax = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanmax)
94+
nanmin = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanmin)
95+
any = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanany)
96+
all = partial(_numbagg_wrapper, numbagg_func=numbagg.grouped.group_nanall)
97+
98+
# sum = nansum
99+
# mean = nanmean
100+
# sum_of_squares = nansum_of_squares

flox/aggregations.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,28 @@ def generic_aggregate(
6262
except AttributeError:
6363
method = get_npg_aggregation(func, engine="numpy")
6464

65+
elif engine == "numbagg":
66+
from . import aggregate_numbagg
67+
68+
try:
69+
if (
70+
# numabgg hardcodes ddof=1
71+
("var" in func or "std" in func)
72+
and kwargs.get("ddof", 0) == 0
73+
):
74+
method = get_npg_aggregation(func, engine="numpy")
75+
76+
else:
77+
method = getattr(aggregate_numbagg, func)
78+
except AttributeError:
79+
method = get_npg_aggregation(func, engine="numpy")
80+
6581
elif engine in ["numpy", "numba"]:
6682
method = get_npg_aggregation(func, engine=engine)
6783

6884
else:
6985
raise ValueError(
70-
f"Expected engine to be one of ['flox', 'numpy', 'numba']. Received {engine} instead."
86+
f"Expected engine to be one of ['flox', 'numpy', 'numba', 'numbagg']. Received {engine} instead."
7187
)
7288

7389
group_idx = np.asarray(group_idx, like=array)

0 commit comments

Comments
 (0)