Skip to content

Commit 714b062

Browse files
committed
DEPR: deprecate element-wise operations in (Series|DataFrame).transform
1 parent f00efd0 commit 714b062

11 files changed

+288
-173
lines changed

pandas/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,3 +2010,11 @@ def warsaw(request) -> str:
20102010
@pytest.fixture()
20112011
def arrow_string_storage():
20122012
return ("pyarrow", "pyarrow_numpy")
2013+
2014+
2015+
@pytest.fixture(params=[True, False])
2016+
def series_ops_only(request):
2017+
"""
2018+
Parameter used in Series.transform and DataFrame.transform. Remove in pandas v3.0.
2019+
"""
2020+
return request.param

pandas/core/apply.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,14 @@ def transform(self) -> DataFrame | Series:
211211
axis = self.axis
212212
args = self.args
213213
kwargs = self.kwargs
214+
by_row = self.by_row
214215

215216
is_series = obj.ndim == 1
216217

217218
if obj._get_axis_number(axis) == 1:
218219
assert not is_series
219-
return obj.T.transform(func, 0, *args, **kwargs).T
220+
soo = False if by_row else True
221+
return obj.T.transform(func, 0, *args, series_ops_only=soo, **kwargs).T
220222

221223
if is_list_like(func) and not is_dict_like(func):
222224
func = cast(list[AggFuncTypeBase], func)
@@ -230,14 +232,17 @@ def transform(self) -> DataFrame | Series:
230232
func = cast(AggFuncTypeDict, func)
231233
return self.transform_dict_like(func)
232234

233-
# func is either str or callable
234-
func = cast(AggFuncTypeBase, func)
235-
try:
236-
result = self.transform_str_or_callable(func)
237-
except TypeError:
238-
raise
239-
except Exception as err:
240-
raise ValueError("Transform function failed") from err
235+
if not self.by_row:
236+
result = obj.apply(func, by_row=by_row, args=args, **kwargs)
237+
else:
238+
# func is either str or callable
239+
func = cast(AggFuncTypeBase, func)
240+
try:
241+
result = self.transform_str_or_callable(func)
242+
except TypeError:
243+
raise
244+
except Exception as err:
245+
raise ValueError("Transform function failed") from err
241246

242247
# Functions that transform may return empty Series/DataFrame
243248
# when the dtype is not appropriate
@@ -267,6 +272,7 @@ def transform_dict_like(self, func) -> DataFrame:
267272
obj = self.obj
268273
args = self.args
269274
kwargs = self.kwargs
275+
soo = False if self.by_row else True
270276

271277
# transform is currently only for Series/DataFrame
272278
assert isinstance(obj, ABCNDFrame)
@@ -279,7 +285,7 @@ def transform_dict_like(self, func) -> DataFrame:
279285
results: dict[Hashable, DataFrame | Series] = {}
280286
for name, how in func.items():
281287
colg = obj._gotitem(name, ndim=1)
282-
results[name] = colg.transform(how, 0, *args, **kwargs)
288+
results[name] = colg.transform(how, 0, *args, series_ops_only=soo, **kwargs)
283289
return concat(results, axis=1)
284290

