From ca81e6cfecec76bd74fbfc83e29cada1c4e0b48d Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 6 Dec 2021 01:50:42 -0800 Subject: [PATCH] BUG: FloatingArray * np.timedelta64 (#44772) --- doc/source/whatsnew/v1.4.0.rst | 1 + pandas/core/arrays/boolean.py | 31 ----------------- pandas/core/arrays/floating.py | 21 ------------ pandas/core/arrays/integer.py | 29 ---------------- pandas/core/arrays/masked.py | 45 +++++++++++++++++++++++++ pandas/core/arrays/numeric.py | 4 --- pandas/tests/arithmetic/test_numeric.py | 11 ++++-- 7 files changed, 54 insertions(+), 88 deletions(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index c73706878e856..59f155e0e5118 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -644,6 +644,7 @@ Numeric - Bug in arithmetic operations involving :class:`RangeIndex` where the result would have the incorrect ``name`` (:issue:`43962`) - Bug in arithmetic operations involving :class:`Series` where the result could have the incorrect ``name`` when the operands having matching NA or matching tuple names (:issue:`44459`) - Bug in division with ``IntegerDtype`` or ``BooleanDtype`` array and NA scalar incorrectly raising (:issue:`44685`) +- Bug in multiplying a :class:`Series` with ``FloatingDtype`` with a timedelta-like scalar incorrectly raising (:issue:`44772`) - Conversion diff --git a/pandas/core/arrays/boolean.py b/pandas/core/arrays/boolean.py index 09cdb985ddb2e..d501af6212ce3 100644 --- a/pandas/core/arrays/boolean.py +++ b/pandas/core/arrays/boolean.py @@ -23,7 +23,6 @@ from pandas.core.dtypes.common import ( is_bool_dtype, - is_float, is_float_dtype, is_integer_dtype, is_list_like, @@ -532,35 +531,5 @@ def _arith_method(self, other, op): return self._maybe_mask_result(result, mask, other, op_name) - def _maybe_mask_result(self, result, mask, other, op_name: str): - """ - Parameters - ---------- - result : array-like - mask : array-like bool - other : scalar or array-like - op_name : str - """ - # if we have a float operand we are by-definition - # a float result - # or our op is a divide - if (is_float_dtype(other) or is_float(other)) or ( - op_name in ["rtruediv", "truediv"] - ): - from pandas.core.arrays import FloatingArray - - return FloatingArray(result, mask, copy=False) - - elif is_bool_dtype(result): - return BooleanArray(result, mask, copy=False) - - elif is_integer_dtype(result): - from pandas.core.arrays import IntegerArray - - return IntegerArray(result, mask, copy=False) - else: - result[mask] = np.nan - return result - def __abs__(self): return self.copy() diff --git a/pandas/core/arrays/floating.py b/pandas/core/arrays/floating.py index 396ed7eb4abeb..7afaa12da12fb 100644 --- a/pandas/core/arrays/floating.py +++ b/pandas/core/arrays/floating.py @@ -354,27 +354,6 @@ def max(self, *, skipna=True, axis: int | None = 0, **kwargs): nv.validate_max((), kwargs) return super()._reduce("max", skipna=skipna, axis=axis) - def _maybe_mask_result(self, result, mask, other, op_name: str): - """ - Parameters - ---------- - result : array-like - mask : array-like bool - other : scalar or array-like - op_name : str - """ - # TODO are there cases we don't end up with float? - # if we have a float operand we are by-definition - # a float result - # or our op is a divide - # if (is_float_dtype(other) or is_float(other)) or ( - # op_name in ["rtruediv", "truediv"] - # ): - # result[mask] = np.nan - # return result - - return type(self)(result, mask, copy=False) - _dtype_docstring = """ An ExtensionDtype for {dtype} data. diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 0e82ef731bb63..bbc1304d6596c 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -5,7 +5,6 @@ import numpy as np from pandas._libs import ( - iNaT, lib, missing as libmissing, ) @@ -26,7 +25,6 @@ from pandas.core.dtypes.common import ( is_bool_dtype, is_datetime64_dtype, - is_float, is_float_dtype, is_integer_dtype, is_object_dtype, @@ -427,33 +425,6 @@ def max(self, *, skipna=True, axis: int | None = 0, **kwargs): nv.validate_max((), kwargs) return super()._reduce("max", skipna=skipna, axis=axis) - def _maybe_mask_result(self, result, mask, other, op_name: str): - """ - Parameters - ---------- - result : array-like - mask : array-like bool - other : scalar or array-like - op_name : str - """ - # if we have a float operand we are by-definition - # a float result - # or our op is a divide - if (is_float_dtype(other) or is_float(other)) or ( - op_name in ["rtruediv", "truediv"] - ): - from pandas.core.arrays import FloatingArray - - return FloatingArray(result, mask, copy=False) - - if result.dtype == "timedelta64[ns]": - from pandas.core.arrays import TimedeltaArray - - result[mask] = iNaT - return TimedeltaArray._simple_new(result) - - return type(self)(result, mask, copy=False) - _dtype_docstring = """ An ExtensionDtype for {dtype} integer data. diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index a882fe5d2da21..ef0b407eebd9e 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -12,6 +12,7 @@ import numpy as np from pandas._libs import ( + iNaT, lib, missing as libmissing, ) @@ -39,9 +40,11 @@ is_bool, is_bool_dtype, is_dtype_equal, + is_float, is_float_dtype, is_integer_dtype, is_list_like, + is_numeric_dtype, is_object_dtype, is_scalar, is_string_dtype, @@ -543,6 +546,48 @@ def _cmp_method(self, other, op) -> BooleanArray: return BooleanArray(result, mask, copy=False) + def _maybe_mask_result(self, result, mask, other, op_name: str): + """ + Parameters + ---------- + result : array-like + mask : array-like bool + other : scalar or array-like + op_name : str + """ + # if we have a float operand we are by-definition + # a float result + # or our op is a divide + if ( + (is_float_dtype(other) or is_float(other)) + or (op_name in ["rtruediv", "truediv"]) + or (is_float_dtype(self.dtype) and is_numeric_dtype(result.dtype)) + ): + from pandas.core.arrays import FloatingArray + + return FloatingArray(result, mask, copy=False) + + elif is_bool_dtype(result): + from pandas.core.arrays import BooleanArray + + return BooleanArray(result, mask, copy=False) + + elif result.dtype == "timedelta64[ns]": + # e.g. test_numeric_arr_mul_tdscalar_numexpr_path + from pandas.core.arrays import TimedeltaArray + + result[mask] = iNaT + return TimedeltaArray._simple_new(result) + + elif is_integer_dtype(result): + from pandas.core.arrays import IntegerArray + + return IntegerArray(result, mask, copy=False) + + else: + result[mask] = np.nan + return result + def isna(self) -> np.ndarray: return self._mask.copy() diff --git a/pandas/core/arrays/numeric.py b/pandas/core/arrays/numeric.py index b17d40b35903b..fc42c11c510d9 100644 --- a/pandas/core/arrays/numeric.py +++ b/pandas/core/arrays/numeric.py @@ -14,7 +14,6 @@ missing as libmissing, ) from pandas.compat.numpy import function as nv -from pandas.errors import AbstractMethodError from pandas.core.dtypes.common import ( is_float, @@ -80,9 +79,6 @@ class NumericArray(BaseMaskedArray): Base class for IntegerArray and FloatingArray. """ - def _maybe_mask_result(self, result, mask, other, op_name: str): - raise AbstractMethodError(self) - def _arith_method(self, other, op): op_name = op.__name__ omask = None diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index a33febbfbe960..4935151ec986e 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -213,13 +213,18 @@ def test_numeric_arr_mul_tdscalar(self, scalar_td, numeric_idx, box_with_array): ], ids=lambda x: type(x).__name__, ) - def test_numeric_arr_mul_tdscalar_numexpr_path(self, scalar_td, box_with_array): + @pytest.mark.parametrize("dtype", [np.int64, np.float64]) + def test_numeric_arr_mul_tdscalar_numexpr_path( + self, dtype, scalar_td, box_with_array + ): + # GH#44772 for the float64 case box = box_with_array - arr = np.arange(2 * 10 ** 4).astype(np.int64) + arr_i8 = np.arange(2 * 10 ** 4).astype(np.int64, copy=False) + arr = arr_i8.astype(dtype, copy=False) obj = tm.box_expected(arr, box, transpose=False) - expected = arr.view("timedelta64[D]").astype("timedelta64[ns]") + expected = arr_i8.view("timedelta64[D]").astype("timedelta64[ns]") expected = tm.box_expected(expected, box, transpose=False) result = obj * scalar_td