Skip to content

Commit 969f342

Browse files
committed
fix and test index division by zero
1 parent b6acf5e commit 969f342

File tree

5 files changed

+150
-18
lines changed

5 files changed

+150
-18
lines changed

pandas/core/indexes/base.py

+33
Original file line numberDiff line numberDiff line change
@@ -4044,6 +4044,8 @@ def _evaluate_numeric_binop(self, other):
40444044
attrs = self._maybe_update_attributes(attrs)
40454045
with np.errstate(all='ignore'):
40464046
result = op(values, other)
4047+
4048+
result = dispatch_missing(op, values, other, result)
40474049
return constructor(result, **attrs)
40484050

40494051
return _evaluate_numeric_binop
@@ -4167,6 +4169,37 @@ def invalid_op(self, other=None):
41674169
Index._add_comparison_methods()
41684170

41694171

4172+
def dispatch_missing(op, left, right, result):
4173+
"""
4174+
Fill nulls caused by division by zero, casting to a diffferent dtype
4175+
if necessary.
4176+
4177+
Parameters
4178+
----------
4179+
op : function (operator.add, operator.div, ...)
4180+
left : object, usually Index
4181+
right : object
4182+
result : ndarray
4183+
4184+
Returns
4185+
-------
4186+
result : ndarray
4187+
"""
4188+
opstr = '__{opname}__'.format(opname=op.__name__).replace('____', '__')
4189+
if op in [operator.div, operator.truediv, operator.floordiv]:
4190+
result = missing.mask_zero_div_zero(left, right, result)
4191+
elif op is operator.mod:
4192+
result = missing.fill_zeros(result, left, right,
4193+
opstr, np.nan)
4194+
elif op is divmod:
4195+
res0 = missing.fill_zeros(result[0], left, right,
4196+
opstr, np.nan)
4197+
res1 = missing.fill_zeros(result[1], left, right,
4198+
opstr, np.nan)
4199+
result = (res0, res1)
4200+
return result
4201+
4202+
41704203
def _ensure_index_from_sequences(sequences, names=None):
41714204
"""Construct an index from sequences of data.
41724205

pandas/core/indexes/range.py

+13-18
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ def __getitem__(self, key):
550550
return super_getitem(key)
551551

552552
def __floordiv__(self, other):
553-
if is_integer(other):
553+
if is_integer(other) and other != 0:
554554
if (len(self) == 0 or
555555
self._start % other == 0 and
556556
self._step % other == 0):
@@ -592,26 +592,27 @@ def _evaluate_numeric_binop(self, other):
592592
attrs = self._get_attributes_dict()
593593
attrs = self._maybe_update_attributes(attrs)
594594

595+
left, right = self, other
595596
if reversed:
596-
self, other = other, self
597+
left, right = right, left
597598

598599
try:
599600
# apply if we have an override
600601
if step:
601602
with np.errstate(all='ignore'):
602-
rstep = step(self._step, other)
603+
rstep = step(left._step, right)
603604

604605
# we don't have a representable op
605606
# so return a base index
606607
if not is_integer(rstep) or not rstep:
607608
raise ValueError
608609

609610
else:
610-
rstep = self._step
611+
rstep = left._step
611612

612613
with np.errstate(all='ignore'):
613-
rstart = op(self._start, other)
614-
rstop = op(self._stop, other)
614+
rstart = op(left._start, right)
615+
rstop = op(left._stop, right)
615616

616617
result = RangeIndex(rstart,
617618
rstop,
@@ -627,18 +628,12 @@ def _evaluate_numeric_binop(self, other):
627628

628629
return result
629630

630-
except (ValueError, TypeError, AttributeError):
631-
pass
632-
633-
# convert to Int64Index ops
634-
if isinstance(self, RangeIndex):
635-
self = self.values
636-
if isinstance(other, RangeIndex):
637-
other = other.values
638-
639-
with np.errstate(all='ignore'):
640-
results = op(self, other)
641-
return Index(results, **attrs)
631+
except (ValueError, TypeError, AttributeError,
632+
ZeroDivisionError):
633+
# Defer to Int64Index implementation
634+
if reversed:
635+
return op(other, self._int64index)
636+
return op(self._int64index, other)
642637

643638
return _evaluate_numeric_binop
644639

pandas/core/missing.py

+38
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,44 @@ def fill_zeros(result, x, y, name, fill):
645645
return result
646646

647647

648+
def mask_zero_div_zero(x, y, result):
649+
"""
650+
Set results of 0 / 0 or 0 // 0 to np.nan, regardless of the dtypes
651+
of the numerator or the denominator
652+
653+
Parameters
654+
----------
655+
x : ndarray
656+
y : ndarray
657+
result : ndarray
658+
659+
Returns
660+
-------
661+
filled_result : ndarray
662+
"""
663+
if is_scalar(y):
664+
y = np.array(y)
665+
666+
zmask = y == 0
667+
if zmask.any():
668+
shape = result.shape
669+
670+
nan_mask = (zmask & (x == 0)).ravel()
671+
neginf_mask = (zmask & (x < 0)).ravel()
672+
posinf_mask = (zmask & (x > 0)).ravel()
673+
674+
if nan_mask.any() or neginf_mask.any() or posinf_mask.any():
675+
result = result.astype('float64', copy=False).ravel()
676+
677+
np.putmask(result, nan_mask, np.nan)
678+
np.putmask(result, posinf_mask, np.inf)
679+
np.putmask(result, neginf_mask, -np.inf)
680+
681+
result = result.reshape(shape)
682+
683+
return result
684+
685+
648686
def _interp_limit(invalid, fw_limit, bw_limit):
649687
"""Get idx of values that won't be filled b/c they exceed the limits.
650688

