Skip to content

Commit ac83a2a

Browse files
committed
BUG: vectorized DateOffset match non-vectorized
1 parent faa6cc7 commit ac83a2a

File tree

5 files changed

+88
-29
lines changed

5 files changed

+88
-29
lines changed

doc/source/timeseries.rst

+46
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,52 @@ These can be used as arguments to ``date_range``, ``bdate_range``, constructors
954954
for ``DatetimeIndex``, as well as various other timeseries-related functions
955955
in pandas.
956956

957+
Anchored Offset Semantics
958+
~~~~~~~~~~~~~~~~~~~~~~~~~
959+
960+
For those offsets that are anchored to the start or end of specific
961+
frequency (``MonthEnd``, ``MonthBegin``, ``WeekEnd``, etc) the following
962+
rules apply to rolling forward and backwards.
963+
964+
When ``n`` is not 0, if the given date is not on an anchor point, it snapped to the next(previous)
965+
anchor point, and moved ``|n|-1`` additional steps forwards or backwards.
966+
967+
.. ipython:: python
968+
969+
pd.Timestamp('2014-01-02') + MonthBegin(n=1)
970+
pd.Timestamp('2014-01-02') + MonthEnd(n=1)
971+
972+
pd.Timestamp('2014-01-02') - MonthBegin(n=1)
973+
pd.Timestamp('2014-01-02') - MonthEnd(n=1)
974+
975+
pd.Timestamp('2014-01-02') + MonthBegin(n=4)
976+
pd.Timestamp('2014-01-02') - MonthBegin(n=4)
977+
978+
If the given date *is* on an anchor point, it is moved ``|n|`` points forwards
979+
or backwards.
980+
981+
.. ipython:: python
982+
983+
pd.Timestamp('2014-01-01') + MonthBegin(n=1)
984+
pd.Timestamp('2014-01-31') + MonthEnd(n=1)
985+
986+
pd.Timestamp('2014-01-01') - MonthBegin(n=1)
987+
pd.Timestamp('2014-01-31') - MonthEnd(n=1)
988+
989+
pd.Timestamp('2014-01-01') + MonthBegin(n=4)
990+
pd.Timestamp('2014-01-31') - MonthBegin(n=4)
991+
992+
For the case when ``n=0``, the date is not moved if on an anchor point, otherwise
993+
it is rolled forward to the next anchor point.
994+
995+
.. ipython:: python
996+
997+
pd.Timestamp('2014-01-02') + MonthBegin(n=0)
998+
pd.Timestamp('2014-01-02') + MonthEnd(n=0)
999+
1000+
pd.Timestamp('2014-01-01') + MonthBegin(n=0)
1001+
pd.Timestamp('2014-01-31') + MonthEnd(n=0)
1002+
9571003
.. _timeseries.legacyaliases:
9581004

9591005
Legacy Aliases

doc/source/whatsnew/v0.18.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ Bug Fixes
190190

191191

192192

193-
193+
- Bug in vectorized ``DateOffset`` when ``n`` parameter is ``0`` (:issue:`11370`)
194194

195195

196196

pandas/tseries/offsets.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def _beg_apply_index(self, i, freq):
444444
from pandas.tseries.frequencies import get_freq_code
445445
base, mult = get_freq_code(freq)
446446
base_period = i.to_period(base)
447-
if self.n < 0:
447+
if self.n <= 0:
448448
# when subtracting, dates on start roll to prior
449449
roll = np.where(base_period.to_timestamp() == i - off,
450450
self.n, self.n + 1)
@@ -464,7 +464,7 @@ def _end_apply_index(self, i, freq):
464464
base, mult = get_freq_code(freq)
465465
base_period = i.to_period(base)
466466
if self.n > 0:
467-
# when adding, dtates on end roll to next
467+
# when adding, dates on end roll to next
468468
roll = np.where(base_period.to_timestamp(how='end') == i - off,
469469
self.n, self.n - 1)
470470
else:
@@ -1081,8 +1081,7 @@ def apply(self, other):
10811081

10821082
@apply_index_wraps
10831083
def apply_index(self, i):
1084-
months = self.n - 1 if self.n >= 0 else self.n
1085-
shifted = tslib.shift_months(i.asi8, months, 'end')
1084+
shifted = tslib.shift_months(i.asi8, self.n, 'end')
10861085
return i._shallow_copy(shifted)
10871086

10881087
def onOffset(self, dt):
@@ -1108,8 +1107,7 @@ def apply(self, other):
11081107

11091108
@apply_index_wraps
11101109
def apply_index(self, i):
1111-
months = self.n + 1 if self.n < 0 else self.n
1112-
shifted = tslib.shift_months(i.asi8, months, 'start')
1110+
shifted = tslib.shift_months(i.asi8, self.n, 'start')
11131111
return i._shallow_copy(shifted)
11141112

11151113
def onOffset(self, dt):
@@ -1777,6 +1775,7 @@ def apply(self, other):
17771775
@apply_index_wraps
17781776
def apply_index(self, i):
17791777
freq_month = 12 if self.startingMonth == 1 else self.startingMonth - 1
1778+
# freq_month = self.startingMonth
17801779
freqstr = 'Q-%s' % (_int_to_month[freq_month],)
17811780
return self._beg_apply_index(i, freqstr)
17821781

