pandas-dev/pandas · #71734
ENH: add Series.dt.shift (GH#31705)
doc/source/reference/series.rst2 + / 0 −
@@ -371,6 +371,7 @@ Datetime methods Series.dt.month_name Series.dt.day_name Series.dt.as_unit+ Series.dt.shift Period properties ^^^^^^^^^^^^^^^^^@@ -407,6 +408,7 @@ Timedelta methods Series.dt.to_pytimedelta Series.dt.total_seconds Series.dt.as_unit+ Series.dt.shift .. _api.series.str:doc/source/whatsnew/v3.2.0.rst1 + / 0 −
@@ -35,6 +35,7 @@ Other enhancements - :meth:`Series.groupby` now accepts ``as_index=False``, returning aggregation results as a :class:`DataFrame` with the group labels as columns (:issue:`36507`) - Added :meth:`Categorical.to_dense` and :meth:`Series.cat.to_dense` to get the values with the dtype of the categories, including extension dtypes (:issue:`26410`) - Added :meth:`DatetimeIndex.set_freq` and :meth:`TimedeltaIndex.set_freq`, which return a new index with the given ``freq`` instead of modifying it in place (:issue:`61094`)+- Added :meth:`Series.dt.shift` to shift datetime, timedelta, and period values by a number of frequency increments, like :meth:`DatetimeIndex.shift` (:issue:`31705`) - Display formatting for complex numbers in ``object`` dtype and for ``interval`` dtype values with float endpoints now respects the ``display.precision`` and ``display.float_format`` options (:issue:`25920`). - Printing a :class:`.Styler` now shows the underlying data instead of the object's address, so the table also appears in notebooks exported to PDF with ``nbconvert`` (:issue:`55908`) - The :class:`ExtensionArray` base tests in ``pandas.tests.extension.base`` now issue a :class:`errors.PerformanceWarning` when the array uses the default, possibly slow, implementation of ``unique``, ``factorize``, ``argsort``, ``argmin``, ``argmax``, or ``searchsorted`` (:issue:`24433`)pandas/core/indexes/accessors.py71 + / 0 −
@@ -44,11 +44,14 @@ PandasObject, ) from pandas.core.indexes.datetimes import DatetimeIndex+from pandas.core.indexes.period import PeriodIndex from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.tseries import frequencies if TYPE_CHECKING:+ from pandas._typing import Frequency+ from pandas import ( DataFrame, Series,@@ -134,6 +137,74 @@ def _delegate_method(self, name: str, *args, **kwargs): self._parent ) + def shift(self, periods: int = 1, freq: Frequency | None = None) -> Series:+ """+ Shift the values by desired number of time frequency increments.++ Unlike :meth:`Series.shift`, which moves values to different positions,+ this adds ``periods * freq`` to each value, like :meth:`DatetimeIndex.shift`.++ Parameters+ ----------+ periods : int, default 1+ Number of periods (or increments) to shift by,+ can be positive or negative.+ freq : pandas.DateOffset, pandas.Timedelta or str, optional+ Frequency increment to shift by. If None, the frequency inferred+ from the values (see :attr:`Series.dt.freq`) is used, so values in+ descending order move backward for positive ``periods``. Must be+ None for period values, which are shifted by their own frequency.++ Returns+ -------+ Series+ Series with the shifted values, indexed like the original Series.++ Raises+ ------+ NullFrequencyError+ If ``freq`` is None, ``periods`` is nonzero, and no frequency can+ be inferred from the values.+ TypeError+ If ``freq`` is passed for period values.++ See Also+ --------+ DatetimeIndex.shift : Shift values of DatetimeIndex.+ Series.shift : Shift values by desired number of positions.++ Examples+ --------+ >>> ser = pd.Series(pd.date_range("2020-01-01", periods=3, freq="D"))+ >>> ser.dt.shift(1)+ 0 2020-01-02+ 1 2020-01-03+ 2 2020-01-04+ dtype: datetime64[us]++ >>> ser.dt.shift(2, freq="h")+ 0 2020-01-01 02:00:00+ 1 2020-01-02 02:00:00+ 2 2020-01-03 02:00:00+ dtype: datetime64[us]+ """+ from pandas import Series++ values = self._get_values()+ if isinstance(values, PeriodArray):+ if freq is not None:+ raise TypeError(+ "`freq` argument is not supported for Series.dt.shift "+ "with period values"+ )+ values = PeriodIndex(values, copy=False)+ elif freq is None:+ freq = values._inferred_freq_str+ result = values.shift(periods, freq)+ return Series(+ result._values, index=self._parent.index, name=self.name+ ).__finalize__(self._parent)+ @delegate_names( delegate=ArrowExtensionArray,pandas/tests/series/accessors/test_dt_accessor.py56 + / 2 −
@@ -11,7 +11,10 @@ import pytest from pandas._libs.tslibs.timezones import maybe_get_tz-from pandas.errors import Pandas4Warning+from pandas.errors import (+ NullFrequencyError,+ Pandas4Warning,+) from pandas.core.dtypes.common import ( is_integer_dtype,@@ -30,7 +33,7 @@ _deprecated_dt_attrs = {"dayofweek", "dayofyear", "daysinmonth", "weekday"} ok_for_period = PeriodArray._datetimelike_ops + list(_deprecated_dt_attrs)-ok_for_period_methods = ["strftime", "to_timestamp", "asfreq"]+ok_for_period_methods = ["strftime", "to_timestamp", "asfreq", "shift"] # ``freq`` is exposed on the dt accessor (DatetimeProperties) but lives there # directly rather than on the underlying array, so add it explicitly. ok_for_dt = [*DatetimeArray._datetimelike_ops, "freq", *list(_deprecated_dt_attrs)]@@ -48,6 +51,7 @@ "month_name", "isocalendar", "as_unit",+ "shift", ] ok_for_td = [*TimedeltaArray._datetimelike_ops, "freq"] ok_for_td_methods = [@@ -58,6 +62,7 @@ "floor", "ceil", "as_unit",+ "shift", ] @@ -1064,3 +1069,52 @@ def test_deprecated_aliases(ser): with tm.assert_produces_warning(Pandas4Warning, match=msg.format("daysinmonth")): result = ser.dt.daysinmonth tm.assert_series_equal(result, ser.dt.days_in_month)+++@pytest.mark.parametrize(+ "values",+ [+ pd.date_range("2020-01-01", periods=3, freq="D"),+ pd.date_range("2020-03-07", periods=3, freq="D", tz="US/Eastern"),+ pd.date_range("2020-01-31", periods=3, freq="ME"),+ pd.timedelta_range("1 day", periods=3, freq="h"),+ pd.period_range("2020-01", periods=3, freq="M"),+ ],+ ids=["datetime64", "datetime64tz", "month_end", "timedelta64", "period"],+)+@pytest.mark.parametrize("periods", [2, -1, 0])+def test_dt_shift(values, periods):+ # GH#31705 without freq, shift by the inferred frequency like Index.shift+ ser = pd.Series(values, index=list("abc"), name="xxx")+ expected = pd.Series(values.shift(periods), index=ser.index, name="xxx")++ result = ser.dt.shift(periods)+ tm.assert_series_equal(result, expected)++ result = ser.astype("category").dt.shift(periods)+ tm.assert_series_equal(result, expected)+++@pytest.mark.parametrize(+ "ser",+ [+ pd.Series(pd.to_datetime(["2020-01-01", "2020-01-03", pd.NaT])),+ pd.Series(pd.to_timedelta(["1 day", "3 days", pd.NaT])),+ ],+ ids=["datetime64", "timedelta64"],+)+def test_dt_shift_freq(ser):+ # GH#31705 an explicit freq does not require regular values+ result = ser.dt.shift(-2, freq="h")+ expected = ser - pd.Timedelta(hours=2)+ tm.assert_series_equal(result, expected)++ with pytest.raises(NullFrequencyError, match="Cannot shift with no freq"):+ ser.dt.shift(1)+ tm.assert_series_equal(ser.dt.shift(0), ser)+++def test_dt_shift_period_freq_raises():+ ser = pd.Series(pd.period_range("2020-01", periods=3, freq="M"))+ with pytest.raises(TypeError, match="not supported for Series.dt.shift"):+ ser.dt.shift(1, freq="M")