Skip to content

Commit 0595cc9

Browse files
TomAugspurgerPingviinituutti
authored andcommitted
DEPR/API: Non-ns precision in Index constructors (pandas-dev#24806)
1 parent 86b327f commit 0595cc9

File tree

7 files changed

+80
-22
lines changed

7 files changed

+80
-22
lines changed

doc/source/whatsnew/v0.24.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,7 @@ Other API Changes
11591159
- :meth:`CategoricalIndex.reindex` now raises a ``ValueError`` if the target index is non-unique and not equal to the current index. It previously only raised if the target index was not of a categorical dtype (:issue:`23963`).
11601160
- :func:`Series.to_list` and :func:`Index.to_list` are now aliases of ``Series.tolist`` respectively ``Index.tolist`` (:issue:`8826`)
11611161
- The result of ``SparseSeries.unstack`` is now a :class:`DataFrame` with sparse values, rather than a :class:`SparseDataFrame` (:issue:`24372`).
1162+
- :class:`DatetimeIndex` and :class:`TimedeltaIndex` no longer ignore the dtype precision. Passing a non-nanosecond resolution dtype will raise a ``ValueError`` (:issue:`24753`)
11621163

11631164

11641165
.. _whatsnew_0240.api.extension:
@@ -1259,6 +1260,7 @@ Deprecations
12591260
- :meth:`Series.nonzero` is deprecated and will be removed in a future version (:issue:`18262`)
12601261
- Passing an integer to :meth:`Series.fillna` and :meth:`DataFrame.fillna` with ``timedelta64[ns]`` dtypes is deprecated, will raise ``TypeError`` in a future version. Use ``obj.fillna(pd.Timedelta(...))`` instead (:issue:`24694`)
12611262
- ``Series.cat.categorical``, ``Series.cat.name`` and ``Sersies.cat.index`` have been deprecated. Use the attributes on ``Series.cat`` or ``Series`` directly. (:issue:`24751`).
1263+
- Passing a dtype without a precision like ``np.dtype('datetime64')`` or ``timedelta64`` to :class:`Index`, :class:`DatetimeIndex` and :class:`TimedeltaIndex` is now deprecated. Use the nanosecond-precision dtype instead (:issue:`24753`).
12621264

12631265
.. _whatsnew_0240.deprecations.datetimelike_int_ops:
12641266

pandas/core/arrays/datetimes.py

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
from datetime import datetime, time, timedelta
3+
import textwrap
34
import warnings
45

56
import numpy as np
@@ -1986,6 +1987,15 @@ def _validate_dt64_dtype(dtype):
19861987
"""
19871988
if dtype is not None:
19881989
dtype = pandas_dtype(dtype)
1990+
if is_dtype_equal(dtype, np.dtype("M8")):
1991+
# no precision, warn
1992+
dtype = _NS_DTYPE
1993+
msg = textwrap.dedent("""\
1994+
Passing in 'datetime64' dtype with no precision is deprecated
1995+
and will raise in a future version. Please pass in
1996+
'datetime64[ns]' instead.""")
1997+
warnings.warn(msg, FutureWarning, stacklevel=5)
1998+
19891999
if ((isinstance(dtype, np.dtype) and dtype != _NS_DTYPE)
19902000
or not isinstance(dtype, (np.dtype, DatetimeTZDtype))):
19912001
raise ValueError("Unexpected value for 'dtype': '{dtype}'. "

pandas/core/arrays/timedeltas.py

+23-15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import division
33

44
from datetime import timedelta
5+
import textwrap
56
import warnings
67

78
import numpy as np
@@ -15,8 +16,8 @@
1516
from pandas.util._decorators import Appender
1617

1718
from pandas.core.dtypes.common import (
18-
_NS_DTYPE, _TD_DTYPE, ensure_int64, is_datetime64_dtype, is_float_dtype,
19-
is_integer_dtype, is_list_like, is_object_dtype, is_scalar,
19+
_NS_DTYPE, _TD_DTYPE, ensure_int64, is_datetime64_dtype, is_dtype_equal,
20+
is_float_dtype, is_integer_dtype, is_list_like, is_object_dtype, is_scalar,
2021
is_string_dtype, is_timedelta64_dtype, is_timedelta64_ns_dtype,
2122
pandas_dtype)
2223
from pandas.core.dtypes.dtypes import DatetimeTZDtype
@@ -160,16 +161,8 @@ def __init__(self, values, dtype=_TD_DTYPE, freq=None, copy=False):
160161
# nanosecond UTC (or tz-naive) unix timestamps
161162
values = values.view(_TD_DTYPE)
162163

163-
if values.dtype != _TD_DTYPE:
164-
raise TypeError(_BAD_DTYPE.format(dtype=values.dtype))
165-
166-
try:
167-
dtype_mismatch = dtype != _TD_DTYPE
168-
except TypeError:
169-
raise TypeError(_BAD_DTYPE.format(dtype=dtype))
170-
else:
171-
if dtype_mismatch:
172-
raise TypeError(_BAD_DTYPE.format(dtype=dtype))
164+
_validate_td64_dtype(values.dtype)
165+
dtype = _validate_td64_dtype(dtype)
173166

174167
if freq == "infer":
175168
msg = (
@@ -204,9 +197,8 @@ def _simple_new(cls, values, freq=None, dtype=_TD_DTYPE):
204197
@classmethod
205198
def _from_sequence(cls, data, dtype=_TD_DTYPE, copy=False,
206199
freq=None, unit=None):
207-
if dtype != _TD_DTYPE:
208-
raise ValueError("Only timedelta64[ns] dtype is valid.")
209-
200+
if dtype:
201+
_validate_td64_dtype(dtype)
210202
freq, freq_infer = dtl.maybe_infer_freq(freq)
211203

212204
data, inferred_freq = sequence_to_td64ns(data, copy=copy, unit=unit)
@@ -997,6 +989,22 @@ def objects_to_td64ns(data, unit="ns", errors="raise"):
997989
return result.view('timedelta64[ns]')
998990

999991

992+
def _validate_td64_dtype(dtype):
993+
dtype = pandas_dtype(dtype)
994+
if is_dtype_equal(dtype, np.dtype("timedelta64")):
995+
dtype = _TD_DTYPE
996+
msg = textwrap.dedent("""\
997+
Passing in 'timedelta' dtype with no precision is deprecated
998+
and will raise in a future version. Please pass in
999+
'timedelta64[ns]' instead.""")
1000+
warnings.warn(msg, FutureWarning, stacklevel=4)
1001+
1002+
if not is_dtype_equal(dtype, _TD_DTYPE):
1003+
raise ValueError(_BAD_DTYPE.format(dtype=dtype))
1004+
1005+
return dtype
1006+
1007+
10001008
def _generate_regular_range(start, end, periods, offset):
10011009
stride = offset.nanos
10021010
if periods is None:

pandas/core/indexes/base.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,14 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None,
311311
elif (is_timedelta64_dtype(data) or
312312
(dtype is not None and is_timedelta64_dtype(dtype))):
313313
from pandas import TimedeltaIndex
314-
result = TimedeltaIndex(data, copy=copy, name=name, **kwargs)
315-
if dtype is not None and _o_dtype == dtype:
316-
return Index(result.to_pytimedelta(), dtype=_o_dtype)
314+
if dtype is not None and is_dtype_equal(_o_dtype, dtype):
315+
# Note we can pass copy=False because the .astype below
316+
# will always make a copy
317+
result = TimedeltaIndex(data, copy=False, name=name, **kwargs)
318+
return result.astype(object)
317319
else:
320+
result = TimedeltaIndex(data, copy=copy, name=name,
321+
dtype=dtype, **kwargs)
318322
return result
319323

320324
elif is_period_dtype(data) and not is_object_dtype(dtype):

pandas/tests/arrays/test_timedeltas.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ def test_non_array_raises(self):
2323
TimedeltaArray([1, 2, 3])
2424

2525
def test_other_type_raises(self):
26-
with pytest.raises(TypeError,
26+
with pytest.raises(ValueError,
2727
match="dtype bool cannot be converted"):
2828
TimedeltaArray(np.array([1, 2, 3], dtype='bool'))
2929

3030
def test_incorrect_dtype_raises(self):
3131
# TODO: why TypeError for 'category' but ValueError for i8?
32-
with pytest.raises(TypeError,
32+
with pytest.raises(ValueError,
3333
match=r'category cannot be converted '
3434
r'to timedelta64\[ns\]'):
3535
TimedeltaArray(np.array([1, 2, 3], dtype='i8'), dtype='category')
3636

37-
with pytest.raises(TypeError,
37+
with pytest.raises(ValueError,
3838
match=r"dtype int64 cannot be converted "
3939
r"to timedelta64\[ns\]"):
4040
TimedeltaArray(np.array([1, 2, 3], dtype='i8'),
@@ -52,7 +52,7 @@ def test_copy(self):
5252

5353
class TestTimedeltaArray(object):
5454
def test_from_sequence_dtype(self):
55-
msg = r"Only timedelta64\[ns\] dtype is valid"
55+
msg = "dtype .*object.* cannot be converted to timedelta64"
5656
with pytest.raises(ValueError, match=msg):
5757
TimedeltaArray._from_sequence([], dtype=object)
5858

pandas/tests/indexes/datetimes/test_construction.py

+17
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,23 @@ def test_construction_with_nat_and_tzlocal(self):
634634
expected = DatetimeIndex([Timestamp('2018', tz=tz), pd.NaT])
635635
tm.assert_index_equal(result, expected)
636636

637+
def test_constructor_no_precision_warns(self):
638+
# GH-24753, GH-24739
639+
expected = pd.DatetimeIndex(['2000'], dtype='datetime64[ns]')
640+
641+
# we set the stacklevel for DatetimeIndex
642+
with tm.assert_produces_warning(FutureWarning):
643+
result = pd.DatetimeIndex(['2000'], dtype='datetime64')
644+
tm.assert_index_equal(result, expected)
645+
646+
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
647+
result = pd.Index(['2000'], dtype='datetime64')
648+
tm.assert_index_equal(result, expected)
649+
650+
def test_constructor_wrong_precision_raises(self):
651+
with pytest.raises(ValueError):
652+
pd.DatetimeIndex(['2000'], dtype='datetime64[us]')
653+
637654

638655
class TestTimeSeries(object):
639656

pandas/tests/indexes/timedeltas/test_construction.py

+17
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,20 @@ def test_constructor_name(self):
180180
# GH10025
181181
idx2 = TimedeltaIndex(idx, name='something else')
182182
assert idx2.name == 'something else'
183+
184+
def test_constructor_no_precision_warns(self):
185+
# GH-24753, GH-24739
186+
expected = pd.TimedeltaIndex(['2000'], dtype='timedelta64[ns]')
187+
188+
# we set the stacklevel for DatetimeIndex
189+
with tm.assert_produces_warning(FutureWarning):
190+
result = pd.TimedeltaIndex(['2000'], dtype='timedelta64')
191+
tm.assert_index_equal(result, expected)
192+
193+
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
194+
result = pd.Index(['2000'], dtype='timedelta64')
195+
tm.assert_index_equal(result, expected)
196+
197+
def test_constructor_wrong_precision_raises(self):
198+
with pytest.raises(ValueError):
199+
pd.TimedeltaIndex(['2000'], dtype='timedelta64[us]')

0 commit comments

Comments
 (0)