Skip to content

Commit a8a7e5b

Browse files
committed
Optimization for the case float keys are integral and fit a compact non-negative range
1 parent 14c223a commit a8a7e5b

2 files changed

Lines changed: 51 additions & 4 deletions

File tree

src/blosc2/groupby.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,13 @@ def _execute_with_result_target(self, specs: list[_AggSpec]):
264264
fast = self._try_execute_cython_float_integral_key_f64_sum(specs)
265265
if fast is not None:
266266
return fast
267-
fast = self._try_execute_cython_float_hash(specs)
267+
# Dense single-key path also covers integral-valued float keys with a
268+
# compact non-negative range (e.g. float32 second/id columns), so try
269+
# it before the generic float hash path, which is markedly slower.
270+
fast = self._try_execute_dense_single_int_key(specs)
268271
if fast is not None:
269272
return fast
270-
fast = self._try_execute_dense_single_int_key(specs)
273+
fast = self._try_execute_cython_float_hash(specs)
271274
if fast is not None:
272275
return fast
273276

@@ -1033,8 +1036,12 @@ def _try_execute_dense_single_int_key(self, specs: list[_AggSpec]): # noqa: C90
10331036
key_info = self.table._schema.columns_by_name[key_name]
10341037
key_is_dict = self.table._is_dictionary_column(key_info)
10351038
key_dtype = np.dtype(np.int32) if key_is_dict else getattr(key_info.spec, "dtype", None)
1036-
if key_dtype is None or key_dtype.kind not in "biu":
1039+
if key_dtype is None or key_dtype.kind not in "biuf":
10371040
return None
1041+
# Float keys are accepted only when their values are integral and fit a
1042+
# compact non-negative range; per-chunk casting verifies integrality and
1043+
# bails (falling back to the hash path) on the first fractional value.
1044+
key_is_integral_float = (not key_is_dict) and key_dtype.kind == "f"
10381045
if any(spec.op in {"min", "max"} and spec.input_col is not None for spec in specs):
10391046
for spec in specs:
10401047
if spec.op in {"min", "max"} and spec.input_col is not None:
@@ -1107,6 +1114,16 @@ def ensure_size(size: int) -> bool:
11071114
keys = np.asarray(raw_keys[live_mask])
11081115
if keys.dtype.kind == "b":
11091116
keys = keys.astype(np.int8, copy=False)
1117+
elif key_is_integral_float:
1118+
# Non-finite keys (NaN/inf, e.g. a retained NaN group when
1119+
# dropna=False) and fractional keys both make the dense integer
1120+
# mapping invalid; defer to the generic/hash fallback.
1121+
if not np.all(np.isfinite(keys)):
1122+
return None
1123+
keys_int = keys.astype(np.int64)
1124+
if not np.array_equal(keys_int, keys):
1125+
return None
1126+
keys = keys_int
11101127
if len(keys) == 0:
11111128
continue
11121129
min_key = int(np.min(keys))
@@ -1159,7 +1176,12 @@ def ensure_size(size: int) -> bool:
11591176
group_codes = np.nonzero(present)[0]
11601177
rows = []
11611178
for code in group_codes:
1162-
key_value = self.table._cols[key_name].decode(int(code)) if key_is_dict else _python_scalar(code)
1179+
if key_is_dict:
1180+
key_value = self.table._cols[key_name].decode(int(code))
1181+
elif key_is_integral_float:
1182+
key_value = float(code)
1183+
else:
1184+
key_value = _python_scalar(code)
11631185
row = {key_name: key_value}
11641186
for spec in specs:
11651187
state = states[spec.output_col]

tests/ctable/test_groupby.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,31 @@ def test_groupby_float_integral_fast_path_falls_back_for_nan_group_when_kept():
265265
assert got[1][1] == 2.0
266266

267267

268+
@pytest.mark.parametrize("row_type", [Float64KeyRow, Float32KeyRow])
269+
def test_groupby_integral_float_key_dense_min_max(row_type):
270+
# Non-negative integral float keys take the dense single-key path (the same
271+
# one used for integer keys), which also covers min/max -- not just sum.
272+
t = CTable(row_type, new_data=[(2.0, 5.0), (1.0, 9.0), (2.0, 3.0), (1.0, 4.0)])
273+
274+
out_max = t.group_by("key", sort=True).max("value")
275+
out_min = t.group_by("key", sort=True).min("value")
276+
277+
assert rows(out_max) == [(1.0, 9.0), (2.0, 5.0)]
278+
assert rows(out_min) == [(1.0, 4.0), (2.0, 3.0)]
279+
# The key column keeps its original float dtype in the result.
280+
assert out_max._cols["key"][:].dtype == t._cols["key"][:].dtype
281+
282+
283+
def test_groupby_integral_float_key_falls_back_for_negative_keys():
284+
# Negative keys cannot use the dense (non-negative) mapping; the generic
285+
# path must still produce correct max results.
286+
t = CTable(Float64KeyRow, new_data=[(-1.0, 5.0), (-1.0, 8.0), (2.0, 3.0)])
287+
288+
out = t.group_by("key", sort=True).max("value")
289+
290+
assert rows(out) == [(-1.0, 8.0), (2.0, 3.0)]
291+
292+
268293
def test_group_reduce_object_keys_sort_with_none():
269294
groups, sizes = blosc2.group_reduce(
270295
np.array([None, "b", "a", "b"], dtype=object), sort=True, dropna=False

0 commit comments

Comments
 (0)