Skip to content

Commit acf8a22

Browse files
committed
DEP: Partial failure in Series.transform and DataFrame.transform.
1 parent 782dc18 commit acf8a22

File tree

5 files changed

+80
-14
lines changed

5 files changed

+80
-14
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ Deprecations
363363
- Deprecated :meth:`core.window.ewm.ExponentialMovingWindow.vol` (:issue:`39220`)
364364
- Using ``.astype`` to convert between ``datetime64[ns]`` dtype and :class:`DatetimeTZDtype` is deprecated and will raise in a future version, use ``obj.tz_localize`` or ``obj.dt.tz_localize`` instead (:issue:`38622`)
365365
- Deprecated casting ``datetime.date`` objects to ``datetime64`` when used as ``fill_value`` in :meth:`DataFrame.unstack`, :meth:`DataFrame.shift`, :meth:`Series.shift`, and :meth:`DataFrame.reindex`, pass ``pd.Timestamp(dateobj)`` instead (:issue:`39767`)
366+
- Deprecated allowing partial failure in :meth:`Series.transform` and :meth:`DataFrame.transform` when ``func`` is list-like and dict-like; will raise if any function fails on column in a future version (:issue:`40211`)
366367

367368
.. ---------------------------------------------------------------------------
368369

pandas/core/apply.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Union,
1616
cast,
1717
)
18+
import warnings
1819

1920
import numpy as np
2021

@@ -267,6 +268,7 @@ def transform_dict_like(self, func):
267268
func = self.normalize_dictlike_arg("transform", obj, func)
268269

269270
results: Dict[Hashable, FrameOrSeriesUnion] = {}
271+
failed_names = []
270272
for name, how in func.items():
271273
colg = obj._gotitem(name, ndim=1)
272274
try:
@@ -277,10 +279,21 @@ def transform_dict_like(self, func):
277279
"No transform functions were provided",
278280
}:
279281
raise err
280-
282+
else:
283+
failed_names.append(name)
281284
# combine results
282285
if not results:
283286
raise ValueError("Transform function failed")
287+
if len(failed_names) > 0:
288+
warnings.warn(
289+
f"Columns {failed_names} did not transform successfully. "
290+
f"Allowing for partial failure is deprecated, this will raise "
291+
f"a ValueError in a future version of pandas."
292+
f"Drop these columns before calling transform to avoid"
293+
f"this warning.",
294+
FutureWarning,
295+
stacklevel=4,
296+
)
284297
return concat(results, axis=1)
285298

286299
def transform_str_or_callable(self, func) -> FrameOrSeriesUnion:

pandas/tests/apply/common.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from pandas.core.groupby.base import transformation_kernels
2+
3+
# tshift only works on time index and is deprecated
4+
# There is no Series.cumcount or DataFrame.cumcount
5+
series_transform_kernels = [
6+
x for x in sorted(transformation_kernels) if x not in ["tshift", "cumcount"]
7+
]
8+
frame_transform_kernels = [
9+
x for x in sorted(transformation_kernels) if x not in ["tshift", "cumcount"]
10+
]

pandas/tests/apply/test_frame_transform.py

+12-13
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,10 @@
99
Series,
1010
)
1111
import pandas._testing as tm
12-
from pandas.core.groupby.base import transformation_kernels
12+
from pandas.core.base import SpecificationError
13+
from pandas.tests.apply.common import frame_transform_kernels
1314
from pandas.tests.frame.common import zip_frames
1415

15-
# tshift only works on time index and is deprecated
16-
# There is no DataFrame.cumcount
17-
frame_kernels = [
18-
x for x in sorted(transformation_kernels) if x not in ["tshift", "cumcount"]
19-
]
20-
2116

