Repository: pandas-dev/pandas

Issue:
Regression: divmod(my_series, some_integer) no longer works since version 0.13.
In Pandas version 0.12, you could apply the Python built-in `divmod` function to a `Series` and an integer:

```
Enthought Canopy Python 2.7.6 | 64-bit | (default, Jun  4 2014, 16:42:26) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas as pd
>>> pd.__version__
'0.12.0'
>>> divmod(pd.Series(range(4)), 2)
(0    0
1    0
2    1
3    1
dtype: int64, 0    0
1    1
2    0
3    1
dtype: int64)
>>> 
```

With version >= 0.13, it appears that this usage is no longer supported:

```
Enthought Canopy Python 2.7.6 | 64-bit | (default, Jun  4 2014, 16:42:26) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas as pd
>>> pd.__version__
'0.14.1'
>>> divmod(pd.Series(range(4)), 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for divmod(): 'Series' and 'int'
```

Was this change intentional?

Some context: I was using this to read a climate datafile that had a 4-digit column holding combined month and day values. The original code looked something like: `month, day = divmod(df['MODA'], 100)`, but broke after upgrading to version 0.14.



Discussion:
`__divmod__` was not included when Series was refactored in 0.13.

You can simply do:

`s // 2, s % 2` if you want to get the same results

If you would like to add the method, a PR would be accepted.

I don't think was included as no tests for this (and to be honest never seen it used; its more of a 'functional' way of working with objects)


Resolving patch:
diff --git a/pandas/core/ops.py b/pandas/core/ops.py
--- a/pandas/core/ops.py
+++ b/pandas/core/ops.py
@@ -39,7 +39,8 @@
 
 
 def _create_methods(arith_method, comp_method, bool_method,
-                    use_numexpr, special=False, default_axis='columns'):
+                    use_numexpr, special=False, default_axis='columns',
+                    have_divmod=False):
     # creates actual methods based upon arithmetic, comp and bool method
     # constructors.
 
@@ -127,6 +128,15 @@ def names(x):
                                   names('ror_'), op('|')),
                  rxor=bool_method(lambda x, y: operator.xor(y, x),
                                   names('rxor'), op('^'))))
+    if have_divmod:
+        # divmod doesn't have an op that is supported by numexpr
+        new_methods['divmod'] = arith_method(
+            divmod,
+            names('divmod'),
+            None,
+            default_axis=default_axis,
+            construct_result=_construct_divmod_result,
+        )
 
     new_methods = dict((names(k), v) for k, v in new_methods.items())
     return new_methods
@@ -156,7 +166,7 @@ def add_methods(cls, new_methods, force, select, exclude):
 def add_special_arithmetic_methods(cls, arith_method=None,
                                    comp_method=None, bool_method=None,
                                    use_numexpr=True, force=False, select=None,
-                                   exclude=None):
+                                   exclude=None, have_divmod=False):
     """
     Adds the full suite of special arithmetic methods (``__add__``,
     ``__sub__``, etc.) to the class.
@@ -177,6 +187,9 @@ def add_special_arithmetic_methods(cls, arith_method=None,
         if passed, only sets functions with names in select
     exclude : iterable of strings (optional)
         if passed, will not set functions with names in exclude
+    have_divmod : bool, (optional)
+        should a divmod method be added? this method is special because it
+        returns a tuple of cls instead of a single element of type cls
     """
 
     # in frame, special methods have default_axis = None, comp methods use
@@ -184,7 +197,7 @@ def add_special_arithmetic_methods(cls, arith_method=None,
 
     new_methods = _create_methods(arith_method, comp_method,
                                   bool_method, use_numexpr, default_axis=None,
-                                  special=True)
+                                  special=True, have_divmod=have_divmod)
 
     # inplace operators (I feel like these should get passed an `inplace=True`
     # or just be removed
@@ -618,8 +631,22 @@ def _align_method_SERIES(left, right, align_asobject=False):
     return left, right
 
 
+def _construct_result(left, result, index, name, dtype):
+    return left._constructor(result, index=index, name=name, dtype=dtype)
+
+
+def _construct_divmod_result(left, result, index, name, dtype):
+    """divmod returns a tuple of like indexed series instead of a single series.
+    """
+    constructor = left._constructor
+    return (
+        constructor(result[0], index=index, name=name, dtype=dtype),
+        constructor(result[1], index=index, name=name, dtype=dtype),
+    )
+
+
 def _arith_method_SERIES(op, name, str_rep, fill_zeros=None, default_axis=None,
-                         **eval_kwargs):
+                         construct_result=_construct_result, **eval_kwargs):
     """
     Wrapper function for Series arithmetic operations, to avoid
     code duplication.
@@ -692,8 +719,14 @@ def wrapper(left, right, name=name, na_op=na_op):
                 lvalues = lvalues.values
 
         result = wrap_results(safe_na_op(lvalues, rvalues))
-        return left._constructor(result, index=left.index,
-                                 name=name, dtype=dtype)
+        return construct_result(
+            left,
+            result,
+            index=left.index,
+            name=name,
+            dtype=dtype,
+        )
+
     return wrapper
 
 
@@ -933,6 +966,10 @@ def wrapper(self, other):
                                  'desc': 'Integer division',
                                  'reversed': False,
                                  'reverse': 'rfloordiv'},
+                    'divmod': {'op': 'divmod',
+                               'desc': 'Integer division and modulo',
+                               'reversed': False,
+                               'reverse': None},
 
                     'eq': {'op': '==',
                                  'desc': 'Equal to',
@@ -1033,7 +1070,8 @@ def flex_wrapper(self, other, level=None, fill_value=None, axis=0):
 
 series_special_funcs = dict(arith_method=_arith_method_SERIES,
                             comp_method=_comp_method_SERIES,
-                            bool_method=_bool_method_SERIES)
+                            bool_method=_bool_method_SERIES,
+                            have_divmod=True)
 
 _arith_doc_FRAME = """
 Binary operator %s with support to substitute a fill_value for missing data in
diff --git a/pandas/indexes/base.py b/pandas/indexes/base.py
--- a/pandas/indexes/base.py
+++ b/pandas/indexes/base.py
@@ -3426,7 +3426,7 @@ def _validate_for_numeric_binop(self, other, op, opstr):
     def _add_numeric_methods_binary(cls):
         """ add in numeric methods """
 
-        def _make_evaluate_binop(op, opstr, reversed=False):
+        def _make_evaluate_binop(op, opstr, reversed=False, constructor=Index):
             def _evaluate_numeric_binop(self, other):
 
                 from pandas.tseries.offsets import DateOffset
@@ -3448,7 +3448,7 @@ def _evaluate_numeric_binop(self, other):
                 attrs = self._maybe_update_attributes(attrs)
                 with np.errstate(all='ignore'):
                     result = op(values, other)
-                return Index(result, **attrs)
+                return constructor(result, **attrs)
 
             return _evaluate_numeric_binop
 
@@ -3478,6 +3478,15 @@ def _evaluate_numeric_binop(self, other):
             cls.__rdiv__ = _make_evaluate_binop(
                 operator.div, '__div__', reversed=True)
 
+        cls.__divmod__ = _make_evaluate_binop(
+            divmod,
+            '__divmod__',
+            constructor=lambda result, **attrs: (
+                Index(result[0], **attrs),
+                Index(result[1], **attrs),
+            ),
+        )
+
     @classmethod
     def _add_numeric_methods_unary(cls):
         """ add in numeric unary methods """


Test patch:
diff --git a/pandas/tests/indexes/test_numeric.py b/pandas/tests/indexes/test_numeric.py
--- a/pandas/tests/indexes/test_numeric.py
+++ b/pandas/tests/indexes/test_numeric.py
@@ -73,6 +73,30 @@ def test_numeric_compat(self):
         self.assertRaises(ValueError, lambda: idx * idx[0:3])
         self.assertRaises(ValueError, lambda: idx * np.array([1, 2]))
 
+        result = divmod(idx, 2)
+        with np.errstate(all='ignore'):
+            div, mod = divmod(idx.values, 2)
+            expected = Index(div), Index(mod)
+        for r, e in zip(result, expected):
+            tm.assert_index_equal(r, e)
+
+        result = divmod(idx, np.full_like(idx.values, 2))
+        with np.errstate(all='ignore'):
+            div, mod = divmod(idx.values, np.full_like(idx.values, 2))
+            expected = Index(div), Index(mod)
+        for r, e in zip(result, expected):
+            tm.assert_index_equal(r, e)
+
+        result = divmod(idx, Series(np.full_like(idx.values, 2)))
+        with np.errstate(all='ignore'):
+            div, mod = divmod(
+                idx.values,
+                np.full_like(idx.values, 2),
+            )
+            expected = Index(div), Index(mod)
+        for r, e in zip(result, expected):
+            tm.assert_index_equal(r, e)
+
     def test_explicit_conversions(self):
 
         # GH 8608
diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py
--- a/pandas/tests/series/test_operators.py
+++ b/pandas/tests/series/test_operators.py
@@ -1,6 +1,7 @@
 # coding=utf-8
 # pylint: disable-msg=E1101,W0612
 
+from collections import Iterable
 from datetime import datetime, timedelta
 import operator
 from itertools import product, starmap
@@ -19,7 +20,7 @@
 from pandas.compat import range, zip
 from pandas import compat
 from pandas.util.testing import (assert_series_equal, assert_almost_equal,
-                                 assert_frame_equal)
+                                 assert_frame_equal, assert_index_equal)
 import pandas.util.testing as tm
 
 from .common import TestData
@@ -185,6 +186,34 @@ def check_comparators(series, other, check_dtype=True):
         check_comparators(self.ts, 5)
         check_comparators(self.ts, self.ts + 1, check_dtype=False)
 
+    def test_divmod(self):
+        def check(series, other):
+            results = divmod(series, other)
+            if isinstance(other, Iterable) and len(series) != len(other):
+                # if the lengths don't match, this is the test where we use
+                # `self.ts[::2]`. Pad every other value in `other_np` with nan.
+                other_np = []
+                for n in other:
+                    other_np.append(n)
+                    other_np.append(np.nan)
+            else:
+                other_np = other
+            other_np = np.asarray(other_np)
+            with np.errstate(all='ignore'):
+                expecteds = divmod(series.values, np.asarray(other_np))
+
+            for result, expected in zip(results, expecteds):
+                # check the values, name, and index separatly
+                assert_almost_equal(np.asarray(result), expected)
+
+                self.assertEqual(result.name, series.name)
+                assert_index_equal(result.index, series.index)
+
+        check(self.ts, self.ts * 2)
+        check(self.ts, self.ts * 0)
+        check(self.ts, self.ts[::2])
+        check(self.ts, 5)
+
     def test_operators_empty_int_corner(self):
         s1 = Series([], [], dtype=np.int32)
         s2 = Series({'x': 0.})