Skip to content

Commit d231fb5

Browse files
rhshadrachvladu
authored andcommitted
DEPR: Series/DataFrame.transform partial failure except TypeError (pandas-dev#40288)
1 parent 1b3198f commit d231fb5

File tree

5 files changed

+104
-26
lines changed

5 files changed

+104
-26
lines changed

doc/source/whatsnew/v1.3.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ Deprecations
423423
- 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`)
424424
- 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`)
425425
- Deprecated :meth:`.Styler.set_na_rep` and :meth:`.Styler.set_precision` in favour of :meth:`.Styler.format` with ``na_rep`` and ``precision`` as existing and new input arguments respectively (:issue:`40134`, :issue:`40425`)
426-
- Deprecated allowing partial failure in :meth:`Series.transform` and :meth:`DataFrame.transform` when ``func`` is list-like or dict-like; will raise if any function fails on a column in a future version (:issue:`40211`)
426+
- Deprecated allowing partial failure in :meth:`Series.transform` and :meth:`DataFrame.transform` when ``func`` is list-like or dict-like and raises anything but ``TypeError``; ``func`` raising anything but a ``TypeError`` will raise in a future version (:issue:`40211`)
427427
- Deprecated support for ``np.ma.mrecords.MaskedRecords`` in the :class:`DataFrame` constructor, pass ``{name: data[name] for name in data.dtype.names}`` instead (:issue:`40363`)
428428
- Deprecated the use of ``**kwargs`` in :class:`.ExcelWriter`; use the keyword argument ``engine_kwargs`` instead (:issue:`40430`)
429429

pandas/core/apply.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,10 @@ def transform(self) -> FrameOrSeriesUnion:
227227
func = cast(AggFuncTypeBase, func)
228228
try:
229229
result = self.transform_str_or_callable(func)
230-
except Exception:
231-
raise ValueError("Transform function failed")
230+
except TypeError:
231+
raise
232+
except Exception as err:
233+
raise ValueError("Transform function failed") from err
232234

233235
# Functions that transform may return empty Series/DataFrame
234236
# when the dtype is not appropriate
@@ -265,6 +267,7 @@ def transform_dict_like(self, func):
265267

266268
results: Dict[Hashable, FrameOrSeriesUnion] = {}
267269
failed_names = []
270+
all_type_errors = True
268271
for name, how in func.items():
269272
colg = obj._gotitem(name, ndim=1)
270273
try:
@@ -275,16 +278,18 @@ def transform_dict_like(self, func):
275278
"No transform functions were provided",
276279
}:
277280
raise err
278-
else:
281+
elif not isinstance(err, TypeError):
282+
all_type_errors = False
279283
failed_names.append(name)
280284
# combine results
281285
if not results:
282-
raise ValueError("Transform function failed")
286+
klass = TypeError if all_type_errors else ValueError
287+
raise klass("Transform function failed")
283288
if len(failed_names) > 0:
284289
warnings.warn(
285-
f"{failed_names} did not transform successfully. "
286-
f"Allowing for partial failure is deprecated, this will raise "
287-
f"a ValueError in a future version of pandas."
290+
f"{failed_names} did not transform successfully and did not raise "
291+
f"a TypeError. If any error is raised except for TypeError, "
292+
f"this will raise in a future version of pandas. "
288293
f"Drop these columns/ops to avoid this warning.",
289294
FutureWarning,
290295
stacklevel=4,

pandas/tests/apply/test_frame_transform.py

+48-10
Original file line numberDiff line numberDiff line change
@@ -156,35 +156,68 @@ def test_transform_method_name(method):
156156

157157

158158
@pytest.mark.parametrize("op", [*frame_kernels_raise, lambda x: x + 1])
159-
def test_transform_bad_dtype(op, frame_or_series):
159+
def test_transform_bad_dtype(op, frame_or_series, request):
160160
# GH 35964
161+
if op == "rank":
162+
request.node.add_marker(
163+
pytest.mark.xfail(reason="GH 40418: rank does not raise a TypeError")
164+
)
165+
161166
obj = DataFrame({"A": 3 * [object]}) # DataFrame that will fail on most transforms
162167
if frame_or_series is not DataFrame:
163168
obj = obj["A"]
164169

165-
msg = "Transform function failed"
166-
167170
# tshift is deprecated
168171
warn = None if op != "tshift" else FutureWarning
169172
with tm.assert_produces_warning(warn):
170-
with pytest.raises(ValueError, match=msg):
173+
with pytest.raises(TypeError, match="unsupported operand|not supported"):
171174
obj.transform(op)
172-
with pytest.raises(ValueError, match=msg):
175+
with pytest.raises(TypeError, match="Transform function failed"):
173176
obj.transform([op])
174-
with pytest.raises(ValueError, match=msg):
177+
with pytest.raises(TypeError, match="Transform function failed"):
175178
obj.transform({"A": op})
176-
with pytest.raises(ValueError, match=msg):
179+
with pytest.raises(TypeError, match="Transform function failed"):
177180
obj.transform({"A": [op]})
178181

179182

180183
@pytest.mark.parametrize("op", frame_kernels_raise)
181-
def test_transform_partial_failure(op):
182-
# GH 35964 & GH 40211
183-
match = "Allowing for partial failure is deprecated"
184+
def test_transform_partial_failure_typeerror(op):
185+
# GH 35964
186+
187+
if op == "rank":
188+
pytest.skip("GH 40418: rank does not raise a TypeError")
184189

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

193+
expected = df[["B"]].transform([op])
194+
result = df.transform([op])
195+
tm.assert_equal(result, expected)
196+
197+
expected = df[["B"]].transform({"B": op})
198+
result = df.transform({"A": op, "B": op})
199+
tm.assert_equal(result, expected)
200+
201+
expected = df[["B"]].transform({"B": [op]})
202+
result = df.transform({"A": [op], "B": [op]})
203+
tm.assert_equal(result, expected)
204+
205+
expected = df.transform({"A": ["shift"], "B": [op]})
206+
result = df.transform({"A": [op, "shift"], "B": [op]})
207+
tm.assert_equal(result, expected)
208+
209+
210+
def test_transform_partial_failure_valueerror():
211+
# GH 40211
212+
match = ".*did not transform successfully and did not raise a TypeError"
213+
214+
def op(x):
215+
if np.sum(np.sum(x)) < 10:
216+
raise ValueError
217+
return x
218+
219+
df = DataFrame({"A": [1, 2, 3], "B": [400, 500, 600]})
220+
188221
expected = df[["B"]].transform([op])
189222
with tm.assert_produces_warning(FutureWarning, match=match):
190223
result = df.transform([op])
@@ -200,6 +233,11 @@ def test_transform_partial_failure(op):
200233
result = df.transform({"A": [op], "B": [op]})
201234
tm.assert_equal(result, expected)
202235

236+
expected = df.transform({"A": ["shift"], "B": [op]})
237+
with tm.assert_produces_warning(FutureWarning, match=match, check_stacklevel=False):
238+
result = df.transform({"A": [op, "shift"], "B": [op]})
239+
tm.assert_equal(result, expected)
240+
203241

204242
@pytest.mark.parametrize("use_apply", [True, False])
205243
def test_transform_passes_args(use_apply, frame_or_series):

pandas/tests/apply/test_invalid_arg.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def test_transform_none_to_type():
276276
# GH#34377
277277
df = DataFrame({"a": [None]})
278278
msg = "Transform function failed"
279-
with pytest.raises(ValueError, match=msg):
279+
with pytest.raises(TypeError, match=msg):
280280
df.transform({"a": int})
281281

282282

pandas/tests/apply/test_series_apply.py

+42-7
Original file line numberDiff line numberDiff line change
@@ -259,29 +259,64 @@ def test_transform(string_series):
259259

260260
@pytest.mark.parametrize("op", series_transform_kernels)
261261
def test_transform_partial_failure(op, request):
262-
# GH 35964 & GH 40211
262+
# GH 35964
263263
if op in ("ffill", "bfill", "pad", "backfill", "shift"):
264264
request.node.add_marker(
265265
pytest.mark.xfail(reason=f"{op} is successful on any dtype")
266266
)
267-
match = "Allowing for partial failure is deprecated"
267+
if op in ("rank", "fillna"):
268+
pytest.skip(f"{op} doesn't raise TypeError on object")
268269

269270
# Using object makes most transform kernels fail
270271
ser = Series(3 * [object])
271272

272273
expected = ser.transform(["shift"])
273-
with tm.assert_produces_warning(FutureWarning, match=match):
274-
result = ser.transform([op, "shift"])
274+
result = ser.transform([op, "shift"])
275275
tm.assert_equal(result, expected)
276276

277277
expected = ser.transform({"B": "shift"})
278-
with tm.assert_produces_warning(FutureWarning, match=match):
279-
result = ser.transform({"A": op, "B": "shift"})
278+
result = ser.transform({"A": op, "B": "shift"})
280279
tm.assert_equal(result, expected)
281280

282281
expected = ser.transform({"B": ["shift"]})
282+
result = ser.transform({"A": [op], "B": ["shift"]})
283+
tm.assert_equal(result, expected)
284+
285+
expected = ser.transform({"A": ["shift"], "B": [op]})
286+
result = ser.transform({"A": [op, "shift"], "B": [op]})
287+
tm.assert_equal(result, expected)
288+
289+
290+
def test_transform_partial_failure_valueerror():
291+
# GH 40211
292+
match = ".*did not transform successfully and did not raise a TypeError"
293+
294+
def noop(x):
295+
return x
296+
297+
def raising_op(_):
298+
raise ValueError
299+
300+
ser = Series(3 * [object])
301+
302+
expected = ser.transform([noop])
303+
with tm.assert_produces_warning(FutureWarning, match=match):
304+
result = ser.transform([noop, raising_op])
305+
tm.assert_equal(result, expected)
306+
307+
expected = ser.transform({"B": noop})
283308
with tm.assert_produces_warning(FutureWarning, match=match):
284-
result = ser.transform({"A": [op], "B": ["shift"]})
309+
result = ser.transform({"A": raising_op, "B": noop})
310+
tm.assert_equal(result, expected)
311+
312+
expected = ser.transform({"B": [noop]})
313+
with tm.assert_produces_warning(FutureWarning, match=match):
314+
result = ser.transform({"A": [raising_op], "B": [noop]})
315+
tm.assert_equal(result, expected)
316+
317+
expected = ser.transform({"A": [noop], "B": [noop]})
318+
with tm.assert_produces_warning(FutureWarning, match=match, check_stacklevel=False):
319+
result = ser.transform({"A": [noop, raising_op], "B": [noop]})
285320
tm.assert_equal(result, expected)
286321

287322

0 commit comments

Comments
 (0)