From 734be5338bb8a7da4c93c94dd8ad89c306e217b7 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 14 May 2018 09:37:35 -0500 Subject: [PATCH 1/3] Warn on ndarray[int] // timedelta Closes #19761. ```python In [2]: pd.DatetimeIndex(['1931', '1970', '2017']).view('i8') // pd.Timedelta(1, unit='s') pandas-dev/bin/ipython:1: FutureWarning: Floor division between integer array and Timedelta is deprecated. Use 'array // timedelta.value' instead. Out[2]: array([-1230768000, 0, 1483228800]) ``` --- doc/source/timeseries.rst | 2 +- doc/source/whatsnew/v0.23.0.txt | 1 + pandas/_libs/tslibs/timedeltas.pyx | 11 +++++++++++ pandas/tests/scalar/timedelta/test_timedelta.py | 12 ++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index adb4cdf2974a0..e223e3efd8ca2 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -308,7 +308,7 @@ We convert the ``DatetimeIndex`` to an ``int64`` array, then divide by the conve .. ipython:: python - stamps.view('int64') // pd.Timedelta(1, unit='s') + stamps.view('int64') // pd.Timedelta(1, unit='s').value .. _timeseries.origin: diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 1660c8d9fcdc5..dcd7c56b8013f 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -1004,6 +1004,7 @@ Deprecations of the ``Series`` and ``Index`` classes have been deprecated and will be removed in a future version (:issue:`20419`). - ``DatetimeIndex.offset`` is deprecated. Use ``DatetimeIndex.freq`` instead (:issue:`20716`) +- Floor division between an integer ndarray and a :class:`Timedelta` is deprecated. Divide by :attr:`Timedelta.value` instead (:issue:`19761`) - Setting ``PeriodIndex.freq`` (which was not guaranteed to work correctly) is deprecated. Use :meth:`PeriodIndex.asfreq` instead (:issue:`20678`) - ``Index.get_duplicates()`` is deprecated and will be removed in a future version (:issue:`20239`) - The previous default behavior of negative indices in ``Categorical.take`` is deprecated. In a future version it will change from meaning missing values to meaning positional indices from the right. The future behavior is consistent with :meth:`Series.take` (:issue:`20664`). diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 7aeff9bec75b5..a7a12fcd68691 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # cython: profile=False import collections +import textwrap +import warnings import sys cdef bint PY3 = (sys.version_info[0] >= 3) @@ -1188,6 +1190,15 @@ class Timedelta(_Timedelta): if other.dtype.kind == 'm': # also timedelta-like return _broadcast_floordiv_td64(self.value, other, _rfloordiv) + elif other.dtype.kind == 'i': + # Backwards compatibility + # GH-19761 + msg = textwrap.dedent("""\ + Floor division between integer array and Timedelta is + deprecated. Use 'array // timedelta.value' instead. + """) + warnings.warn(msg, FutureWarning) + return other // self.value raise TypeError('Invalid dtype {dtype} for ' '{op}'.format(dtype=other.dtype, op='__floordiv__')) diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index ab2bf92a26826..3fdc2aa71bfc0 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -21,6 +21,18 @@ def test_arithmetic_overflow(self): with pytest.raises(OverflowError): pd.Timestamp('1700-01-01') + timedelta(days=13 * 19999) + def test_array_timedelta_floordiv(self): + # https://github.com/pandas-dev/pandas/issues/19761 + ints = pd.date_range('2012-10-08', periods=4, freq='D').view('i8') + msg = r"Use 'array // timedelta.value'" + with tm.assert_produces_warning(FutureWarning) as m: + result = ints // pd.Timedelta(1, unit='s') + + assert msg in str(m[0].message) + expected = np.array([1349654400, 1349740800, 1349827200, 1349913600], + dtype='i8') + tm.assert_numpy_array_equal(result, expected) + def test_ops_error_str(self): # GH 13624 td = Timedelta('1 day') From 572489d6aff9dd982471caddc10b67a6f28cded2 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 14 May 2018 15:49:39 -0500 Subject: [PATCH 2/3] Fail earlier for divmod, rdivmod --- pandas/_libs/tslibs/timedeltas.pyx | 10 ++++++++++ pandas/tests/scalar/timedelta/test_arithmetic.py | 8 +++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index a7a12fcd68691..248c648c33db3 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1221,6 +1221,11 @@ class Timedelta(_Timedelta): def __rmod__(self, other): # Naive implementation, room for optimization + if hasattr(other, 'dtype') and other.dtype.kind == 'i': + # TODO: Remove this check with backwards-compat shim + # for integer / Timedelta is removed. + raise TypeError("Invalid type {dtype} for " + "{op}".format(dtype=other.dtype, op='__mod__')) return self.__rdivmod__(other)[1] def __divmod__(self, other): @@ -1230,6 +1235,11 @@ class Timedelta(_Timedelta): def __rdivmod__(self, other): # Naive implementation, room for optimization + if hasattr(other, 'dtype') and other.dtype.kind == 'i': + # TODO: Remove this check with backwards-compat shim + # for integer / Timedelta is removed. + raise TypeError("Invalid type {dtype} for " + "{op}".format(dtype=other.dtype, op='__mod__')) div = other // self return div, other - div * self diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py index 179768fcc6709..9636c92ec22d5 100644 --- a/pandas/tests/scalar/timedelta/test_arithmetic.py +++ b/pandas/tests/scalar/timedelta/test_arithmetic.py @@ -403,10 +403,11 @@ def test_td_rfloordiv_numeric_scalar(self): with pytest.raises(TypeError): td.__rfloordiv__(np.float64(2.0)) - with pytest.raises(TypeError): - td.__rfloordiv__(np.int32(2.0)) with pytest.raises(TypeError): td.__rfloordiv__(np.uint8(9)) + with tm.assert_produces_warning(FutureWarning): + # GH-19761: Change to TypeError. + td.__rfloordiv__(np.int32(2.0)) def test_td_rfloordiv_timedeltalike_array(self): # GH#18846 @@ -432,7 +433,8 @@ def test_td_rfloordiv_numeric_series(self): ser = pd.Series([1], dtype=np.int64) res = td.__rfloordiv__(ser) assert res is NotImplemented - with pytest.raises(TypeError): + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # TODO: GH-19761. Change to TypeError. ser // td def test_mod_timedeltalike(self): From 9fa7583804393171fbed726f9184c5e143694ff4 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 14 May 2018 21:15:00 -0500 Subject: [PATCH 3/3] Updated docs --- doc/source/timeseries.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index e223e3efd8ca2..73e3e721aad71 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -257,7 +257,7 @@ Pass ``errors='coerce'`` to convert unparseable data to ``NaT`` (not a time): Epoch Timestamps ~~~~~~~~~~~~~~~~ -pandas supports converting integer or float epoch times to ``Timestamp`` and +pandas supports converting integer or float epoch times to ``Timestamp`` and ``DatetimeIndex``. The default unit is nanoseconds, since that is how ``Timestamp`` objects are stored internally. However, epochs are often stored in another ``unit`` which can be specified. These are computed from the starting point specified by the @@ -304,11 +304,12 @@ To invert the operation from above, namely, to convert from a ``Timestamp`` to a stamps = pd.date_range('2012-10-08 18:15:05', periods=4, freq='D') stamps -We convert the ``DatetimeIndex`` to an ``int64`` array, then divide by the conversion unit. +We subtract the epoch (midnight at January 1, 1970 UTC) and then floor divide by the +"unit" (1 second). .. ipython:: python - stamps.view('int64') // pd.Timedelta(1, unit='s').value + (stamps - pd.Timestamp("1970-01-01")) // pd.Timedelta('1s') .. _timeseries.origin: