Skip to content

Commit abadc4f

Browse files
Backport PR #32591: REG: dt64 shift with integer fill_value (#32647)
1 parent 0e36d7e commit abadc4f

File tree

6 files changed

+110
-5
lines changed

6 files changed

+110
-5
lines changed

doc/source/whatsnew/v1.0.2.rst

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Fixed regressions
2929
- Fixed regression in :meth:`read_csv` in which the ``encoding`` option was not recognized with certain file-like objects (:issue:`31819`)
3030
- Fixed regression in :meth:`DataFrame.reindex` and :meth:`Series.reindex` when reindexing with (tz-aware) index and ``method=nearest`` (:issue:`26683`)
3131
- Fixed regression in :meth:`DataFrame.reindex_like` on a :class:`DataFrame` subclass raised an ``AssertionError`` (:issue:`31925`)
32+
- Fixed regression in :meth:`Series.shift` with ``datetime64`` dtype when passing an integer ``fill_value`` (:issue:`32591`)
3233

3334

3435
.. ---------------------------------------------------------------------------

pandas/core/arrays/datetimelike.py

+51
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,57 @@ def _from_factorized(cls, values, original):
725725
def _values_for_argsort(self):
726726
return self._data
727727

728+
@Appender(ExtensionArray.shift.__doc__)
729+
def shift(self, periods=1, fill_value=None, axis=0):
730+
if not self.size or periods == 0:
731+
return self.copy()
732+
733+
if is_valid_nat_for_dtype(fill_value, self.dtype):
734+
fill_value = NaT
735+
elif not isinstance(fill_value, self._recognized_scalars):
736+
# only warn if we're not going to raise
737+
if self._scalar_type is Period and lib.is_integer(fill_value):
738+
# kludge for #31971 since Period(integer) tries to cast to str
739+
new_fill = Period._from_ordinal(fill_value, freq=self.freq)
740+
else:
741+
new_fill = self._scalar_type(fill_value)
742+
743+
# stacklevel here is chosen to be correct when called from
744+
# DataFrame.shift or Series.shift
745+
warnings.warn(
746+
f"Passing {type(fill_value)} to shift is deprecated and "
747+
"will raise in a future version, pass "
748+
f"{self._scalar_type.__name__} instead.",
749+
FutureWarning,
750+
stacklevel=7,
751+
)
752+
fill_value = new_fill
753+
754+
fill_value = self._unbox_scalar(fill_value)
755+
756+
new_values = self._data
757+
758+
# make sure array sent to np.roll is c_contiguous
759+
f_ordered = new_values.flags.f_contiguous
760+
if f_ordered:
761+
new_values = new_values.T
762+
axis = new_values.ndim - axis - 1
763+
764+
new_values = np.roll(new_values, periods, axis=axis)
765+
766+
axis_indexer = [slice(None)] * self.ndim
767+
if periods > 0:
768+
axis_indexer[axis] = slice(None, periods)
769+
else:
770+
axis_indexer[axis] = slice(periods, None)
771+
new_values[tuple(axis_indexer)] = fill_value
772+
773+
# restore original order
774+
if f_ordered:
775+
new_values = new_values.T
776+
777+
return type(self)._simple_new(new_values, dtype=self.dtype)
778+
728779
# ------------------------------------------------------------------
729780
# Additional array methods
730781
# These are not part of the EA API, but we implement them because

pandas/core/internals/blocks.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -1897,10 +1897,7 @@ def diff(self, n: int, axis: int = 1) -> List["Block"]:
18971897
return super().diff(n, axis)
18981898

