diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 752d08a526d8c..18b67c435b11b 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -211,6 +211,7 @@ Other enhancements - :meth:`Series.map` can now accept kwargs to pass on to func (:issue:`59814`) - :meth:`Series.map` now accepts an ``engine`` parameter to allow execution with a third-party execution engine (:issue:`61125`) - :meth:`Series.rank` and :meth:`DataFrame.rank` with numpy-nullable dtypes preserve ``NA`` values and return ``UInt64`` dtype where appropriate instead of casting ``NA`` to ``NaN`` with ``float64`` dtype (:issue:`62043`) +- :meth:`Series.round` and :meth:`DataFrame.round` now operate pointwise on columns of object dtype (:issue:`62174`) - :meth:`Series.str.get_dummies` now accepts a ``dtype`` parameter to specify the dtype of the resulting DataFrame (:issue:`47872`) - :meth:`pandas.concat` will raise a ``ValueError`` when ``ignore_index=True`` and ``keys`` is not ``None`` (:issue:`59274`) - :py:class:`frozenset` elements in pandas objects are now natively printed (:issue:`60690`) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index a35ef122ed512..bc526c8d48785 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -99,10 +99,8 @@ is_dataclass, is_dict_like, is_float, - is_float_dtype, is_hashable, is_integer, - is_integer_dtype, is_iterator, is_list_like, is_scalar, @@ -11560,24 +11558,10 @@ def round( def _dict_round(df: DataFrame, decimals) -> Iterator[Series]: for col, vals in df.items(): try: - yield _series_round(vals, decimals[col]) + yield vals.round(decimals[col]) except KeyError: yield vals - def _series_round(ser: Series, decimals: int) -> Series: - if is_integer_dtype(ser.dtype) or is_float_dtype(ser.dtype): - return ser.round(decimals) - elif isinstance(ser._values, (DatetimeArray, TimedeltaArray, PeriodArray)): - # GH#57781 - # TODO: also the ArrowDtype analogues? - warnings.warn( - "obj.round has no effect with datetime, timedelta, " - "or period dtypes. Use obj.dt.round(...) instead.", - UserWarning, - stacklevel=find_stack_level(), - ) - return ser - nv.validate_round(args, kwargs) if isinstance(decimals, (dict, Series)): diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index a9ad561cbc393..02dd396cf5457 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import inspect import re from typing import ( @@ -345,7 +346,6 @@ def apply(self, func, **kwargs) -> list[Block]: one """ result = func(self.values, **kwargs) - result = maybe_coerce_values(result) return self._split_op_result(result) @@ -1503,9 +1503,8 @@ def quantile( def round(self, decimals: int) -> Self: """ Rounds the values. - If the block is not of an integer or float dtype, nothing happens. - This is consistent with DataFrame.round behavior. - (Note: Series.round would raise) + If the block is of object dtype, it will operate pointwise and possibly raise. + Otherwise, if the block is not of an integer or float dtype, nothing happens. Parameters ---------- @@ -1513,6 +1512,16 @@ def round(self, decimals: int) -> Self: Number of decimal places to round to. Caller is responsible for validating this """ + if self.dtype == _dtype_obj: + round_func = functools.partial(round, ndigits=decimals) + if self.values.ndim == 1: + values = algos.map_array(self.values, round_func) + else: + values = algos.map_array(self.values.ravel(), round_func).reshape( + self.values.shape + ) + return self.make_block_same_class(values, refs=None) + if not self.is_numeric or self.is_bool: if isinstance(self.values, (DatetimeArray, TimedeltaArray, PeriodArray)): # GH#57781 diff --git a/pandas/core/series.py b/pandas/core/series.py index 1a8645cf1815d..2f0e4b520169e 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2515,8 +2515,6 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Series: dtype: float64 """ nv.validate_round(args, kwargs) - if self.dtype == "object": - raise TypeError("Expected numeric dtype, got object instead.") new_mgr = self._mgr.round(decimals=decimals) return self._constructor_from_mgr(new_mgr, axes=new_mgr.axes).__finalize__( self, method="round" diff --git a/pandas/tests/frame/methods/test_round.py b/pandas/tests/frame/methods/test_round.py index 5f2566fefca76..2571bbe112772 100644 --- a/pandas/tests/frame/methods/test_round.py +++ b/pandas/tests/frame/methods/test_round.py @@ -227,3 +227,41 @@ def test_round_empty_not_input(self): result = df.round() tm.assert_frame_equal(df, result) assert df is not result + + def test_round_object_columns(self): + # GH#62174 + df = DataFrame( + { + "a": Series([1.1111, 2.2222, 3.3333], dtype="object"), + "b": Series([4.4444, 5.5555, 6.6666]), + "c": Series([7.7777, 8.8888, 9.9999], dtype="object"), + } + ) + result = df.round(2) + expected = DataFrame( + { + "a": Series([1.11, 2.22, 3.33]), + "b": Series([4.44, 5.56, 6.67]), + "c": Series([7.78, 8.89, 10.0]), + } + ) + tm.assert_frame_equal(result, expected) + + def test_round_object_columns_with_dict(self): + # GH#62174 + df = DataFrame( + { + "a": Series([1.1111, 2.2222, 3.3333], dtype="object"), + "b": Series([4.4444, 5.5555, 6.6666]), + "c": Series([7.7777, 8.8888, 9.9999], dtype="object"), + } + ) + result = df.round({"a": 1, "b": 2, "c": 3}) + expected = DataFrame( + { + "a": Series([1.1, 2.2, 3.3]), + "b": Series([4.44, 5.56, 6.67]), + "c": Series([7.778, 8.889, 10.0]), + } + ) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/series/methods/test_round.py b/pandas/tests/series/methods/test_round.py index a78f77e990ae1..d857f1023c28b 100644 --- a/pandas/tests/series/methods/test_round.py +++ b/pandas/tests/series/methods/test_round.py @@ -73,9 +73,16 @@ def test_round_ea_boolean(self): result.iloc[0] = False tm.assert_series_equal(ser, expected) - def test_round_dtype_object(self): - # GH#61206 - ser = Series([0.2], dtype="object") - msg = "Expected numeric dtype, got object instead." + def test_round_numeric_dtype_object(self): + # GH#61206, GH#62174 + ser = Series([0.232], dtype="object") + expected = Series([0.2]) + result = ser.round(1) + tm.assert_series_equal(result, expected) + + def test_round_non_numeric_dtype_object(self): + # GH#62174 + ser = Series(["bar"], dtype="object") + msg = "Expected numeric entries for dtype object." with pytest.raises(TypeError, match=msg): ser.round()