Skip to content

Commit d92e5e3

Browse files
committed
Merge pull request #7905 from sinhrks/businesshour
ENH: Add BusinessHour offset
2 parents 42523db + 7940ab3 commit d92e5e3

File tree

7 files changed

+1016
-16
lines changed

7 files changed

+1016
-16
lines changed

doc/source/timeseries.rst

+99-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
.. ipython:: python
55
:suppress:
66
7-
from datetime import datetime, timedelta
7+
from datetime import datetime, timedelta, time
88
import numpy as np
99
np.random.seed(123456)
1010
from pandas import *
@@ -482,6 +482,7 @@ frequency increment. Specific offset logic like "month", "business day", or
482482
BYearEnd, "business year end"
483483
BYearBegin, "business year begin"
484484
FY5253, "retail (aka 52-53 week) year"
485+
BusinessHour, "business hour"
485486
Hour, "one hour"
486487
Minute, "one minute"
487488
Second, "one second"
@@ -667,6 +668,102 @@ in the usual way.
667668
have to change to fix the timezone issues, the behaviour of the
668669
``CustomBusinessDay`` class may have to change in future versions.
669670

671+
.. _timeseries.businesshour:
672+
673+
Business Hour
674+
~~~~~~~~~~~~~
675+
676+
The ``BusinessHour`` class provides a business hour representation on ``BusinessDay``,
677+
allowing to use specific start and end times.
678+
679+
By default, ``BusinessHour`` uses 9:00 - 17:00 as business hours.
680+
Adding ``BusinessHour`` will increment ``Timestamp`` by hourly.
681+
If target ``Timestamp`` is out of business hours, move to the next business hour then increment it.
682+
If the result exceeds the business hours end, remaining is added to the next business day.
683+
684+
.. ipython:: python
685+
686+
bh = BusinessHour()
687+
bh
688+
689+
# 2014-08-01 is Friday
690+
Timestamp('2014-08-01 10:00').weekday()
691+
Timestamp('2014-08-01 10:00') + bh
692+
693+
# Below example is the same as Timestamp('2014-08-01 09:00') + bh
694+
Timestamp('2014-08-01 08:00') + bh
695+
696+
# If the results is on the end time, move to the next business day
697+
Timestamp('2014-08-01 16:00') + bh
698+
699+
# Remainings are added to the next day
700+
Timestamp('2014-08-01 16:30') + bh
701+
702+
# Adding 2 business hours
703+
Timestamp('2014-08-01 10:00') + BusinessHour(2)
704+
705+
# Subtracting 3 business hours
706+
Timestamp('2014-08-01 10:00') + BusinessHour(-3)
707+
708+
Also, you can specify ``start`` and ``end`` time by keywords.
709+
Argument must be ``str`` which has ``hour:minute`` representation or ``datetime.time`` instance.
710+
Specifying seconds, microseconds and nanoseconds as business hour results in ``ValueError``.
711+
712+
.. ipython:: python
713+
714+
bh = BusinessHour(start='11:00', end=time(20, 0))
715+
bh
716+
717+
Timestamp('2014-08-01 13:00') + bh
718+
Timestamp('2014-08-01 09:00') + bh
719+
Timestamp('2014-08-01 18:00') + bh
720+
721+
Passing ``start`` time later than ``end`` represents midnight business hour.
722+
In this case, business hour exceeds midnight and overlap to the next day.
723+
Valid business hours are distinguished by whether it started from valid ``BusinessDay``.
724+
725+
.. ipython:: python
726+
727+
bh = BusinessHour(start='17:00', end='09:00')
728+
bh
729+
730+
Timestamp('2014-08-01 17:00') + bh
731+
Timestamp('2014-08-01 23:00') + bh
732+
733+
# Although 2014-08-02 is Satuaday,
734+
# it is valid because it starts from 08-01 (Friday).
735+
Timestamp('2014-08-02 04:00') + bh
736+
737+
# Although 2014-08-04 is Monday,
738+
# it is out of business hours because it starts from 08-03 (Sunday).
739+
Timestamp('2014-08-04 04:00') + bh
740+
741+
Applying ``BusinessHour.rollforward`` and ``rollback`` to out of business hours results in
742+
the next business hour start or previous day's end. Different from other offsets, ``BusinessHour.rollforward``
743+
may output different results from ``apply`` by definition.
744+
745+
This is because one day's business hour end is equal to next day's business hour start. For example,
746+
under the default business hours (9:00 - 17:00), there is no gap (0 minutes) between ``2014-08-01 17:00`` and
747+
``2014-08-04 09:00``.
748+
749+
.. ipython:: python
750+
751+
# This adjusts a Timestamp to business hour edge
752+
BusinessHour().rollback(Timestamp('2014-08-02 15:00'))
753+
BusinessHour().rollforward(Timestamp('2014-08-02 15:00'))
754+
755+
# It is the same as BusinessHour().apply(Timestamp('2014-08-01 17:00')).
756+
# And it is the same as BusinessHour().apply(Timestamp('2014-08-04 09:00'))
757+
BusinessHour().apply(Timestamp('2014-08-02 15:00'))
758+
759+
# BusinessDay results (for reference)
760+
BusinessHour().rollforward(Timestamp('2014-08-02'))
761+
762+
# It is the same as BusinessDay().apply(Timestamp('2014-08-01'))
763+
# The result is the same as rollworward because BusinessDay never overlap.
764+
BusinessHour().apply(Timestamp('2014-08-02'))
765+
766+
670767
Offset Aliases
671768
~~~~~~~~~~~~~~
672769

