Repository: pandas-dev/pandas

Issue:
AttributeError with divmod(Series, Series) with 0 in the denominator
Strange one. This works,

```python
In [39]: a = pd.Series([1, 1])

In [40]: divmod(a, pd.Series([1, 2]));
```

This raises

```pytb
In [41]: divmod(a, pd.Series([0, 2]));
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-41-04277abe6f5a> in <module>
----> 1 divmod(a, pd.Series([0, 2]));

~/sandbox/pandas/pandas/core/ops.py in wrapper(left, right)
   1714             rvalues = rvalues.values
   1715
-> 1716         result = safe_na_op(lvalues, rvalues)
   1717         return construct_result(left, result,
   1718                                 index=left.index, name=res_name, dtype=None)

~/sandbox/pandas/pandas/core/ops.py in safe_na_op(lvalues, rvalues)
   1660         try:
   1661             with np.errstate(all='ignore'):
-> 1662                 return na_op(lvalues, rvalues)
   1663         except Exception:
   1664             if is_object_dtype(lvalues):

~/sandbox/pandas/pandas/core/ops.py in na_op(x, y)
   1640             result = masked_arith_op(x, y, op)
   1641
-> 1642         result = missing.fill_zeros(result, x, y, op_name, fill_zeros)
   1643         return result
   1644

~/sandbox/pandas/pandas/core/missing.py in fill_zeros(result, x, y, name, fill)
    544             mask = ((y == 0) & ~np.isnan(result)).ravel()
    545
--> 546             shape = result.shape
    547             result = result.astype('float64', copy=False).ravel()
    548

AttributeError: 'tuple' object has no attribute 'shape'
```


Discussion:
This sends us down the bad path: https://github.com/pandas-dev/pandas/blob/dfcd2b2c575d474014908802d9cedf1ac3259635/pandas/core/missing.py#L540

Haven't looked into it. Don't plan to look further right now.
I've got a patch for the immediate issue by just checking for divmod in `fill_zeros` and operating element-wise, but it surfaces another question about what we expect this to operation to return.

```
a = pd.Series([1, 1])
b = pd.Series([0, 2])

result = divmod(a, b)
expected = a // b, a % b
```

result[0] matches expected[0], but result[1] and expected[1] are:

```
>>> res1[1]
0    inf
1    1.0
dtype: float64
>>> expected[1]
0    NaN
1    1.0
dtype: float64
```

see what numpy does for float args 
Looks like #23293 made it so this doesn't raise, but does use incorrect fill value.  Will fix in a variant of #27130.

Resolving patch:
diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py
--- 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
--- 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


Test patch:
diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py
--- 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
--- 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))