Skip to content

Commit afb0bf8

Browse files
committed
deprecate instead of treating as a bug
1 parent 57b76eb commit afb0bf8

File tree

6 files changed

+109
-90
lines changed

6 files changed

+109
-90
lines changed

doc/source/whatsnew/v2.1.0.rst

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -114,48 +114,10 @@ Notable bug fixes
114114

115115
These are bug fixes that might have notable behavior changes.
116116

117-
.. _whatsnew_210.notable_bug_fixes.series_agg:
118-
119-
User defined functions in Series.agg will always be passed the whole :class:`Series` for evaluation
120-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
121-
122-
Previously, :meth:`Series.agg` would attempt to apply user-defined functions on each element first and only if that failed would it apply user-defined function to the whole :class:`Series`. This would mean it in some cases didn't aggregate when given an aggregation function, and that the result for :meth:`Series.agg` could be different than the single-column result from :meth:`DataFrame.agg`:
123-
124-
*Previous behavior*:
125-
126-
.. code-block:: ipython
127-
128-
In [1]: ser = pd.Series([1, 2, 3])
129-
In [2]: ser.agg(lambda x: np.sum(x, where=True))
130-
Out[2]:
131-
0 1
132-
1 2
133-
2 3
134-
dtype: int64
135-
In [3]: ser.agg(type)
136-
Out[3]:
137-
0 <class 'int'>
138-
1 <class 'int'>
139-
2 <class 'int'>
140-
dtype: object
141-
In [3]: df = ser.to_frame()
142-
In [4]: df.agg(type)[0]
143-
pandas.core.series.Series
144-
145-
Now user-defined functions in :meth:`Series.agg` will always be passed the whole :class:`Series` for evaluation:
146-
147-
*New behavior*:
148-
149-
.. ipython:: python
150-
:okexcept:
151-
152-
ser = pd.Series([1, 2, 3])
153-
ser.agg(lambda x: np.sum(x, where=True)) # fails, as it should
154-
ser.agg(type)
155-
ser.agg(type) == ser.to_frame().agg(type)[0]
156-
157-
More generally, the result from :meth:`Series.agg` will now always be the same as the single-column result from :meth:`DataFrame.agg` (:issue:`53325`).
117+
.. _whatsnew_210.notable_bug_fixes.notable_bug_fix1:
158118

119+
notable_bug_fix1
120+
^^^^^^^^^^^^^^^^
159121

160122
.. _whatsnew_210.notable_bug_fixes.notable_bug_fix2:
161123

@@ -273,6 +235,8 @@ Deprecations
273235
- Deprecated ``axis=1`` in :meth:`DataFrame.groupby` and in :class:`Grouper` constructor, do ``frame.T.groupby(...)`` instead (:issue:`51203`)
274236
- Deprecated accepting slices in :meth:`DataFrame.take`, call ``obj[slicer]`` or pass a sequence of integers instead (:issue:`51539`)
275237
- Deprecated explicit support for subclassing :class:`Index` (:issue:`45289`)
238+
- Deprecated making functions given to :meth:`Series.agg` attempt to operate on each element in the :class:`Series` and only operate on the whole :class:`Series` if the elementwise operations failed. In the future, functions given to :meth:`Series.agg` will always operate on the whole :class:`Series` only. To keep the current behavior, use :meth:`Series.transform` instead. (:issue:`53325`)
239+
- Deprecated making the functions in a list of functions given to :meth:`DataFrame.agg` attempt to operate on each element in the :class:`DataFrame` and only operate on the columns of the :class:`DataFrame` if the elementwise operations failed. To keep the current behavior, use :meth:`DataFrame.transform` instead. (:issue:`53325`)
276240
- Deprecated passing a :class:`DataFrame` to :meth:`DataFrame.from_records`, use :meth:`DataFrame.set_index` or :meth:`DataFrame.drop` instead (:issue:`51353`)
277241
- Deprecated silently dropping unrecognized timezones when parsing strings to datetimes (:issue:`18702`)
278242
- Deprecated the ``axis`` keyword in :meth:`DataFrame.ewm`, :meth:`Series.ewm`, :meth:`DataFrame.rolling`, :meth:`Series.rolling`, :meth:`DataFrame.expanding`, :meth:`Series.expanding` (:issue:`51778`)

pandas/core/apply.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,23 +1121,28 @@ def apply(self) -> DataFrame | Series:
11211121
def agg(self):
11221122
result = super().agg()
11231123
if result is None:
1124+
obj = self.obj
11241125
func = self.func
1125-
11261126
# string, list-like, and dict-like are entirely handled in super
11271127
assert callable(func)
11281128

