diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 3829306f77a0b..4a11028950966 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -37,6 +37,7 @@ Other enhancements - :meth:`to_numeric` now preserves float64 arrays when downcasting would generate values not representable in float32 (:issue:`43693`) - :meth:`Series.reset_index` and :meth:`DataFrame.reset_index` now support the argument ``allow_duplicates`` (:issue:`44410`) - :meth:`.GroupBy.min` and :meth:`.GroupBy.max` now supports `Numba `_ execution with the ``engine`` keyword (:issue:`45428`) +- Implemented a ``bool``-dtype :class:`Index`, passing a bool-dtype arraylike to ``pd.Index`` will now retain ``bool`` dtype instead of casting to ``object`` (:issue:`45061`) - .. --------------------------------------------------------------------------- diff --git a/pandas/_libs/index.pyi b/pandas/_libs/index.pyi index 86f2429575ebb..184bad1c080b3 100644 --- a/pandas/_libs/index.pyi +++ b/pandas/_libs/index.pyi @@ -42,6 +42,7 @@ class ObjectEngine(IndexEngine): ... class DatetimeEngine(Int64Engine): ... class TimedeltaEngine(DatetimeEngine): ... class PeriodEngine(Int64Engine): ... +class BoolEngine(UInt8Engine): ... class BaseMultiIndexCodesEngine: levels: list[np.ndarray] diff --git a/pandas/_libs/index.pyx b/pandas/_libs/index.pyx index d8311282d1193..0e9a330587f07 100644 --- a/pandas/_libs/index.pyx +++ b/pandas/_libs/index.pyx @@ -802,6 +802,13 @@ cdef class BaseMultiIndexCodesEngine: include "index_class_helper.pxi" +cdef class BoolEngine(UInt8Engine): + cdef _check_type(self, object val): + if not util.is_bool_object(val): + raise KeyError(val) + return val + + @cython.internal @cython.freelist(32) cdef class SharedEngine: diff --git a/pandas/conftest.py b/pandas/conftest.py index ba90c9eedb53c..a9c8ab833b40f 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -555,7 +555,8 @@ def _create_mi_with_dt64tz_level(): "num_uint8": tm.makeNumericIndex(100, dtype="uint8"), "num_float64": tm.makeNumericIndex(100, dtype="float64"), "num_float32": tm.makeNumericIndex(100, dtype="float32"), - "bool": tm.makeBoolIndex(10), + "bool-object": tm.makeBoolIndex(10).astype(object), + "bool-dtype": Index(np.random.randn(10) < 0), "categorical": tm.makeCategoricalIndex(100), "interval": tm.makeIntervalIndex(100), "empty": Index([]), @@ -630,7 +631,7 @@ def index_flat_unique(request): key for key in indices_dict if not ( - key in ["int", "uint", "range", "empty", "repeats"] + key in ["int", "uint", "range", "empty", "repeats", "bool-dtype"] or key.startswith("num_") ) and not isinstance(indices_dict[key], MultiIndex) diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index 36eabe93dbd7e..ddb8e19d1c558 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -220,9 +220,6 @@ def _reconstruct_data( elif is_bool_dtype(dtype): values = values.astype(dtype, copy=False) - # we only support object dtypes bool Index - if isinstance(original, ABCIndex): - values = values.astype(object, copy=False) elif dtype is not None: if is_datetime64_dtype(dtype): dtype = np.dtype("datetime64[ns]") @@ -830,7 +827,10 @@ def value_counts( ------- Series """ - from pandas.core.series import Series + from pandas import ( + Index, + Series, + ) name = getattr(values, "name", None) @@ -868,7 +868,13 @@ def value_counts( else: keys, counts = value_counts_arraylike(values, dropna) - result = Series(counts, index=keys, name=name) + # For backwards compatibility, we let Index do its normal type + # inference, _except_ for if if infers from object to bool. + idx = Index._with_infer(keys) + if idx.dtype == bool and keys.dtype == object: + idx = idx.astype(object) + + result = Series(counts, index=idx, name=name) if sort: result = result.sort_values(ascending=ascending) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index f6feec5fd97a6..847c5a607c086 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -505,6 +505,10 @@ def __new__( if data.dtype.kind in ["i", "u", "f"]: # maybe coerce to a sub-class arr = data + elif data.dtype.kind == "b": + # No special subclass, and Index._ensure_array won't do this + # for us. + arr = np.asarray(data) else: arr = com.asarray_tuplesafe(data, dtype=_dtype_obj) @@ -702,7 +706,7 @@ def _with_infer(cls, *args, **kwargs): # "Union[ExtensionArray, ndarray[Any, Any]]"; expected # "ndarray[Any, Any]" values = lib.maybe_convert_objects(result._values) # type: ignore[arg-type] - if values.dtype.kind in ["i", "u", "f"]: + if values.dtype.kind in ["i", "u", "f", "b"]: return Index(values, name=result.name) return result @@ -872,9 +876,12 @@ def _engine( ): return libindex.ExtensionEngine(target_values) + target_values = cast(np.ndarray, target_values) # to avoid a reference cycle, bind `target_values` to a local variable, so # `self` is not passed into the lambda. - target_values = cast(np.ndarray, target_values) + if target_values.dtype == bool: + return libindex.BoolEngine(target_values) + # error: Argument 1 to "ExtensionEngine" has incompatible type # "ndarray[Any, Any]"; expected "ExtensionArray" return self._engine_type(target_values) # type:ignore[arg-type] @@ -2680,7 +2687,6 @@ def _is_all_dates(self) -> bool: """ Whether or not the index values only consist of dates. """ - if needs_i8_conversion(self.dtype): return True elif self.dtype != _dtype_obj: @@ -7302,7 +7308,7 @@ def _maybe_cast_data_without_dtype( FutureWarning, stacklevel=3, ) - if result.dtype.kind in ["b", "c"]: + if result.dtype.kind in ["c"]: return subarr result = ensure_wrapped_if_datetimelike(result) return result diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py index 4d9420fc0510d..3f0a5deb97548 100644 --- a/pandas/core/tools/datetimes.py +++ b/pandas/core/tools/datetimes.py @@ -1076,6 +1076,8 @@ def to_datetime( result = convert_listlike(arg, format) else: result = convert_listlike(np.array([arg]), format)[0] + if isinstance(arg, bool) and isinstance(result, np.bool_): + result = bool(result) # TODO: avoid this kludge. # error: Incompatible return value type (got "Union[Timestamp, NaTType, # Series, Index]", expected "Union[DatetimeIndex, Series, float, str, diff --git a/pandas/core/util/hashing.py b/pandas/core/util/hashing.py index 892fa83f98755..79db60ab5a7ce 100644 --- a/pandas/core/util/hashing.py +++ b/pandas/core/util/hashing.py @@ -319,7 +319,7 @@ def _hash_ndarray( # First, turn whatever array this is into unsigned 64-bit ints, if we can # manage it. - elif isinstance(dtype, bool): + elif dtype == bool: vals = vals.astype("u8") elif issubclass(dtype.type, (np.datetime64, np.timedelta64)): vals = vals.view("i8").astype("u8", copy=False) diff --git a/pandas/tests/base/test_value_counts.py b/pandas/tests/base/test_value_counts.py index 13bf096cfe167..c46f1b036dbee 100644 --- a/pandas/tests/base/test_value_counts.py +++ b/pandas/tests/base/test_value_counts.py @@ -284,7 +284,7 @@ def test_value_counts_with_nan(dropna, index_or_series): obj = klass(values) res = obj.value_counts(dropna=dropna) if dropna is True: - expected = Series([1], index=[True]) + expected = Series([1], index=Index([True], dtype=obj.dtype)) else: expected = Series([1, 1, 1], index=[True, pd.NA, np.nan]) tm.assert_series_equal(res, expected) diff --git a/pandas/tests/indexes/common.py b/pandas/tests/indexes/common.py index 0cf66c0814293..3f8c679c6162f 100644 --- a/pandas/tests/indexes/common.py +++ b/pandas/tests/indexes/common.py @@ -216,6 +216,8 @@ def test_ensure_copied_data(self, index): # RangeIndex cannot be initialized from data # MultiIndex and CategoricalIndex are tested separately return + elif index.dtype == object and index.inferred_type == "boolean": + init_kwargs["dtype"] = index.dtype index_type = type(index) result = index_type(index.values, copy=True, **init_kwargs) @@ -522,6 +524,9 @@ def test_fillna(self, index): # GH 11343 if len(index) == 0: return + elif index.dtype == bool: + # can't hold NAs + return elif isinstance(index, NumericIndex) and is_integer_dtype(index.dtype): return elif isinstance(index, MultiIndex): diff --git a/pandas/tests/indexes/multi/test_indexing.py b/pandas/tests/indexes/multi/test_indexing.py index 599455b2d2ba1..9626352ac7e36 100644 --- a/pandas/tests/indexes/multi/test_indexing.py +++ b/pandas/tests/indexes/multi/test_indexing.py @@ -621,13 +621,22 @@ def test_get_loc_implicit_cast(self, level, dtypes): idx = MultiIndex.from_product(levels) assert idx.get_loc(tuple(key)) == 3 - def test_get_loc_cast_bool(self): - # GH 19086 : int is casted to bool, but not vice-versa - levels = [[False, True], np.arange(2, dtype="int64")] + @pytest.mark.parametrize("dtype", [bool, object]) + def test_get_loc_cast_bool(self, dtype): + # GH 19086 : int is casted to bool, but not vice-versa (for object dtype) + # With bool dtype, we don't cast in either direction. + levels = [Index([False, True], dtype=dtype), np.arange(2, dtype="int64")] idx = MultiIndex.from_product(levels) - assert idx.get_loc((0, 1)) == 1 - assert idx.get_loc((1, 0)) == 2 + if dtype is bool: + with pytest.raises(KeyError, match=r"^\(0, 1\)$"): + assert idx.get_loc((0, 1)) == 1 + with pytest.raises(KeyError, match=r"^\(1, 0\)$"): + assert idx.get_loc((1, 0)) == 2 + else: + # We use python object comparisons, which treat 0 == False and 1 == True + assert idx.get_loc((0, 1)) == 1 + assert idx.get_loc((1, 0)) == 2 with pytest.raises(KeyError, match=r"^\(False, True\)$"): idx.get_loc((False, True)) diff --git a/pandas/tests/indexes/test_any_index.py b/pandas/tests/indexes/test_any_index.py index c7aae5d69b8e3..8f0d1179a99c1 100644 --- a/pandas/tests/indexes/test_any_index.py +++ b/pandas/tests/indexes/test_any_index.py @@ -49,6 +49,10 @@ def test_mutability(index): def test_map_identity_mapping(index): # GH#12766 result = index.map(lambda x: x) + if index.dtype == object and result.dtype == bool: + assert (index == result).all() + # TODO: could work that into the 'exact="equiv"'? + return # FIXME: doesn't belong in this file anymore! tm.assert_index_equal(result, index, exact="equiv") diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index b2b902bd816c8..9df759588033f 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -321,15 +321,21 @@ def test_view_with_args(self, index): "unicode", "string", pytest.param("categorical", marks=pytest.mark.xfail(reason="gh-25464")), - "bool", + "bool-object", + "bool-dtype", "empty", ], indirect=True, ) def test_view_with_args_object_array_raises(self, index): - msg = "Cannot change data-type for object array" - with pytest.raises(TypeError, match=msg): - index.view("i8") + if index.dtype == bool: + msg = "When changing to a larger dtype" + with pytest.raises(ValueError, match=msg): + index.view("i8") + else: + msg = "Cannot change data-type for object array" + with pytest.raises(TypeError, match=msg): + index.view("i8") @pytest.mark.parametrize("index", ["int", "range"], indirect=True) def test_astype(self, index): @@ -397,9 +403,9 @@ def test_is_(self): def test_asof_numeric_vs_bool_raises(self): left = Index([1, 2, 3]) - right = Index([True, False]) + right = Index([True, False], dtype=object) - msg = "Cannot compare dtypes int64 and object" + msg = "Cannot compare dtypes int64 and bool" with pytest.raises(TypeError, match=msg): left.asof(right[0]) # TODO: should right.asof(left[0]) also raise? @@ -591,7 +597,8 @@ def test_append_empty_preserve_name(self, name, expected): "index, expected", [ ("string", False), - ("bool", False), + ("bool-object", False), + ("bool-dtype", False), ("categorical", False), ("int", True), ("datetime", False), @@ -606,7 +613,8 @@ def test_is_numeric(self, index, expected): "index, expected", [ ("string", True), - ("bool", True), + ("bool-object", True), + ("bool-dtype", False), ("categorical", False), ("int", False), ("datetime", False), @@ -621,7 +629,8 @@ def test_is_object(self, index, expected): "index, expected", [ ("string", False), - ("bool", False), + ("bool-object", False), + ("bool-dtype", False), ("categorical", False), ("int", False), ("datetime", True), diff --git a/pandas/tests/indexes/test_common.py b/pandas/tests/indexes/test_common.py index 6159c53ea5bf4..ce5166aa8cf0b 100644 --- a/pandas/tests/indexes/test_common.py +++ b/pandas/tests/indexes/test_common.py @@ -88,7 +88,9 @@ def test_constructor_non_hashable_name(self, index_flat): def test_constructor_unwraps_index(self, index_flat): a = index_flat - b = type(a)(a) + # Passing dtype is necessary for Index([True, False], dtype=object) + # case. + b = type(a)(a, dtype=a.dtype) tm.assert_equal(a._data, b._data) def test_to_flat_index(self, index_flat): @@ -426,6 +428,9 @@ def test_hasnans_isnans(self, index_flat): return elif isinstance(index, NumericIndex) and is_integer_dtype(index.dtype): return + elif index.dtype == bool: + # values[1] = np.nan below casts to True! + return values[1] = np.nan diff --git a/pandas/tests/indexes/test_index_new.py b/pandas/tests/indexes/test_index_new.py index 5f57e03ea9444..3052c9d7ee69b 100644 --- a/pandas/tests/indexes/test_index_new.py +++ b/pandas/tests/indexes/test_index_new.py @@ -74,7 +74,7 @@ def test_constructor_dtypes_to_object(self, cast_index, vals): index = Index(vals) assert type(index) is Index - assert index.dtype == object + assert index.dtype == bool def test_constructor_categorical_to_object(self): # GH#32167 Categorical data and dtype=object should return object-dtype diff --git a/pandas/tests/indexes/test_numpy_compat.py b/pandas/tests/indexes/test_numpy_compat.py index 8f3ecce223afa..7d46f6d18a318 100644 --- a/pandas/tests/indexes/test_numpy_compat.py +++ b/pandas/tests/indexes/test_numpy_compat.py @@ -68,8 +68,10 @@ def test_numpy_ufuncs_basic(index, func): with tm.external_error_raised((TypeError, AttributeError)): with np.errstate(all="ignore"): func(index) - elif isinstance(index, NumericIndex) or ( - not isinstance(index.dtype, np.dtype) and index.dtype._is_numeric + elif ( + isinstance(index, NumericIndex) + or (not isinstance(index.dtype, np.dtype) and index.dtype._is_numeric) + or index.dtype == bool ): # coerces to float (e.g. np.sin) with np.errstate(all="ignore"): @@ -77,7 +79,7 @@ def test_numpy_ufuncs_basic(index, func): exp = Index(func(index.values), name=index.name) tm.assert_index_equal(result, exp) - if type(index) is not Index: + if type(index) is not Index or index.dtype == bool: # i.e NumericIndex assert isinstance(result, Float64Index) else: @@ -117,8 +119,10 @@ def test_numpy_ufuncs_other(index, func): with tm.external_error_raised(TypeError): func(index) - elif isinstance(index, NumericIndex) or ( - not isinstance(index.dtype, np.dtype) and index.dtype._is_numeric + elif ( + isinstance(index, NumericIndex) + or (not isinstance(index.dtype, np.dtype) and index.dtype._is_numeric) + or index.dtype == bool ): # Results in bool array result = func(index) diff --git a/pandas/tests/indexes/test_setops.py b/pandas/tests/indexes/test_setops.py index bad75b7429efb..f4f572d8f79fc 100644 --- a/pandas/tests/indexes/test_setops.py +++ b/pandas/tests/indexes/test_setops.py @@ -9,7 +9,6 @@ import pytest from pandas.core.dtypes.cast import find_common_type -from pandas.core.dtypes.common import is_dtype_equal from pandas import ( CategoricalIndex, @@ -55,14 +54,20 @@ def test_union_different_types(index_flat, index_flat2, request): if ( not idx1.is_unique + and not idx2.is_unique + and not idx2.is_monotonic_decreasing and idx1.dtype.kind == "i" - and is_dtype_equal(idx2.dtype, "boolean") + and idx2.dtype.kind == "b" ) or ( not idx2.is_unique + and not idx1.is_unique + and not idx1.is_monotonic_decreasing and idx2.dtype.kind == "i" - and is_dtype_equal(idx1.dtype, "boolean") + and idx1.dtype.kind == "b" ): - mark = pytest.mark.xfail(reason="GH#44000 True==1", raises=ValueError) + mark = pytest.mark.xfail( + reason="GH#44000 True==1", raises=ValueError, strict=False + ) request.node.add_marker(mark) common_dtype = find_common_type([idx1.dtype, idx2.dtype]) @@ -231,7 +236,11 @@ def test_union_base(self, index): def test_difference_base(self, sort, index): first = index[2:] second = index[:4] - if isinstance(index, CategoricalIndex) or index.is_boolean(): + if index.is_boolean(): + # i think (TODO: be sure) there assumptions baked in about + # the index fixture that don't hold here? + answer = set(first).difference(set(second)) + elif isinstance(index, CategoricalIndex): answer = [] else: answer = index[4:] diff --git a/pandas/tests/reshape/concat/test_append_common.py b/pandas/tests/reshape/concat/test_append_common.py index 36bca1c2b654e..0a330fd12d76d 100644 --- a/pandas/tests/reshape/concat/test_append_common.py +++ b/pandas/tests/reshape/concat/test_append_common.py @@ -61,10 +61,7 @@ def _check_expected_dtype(self, obj, label): considering not-supported dtypes """ if isinstance(obj, Index): - if label == "bool": - assert obj.dtype == "object" - else: - assert obj.dtype == label + assert obj.dtype == label elif isinstance(obj, Series): if label.startswith("period"): assert obj.dtype == "Period[M]" @@ -185,7 +182,7 @@ def test_concatlike_same_dtypes(self, item): with pytest.raises(TypeError, match=msg): pd.concat([Series(vals1), Series(vals2), vals3]) - def test_concatlike_dtypes_coercion(self, item, item2): + def test_concatlike_dtypes_coercion(self, item, item2, request): # GH 13660 typ1, vals1 = item typ2, vals2 = item2 @@ -210,9 +207,13 @@ def test_concatlike_dtypes_coercion(self, item, item2): # series coerces to numeric based on numpy rule # index doesn't because bool is object dtype exp_series_dtype = typ2 + mark = pytest.mark.xfail(reason="GH#39187 casting to object") + request.node.add_marker(mark) warn = FutureWarning elif typ2 == "bool" and typ1 in ("int64", "float64"): exp_series_dtype = typ1 + mark = pytest.mark.xfail(reason="GH#39187 casting to object") + request.node.add_marker(mark) warn = FutureWarning elif ( typ1 == "datetime64[ns, US/Eastern]" @@ -229,7 +230,9 @@ def test_concatlike_dtypes_coercion(self, item, item2): # ----- Index ----- # # index.append - res = Index(vals1).append(Index(vals2)) + with tm.assert_produces_warning(warn, match="concatenating bool-dtype"): + # GH#39817 + res = Index(vals1).append(Index(vals2)) exp = Index(exp_data, dtype=exp_index_dtype) tm.assert_index_equal(res, exp) diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index a994c902f0b16..6a48d452f9624 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -758,26 +758,26 @@ def test_series_where(self, obj, key, expected, val, is_inplace): self._check_inplace(is_inplace, orig, arr, obj) def test_index_where(self, obj, key, expected, val): - if obj.dtype == bool or obj.dtype.kind == "c" or expected.dtype.kind == "c": - # TODO(GH#45061): Should become unreachable (at least the bool part) + if obj.dtype.kind == "c" or expected.dtype.kind == "c": + # TODO(Index[complex]): Should become unreachable pytest.skip("test not applicable for this dtype") mask = np.zeros(obj.shape, dtype=bool) mask[key] = True res = Index(obj).where(~mask, val) - tm.assert_index_equal(res, Index(expected)) + tm.assert_index_equal(res, Index(expected, dtype=expected.dtype)) def test_index_putmask(self, obj, key, expected, val): - if obj.dtype == bool or obj.dtype.kind == "c" or expected.dtype.kind == "c": - # TODO(GH#45061): Should become unreachable (at least the bool part) + if obj.dtype.kind == "c" or expected.dtype.kind == "c": + # TODO(Index[complex]): Should become unreachable pytest.skip("test not applicable for this dtype") mask = np.zeros(obj.shape, dtype=bool) mask[key] = True res = Index(obj).putmask(mask, val) - tm.assert_index_equal(res, Index(expected)) + tm.assert_index_equal(res, Index(expected, dtype=expected.dtype)) @pytest.mark.parametrize( diff --git a/pandas/tests/series/methods/test_drop.py b/pandas/tests/series/methods/test_drop.py index 59a60019bb1c1..a625e890393a6 100644 --- a/pandas/tests/series/methods/test_drop.py +++ b/pandas/tests/series/methods/test_drop.py @@ -54,7 +54,8 @@ def test_drop_with_ignore_errors(): # GH 8522 ser = Series([2, 3], index=[True, False]) - assert ser.index.is_object() + assert not ser.index.is_object() + assert ser.index.dtype == bool result = ser.drop(True) expected = Series([3], index=[False]) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/methods/test_value_counts.py b/pandas/tests/series/methods/test_value_counts.py index c914dba75dc35..1977bf88481a5 100644 --- a/pandas/tests/series/methods/test_value_counts.py +++ b/pandas/tests/series/methods/test_value_counts.py @@ -194,7 +194,7 @@ def test_value_counts_categorical_with_nan(self): ( Series([False, True, True, pd.NA]), True, - Series([2, 1], index=[True, False]), + Series([2, 1], index=pd.Index([True, False], dtype=object)), ), ( Series(range(3), index=[True, False, np.nan]).index, diff --git a/pandas/tests/series/test_logical_ops.py b/pandas/tests/series/test_logical_ops.py index 9648b01492e02..38e3c5ec8a6f2 100644 --- a/pandas/tests/series/test_logical_ops.py +++ b/pandas/tests/series/test_logical_ops.py @@ -268,7 +268,9 @@ def test_logical_ops_with_index(self, op): def test_reversed_xor_with_index_returns_index(self): # GH#22092, GH#19792 ser = Series([True, True, False, False]) - idx1 = Index([True, False, True, False]) + idx1 = Index( + [True, False, True, False], dtype=object + ) # TODO: raises if bool-dtype idx2 = Index([1, 0, 1, 0]) msg = "operating as a set operation" @@ -325,7 +327,7 @@ def test_reversed_logical_op_with_index_returns_series(self, op): [ (ops.rand_, Index([False, True])), (ops.ror_, Index([False, True])), - (ops.rxor, Index([])), + (ops.rxor, Index([], dtype=bool)), ], ) def test_reverse_ops_with_index(self, op, expected): diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 749ed1bb979e9..84e8d72711305 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -56,6 +56,12 @@ def test_factorize(self, index_or_series_obj, sort): if isinstance(obj, MultiIndex): constructor = MultiIndex.from_tuples expected_uniques = constructor(obj.unique()) + if ( + isinstance(obj, Index) + and expected_uniques.dtype == bool + and obj.dtype == object + ): + expected_uniques = expected_uniques.astype(object) if sort: expected_uniques = expected_uniques.sort_values() @@ -1240,7 +1246,7 @@ def test_dropna(self): tm.assert_series_equal( Series([True] * 3 + [False] * 2 + [None] * 5).value_counts(dropna=True), - Series([3, 2], index=[True, False]), + Series([3, 2], index=Index([True, False], dtype=object)), ) tm.assert_series_equal( Series([True] * 5 + [False] * 3 + [None] * 2).value_counts(dropna=False),