From 24dd512aedf39126a5b6ee7d0e0f3ddfedb775ef Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 7 Jul 2019 09:17:25 -0700 Subject: [PATCH 1/2] Fix+test division by negative zero --- pandas/core/arrays/integer.py | 5 +- pandas/core/ops/missing.py | 8 ++- pandas/tests/arithmetic/conftest.py | 6 +- pandas/tests/arithmetic/test_numeric.py | 67 ++++++++++++++++++++-- pandas/tests/extension/test_categorical.py | 11 +++- pandas/tests/io/pytables/test_pytables.py | 1 + setup.cfg | 2 + 7 files changed, 86 insertions(+), 14 deletions(-) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index c999c4db232e6..867122964fe59 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -1,5 +1,4 @@ import numbers -import sys from typing import Type import warnings @@ -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 @@ -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. diff --git a/pandas/core/ops/missing.py b/pandas/core/ops/missing.py index 4ca1861baf237..608c2550994f1 100644 --- a/pandas/core/ops/missing.py +++ b/pandas/core/ops/missing.py @@ -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 diff --git a/pandas/tests/arithmetic/conftest.py b/pandas/tests/arithmetic/conftest.py index c67a67bb31d62..f047154f2c636 100644 --- a/pandas/tests/arithmetic/conftest.py +++ b/pandas/tests/arithmetic/conftest.py @@ -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) diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index f582bf8b13975..30120f90e386b 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -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 @@ -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 @@ -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]) @@ -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( diff --git a/pandas/tests/extension/test_categorical.py b/pandas/tests/extension/test_categorical.py index f7456d24ad6d3..542623485fa2e 100644 --- a/pandas/tests/extension/test_categorical.py +++ b/pandas/tests/extension/test_categorical.py @@ -41,14 +41,19 @@ def dtype(): return CategoricalDtype() -@pytest.fixture def data(): - """Length-100 array for this type. + return Categorical(make_data()) + + +@pytest.fixture(name="data") +def data_fixture(): + """ + Length-100 array for this type. * data[0] and data[1] should both be non missing * data[0] and data[1] should not gbe equal """ - return Categorical(make_data()) + return data() @pytest.fixture diff --git a/pandas/tests/io/pytables/test_pytables.py b/pandas/tests/io/pytables/test_pytables.py index fee7e1cb2ba5f..3ff50cc7b1097 100644 --- a/pandas/tests/io/pytables/test_pytables.py +++ b/pandas/tests/io/pytables/test_pytables.py @@ -4339,6 +4339,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): # # for now, #1848 # df = DataFrame(np.random.randn(10, 4), diff --git a/setup.cfg b/setup.cfg index fee0ab60f25b5..7549bfe2e325d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 exclude_lines = # Have to re-enable the standard pragma From a0d8e710a6949e6e0e14fee217ea99077a674e9b Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 7 Jul 2019 18:15:34 -0700 Subject: [PATCH 2/2] revert --- pandas/tests/extension/test_categorical.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pandas/tests/extension/test_categorical.py b/pandas/tests/extension/test_categorical.py index 542623485fa2e..f7456d24ad6d3 100644 --- a/pandas/tests/extension/test_categorical.py +++ b/pandas/tests/extension/test_categorical.py @@ -41,19 +41,14 @@ def dtype(): return CategoricalDtype() +@pytest.fixture def data(): - return Categorical(make_data()) - - -@pytest.fixture(name="data") -def data_fixture(): - """ - Length-100 array for this type. + """Length-100 array for this type. * data[0] and data[1] should both be non missing * data[0] and data[1] should not gbe equal """ - return data() + return Categorical(make_data()) @pytest.fixture