Skip to content

Commit a7469cf

Browse files
lllllllllljreback
authored andcommitted
ENH: Add divmod to series and index. (#14208)
1 parent 2a5c747 commit a7469cf

File tree

6 files changed

+139
-10
lines changed

6 files changed

+139
-10
lines changed

doc/source/basics.rst

+26
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,32 @@ And similarly for ``axis="items"`` and ``axis="minor"``.
188188
match the broadcasting behavior of Panel. Though it would require a
189189
transition period so users can change their code...
190190

191+
Series and Index also support the :func:`divmod` builtin. This function takes
192+
the floor division and modulo operation at the same time returning a two-tuple
193+
of the same type as the left hand side. For example:
194+
195+
.. ipython:: python
196+
197+
s = pd.Series(np.arange(10))
198+
s
199+
div, rem = divmod(s, 3)
200+
div
201+
rem
202+
203+
idx = pd.Index(np.arange(10))
204+
idx
205+
div, rem = divmod(idx, 3)
206+
div
207+
rem
208+
209+
We can also do elementwise :func:`divmod`:
210+
211+
.. ipython:: python
212+
213+
div, rem = divmod(s, [2, 2, 3, 3, 4, 4, 5, 5, 6, 6])
214+
div
215+
rem
216+
191217
Missing data / operations with fill values
192218
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
193219

doc/source/whatsnew/v0.19.0.txt

+3
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,9 @@ Other API Changes
13301330
- ``pd.read_csv()`` in the C engine will now issue a ``ParserWarning`` or raise a ``ValueError`` when ``sep`` encoded is more than one character long (:issue:`14065`)
13311331
- ``DataFrame.values`` will now return ``float64`` with a ``DataFrame`` of mixed ``int64`` and ``uint64`` dtypes, conforming to ``np.find_common_type`` (:issue:`10364`, :issue:`13917`)
13321332
- ``pd.read_stata()`` can now handle some format 111 files, which are produced by SAS when generating Stata dta files (:issue:`11526`)
1333+
- ``Series`` and ``Index`` now support ``divmod`` which will return a tuple of
1334+
series or indices. This behaves like a standard binary operator with regards
1335+
to broadcasting rules (:issue:`14208`).
13331336

13341337
.. _whatsnew_0190.deprecations:
13351338

pandas/core/ops.py

+45-7
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939

4040

4141
def _create_methods(arith_method, comp_method, bool_method,
42-
use_numexpr, special=False, default_axis='columns'):
42+
use_numexpr, special=False, default_axis='columns',
43+
have_divmod=False):
4344
# creates actual methods based upon arithmetic, comp and bool method
4445
# constructors.
4546

@@ -127,6 +128,15 @@ def names(x):
127128
names('ror_'), op('|')),
128129
rxor=bool_method(lambda x, y: operator.xor(y, x),
129130
names('rxor'), op('^'))))
131+
if have_divmod:
132+
# divmod doesn't have an op that is supported by numexpr
133+
new_methods['divmod'] = arith_method(
134+
divmod,
135+
names('divmod'),
136+
None,
137+
default_axis=default_axis,
138+
construct_result=_construct_divmod_result,
139+
)
130140

131141
new_methods = dict((names(k), v) for k, v in new_methods.items())
132142
return new_methods
@@ -156,7 +166,7 @@ def add_methods(cls, new_methods, force, select, exclude):
156166
def add_special_arithmetic_methods(cls, arith_method=None,
157167
comp_method=None, bool_method=None,
158168
use_numexpr=True, force=False, select=None,
159-
exclude=None):
169+
exclude=None, have_divmod=False):
160170
"""
161171
Adds the full suite of special arithmetic methods (``__add__``,
162172
``__sub__``, etc.) to the class.
@@ -177,14 +187,17 @@ def add_special_arithmetic_methods(cls, arith_method=None,
177187
if passed, only sets functions with names in select
178188
exclude : iterable of strings (optional)
179189
if passed, will not set functions with names in exclude
190+
have_divmod : bool, (optional)
191+
should a divmod method be added? this method is special because it
192+
returns a tuple of cls instead of a single element of type cls
180193
"""
181194

