Skip to content

Commit

Permalink
DEPR: deprecate integer add/sub with DTI/TDI/PI/Timestamp/Period (pan…
Browse files Browse the repository at this point in the history
  • Loading branch information
jbrockmendel authored and tm9k1 committed Nov 19, 2018
1 parent 83a77e5 commit 0889d07
Show file tree
Hide file tree
Showing 21 changed files with 406 additions and 168 deletions.
50 changes: 50 additions & 0 deletions doc/source/whatsnew/v0.24.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,56 @@ Deprecations
- :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`)
- The class ``FrozenNDArray`` has been deprecated. When unpickling, ``FrozenNDArray`` will be unpickled to ``np.ndarray`` once this class is removed (:issue:`9031`)

.. _whatsnew_0240.deprecations.datetimelike_int_ops:

Integer Addition/Subtraction with Datetime-like Classes Is Deprecated
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the past, users could add or subtract integers or integer-dtypes arrays
from :class:`Period`, :class:`PeriodIndex`, and in some cases
:class:`Timestamp`, :class:`DatetimeIndex` and :class:`TimedeltaIndex`.

This usage is now deprecated. Instead add or subtract integer multiples of
the object's ``freq`` attribute (:issue:`21939`)

Previous Behavior:

.. code-block:: ipython

In [3]: per = pd.Period('2016Q1')
In [4]: per + 3
Out[4]: Period('2016Q4', 'Q-DEC')

In [5]: ts = pd.Timestamp('1994-05-06 12:15:16', freq=pd.offsets.Hour())
In [6]: ts + 2
Out[6]: Timestamp('1994-05-06 14:15:16', freq='H')

In [7]: tdi = pd.timedelta_range('1D', periods=2)
In [8]: tdi - np.array([2, 1])
Out[8]: TimedeltaIndex(['-1 days', '1 days'], dtype='timedelta64[ns]', freq=None)

In [9]: dti = pd.date_range('2001-01-01', periods=2, freq='7D')
In [10]: dti + pd.Index([1, 2])
Out[10]: DatetimeIndex(['2001-01-08', '2001-01-22'], dtype='datetime64[ns]', freq=None)

Current Behavior:

.. ipython:: python
:okwarning:
per = pd.Period('2016Q1')
per + 3

per = pd.Period('2016Q1')
per + 3 * per.freq

ts = pd.Timestamp('1994-05-06 12:15:16', freq=pd.offsets.Hour())
ts + 2 * ts.freq

tdi = pd.timedelta_range('1D', periods=2)
tdi - np.array([2 * tdi.freq, 1 * tdi.freq])

dti = pd.date_range('2001-01-01', periods=2, freq='7D')
dti + pd.Index([1 * dti.freq, 2 * dti.freq])

.. _whatsnew_0240.prior_deprecations:

Removal of prior version deprecations/changes
Expand Down
11 changes: 8 additions & 3 deletions pandas/_libs/tslibs/period.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ cdef extern from "src/datetime/np_datetime.h":
cimport util
from util cimport is_period_object, is_string_object

from timestamps import Timestamp
from timestamps import Timestamp, maybe_integer_op_deprecated
from timezones cimport is_utc, is_tzlocal, get_dst_info
from timedeltas import Timedelta
from timedeltas cimport delta_to_nanoseconds
Expand Down Expand Up @@ -1645,6 +1645,8 @@ cdef class _Period(object):
elif other is NaT:
return NaT
elif util.is_integer_object(other):
maybe_integer_op_deprecated(self)

ordinal = self.ordinal + other * self.freq.n
return Period(ordinal=ordinal, freq=self.freq)
elif (PyDateTime_Check(other) or
Expand All @@ -1671,6 +1673,8 @@ cdef class _Period(object):
neg_other = -other
return self + neg_other
elif util.is_integer_object(other):
maybe_integer_op_deprecated(self)

ordinal = self.ordinal - other * self.freq.n
return Period(ordinal=ordinal, freq=self.freq)
elif is_period_object(other):
Expand Down Expand Up @@ -1756,7 +1760,7 @@ cdef class _Period(object):
def end_time(self):
# freq.n can't be negative or 0
# ordinal = (self + self.freq.n).start_time.value - 1
ordinal = (self + 1).start_time.value - 1
ordinal = (self + self.freq).start_time.value - 1
return Timestamp(ordinal)

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

end = how == 'E'
if end:
return (self + 1).to_timestamp(how='start') - Timedelta(1, 'ns')
endpoint = (self + self.freq).to_timestamp(how='start')
return endpoint - Timedelta(1, 'ns')

if freq is None:
base, mult = get_freq_code(self.freq)
Expand Down
16 changes: 15 additions & 1 deletion pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,19 @@ from timezones cimport (
_zero_time = datetime_time(0, 0)
_no_input = object()


# ----------------------------------------------------------------------

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


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

def __add__(self, other):
cdef int64_t other_int, nanos
cdef:
int64_t other_int, nanos

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

elif is_integer_object(other):
maybe_integer_op_deprecated(self)

if self is NaT:
# to be compat with Period
return NaT
Expand Down
6 changes: 6 additions & 0 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pandas._libs import lib, iNaT, NaT
from pandas._libs.tslibs import timezones
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds, Timedelta
from pandas._libs.tslibs.timestamps import maybe_integer_op_deprecated
from pandas._libs.tslibs.period import (
Period, DIFFERENT_FREQ_INDEX, IncompatibleFrequency)

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

# array-like others
Expand All @@ -647,6 +649,7 @@ def __add__(self, other):
# DatetimeIndex, ndarray[datetime64]
return self._add_datetime_arraylike(other)
elif is_integer_dtype(other):
maybe_integer_op_deprecated(self)
result = self._addsub_int_array(other, operator.add)
elif is_float_dtype(other):
# Explicitly catch invalid dtypes
Expand Down Expand Up @@ -692,7 +695,9 @@ def __sub__(self, other):
elif lib.is_integer(other):
# This check must come after the check for np.timedelta64
# as is_integer returns True for these
maybe_integer_op_deprecated(self)
result = self._time_shift(-other)

elif isinstance(other, Period):
result = self._sub_period(other)

Expand All @@ -710,6 +715,7 @@ def __sub__(self, other):
# PeriodIndex
result = self._sub_period_array(other)
elif is_integer_dtype(other):
maybe_integer_op_deprecated(self)
result = self._addsub_int_array(other, operator.sub)
elif isinstance(other, ABCIndexClass):
raise TypeError("cannot subtract {cls} and {typ}"
Expand Down
7 changes: 4 additions & 3 deletions pandas/core/arrays/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ def to_timestamp(self, freq=None, how='start'):
return self.to_timestamp(how='start') + adjust
else:
adjust = Timedelta(1, 'ns')
return (self + 1).to_timestamp(how='start') - adjust
return (self + self.freq).to_timestamp(how='start') - adjust

if freq is None:
base, mult = frequencies.get_freq_code(self.freq)
Expand Down Expand Up @@ -718,10 +718,11 @@ def _sub_period(self, other):
@Appender(dtl.DatetimeLikeArrayMixin._addsub_int_array.__doc__)
def _addsub_int_array(
self,
other, # type: Union[Index, ExtensionArray, np.ndarray[int]]
op, # type: Callable[Any, Any]
other, # type: Union[Index, ExtensionArray, np.ndarray[int]]
op # type: Callable[Any, Any]
):
# type: (...) -> PeriodArray

assert op in [operator.add, operator.sub]
if op is operator.sub:
other = -other
Expand Down
11 changes: 6 additions & 5 deletions pandas/core/resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -1429,7 +1429,7 @@ def _get_time_delta_bins(self, ax):
freq=self.freq,
name=ax.name)

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

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

freq = self.freq

if not len(ax):
binner = labels = PeriodIndex(
data=[], freq=self.freq, name=ax.name)
binner = labels = PeriodIndex(data=[], freq=freq, name=ax.name)
return binner, [], labels

labels = binner = PeriodIndex(start=ax[0],
end=ax[-1],
freq=self.freq,
freq=freq,
name=ax.name)

end_stamps = (labels + 1).asfreq(self.freq, 's').to_timestamp()
end_stamps = (labels + freq).asfreq(freq, 's').to_timestamp()
if ax.tzinfo:
end_stamps = end_stamps.tz_localize(ax.tzinfo)
bins = ax.searchsorted(end_stamps, side='left')
Expand Down
8 changes: 4 additions & 4 deletions pandas/plotting/_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ def period_break(dates, period):
Name of the period to monitor.
"""
current = getattr(dates, period)
previous = getattr(dates - 1, period)
previous = getattr(dates - 1 * dates.freq, period)
return np.nonzero(current - previous)[0]


Expand Down Expand Up @@ -660,7 +660,7 @@ def first_label(label_flags):

def _hour_finder(label_interval, force_year_start):
_hour = dates_.hour
_prev_hour = (dates_ - 1).hour
_prev_hour = (dates_ - 1 * dates_.freq).hour
hour_start = (_hour - _prev_hour) != 0
info_maj[day_start] = True
info_min[hour_start & (_hour % label_interval == 0)] = True
Expand All @@ -674,7 +674,7 @@ def _hour_finder(label_interval, force_year_start):
def _minute_finder(label_interval):
hour_start = period_break(dates_, 'hour')
_minute = dates_.minute
_prev_minute = (dates_ - 1).minute
_prev_minute = (dates_ - 1 * dates_.freq).minute
minute_start = (_minute - _prev_minute) != 0
info_maj[hour_start] = True
info_min[minute_start & (_minute % label_interval == 0)] = True
Expand All @@ -687,7 +687,7 @@ def _minute_finder(label_interval):
def _second_finder(label_interval):
minute_start = period_break(dates_, 'minute')
_second = dates_.second
_prev_second = (dates_ - 1).second
_prev_second = (dates_ - 1 * dates_.freq).second
second_start = (_second - _prev_second) != 0
info['maj'][minute_start] = True
info['min'][second_start & (_second % label_interval == 0)] = True
Expand Down
Loading

0 comments on commit 0889d07

Please sign in to comment.