Skip to content

Commit 31f06e6

Browse files
committed
ENH: Added CustomBusinessMonth offset.
1 parent 6c36769 commit 31f06e6

File tree

6 files changed

+516
-28
lines changed

6 files changed

+516
-28
lines changed

doc/source/timeseries.rst

+18
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ frequency increment. Specific offset logic like "month", "business day", or
432432
MonthBegin, "calendar month begin"
433433
BMonthEnd, "business month end"
434434
BMonthBegin, "business month begin"
435+
CBMonthEnd, "custom business month end"
436+
CBMonthBegin, "custom business month begin"
435437
QuarterEnd, "calendar quarter end"
436438
QuarterBegin, "calendar quarter begin"
437439
BQuarterEnd, "business quarter end"
@@ -558,6 +560,20 @@ As of v0.14 holiday calendars can be used to provide the list of holidays. See
558560
# Tuesday after MLK Day (Monday is skipped because it's a holiday)
559561
dt + bday_us
560562
563+
Monthly offsets that respect a certain holiday calendar can be defined
564+
in the usual way.
565+
566+
.. ipython:: python
567+
568+
from pandas.tseries.offsets import CustomBusinessMonthBegin
569+
bmth_us = CustomBusinessMonthBegin(calendar=USFederalHolidayCalendar())
570+
# Skip new years
571+
dt = datetime(2013, 12, 17)
572+
dt + bmth_us
573+
574+
# Define date index with custom offset
575+
from pandas import DatetimeIndex
576+
DatetimeIndex(start='20100101',end='20120101',freq=bmth_us)
561577
562578
.. note::
563579

@@ -601,8 +617,10 @@ frequencies. We will refer to these aliases as *offset aliases*
601617
"W", "weekly frequency"
602618
"M", "month end frequency"
603619
"BM", "business month end frequency"
620+
"CBM", "custom business month end frequency"
604621
"MS", "month start frequency"
605622
"BMS", "business month start frequency"
623+
"CBMS", "custom business month start frequency"
606624
"Q", "quarter end frequency"
607625
"BQ", "business quarter endfrequency"
608626
"QS", "quarter start frequency"

doc/source/v0.14.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ Enhancements
477477
- Implemented ``Panel.pct_change`` (:issue:`6904`)
478478
- Added ``how`` option to rolling-moment functions to dictate how to handle resampling; :func:``rolling_max`` defaults to max,
479479
:func:``rolling_min`` defaults to min, and all others default to mean (:issue:`6297`)
480+
- ``CustomBuisnessMonthBegin`` and ``CustomBusinessMonthEnd`` are now available (:issue:`6866`)
480481

481482
Performance
482483
~~~~~~~~~~~

pandas/core/datetools.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@
1010
try:
1111
cday = CDay()
1212
customBusinessDay = CustomBusinessDay()
13+
customBusinessMonthEnd = CBMonthEnd()
14+
customBusinessMonthBegin = CBMonthBegin()
1315
except NotImplementedError:
1416
cday = None
1517
customBusinessDay = None
18+
customBusinessMonthEnd = None
19+
customBusinessMonthBegin = None
1620
monthEnd = MonthEnd()
1721
yearEnd = YearEnd()
1822
yearBegin = YearBegin()
1923
bmonthEnd = BMonthEnd()
20-
businessMonthEnd = bmonthEnd
24+
bmonthBegin = BMonthBegin()
25+
cbmonthEnd = customBusinessMonthEnd
26+
cbmonthBegin = customBusinessMonthBegin
2127
bquarterEnd = BQuarterEnd()
2228
quarterEnd = QuarterEnd()
2329
byearEnd = BYearEnd()

pandas/tseries/offsets.py

+154-22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from datetime import date, datetime, timedelta
23
from pandas.compat import range
34
from pandas import compat
@@ -16,6 +17,7 @@
1617
import functools
1718