182195
# in frame, special methods have default_axis = None, comp methods use
183196
# 'columns'
184197

185198
new_methods = _create_methods(arith_method, comp_method,
186199
bool_method, use_numexpr, default_axis=None,
187-
special=True)
200+
special=True, have_divmod=have_divmod)
188201

189202
# inplace operators (I feel like these should get passed an `inplace=True`
190203
# or just be removed
@@ -618,8 +631,22 @@ def _align_method_SERIES(left, right, align_asobject=False):
618631
return left, right
619632

620633

634+
def _construct_result(left, result, index, name, dtype):
635+
return left._constructor(result, index=index, name=name, dtype=dtype)
636+
637+
638+
def _construct_divmod_result(left, result, index, name, dtype):
639+
"""divmod returns a tuple of like indexed series instead of a single series.
640+
"""
641+
constructor = left._constructor
642+
return (
643+
constructor(result[0], index=index, name=name, dtype=dtype),
644+
constructor(result[1], index=index, name=name, dtype=dtype),
645+
)
646+
647+
621648
def _arith_method_SERIES(op, name, str_rep, fill_zeros=None, default_axis=None,
622-
**eval_kwargs):
649+
construct_result=_construct_result, **eval_kwargs):
623650
"""
624651
Wrapper function for Series arithmetic operations, to avoid
625652
code duplication.
@@ -692,8 +719,14 @@ def wrapper(left, right, name=name, na_op=na_op):
692719
lvalues = lvalues.values
693720

694721
result = wrap_results(safe_na_op(lvalues, rvalues))
695-
return left._constructor(result, index=left.index,
696-
name=name, dtype=dtype)
722+
return construct_result(
723+
left,
724+
result,
725+
index=left.index,
726+
name=name,
727+
dtype=dtype,
728+
)
729+
697730
return wrapper
698731

699732

@@ -933,6 +966,10 @@ def wrapper(self, other):
933966
'desc': 'Integer division',
934967
'reversed': False,
935968
'reverse': 'rfloordiv'},
969+
'divmod': {'op': 'divmod',
970+
'desc': 'Integer division and modulo',
971+
'reversed': False,
972+
'reverse': None},
936973

937974
'eq': {'op': '==',
938975
'desc': 'Equal to',
@@ -1033,7 +1070,8 @@ def flex_wrapper(self, other, level=None, fill_value=None, axis=0):
10331070

10341071
series_special_funcs = dict(arith_method=_arith_method_SERIES,
10351072
comp_method=_comp_method_SERIES,
1036-
bool_method=_bool_method_SERIES)
1073+
bool_method=_bool_method_SERIES,
1074+
have_divmod=True)
10371075

10381076
_arith_doc_FRAME = """
10391077
Binary operator %s with support to substitute a fill_value for missing data in

pandas/indexes/base.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -3426,7 +3426,7 @@ def _validate_for_numeric_binop(self, other, op, opstr):
34263426
def _add_numeric_methods_binary(cls):
34273427
""" add in numeric methods """
34283428

3429-
def _make_evaluate_binop(op, opstr, reversed=False):
3429+
def _make_evaluate_binop(op, opstr, reversed=False, constructor=Index):
34303430
def _evaluate_numeric_binop(self, other):
34313431

34323432
from pandas.tseries.offsets import DateOffset
@@ -3448,7 +3448,7 @@ def _evaluate_numeric_binop(self, other):
34483448
attrs = self._maybe_update_attributes(attrs)
34493449
with np.errstate(all='ignore'):
34503450
result = op(values, other)
3451-
return Index(result, **attrs)
3451+
return constructor(result, **attrs)
34523452

34533453
return _evaluate_numeric_binop
34543454

