@@ -250,14 +250,28 @@ def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs)
250
250
if cyfunc and not args and not kwargs :
251
251
return getattr (self , cyfunc )()
252
252
253
+ if self .ngroups == 0 :
254
+ # e.g. test_evaluate_with_empty_groups without any groups to
255
+ # iterate over, we have no output on which to do dtype
256
+ # inference. We default to using the existing dtype.
257
+ # xref GH#51445
258
+ obj = self ._obj_with_exclusions
259
+ return self .obj ._constructor (
260
+ [],
261
+ name = self .obj .name ,
262
+ index = self .grouper .result_index ,
263
+ dtype = obj .dtype ,
264
+ )
265
+
253
266
if self .grouper .nkeys > 1 :
254
267
return self ._python_agg_general (func , * args , ** kwargs )
255
268
256
269
try :
257
270
return self ._python_agg_general (func , * args , ** kwargs )
258
271
except KeyError :
259
- # TODO: KeyError is raised in _python_agg_general,
260
- # see test_groupby.test_basic
272
+ # KeyError raised in test_groupby.test_basic is bc the func does
273
+ # a dictionary lookup on group.name, but group name is not
274
+ # pinned in _python_agg_general, only in _aggregate_named
261
275
result = self ._aggregate_named (func , * args , ** kwargs )
262
276
263
277
# result is a dict whose keys are the elements of result_index
@@ -267,6 +281,15 @@ def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs)
267
281
268
282
agg = aggregate
269
283
284
+ def _python_agg_general (self , func , * args , ** kwargs ):
285
+ func = com .is_builtin_func (func )
286
+ f = lambda x : func (x , * args , ** kwargs )
287
+
288
+ obj = self ._obj_with_exclusions
289
+ result = self .grouper .agg_series (obj , f )
290
+ res = obj ._constructor (result , name = obj .name )
291
+ return self ._wrap_aggregated_output (res )
292
+
270
293
def _aggregate_multiple_funcs (self , arg , * args , ** kwargs ) -> DataFrame :
271
294
if isinstance (arg , dict ):
272
295
if self .as_index :
@@ -308,18 +331,6 @@ def _aggregate_multiple_funcs(self, arg, *args, **kwargs) -> DataFrame:
308
331
output = self ._reindex_output (output )
309
332
return output
310
333
311
- def _indexed_output_to_ndframe (
312
- self , output : Mapping [base .OutputKey , ArrayLike ]
313
- ) -> Series :
314
- """
315
- Wrap the dict result of a GroupBy aggregation into a Series.
316
- """
317
- assert len (output ) == 1
318
- values = next (iter (output .values ()))
319
- result = self .obj ._constructor (values )
320
- result .name = self .obj .name
321
- return result
322
-
323
334
def _wrap_applied_output (
324
335
self ,
325
336
data : Series ,
@@ -1319,6 +1330,31 @@ def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs)
1319
1330
1320
1331
agg = aggregate
1321
1332
1333
+ def _python_agg_general (self , func , * args , ** kwargs ):
1334
+ func = com .is_builtin_func (func )
1335
+ f = lambda x : func (x , * args , ** kwargs )
1336
+
1337
+ # iterate through "columns" ex exclusions to populate output dict
1338
+ output : dict [base .OutputKey , ArrayLike ] = {}
1339
+
1340
+ if self .ngroups == 0 :
1341
+ # e.g. test_evaluate_with_empty_groups different path gets different
1342
+ # result dtype in empty case.
1343
+ return self ._python_apply_general (f , self ._selected_obj , is_agg = True )
1344
+
1345
+ for idx , obj in enumerate (self ._iterate_slices ()):
1346
+ name = obj .name
1347
+ result = self .grouper .agg_series (obj , f )
1348
+ key = base .OutputKey (label = name , position = idx )
1349
+ output [key ] = result
1350
+
1351
+ if not output :
1352
+ # e.g. test_margins_no_values_no_cols
1353
+ return self ._python_apply_general (f , self ._selected_obj )
1354
+
1355
+ res = self ._indexed_output_to_ndframe (output )
1356
+ return self ._wrap_aggregated_output (res )
1357
+
1322
1358
def _iterate_slices (self ) -> Iterable [Series ]:
1323
1359
obj = self ._selected_obj
1324
1360
if self .axis == 1 :
@@ -1885,7 +1921,9 @@ def nunique(self, dropna: bool = True) -> DataFrame:
1885
1921
1886
1922
if self .axis != 0 :
1887
1923
# see test_groupby_crash_on_nunique
1888
- return self ._python_agg_general (lambda sgb : sgb .nunique (dropna ))
1924
+ return self ._python_apply_general (
1925
+ lambda sgb : sgb .nunique (dropna ), self ._obj_with_exclusions , is_agg = True
1926
+ )
1889
1927
1890
1928
obj = self ._obj_with_exclusions
1891
1929
results = self ._apply_to_column_groupbys (
0 commit comments