285291
def transform_str_or_callable(self, func) -> DataFrame | Series:
@@ -602,7 +608,10 @@ def apply_list_or_dict_like(self) -> DataFrame | Series:
602608
Result when self.func is a list-like or dict-like, None otherwise.
603609
"""
604610
if self.axis == 1 and isinstance(self.obj, ABCDataFrame):
605-
return self.obj.T.apply(self.func, 0, args=self.args, **self.kwargs).T
611+
soo = False if self.by_row else True
612+
return self.obj.T.apply(
613+
self.func, 0, args=self.args, series_ops_only=soo, **self.kwargs
614+
).T
606615

607616
func = self.func
608617
kwargs = self.kwargs

pandas/core/frame.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9908,11 +9908,30 @@ def aggregate(self, func=None, axis: Axis = 0, *args, **kwargs):
99089908
axis=_shared_doc_kwargs["axis"],
99099909
)
99109910
def transform(
9911-
self, func: AggFuncType, axis: Axis = 0, *args, **kwargs
9911+
self,
9912+
func: AggFuncType,
9913+
axis: Axis = 0,
9914+
*args,
9915+
series_ops_only: bool = False,
9916+
**kwargs,
99129917
) -> DataFrame:
99139918
from pandas.core.apply import frame_apply
99149919

9915-
op = frame_apply(self, func=func, axis=axis, args=args, kwargs=kwargs)
9920+
if not series_ops_only and is_list_like(func):
9921+
cls_name = type(self).__name__
9922+
warnings.warn(
9923+
f"{cls_name}.transform will in the future only operate on "
9924+
"whole series. Set series_ops_only = True to opt into the new behavior "
9925+
f"or use {cls_name}.map to continue operating on series elements.",
9926+
FutureWarning,
9927+
stacklevel=find_stack_level(),
9928+
)
9929+
9930+
by_row = False if series_ops_only else "compat"
9931+
9932+
op = frame_apply(
9933+
self, func=func, axis=axis, args=args, by_row=by_row, kwargs=kwargs
9934+
)
99169935
result = op.transform()
99179936
assert isinstance(result, DataFrame)
99189937
return result

pandas/core/series.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4617,12 +4617,30 @@ def aggregate(self, func=None, axis: Axis = 0, *args, **kwargs):
46174617
axis=_shared_doc_kwargs["axis"],
46184618
)
46194619
def transform(
4620-
self, func: AggFuncType, axis: Axis = 0, *args, **kwargs
4620+
self,
4621+
func: AggFuncType,
4622+
axis: Axis = 0,
4623+
*args,
4624+
series_ops_only: bool = False,
4625+
**kwargs,
46214626
) -> DataFrame | Series:
46224627
# Validate axis argument
46234628
self._get_axis_number(axis)
4629+
if not series_ops_only and not isinstance(func, str):
4630+
cls_name = type(self).__name__
4631+
warnings.warn(
4632+
f"{cls_name}.transform will in the future only operate on "
4633+
"whole series. Set series_ops_only = True to opt into the new behavior "
4634+
f"or use {cls_name}.map to continue operating on series elements.",
4635+
FutureWarning,
4636+
stacklevel=find_stack_level(),
4637+
)
4638+
4639+
by_row = False if series_ops_only else "compat"
46244640
ser = self.copy(deep=False) if using_copy_on_write() else self
4625-
result = SeriesApply(ser, func=func, args=args, kwargs=kwargs).transform()
4641+
result = SeriesApply(
4642+
ser, func=func, by_row=by_row, args=args, kwargs=kwargs
4643+
).transform()
46264644
return result
46274645

46284646
def apply(

pandas/tests/apply/common.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,34 @@
1+
import pandas as pd
2+
import pandas._testing as tm
3+
from pandas.api.types import is_list_like
14
from pandas.core.groupby.base import transformation_kernels
25

36
# There is no Series.cumcount or DataFrame.cumcount
47
series_transform_kernels = [
58
x for x in sorted(transformation_kernels) if x != "cumcount"
69
]
710
frame_transform_kernels = [x for x in sorted(transformation_kernels) if x != "cumcount"]
11+
12+
13+
def transform_obj(obj, func, *args, axis=0, series_ops_only=False, **kwargs):
14+
"""helper function to ease use of series_ops_only and deprecation warning."""
15+
if series_ops_only:
16+
result = obj.transform(
17+
func, axis, *args, series_ops_only=series_ops_only, **kwargs
18+
)
19+
elif isinstance(obj, pd.DataFrame) and not is_list_like(func):
20+
result = obj.transform(func, axis, *args, **kwargs)
21+
elif isinstance(func, str):
22+
result = obj.transform(func, axis, *args, **kwargs)
23+
else:
24+
cls_name = type(obj).__name__
25+
msg = (
26+
f"{cls_name}.transform will in the future only operate on "
27+
"whole series. Set series_ops_only = True to opt into the new behavior "
28+
f"or use {cls_name}.map to continue operating on series elements."
29+
)
30+
with tm.assert_produces_warning(FutureWarning, match=msg):
31+
result = obj.transform(
32+
func, axis, *args, series_ops_only=series_ops_only, **kwargs
33+
)
34+
return result

0 commit comments

Comments
 (0)