From a9cd5cb00363a04b774b08f6b98a85783d106346 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 6 Jan 2023 18:53:07 -0800 Subject: [PATCH 1/4] ENH: DTI/TDI as_unit --- doc/source/reference/indexing.rst | 2 ++ doc/source/whatsnew/v2.0.0.rst | 1 + pandas/core/indexes/datetimelike.py | 8 ++++++++ pandas/tests/indexes/datetimes/methods/test_snap.py | 13 +++---------- pandas/tests/series/test_constructors.py | 5 ++--- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/doc/source/reference/indexing.rst b/doc/source/reference/indexing.rst index 662e8718fa5ba..eccfc368301bf 100644 --- a/doc/source/reference/indexing.rst +++ b/doc/source/reference/indexing.rst @@ -385,6 +385,7 @@ Conversion .. autosummary:: :toctree: api/ + DatetimeIndex.as_unit DatetimeIndex.to_period DatetimeIndex.to_pydatetime DatetimeIndex.to_series @@ -423,6 +424,7 @@ Conversion .. autosummary:: :toctree: api/ + TimedeltaIndex.as_unit TimedeltaIndex.to_pytimedelta TimedeltaIndex.to_series TimedeltaIndex.round diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 9d44c770f58ef..456fec58c7ab3 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -108,6 +108,7 @@ Other enhancements - :meth:`DataFrame.plot.hist` now recognizes ``xlabel`` and ``ylabel`` arguments (:issue:`49793`) - Improved error message in :func:`to_datetime` for non-ISO8601 formats, informing users about the position of the first error (:issue:`50361`) - 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`) +- Added :meth:`DatetimeIndex.as_unit` and :meth:`TimedeltaIndex.as_unit` to convert to different resolutions; supported resolutions are "s", "ms", "us", and "ns" (:issue:`??`) - .. --------------------------------------------------------------------------- diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index fde000f84e581..6b5f3336ecd21 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -396,6 +396,14 @@ class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin): _join_precedence = 10 + @property + def unit(self) -> str: + return self._data.unit + + def as_unit(self, unit: _TDT) -> _TDT: + arr = self._data.as_unit(unit) + return type(self)._simple_new(arr, name=self.name) + def _with_freq(self, freq): arr = self._data._with_freq(freq) return type(self)._simple_new(arr, name=self._name) diff --git a/pandas/tests/indexes/datetimes/methods/test_snap.py b/pandas/tests/indexes/datetimes/methods/test_snap.py index bceb37fdc90cf..7064e9e7993f8 100644 --- a/pandas/tests/indexes/datetimes/methods/test_snap.py +++ b/pandas/tests/indexes/datetimes/methods/test_snap.py @@ -7,13 +7,6 @@ import pandas._testing as tm -def astype_non_nano(dti_nano, unit): - # TODO(2.0): remove once DTI supports as_unit - dta = dti_nano._data.as_unit(unit) - dti = DatetimeIndex(dta, name=dti_nano.name) - return dti - - @pytest.mark.parametrize("tz", [None, "Asia/Shanghai", "Europe/Berlin"]) @pytest.mark.parametrize("name", [None, "my_dti"]) @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"]) @@ -32,12 +25,12 @@ def test_dti_snap(name, tz, unit): tz=tz, freq="D", ) - dti = astype_non_nano(dti, unit) + dti = dti.as_unit(unit) result = dti.snap(freq="W-MON") expected = date_range("12/31/2001", "1/7/2002", name=name, tz=tz, freq="w-mon") expected = expected.repeat([3, 4]) - expected = astype_non_nano(expected, unit) + expected = expected.as_unit(unit) tm.assert_index_equal(result, expected) assert result.tz == expected.tz assert result.freq is None @@ -47,7 +40,7 @@ def test_dti_snap(name, tz, unit): expected = date_range("1/1/2002", "1/7/2002", name=name, tz=tz, freq="b") expected = expected.repeat([1, 1, 1, 2, 2]) - expected = astype_non_nano(expected, unit) + expected = expected.as_unit(unit) tm.assert_index_equal(result, expected) assert result.tz == expected.tz assert result.freq is None diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py index f856f18552594..e1f59e610ab45 100644 --- a/pandas/tests/series/test_constructors.py +++ b/pandas/tests/series/test_constructors.py @@ -1587,9 +1587,8 @@ def test_convert_non_ns(self): ser = Series(arr) assert ser.dtype == arr.dtype - tdi = timedelta_range("00:00:01", periods=3, freq="s") - tda = tdi._data.as_unit("s") - expected = Series(tda) + tdi = timedelta_range("00:00:01", periods=3, freq="s").as_unit("s") + expected = Series(tdi) assert expected.dtype == arr.dtype tm.assert_series_equal(ser, expected) From d76d24f304a5b39a39b62a537ef69f3bce816f26 Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 7 Jan 2023 13:40:15 -0800 Subject: [PATCH 2/4] mypy fixup --- pandas/core/indexes/datetimelike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 6b5f3336ecd21..f3b40050724c5 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -400,7 +400,7 @@ class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin): def unit(self) -> str: return self._data.unit - def as_unit(self, unit: _TDT) -> _TDT: + def as_unit(self: _TDT, unit: str) -> _TDT: arr = self._data.as_unit(unit) return type(self)._simple_new(arr, name=self.name) From eefab973a77e53d5cf6fa44f2e0cf6a183802e06 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 8 Jan 2023 08:17:50 -0800 Subject: [PATCH 3/4] docstring --- doc/source/whatsnew/v2.0.0.rst | 2 +- pandas/core/indexes/datetimelike.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index fe99944d60670..6335a0ab13b99 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -108,7 +108,7 @@ Other enhancements - :meth:`DataFrame.plot.hist` now recognizes ``xlabel`` and ``ylabel`` arguments (:issue:`49793`) - Improved error message in :func:`to_datetime` for non-ISO8601 formats, informing users about the position of the first error (:issue:`50361`) - 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`) -- Added :meth:`DatetimeIndex.as_unit` and :meth:`TimedeltaIndex.as_unit` to convert to different resolutions; supported resolutions are "s", "ms", "us", and "ns" (:issue:`??`) +- 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`) - .. --------------------------------------------------------------------------- diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index f3b40050724c5..002b9832e9e6e 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -401,6 +401,17 @@ def unit(self) -> str: return self._data.unit def as_unit(self: _TDT, unit: str) -> _TDT: + """ + Convert to a dtype with the given unit resolution. + + Parameters + ---------- + unit : {'s', 'ms', 'us', 'ns'} + + Returns + ------- + same type as self + """ arr = self._data.as_unit(unit) return type(self)._simple_new(arr, name=self.name) From 04baed54dd1a522bc6e857e0c5f51c3f54181bb0 Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 9 Jan 2023 13:07:05 -0800 Subject: [PATCH 4/4] validation for uit --- pandas/core/arrays/datetimelike.py | 3 +++ pandas/tests/arrays/test_timedeltas.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index d7d28eed16f8b..5db3f02e2dec7 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -1966,6 +1966,9 @@ def unit(self) -> str: return dtype_to_unit(self.dtype) # type: ignore[arg-type] def as_unit(self: TimelikeOpsT, unit: str) -> TimelikeOpsT: + if unit not in ["s", "ms", "us", "ns"]: + raise ValueError("Supported units are 's', 'ms', 'us', 'ns'") + dtype = np.dtype(f"{self.dtype.kind}8[{unit}]") new_values = astype_overflowsafe(self._ndarray, dtype, round_ok=True) diff --git a/pandas/tests/arrays/test_timedeltas.py b/pandas/tests/arrays/test_timedeltas.py index b322a5b57591a..c8ff9c2402f2f 100644 --- a/pandas/tests/arrays/test_timedeltas.py +++ b/pandas/tests/arrays/test_timedeltas.py @@ -29,6 +29,15 @@ def test_non_nano(self, unit): assert tda.dtype == arr.dtype assert tda[0].unit == unit + def test_as_unit_raises(self, tda): + # GH#50616 + with pytest.raises(ValueError, match="Supported units"): + tda.as_unit("D") + + tdi = pd.Index(tda) + with pytest.raises(ValueError, match="Supported units"): + tdi.as_unit("D") + @pytest.mark.parametrize("field", TimedeltaArray._field_ops) def test_fields(self, tda, field): as_nano = tda._ndarray.astype("m8[ns]")