Skip to content

Commit b37ee68

Browse files
BUG: Series/Frame invert dtypes (pandas-dev#31183) (pandas-dev#31493)
Co-authored-by: Tom Augspurger <[email protected]>
1 parent 8f67bad commit b37ee68

File tree

9 files changed

+73
-3
lines changed

9 files changed

+73
-3
lines changed

doc/source/whatsnew/v1.0.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,7 @@ Numeric
11081108
- Bug in :meth:`DataFrame.round` where a :class:`DataFrame` with a :class:`CategoricalIndex` of :class:`IntervalIndex` columns would incorrectly raise a ``TypeError`` (:issue:`30063`)
11091109
- Bug in :meth:`Series.pct_change` and :meth:`DataFrame.pct_change` when there are duplicated indices (:issue:`30463`)
11101110
- Bug in :class:`DataFrame` cumulative operations (e.g. cumsum, cummax) incorrect casting to object-dtype (:issue:`19296`)
1111+
- Bug in dtypes being lost in ``DataFrame.__invert__`` (``~`` operator) with mixed dtypes (:issue:`31183`)
11111112
- Bug in :class:`~DataFrame.diff` losing the dtype for extension types (:issue:`30889`)
11121113
- Bug in :class:`DataFrame.diff` raising an ``IndexError`` when one of the columns was a nullable integer dtype (:issue:`30967`)
11131114

@@ -1260,6 +1261,7 @@ ExtensionArray
12601261
- Bug in :class:`arrays.PandasArray` when setting a scalar string (:issue:`28118`, :issue:`28150`).
12611262
- Bug where nullable integers could not be compared to strings (:issue:`28930`)
12621263
- Bug where :class:`DataFrame` constructor raised ``ValueError`` with list-like data and ``dtype`` specified (:issue:`30280`)
1264+
- Bug in dtype being lost in ``__invert__`` (``~`` operator) for extension-array backed ``Series`` and ``DataFrame`` (:issue:`23087`)
12631265

12641266

12651267
Other

pandas/core/arrays/masked.py

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def __iter__(self):
4848
def __len__(self) -> int:
4949
return len(self._data)
5050

51+
def __invert__(self):
52+
return type(self)(~self._data, self._mask)
53+
5154
def to_numpy(
5255
self, dtype=None, copy=False, na_value: "Scalar" = lib.no_default,
5356
):

pandas/core/generic.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1470,8 +1470,9 @@ def __invert__(self):
14701470
# inv fails with 0 len
14711471
return self
14721472

1473-
arr = operator.inv(com.values_from_object(self))
1474-
return self.__array_wrap__(arr)
1473+
new_data = self._data.apply(operator.invert)
1474+
result = self._constructor(new_data).__finalize__(self)
1475+
return result
14751476

14761477
def __nonzero__(self):
14771478
raise ValueError(

pandas/tests/arrays/sparse/test_arithmetics.py

+8
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,14 @@ def test_invert(fill_value):
476476
expected = SparseArray(~arr, fill_value=not fill_value)
477477
tm.assert_sp_array_equal(result, expected)
478478

479+
result = ~pd.Series(sparray)
480+
expected = pd.Series(expected)
481+
tm.assert_series_equal(result, expected)
482+
483+
result = ~pd.DataFrame({"A": sparray})
484+
expected = pd.DataFrame({"A": expected})
485+
tm.assert_frame_equal(result, expected)
486+
479487

480488
@pytest.mark.parametrize("fill_value", [0, np.nan])
481489
@pytest.mark.parametrize("op", [operator.pos, operator.neg])

pandas/tests/arrays/test_boolean.py

+18
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,24 @@ def test_ufunc_reduce_raises(values):
471471
np.add.reduce(a)
472472

473473

474+
class TestUnaryOps:
475+
def test_invert(self):
476+
a = pd.array([True, False, None], dtype="boolean")
477+
expected = pd.array([False, True, None], dtype="boolean")
478+
tm.assert_extension_array_equal(~a, expected)
479+
480+
expected = pd.Series(expected, index=["a", "b", "c"], name="name")
481+
result = ~pd.Series(a, index=["a", "b", "c"], name="name")
482+
tm.assert_series_equal(result, expected)
483+
484+
df = pd.DataFrame({"A": a, "B": [True, False, False]}, index=["a", "b", "c"])
485+
result = ~df
486+
expected = pd.DataFrame(
487+
{"A": expected, "B": [False, True, True]}, index=["a", "b", "c"]
488+
)
489+
tm.assert_frame_equal(result, expected)
490+
491+
474492
class TestLogicalOps(BaseOpsUtil):
475493
def test_numpy_scalars_ok(self, all_logical_operators):
476494
a = pd.array([True, False, None], dtype="boolean")

pandas/tests/extension/base/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ class TestMyDtype(BaseDtypeTests):
4949
from .io import BaseParsingTests # noqa
5050
from .methods import BaseMethodsTests # noqa
5151
from .missing import BaseMissingTests # noqa
52-
from .ops import BaseArithmeticOpsTests, BaseComparisonOpsTests, BaseOpsUtil # noqa
52+
from .ops import ( # noqa
53+
BaseArithmeticOpsTests,
54+
BaseComparisonOpsTests,
55+
BaseOpsUtil,
56+
BaseUnaryOpsTests,
57+
)
5358
from .printing import BasePrintingTests # noqa
5459
from .reduce import ( # noqa
5560
BaseBooleanReduceTests,

pandas/tests/extension/base/ops.py

+8
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,11 @@ def test_direct_arith_with_series_returns_not_implemented(self, data):
168168
assert result is NotImplemented
169169
else:
170170
raise pytest.skip(f"{type(data).__name__} does not implement __eq__")
171+
172+
173+
class BaseUnaryOpsTests(BaseOpsUtil):
174+
def test_invert(self, data):
175+
s = pd.Series(data, name="name")
176+
result = ~s
177+
expected = pd.Series(~data, name="name")
178+
self.assert_series_equal(result, expected)

pandas/tests/extension/test_boolean.py

+4
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,10 @@ class TestPrinting(base.BasePrintingTests):
342342
pass
343343

344344

345+
class TestUnaryOps(base.BaseUnaryOpsTests):
346+
pass
347+
348+
345349
# TODO parsing not yet supported
346350
# class TestParsing(base.BaseParsingTests):
347351
# pass

pandas/tests/frame/test_operators.py

+21
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ def test_invert(self, float_frame):
6161

6262
tm.assert_frame_equal(-(df < 0), ~(df < 0))
6363

64+
def test_invert_mixed(self):
65+
shape = (10, 5)
66+
df = pd.concat(
67+
[
68+
pd.DataFrame(np.zeros(shape, dtype="bool")),
69+
pd.DataFrame(np.zeros(shape, dtype=int)),
70+
],
71+
axis=1,
72+
ignore_index=True,
73+
)
74+
result = ~df
75+
expected = pd.concat(
76+
[
77+
pd.DataFrame(np.ones(shape, dtype="bool")),
78+
pd.DataFrame(-np.ones(shape, dtype=int)),
79+
],
80+
axis=1,
81+
ignore_index=True,
82+
)
83+
tm.assert_frame_equal(result, expected)
84+
6485
@pytest.mark.parametrize(
6586
"df",
6687
[

0 commit comments

Comments
 (0)