Skip to content

Commit 4008d90

Browse files
pvomelvenyTomAugspurger
authored andcommitted
BUG: Allow non-callable attributes in aggregate function. Fixes GH16405 (#16458)
(cherry picked from commit a67c7aa)
1 parent 544fb11 commit 4008d90

File tree

4 files changed

+72
-2
lines changed

4 files changed

+72
-2
lines changed

doc/source/whatsnew/v0.20.2.txt

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Reshaping
9393
- Bug in ``pd.wide_to_long()`` where no error was raised when ``i`` was not a unique identifier (:issue:`16382`)
9494
- Bug in ``Series.isin(..)`` with a list of tuples (:issue:`16394`)
9595
- Bug in construction of a ``DataFrame`` with mixed dtypes including an all-NaT column. (:issue:`16395`)
96+
- Bug in ``DataFrame.agg()`` and ``Series.agg()`` with aggregating on non-callable attributes (:issue:`16405`)
9697

9798
Numeric
9899
^^^^^^^

pandas/core/base.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def aggregate(self, func, *args, **kwargs):
378378
def _try_aggregate_string_function(self, arg, *args, **kwargs):
379379
"""
380380
if arg is a string, then try to operate on it:
381-
- try to find a function on ourselves
381+
- try to find a function (or attribute) on ourselves
382382
- try to find a numpy function
383383
- raise
384384
@@ -387,7 +387,15 @@ def _try_aggregate_string_function(self, arg, *args, **kwargs):
387387

388388
f = getattr(self, arg, None)
389389
if f is not None:
390-
return f(*args, **kwargs)
390+
if callable(f):
391+
return f(*args, **kwargs)
392+
393+
# people may try to aggregate on a non-callable attribute
394+
# but don't let them think they can pass args to it
395+
assert len(args) == 0
396+
assert len([kwarg for kwarg in kwargs
397+
if kwarg not in ['axis', '_level']]) == 0
398+
return f
391399

392400
f = getattr(np, arg, None)
393401
if f is not None:

pandas/tests/frame/test_apply.py

+45
Original file line numberDiff line numberDiff line change
@@ -635,3 +635,48 @@ def test_nuiscance_columns(self):
635635
expected = DataFrame([[6, 6., 'foobarbaz']],
636636
index=['sum'], columns=['A', 'B', 'C'])
637637
assert_frame_equal(result, expected)
638+
639+
def test_non_callable_aggregates(self):
640+
641+
# GH 16405
642+
# 'size' is a property of frame/series
643+
# validate that this is working
644+
df = DataFrame({'A': [None, 2, 3],
645+
'B': [1.0, np.nan, 3.0],
646+
'C': ['foo', None, 'bar']})
647+
648+
# Function aggregate
649+
result = df.agg({'A': 'count'})
650+
expected = pd.Series({'A': 2})
651+
652+
assert_series_equal(result, expected)
653+
654+
# Non-function aggregate
655+
result = df.agg({'A': 'size'})
656+
expected = pd.Series({'A': 3})
657+
658+
assert_series_equal(result, expected)
659+
660+
# Mix function and non-function aggs
661+
result1 = df.agg(['count', 'size'])
662+
result2 = df.agg({'A': ['count', 'size'],
663+
'B': ['count', 'size'],
664+
'C': ['count', 'size']})
665+
expected = pd.DataFrame({'A': {'count': 2, 'size': 3},
666+
'B': {'count': 2, 'size': 3},
667+
'C': {'count': 2, 'size': 3}})
668+
669+
assert_frame_equal(result1, result2, check_like=True)
670+
assert_frame_equal(result2, expected, check_like=True)
671+
672+
# Just functional string arg is same as calling df.arg()
673+
result = df.agg('count')
674+
expected = df.count()
675+
676+
assert_series_equal(result, expected)
677+
678+
# Just a string attribute arg same as calling df.arg
679+
result = df.agg('size')
680+
expected = df.size
681+
682+
assert result == expected

pandas/tests/series/test_apply.py

+16
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,22 @@ def test_reduce(self):
306306
name=self.series.name)
307307
assert_series_equal(result, expected)
308308

309+
def test_non_callable_aggregates(self):
310+
# test agg using non-callable series attributes
311+
s = Series([1, 2, None])
312+
313+
# Calling agg w/ just a string arg same as calling s.arg
314+
result = s.agg('size')
315+
expected = s.size
316+
assert result == expected
317+
318+
# test when mixed w/ callable reducers
319+
result = s.agg(['size', 'count', 'mean'])
320+
expected = Series(OrderedDict({'size': 3.0,
321+
'count': 2.0,
322+
'mean': 1.5}))
323+
assert_series_equal(result[expected.index], expected)
324+
309325

310326
class TestSeriesMap(TestData):
311327

0 commit comments

Comments
 (0)