Skip to content

Commit 9bc3ee0

Browse files
authored
REG: dt64 shift with integer fill_value (#32591)
1 parent 1b76440 commit 9bc3ee0

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
@@ -745,6 +745,57 @@ def _from_factorized(cls, values, original):
745745
def _values_for_argsort(self):
746746
return self._data
747747

748+
@Appender(ExtensionArray.shift.__doc__)
749+
def shift(self, periods=1, fill_value=None, axis=0):
750+
if not self.size or periods == 0:
751+
return self.copy()
752+
753+
if is_valid_nat_for_dtype(fill_value, self.dtype):
754+
fill_value = NaT
755+
elif not isinstance(fill_value, self._recognized_scalars):
756+
# only warn if we're not going to raise
757+
if self._scalar_type is Period and lib.is_integer(fill_value):
758+
# kludge for #31971 since Period(integer) tries to cast to str
759+
new_fill = Period._from_ordinal(fill_value, freq=self.freq)
760+
else:
761+
new_fill = self._scalar_type(fill_value)
762+
763+
# stacklevel here is chosen to be correct when called from
764+
# DataFrame.shift or Series.shift
765+
warnings.warn(
766+
f"Passing {type(fill_value)} to shift is deprecated and "
767+
"will raise in a future version, pass "
768+
f"{self._scalar_type.__name__} instead.",
769+
FutureWarning,
770+
stacklevel=7,
771+
)
772+
fill_value = new_fill
773+
774+
fill_value = self._unbox_scalar(fill_value)
775+
776+
new_values = self._data
777+
778+
# make sure array sent to np.roll is c_contiguous
779+
f_ordered = new_values.flags.f_contiguous
780+
if f_ordered:
781+
new_values = new_values.T
782+
axis = new_values.ndim - axis - 1
783+
784+
new_values = np.roll(new_values, periods, axis=axis)
785+
786+
axis_indexer = [slice(None)] * self.ndim
787+
if periods > 0:
788+
axis_indexer[axis] = slice(None, periods)
789+
else:
790+
axis_indexer[axis] = slice(periods, None)
791+
new_values[tuple(axis_indexer)] = fill_value
792+
793+
# restore original order
794+
if f_ordered:
795+
new_values = new_values.T
796+
797+
return type(self)._simple_new(new_values, dtype=self.dtype)
798+
748799
# ------------------------------------------------------------------
749800
# Additional array methods
750801
# 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
@@ -1917,10 +1917,7 @@ def diff(self, n: int, axis: int = 1) -> List["Block"]:
19171917
return super().diff(n, axis)
19181918

19191919
def shift(
1920-
self,
1921-
periods: int,
1922-
axis: libinternals.BlockPlacement = 0,
1923-
fill_value: Any = None,
1920+
self, periods: int, axis: int = 0, fill_value: Any = None,
19241921
) -> List["ExtensionBlock"]:
19251922
"""
19261923
Shift the block by `periods`.
@@ -2173,14 +2170,20 @@ def internal_values(self):
21732170

21742171
def iget(self, key):
21752172
# GH#31649 we need to wrap scalars in Timestamp/Timedelta
2176-
# TODO: this can be removed if we ever have 2D EA
2173+
# TODO(EA2D): this can be removed if we ever have 2D EA
21772174
result = super().iget(key)
21782175
if isinstance(result, np.datetime64):
21792176
result = Timestamp(result)
21802177
elif isinstance(result, np.timedelta64):
21812178
result = Timedelta(result)
21822179
return result
21832180

2181+
def shift(self, periods, axis=0, fill_value=None):
2182+
# TODO(EA2D) this is unnecessary if these blocks are backed by 2D EAs
2183+
values = self.array_values()
2184+
new_values = values.shift(periods, fill_value=fill_value, axis=axis)
2185+
return self.make_block_same_class(new_values)
2186+
21842187

21852188
class DatetimeBlock(DatetimeLikeBlockMixin, Block):
21862189
__slots__ = ()

pandas/tests/arrays/test_datetimelike.py

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

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

244261
class TestDatetimeArray(SharedTests):
245262
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)