Skip to content

Fix IntegerArray pow for special cases #30210

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
3 changes: 2 additions & 1 deletion doc/source/whatsnew/v1.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,8 @@ Other
- Bug in :meth:`DataFrame.append` that raised ``IndexError`` when appending with empty list (:issue:`28769`)
- Fix :class:`AbstractHolidayCalendar` to return correct results for
years after 2030 (now goes up to 2200) (:issue:`27790`)
- Fixed :class:`IntegerArray` returning ``NA`` rather than ``inf`` for operations dividing by 0 (:issue:`27398`)
- Fixed :class:`IntegerArray` returning ``inf`` rather than ``NA`` for operations dividing by 0 (:issue:`27398`)
- Fixed ``pow`` operations for :class:`IntegerArray` when the other value is ``0`` or ``1`` (:issue:`29997`)
- Bug in :meth:`Series.count` raises if use_inf_as_na is enabled (:issue:`29478`)


Expand Down
25 changes: 18 additions & 7 deletions pandas/core/arrays/integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,13 +718,13 @@ def _create_arithmetic_method(cls, op):
@unpack_zerodim_and_defer(op.__name__)
def integer_arithmetic_method(self, other):

mask = None
omask = None

if getattr(other, "ndim", 0) > 1:
raise NotImplementedError("can only perform ops with 1-d structures")

if isinstance(other, IntegerArray):
other, mask = other._data, other._mask
other, omask = other._data, other._mask

elif is_list_like(other):
other = np.asarray(other)
Expand All @@ -742,17 +742,28 @@ def integer_arithmetic_method(self, other):
raise TypeError("can only perform ops with numeric values")

# nans propagate
if mask is None:
if omask is None:
mask = self._mask.copy()
else:
mask = self._mask | mask
mask = self._mask | omask

# 1 ** np.nan is 1. So we have to unmask those.
if op_name == "pow":
mask = np.where(self == 1, False, mask)
# 1 ** x is 1.
mask = np.where((self._data == 1) & ~self._mask, False, mask)
# x ** 0 is 1.
if omask is not None:
mask = np.where((other == 0) & ~omask, False, mask)
else:
mask = np.where(other == 0, False, mask)

elif op_name == "rpow":
mask = np.where(other == 1, False, mask)
# 1 ** x is 1.
if omask is not None:
mask = np.where((other == 1) & ~omask, False, mask)
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't this check for self._data == 1 ?

Copy link
Member

Choose a reason for hiding this comment

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

Ah, no, because it is for the reversed op? (maybe add a comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your call on whether a comment is needed. It's clear IMO, though I wrote it :)

else:
mask = np.where(other == 1, False, mask)
# x ** 0 is 1.
mask = np.where((self._data == 0) & ~self._mask, False, mask)

with np.errstate(all="ignore"):
result = op(self._data, other)
Expand Down
50 changes: 41 additions & 9 deletions pandas/tests/arrays/test_integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,22 +346,54 @@ def test_divide_by_zero(self, zero, negative):
result = a / zero
expected = np.array([np.nan, np.inf, -np.inf, np.nan])
if negative:
values = [np.nan, -np.inf, np.inf, np.nan]
else:
values = [np.nan, np.inf, -np.inf, np.nan]
expected = np.array(values)
expected *= -1
tm.assert_numpy_array_equal(result, expected)

def test_pow(self):
# https://github.com/pandas-dev/pandas/issues/22022
a = integer_array([1, np.nan, np.nan, 1])
b = integer_array([1, np.nan, 1, np.nan])
def test_pow_scalar(self):
a = pd.array([0, 1, None, 2], dtype="Int64")
result = a ** 0
expected = pd.array([1, 1, 1, 1], dtype="Int64")
tm.assert_extension_array_equal(result, expected)

result = a ** 1
expected = pd.array([0, 1, None, 2], dtype="Int64")
tm.assert_extension_array_equal(result, expected)

# result = a ** pd.NA
# expected = pd.array([None, 1, None, None], dtype="Int64")
# tm.assert_extension_array_equal(result, expected)

result = a ** np.nan
expected = np.array([np.nan, 1, np.nan, np.nan], dtype="float64")
tm.assert_numpy_array_equal(result, expected)

# reversed
result = 0 ** a
expected = pd.array([1, 0, None, 0], dtype="Int64")
tm.assert_extension_array_equal(result, expected)

result = 1 ** a
expected = pd.array([1, 1, 1, 1], dtype="Int64")
tm.assert_extension_array_equal(result, expected)

# result = pd.NA ** a
# expected = pd.array([1, None, None, None], dtype="Int64")
# tm.assert_extension_array_equal(result, expected)

result = np.nan ** a
expected = np.array([1, np.nan, np.nan, np.nan], dtype="float64")
tm.assert_numpy_array_equal(result, expected)

def test_pow_array(self):
a = integer_array([0, 0, 0, 1, 1, 1, None, None, None])
b = integer_array([0, 1, None, 0, 1, None, 0, 1, None])
result = a ** b
expected = pd.core.arrays.integer_array([1, np.nan, np.nan, 1])
expected = integer_array([1, 0, None, 1, 1, 1, 1, None, None])
tm.assert_extension_array_equal(result, expected)

def test_rpow_one_to_na(self):
# https://github.com/pandas-dev/pandas/issues/22022
# https://github.com/pandas-dev/pandas/issues/29997
arr = integer_array([np.nan, np.nan])
result = np.array([1.0, 2.0]) ** arr
expected = np.array([1.0, np.nan])
Expand Down