diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 5c53267158eab..c402f7b6d08b6 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -404,6 +404,7 @@ Other API changes - Index set operations (like union or intersection) will now ignore the dtype of an empty ``RangeIndex`` or empty ``Index`` with object dtype when determining the dtype of the resulting Index (:issue:`60797`) +- ``np_find_common_type`` will now return ``object`` for mixed ``int64`` and ``uint64`` dtypes to avoid precision lost (:issue:`61676`, :issue:`61688`) .. --------------------------------------------------------------------------- .. _whatsnew_300.deprecations: diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index 7fc391d3ffb51..0406252521e6a 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -47,7 +47,6 @@ is_bool_dtype, is_complex_dtype, is_dict_like, - is_dtype_equal, is_extension_array_dtype, is_float, is_float_dtype, @@ -55,7 +54,6 @@ is_integer_dtype, is_list_like, is_object_dtype, - is_signed_integer_dtype, needs_i8_conversion, ) from pandas.core.dtypes.concat import concat_compat @@ -508,16 +506,6 @@ def isin(comps: ListLike, values: ListLike) -> npt.NDArray[np.bool_]: orig_values = list(values) values = _ensure_arraylike(orig_values, func_name="isin-targets") - if ( - len(values) > 0 - and values.dtype.kind in "iufcb" - and not is_signed_integer_dtype(comps) - and not is_dtype_equal(values, comps) - ): - # GH#46485 Use object to avoid upcast to float64 later - # TODO: Share with _find_common_type_compat - values = construct_1d_object_array_from_listlike(orig_values) - elif isinstance(values, ABCMultiIndex): # Avoid raising in extract_array values = np.array(values) diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index dae04ba6244d4..2ff0561d53f23 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -1418,6 +1418,13 @@ def np_find_common_type(*dtypes: np.dtype) -> np.dtype: # so fall back to object (find_common_dtype did unless there # was only one dtype) common_dtype = np.dtype("O") + elif ( + # Some precision is lost with float64 when handling uint64/int64 + # Use object instead for the common type + all(np.dtype(x).kind in "iu" and np.dtype(x).itemsize == 8 for x in dtypes) + and common_dtype == np.float64 + ): + common_dtype = np.dtype("O") except TypeError: common_dtype = np.dtype("O") diff --git a/pandas/tests/arrays/integer/test_concat.py b/pandas/tests/arrays/integer/test_concat.py index feba574da548f..8fc5405be712e 100644 --- a/pandas/tests/arrays/integer/test_concat.py +++ b/pandas/tests/arrays/integer/test_concat.py @@ -14,7 +14,7 @@ (["Int8", "Int16"], "Int16"), (["UInt8", "Int8"], "Int16"), (["Int32", "UInt32"], "Int64"), - (["Int64", "UInt64"], "Float64"), + (["Int64", "UInt64"], "object"), (["Int64", "boolean"], "object"), (["UInt8", "boolean"], "object"), ], @@ -48,7 +48,7 @@ def test_concat_series(to_concat_dtypes, result_dtype): (["Int8", "int16"], "Int16"), (["UInt8", "int8"], "Int16"), (["Int32", "uint32"], "Int64"), - (["Int64", "uint64"], "Float64"), + (["Int64", "uint64"], "object"), (["Int64", "bool"], "object"), (["UInt8", "bool"], "object"), ], diff --git a/pandas/tests/dtypes/cast/test_find_common_type.py b/pandas/tests/dtypes/cast/test_find_common_type.py index 83ef7382fbe8a..1169ff967b357 100644 --- a/pandas/tests/dtypes/cast/test_find_common_type.py +++ b/pandas/tests/dtypes/cast/test_find_common_type.py @@ -31,7 +31,7 @@ ((np.float16, np.float32), np.float32), ((np.float16, np.int16), np.float32), ((np.float32, np.int16), np.float32), - ((np.uint64, np.int64), np.float64), + ((np.uint64, np.int64), object), ((np.int16, np.float64), np.float64), ((np.float16, np.int64), np.float64), # Into others. @@ -155,9 +155,16 @@ def test_interval_dtype(left, right): elif left.subtype.kind in ["i", "u", "f"]: # i.e. numeric if right.subtype.kind in ["i", "u", "f"]: - # both numeric -> common numeric subtype - expected = IntervalDtype(np.float64, "right") - assert result == expected + if ( + left.subtype.kind in ["i", "u"] + and right.subtype.kind in ["i", "u"] + and left.subtype.kind != right.subtype.kind + ): + assert result == object + else: + # both numeric -> common numeric subtype + expected = IntervalDtype(np.float64, "right") + assert result == expected else: assert result == object diff --git a/pandas/tests/indexes/interval/test_setops.py b/pandas/tests/indexes/interval/test_setops.py index 1b0816a9405cb..97f8a662b493d 100644 --- a/pandas/tests/indexes/interval/test_setops.py +++ b/pandas/tests/indexes/interval/test_setops.py @@ -55,6 +55,7 @@ def test_union_empty_result(self, closed, sort): tm.assert_index_equal(result, expected) other = empty_index(dtype="uint64", closed=closed) + expected = Index([], dtype="object") result = index.union(other, sort=sort) tm.assert_index_equal(result, expected) @@ -117,6 +118,7 @@ def test_intersection_empty_result(self, closed, sort): tm.assert_index_equal(result, expected) other = monotonic_index(300, 314, dtype="uint64", closed=closed) + expected = Index([], dtype="object") result = index.intersection(other, sort=sort) tm.assert_index_equal(result, expected) diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 7fb421e27bb40..ec87441e3941a 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -1197,6 +1197,13 @@ def test_isin_unsigned_dtype(self): expected = Series(False) tm.assert_series_equal(result, expected) + def test_isin_unsigned_dtype_other_side(self): + # GH#46485 + ser = Series([1378774140726870442], dtype=np.int64) + result = ser.isin([np.uint64(1378774140726870528)]) + expected = Series(False) + tm.assert_series_equal(result, expected) + class TestValueCounts: def test_value_counts(self):