@@ -3478,6 +3478,15 @@ def _evaluate_numeric_binop(self, other):
34783478
cls.__rdiv__ = _make_evaluate_binop(
34793479
operator.div, '__div__', reversed=True)
34803480

3481+
cls.__divmod__ = _make_evaluate_binop(
3482+
divmod,
3483+
'__divmod__',
3484+
constructor=lambda result, **attrs: (
3485+
Index(result[0], **attrs),
3486+
Index(result[1], **attrs),
3487+
),
3488+
)
3489+
34813490
@classmethod
34823491
def _add_numeric_methods_unary(cls):
34833492
""" add in numeric unary methods """

pandas/tests/indexes/test_numeric.py

+24
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,30 @@ def test_numeric_compat(self):
7373
self.assertRaises(ValueError, lambda: idx * idx[0:3])
7474
self.assertRaises(ValueError, lambda: idx * np.array([1, 2]))
7575

76+
result = divmod(idx, 2)
77+
with np.errstate(all='ignore'):
78+
div, mod = divmod(idx.values, 2)
79+
expected = Index(div), Index(mod)
80+
for r, e in zip(result, expected):
81+
tm.assert_index_equal(r, e)
82+
83+
result = divmod(idx, np.full_like(idx.values, 2))
84+
with np.errstate(all='ignore'):
85+
div, mod = divmod(idx.values, np.full_like(idx.values, 2))
86+
expected = Index(div), Index(mod)
87+
for r, e in zip(result, expected):
88+
tm.assert_index_equal(r, e)
89+
90+
result = divmod(idx, Series(np.full_like(idx.values, 2)))
91+
with np.errstate(all='ignore'):
92+
div, mod = divmod(
93+
idx.values,
94+
np.full_like(idx.values, 2),
95+
)
96+
expected = Index(div), Index(mod)
97+
for r, e in zip(result, expected):
98+
tm.assert_index_equal(r, e)
99+
76100
def test_explicit_conversions(self):
77101

78102
# GH 8608

pandas/tests/series/test_operators.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# coding=utf-8
22
# pylint: disable-msg=E1101,W0612
33

4+
from collections import Iterable
45
from datetime import datetime, timedelta
56
import operator
67
from itertools import product, starmap
@@ -19,7 +20,7 @@
1920
from pandas.compat import range, zip
2021
from pandas import compat
2122
from pandas.util.testing import (assert_series_equal, assert_almost_equal,
22-
assert_frame_equal)
23+
assert_frame_equal, assert_index_equal)
2324
import pandas.util.testing as tm
2425

2526
from .common import TestData
@@ -185,6 +186,34 @@ def check_comparators(series, other, check_dtype=True):
185186
check_comparators(self.ts, 5)
186187
check_comparators(self.ts, self.ts + 1, check_dtype=False)
187188

189+
def test_divmod(self):
190+
def check(series, other):
191+
results = divmod(series, other)
192+
if isinstance(other, Iterable) and len(series) != len(other):
193+
# if the lengths don't match, this is the test where we use
194+
# `self.ts[::2]`. Pad every other value in `other_np` with nan.
195+
other_np = []
196+
for n in other:
197+
other_np.append(n)
198+
other_np.append(np.nan)
199+
else:
200+
other_np = other
201+
other_np = np.asarray(other_np)
202+
with np.errstate(all='ignore'):
203+
expecteds = divmod(series.values, np.asarray(other_np))
204+
205+
for result, expected in zip(results, expecteds):
206+
# check the values, name, and index separatly
207+
assert_almost_equal(np.asarray(result), expected)
208+
209+
self.assertEqual(result.name, series.name)
210+
assert_index_equal(result.index, series.index)
211+
212+
check(self.ts, self.ts * 2)
213+
check(self.ts, self.ts * 0)
214+
check(self.ts, self.ts[::2])
215+
check(self.ts, 5)
216+
188217
def test_operators_empty_int_corner(self):
189218
s1 = Series([], [], dtype=np.int32)
190219
s2 = Series({'x': 0.})

0 commit comments

Comments
 (0)