1129-
# try a regular apply, this evaluates lambdas
1130-
# row-by-row; however if the lambda is expected a Series
1131-
# expression, e.g.: lambda x: x-x.quantile(0.25)
1132-
# this will fail, so we can try a vectorized evaluation
1133-
1134-
# we cannot FIRST try the vectorized evaluation, because
1135-
# then .agg and .apply would have different semantics if the
1136-
# operation is actually defined on the Series, e.g. str
1129+
# GH53325: The setup below is just to keep current behavior while emitting a
1130+
# deprecation message. In the future this will all be replaced with a simple
1131+
# `result = f(self.obj)`.
1132+
if isinstance(func, np.ufunc):
1133+
with np.errstate(all="ignore"):
1134+
return func(obj)
11371135
try:
1138-
result = self.obj.apply(func, args=self.args, **self.kwargs)
1136+
result = obj.apply(func)
11391137
except (ValueError, AttributeError, TypeError):
1140-
result = func(self.obj, *self.args, **self.kwargs)
1138+
result = func(self.obj)
1139+
else:
1140+
msg = (
1141+
f"using {func} in {type(obj).__name__}.agg cannot aggregate and "
1142+
f"has been deprecated. Use {type(obj).__name__}.transform to "
1143+
f"keep behavior unchanged."
1144+
)
1145+
warnings.warn(msg, FutureWarning, stacklevel=find_stack_level())
11411146

11421147
return result
11431148

pandas/tests/apply/test_frame_apply.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,8 +1478,8 @@ def test_any_apply_keyword_non_zero_axis_regression():
14781478
tm.assert_series_equal(result, expected)
14791479

14801480

1481-
def test_agg_list_like_func_with_args():
1482-
# GH 50624
1481+
def test_agg_mapping_func_deprecated():
1482+
# GH 53325
14831483
df = DataFrame({"x": [1, 2, 3]})
14841484

14851485
def foo1(x, a=1, c=0):
@@ -1488,16 +1488,13 @@ def foo1(x, a=1, c=0):
14881488
def foo2(x, b=2, c=0):
14891489
return x + b + c
14901490

1491-
msg = r"foo1\(\) got an unexpected keyword argument 'b'"
1492-
with pytest.raises(TypeError, match=msg):
1493-
df.agg([foo1, foo2], 0, 3, b=3, c=4)
1494-
1495-
result = df.agg([foo1, foo2], 0, 3, c=4)
1496-
expected = DataFrame(
1497-
[[8, 8], [9, 9], [10, 10]],
1498-
columns=MultiIndex.from_tuples([("x", "foo1"), ("x", "foo2")]),
1499-
)
1500-
tm.assert_frame_equal(result, expected)
1491+
# single func already takes the vectorized path
1492+
df.agg(foo1, 0, 3, c=4)
1493+
msg = "using .+ in Series.agg cannot aggregate and"
1494+
with tm.assert_produces_warning(FutureWarning, match=msg):
1495+
df.agg([foo1, foo2], 0, 3, c=4)
1496+
with tm.assert_produces_warning(FutureWarning, match=msg):
1497+
df.agg({"x": foo1}, 0, 3, c=4)
15011498

15021499

15031500
def test_agg_std():

pandas/tests/apply/test_frame_transform.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ def test_transform_empty_listlike(float_frame, ops, frame_or_series):
6666
obj.transform(ops)
6767

6868

69+
def test_transform_listlike_func_with_args():
70+
# GH 50624
71+
df = DataFrame({"x": [1, 2, 3]})
72+
73+
def foo1(x, a=1, c=0):
74+
return x + a + c
75+
76+
def foo2(x, b=2, c=0):
77+
return x + b + c
78+
79+
msg = r"foo1\(\) got an unexpected keyword argument 'b'"
80+
with pytest.raises(TypeError, match=msg):
81+
df.transform([foo1, foo2], 0, 3, b=3, c=4)
82+
83+
result = df.transform([foo1, foo2], 0, 3, c=4)
84+
expected = DataFrame(
85+
[[8, 8], [9, 9], [10, 10]],
86+
columns=MultiIndex.from_tuples([("x", "foo1"), ("x", "foo2")]),
87+
)
88+
tm.assert_frame_equal(result, expected)
89+
90+
6991
@pytest.mark.parametrize("box", [dict, Series])
7092
def test_transform_dictlike(axis, float_frame, box):
7193
# GH 35964

pandas/tests/apply/test_series_apply.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,18 @@ def test_apply_args():
9898
assert isinstance(result[0], list)
9999

100100

101-
@pytest.mark.parametrize(
102-
"args, kwargs, increment",
103-
[((), {}, 0), ((), {"a": 1}, 1), ((2, 3), {}, 32), ((1,), {"c": 2}, 201)],
104-
)
105-
def test_agg_args(args, kwargs, increment):
106-
# GH 43357
107-
def f(x, a=0, b=0, c=0):
108-
return x + a + 10 * b + 100 * c
101+
def test_agg_args():
102+
def f(x, increment):
103+
return x.sum() + increment
109104

