Skip to content
Open
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +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` 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`)
Expand Down
9 changes: 1 addition & 8 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -11312,15 +11310,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)):
Expand Down
25 changes: 21 additions & 4 deletions pandas/core/internals/blocks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import functools
import inspect
import re
from typing import (
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -1503,16 +1503,33 @@ 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
----------
decimals: int,
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:
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 not self.is_numeric or self.is_bool:
return self.copy(deep=False)
# TODO: round only defined on BaseMaskedArray
Expand Down
2 changes: 0 additions & 2 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -2517,8 +2517,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"
Expand Down
38 changes: 38 additions & 0 deletions pandas/tests/frame/methods/test_round.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
15 changes: 11 additions & 4 deletions pandas/tests/series/methods/test_round.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading