From 4e617e64598f525da288f2c7457306b710e31d6f Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Thu, 4 Jul 2019 18:20:27 -0700 Subject: [PATCH 1/5] Fix fill value, closes #26987 --- pandas/core/missing.py | 25 ++++++++++- pandas/core/ops.py | 11 +---- pandas/tests/arithmetic/test_numeric.py | 52 ++++++++++++++++++++++- pandas/tests/sparse/series/test_series.py | 1 + 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/pandas/core/missing.py b/pandas/core/missing.py index ad4b5e4523806..bdd26b8360fb9 100644 --- a/pandas/core/missing.py +++ b/pandas/core/missing.py @@ -702,7 +702,7 @@ def dispatch_missing(op, left, right, result): result : ndarray """ opstr = "__{opname}__".format(opname=op.__name__).replace("____", "__") - if op in [operator.truediv, operator.floordiv, getattr(operator, "div", None)]: + if op in [operator.truediv, operator.floordiv]: result = mask_zero_div_zero(left, right, result) elif op is operator.mod: result = fill_zeros(result, left, right, opstr, np.nan) @@ -713,6 +713,29 @@ def dispatch_missing(op, left, right, result): 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. + """ + from pandas.core.ops import 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 + + def _interp_limit(invalid, fw_limit, bw_limit): """ Get indexers of values that won't be filled diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 5c58a1433ba3c..c7cc6fc452c47 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -295,7 +295,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 @@ -1718,14 +1718,7 @@ def na_op(x, y): return libalgos.arrmap_object(x, lambda val: op(val, y)) raise - 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): diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 7dcd0cc820061..f5dd5b0638362 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -299,6 +299,54 @@ def test_ser_div_ser(self, dtype1, dtype2): tm.assert_series_equal(result, expected) assert not result.equals(second / first) + @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_divmod_zero(self, dtype1, dtype2): + # GH#26987 + 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 +710,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)) From e64bdd0826920894b851fa69a5bc2cbe16dd41d8 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Thu, 4 Jul 2019 18:21:09 -0700 Subject: [PATCH 2/5] whatsnew --- doc/source/whatsnew/v0.25.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.25.1.rst b/doc/source/whatsnew/v0.25.1.rst index 8690e1974330b..6e2ac4490a543 100644 --- a/doc/source/whatsnew/v0.25.1.rst +++ b/doc/source/whatsnew/v0.25.1.rst @@ -56,7 +56,7 @@ Timezones Numeric ^^^^^^^ - +- Bug in ``divmod`` with a :class:`Series` object containing zeros incorrectly raising ``AttributeError`` (:issue:`26987`) - - - From 4b8f4d1f728d53d18f9cd06d40c4a80418c4364a Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Fri, 5 Jul 2019 07:58:21 -0700 Subject: [PATCH 3/5] share code with frame op --- pandas/core/ops/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index 3ffaedead7e37..75c7ee2760fb0 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -2154,8 +2154,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__" From e53fb4902a0d67280f52156f1faef7c9e1b26fe6 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Fri, 5 Jul 2019 08:33:12 -0700 Subject: [PATCH 4/5] move whatsnew to 0.25.0 --- doc/source/whatsnew/v0.25.0.rst | 2 +- doc/source/whatsnew/v0.25.1.rst | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index 101addfa097f8..7ee8d2c7864f4 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 6e2ac4490a543..6234bc0f7bd35 100644 --- a/doc/source/whatsnew/v0.25.1.rst +++ b/doc/source/whatsnew/v0.25.1.rst @@ -56,7 +56,6 @@ Timezones Numeric ^^^^^^^ -- Bug in ``divmod`` with a :class:`Series` object containing zeros incorrectly raising ``AttributeError`` (:issue:`26987`) - - - From 7c645996d7e2b66152a827e5384e26e64b4d1b91 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Fri, 5 Jul 2019 08:34:36 -0700 Subject: [PATCH 5/5] use fixture --- pandas/tests/arithmetic/test_numeric.py | 39 ++++--------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index f5dd5b0638362..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,25 +285,10 @@ def test_ser_div_ser(self, dtype1, dtype2): tm.assert_series_equal(result, expected) assert not result.equals(second / first) - @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_divmod_zero(self, dtype1, dtype2): + 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)