Skip to content

Commit 59f7baf

Browse files
authored
ENH: DTI/TDI as_unit (#50616)
* ENH: DTI/TDI as_unit * mypy fixup * docstring * validation for uit
1 parent bad3774 commit 59f7baf

File tree

7 files changed

+39
-13
lines changed

7 files changed

+39
-13
lines changed

doc/source/reference/indexing.rst

+2
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ Conversion
385385
.. autosummary::
386386
:toctree: api/
387387

388+
DatetimeIndex.as_unit
388389
DatetimeIndex.to_period
389390
DatetimeIndex.to_pydatetime
390391
DatetimeIndex.to_series
@@ -423,6 +424,7 @@ Conversion
423424
.. autosummary::
424425
:toctree: api/
425426

427+
TimedeltaIndex.as_unit
426428
TimedeltaIndex.to_pytimedelta
427429
TimedeltaIndex.to_series
428430
TimedeltaIndex.round

doc/source/whatsnew/v2.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Other enhancements
150150
- :meth:`DataFrame.plot.hist` now recognizes ``xlabel`` and ``ylabel`` arguments (:issue:`49793`)
151151
- Improved error message in :func:`to_datetime` for non-ISO8601 formats, informing users about the position of the first error (:issue:`50361`)
152152
- Improved error message when trying to align :class:`DataFrame` objects (for example, in :func:`DataFrame.compare`) to clarify that "identically labelled" refers to both index and columns (:issue:`50083`)
153+
- Added :meth:`DatetimeIndex.as_unit` and :meth:`TimedeltaIndex.as_unit` to convert to different resolutions; supported resolutions are "s", "ms", "us", and "ns" (:issue:`50616`)
153154
-
154155

155156
.. ---------------------------------------------------------------------------

pandas/core/arrays/datetimelike.py

+3
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,9 @@ def unit(self) -> str:
19711971
return dtype_to_unit(self.dtype) # type: ignore[arg-type]
19721972

19731973
def as_unit(self: TimelikeOpsT, unit: str) -> TimelikeOpsT:
1974+
if unit not in ["s", "ms", "us", "ns"]:
1975+
raise ValueError("Supported units are 's', 'ms', 'us', 'ns'")
1976+
19741977
dtype = np.dtype(f"{self.dtype.kind}8[{unit}]")
19751978
new_values = astype_overflowsafe(self._ndarray, dtype, round_ok=True)
19761979

pandas/core/indexes/datetimelike.py

+19
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,25 @@ class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin):
396396

397397
_join_precedence = 10
398398

399+
@property
400+
def unit(self) -> str:
401+
return self._data.unit
402+
403+
def as_unit(self: _TDT, unit: str) -> _TDT:
404+
"""
405+
Convert to a dtype with the given unit resolution.
406+
407+
Parameters
408+
----------
409+
unit : {'s', 'ms', 'us', 'ns'}
410+
411+
Returns
412+
-------
413+
same type as self
414+
"""
415+
arr = self._data.as_unit(unit)
416+
return type(self)._simple_new(arr, name=self.name)
417+
399418
def _with_freq(self, freq):
400419
arr = self._data._with_freq(freq)
401420
return type(self)._simple_new(arr, name=self._name)

pandas/tests/arrays/test_timedeltas.py

+9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ def test_non_nano(self, unit):
2929
assert tda.dtype == arr.dtype
3030
assert tda[0].unit == unit
3131

32+
def test_as_unit_raises(self, tda):
33+
# GH#50616
34+
with pytest.raises(ValueError, match="Supported units"):
35+
tda.as_unit("D")
36+
37+
tdi = pd.Index(tda)
38+
with pytest.raises(ValueError, match="Supported units"):
39+
tdi.as_unit("D")
40+
3241
@pytest.mark.parametrize("field", TimedeltaArray._field_ops)
3342
def test_fields(self, tda, field):
3443
as_nano = tda._ndarray.astype("m8[ns]")

pandas/tests/indexes/datetimes/methods/test_snap.py

+3-10
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@
77
import pandas._testing as tm
88

99

10-
def astype_non_nano(dti_nano, unit):
11-
# TODO(2.0): remove once DTI supports as_unit
12-
dta = dti_nano._data.as_unit(unit)
13-
dti = DatetimeIndex(dta, name=dti_nano.name)
14-
return dti
15-
16-
1710
@pytest.mark.parametrize("tz", [None, "Asia/Shanghai", "Europe/Berlin"])
1811
@pytest.mark.parametrize("name", [None, "my_dti"])
1912
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
@@ -32,12 +25,12 @@ def test_dti_snap(name, tz, unit):
3225
tz=tz,
3326
freq="D",
3427
)
35-
dti = astype_non_nano(dti, unit)
28+
dti = dti.as_unit(unit)
3629

3730
result = dti.snap(freq="W-MON")
3831
expected = date_range("12/31/2001", "1/7/2002", name=name, tz=tz, freq="w-mon")
3932
expected = expected.repeat([3, 4])
40-
expected = astype_non_nano(expected, unit)
33+
expected = expected.as_unit(unit)
4134
tm.assert_index_equal(result, expected)
4235
assert result.tz == expected.tz
4336
assert result.freq is None
@@ -47,7 +40,7 @@ def test_dti_snap(name, tz, unit):
4740

4841
expected = date_range("1/1/2002", "1/7/2002", name=name, tz=tz, freq="b")
4942
expected = expected.repeat([1, 1, 1, 2, 2])
50-
expected = astype_non_nano(expected, unit)
43+
expected = expected.as_unit(unit)
5144
tm.assert_index_equal(result, expected)
5245
assert result.tz == expected.tz
5346
assert result.freq is None

pandas/tests/series/test_constructors.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1587,9 +1587,8 @@ def test_convert_non_ns(self):
15871587
ser = Series(arr)
15881588
assert ser.dtype == arr.dtype
15891589

1590-
tdi = timedelta_range("00:00:01", periods=3, freq="s")
1591-
tda = tdi._data.as_unit("s")
1592-
expected = Series(tda)
1590+
tdi = timedelta_range("00:00:01", periods=3, freq="s").as_unit("s")
1591+
expected = Series(tdi)
15931592
assert expected.dtype == arr.dtype
15941593
tm.assert_series_equal(ser, expected)
15951594

0 commit comments

Comments
 (0)