From 5f0bd7dfbbc5aab5e53f255459954d17bc11ac0d Mon Sep 17 00:00:00 2001 From: sharkipelago Date: Fri, 22 Aug 2025 13:43:13 -0400 Subject: [PATCH 1/7] changed series.round() object dtype behavior to pointwise --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/series.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index b94d82f3c9783..50a9955195154 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -192,6 +192,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` now operates pointwise on columns of object dtype (:issue:`61682`) - :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/series.py b/pandas/core/series.py index 00cff09801f1a..4e0ebb7c6eb35 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2517,7 +2517,11 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Series: """ nv.validate_round(args, kwargs) if self.dtype == "object": - raise TypeError("Expected numeric dtype, got object instead.") + round_func = functools.partial(round, ndigits=decimals) + new_values = self._map_values(round_func) + return self._constructor( + new_values, index=self.index, copy=False + ).__finalize__(self, method="map") new_mgr = self._mgr.round(decimals=decimals) return self._constructor_from_mgr(new_mgr, axes=new_mgr.axes).__finalize__( self, method="round" From a9196468865b141304eaa27fe585832a0249acc8 Mon Sep 17 00:00:00 2001 From: sharkipelago Date: Fri, 22 Aug 2025 18:46:36 -0400 Subject: [PATCH 2/7] updated tests --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/core/series.py | 13 ++++++++----- pandas/tests/series/methods/test_round.py | 11 +++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 50a9955195154..8d6c384712ca8 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -192,7 +192,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` now operates pointwise on columns of object dtype (:issue:`61682`) +- :meth:`Series.round` now operates pointwise on columns of object dtype (:issue:`62173`) - :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/series.py b/pandas/core/series.py index 4e0ebb7c6eb35..6222fa036dddf 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2517,11 +2517,14 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Series: """ nv.validate_round(args, kwargs) if self.dtype == "object": - round_func = functools.partial(round, ndigits=decimals) - new_values = self._map_values(round_func) - return self._constructor( - new_values, index=self.index, copy=False - ).__finalize__(self, method="map") + try: + round_func = functools.partial(round, ndigits=decimals) + new_values = self._map_values(round_func) + return self._constructor( + new_values, index=self.index, copy=False + ).__finalize__(self, method="map") + except TypeError as e: + raise TypeError("Expected numeric entries for dtype object.") from e 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/series/methods/test_round.py b/pandas/tests/series/methods/test_round.py index a78f77e990ae1..d8828200dcd6f 100644 --- a/pandas/tests/series/methods/test_round.py +++ b/pandas/tests/series/methods/test_round.py @@ -74,8 +74,11 @@ def test_round_ea_boolean(self): 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." + # GH#61206, GH#62173 + ser = Series([0.232], dtype="object") + expected = Series([0.2]) + tm.assert_series_equal(ser.round(1), expected) + ser2 = Series(["bar"], dtype="object") + msg = "Expected numeric entries for dtype object." with pytest.raises(TypeError, match=msg): - ser.round() + ser2.round() From fd1f6b69ddaca596f8724c4bee28edb8ef3ced44 Mon Sep 17 00:00:00 2001 From: sharkipelago Date: Fri, 22 Aug 2025 18:58:44 -0400 Subject: [PATCH 3/7] corrected PR number --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/tests/series/methods/test_round.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index d414e234ec59d..17d6226d2c60a 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -210,7 +210,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` now operates pointwise on columns of object dtype (:issue:`62173`) +- :meth:`Series.round` now operates 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/tests/series/methods/test_round.py b/pandas/tests/series/methods/test_round.py index d8828200dcd6f..9f13cc5efa0c1 100644 --- a/pandas/tests/series/methods/test_round.py +++ b/pandas/tests/series/methods/test_round.py @@ -74,7 +74,7 @@ def test_round_ea_boolean(self): tm.assert_series_equal(ser, expected) def test_round_dtype_object(self): - # GH#61206, GH#62173 + # GH#61206, GH#62174 ser = Series([0.232], dtype="object") expected = Series([0.2]) tm.assert_series_equal(ser.round(1), expected) From 83acc9eb95da8e64a80de2d35f6d0ed35d36f9d7 Mon Sep 17 00:00:00 2001 From: sharkipelago Date: Sun, 14 Sep 2025 16:03:04 -0400 Subject: [PATCH 4/7] moved logic to block-level --- pandas/core/internals/blocks.py | 19 ++++++++++++++++--- pandas/core/series.py | 9 --------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 766c45fe1de6b..672aa0e27d37d 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 ( @@ -1503,9 +1504,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 +1513,19 @@ 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) + try: + values = algos.map_array(self.values, round_func) + except TypeError as err: + raise TypeError("Expected numeric entries for dtype object.") from err + + refs = None + if values is self.values: + refs = self.refs + + return self.make_block_same_class(values, refs=refs) + if not self.is_numeric or self.is_bool: return self.copy(deep=False) # TODO: round only defined on BaseMaskedArray diff --git a/pandas/core/series.py b/pandas/core/series.py index c7262bdb0096f..f92a51455229a 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2514,15 +2514,6 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Series: dtype: float64 """ nv.validate_round(args, kwargs) - if self.dtype == "object": - try: - round_func = functools.partial(round, ndigits=decimals) - new_values = self._map_values(round_func) - return self._constructor( - new_values, index=self.index, copy=False - ).__finalize__(self, method="map") - except TypeError as e: - raise TypeError("Expected numeric entries for dtype object.") from e new_mgr = self._mgr.round(decimals=decimals) return self._constructor_from_mgr(new_mgr, axes=new_mgr.axes).__finalize__( self, method="round" From 084569f4f88402b478bd70cd20a89d2fc054d0b2 Mon Sep 17 00:00:00 2001 From: sharkipelago Date: Sat, 20 Sep 2025 19:04:50 -0400 Subject: [PATCH 5/7] updated for frames to operate pointwise --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/core/frame.py | 9 +----- pandas/core/internals/blocks.py | 9 ++++-- pandas/tests/frame/methods/test_round.py | 38 +++++++++++++++++++++++ pandas/tests/series/methods/test_round.py | 12 ++++--- 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 6b09cb4843eb4..cebd9b4bbd2f8 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -210,7 +210,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` now operates pointwise on columns of object dtype (:issue:`62174`) +- :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 f61e231736e31..149d7f1def67a 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, @@ -11308,15 +11306,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) - 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 672aa0e27d37d..3ebdd0a0fc461 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -262,7 +262,7 @@ def make_block_same_class( @final def __repr__(self) -> str: - # don't want to print out all of the items here + # don't want to out all of the items here name = type(self).__name__ if self.ndim == 1: result = f"{name}: {len(self)} dtype: {self.dtype}" @@ -346,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) @@ -1515,8 +1514,12 @@ def round(self, decimals: int) -> Self: """ if self.dtype == _dtype_obj: round_func = functools.partial(round, ndigits=decimals) + mapper = functools.partial(algos.map_array, mapper=round_func) try: - values = algos.map_array(self.values, round_func) + if self.values.ndim == 1: + values = algos.map_array(self.values, round_func) + else: + values = np.apply_along_axis(mapper, 0, self.values) except TypeError as err: raise TypeError("Expected numeric entries for dtype object.") from err diff --git a/pandas/tests/frame/methods/test_round.py b/pandas/tests/frame/methods/test_round.py index a96df27b48d7d..af5390cd0c9bd 100644 --- a/pandas/tests/frame/methods/test_round.py +++ b/pandas/tests/frame/methods/test_round.py @@ -223,3 +223,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 9f13cc5efa0c1..d857f1023c28b 100644 --- a/pandas/tests/series/methods/test_round.py +++ b/pandas/tests/series/methods/test_round.py @@ -73,12 +73,16 @@ def test_round_ea_boolean(self): result.iloc[0] = False tm.assert_series_equal(ser, expected) - def test_round_dtype_object(self): + def test_round_numeric_dtype_object(self): # GH#61206, GH#62174 ser = Series([0.232], dtype="object") expected = Series([0.2]) - tm.assert_series_equal(ser.round(1), expected) - ser2 = Series(["bar"], dtype="object") + 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): - ser2.round() + ser.round() From 8226901c19e78a5582f0a7a7b051fd5318fbc674 Mon Sep 17 00:00:00 2001 From: sharkipelago Date: Mon, 22 Sep 2025 14:38:03 -0400 Subject: [PATCH 6/7] updated 2d array case --- pandas/core/internals/blocks.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 3ebdd0a0fc461..37555048ac09b 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -262,7 +262,7 @@ def make_block_same_class( @final def __repr__(self) -> str: - # don't want to out all of the items here + # don't want to print out all of the items here name = type(self).__name__ if self.ndim == 1: result = f"{name}: {len(self)} dtype: {self.dtype}" @@ -1514,12 +1514,13 @@ def round(self, decimals: int) -> Self: """ if self.dtype == _dtype_obj: round_func = functools.partial(round, ndigits=decimals) - mapper = functools.partial(algos.map_array, mapper=round_func) try: if self.values.ndim == 1: values = algos.map_array(self.values, round_func) else: - values = np.apply_along_axis(mapper, 0, self.values) + values = algos.map_array(self.values.ravel(), round_func).reshape( + self.values.shape + ) except TypeError as err: raise TypeError("Expected numeric entries for dtype object.") from err From 6de7c1f2907f7789d3c125cd7fee4009e3457b2a Mon Sep 17 00:00:00 2001 From: sharkipelago Date: Tue, 4 Nov 2025 06:19:26 -0500 Subject: [PATCH 7/7] removed catch-reraise and redundant values check --- pandas/core/internals/blocks.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 7dec16649b6a3..02dd396cf5457 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1514,21 +1514,13 @@ def round(self, decimals: int) -> Self: """ if self.dtype == _dtype_obj: round_func = functools.partial(round, ndigits=decimals) - try: - 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 - ) - except TypeError as err: - raise TypeError("Expected numeric entries for dtype object.") from err - - refs = None - if values is self.values: - refs = self.refs - - return self.make_block_same_class(values, refs=refs) + 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)):