diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index 241e445bf6686..9f59be73e501c 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -1007,7 +1007,7 @@ Numeric - Raises a helpful exception when a non-numeric index is sent to :meth:`interpolate` with methods which require numeric index. (:issue:`21662`) - Bug in :meth:`~pandas.eval` when comparing floats with scalar operators, for example: ``x < -0.1`` (:issue:`25928`) - Fixed bug where casting all-boolean array to integer extension array failed (:issue:`25211`) -- +- Bug in ``divmod`` with a :class:`Series` object containing zeros incorrectly raising ``AttributeError`` (:issue:`26987`) - Conversion diff --git a/doc/source/whatsnew/v0.25.1.rst b/doc/source/whatsnew/v0.25.1.rst index 8690e1974330b..6234bc0f7bd35 100644 --- a/doc/source/whatsnew/v0.25.1.rst +++ b/doc/source/whatsnew/v0.25.1.rst @@ -56,7 +56,6 @@ Timezones Numeric ^^^^^^^ - - - - diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index 3ce6da6891a7f..df2907bf591dd 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -249,7 +249,7 @@ def _gen_fill_zeros(name): """ name = name.strip("__") if "div" in name: - # truediv, floordiv, div, and reversed variants + # truediv, floordiv, and reversed variants fill_value = np.inf elif "mod" in name: # mod, rmod @@ -1668,14 +1668,7 @@ def na_op(x, y): except TypeError: result = masked_arith_op(x, y, op) - if isinstance(result, tuple): - # e.g. divmod - result = tuple( - missing.fill_zeros(r, x, y, op_name, fill_zeros) for r in result - ) - else: - result = missing.fill_zeros(result, x, y, op_name, fill_zeros) - return result + return missing.dispatch_fill_zeros(op, x, y, result, fill_zeros) def wrapper(left, right): if isinstance(right, ABCDataFrame): @@ -2157,8 +2150,7 @@ def na_op(x, y): except TypeError: result = masked_arith_op(x, y, op) - result = missing.fill_zeros(result, x, y, op_name, fill_zeros) - return result + return missing.dispatch_fill_zeros(op, x, y, result, fill_zeros) if op_name in _op_descriptions: # i.e. include "add" but not "__add__" diff --git a/pandas/core/ops/missing.py b/pandas/core/ops/missing.py index 947dfc68ac7c3..4ca1861baf237 100644 --- a/pandas/core/ops/missing.py +++ b/pandas/core/ops/missing.py @@ -27,6 +27,8 @@ from pandas.core.dtypes.common import is_float_dtype, is_integer_dtype, is_scalar +from .roperator import rdivmod + def fill_zeros(result, x, y, name, fill): """ @@ -163,3 +165,24 @@ def dispatch_missing(op, left, right, result): res1 = fill_zeros(result[1], left, right, opstr, np.nan) result = (res0, res1) return result + + +# FIXME: de-duplicate with dispatch_missing +def dispatch_fill_zeros(op, left, right, result, fill_value): + """ + Call fill_zeros with the appropriate fill value depending on the operation, + with special logic for divmod and rdivmod. + """ + if op is divmod: + result = ( + fill_zeros(result[0], left, right, "__floordiv__", np.inf), + fill_zeros(result[1], left, right, "__mod__", np.nan), + ) + elif op is rdivmod: + result = ( + fill_zeros(result[0], left, right, "__rfloordiv__", np.inf), + fill_zeros(result[1], left, right, "__rmod__", np.nan), + ) + else: + result = fill_zeros(result, left, right, op.__name__, fill_value) + return result diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 7dcd0cc820061..f582bf8b13975 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -265,25 +265,11 @@ def test_divmod_zero(self, zero, numeric_idx): # ------------------------------------------------------------------ - @pytest.mark.parametrize( - "dtype2", - [ - np.int64, - np.int32, - np.int16, - np.int8, - np.float64, - np.float32, - np.float16, - np.uint64, - np.uint32, - np.uint16, - np.uint8, - ], - ) @pytest.mark.parametrize("dtype1", [np.int64, np.float64, np.uint64]) - def test_ser_div_ser(self, dtype1, dtype2): + def test_ser_div_ser(self, dtype1, any_real_dtype): # no longer do integer div for any ops, but deal with the 0's + dtype2 = any_real_dtype + first = Series([3, 4, 5, 8], name="first").astype(dtype1) second = Series([0, 0, 0, 3], name="second").astype(dtype2) @@ -299,6 +285,39 @@ def test_ser_div_ser(self, dtype1, dtype2): tm.assert_series_equal(result, expected) assert not result.equals(second / first) + @pytest.mark.parametrize("dtype1", [np.int64, np.float64, np.uint64]) + def test_ser_divmod_zero(self, dtype1, any_real_dtype): + # GH#26987 + dtype2 = any_real_dtype + left = pd.Series([1, 1]).astype(dtype1) + right = pd.Series([0, 2]).astype(dtype2) + + expected = left // right, left % right + result = divmod(left, right) + + tm.assert_series_equal(result[0], expected[0]) + tm.assert_series_equal(result[1], expected[1]) + + # rdivmod case + result = divmod(left.values, right) + tm.assert_series_equal(result[0], expected[0]) + tm.assert_series_equal(result[1], expected[1]) + + def test_ser_divmod_inf(self): + left = pd.Series([np.inf, 1.0]) + right = pd.Series([np.inf, 2.0]) + + expected = left // right, left % right + result = divmod(left, right) + + tm.assert_series_equal(result[0], expected[0]) + tm.assert_series_equal(result[1], expected[1]) + + # rdivmod case + result = divmod(left.values, right) + tm.assert_series_equal(result[0], expected[0]) + tm.assert_series_equal(result[1], expected[1]) + def test_rdiv_zero_compat(self): # GH#8674 zero_array = np.array([0] * 5) @@ -662,7 +681,9 @@ def test_modulo2(self): result2 = p["second"] % p["first"] assert not result.equals(result2) - # GH#9144 + def test_modulo_zero_int(self): + # GH#9144 + with np.errstate(all="ignore"): s = Series([0, 1]) result = s % 0 diff --git a/pandas/tests/sparse/series/test_series.py b/pandas/tests/sparse/series/test_series.py index 8895544958d7a..5619a0a11fb11 100644 --- a/pandas/tests/sparse/series/test_series.py +++ b/pandas/tests/sparse/series/test_series.py @@ -578,6 +578,7 @@ def check(a, b): _check_op(a, b, lambda x, y: operator.floordiv(y, x)) _check_op(a, b, lambda x, y: operator.mul(y, x)) + # FIXME: don't leave commented-out # NaN ** 0 = 1 in C? # _check_op(a, b, operator.pow) # _check_op(a, b, lambda x, y: operator.pow(y, x))