Skip to content

Commit 269fe0d

Browse files
jbrockmendelPingviinituutti
authored andcommitted
DEPR: deprecate integer add/sub with DTI/TDI/PI/Timestamp/Period (pandas-dev#22535)
1 parent f4e368f commit 269fe0d

File tree

21 files changed

+406
-168
lines changed

21 files changed

+406
-168
lines changed

doc/source/whatsnew/v0.24.0.txt

+50
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,56 @@ Deprecations
955955
- :meth:`Timestamp.tz_localize`, :meth:`DatetimeIndex.tz_localize`, and :meth:`Series.tz_localize` have deprecated the ``errors`` argument in favor of the ``nonexistent`` argument (:issue:`8917`)
956956
- The class ``FrozenNDArray`` has been deprecated. When unpickling, ``FrozenNDArray`` will be unpickled to ``np.ndarray`` once this class is removed (:issue:`9031`)
957957

958+
.. _whatsnew_0240.deprecations.datetimelike_int_ops:
959+
960+
Integer Addition/Subtraction with Datetime-like Classes Is Deprecated
961+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
962+
In the past, users could add or subtract integers or integer-dtypes arrays
963+
from :class:`Period`, :class:`PeriodIndex`, and in some cases
964+
:class:`Timestamp`, :class:`DatetimeIndex` and :class:`TimedeltaIndex`.
965+
966+
This usage is now deprecated. Instead add or subtract integer multiples of
967+
the object's ``freq`` attribute (:issue:`21939`)
968+
969+
Previous Behavior:
970+
971+
.. code-block:: ipython
972+
973+
In [3]: per = pd.Period('2016Q1')
974+
In [4]: per + 3
975+
Out[4]: Period('2016Q4', 'Q-DEC')
976+
977+
In [5]: ts = pd.Timestamp('1994-05-06 12:15:16', freq=pd.offsets.Hour())
978+
In [6]: ts + 2
979+
Out[6]: Timestamp('1994-05-06 14:15:16', freq='H')
980+
981+
In [7]: tdi = pd.timedelta_range('1D', periods=2)
982+
In [8]: tdi - np.array([2, 1])
983+
Out[8]: TimedeltaIndex(['-1 days', '1 days'], dtype='timedelta64[ns]', freq=None)
984+
985+
In [9]: dti = pd.date_range('2001-01-01', periods=2, freq='7D')
986+
In [10]: dti + pd.Index([1, 2])
987+
Out[10]: DatetimeIndex(['2001-01-08', '2001-01-22'], dtype='datetime64[ns]', freq=None)
988+
989+
Current Behavior:
990+
991+
.. ipython:: python
992+
:okwarning:
993+
per = pd.Period('2016Q1')
994+
per + 3
995+
996+
per = pd.Period('2016Q1')
997+
per + 3 * per.freq
998+
999+
ts = pd.Timestamp('1994-05-06 12:15:16', freq=pd.offsets.Hour())
1000+
ts + 2 * ts.freq
1001+
1002+
tdi = pd.timedelta_range('1D', periods=2)
1003+
tdi - np.array([2 * tdi.freq, 1 * tdi.freq])
1004+
1005+
dti = pd.date_range('2001-01-01', periods=2, freq='7D')
1006+
dti + pd.Index([1 * dti.freq, 2 * dti.freq])
1007+
9581008
.. _whatsnew_0240.prior_deprecations:
9591009

9601010
Removal of prior version deprecations/changes

pandas/_libs/tslibs/period.pyx

+8-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ cdef extern from "src/datetime/np_datetime.h":
3333
cimport util
3434
from util cimport is_period_object, is_string_object
3535

36-
from timestamps import Timestamp
36+
from timestamps import Timestamp, maybe_integer_op_deprecated
3737
from timezones cimport is_utc, is_tzlocal, get_dst_info
3838
from timedeltas import Timedelta
3939
from timedeltas cimport delta_to_nanoseconds
@@ -1645,6 +1645,8 @@ cdef class _Period(object):
16451645
elif other is NaT:
16461646
return NaT
16471647
elif util.is_integer_object(other):
1648+
maybe_integer_op_deprecated(self)
1649+
16481650
ordinal = self.ordinal + other * self.freq.n
16491651
return Period(ordinal=ordinal, freq=self.freq)
16501652
elif (PyDateTime_Check(other) or
@@ -1671,6 +1673,8 @@ cdef class _Period(object):
16711673
neg_other = -other
16721674
return self + neg_other
16731675
elif util.is_integer_object(other):
1676+
maybe_integer_op_deprecated(self)
1677+
16741678
ordinal = self.ordinal - other * self.freq.n
16751679
return Period(ordinal=ordinal, freq=self.freq)
16761680
elif is_period_object(other):
@@ -1756,7 +1760,7 @@ cdef class _Period(object):
17561760
def end_time(self):
17571761
# freq.n can't be negative or 0
17581762
# ordinal = (self + self.freq.n).start_time.value - 1
1759-
ordinal = (self + 1).start_time.value - 1
1763+
ordinal = (self + self.freq).start_time.value - 1
17601764
return Timestamp(ordinal)
17611765

17621766
def to_timestamp(self, freq=None, how='start', tz=None):
@@ -1783,7 +1787,8 @@ cdef class _Period(object):
17831787

17841788
end = how == 'E'
17851789
if end:
1786-
return (self + 1).to_timestamp(how='start') - Timedelta(1, 'ns')
1790+
endpoint = (self + self.freq).to_timestamp(how='start')
1791+
return endpoint - Timedelta(1, 'ns')
17871792

17881793
if freq is None:
17891794
base, mult = get_freq_code(self.freq)

pandas/_libs/tslibs/timestamps.pyx

+15-1
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,19 @@ from timezones cimport (
4040
_zero_time = datetime_time(0, 0)
4141
_no_input = object()
4242

43+
4344
# ----------------------------------------------------------------------
4445

46+
def maybe_integer_op_deprecated(obj):
47+
# GH#22535 add/sub of integers and int-arrays is deprecated
48+
if obj.freq is not None:
49+
warnings.warn("Addition/subtraction of integers and integer-arrays "
50+
"to {cls} is deprecated, will be removed in a future "
51+
"version. Instead of adding/subtracting `n`, use "
52+
"`n * self.freq`"
53+
.format(cls=type(obj).__name__),
54+
FutureWarning)
55+
4556

4657
cdef inline object create_timestamp_from_ts(int64_t value,
4758
npy_datetimestruct dts,
@@ -315,14 +326,17 @@ cdef class _Timestamp(datetime):
315326
return np.datetime64(self.value, 'ns')
316327

317328
def __add__(self, other):
318-
cdef int64_t other_int, nanos
329+
cdef:
330+
int64_t other_int, nanos
319331

320332
if is_timedelta64_object(other):
321333
other_int = other.astype('timedelta64[ns]').view('i8')
322334
return Timestamp(self.value + other_int,
323335
tz=self.tzinfo, freq=self.freq)
324336

325337
elif is_integer_object(other):
338+
maybe_integer_op_deprecated(self)
339+
326340
if self is NaT:
327341
# to be compat with Period
328342
return NaT

pandas/core/arrays/datetimelike.py

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pandas._libs import lib, iNaT, NaT
99
from pandas._libs.tslibs import timezones
1010
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds, Timedelta
11+
from pandas._libs.tslibs.timestamps import maybe_integer_op_deprecated
1112
from pandas._libs.tslibs.period import (
1213
Period, DIFFERENT_FREQ_INDEX, IncompatibleFrequency)
1314

@@ -634,6 +635,7 @@ def __add__(self, other):
634635
elif lib.is_integer(other):
635636
# This check must come after the check for np.timedelta64
636637
# as is_integer returns True for these
638+
maybe_integer_op_deprecated(self)
637639
result = self._time_shift(other)
638640

639641
# array-like others
@@ -647,6 +649,7 @@ def __add__(self, other):
647649
# DatetimeIndex, ndarray[datetime64]
648650
return self._add_datetime_arraylike(other)
649651
elif is_integer_dtype(other):
652+
maybe_integer_op_deprecated(self)
650653
result = self._addsub_int_array(other, operator.add)
651654
elif is_float_dtype(other):
652655
# Explicitly catch invalid dtypes
@@ -692,7 +695,9 @@ def __sub__(self, other):
692695
elif lib.is_integer(other):
693696
# This check must come after the check for np.timedelta64
694697
# as is_integer returns True for these
698+
maybe_integer_op_deprecated(self)
695699
result = self._time_shift(-other)
700+
696701
elif isinstance(other, Period):
697702
result = self._sub_period(other)
698703

@@ -710,6 +715,7 @@ def __sub__(self, other):
710715
# PeriodIndex
711716
result = self._sub_period_array(other)
712717
elif is_integer_dtype(other):
718+
maybe_integer_op_deprecated(self)
713719
result = self._addsub_int_array(other, operator.sub)
714720
elif isinstance(other, ABCIndexClass):
715721
raise TypeError("cannot subtract {cls} and {typ}"

pandas/core/arrays/period.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ def to_timestamp(self, freq=None, how='start'):
592592
return self.to_timestamp(how='start') + adjust
593593
else:
594594
adjust = Timedelta(1, 'ns')
595-
return (self + 1).to_timestamp(how='start') - adjust
595+
return (self + self.freq).to_timestamp(how='start') - adjust
596596

597597
if freq is None:
598598
base, mult = frequencies.get_freq_code(self.freq)
@@ -718,10 +718,11 @@ def _sub_period(self, other):
718718
@Appender(dtl.DatetimeLikeArrayMixin._addsub_int_array.__doc__)
719719
def _addsub_int_array(
720720
self,
721-
other, # type: Union[Index, ExtensionArray, np.ndarray[int]]
722-
op, # type: Callable[Any, Any]
721+
other, # type: Union[Index, ExtensionArray, np.ndarray[int]]
722+
op # type: Callable[Any, Any]
723723
):
724724
# type: (...) -> PeriodArray
725+
725726
assert op in [operator.add, operator.sub]
726727
if op is operator.sub:
727728
other = -other

pandas/core/resample.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -1429,7 +1429,7 @@ def _get_time_delta_bins(self, ax):
14291429
freq=self.freq,
14301430
name=ax.name)
14311431

1432-
end_stamps = labels + 1
1432+
end_stamps = labels + self.freq
14331433
bins = ax.searchsorted(end_stamps, side='left')
14341434

14351435
# Addresses GH #10530
@@ -1443,17 +1443,18 @@ def _get_time_period_bins(self, ax):
14431443
raise TypeError('axis must be a DatetimeIndex, but got '
14441444
'an instance of %r' % type(ax).__name__)
14451445

1446+
freq = self.freq
1447+
14461448
if not len(ax):
1447-
binner = labels = PeriodIndex(
1448-
data=[], freq=self.freq, name=ax.name)
1449+
binner = labels = PeriodIndex(data=[], freq=freq, name=ax.name)
14491450
return binner, [], labels
14501451

14511452
labels = binner = PeriodIndex(start=ax[0],
14521453
end=ax[-1],
1453-
freq=self.freq,
1454+
freq=freq,
14541455
name=ax.name)
14551456

1456-
end_stamps = (labels + 1).asfreq(self.freq, 's').to_timestamp()
1457+
end_stamps = (labels + freq).asfreq(freq, 's').to_timestamp()
14571458
if ax.tzinfo:
14581459
end_stamps = end_stamps.tz_localize(ax.tzinfo)
14591460
bins = ax.searchsorted(end_stamps, side='left')

pandas/plotting/_converter.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ def period_break(dates, period):
574574
Name of the period to monitor.
575575
"""
576576
current = getattr(dates, period)
577-
previous = getattr(dates - 1, period)
577+
previous = getattr(dates - 1 * dates.freq, period)
578578
return np.nonzero(current - previous)[0]
579579

580580

@@ -660,7 +660,7 @@ def first_label(label_flags):
660660

661661
def _hour_finder(label_interval, force_year_start):
662662
_hour = dates_.hour
663-
_prev_hour = (dates_ - 1).hour
663+
_prev_hour = (dates_ - 1 * dates_.freq).hour
664664
hour_start = (_hour - _prev_hour) != 0
665665
info_maj[day_start] = True
666666
info_min[hour_start & (_hour % label_interval == 0)] = True
@@ -674,7 +674,7 @@ def _hour_finder(label_interval, force_year_start):
674674
def _minute_finder(label_interval):
675675
hour_start = period_break(dates_, 'hour')
676676
_minute = dates_.minute
677-
_prev_minute = (dates_ - 1).minute
677+
_prev_minute = (dates_ - 1 * dates_.freq).minute
678678
minute_start = (_minute - _prev_minute) != 0
679679
info_maj[hour_start] = True
680680
info_min[minute_start & (_minute % label_interval == 0)] = True
@@ -687,7 +687,7 @@ def _minute_finder(label_interval):
687687
def _second_finder(label_interval):
688688
minute_start = period_break(dates_, 'minute')
689689
_second = dates_.second
690-
_prev_second = (dates_ - 1).second
690+
_prev_second = (dates_ - 1 * dates_.freq).second
691691
second_start = (_second - _prev_second) != 0
692692
info['maj'][minute_start] = True
693693
info['min'][second_start & (_second % label_interval == 0)] = True

0 commit comments

Comments
 (0)