Skip to content

BUG: Fix+test division by negative zero #27278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions pandas/core/arrays/integer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import numbers
import sys
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all edits in this file are in the "misc" category

from typing import Type
import warnings

Expand Down Expand Up @@ -675,7 +674,7 @@ def _maybe_mask_result(self, result, mask, other, op_name):
# a float result
# or our op is a divide
if (is_float_dtype(other) or is_float(other)) or (
op_name in ["rtruediv", "truediv", "rdiv", "div"]
op_name in ["rtruediv", "truediv"]
):
result[mask] = np.nan
return result
Expand Down Expand Up @@ -747,8 +746,6 @@ def integer_arithmetic_method(self, other):
IntegerArray._add_comparison_ops()


module = sys.modules[__name__]

_dtype_docstring = """
An ExtensionDtype for {dtype} integer data.

Expand Down
8 changes: 6 additions & 2 deletions pandas/core/ops/missing.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,13 @@ def mask_zero_div_zero(x, y, result, copy=False):
if zmask.any():
shape = result.shape

# Flip sign if necessary for -0.0
zneg_mask = zmask & np.signbit(y)
zpos_mask = zmask & ~zneg_mask

nan_mask = (zmask & (x == 0)).ravel()
neginf_mask = (zmask & (x < 0)).ravel()
posinf_mask = (zmask & (x > 0)).ravel()
neginf_mask = ((zpos_mask & (x < 0)) | (zneg_mask & (x > 0))).ravel()
posinf_mask = ((zpos_mask & (x > 0)) | (zneg_mask & (x < 0))).ravel()

if nan_mask.any() or neginf_mask.any() or posinf_mask.any():
# Fill negative/0 with -inf, positive/0 with +inf, 0/0 with NaN
Expand Down
6 changes: 5 additions & 1 deletion pandas/tests/arithmetic/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ def one(request):
for box_cls in [pd.Index, np.array]
for dtype in [np.int64, np.uint64, np.float64]
]
zeros.extend(
[box_cls([-0.0] * 5, dtype=np.float64) for box_cls in [pd.Index, np.array]]
)
zeros.extend([np.array(0, dtype=dtype) for dtype in [np.int64, np.uint64, np.float64]])
zeros.extend([0, 0.0])
zeros.extend([np.array(-0.0, dtype=np.float64)])
zeros.extend([0, 0.0, -0.0])


@pytest.fixture(params=zeros)
Expand Down
67 changes: 63 additions & 4 deletions pandas/tests/arithmetic/test_numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@
from pandas.core import ops
import pandas.util.testing as tm


def adjust_negative_zero(zero, expected):
"""
Helper to adjust the expected result if we are dividing by -0.0
as opposed to 0.0
"""
if np.signbit(np.array(zero)).any():
# All entries in the `zero` fixture should be either
# all-negative or no-negative.
assert np.signbit(np.array(zero)).all()

expected *= -1

return expected


# ------------------------------------------------------------------
# Comparisons

Expand Down Expand Up @@ -229,20 +245,27 @@ def test_div_zero(self, zero, numeric_idx):
idx = numeric_idx

expected = pd.Index([np.nan, np.inf, np.inf, np.inf, np.inf], dtype=np.float64)
# We only adjust for Index, because Series does not yet apply
# the adjustment correctly.
expected2 = adjust_negative_zero(zero, expected)

result = idx / zero
tm.assert_index_equal(result, expected)
tm.assert_index_equal(result, expected2)
ser_compat = Series(idx).astype("i8") / np.array(zero).astype("i8")
tm.assert_series_equal(ser_compat, Series(result))
tm.assert_series_equal(ser_compat, Series(expected))

def test_floordiv_zero(self, zero, numeric_idx):
idx = numeric_idx

expected = pd.Index([np.nan, np.inf, np.inf, np.inf, np.inf], dtype=np.float64)
# We only adjust for Index, because Series does not yet apply
# the adjustment correctly.
expected2 = adjust_negative_zero(zero, expected)

result = idx // zero
tm.assert_index_equal(result, expected)
tm.assert_index_equal(result, expected2)
ser_compat = Series(idx).astype("i8") // np.array(zero).astype("i8")
tm.assert_series_equal(ser_compat, Series(result))
tm.assert_series_equal(ser_compat, Series(expected))

def test_mod_zero(self, zero, numeric_idx):
idx = numeric_idx
Expand All @@ -258,11 +281,27 @@ def test_divmod_zero(self, zero, numeric_idx):

exleft = pd.Index([np.nan, np.inf, np.inf, np.inf, np.inf], dtype=np.float64)
exright = pd.Index([np.nan, np.nan, np.nan, np.nan, np.nan], dtype=np.float64)
exleft = adjust_negative_zero(zero, exleft)

result = divmod(idx, zero)
tm.assert_index_equal(result[0], exleft)
tm.assert_index_equal(result[1], exright)

@pytest.mark.parametrize("op", [operator.truediv, operator.floordiv])
def test_div_negative_zero(self, zero, numeric_idx, op):
# Check that -1 / -0.0 returns np.inf, not -np.inf
if isinstance(numeric_idx, pd.UInt64Index):
return
idx = numeric_idx - 3

expected = pd.Index(
[-np.inf, -np.inf, -np.inf, np.nan, np.inf], dtype=np.float64
)
expected = adjust_negative_zero(zero, expected)

result = op(idx, zero)
tm.assert_index_equal(result, expected)

# ------------------------------------------------------------------

@pytest.mark.parametrize("dtype1", [np.int64, np.float64, np.uint64])
Expand Down Expand Up @@ -896,6 +935,26 @@ def check(series, other):
check(tser, tser[::2])
check(tser, 5)

@pytest.mark.xfail(
reason="Series division does not yet fill 1/0 consistently; Index does."
)
def test_series_divmod_zero(self):
# Check that divmod uses pandas convention for division by zero,
# which does not match numpy.
# pandas convention has
# 1/0 == np.inf
# -1/0 == -np.inf
# 1/-0.0 == -np.inf
# -1/-0.0 == np.inf
tser = tm.makeTimeSeries().rename("ts")
other = tser * 0

result = divmod(tser, other)
exp1 = pd.Series([np.inf] * len(tser), index=tser.index)
exp2 = pd.Series([np.nan] * len(tser), index=tser.index)
tm.assert_series_equal(result[0], exp1)
tm.assert_series_equal(result[1], exp2)


class TestUFuncCompat:
@pytest.mark.parametrize(
Expand Down
1 change: 1 addition & 0 deletions pandas/tests/io/pytables/test_pytables.py
Original file line number Diff line number Diff line change
Expand Up @@ -4337,6 +4337,7 @@ def test_store_datetime_mixed(self):
df["d"] = ts.index[:3]
self._check_roundtrip(df, tm.assert_frame_equal)

# FIXME: don't leave commented-out code
# def test_cant_write_multiindex_table(self):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

misc category

# # for now, #1848
# df = DataFrame(np.random.randn(10, 4),
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ plugins = Cython.Coverage
[coverage:report]
ignore_errors = False
show_missing = True
omit =
pandas/_version.py
# Regexes for lines to exclude from consideration
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

misc category, xref #26877

exclude_lines =
# Have to re-enable the standard pragma
Expand Down