Skip to content

Commit 029ae81

Browse files
authored
BUG: SparseArray.__floordiv__ not matching non-Sparse Series behavior (#44928)
1 parent 9a03f9c commit 029ae81

File tree

4 files changed

+31
-25
lines changed

4 files changed

+31
-25
lines changed

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,7 @@ Sparse
828828
- Bug in :meth:`SparseArray.max` and :meth:`SparseArray.min` raising ``ValueError`` for arrays with 0 non-null elements (:issue:`43527`)
829829
- Bug in :meth:`DataFrame.sparse.to_coo` silently converting non-zero fill values to zero (:issue:`24817`)
830830
- 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`)
831+
- Bug in :class:`SparseArray` arithmetic methods ``floordiv`` and ``mod`` behaviors when dividing by zero not matching the non-sparse :class:`Series` behavior (:issue:`38172`)
831832
-
832833

833834
ExtensionArray

pandas/_libs/sparse_op_helper.pxi.in

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ cdef inline sparse_t __mod__(sparse_t a, sparse_t b):
4242
cdef inline sparse_t __floordiv__(sparse_t a, sparse_t b):
4343
if b == 0:
4444
if sparse_t is float64_t:
45+
# Match non-sparse Series behavior implemented in mask_zero_div_zero
46+
if a > 0:
47+
return INF
48+
elif a < 0:
49+
return -INF
4550
return NaN
4651
else:
4752
return 0

pandas/core/arrays/sparse/array.py

+10
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,16 @@ def _sparse_array_op(
220220
left_sp_values = left.sp_values
221221
right_sp_values = right.sp_values
222222

223+
if (
224+
name in ["floordiv", "mod"]
225+
and (right == 0).any()
226+
and left.dtype.kind in ["i", "u"]
227+
):
228+
# Match the non-Sparse Series behavior
229+
opname = f"sparse_{name}_float64"
230+
left_sp_values = left_sp_values.astype("float64")
231+
right_sp_values = right_sp_values.astype("float64")
232+
223233
sparse_op = getattr(splib, opname)
224234

225235
with np.errstate(all="ignore"):

pandas/tests/arrays/sparse/test_arithmetics.py

+15-25
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,23 @@ class TestSparseArrayArithmetics:
3434
def _assert(self, a, b):
3535
tm.assert_numpy_array_equal(a, b)
3636

37-
def _check_numeric_ops(self, a, b, a_dense, b_dense, mix, op):
37+
def _check_numeric_ops(self, a, b, a_dense, b_dense, mix: bool, op):
38+
# Check that arithmetic behavior matches non-Sparse Series arithmetic
39+
40+
if isinstance(a_dense, np.ndarray):
41+
expected = op(pd.Series(a_dense), b_dense).values
42+
elif isinstance(b_dense, np.ndarray):
43+
expected = op(a_dense, pd.Series(b_dense)).values
44+
else:
45+
raise NotImplementedError
46+
3847
with np.errstate(invalid="ignore", divide="ignore"):
3948
if mix:
4049
result = op(a, b_dense).to_dense()
4150
else:
4251
result = op(a, b).to_dense()
4352

44-
if op in [operator.truediv, ops.rtruediv]:
45-
# pandas uses future division
46-
expected = op(a_dense * 1.0, b_dense)
47-
else:
48-
expected = op(a_dense, b_dense)
49-
50-
if op in [operator.floordiv, ops.rfloordiv]:
51-
# Series sets 1//0 to np.inf, which SparseArray does not do (yet)
52-
mask = np.isinf(expected)
53-
if mask.any():
54-
expected[mask] = np.nan
55-
56-
self._assert(result, expected)
53+
self._assert(result, expected)
5754

5855
def _check_bool_result(self, res):
5956
assert isinstance(res, self._klass)
@@ -125,7 +122,7 @@ def test_float_scalar(
125122
):
126123
op = all_arithmetic_functions
127124

128-
if not np_version_under1p20:
125+
if np_version_under1p20:
129126
if op in [operator.floordiv, ops.rfloordiv]:
130127
if op is operator.floordiv and scalar != 0:
131128
pass
@@ -158,9 +155,7 @@ def test_float_scalar_comparison(self, kind):
158155
self._check_comparison_ops(a, 0, values, 0)
159156
self._check_comparison_ops(a, 3, values, 3)
160157

161-
def test_float_same_index_without_nans(
162-
self, kind, mix, all_arithmetic_functions, request
163-
):
158+
def test_float_same_index_without_nans(self, kind, mix, all_arithmetic_functions):
164159
# when sp_index are the same
165160
op = all_arithmetic_functions
166161

@@ -178,13 +173,12 @@ def test_float_same_index_with_nans(
178173
op = all_arithmetic_functions
179174

180175
if (
181-
not np_version_under1p20
176+
np_version_under1p20
182177
and op is ops.rfloordiv
183178
and not (mix and kind == "block")
184179
):
185180
mark = pytest.mark.xfail(raises=AssertionError, reason="GH#38172")
186181
request.node.add_marker(mark)
187-
188182
values = self._base([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
189183
rvalues = self._base([np.nan, 2, 3, 4, np.nan, 0, 1, 3, 2, np.nan])
190184

@@ -360,11 +354,7 @@ def test_bool_array_logical(self, kind, fill_value):
360354
def test_mixed_array_float_int(self, kind, mix, all_arithmetic_functions, request):
361355
op = all_arithmetic_functions
362356

363-
if (
364-
not np_version_under1p20
365-
and op in [operator.floordiv, ops.rfloordiv]
366-
and mix
367-
):
357+
if np_version_under1p20 and op in [operator.floordiv, ops.rfloordiv] and mix:
368358
mark = pytest.mark.xfail(raises=AssertionError, reason="GH#38172")
369359
request.node.add_marker(mark)
370360

0 commit comments

Comments
 (0)