Skip to content

Commit

Permalink
BUG: Series/Index arithmetic result names with NAs (pandas-dev#44459)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbrockmendel authored Nov 20, 2021
1 parent 491b3a4 commit 7b8c0af
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 8 deletions.
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ Numeric
- Bug in ``numexpr`` engine still being used when the option ``compute.use_numexpr`` is set to ``False`` (:issue:`32556`)
- Bug in :class:`DataFrame` arithmetic ops with a subclass whose :meth:`_constructor` attribute is a callable other than the subclass itself (:issue:`43201`)
- Bug in arithmetic operations involving :class:`RangeIndex` where the result would have the incorrect ``name`` (:issue:`43962`)
- Bug in arithmetic operations involving :class:`Series` where the result could have the incorrect ``name`` when the operands having matching NA or matching tuple names (:issue:`44459`)
-

Conversion
Expand Down
5 changes: 5 additions & 0 deletions pandas/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1656,6 +1656,11 @@ def __init__(self, **kwargs):
("foo", None, None),
("Egon", "Venkman", None),
("NCC1701D", "NCC1701D", "NCC1701D"),
# possibly-matching NAs
(np.nan, np.nan, np.nan),
(np.nan, pd.NaT, None),
(np.nan, pd.NA, None),
(pd.NA, pd.NA, pd.NA),
]
)
def names(request):
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2950,7 +2950,7 @@ def _get_reconciled_name_object(self, other):
case make a shallow copy of self.
"""
name = get_op_result_name(self, other)
if self.name != name:
if self.name is not name:
return self.rename(name)
return self

Expand Down
20 changes: 16 additions & 4 deletions pandas/core/ops/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Callable

from pandas._libs.lib import item_from_zerodim
from pandas._libs.missing import is_matching_na
from pandas._typing import F

from pandas.core.dtypes.generic import (
Expand Down Expand Up @@ -116,10 +117,21 @@ def _maybe_match_name(a, b):
a_has = hasattr(a, "name")
b_has = hasattr(b, "name")
if a_has and b_has:
if a.name == b.name:
return a.name
else:
# TODO: what if they both have np.nan for their names?
try:
if a.name == b.name:
return a.name
elif is_matching_na(a.name, b.name):
# e.g. both are np.nan
return a.name
else:
return None
except TypeError:
# pd.NA
if is_matching_na(a.name, b.name):
return a.name
return None
except ValueError:
# e.g. np.int64(1) vs (np.int64(1), np.int64(2))
return None
elif a_has:
return a.name
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/series/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,9 +789,9 @@ def test_series_ops_name_retention(

assert isinstance(result, Series)
if box in [Index, Series]:
assert result.name == names[2]
assert result.name is names[2] or result.name == names[2]
else:
assert result.name == names[0]
assert result.name is names[0] or result.name == names[0]

def test_binop_maybe_preserve_name(self, datetime_series):
# names match, preserve
Expand Down
26 changes: 25 additions & 1 deletion pandas/tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,34 @@ def test_random_state():
(Series([1], name="x"), Series([2]), None),
(Series([1], name="x"), [2], "x"),
([1], Series([2], name="y"), "y"),
# matching NAs
(Series([1], name=np.nan), pd.Index([], name=np.nan), np.nan),
(Series([1], name=np.nan), pd.Index([], name=pd.NaT), None),
(Series([1], name=pd.NA), pd.Index([], name=pd.NA), pd.NA),
# tuple name GH#39757
(
Series([1], name=np.int64(1)),
pd.Index([], name=(np.int64(1), np.int64(2))),
None,
),
(
Series([1], name=(np.int64(1), np.int64(2))),
pd.Index([], name=(np.int64(1), np.int64(2))),
(np.int64(1), np.int64(2)),
),
pytest.param(
Series([1], name=(np.float64("nan"), np.int64(2))),
pd.Index([], name=(np.float64("nan"), np.int64(2))),
(np.float64("nan"), np.int64(2)),
marks=pytest.mark.xfail(
reason="Not checking for matching NAs inside tuples."
),
),
],
)
def test_maybe_match_name(left, right, expected):
assert ops.common._maybe_match_name(left, right) == expected
res = ops.common._maybe_match_name(left, right)
assert res is expected or res == expected


def test_standardize_mapping():
Expand Down

0 comments on commit 7b8c0af

Please sign in to comment.