Skip to content

Commit 0dbb07c

Browse files
committed
ENH: Add BusinessHour offset
1 parent 0ecb4cb commit 0dbb07c

File tree

7 files changed

+1025
-18
lines changed

7 files changed

+1025
-18
lines changed

doc/source/timeseries.rst

+100-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,11 +482,13 @@ 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"
488489
Milli, "one millisecond"
489490
Micro, "one microsecond"
491+
Nano, "one nanosecond"
490492

491493

492494
The basic ``DateOffset`` takes the same arguments as
@@ -667,6 +669,102 @@ in the usual way.
667669
have to change to fix the timezone issues, the behaviour of the
668670
``CustomBusinessDay`` class may have to change in future versions.
669671

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

@@ -696,6 +794,7 @@ frequencies. We will refer to these aliases as *offset aliases*
696794
"BA", "business year end frequency"
697795
"AS", "year start frequency"
698796
"BAS", "business year start frequency"
797+
"BH", "business hour frequency"
699798
"H", "hourly frequency"
700799
"T", "minutely frequency"
701800
"S", "secondly frequency"

doc/source/v0.15.0.txt

+11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ users upgrade to this version.
1919
- Internal refactoring of the ``Index`` class to no longer sub-class ``ndarray``, see :ref:`Internal Refactoring <whatsnew_0150.refactoring>`
2020
- New datetimelike properties accessor ``.dt`` for Series, see :ref:`Datetimelike Properties <whatsnew_0150.dt>`
2121
- dropping support for ``PyTables`` less than version 3.0.0, and ``numexpr`` less than version 2.1 (:issue:`7990`)
22+
- New datetimelike properties accessor ``.dt`` for Series, see :ref:`Dateimelike Properties <whatsnew_0150.dt>`
23+
- ``BusinessHour`` offset is supported, see :ref:`here <timeseries.businesshour>`
2224

2325
- :ref:`Other Enhancements <whatsnew_0150.enhancements>`
2426

@@ -174,6 +176,7 @@ API changes
174176
for localizing a specific level of a MultiIndex (:issue:`7846`)
175177

176178
- ``Timestamp.__repr__`` displays ``dateutil.tz.tzoffset`` info (:issue:`7907`)
179+
177180
- ``merge``, ``DataFrame.merge``, and ``ordered_merge`` now return the same type
178181
as the ``left`` argument. (:issue:`7737`)
179182

@@ -232,6 +235,14 @@ API changes
232235
idx.duplicated()
233236
idx.drop_duplicates()
234237

238+
- ``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`)
239+
240+
.. ipython:: python
241+
242+
Timestamp('2014-08-01 09:00') + BusinessHour()
243+
Timestamp('2014-08-01 07:00') + BusinessHour()
244+
Timestamp('2014-08-01 16:30') + BusinessHour()
245+
235246
.. _whatsnew_0150.dt:
236247

237248
.dt accessor

pandas/tseries/frequencies.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,12 @@ def get_freq(self):
718718
delta = self.deltas[0]
719719
if _is_multiple(delta, _ONE_DAY):
720720
return self._infer_daily_rule()
721+
elif _is_multiple(delta, _ONE_HOUR):
722+
return self._infer_hourly_rule()
721723
else:
722724
# Possibly intraday frequency
723725
if not self.is_unique:
724726
return None
725-
if _is_multiple(delta, _ONE_HOUR):
726-
# Hours
727-
return _maybe_add_count('H', delta / _ONE_HOUR)
728727
elif _is_multiple(delta, _ONE_MINUTE):
729728
# Minutes
730729
return _maybe_add_count('T', delta / _ONE_MINUTE)
@@ -745,6 +744,10 @@ def get_freq(self):
745744
def day_deltas(self):
746745
return [x / _ONE_DAY for x in self.deltas]
747746

747+
@cache_readonly
748+
def hour_deltas(self):
749+
return [x / _ONE_HOUR for x in self.deltas]
750+
748751
@cache_readonly
749752
def fields(self):
750753
return tslib.build_field_sarray(self.values)
@@ -840,6 +843,14 @@ def _infer_daily_rule(self):
840843
if wom_rule:
841844
return wom_rule
842845

846+
def _infer_hourly_rule(self):
847+
# Business hourly, maybe. 17: one day / 65: one weekend
848+
if self.hour_deltas in ([1, 17], [1, 65], [1, 17, 65]):
849+
return 'BH'
850+
elif not self.is_unique:
851+
return None
852+
return _maybe_add_count('H', self.deltas[0] / _ONE_HOUR)
853+
843854
def _get_annual_rule(self):
844855
if len(self.ydiffs) > 1:
845856
return None

0 commit comments

Comments
 (0)