Skip to content

Commit a1f5ef3

Browse files
sinhrksjreback
authored andcommitted
ENH: Support CustomBusinessHour
closes #11514 Author: sinhrks <[email protected]> Closes #12847 from sinhrks/custombhour and squashes the following commits: 6fe9a7b [sinhrks] ENH: Support CustomBusinessHour
1 parent 51ac022 commit a1f5ef3

File tree

4 files changed

+419
-68
lines changed

4 files changed

+419
-68
lines changed

doc/source/timeseries.rst

+35
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ frequency increment. Specific offset logic like "month", "business day", or
563563
BYearBegin, "business year begin"
564564
FY5253, "retail (aka 52-53 week) year"
565565
BusinessHour, "business hour"
566+
CustomBusinessHour, "custom business hour"
566567
Hour, "one hour"
567568
Minute, "one minute"
568569
Second, "one second"
@@ -883,6 +884,40 @@ under the default business hours (9:00 - 17:00), there is no gap (0 minutes) bet
883884
# The result is the same as rollworward because BusinessDay never overlap.
884885
BusinessHour().apply(Timestamp('2014-08-02'))
885886
887+
``BusinessHour`` regards Saturday and Sunday as holidays. To use arbitrary holidays,
888+
you can use ``CustomBusinessHour`` offset, see :ref:`Custom Business Hour <timeseries.custombusinesshour>`:
889+
890+
.. _timeseries.custombusinesshour:
891+
892+
Custom Business Hour
893+
~~~~~~~~~~~~~~~~~~~~
894+
895+
.. versionadded:: 0.18.1
896+
897+
The ``CustomBusinessHour`` is a mixture of ``BusinessHour`` and ``CustomBusinessDay`` which
898+
allows you to specify arbitrary holidays. ``CustomBusinessHour`` works as the same
899+
as ``BusinessHour`` except that it skips specified custom holidays.
900+
901+
.. ipython:: python
902+
903+
from pandas.tseries.holiday import USFederalHolidayCalendar
904+
bhour_us = CustomBusinessHour(calendar=USFederalHolidayCalendar())
905+
# Friday before MLK Day
906+
dt = datetime(2014, 1, 17, 15)
907+
908+
dt + bhour_us
909+
910+
# Tuesday after MLK Day (Monday is skipped because it's a holiday)
911+
dt + bhour_us * 2
912+
913+
You can use keyword arguments suported by either ``BusinessHour`` and ``CustomBusinessDay``.
914+
915+
.. ipython:: python
916+
917+
bhour_mon = CustomBusinessHour(start='10:00', weekmask='Tue Wed Thu Fri')
918+
919+
# Monday is skipped because it's a holiday, business hour starts from 10:00
920+
dt + bhour_mon * 2
886921
887922
Offset Aliases
888923
~~~~~~~~~~~~~~

doc/source/whatsnew/v0.18.1.txt

+21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ We recommend that all users upgrade to this version.
99

1010
Highlights include:
1111

12+
- Custom business hour offset, see :ref:`here <whatsnew_0181.enhancements.custombusinesshour>`.
13+
14+
1215
.. contents:: What's new in v0.18.1
1316
:local:
1417
:backlinks: none
@@ -18,11 +21,27 @@ Highlights include:
1821
New features
1922
~~~~~~~~~~~~
2023

24+
.. _whatsnew_0181.enhancements.custombusinesshour:
25+
26+
Custom Business Hour
27+
^^^^^^^^^^^^^^^^^^^^
2128

29+
The ``CustomBusinessHour`` is a mixture of ``BusinessHour`` and ``CustomBusinessDay`` which
30+
allows you to specify arbitrary holidays. For details,
31+
see :ref:`Custom Business Hour <timeseries.custombusinesshour>` (:issue:`11514`)
2232

33+
.. ipython:: python
2334

35+
from pandas.tseries.offsets import CustomBusinessHour
36+
from pandas.tseries.holiday import USFederalHolidayCalendar
37+
bhour_us = CustomBusinessHour(calendar=USFederalHolidayCalendar())
38+
# Friday before MLK Day
39+
dt = datetime(2014, 1, 17, 15)
2440

41+
dt + bhour_us
2542

43+
# Tuesday after MLK Day (Monday is skipped because it's a holiday)
44+
dt + bhour_us * 2
2645

2746
.. _whatsnew_0181.enhancements:
2847

@@ -216,6 +235,7 @@ Bug Fixes
216235

217236

218237

