diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 8fe3023e9537c..f143cbd13c22c 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -436,6 +436,7 @@ Datetimelike - Fixed bug where two :class:`DateOffset` objects with different ``normalize`` attributes could evaluate as equal (:issue:`21404`) - Fixed bug where :meth:`Timestamp.resolution` incorrectly returned 1-microsecond ``timedelta`` instead of 1-nanosecond :class:`Timedelta` (:issue:`21336`,:issue:`21365`) +- Fixed bug where :class:`DataFrame` with ``dtype='datetime64[ns]'`` operating with :class:`DateOffset` could cast to ``dtype='object'`` (:issue:`21610`) Timedelta ^^^^^^^^^ diff --git a/pandas/core/internals/__init__.py b/pandas/core/internals/__init__.py index fde3aaa14ac5d..7ef21784b2d02 100644 --- a/pandas/core/internals/__init__.py +++ b/pandas/core/internals/__init__.py @@ -63,7 +63,9 @@ ABCSeries, ABCDatetimeIndex, ABCExtensionArray, - ABCIndexClass) + ABCIndexClass, + ABCDateOffset, +) import pandas.core.common as com import pandas.core.algorithms as algos @@ -2737,7 +2739,7 @@ def _try_coerce_args(self, values, other): def _try_coerce_result(self, result): """ reverse of try_coerce_args """ - if isinstance(result, np.ndarray): + if isinstance(result, (np.ndarray, Block)): if result.dtype.kind in ['i', 'f', 'O']: try: result = result.astype('M8[ns]') @@ -2785,6 +2787,17 @@ def set(self, locs, values, check=False): self.values[locs] = values + def eval(self, func, other, try_cast=False, **kwargs): + block = super(DatetimeBlock, self).eval(func, other, try_cast=try_cast, + **kwargs)[0] + if try_cast: + if isinstance(other, (np.datetime64, date)): + block = TimeDeltaBlock(block.values, block.mgr_locs, + ndim=block.ndim) + elif isinstance(other, ABCDateOffset): + block = self._try_coerce_result(block) + return [block] + class DatetimeTZBlock(NonConsolidatableMixIn, DatetimeBlock): """ implement a datetime64 block with a tz attribute """ @@ -2920,6 +2933,8 @@ def _try_coerce_result(self, result): if isinstance(result, np.ndarray): if result.dtype.kind in ['i', 'f', 'O']: result = result.astype('M8[ns]') + elif isinstance(result, Block): + result = self.make_block_same_class(result.values.flat) elif isinstance(result, (np.integer, np.float, np.datetime64)): result = tslibs.Timestamp(result, tz=self.values.tz) if isinstance(result, np.ndarray): diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index fb381a5640519..9a41360f4b7bd 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import datetime import pytest import numpy as np @@ -211,6 +212,29 @@ def test_df_sub_datetime64_not_ns(self): pd.Timedelta(days=2)]) tm.assert_frame_equal(res, expected) + def test_timestamp_df_add_dateoffset(self): + # GH 21610 + expected = pd.DataFrame([pd.Timestamp('2019')]) + result = pd.DataFrame([pd.Timestamp('2018')]) + pd.DateOffset(years=1) + tm.assert_frame_equal(expected, result) + + expected = pd.DataFrame([pd.Timestamp('2019', tz='Asia/Shanghai')]) + result = (pd.DataFrame([pd.Timestamp('2018', tz='Asia/Shanghai')]) + + pd.DateOffset(years=1)) + tm.assert_frame_equal(expected, result) + + @pytest.mark.parametrize('other', [ + pd.Timestamp('2017'), + np.datetime64('2017'), + datetime.datetime(2017, 1, 1), + datetime.date(2017, 1, 1), + ]) + def test_timestamp_df_sub_timestamp(self, other): + # GH 8554 12437 + expected = pd.DataFrame([pd.Timedelta('365d')]) + result = pd.DataFrame([pd.Timestamp('2018')]) - other + tm.assert_frame_equal(expected, result) + @pytest.mark.parametrize('data', [ [1, 2, 3], [1.1, 2.2, 3.3], diff --git a/pandas/tests/internals/test_internals.py b/pandas/tests/internals/test_internals.py index 39418fb72bf4a..f85978e2e3a58 100644 --- a/pandas/tests/internals/test_internals.py +++ b/pandas/tests/internals/test_internals.py @@ -1238,7 +1238,6 @@ class TestCanHoldElement(object): (2**63, 'complex128'), (True, 'bool'), (np.timedelta64(20, 'ns'), '