1819
__all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay',
20+
'CBMonthEnd','CBMonthBegin',
1921
'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd',
2022
'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd',
2123
'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd',
@@ -703,6 +705,132 @@ def onOffset(cls, dt):
703705
_prefix = 'BMS'
704706

705707

708+
709+
class CustomBusinessMonthEnd(MonthOffset):
710+
"""
711+
**EXPERIMENTAL** DateOffset of one custom business month
712+
713+
.. warning:: EXPERIMENTAL
714+
715+
This class is not officially supported and the API is likely to change
716+
in future versions. Use this at your own risk.
717+
718+
Parameters
719+
----------
720+
n : int, default 1
721+
offset : timedelta, default timedelta(0)
722+
normalize : bool, default False
723+
Normalize start/end dates to midnight before generating date range
724+
weekmask : str, Default 'Mon Tue Wed Thu Fri'
725+
weekmask of valid business days, passed to ``numpy.busdaycalendar``
726+
holidays : list
727+
list/array of dates to exclude from the set of valid business days,
728+
passed to ``numpy.busdaycalendar``
729+
"""
730+
731+
_cacheable = False
732+
_prefix = 'CBM'
733+
def __init__(self, n=1, **kwds):
734+
self.n = int(n)
735+
self.kwds = kwds
736+
self.offset = kwds.get('offset', timedelta(0))
737+
self.normalize = kwds.get('normalize', False)
738+
self.weekmask = kwds.get('weekmask', 'Mon Tue Wed Thu Fri')
739+
holidays = kwds.get('holidays', [])
740+
self.cbday = CustomBusinessDay(n=self.n,**kwds)
741+
self.m_offset = MonthEnd()
742+
743+
@apply_nat
744+
def apply(self,other):
745+
n = self.n
746+
dt_in = other
747+
# First move to month offset
748+
cur_mend = self.m_offset.rollforward(dt_in)
749+
# Find this custom month offset
750+
cur_cmend = self.cbday.rollback(cur_mend)
751+
752+
# handle zero case. arbitrarily rollforward
753+
if n == 0 and dt_in != cur_cmend:
754+
n += 1
755+
756+
if dt_in < cur_cmend and n >= 1:
757+
n -= 1
758+
elif dt_in > cur_cmend and n <= -1:
759+
n += 1
760+
761+
new = cur_mend + n * MonthEnd()
762+
result = self.cbday.rollback(new)
763+
return as_timestamp(result)
764+
765+
def __repr__(self):
766+
if sys.version_info.major < 3:
767+
return BusinessDay.__repr__.__func__(self)
768+
else:
769+
return BusinessDay.__repr__(self)
770+
771+
class CustomBusinessMonthBegin(MonthOffset):
772+
"""
773+
**EXPERIMENTAL** DateOffset of one custom business month
774+
775+
.. warning:: EXPERIMENTAL
776+
777+
This class is not officially supported and the API is likely to change
778+
in future versions. Use this at your own risk.
779+
780+
Parameters
781+
----------
782+
n : int, default 1
783+
offset : timedelta, default timedelta(0)
784+
normalize : bool, default False
785+
Normalize start/end dates to midnight before generating date range
786+
weekmask : str, Default 'Mon Tue Wed Thu Fri'
787+
weekmask of valid business days, passed to ``numpy.busdaycalendar``
788+
holidays : list
789+
list/array of dates to exclude from the set of valid business days,
790+
passed to ``numpy.busdaycalendar``
791+
"""
792+
793+
_cacheable = False
794+
_prefix = 'CBMS'
795+
def __init__(self, n=1, **kwds):
796+
self.n = int(n)
797+
self.kwds = kwds
798+
self.offset = kwds.get('offset', timedelta(0))
799+
self.normalize = kwds.get('normalize', False)
800+
self.weekmask = kwds.get('weekmask', 'Mon Tue Wed Thu Fri')
801+
holidays = kwds.get('holidays', [])
802+
self.cbday = CustomBusinessDay(n=self.n,**kwds)
803+
self.m_offset = MonthBegin()
804+
805+
@apply_nat
806+
def apply(self,other):
807+
n = self.n
808+
dt_in = other
809+
# First move to month offset
810+
cur_mbegin = self.m_offset.rollback(dt_in)
811+
# Find this custom month offset
812+
cur_cmbegin = self.cbday.rollforward(cur_mbegin)
813+
814+
# handle zero case. arbitrarily rollforward
815+
if n == 0 and dt_in != cur_cmbegin:
816+
n += 1
817+
818+
if dt_in > cur_cmbegin and n <= -1:
819+
n += 1
820+
elif dt_in < cur_cmbegin and n >= 1:
821+
n -= 1
822+
823+
new = cur_mbegin + n * MonthBegin()
824+
result = self.cbday.rollforward(new)
825+
return as_timestamp(result)
826+
827+
828+
def __repr__(self):
829+
if sys.version_info.major < 3:
830+
return BusinessDay.__repr__.__func__(self)
831+
else:
832+
return BusinessDay.__repr__(self)
833+
706834
class Week(DateOffset):
707835
"""
708836
Weekly offset
@@ -1906,6 +2034,8 @@ class Nano(Tick):
19062034
BDay = BusinessDay
19072035
BMonthEnd = BusinessMonthEnd
19082036
BMonthBegin = BusinessMonthBegin
2037+
CBMonthEnd = CustomBusinessMonthEnd
2038+
CBMonthBegin = CustomBusinessMonthBegin
19092039
CDay = CustomBusinessDay
19102040

19112041

@@ -1988,28 +2118,30 @@ def generate_range(start=None, end=None, periods=None,
19882118
cur = next_date
19892119

19902120
prefix_mapping = dict((offset._prefix, offset) for offset in [
1991-
YearBegin, # 'AS'
1992-
YearEnd, # 'A'
1993-
BYearBegin, # 'BAS'
1994-
BYearEnd, # 'BA'
1995-
BusinessDay, # 'B'
1996-
BusinessMonthBegin, # 'BMS'
1997-
BusinessMonthEnd, # 'BM'
1998-
BQuarterEnd, # 'BQ'
1999-
BQuarterBegin, # 'BQS'
2000-
CustomBusinessDay, # 'C'
2001-
MonthEnd, # 'M'
2002-
MonthBegin, # 'MS'
2003-
Week, # 'W'
2004-
Second, # 'S'
2005-
Minute, # 'T'
2006-
Micro, # 'U'
2007-
QuarterEnd, # 'Q'
2008-
QuarterBegin, # 'QS'
2009-
Milli, # 'L'
2010-
Hour, # 'H'
2011-
Day, # 'D'
2012-
WeekOfMonth, # 'WOM'
2121+
YearBegin, # 'AS'
2122+
YearEnd, # 'A'
2123+
BYearBegin, # 'BAS'
2124+
BYearEnd, # 'BA'
2125+
BusinessDay, # 'B'
2126+
BusinessMonthBegin, # 'BMS'
2127+
BusinessMonthEnd, # 'BM'
2128+
BQuarterEnd, # 'BQ'
2129+
BQuarterBegin, # 'BQS'
2130+
CustomBusinessDay, # 'C'
2131+
CustomBusinessMonthEnd, # 'CBM'
2132+
CustomBusinessMonthBegin, # 'CBMS'
2133+
MonthEnd, # 'M'
2134+
MonthBegin, # 'MS'
2135+
Week, # 'W'
2136+
Second, # 'S'
2137+
Minute, # 'T'
2138+
Micro, # 'U'
2139+
QuarterEnd, # 'Q'
2140+
QuarterBegin, # 'QS'
2141+
Milli, # 'L'
2142+
Hour, # 'H'
2143+
Day, # 'D'
2144+
WeekOfMonth, # 'WOM'
20132145
FY5253,
20142146
FY5253Quarter,
20152147
])

0 commit comments

Comments
 (0)