Skip to content

Commit c8d4772

Browse files
committed
ENH: Add BusinessHour offset
1 parent 0efd4b3 commit c8d4772

File tree

8 files changed

+1017
-17
lines changed

8 files changed

+1017
-17
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.15.2.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ API changes
4949
In [3]: cat = pd.Categorical(['a', 'b', 'a'], categories=['a', 'b', 'c'])
5050

5151
In [4]: cat
52-
Out[4]:
52+
Out[4]:
5353
[a, b, a]
5454
Categories (3, object): [a < b < c]
5555

doc/source/whatsnew/v0.16.0.txt

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ enhancements, and performance improvements along with a large number of bug fixe
88
users upgrade to this version.
99

1010
- Highlights include:
11+
- ``BusinessHour`` offset is supported, see :ref:`here <timeseries.businesshour>`
1112

1213
- Check the :ref:`API Changes <whatsnew_0160.api>` and :ref:`deprecations <whatsnew_0160.deprecations>` before updating
1314

@@ -162,6 +163,14 @@ Enhancements
162163
- ``StringMethods.pad()`` and ``center()`` now accept ``fillchar`` option to specify filling character (:issue:`9352`)
163164
- Added ``StringMethods.zfill()`` which behave as the same as standard ``str`` (:issue:`9387`)
164165

166+
- ``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`)
167+
168+
.. ipython:: python
169+
170+
Timestamp('2014-08-01 09:00') + BusinessHour()
171+
Timestamp('2014-08-01 07:00') + BusinessHour()
172+
Timestamp('2014-08-01 16:30') + BusinessHour()
173+
165174
Performance
166175
~~~~~~~~~~~
167176

pandas/tseries/frequencies.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -741,15 +741,15 @@ def __init__(self, index, warn=True):
741741
@cache_readonly
742742
def deltas(self):
743743
return tslib.unique_deltas(self.values)
744-
744+
745745
@cache_readonly
746746
def deltas_asi8(self):
747747
return tslib.unique_deltas(self.index.asi8)
748748

749749
@cache_readonly
750750
def is_unique(self):
751751
return len(self.deltas) == 1
752-
752+
753753
@cache_readonly
754754
def is_unique_asi8(self):
755755
return len(self.deltas_asi8) == 1
@@ -762,10 +762,13 @@ def get_freq(self):
762762
if _is_multiple(delta, _ONE_DAY):
763763
return self._infer_daily_rule()
764764
else:
765-
# Possibly intraday frequency. Here we use the
765+
# Business hourly, maybe. 17: one day / 65: one weekend
766+
if self.hour_deltas in ([1, 17], [1, 65], [1, 17, 65]):
767+
return 'BH'
768+
# Possibly intraday frequency. Here we use the
766769
# original .asi8 values as the modified values
767770
# will not work around DST transitions. See #8772
768-
if not self.is_unique_asi8:
771+
elif not self.is_unique_asi8:
769772
return None
770773
delta = self.deltas_asi8[0]
771774
if _is_multiple(delta, _ONE_HOUR):
@@ -791,6 +794,10 @@ def get_freq(self):
791794
def day_deltas(self):
792795
return [x / _ONE_DAY for x in self.deltas]
793796

797+
@cache_readonly
798+
def hour_deltas(self):
799+
return [x / _ONE_HOUR for x in self.deltas]
800+
794801
@cache_readonly
795802
def fields(self):
796803
return tslib.build_field_sarray(self.values)

0 commit comments

Comments
 (0)