From 437a2d60e45012fb079ec5d06cd3e9299b8735f2 Mon Sep 17 00:00:00 2001 From: Matthew Murray Date: Wed, 19 Aug 2026 14:21:57 +0000 Subject: [PATCH] fix(to_numpy): return object dtype for bool/numeric and datetime/timedelta mixes in pandas-compatible mode --- python/cudf/cudf/core/frame.py | 19 ++++++++++++++++--- python/cudf/cudf/utils/dtypes.py | 30 ++++++++++++++++++------------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/python/cudf/cudf/core/frame.py b/python/cudf/cudf/core/frame.py index 68aa59d65e22..db400046aeaa 100644 --- a/python/cudf/cudf/core/frame.py +++ b/python/cudf/cudf/core/frame.py @@ -35,6 +35,7 @@ ) from cudf.core.mixins import BinaryOperand, Scannable from cudf.utils.dtypes import ( + _is_pandas_object_dtype_mix, dtype_from_pylibcudf_column, find_common_type, is_pandas_nullable_extension_dtype, @@ -719,9 +720,21 @@ def to_array( # otherwise store the null sentinel instead. to_dtype = np.dtype("float64") else: - to_dtype = find_common_type( - [dtype for _, dtype in self._dtypes] - ) + column_dtypes = [dtype for _, dtype in self._dtypes] + if ( + module is not cupy + and cudf.get_option("mode.pandas_compatible") + and _is_pandas_object_dtype_mix(column_dtypes) + ): + # pandas coerces bool+numeric and datetime64+timedelta64 + # mixes to `object` rather than promoting them; match + # that here instead of calling find_common_type (which + # raises in this case so the cudf.pandas proxy can fall + # back to pandas). GPU arrays can't hold objects, so this + # fallback only applies to numpy output. + to_dtype = np.dtype("object") + else: + to_dtype = find_common_type(column_dtypes) if to_dtype is not None and any( col.has_nulls() for col in self._columns ): diff --git a/python/cudf/cudf/utils/dtypes.py b/python/cudf/cudf/utils/dtypes.py index 8322926040f0..9d715d94f7a7 100644 --- a/python/cudf/cudf/utils/dtypes.py +++ b/python/cudf/cudf/utils/dtypes.py @@ -304,6 +304,18 @@ def _get_nan_for_dtype(dtype: DtypeObj) -> ScalarLike: return np.float64("nan") +def _is_pandas_object_dtype_mix(dtypes: Iterable[DtypeObj]) -> bool: + """ + Whether ``dtypes`` mixes bool with numeric, or datetime64 with + timedelta64. Pandas coerces these combinations to ``object`` rather + than promoting via NumPy's type promotion rules. + """ + kinds = {dtype.kind for dtype in dtypes if isinstance(dtype, np.dtype)} + return ("b" in kinds and bool(kinds & set("iuf"))) or ( + "M" in kinds and "m" in kinds + ) + + def find_common_type(dtypes: Iterable[DtypeObj]) -> DtypeObj: """ Wrapper over np.result_type to handle cudf specific types. @@ -398,24 +410,18 @@ def find_common_type(dtypes: Iterable[DtypeObj]) -> DtypeObj: "not supported" ) - if pandas_compatible: + if pandas_compatible and _is_pandas_object_dtype_mix(dtypes): # cudf follows NumPy promotion: bool+int->int, bool+float->float, # datetime64+timedelta64->datetime64. Pandas returns `object` for # these mixes. Raise so that, when used as the cudf.pandas fast path # for `pandas.core.dtypes.cast.find_common_type`, we fall back to # pandas' implementation rather than silently producing a different # answer. - kinds = {dtype.kind for dtype in dtypes if isinstance(dtype, np.dtype)} - if "b" in kinds and kinds & set("iuf"): - raise NotImplementedError( - "Common type of bool with numeric dtypes is not supported " - "in pandas-compatible mode." - ) - if "M" in kinds and "m" in kinds: - raise NotImplementedError( - "Common type of datetime64 with timedelta64 is not supported " - "in pandas-compatible mode." - ) + raise NotImplementedError( + "Common type of bool with numeric, or datetime64 with " + "timedelta64, dtypes is not supported in pandas-compatible " + "mode." + ) try: common_dtype = np.result_type(*dtypes) # noqa: TID251