diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 044361de218df..0ce7fbfe59c9f 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -825,6 +825,7 @@ Sparse - Bug in :meth:`SparseArray.max` and :meth:`SparseArray.min` raising ``ValueError`` for arrays with 0 non-null elements (:issue:`43527`) - Bug in :meth:`DataFrame.sparse.to_coo` silently converting non-zero fill values to zero (:issue:`24817`) - Bug in :class:`SparseArray` comparison methods with an array-like operand of mismatched length raising ``AssertionError`` or unclear ``ValueError`` depending on the input (:issue:`43863`) +- Bug in :class:`SparseArray` arithmetic methods ``floordiv`` and ``mod`` behaviors when dividing by zero not matching the non-sparse :class:`Series` behavior (:issue:`38172`) - ExtensionArray diff --git a/pandas/_libs/sparse_op_helper.pxi.in b/pandas/_libs/sparse_op_helper.pxi.in index c6e65f8b96187..e6a2c7b1b050a 100644 --- a/pandas/_libs/sparse_op_helper.pxi.in +++ b/pandas/_libs/sparse_op_helper.pxi.in @@ -42,6 +42,11 @@ cdef inline sparse_t __mod__(sparse_t a, sparse_t b): cdef inline sparse_t __floordiv__(sparse_t a, sparse_t b): if b == 0: if sparse_t is float64_t: + # Match non-sparse Series behavior implemented in mask_zero_div_zero + if a > 0: + return INF + elif a < 0: + return -INF return NaN else: return 0 diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py index f69b9868b10e4..17c5320b1e941 100644 --- a/pandas/core/arrays/sparse/array.py +++ b/pandas/core/arrays/sparse/array.py @@ -220,6 +220,16 @@ def _sparse_array_op( left_sp_values = left.sp_values right_sp_values = right.sp_values + if ( + name in ["floordiv", "mod"] + and (right == 0).any() + and left.dtype.kind in ["i", "u"] + ): + # Match the non-Sparse Series behavior + opname = f"sparse_{name}_float64" + left_sp_values = left_sp_values.astype("float64") + right_sp_values = right_sp_values.astype("float64") + sparse_op = getattr(splib, opname) with np.errstate(all="ignore"): diff --git a/pandas/tests/arrays/sparse/test_arithmetics.py b/pandas/tests/arrays/sparse/test_arithmetics.py index d7c39c0e0708e..012fe61fdba05 100644 --- a/pandas/tests/arrays/sparse/test_arithmetics.py +++ b/pandas/tests/arrays/sparse/test_arithmetics.py @@ -34,26 +34,23 @@ class TestSparseArrayArithmetics: def _assert(self, a, b): tm.assert_numpy_array_equal(a, b) - def _check_numeric_ops(self, a, b, a_dense, b_dense, mix, op): + def _check_numeric_ops(self, a, b, a_dense, b_dense, mix: bool, op): + # Check that arithmetic behavior matches non-Sparse Series arithmetic + + if isinstance(a_dense, np.ndarray): + expected = op(pd.Series(a_dense), b_dense).values + elif isinstance(b_dense, np.ndarray): + expected = op(a_dense, pd.Series(b_dense)).values + else: + raise NotImplementedError + with np.errstate(invalid="ignore", divide="ignore"): if mix: result = op(a, b_dense).to_dense() else: result = op(a, b).to_dense() - if op in [operator.truediv, ops.rtruediv]: - # pandas uses future division - expected = op(a_dense * 1.0, b_dense) - else: - expected = op(a_dense, b_dense) - - if op in [operator.floordiv, ops.rfloordiv]: - # Series sets 1//0 to np.inf, which SparseArray does not do (yet) - mask = np.isinf(expected) - if mask.any(): - expected[mask] = np.nan - - self._assert(result, expected) + self._assert(result, expected) def _check_bool_result(self, res): assert isinstance(res, self._klass) @@ -125,7 +122,7 @@ def test_float_scalar( ): op = all_arithmetic_functions - if not np_version_under1p20: + if np_version_under1p20: if op in [operator.floordiv, ops.rfloordiv]: if op is operator.floordiv and scalar != 0: pass @@ -158,9 +155,7 @@ def test_float_scalar_comparison(self, kind): self._check_comparison_ops(a, 0, values, 0) self._check_comparison_ops(a, 3, values, 3) - def test_float_same_index_without_nans( - self, kind, mix, all_arithmetic_functions, request - ): + def test_float_same_index_without_nans(self, kind, mix, all_arithmetic_functions): # when sp_index are the same op = all_arithmetic_functions @@ -178,13 +173,12 @@ def test_float_same_index_with_nans( op = all_arithmetic_functions if ( - not np_version_under1p20 + np_version_under1p20 and op is ops.rfloordiv and not (mix and kind == "block") ): mark = pytest.mark.xfail(raises=AssertionError, reason="GH#38172") request.node.add_marker(mark) - values = self._base([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan]) rvalues = self._base([np.nan, 2, 3, 4, np.nan, 0, 1, 3, 2, np.nan]) @@ -360,11 +354,7 @@ def test_bool_array_logical(self, kind, fill_value): def test_mixed_array_float_int(self, kind, mix, all_arithmetic_functions, request): op = all_arithmetic_functions - if ( - not np_version_under1p20 - and op in [operator.floordiv, ops.rfloordiv] - and mix - ): + if np_version_under1p20 and op in [operator.floordiv, ops.rfloordiv] and mix: mark = pytest.mark.xfail(raises=AssertionError, reason="GH#38172") request.node.add_marker(mark)