pandas/tseries/tests/test_timeseries.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -2622,7 +2622,8 @@ def test_datetime64_with_DateOffset(self):
26222622
assert_func(result, exp)
26232623

26242624
s = klass([Timestamp('2000-01-05 00:15:00'), Timestamp('2000-01-31 00:23:00'),
2625-
Timestamp('2000-01-01'), Timestamp('2000-02-29'), Timestamp('2000-12-31')])
2625+
Timestamp('2000-01-01'), Timestamp('2000-03-31'),
2626+
Timestamp('2000-02-29'), Timestamp('2000-12-31')])
26262627

26272628
#DateOffset relativedelta fastpath
26282629
relative_kwargs = [('years', 2), ('months', 5), ('days', 3),
@@ -2659,11 +2660,15 @@ def test_datetime64_with_DateOffset(self):
26592660
else:
26602661
do = do
26612662
kwargs = {}
2662-
op = getattr(pd.offsets,do)(5, normalize=normalize, **kwargs)
2663-
assert_func(klass([x + op for x in s]), s + op)
2664-
assert_func(klass([x - op for x in s]), s - op)
2665-
assert_func(klass([op + x for x in s]), op + s)
26662663

2664+
for n in [0, 5]:
2665+
if (do in ['WeekOfMonth','LastWeekOfMonth',
2666+
'FY5253Quarter','FY5253'] and n == 0):
2667+
continue
2668+
op = getattr(pd.offsets,do)(n, normalize=normalize, **kwargs)
2669+
assert_func(klass([x + op for x in s]), s + op)
2670+
assert_func(klass([x - op for x in s]), s - op)
2671+
assert_func(klass([op + x for x in s]), op + s)
26672672
# def test_add_timedelta64(self):
26682673
# rng = date_range('1/1/2000', periods=5)
26692674
# delta = rng.values[3] - rng.values[1]

pandas/tslib.pyx

+26-17
Original file line numberDiff line numberDiff line change
@@ -4458,7 +4458,8 @@ def shift_months(int64_t[:] dtindex, int months, object day=None):
44584458
Py_ssize_t i
44594459
pandas_datetimestruct dts
44604460
int count = len(dtindex)
4461-
int days_in_current_month
4461+
int months_to_roll
4462+
bint roll_check
44624463
int64_t[:] out = np.empty(count, dtype='int64')
44634464

44644465
if day is None:
@@ -4472,36 +4473,44 @@ def shift_months(int64_t[:] dtindex, int months, object day=None):
44724473
dts.day = min(dts.day, days_in_month(dts))
44734474
out[i] = pandas_datetimestruct_to_datetime(PANDAS_FR_ns, &dts)
44744475
elif day == 'start':
4476+
roll_check = False
4477+
if months <= 0:
4478+
months += 1
4479+
roll_check = True
44754480
with nogil:
44764481
for i in range(count):
44774482
if dtindex[i] == NPY_NAT: out[i] = NPY_NAT; continue
44784483
pandas_datetime_to_datetimestruct(dtindex[i], PANDAS_FR_ns, &dts)
4479-
dts.year = _year_add_months(dts, months)
4480-
dts.month = _month_add_months(dts, months)
4484+
months_to_roll = months
4485+
4486+
# offset semantics - if on the anchor point and going backwards
4487+
# shift to next
4488+
if roll_check and dts.day == 1:
4489+
months_to_roll -= 1
4490+
4491+
dts.year = _year_add_months(dts, months_to_roll)
4492+
dts.month = _month_add_months(dts, months_to_roll)
4493+
dts.day = 1
44814494

4482-
# offset semantics - when subtracting if at the start anchor
4483-
# point, shift back by one more month
4484-
if months <= 0 and dts.day == 1:
4485-
dts.year = _year_add_months(dts, -1)
4486-
dts.month = _month_add_months(dts, -1)
4487-
else:
4488-
dts.day = 1
44894495
out[i] = pandas_datetimestruct_to_datetime(PANDAS_FR_ns, &dts)
44904496
elif day == 'end':
4497+
roll_check = False
4498+
if months > 0:
4499+
months -= 1
4500+
roll_check = True
44914501
with nogil:
44924502
for i in range(count):
44934503
if dtindex[i] == NPY_NAT: out[i] = NPY_NAT; continue
44944504
pandas_datetime_to_datetimestruct(dtindex[i], PANDAS_FR_ns, &dts)
4495-
days_in_current_month = days_in_month(dts)
4496-
4497-
dts.year = _year_add_months(dts, months)
4498-
dts.month = _month_add_months(dts, months)
4505+
months_to_roll = months
44994506

45004507
# similar semantics - when adding shift forward by one
45014508
# month if already at an end of month
4502-
if months >= 0 and dts.day == days_in_current_month:
4503-
dts.year = _year_add_months(dts, 1)
4504-
dts.month = _month_add_months(dts, 1)
4509+
if roll_check and dts.day == days_in_month(dts):
4510+
months_to_roll += 1
4511+
4512+
dts.year = _year_add_months(dts, months_to_roll)
4513+
dts.month = _month_add_months(dts, months_to_roll)
45054514

45064515
dts.day = days_in_month(dts)
45074516
out[i] = pandas_datetimestruct_to_datetime(PANDAS_FR_ns, &dts)

0 commit comments

Comments
 (0)