@@ -696,6 +793,7 @@ frequencies. We will refer to these aliases as *offset aliases*
696793
"BA", "business year end frequency"
697794
"AS", "year start frequency"
698795
"BAS", "business year start frequency"
796+
"BH", "business hour frequency"
699797
"H", "hourly frequency"
700798
"T", "minutely frequency"
701799
"S", "secondly frequency"

doc/source/whatsnew/v0.16.1.txt

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Highlights include:
1414

1515
- New method ``sample`` for drawing random samples from Series, DataFrames and Panels. See :ref:`here <whatsnew_0161.enchancements.sample>`
1616

17+
- ``BusinessHour`` offset is supported, see :ref:`here <timeseries.businesshour>`
18+
1719
.. contents:: What's new in v0.16.1
1820
:local:
1921
:backlinks: none
@@ -27,6 +29,14 @@ Highlights include:
2729
Enhancements
2830
~~~~~~~~~~~~
2931

32+
- ``BusinessHour`` offset is now supported, which represents business hours starting from 09:00 - 17:00 on ``BusinessDay`` by default. See :ref:`Here <timeseries.businesshour>` for details. (:issue:`7905`)
33+
34+
.. ipython:: python
35+
36+
Timestamp('2014-08-01 09:00') + BusinessHour()
37+
Timestamp('2014-08-01 07:00') + BusinessHour()
38+
Timestamp('2014-08-01 16:30') + BusinessHour()
39+
3040
- Added ``StringMethods.capitalize()`` and ``swapcase`` which behave as the same as standard ``str`` (:issue:`9766`)
3141
- ``DataFrame.diff`` now takes an ``axis`` parameter that determines the direction of differencing (:issue:`9727`)
3242
- Added ``StringMethods`` (.str accessor) to ``Index`` (:issue:`9068`)

pandas/tseries/frequencies.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -743,15 +743,15 @@ def __init__(self, index, warn=True):
743743
@cache_readonly
744744
def deltas(self):
745745
return tslib.unique_deltas(self.values)
746-
746+
747747
@cache_readonly
748748
def deltas_asi8(self):
749749
return tslib.unique_deltas(self.index.asi8)
750750

751751
@cache_readonly
752752
def is_unique(self):
753753
return len(self.deltas) == 1
754-
754+
755755
@cache_readonly
756756
def is_unique_asi8(self):
757757
return len(self.deltas_asi8) == 1
@@ -764,10 +764,13 @@ def get_freq(self):
764764
if _is_multiple(delta, _ONE_DAY):
765765
return self._infer_daily_rule()
766766
else:
767-
# Possibly intraday frequency. Here we use the
767+
# Business hourly, maybe. 17: one day / 65: one weekend
768+
if self.hour_deltas in ([1, 17], [1, 65], [1, 17, 65]):
769+
return 'BH'
770+
# Possibly intraday frequency. Here we use the
768771
# original .asi8 values as the modified values
769772
# will not work around DST transitions. See #8772
770-
if not self.is_unique_asi8:
773+
elif not self.is_unique_asi8:
771774
return None
772775
delta = self.deltas_asi8[0]
773776
if _is_multiple(delta, _ONE_HOUR):
@@ -793,6 +796,10 @@ def get_freq(self):
793796
def day_deltas(self):
794797
return [x / _ONE_DAY for x in self.deltas]
795798

799+
@cache_readonly
800+
def hour_deltas(self):
801+
return [x / _ONE_HOUR for x in self.deltas]
802+
796803
@cache_readonly
797804
def fields(self):
798805
return tslib.build_field_sarray(self.values)

0 commit comments

Comments
 (0)