238+
219239
- Bug in ``value_counts`` when ``normalize=True`` and ``dropna=True`` where nulls still contributed to the normalized count (:issue:`12558`)
220240
- Bug in ``Panel.fillna()`` ignoring ``inplace=True`` (:issue:`12633`)
221241
- Bug in ``read_csv`` when specifying ``names``, ```usecols``, and ``parse_dates`` simultaneously with the C engine (:issue:`9755`)
@@ -231,6 +251,7 @@ Bug Fixes
231251
- Bug in ``.str`` accessor methods may raise ``ValueError`` if input has ``name`` and the result is ``DataFrame`` or ``MultiIndex`` (:issue:`12617`)
232252

233253

254+
234255
- Bug in ``CategoricalIndex.get_loc`` returns different result from regular ``Index`` (:issue:`12531`)
235256
- Bug in ``PeriodIndex.resample`` where name not propagated (:issue:`12769`)
236257

pandas/tseries/offsets.py

+91-63
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
__all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay',
1919
'CBMonthEnd', 'CBMonthBegin',
2020
'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd',
21-
'BusinessHour',
21+
'BusinessHour', 'CustomBusinessHour',
2222
'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd',
2323
'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd',
2424
'LastWeekOfMonth', 'FY5253Quarter', 'FY5253',
@@ -669,20 +669,9 @@ def onOffset(self, dt):
669669
return dt.weekday() < 5
670670

671671

672-
class BusinessHour(BusinessMixin, SingleConstructorOffset):
673-
"""
674-
DateOffset subclass representing possibly n business days
675-
676-
.. versionadded: 0.16.1
677-
678-
"""
679-
_prefix = 'BH'
680-
_anchor = 0
681-
682-
def __init__(self, n=1, normalize=False, **kwds):
683-
self.n = int(n)
684-
self.normalize = normalize
672+
class BusinessHourMixin(BusinessMixin):
685673

674+
def __init__(self, **kwds):
686675
# must be validated here to equality check
687676
kwds['start'] = self._validate_time(kwds.get('start', '09:00'))
688677
kwds['end'] = self._validate_time(kwds.get('end', '17:00'))
@@ -691,12 +680,6 @@ def __init__(self, n=1, normalize=False, **kwds):
691680
self.start = kwds.get('start', '09:00')
692681
self.end = kwds.get('end', '17:00')
693682

694-
# used for moving to next businessday
695-
if self.n >= 0:
696-
self.next_bday = BusinessDay(n=1)
697-
else:
698-
self.next_bday = BusinessDay(n=-1)
699-
700683
def _validate_time(self, t_input):
701684
from datetime import time as dt_time
702685
import time
@@ -722,13 +705,6 @@ def _get_daytime_flag(self):
722705
else:
723706
return False
724707

725-
def _repr_attrs(self):
726-
out = super(BusinessHour, self)._repr_attrs()
727-
attrs = ['BH=%s-%s' % (self.start.strftime('%H:%M'),
728-
self.end.strftime('%H:%M'))]
729-
out += ': ' + ', '.join(attrs)
730-
return out
731-
732708
def _next_opening_time(self, other):
733709
"""
734710
If n is positive, return tomorrow's business day opening time.
@@ -905,6 +881,38 @@ def _onOffset(self, dt, businesshours):
905881
else:
906882
return False
907883

884+
def _repr_attrs(self):
885+
out = super(BusinessHourMixin, self)._repr_attrs()
886+
start = self.start.strftime('%H:%M')
887+
end = self.end.strftime('%H:%M')
888+
attrs = ['{prefix}={start}-{end}'.format(prefix=self._prefix,
889+
start=start, end=end)]
890+
out += ': ' + ', '.join(attrs)
891+
return out
892+
893+
894+
class BusinessHour(BusinessHourMixin, SingleConstructorOffset):
895+
"""
896+
DateOffset subclass representing possibly n business days
897+
898+
.. versionadded: 0.16.1
899+
900+
"""
901+
_prefix = 'BH'
902+
_anchor = 0
903+
904+
def __init__(self, n=1, normalize=False, **kwds):
905+
self.n = int(n)
906+
self.normalize = normalize
907+
super(BusinessHour, self).__init__(**kwds)
908+
909+
# used for moving to next businessday
910+
if self.n >= 0:
911+
nb_offset = 1
912+
else:
913+
nb_offset = -1
914+
self.next_bday = BusinessDay(n=nb_offset)
915+
908916

909917
class CustomBusinessDay(BusinessDay):
910918
"""
@@ -976,18 +984,7 @@ def get_calendar(self, weekmask, holidays, calendar):
976984
if holidays:
977985
kwargs['holidays'] = holidays
978986

979-
try:
980-
busdaycalendar = np.busdaycalendar(**kwargs)
981-
except:
982-
# Check we have the required numpy version
983-
from distutils.version import LooseVersion
984-
985-
if LooseVersion(np.__version__) < '1.7.0':
986-
raise NotImplementedError(
987-
"CustomBusinessDay requires numpy >= "
988-
"1.7.0. Current version: " + np.__version__)
989-
else:
990-
raise
987+
busdaycalendar = np.busdaycalendar(**kwargs)
991988
return busdaycalendar, holidays
992989

993990
def __getstate__(self):
@@ -1067,6 +1064,36 @@ def onOffset(self, dt):
10671064
return np.is_busday(day64, busdaycal=self.calendar)
10681065

10691066

1067+
class CustomBusinessHour(BusinessHourMixin, SingleConstructorOffset):
1068+
"""
1069+
DateOffset subclass representing possibly n custom business days
1070+
1071+
.. versionadded: 0.18.1
1072+
1073+
"""
1074+
_prefix = 'CBH'
1075+
_anchor = 0
1076+
1077+
def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
1078+
holidays=None, calendar=None, **kwds):
1079+
self.n = int(n)
1080+
self.normalize = normalize
1081+
super(CustomBusinessHour, self).__init__(**kwds)
1082+
# used for moving to next businessday
1083+
if self.n >= 0:
1084+
nb_offset = 1
1085+
else:
1086+
nb_offset = -1
1087+
self.next_bday = CustomBusinessDay(n=nb_offset,
1088+
weekmask=weekmask,
1089+
holidays=holidays,
1090+
calendar=calendar)
1091+
1092+
self.kwds['weekmask'] = self.next_bday.weekmask
1093+
self.kwds['holidays'] = self.next_bday.holidays
1094+
self.kwds['calendar'] = self.next_bday.calendar
1095+
1096+
10701097
class MonthOffset(SingleConstructorOffset):
10711098
_adjust_dst = True
10721099

@@ -2673,31 +2700,32 @@ def generate_range(start=None, end=None, periods=None,
26732700
cur = next_date
26742701

26752702
prefix_mapping = dict((offset._prefix, offset) for offset in [
2676-
YearBegin, # 'AS'
2677-
YearEnd, # 'A'
2678-
BYearBegin, # 'BAS'
2679-
BYearEnd, # 'BA'
2680-
BusinessDay, # 'B'
2681-
BusinessMonthBegin, # 'BMS'
2682-
BusinessMonthEnd, # 'BM'
2683-
BQuarterEnd, # 'BQ'
2684-
BQuarterBegin, # 'BQS'
2685-
BusinessHour, # 'BH'
2686-
CustomBusinessDay, # 'C'
2687-
CustomBusinessMonthEnd, # 'CBM'
2703+
YearBegin, # 'AS'
2704+
YearEnd, # 'A'
2705+
BYearBegin, # 'BAS'
2706+
BYearEnd, # 'BA'
2707+
BusinessDay, # 'B'
2708+
BusinessMonthBegin, # 'BMS'
2709+
BusinessMonthEnd, # 'BM'
2710+
BQuarterEnd, # 'BQ'
2711+
BQuarterBegin, # 'BQS'
2712+
BusinessHour, # 'BH'
2713+
CustomBusinessDay, # 'C'
2714+
CustomBusinessMonthEnd, # 'CBM'
26882715
CustomBusinessMonthBegin, # 'CBMS'
2689-
MonthEnd, # 'M'
2690-
MonthBegin, # 'MS'
2691-
Week, # 'W'
2692-
Second, # 'S'
2693-
Minute, # 'T'
2694-
Micro, # 'U'
2695-
QuarterEnd, # 'Q'
2696-
QuarterBegin, # 'QS'
2697-
Milli, # 'L'
2698-
Hour, # 'H'
2699-
Day, # 'D'
2700-
WeekOfMonth, # 'WOM'
2716+
CustomBusinessHour, # 'CBH'
2717+
MonthEnd, # 'M'
2718+
MonthBegin, # 'MS'
2719+
Week, # 'W'
2720+
Second, # 'S'
2721+
Minute, # 'T'
2722+
Micro, # 'U'
2723+
QuarterEnd, # 'Q'
2724+
QuarterBegin, # 'QS'
2725+
Milli, # 'L'
2726+
Hour, # 'H'
2727+
Day, # 'D'
2728+
WeekOfMonth, # 'WOM'
27012729
FY5253,
27022730
FY5253Quarter,
27032731
])

0 commit comments

Comments
 (0)