Skip to content

Commit a359a99

Browse files
jbrockmendeljreback
authored andcommitted
BUG: Fix+test division by negative zero (#27278)
1 parent 65e123c commit a359a99

File tree

6 files changed

+78
-11
lines changed

6 files changed

+78
-11
lines changed

pandas/core/arrays/integer.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import numbers
2-
import sys
32
from typing import Type
43
import warnings
54

@@ -675,7 +674,7 @@ def _maybe_mask_result(self, result, mask, other, op_name):
675674
# a float result
676675
# or our op is a divide
677676
if (is_float_dtype(other) or is_float(other)) or (
678-
op_name in ["rtruediv", "truediv", "rdiv", "div"]
677+
op_name in ["rtruediv", "truediv"]
679678
):
680679
result[mask] = np.nan
681680
return result
@@ -747,8 +746,6 @@ def integer_arithmetic_method(self, other):
747746
IntegerArray._add_comparison_ops()
748747

749748

750-
module = sys.modules[__name__]
751-
752749
_dtype_docstring = """
753750
An ExtensionDtype for {dtype} integer data.
754751

pandas/core/ops/missing.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,13 @@ def mask_zero_div_zero(x, y, result, copy=False):
120120
if zmask.any():
121121
shape = result.shape
122122

123+
# Flip sign if necessary for -0.0
124+
zneg_mask = zmask & np.signbit(y)
125+
zpos_mask = zmask & ~zneg_mask
126+
123127
nan_mask = (zmask & (x == 0)).ravel()
124-
neginf_mask = (zmask & (x < 0)).ravel()
125-
posinf_mask = (zmask & (x > 0)).ravel()
128+
neginf_mask = ((zpos_mask & (x < 0)) | (zneg_mask & (x > 0))).ravel()
129+
posinf_mask = ((zpos_mask & (x > 0)) | (zneg_mask & (x < 0))).ravel()
126130

127131
if nan_mask.any() or neginf_mask.any() or posinf_mask.any():
128132
# Fill negative/0 with -inf, positive/0 with +inf, 0/0 with NaN

pandas/tests/arithmetic/conftest.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ def one(request):
3030
for box_cls in [pd.Index, np.array]
3131
for dtype in [np.int64, np.uint64, np.float64]
3232
]
33+
zeros.extend(
34+
[box_cls([-0.0] * 5, dtype=np.float64) for box_cls in [pd.Index, np.array]]
35+
)
3336
zeros.extend([np.array(0, dtype=dtype) for dtype in [np.int64, np.uint64, np.float64]])
34-
zeros.extend([0, 0.0])
37+
zeros.extend([np.array(-0.0, dtype=np.float64)])
38+
zeros.extend([0, 0.0, -0.0])
3539

3640

3741
@pytest.fixture(params=zeros)

pandas/tests/arithmetic/test_numeric.py

+63-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@
1414
from pandas.core import ops
1515
import pandas.util.testing as tm
1616

17+
18+
def adjust_negative_zero(zero, expected):
19+
"""
20+
Helper to adjust the expected result if we are dividing by -0.0
21+
as opposed to 0.0
22+
"""
23+
if np.signbit(np.array(zero)).any():
24+
# All entries in the `zero` fixture should be either
25+
# all-negative or no-negative.
26+
assert np.signbit(np.array(zero)).all()
27+
28+
expected *= -1
29+
30+
return expected
31+
32+
1733
# ------------------------------------------------------------------
1834
# Comparisons
1935

@@ -229,20 +245,27 @@ def test_div_zero(self, zero, numeric_idx):
229245
idx = numeric_idx
230246

231247
expected = pd.Index([np.nan, np.inf, np.inf, np.inf, np.inf], dtype=np.float64)
248+
# We only adjust for Index, because Series does not yet apply
249+
# the adjustment correctly.
250+
expected2 = adjust_negative_zero(zero, expected)
251+
232252
result = idx / zero
233-
tm.assert_index_equal(result, expected)
253+
tm.assert_index_equal(result, expected2)
234254
ser_compat = Series(idx).astype("i8") / np.array(zero).astype("i8")
235-
tm.assert_series_equal(ser_compat, Series(result))
255+
tm.assert_series_equal(ser_compat, Series(expected))
236256

237257
def test_floordiv_zero(self, zero, numeric_idx):
238258
idx = numeric_idx
239259

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

242265
result = idx // zero
243-
tm.assert_index_equal(result, expected)
266+
tm.assert_index_equal(result, expected2)
244267
ser_compat = Series(idx).astype("i8") // np.array(zero).astype("i8")
245-
tm.assert_series_equal(ser_compat, Series(result))
268+
tm.assert_series_equal(ser_compat, Series(expected))
246269

247270
def test_mod_zero(self, zero, numeric_idx):
248271
idx = numeric_idx
@@ -258,11 +281,27 @@ def test_divmod_zero(self, zero, numeric_idx):
258281

259282
exleft = pd.Index([np.nan, np.inf, np.inf, np.inf, np.inf], dtype=np.float64)
260283
exright = pd.Index([np.nan, np.nan, np.nan, np.nan, np.nan], dtype=np.float64)
284+
exleft = adjust_negative_zero(zero, exleft)
261285

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

290+
@pytest.mark.parametrize("op", [operator.truediv, operator.floordiv])
291+
def test_div_negative_zero(self, zero, numeric_idx, op):
292+
# Check that -1 / -0.0 returns np.inf, not -np.inf
293+
if isinstance(numeric_idx, pd.UInt64Index):
294+
return
295+
idx = numeric_idx - 3
296+
297+
expected = pd.Index(
298+
[-np.inf, -np.inf, -np.inf, np.nan, np.inf], dtype=np.float64
299+
)
300+
expected = adjust_negative_zero(zero, expected)
301+
302+
result = op(idx, zero)
303+
tm.assert_index_equal(result, expected)
304+
266305
# ------------------------------------------------------------------
267306

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

938+
@pytest.mark.xfail(
939+
reason="Series division does not yet fill 1/0 consistently; Index does."
940+
)
941+
def test_series_divmod_zero(self):
942+
# Check that divmod uses pandas convention for division by zero,
943+
# which does not match numpy.
944+
# pandas convention has
945+
# 1/0 == np.inf
946+
# -1/0 == -np.inf
947+
# 1/-0.0 == -np.inf
948+
# -1/-0.0 == np.inf
949+
tser = tm.makeTimeSeries().rename("ts")
950+
other = tser * 0
951+
952+
result = divmod(tser, other)
953+
exp1 = pd.Series([np.inf] * len(tser), index=tser.index)
954+
exp2 = pd.Series([np.nan] * len(tser), index=tser.index)
955+
tm.assert_series_equal(result[0], exp1)
956+
tm.assert_series_equal(result[1], exp2)
957+
899958

900959
class TestUFuncCompat:
901960
@pytest.mark.parametrize(

pandas/tests/io/pytables/test_pytables.py

+1
Original file line numberDiff line numberDiff line change
@@ -4337,6 +4337,7 @@ def test_store_datetime_mixed(self):
43374337
df["d"] = ts.index[:3]
43384338
self._check_roundtrip(df, tm.assert_frame_equal)
43394339

4340+
# FIXME: don't leave commented-out code
43404341
# def test_cant_write_multiindex_table(self):
43414342
# # for now, #1848
43424343
# df = DataFrame(np.random.randn(10, 4),

setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ plugins = Cython.Coverage
8484
[coverage:report]
8585
ignore_errors = False
8686
show_missing = True
87+
omit =
88+
pandas/_version.py
8789
# Regexes for lines to exclude from consideration
8890
exclude_lines =
8991
# Have to re-enable the standard pragma

0 commit comments

Comments
 (0)