pandas/tests/indexes/test_numeric.py

+40
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*- coding: utf-8 -*-
2+
import operator
23

34
import pytest
45

@@ -17,6 +18,11 @@
1718

1819
from pandas.tests.indexes.common import Base
1920

21+
# For testing division by (or of) zero for Series with length 5, this
22+
# gives several scalar-zeros and length-5 vector-zeros
23+
zeros = tm.gen_zeros(5)
24+
zeros = [x for x in zeros if not isinstance(x, Series)]
25+
2026

2127
def full_like(array, value):
2228
"""Compatibility for numpy<1.8.0
@@ -157,6 +163,40 @@ def test_divmod_series(self):
157163
for r, e in zip(result, expected):
158164
tm.assert_series_equal(r, e)
159165

166+
@pytest.mark.parametrize('op', [operator.div, operator.truediv])
167+
@pytest.mark.parametrize('zero', zeros)
168+
def test_div_zero(self, zero, op):
169+
idx = self.create_index()
170+
171+
expected = Index([np.nan, np.inf, np.inf, np.inf, np.inf],
172+
dtype=np.float64)
173+
result = op(idx, zero)
174+
tm.assert_index_equal(result, expected)
175+
ser_compat = op(Series(idx).astype('i8'), np.array(zero).astype('i8'))
176+
tm.assert_series_equal(ser_compat, Series(result))
177+
178+
@pytest.mark.parametrize('zero', zeros)
179+
def test_floordiv_zero(self, zero):
180+
idx = self.create_index()
181+
expected = Index([np.nan, np.inf, np.inf, np.inf, np.inf],
182+
dtype=np.float64)
183+
184+
result = idx // zero
185+
tm.assert_index_equal(result, expected)
186+
ser_compat = Series(idx).astype('i8') // np.array(zero).astype('i8')
187+
tm.assert_series_equal(ser_compat, Series(result))
188+
189+
@pytest.mark.parametrize('zero', zeros)
190+
def test_mod_zero(self, zero):
191+
idx = self.create_index()
192+
193+
expected = Index([np.nan, np.nan, np.nan, np.nan, np.nan],
194+
dtype=np.float64)
195+
result = idx % zero
196+
tm.assert_index_equal(result, expected)
197+
ser_compat = Series(idx).astype('i8') % np.array(zero).astype('i8')
198+
tm.assert_series_equal(ser_compat, Series(result))
199+
160200
def test_explicit_conversions(self):
161201

162202
# GH 8608

pandas/util/testing.py

+26
Original file line numberDiff line numberDiff line change
@@ -1964,6 +1964,32 @@ def add_nans_panel4d(panel4d):
19641964
return panel4d
19651965

19661966

1967+
def gen_zeros(arr_len):
1968+
"""
1969+
For testing division by (or of) zero for Series or Indexes with the given
1970+
length, this gives variants of scalar zeros and vector zeros with different
1971+
dtypes.
1972+
1973+
Generate variants of scalar zeros and all-zero arrays with the given
1974+
length.
1975+
1976+
Parameters
1977+
----------
1978+
arr_len : int
1979+
1980+
Returns
1981+
-------
1982+
zeros : list
1983+
"""
1984+
zeros = [box([0] * arr_len, dtype=dtype)
1985+
for box in [pd.Series, pd.Index, np.array]
1986+
for dtype in [np.int64, np.uint64, np.float64]]
1987+
zeros.extend([np.array(0, dtype=dtype)
1988+
for dtype in [np.int64, np.uint64, np.float64]])
1989+
zeros.extend([0, 0.0, long(0)])
1990+
return zeros
1991+
1992+
19671993
class TestSubDict(dict):
19681994

19691995
def __init__(self, *args, **kwargs):

0 commit comments

Comments
 (0)