Skip to content

Commit 7abf40e

Browse files
committed
API: Handle pow & rpow special cases
Closes pandas-dev#29997
1 parent c0f6428 commit 7abf40e

File tree

3 files changed

+73
-6
lines changed

3 files changed

+73
-6
lines changed

doc/source/reference/arrays.rst

+27-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
.. _api.arrays:
44

5-
=============
6-
Pandas arrays
7-
=============
5+
=========================
6+
Pandas arrays and scalars
7+
=========================
88

99
.. currentmodule:: pandas
1010

@@ -28,6 +28,30 @@ Strings :class:`StringDtype` :class:`str` :ref:`api.array
2828
Boolean (with NA) :class:`BooleanDtype` :class:`bool` :ref:`api.arrays.bool`
2929
=================== ========================= ================== =============================
3030

31+
As the table shows, each extension type is associated with an array class. Pandas may define
32+
a dedicated scalar for the type (for example, :class:`arrays.IntervalArray` uses :class:`Interval`)
33+
or it may re-use Python's scalars (for example, :class:`StringArray` uses Python's :class:`str`).
34+
35+
Additionally, pandas defines a singleton scalar missing value :class:`pandas.NA`. This
36+
value is distinct from ``float('nan')``, :attr:`numpy.nan` and Python's :class:`None`.
37+
38+
.. autosummary::
39+
:toctree: api/
40+
41+
NA
42+
43+
In binary operations, :class:`NA` is treated as numeric. Generally, ``NA`` propagates, so
44+
the result of ``op(NA, other)`` will be ``NA``. There are a few special cases when the
45+
result is known, even when one of the operands is ``NA``.
46+
47+
* ``pd.NA ** 0`` is always 0.
48+
* ``1 ** pd.NA`` is always 1.
49+
50+
In logical operations, :class:`NA` uses Kleene logic.
51+
52+
Creating Arrays
53+
---------------
54+
3155
Pandas and third-party libraries can extend NumPy's type system (see :ref:`extending.extension-types`).
3256
The top-level :meth:`array` method can be used to create a new array, which may be
3357
stored in a :class:`Series`, :class:`Index`, or as a column in a :class:`DataFrame`.

pandas/_libs/missing.pyx

+23-2
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,6 @@ class NAType(C_NAType):
365365
__rmod__ = _create_binary_propagating_op("__rmod__")
366366
__divmod__ = _create_binary_propagating_op("__divmod__", divmod=True)
367367
__rdivmod__ = _create_binary_propagating_op("__rdivmod__", divmod=True)
368-
__pow__ = _create_binary_propagating_op("__pow__")
369-
__rpow__ = _create_binary_propagating_op("__rpow__")
370368
# __lshift__ and __rshift__ are not implemented
371369

372370
__eq__ = _create_binary_propagating_op("__eq__")
@@ -383,6 +381,29 @@ class NAType(C_NAType):
383381
__abs__ = _create_unary_propagating_op("__abs__")
384382
__invert__ = _create_unary_propagating_op("__invert__")
385383

384+
# pow has special
385+
def __pow__(self, other):
386+
if other is C_NA:
387+
return NA
388+
elif isinstance(other, (numbers.Number, np.bool_)):
389+
if other == 0:
390+
return other
391+
else:
392+
return NA
393+
394+
return NotImplemented
395+
396+
def __rpow__(self, other):
397+
if other is C_NA:
398+
return NA
399+
elif isinstance(other, (numbers.Number, np.bool_)):
400+
if other == 1:
401+
return other
402+
else:
403+
return NA
404+
405+
return NotImplemented
406+
386407
# Logical ops using Kleene logic
387408

388409
def __and__(self, other):

pandas/tests/scalar/test_na_scalar.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ def test_arithmetic_ops(all_arithmetic_functions):
3838
op = all_arithmetic_functions
3939

4040
for other in [NA, 1, 1.0, "a", np.int64(1), np.nan]:
41-
if op.__name__ == "rmod" and isinstance(other, str):
41+
if op.__name__ in ("pow", "rpow", "rmod") and isinstance(other, str):
4242
continue
4343
if op.__name__ in ("divmod", "rdivmod"):
4444
assert op(NA, other) is (NA, NA)
4545
else:
46+
if op.__name__ == "rpow":
47+
# avoid special case
48+
other += 1
4649
assert op(NA, other) is NA
4750

4851

@@ -69,6 +72,25 @@ def test_comparison_ops():
6972
assert (other <= NA) is NA
7073

7174

75+
@pytest.mark.parametrize(
76+
"value", [0, 0.0, False, np.bool_(False), np.int_(0), np.float_(0)]
77+
)
78+
def test_pow_special(value):
79+
result = pd.NA ** value
80+
assert isinstance(result, type(value))
81+
assert result == 0
82+
83+
84+
@pytest.mark.parametrize(
85+
"value", [1, 1.0, True, np.bool_(True), np.int_(1), np.float_(1)]
86+
)
87+
def test_rpow_special(value):
88+
result = value ** pd.NA
89+
assert result == 1
90+
if not isinstance(value, (np.float_, np.bool_, np.int_)):
91+
assert isinstance(result, type(value))
92+
93+
7294
def test_unary_ops():
7395
assert +NA is NA
7496
assert -NA is NA

0 commit comments

Comments
 (0)