2217
def unpack_obj(obj, klass, axis):
2318
"""
@@ -44,7 +39,7 @@ def test_transform_ufunc(axis, float_frame, frame_or_series):
4439
tm.assert_equal(result, expected)
4540

4641

47-
@pytest.mark.parametrize("op", frame_kernels)
42+
@pytest.mark.parametrize("op", frame_transform_kernels)
4843
def test_transform_groupby_kernel(axis, float_frame, op):
4944
# GH 35964
5045

@@ -158,7 +153,7 @@ def test_transform_method_name(method):
158153

159154

160155
wont_fail = ["ffill", "bfill", "fillna", "pad", "backfill", "shift"]
161-
frame_kernels_raise = [x for x in frame_kernels if x not in wont_fail]
156+
frame_kernels_raise = [x for x in frame_transform_kernels if x not in wont_fail]
162157

163158

164159
# mypy doesn't allow adding lists of different types
@@ -187,21 +182,25 @@ def test_transform_bad_dtype(op, frame_or_series):
187182

188183
@pytest.mark.parametrize("op", frame_kernels_raise)
189184
def test_transform_partial_failure(op):
190-
# GH 35964
185+
# GH 35964 & GH 40211
186+
match = "Allowing for partial failure is deprecated"
191187

192188
# Using object makes most transform kernels fail
193189
df = DataFrame({"A": 3 * [object], "B": [1, 2, 3]})
194190

195191
expected = df[["B"]].transform([op])
196-
result = df.transform([op])
192+
with tm.assert_produces_warning(FutureWarning, match=match):
193+
result = df.transform([op])
197194
tm.assert_equal(result, expected)
198195

199196
expected = df[["B"]].transform({"B": op})
200-
result = df.transform({"B": op})
197+
with tm.assert_produces_warning(FutureWarning, match=match):
198+
result = df.transform({"A": op, "B": op})
201199
tm.assert_equal(result, expected)
202200

203201
expected = df[["B"]].transform({"B": [op]})
204-
result = df.transform({"B": [op]})
202+
with tm.assert_produces_warning(FutureWarning, match=match):
203+
result = df.transform({"A": [op], "B": [op]})
205204
tm.assert_equal(result, expected)
206205

207206

pandas/tests/apply/test_series_apply.py

+43
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
timedelta_range,
2121
)
2222
import pandas._testing as tm
23+
from pandas.core.base import SpecificationError
24+
from pandas.tests.apply.common import series_transform_kernels
2325

2426

2527
def test_series_map_box_timedelta():
@@ -256,6 +258,47 @@ def test_transform(string_series):
256258
tm.assert_series_equal(result.reindex_like(expected), expected)
257259

258260

261+
@pytest.mark.parametrize("op", series_transform_kernels)
262+
def test_transform_partial_failure(op, request):
263+
# GH 35964 & GH 40211
264+
if op in ("ffill", "bfill", "pad", "backfill", "shift"):
265+
request.node.add_marker(
266+
pytest.mark.xfail(reason=f"{op} is successful on any dtype")
267+
)
268+
match = "Allowing for partial failure is deprecated"
269+
270+
# Using object makes most transform kernels fail
271+
ser = Series(3 * [object])
272+
273+
expected = ser.transform(["shift"])
274+
with tm.assert_produces_warning(FutureWarning, match=match):
275+
result = ser.transform([op, "shift"])
276+
tm.assert_equal(result, expected)
277+
278+
expected = ser.transform({"B": "shift"})
279+
with tm.assert_produces_warning(FutureWarning, match=match):
280+
result = ser.transform({"A": op, "B": "shift"})
281+
tm.assert_equal(result, expected)
282+
283+
expected = ser.transform({"B": ["shift"]})
284+
with tm.assert_produces_warning(FutureWarning, match=match):
285+
result = ser.transform({"A": [op], "B": ["shift"]})
286+
tm.assert_equal(result, expected)
287+
288+
289+
def test_transform_and_agg_error(string_series):
290+
# we are trying to transform with an aggregator
291+
msg = "cannot combine transform and aggregation"
292+
with pytest.raises(ValueError, match=msg):
293+
with np.errstate(all="ignore"):
294+
string_series.agg(["sqrt", "max"])
295+
296+
msg = "cannot perform both aggregation and transformation"
297+
with pytest.raises(ValueError, match=msg):
298+
with np.errstate(all="ignore"):
299+
string_series.agg({"foo": np.sqrt, "bar": "sum"})
300+
301+
259302
def test_demo():
260303
# demonstration tests
261304
s = Series(range(6), dtype="int64", name="series")

0 commit comments

Comments
 (0)