110105
s = Series([1, 2])
111-
result = s.agg(f, 0, *args, **kwargs)
112-
expected = s + increment
113-
tm.assert_series_equal(result, expected)
114-
106+
result = s.agg(f, increment=0)
107+
expected = s.sum()
108+
assert result == expected
115109

116-
def test_agg_list_like_func_with_args():
117-
# GH 50624
118110

111+
def test_agg_mapping_func_deprecated():
112+
# GH 53325
119113
s = Series([1, 2, 3])
120114

121115
def foo1(x, a=1, c=0):
@@ -124,13 +118,13 @@ def foo1(x, a=1, c=0):
124118
def foo2(x, b=2, c=0):
125119
return x + b + c
126120

127-
msg = r"foo1\(\) got an unexpected keyword argument 'b'"
128-
with pytest.raises(TypeError, match=msg):
129-
s.agg([foo1, foo2], 0, 3, b=3, c=4)
130-
131-
result = s.agg([foo1, foo2], 0, 3, c=4)
132-
expected = DataFrame({"foo1": [8, 9, 10], "foo2": [8, 9, 10]})
133-
tm.assert_frame_equal(result, expected)
121+
msg = "using .+ in Series.agg cannot aggregate and"
122+
with tm.assert_produces_warning(FutureWarning, match=msg):
123+
s.agg(foo1, 0, 3, c=4)
124+
with tm.assert_produces_warning(FutureWarning, match=msg):
125+
s.agg([foo1, foo2], 0, 3, c=4)
126+
with tm.assert_produces_warning(FutureWarning, match=msg):
127+
s.agg({"a": foo1, "b": foo2}, 0, 3, c=4)
134128

135129

136130
def test_series_apply_map_box_timestamps(by_row):
@@ -393,13 +387,15 @@ def test_apply_map_evaluate_lambdas_the_same(string_series, func, by_row):
393387

394388
def test_agg_evaluate_lambdas(string_series):
395389
# GH53325
396-
expected = Series
390+
# in the future, the result will be a Series class.
397391

398-
result = string_series.agg(lambda x: type(x))
399-
assert result is expected
392+
with tm.assert_produces_warning(FutureWarning):
393+
result = string_series.agg(lambda x: type(x))
394+
assert isinstance(result, Series) and len(result) == len(string_series)
400395

401-
result = string_series.agg(type)
402-
assert result is expected
396+
with tm.assert_produces_warning(FutureWarning):
397+
result = string_series.agg(type)
398+
assert isinstance(result, Series) and len(result) == len(string_series)
403399

404400

405401
def test_with_nested_series(datetime_series):

pandas/tests/apply/test_series_transform.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@
1010
import pandas._testing as tm
1111

1212

13+
@pytest.mark.parametrize(
14+
"args, kwargs, increment",
15+
[((), {}, 0), ((), {"a": 1}, 1), ((2, 3), {}, 32), ((1,), {"c": 2}, 201)],
16+
)
17+
def test_agg_args(args, kwargs, increment):
18+
# GH 43357
19+
def f(x, a=0, b=0, c=0):
20+
return x + a + 10 * b + 100 * c
21+
22+
s = Series([1, 2])
23+
result = s.transform(f, 0, *args, **kwargs)
24+
expected = s + increment
25+
tm.assert_series_equal(result, expected)
26+
27+
1328
@pytest.mark.parametrize(
1429
"ops, names",
1530
[
@@ -28,6 +43,26 @@ def test_transform_listlike(string_series, ops, names):
2843
tm.assert_frame_equal(result, expected)
2944

3045

46+
def test_transform_listlike_func_with_args():
47+
# GH 50624
48+
49+
s = Series([1, 2, 3])
50+
51+
def foo1(x, a=1, c=0):
52+
return x + a + c
53+
54+
def foo2(x, b=2, c=0):
55+
return x + b + c
56+
57+
msg = r"foo1\(\) got an unexpected keyword argument 'b'"
58+
with pytest.raises(TypeError, match=msg):
59+
s.transform([foo1, foo2], 0, 3, b=3, c=4)
60+
61+
result = s.transform([foo1, foo2], 0, 3, c=4)
62+
expected = DataFrame({"foo1": [8, 9, 10], "foo2": [8, 9, 10]})
63+
tm.assert_frame_equal(result, expected)
64+
65+
3166
@pytest.mark.parametrize("box", [dict, Series])
3267
def test_transform_dictlike(string_series, box):
3368
# GH 35964

0 commit comments

Comments
 (0)