Skip to content

ENH: Support CustomBusinessHour #12847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions doc/source/timeseries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ frequency increment. Specific offset logic like "month", "business day", or
BYearBegin, "business year begin"
FY5253, "retail (aka 52-53 week) year"
BusinessHour, "business hour"
CustomBusinessHour, "custom business hour"
Hour, "one hour"
Minute, "one minute"
Second, "one second"
Expand Down Expand Up @@ -883,6 +884,40 @@ under the default business hours (9:00 - 17:00), there is no gap (0 minutes) bet
# The result is the same as rollworward because BusinessDay never overlap.
BusinessHour().apply(Timestamp('2014-08-02'))

``BusinessHour`` regards Saturday and Sunday as holidays. To use arbitrary holidays,
you can use ``CustomBusinessHour`` offset, see :ref:`Custom Business Hour <timeseries.custombusinesshour>`:

.. _timeseries.custombusinesshour:

Custom Business Hour
~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 0.18.1

The ``CustomBusinessHour`` is a mixture of ``BusinessHour`` and ``CustomBusinessDay`` which
allows you to specify arbitrary holidays. ``CustomBusinessHour`` works as the same
as ``BusinessHour`` except that it skips specified custom holidays.

.. ipython:: python

from pandas.tseries.holiday import USFederalHolidayCalendar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can do this on another PR, but I think we need to be explict about these mimports (e.g. pd.offsets.CustomeBusinessHour)

bhour_us = CustomBusinessHour(calendar=USFederalHolidayCalendar())
# Friday before MLK Day
dt = datetime(2014, 1, 17, 15)

dt + bhour_us

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

You can use keyword arguments suported by either ``BusinessHour`` and ``CustomBusinessDay``.

.. ipython:: python

bhour_mon = CustomBusinessHour(start='10:00', weekmask='Tue Wed Thu Fri')

# Monday is skipped because it's a holiday, business hour starts from 10:00
dt + bhour_mon * 2

Offset Aliases
~~~~~~~~~~~~~~
Expand Down
21 changes: 21 additions & 0 deletions doc/source/whatsnew/v0.18.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ We recommend that all users upgrade to this version.

Highlights include:

- Custom business hour offset, see :ref:`here <whatsnew_0181.enhancements.custombusinesshour>`.


.. contents:: What's new in v0.18.1
:local:
:backlinks: none
Expand All @@ -18,11 +21,27 @@ Highlights include:
New features
~~~~~~~~~~~~

.. _whatsnew_0181.enhancements.custombusinesshour:

Custom Business Hour
^^^^^^^^^^^^^^^^^^^^

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

.. ipython:: python

from pandas.tseries.offsets import CustomBusinessHour
from pandas.tseries.holiday import USFederalHolidayCalendar
bhour_us = CustomBusinessHour(calendar=USFederalHolidayCalendar())
# Friday before MLK Day
dt = datetime(2014, 1, 17, 15)

dt + bhour_us

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

.. _whatsnew_0181.enhancements:

Expand Down Expand Up @@ -216,6 +235,7 @@ Bug Fixes




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



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

Expand Down
154 changes: 91 additions & 63 deletions pandas/tseries/offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
__all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay',
'CBMonthEnd', 'CBMonthBegin',
'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd',
'BusinessHour',
'BusinessHour', 'CustomBusinessHour',
'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd',
'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd',
'LastWeekOfMonth', 'FY5253Quarter', 'FY5253',
Expand Down Expand Up @@ -669,20 +669,9 @@ def onOffset(self, dt):
return dt.weekday() < 5


class BusinessHour(BusinessMixin, SingleConstructorOffset):
"""
DateOffset subclass representing possibly n business days

.. versionadded: 0.16.1

"""
_prefix = 'BH'
_anchor = 0

def __init__(self, n=1, normalize=False, **kwds):
self.n = int(n)
self.normalize = normalize
class BusinessHourMixin(BusinessMixin):

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

# used for moving to next businessday
if self.n >= 0:
self.next_bday = BusinessDay(n=1)
else:
self.next_bday = BusinessDay(n=-1)

def _validate_time(self, t_input):
from datetime import time as dt_time
import time
Expand All @@ -722,13 +705,6 @@ def _get_daytime_flag(self):
else:
return False

def _repr_attrs(self):
out = super(BusinessHour, self)._repr_attrs()
attrs = ['BH=%s-%s' % (self.start.strftime('%H:%M'),
self.end.strftime('%H:%M'))]
out += ': ' + ', '.join(attrs)
return out

