Skip to content

Commit ed60b0c

Browse files
committed
DEPR: deprecate element-wise operations in (Series|DataFrame).transform
1 parent 53243e8 commit ed60b0c

11 files changed

+288
-173
lines changed

pandas/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,3 +2005,11 @@ def warsaw(request) -> str:
20052005
@pytest.fixture()
20062006
def arrow_string_storage():
20072007
return ("pyarrow", "pyarrow_numpy")
2008+
2009+
2010+
@pytest.fixture(params=[True, False])
2011+
def series_ops_only(request):
2012+
"""
2013+
Parameter used in Series.transform and DataFrame.transform. Remove in pandas v3.0.
2014+
"""
2015+
return request.param

pandas/core/apply.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,14 @@ def transform(self) -> DataFrame | Series:
206206
axis = self.axis
207207
args = self.args
208208
kwargs = self.kwargs
209+
by_row = self.by_row
209210

210211
is_series = obj.ndim == 1
211212

212213
if obj._get_axis_number(axis) == 1:
213214
assert not is_series
214-
return obj.T.transform(func, 0, *args, **kwargs).T
215+
soo = False if by_row else True
216+
return obj.T.transform(func, 0, *args, series_ops_only=soo, **kwargs).T
215217

216218
if is_list_like(func) and not is_dict_like(func):
217219
func = cast(list[AggFuncTypeBase], func)
@@ -225,14 +227,17 @@ def transform(self) -> DataFrame | Series:
225227
func = cast(AggFuncTypeDict, func)
226228
return self.transform_dict_like(func)
227229

228-
# func is either str or callable
229-
func = cast(AggFuncTypeBase, func)
230-
try:
231-
result = self.transform_str_or_callable(func)
232-
except TypeError:
233-
raise
234-
except Exception as err:
235-
raise ValueError("Transform function failed") from err
230+
if not self.by_row:
231+
result = obj.apply(func, by_row=by_row, args=args, **kwargs)
232+
else:
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
236241

237242
# Functions that transform may return empty Series/DataFrame
238243
# when the dtype is not appropriate
@@ -262,6 +267,7 @@ def transform_dict_like(self, func) -> DataFrame:
262267
obj = self.obj
263268
args = self.args
264269
kwargs = self.kwargs
270+
soo = False if self.by_row else True
265271

266272
# transform is currently only for Series/DataFrame
267273
assert isinstance(obj, ABCNDFrame)
@@ -274,7 +280,7 @@ def transform_dict_like(self, func) -> DataFrame:
274280
results: dict[Hashable, DataFrame | Series] = {}
275281
for name, how in func.items():
276282
colg = obj._gotitem(name, ndim=1)
277-
results[name] = colg.transform(how, 0, *args, **kwargs)
283+
results[name] = colg.transform(how, 0, *args, series_ops_only=soo, **kwargs)
278284
return concat(results, axis=1)
279285

280286
def transform_str_or_callable(self, func) -> DataFrame | Series:
@@ -591,7 +597,10 @@ def apply_list_or_dict_like(self) -> DataFrame | Series:
591597
Result when self.func is a list-like or dict-like, None otherwise.
592598
"""
593599
if self.axis == 1 and isinstance(self.obj, ABCDataFrame):
594-
return self.obj.T.apply(self.func, 0, args=self.args, **self.kwargs).T
600+
soo = False if self.by_row else True
601+
return self.obj.T.apply(
602+
self.func, 0, args=self.args, series_ops_only=soo, **self.kwargs
603+
).T
595604

596605
func = self.func
597606
kwargs = self.kwargs

pandas/core/frame.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9902,11 +9902,30 @@ def aggregate(self, func=None, axis: Axis = 0, *args, **kwargs):
99029902
axis=_shared_doc_kwargs["axis"],
99039903
)
99049904
def transform(
9905-
self, func: AggFuncType, axis: Axis = 0, *args, **kwargs
9905+
self,
9906+
func: AggFuncType,
9907+
axis: Axis = 0,
9908+
*args,
9909+
series_ops_only: bool = False,
9910+
**kwargs,
99069911
) -> DataFrame:
99079912
from pandas.core.apply import frame_apply
99089913

9909-
op = frame_apply(self, func=func, axis=axis, args=args, kwargs=kwargs)
9914+
if not series_ops_only and is_list_like(func):
9915+
cls_name = type(self).__name__
9916+
warnings.warn(
9917+
f"{cls_name}.transform will in the future only operate on "
9918+
"whole series. Set series_ops_only = True to opt into the new behavior "
9919+
f"or use {cls_name}.map to continue operating on series elements.",
9920+
FutureWarning,
9921+
stacklevel=find_stack_level(),
9922+
)
9923+
9924+
by_row = False if series_ops_only else "compat"
9925+
9926+
op = frame_apply(
9927+
self, func=func, axis=axis, args=args, by_row=by_row, kwargs=kwargs
9928+
)
99109929
result = op.transform()
99119930
assert isinstance(result, DataFrame)
99129931
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)