@@ -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 ]
0 commit comments