Skip to content

Commit 2ecbd58

Browse files
jbrockmendelclaude
andcommitted
BUG: distinguish bool from int in object-dtype hash table (#62888)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ea6c5b8 commit 2ecbd58

5 files changed

Lines changed: 132 additions & 0 deletions

File tree

doc/source/whatsnew/v3.1.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Performance improvements
116116

117117
Bug fixes
118118
~~~~~~~~~
119+
- Bug in object-dtype hash table operations (``factorize``, ``unique``, ``duplicated``, ``isin``, ``value_counts``, ``groupby``, ``Index.get_loc``) not distinguishing between ``int`` and ``bool`` values, e.g. treating ``0`` and ``False`` as equal (:issue:`62888`)
119120
- Fix bug in :func:`to_datetime` that could give an unnecessary ``RuntimeWarning`` when converting DataFrame containing missing values (:issue:`64141`)
120121

121122
Categorical

pandas/_libs/include/pandas/vendored/klib/khash_python.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ static inline int pyobject_cmp(PyObject *a, PyObject *b) {
211211
return tupleobject_cmp((PyTupleObject *)a, (PyTupleObject *)b);
212212
}
213213
// frozenset isn't yet supported
214+
} else if (PyBool_Check(a) != PyBool_Check(b)) {
215+
// GH#62888: distinguish bool from int, e.g. 0 vs False, 1 vs True
216+
return 0;
214217
}
215218

216219
int result = PyObject_RichCompareBool(a, b, Py_EQ);

pandas/tests/groupby/test_groupby.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,3 +3030,20 @@ def test_groupby_function_tuple_1677():
30303030

30313031
result = monthly_group.mean()
30323032
assert isinstance(result.index[0], tuple)
3033+
3034+
3035+
def test_groupby_bool_int_distinguished():
3036+
# GH#62888 - groupby on object dtype should distinguish bool from int
3037+
df = DataFrame(
3038+
{
3039+
"key": np.array([0, False, 0, False, 1, True], dtype=object),
3040+
"val": [1, 2, 3, 4, 5, 6],
3041+
}
3042+
)
3043+
result = df.groupby("key")["val"].sum()
3044+
expected = Series(
3045+
[4, 6, 5, 6],
3046+
index=Index([0, False, 1, True], dtype=object, name="key"),
3047+
name="val",
3048+
)
3049+
tm.assert_series_equal(result, expected)

pandas/tests/indexes/object/test_indexing.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,31 @@ def test_get_indexer_infer_string_missing_values(self):
7272
tm.assert_numpy_array_equal(result, expected)
7373

7474

75+
class TestGetIndexerBoolInt:
76+
def test_get_indexer_bool_int_distinguished(self):
77+
# GH#62888 - get_indexer should not match int 0 with bool False
78+
index = Index([0, 1, 2], dtype=object)
79+
target = Index([False, True], dtype=object)
80+
result = index.get_indexer(target)
81+
expected = np.array([-1, -1], dtype=np.intp)
82+
tm.assert_numpy_array_equal(result, expected)
83+
84+
def test_get_loc_bool_int_distinguished(self):
85+
# GH#62888
86+
index = Index([False, True, 2], dtype=object)
87+
assert index.get_loc(False) == 0
88+
assert index.get_loc(True) == 1
89+
with pytest.raises(KeyError, match="0"):
90+
index.get_loc(0)
91+
with pytest.raises(KeyError, match="1"):
92+
index.get_loc(1)
93+
94+
def test_is_unique_bool_int(self):
95+
# GH#62888 - Index with both int and bool should be unique
96+
index = Index([0, 1, False, True], dtype=object)
97+
assert index.is_unique
98+
99+
75100
class TestGetIndexerNonUnique:
76101
def test_get_indexer_non_unique_nas(self, nulls_fixture):
77102
# even though this isn't non-unique, this should still work

pandas/tests/test_algos.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,42 @@ def test_factorize_interval_non_nano(self, unit):
548548
codes3, cats3 = idx3.factorize()
549549
assert cats3.dtype == f"interval[datetime64[{unit}, US/Pacific], right]"
550550

