From 1df9bf7e156bf2d05b7b1eda13a7b2520161ffa8 Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Thu, 31 Dec 2020 15:47:55 -0500 Subject: [PATCH 1/9] add test for nullable float --- pandas/tests/series/methods/test_round.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pandas/tests/series/methods/test_round.py b/pandas/tests/series/methods/test_round.py index 88d5c428712dc..0027807d82eb8 100644 --- a/pandas/tests/series/methods/test_round.py +++ b/pandas/tests/series/methods/test_round.py @@ -35,14 +35,15 @@ def test_round_numpy_with_nan(self): expected = Series([2.0, np.nan, 0.0]) tm.assert_series_equal(result, expected) - def test_round_builtin(self): - ser = Series([1.123, 2.123, 3.123], index=range(3)) + @pytest.mark.parametrize("dtype", ("float", "Float64")) + def test_round_builtin(self, dtype): + ser = Series([1.123, 2.123, 3.123], index=range(3), dtype=dtype) result = round(ser) - expected_rounded0 = Series([1.0, 2.0, 3.0], index=range(3)) + expected_rounded0 = Series([1.0, 2.0, 3.0], index=range(3), dtype=dtype) tm.assert_series_equal(result, expected_rounded0) decimals = 2 - expected_rounded = Series([1.12, 2.12, 3.12], index=range(3)) + expected_rounded = Series([1.12, 2.12, 3.12], index=range(3), dtype=dtype) result = round(ser, decimals) tm.assert_series_equal(result, expected_rounded) From 192ebadd2ebba9715ac18dc9399453ce40d7cadf Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Thu, 31 Dec 2020 15:48:19 -0500 Subject: [PATCH 2/9] implement round method for FloatingArray --- pandas/core/arrays/floating.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pandas/core/arrays/floating.py b/pandas/core/arrays/floating.py index 1ac23d7893fbf..3492f2c81fb47 100644 --- a/pandas/core/arrays/floating.py +++ b/pandas/core/arrays/floating.py @@ -410,6 +410,14 @@ def max(self, *, skipna=True, **kwargs): nv.validate_max((), kwargs) return super()._reduce("max", skipna=skipna) + def round(self, decimals=0): + values = self._data[~self._mask] + values = np.round(values, decimals) + + data = np.zeros(self._data.shape) + data[~self._mask] = values + return FloatingArray(data, self._mask) + def _maybe_mask_result(self, result, mask, other, op_name: str): """ Parameters From 640302647924c8a656d1c8be0c3645e1ace54b21 Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Thu, 31 Dec 2020 16:34:24 -0500 Subject: [PATCH 3/9] use pytest fixture --- pandas/tests/series/methods/test_round.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pandas/tests/series/methods/test_round.py b/pandas/tests/series/methods/test_round.py index 0027807d82eb8..0f0a4e9adaa80 100644 --- a/pandas/tests/series/methods/test_round.py +++ b/pandas/tests/series/methods/test_round.py @@ -35,16 +35,23 @@ def test_round_numpy_with_nan(self): expected = Series([2.0, np.nan, 0.0]) tm.assert_series_equal(result, expected) - @pytest.mark.parametrize("dtype", ("float", "Float64")) - def test_round_builtin(self, dtype): - ser = Series([1.123, 2.123, 3.123], index=range(3), dtype=dtype) - result = round(ser) - expected_rounded0 = Series([1.0, 2.0, 3.0], index=range(3), dtype=dtype) + def test_round_builtin(self, any_float_allowed_nullable_dtype): + ser = Series( + [1.123, 2.123, 3.123], + index=range(3), + dtype=any_float_allowed_nullable_dtype, + ) + result = round(ser).astype(any_float_allowed_nullable_dtype) + expected_rounded0 = Series( + [1.0, 2.0, 3.0], index=range(3), dtype=any_float_allowed_nullable_dtype + ) tm.assert_series_equal(result, expected_rounded0) decimals = 2 - expected_rounded = Series([1.12, 2.12, 3.12], index=range(3), dtype=dtype) - result = round(ser, decimals) + expected_rounded = Series( + [1.12, 2.12, 3.12], index=range(3), dtype=any_float_allowed_nullable_dtype + ) + result = round(ser, decimals).astype(any_float_allowed_nullable_dtype) tm.assert_series_equal(result, expected_rounded) @pytest.mark.parametrize("method", ["round", "floor", "ceil"]) From 253d27915ec16a68819a777e7cee5ec7cfbd2c77 Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Mon, 4 Jan 2021 17:39:13 -0500 Subject: [PATCH 4/9] review: rewriting using _apply --- pandas/core/arrays/floating.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pandas/core/arrays/floating.py b/pandas/core/arrays/floating.py index 3492f2c81fb47..f45f636341a46 100644 --- a/pandas/core/arrays/floating.py +++ b/pandas/core/arrays/floating.py @@ -410,14 +410,17 @@ def max(self, *, skipna=True, **kwargs): nv.validate_max((), kwargs) return super()._reduce("max", skipna=skipna) - def round(self, decimals=0): + def _apply(self, func, **kwargs): values = self._data[~self._mask] - values = np.round(values, decimals) + values = np.round(values, **kwargs) data = np.zeros(self._data.shape) data[~self._mask] = values return FloatingArray(data, self._mask) + def round(self, decimals=0): + return self._apply(np.round, decimals=decimals) + def _maybe_mask_result(self, result, mask, other, op_name: str): """ Parameters From fe195970ef21ee3bd0118c2aef68d358d634e066 Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Mon, 4 Jan 2021 17:42:46 -0500 Subject: [PATCH 5/9] remove explicit FloatingArray reference from _apply --- pandas/core/arrays/floating.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/arrays/floating.py b/pandas/core/arrays/floating.py index f45f636341a46..9ec7ace371e8c 100644 --- a/pandas/core/arrays/floating.py +++ b/pandas/core/arrays/floating.py @@ -416,7 +416,7 @@ def _apply(self, func, **kwargs): data = np.zeros(self._data.shape) data[~self._mask] = values - return FloatingArray(data, self._mask) + return type(self)(data, self._mask) def round(self, decimals=0): return self._apply(np.round, decimals=decimals) From b557a5048f79842b3ad9873e5b2c7842a1f9b03e Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Mon, 4 Jan 2021 17:45:14 -0500 Subject: [PATCH 6/9] move _apply, round to NumericArray --- pandas/core/arrays/floating.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pandas/core/arrays/floating.py b/pandas/core/arrays/floating.py index 9ec7ace371e8c..1ac23d7893fbf 100644 --- a/pandas/core/arrays/floating.py +++ b/pandas/core/arrays/floating.py @@ -410,17 +410,6 @@ def max(self, *, skipna=True, **kwargs): nv.validate_max((), kwargs) return super()._reduce("max", skipna=skipna) - def _apply(self, func, **kwargs): - values = self._data[~self._mask] - values = np.round(values, **kwargs) - - data = np.zeros(self._data.shape) - data[~self._mask] = values - return type(self)(data, self._mask) - - def round(self, decimals=0): - return self._apply(np.round, decimals=decimals) - def _maybe_mask_result(self, result, mask, other, op_name: str): """ Parameters From 85e6d4fdc9323b2ecc0a0ff167997216e8f97a4c Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Mon, 4 Jan 2021 17:46:38 -0500 Subject: [PATCH 7/9] move _apply, round to NumericArray --- pandas/core/arrays/numeric.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pandas/core/arrays/numeric.py b/pandas/core/arrays/numeric.py index 3c115ec42f6ec..06ba5c94f4519 100644 --- a/pandas/core/arrays/numeric.py +++ b/pandas/core/arrays/numeric.py @@ -130,3 +130,14 @@ def _arith_method(self, other, op): ) return self._maybe_mask_result(result, mask, other, op_name) + + def _apply(self, func, **kwargs): + values = self._data[~self._mask] + values = np.round(values, **kwargs) + + data = np.zeros(self._data.shape) + data[~self._mask] = values + return type(self)(data, self._mask) + + def round(self, decimals=0): + return self._apply(np.round, decimals=decimals) From f79c0cdbf20704afc1dc1d9e5cb632f22ced24bd Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Mon, 4 Jan 2021 18:18:30 -0500 Subject: [PATCH 8/9] typing, consistency with Series.round --- pandas/core/arrays/numeric.py | 38 +++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/pandas/core/arrays/numeric.py b/pandas/core/arrays/numeric.py index 06ba5c94f4519..0325b008d88ee 100644 --- a/pandas/core/arrays/numeric.py +++ b/pandas/core/arrays/numeric.py @@ -1,10 +1,12 @@ import datetime -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Callable, Union import numpy as np from pandas._libs import Timedelta, missing as libmissing +from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError +from pandas.util._decorators import doc from pandas.core.dtypes.common import ( is_float, @@ -56,6 +58,32 @@ def __from_arrow__( return array_class._concat_same_type(results) +_round_doc = """ +Round each value in NumericArray a to the given number of decimals. + +Parameters +---------- +decimals : int, default 0 + Number of decimal places to round to. If decimals is negative, + it specifies the number of positions to the left of the decimal point. +*args, **kwargs + Additional arguments and keywords have no effect but might be + accepted for compatibility with NumPy. + +Returns +------- +NumericArray + Rounded values of the NumericArray. + +See Also +-------- +numpy.around : Round values of an np.array. +DataFrame.round : Round values of a DataFrame. +Series.round : Round values of a Series. + +""" + + class NumericArray(BaseMaskedArray): """ Base class for IntegerArray and FloatingArray. @@ -131,7 +159,7 @@ def _arith_method(self, other, op): return self._maybe_mask_result(result, mask, other, op_name) - def _apply(self, func, **kwargs): + def _apply(self, func: Callable, **kwargs) -> "NumericArray": values = self._data[~self._mask] values = np.round(values, **kwargs) @@ -139,5 +167,7 @@ def _apply(self, func, **kwargs): data[~self._mask] = values return type(self)(data, self._mask) - def round(self, decimals=0): - return self._apply(np.round, decimals=decimals) + @doc(_round_doc) + def round(self, decimals: int = 0, *args, **kwargs) -> "NumericArray": + nv.validate_round(args, kwargs) + return self._apply(np.round, decimals=decimals, **kwargs) From d1cd6d871e0991ec9b0c5e76f618292971157d09 Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Mon, 4 Jan 2021 18:22:24 -0500 Subject: [PATCH 9/9] whatsnew --- doc/source/whatsnew/v1.3.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index d6fd017ff0897..ffacd82b35056 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -54,6 +54,7 @@ Other enhancements - Add support for dict-like names in :class:`MultiIndex.set_names` and :class:`MultiIndex.rename` (:issue:`20421`) - :func:`pandas.read_excel` can now auto detect .xlsb files (:issue:`35416`) - :meth:`.Rolling.sum`, :meth:`.Expanding.sum`, :meth:`.Rolling.mean`, :meth:`.Expanding.mean`, :meth:`.Rolling.median`, :meth:`.Expanding.median`, :meth:`.Rolling.max`, :meth:`.Expanding.max`, :meth:`.Rolling.min`, and :meth:`.Expanding.min` now support ``Numba`` execution with the ``engine`` keyword (:issue:`38895`) +- Added :meth:`NumericArray.round` (:issue:`38844`) .. ---------------------------------------------------------------------------