def _next_opening_time(self, other):
"""
If n is positive, return tomorrow's business day opening time.
Expand Down Expand Up @@ -905,6 +881,38 @@ def _onOffset(self, dt, businesshours):
else:
return False

def _repr_attrs(self):
out = super(BusinessHourMixin, self)._repr_attrs()
start = self.start.strftime('%H:%M')
end = self.end.strftime('%H:%M')
attrs = ['{prefix}={start}-{end}'.format(prefix=self._prefix,
start=start, end=end)]
out += ': ' + ', '.join(attrs)
return out


class BusinessHour(BusinessHourMixin, SingleConstructorOffset):
"""
DateOffset subclass representing possibly n business days

.. versionadded: 0.16.1

"""
_prefix = 'BH'
_anchor = 0

def __init__(self, n=1, normalize=False, **kwds):
self.n = int(n)
self.normalize = normalize
super(BusinessHour, self).__init__(**kwds)

# used for moving to next businessday
if self.n >= 0:
nb_offset = 1
else:
nb_offset = -1
self.next_bday = BusinessDay(n=nb_offset)


class CustomBusinessDay(BusinessDay):
"""
Expand Down Expand Up @@ -976,18 +984,7 @@ def get_calendar(self, weekmask, holidays, calendar):
if holidays:
kwargs['holidays'] = holidays

try:
busdaycalendar = np.busdaycalendar(**kwargs)
except:
# Check we have the required numpy version
from distutils.version import LooseVersion

if LooseVersion(np.__version__) < '1.7.0':
raise NotImplementedError(
"CustomBusinessDay requires numpy >= "
"1.7.0. Current version: " + np.__version__)
else:
raise
busdaycalendar = np.busdaycalendar(**kwargs)
return busdaycalendar, holidays

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


class CustomBusinessHour(BusinessHourMixin, SingleConstructorOffset):
"""
DateOffset subclass representing possibly n custom business days

.. versionadded: 0.18.1

"""
_prefix = 'CBH'
_anchor = 0

def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
holidays=None, calendar=None, **kwds):
self.n = int(n)
self.normalize = normalize
super(CustomBusinessHour, self).__init__(**kwds)
# used for moving to next businessday
if self.n >= 0:
nb_offset = 1
else:
nb_offset = -1
self.next_bday = CustomBusinessDay(n=nb_offset,
weekmask=weekmask,
holidays=holidays,
calendar=calendar)

self.kwds['weekmask'] = self.next_bday.weekmask
self.kwds['holidays'] = self.next_bday.holidays
self.kwds['calendar'] = self.next_bday.calendar


class MonthOffset(SingleConstructorOffset):
_adjust_dst = True

Expand Down Expand Up @@ -2673,31 +2700,32 @@ def generate_range(start=None, end=None, periods=None,
cur = next_date

prefix_mapping = dict((offset._prefix, offset) for offset in [
YearBegin, # 'AS'
YearEnd, # 'A'
BYearBegin, # 'BAS'
BYearEnd, # 'BA'
BusinessDay, # 'B'
BusinessMonthBegin, # 'BMS'
BusinessMonthEnd, # 'BM'
BQuarterEnd, # 'BQ'
BQuarterBegin, # 'BQS'
BusinessHour, # 'BH'
CustomBusinessDay, # 'C'
CustomBusinessMonthEnd, # 'CBM'
YearBegin, # 'AS'
YearEnd, # 'A'
BYearBegin, # 'BAS'
BYearEnd, # 'BA'
BusinessDay, # 'B'
BusinessMonthBegin, # 'BMS'
BusinessMonthEnd, # 'BM'
BQuarterEnd, # 'BQ'
BQuarterBegin, # 'BQS'
BusinessHour, # 'BH'
CustomBusinessDay, # 'C'
CustomBusinessMonthEnd, # 'CBM'
CustomBusinessMonthBegin, # 'CBMS'
MonthEnd, # 'M'
MonthBegin, # 'MS'
Week, # 'W'
Second, # 'S'
Minute, # 'T'
Micro, # 'U'
QuarterEnd, # 'Q'
QuarterBegin, # 'QS'
Milli, # 'L'
Hour, # 'H'
Day, # 'D'
WeekOfMonth, # 'WOM'
CustomBusinessHour, # 'CBH'
MonthEnd, # 'M'
MonthBegin, # 'MS'
Week, # 'W'
Second, # 'S'
Minute, # 'T'
Micro, # 'U'
QuarterEnd, # 'Q'
QuarterBegin, # 'QS'
Milli, # 'L'
Hour, # 'H'
Day, # 'D'
WeekOfMonth, # 'WOM'
FY5253,
FY5253Quarter,
])
Expand Down
Loading