551+
def test_factorize_bool_int_distinguished(self):
552+
# GH#62888 - factorize on object dtype should distinguish bool from int
553+
ser = Series([0, 1, True, False], dtype=object)
554+
codes, uniques = ser.factorize()
555+
556+
expected_codes = np.array([0, 1, 2, 3], dtype=np.intp)
557+
expected_uniques = Index([0, 1, True, False], dtype=object)
558+
tm.assert_numpy_array_equal(codes, expected_codes)
559+
tm.assert_index_equal(uniques, expected_uniques, exact=True)
560+
561+
# Check that the actual types are preserved, not just values
562+
assert type(uniques[0]) is int
563+
assert type(uniques[1]) is int
564+
assert type(uniques[2]) is bool
565+
assert type(uniques[3]) is bool
566+
567+
def test_factorize_bool_int_distinguished_reverse(self):
568+
# GH#62888 - order should not matter
569+
ser = Series([True, False, 0, 1], dtype=object)
570+
codes, uniques = ser.factorize()
571+
572+
expected_codes = np.array([0, 1, 2, 3], dtype=np.intp)
573+
expected_uniques = Index([True, False, 0, 1], dtype=object)
574+
tm.assert_numpy_array_equal(codes, expected_codes)
575+
tm.assert_index_equal(uniques, expected_uniques, exact=True)
576+
577+
def test_factorize_bool_int_with_duplicates(self):
578+
# GH#62888
579+
ser = Series([0, True, 0, False, 1, True], dtype=object)
580+
codes, uniques = ser.factorize()
581+
582+
expected_codes = np.array([0, 1, 0, 2, 3, 1], dtype=np.intp)
583+
expected_uniques = Index([0, True, False, 1], dtype=object)
584+
tm.assert_numpy_array_equal(codes, expected_codes)
585+
tm.assert_index_equal(uniques, expected_uniques, exact=True)
586+
551587

552588
class TestUnique:
553589
def test_ints(self):
@@ -582,6 +618,17 @@ def test_index_returned(self, index):
582618
expected = expected.normalize()
583619
tm.assert_index_equal(result, expected, exact=True)
584620

621+
def test_unique_bool_int_distinguished(self):
622+
# GH#18111, GH#62888
623+
arr = np.array([0, False, 1, True], dtype=object)
624+
result = algos.unique(arr)
625+
expected = np.array([0, False, 1, True], dtype=object)
626+
tm.assert_numpy_array_equal(result, expected)
627+
assert type(result[0]) is int
628+
assert type(result[1]) is bool
629+
assert type(result[2]) is int
630+
assert type(result[3]) is bool
631+
585632
def test_factorize_multiindex_empty(self):
586633
# GH#57517
587634
mi = MultiIndex.from_product(
@@ -1207,6 +1254,22 @@ def test_isin_unsigned_dtype(self):
12071254
expected = Series(False)
12081255
tm.assert_series_equal(result, expected)
12091256

1257+
def test_isin_bool_int_distinguished(self):
1258+
# GH#62888
1259+
ser = Series([0, 1, False, True], dtype=object)
1260+
1261+
result = algos.isin(ser, [0])
1262+
expected = np.array([True, False, False, False])
1263+
tm.assert_numpy_array_equal(result, expected)
1264+
1265+
result = algos.isin(ser, [False])
1266+
expected = np.array([False, False, True, False])
1267+
tm.assert_numpy_array_equal(result, expected)
1268+
1269+
result = algos.isin(ser, [0, False])
1270+
expected = np.array([True, False, True, False])
1271+
tm.assert_numpy_array_equal(result, expected)
1272+
12101273

12111274
class TestValueCounts:
12121275
def test_value_counts(self):
@@ -1446,6 +1509,17 @@ def test_value_counts_series(self):
14461509
)
14471510
tm.assert_series_equal(result, expected)
14481511

1512+
def test_value_counts_bool_int_distinguished(self):
1513+
# GH#62888
1514+
ser = Series([0, False, 0, True, 1, False], dtype=object)
1515+
result = ser.value_counts()
1516+
expected = Series(
1517+
[2, 2, 1, 1],
1518+
index=Index([0, False, True, 1], dtype=object),
1519+
name="count",
1520+
)
1521+
tm.assert_series_equal(result, expected)
1522+
14491523
def test_value_counts_stability(self):
14501524
# GH 63155
14511525
arr = np.random.default_rng(2).integers(0, 32, 64)
@@ -1461,6 +1535,18 @@ def test_value_counts_stability(self):
14611535

14621536

14631537
class TestDuplicated:
1538+
def test_duplicated_bool_int_distinguished(self):
1539+
# GH#62888
1540+
keys = np.array([0, False, 1, True, 0, False], dtype=object)
1541+
1542+
result = algos.duplicated(keys)
1543+
expected = np.array([False, False, False, False, True, True])
1544+
tm.assert_numpy_array_equal(result, expected)
1545+
1546+
result = algos.duplicated(keys, keep="last")
1547+
expected = np.array([True, True, False, False, False, False])
1548+
tm.assert_numpy_array_equal(result, expected)
1549+
14641550
def test_duplicated_with_nas(self):
14651551
keys = np.array([0, 1, np.nan, 0, 2, np.nan], dtype=object)
14661552

0 commit comments

Comments
 (0)