18991899
def shift(
1900-
self,
1901-
periods: int,
1902-
axis: libinternals.BlockPlacement = 0,
1903-
fill_value: Any = None,
1900+
self, periods: int, axis: int = 0, fill_value: Any = None,
19041901
) -> List["ExtensionBlock"]:
19051902
"""
19061903
Shift the block by `periods`.
@@ -2150,14 +2147,20 @@ def get_values(self, dtype=None):
21502147

21512148
def iget(self, key):
21522149
# GH#31649 we need to wrap scalars in Timestamp/Timedelta
2153-
# TODO: this can be removed if we ever have 2D EA
2150+
# TODO(EA2D): this can be removed if we ever have 2D EA
21542151
result = super().iget(key)
21552152
if isinstance(result, np.datetime64):
21562153
result = Timestamp(result)
21572154
elif isinstance(result, np.timedelta64):
21582155
result = Timedelta(result)
21592156
return result
21602157

2158+
def shift(self, periods, axis=0, fill_value=None):
2159+
# TODO(EA2D) this is unnecessary if these blocks are backed by 2D EAs
2160+
values = self.array_values()
2161+
new_values = values.shift(periods, fill_value=fill_value, axis=axis)
2162+
return self.make_block_same_class(new_values)
2163+
21612164

21622165
class DatetimeBlock(DatetimeLikeBlockMixin, Block):
21632166
__slots__ = ()

pandas/tests/arrays/test_datetimelike.py

+17
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,23 @@ def test_inplace_arithmetic(self):
239239
arr -= pd.Timedelta(days=1)
240240
tm.assert_equal(arr, expected)
241241

242+
def test_shift_fill_int_deprecated(self):
243+
# GH#31971
244+
data = np.arange(10, dtype="i8") * 24 * 3600 * 10 ** 9
245+
arr = self.array_cls(data, freq="D")
246+
247+
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
248+
result = arr.shift(1, fill_value=1)
249+
250+
expected = arr.copy()
251+
if self.array_cls is PeriodArray:
252+
fill_val = PeriodArray._scalar_type._from_ordinal(1, freq=arr.freq)
253+
else:
254+
fill_val = arr._scalar_type(1)
255+
expected[0] = fill_val
256+
expected[1:] = arr[:-1]
257+
tm.assert_equal(result, expected)
258+
242259

243260
class TestDatetimeArray(SharedTests):
244261
index_cls = pd.DatetimeIndex

pandas/tests/frame/methods/test_shift.py

+23
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,26 @@ def test_tshift(self, datetime_frame):
185185
msg = "Freq was not given and was not set in the index"
186186
with pytest.raises(ValueError, match=msg):
187187
no_freq.tshift()
188+
189+
def test_shift_dt64values_int_fill_deprecated(self):
190+
# GH#31971
191+
ser = pd.Series([pd.Timestamp("2020-01-01"), pd.Timestamp("2020-01-02")])
192+
df = ser.to_frame()
193+
194+
with tm.assert_produces_warning(FutureWarning):
195+
result = df.shift(1, fill_value=0)
196+
197+
expected = pd.Series([pd.Timestamp(0), ser[0]]).to_frame()
198+
tm.assert_frame_equal(result, expected)
199+
200+
# axis = 1
201+
df2 = pd.DataFrame({"A": ser, "B": ser})
202+
df2._consolidate_inplace()
203+
204+
with tm.assert_produces_warning(FutureWarning):
205+
result = df2.shift(1, axis=1, fill_value=0)
206+
207+
expected = pd.DataFrame(
208+
{"A": [pd.Timestamp(0), pd.Timestamp(0)], "B": df2["A"]}
209+
)
210+
tm.assert_frame_equal(result, expected)

pandas/tests/series/methods/test_shift.py

+10
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,13 @@ def test_shift_categorical(self):
263263

264264
tm.assert_index_equal(s.values.categories, sp1.values.categories)
265265
tm.assert_index_equal(s.values.categories, sn2.values.categories)
266+
267+
def test_shift_dt64values_int_fill_deprecated(self):
268+
# GH#31971
269+
ser = pd.Series([pd.Timestamp("2020-01-01"), pd.Timestamp("2020-01-02")])
270+
271+
with tm.assert_produces_warning(FutureWarning):
272+
result = ser.shift(1, fill_value=0)
273+
274+
expected = pd.Series([pd.Timestamp(0), ser[0]])
275+
tm.assert_series_equal(result, expected)

0 commit comments

Comments
 (0)