Skip to content

Commit 97b612f

Browse files
yrhookejreback
authored andcommitted
ENH: between_time, at_time accept axis parameter (#21799)
1 parent 84cc0fb commit 97b612f

File tree

4 files changed

+134
-50
lines changed

4 files changed

+134
-50
lines changed

doc/source/whatsnew/v0.24.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ Other Enhancements
291291
- :meth:`read_excel()` now accepts ``usecols`` as a list of column names or callable (:issue:`18273`)
292292
- :meth:`MultiIndex.to_flat_index` has been added to flatten multiple levels into a single-level :class:`Index` object.
293293
- :meth:`DataFrame.to_stata` and :class:` pandas.io.stata.StataWriter117` can write mixed sting columns to Stata strl format (:issue:`23633`)
294+
- :meth:`DataFrame.between_time` and :meth:`DataFrame.at_time` have gained the an ``axis`` parameter (:issue: `8839`)
294295

295296
.. _whatsnew_0240.api_breaking:
296297

pandas/core/generic.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -7381,7 +7381,7 @@ def asfreq(self, freq, method=None, how=None, normalize=False,
73817381
return asfreq(self, freq, method=method, how=how, normalize=normalize,
73827382
fill_value=fill_value)
73837383

7384-
def at_time(self, time, asof=False):
7384+
def at_time(self, time, asof=False, axis=None):
73857385
"""
73867386
Select values at particular time of day (e.g. 9:30AM).
73877387
@@ -7393,6 +7393,10 @@ def at_time(self, time, asof=False):
73937393
Parameters
73947394
----------
73957395
time : datetime.time or string
7396+
axis : {0 or 'index', 1 or 'columns'}, default 0
7397+
7398+
.. versionadded:: 0.24.0
7399+
73967400
73977401
Returns
73987402
-------
@@ -7422,14 +7426,20 @@ def at_time(self, time, asof=False):
74227426
DatetimeIndex.indexer_at_time : Get just the index locations for
74237427
values at particular time of the day.
74247428
"""
7429+
if axis is None:
7430+
axis = self._stat_axis_number
7431+
axis = self._get_axis_number(axis)
7432+
7433+
index = self._get_axis(axis)
74257434
try:
7426-
indexer = self.index.indexer_at_time(time, asof=asof)
7427-
return self._take(indexer)
7435+
indexer = index.indexer_at_time(time, asof=asof)
74287436
except AttributeError:
74297437
raise TypeError('Index must be DatetimeIndex')
74307438

7439+
return self._take(indexer, axis=axis)
7440+
74317441
def between_time(self, start_time, end_time, include_start=True,
7432-
include_end=True):
7442+
include_end=True, axis=None):
74337443
"""
74347444
Select values between particular times of the day (e.g., 9:00-9:30 AM).
74357445
@@ -7447,6 +7457,9 @@ def between_time(self, start_time, end_time, include_start=True,
74477457
end_time : datetime.time or string
74487458
include_start : boolean, default True
74497459
include_end : boolean, default True
7460+
axis : {0 or 'index', 1 or 'columns'}, default 0
7461+
7462+
.. versionadded:: 0.24.0
74507463
74517464
Returns
74527465
-------
@@ -7484,14 +7497,20 @@ def between_time(self, start_time, end_time, include_start=True,
74847497
DatetimeIndex.indexer_between_time : Get just the index locations for
74857498
values between particular times of the day.
74867499
"""
7500+
if axis is None:
7501+
axis = self._stat_axis_number
7502+
axis = self._get_axis_number(axis)
7503+
7504+
index = self._get_axis(axis)
74877505
try:
7488-
indexer = self.index.indexer_between_time(
7506+
indexer = index.indexer_between_time(
74897507
start_time, end_time, include_start=include_start,
74907508
include_end=include_end)
7491-
return self._take(indexer)
74927509
except AttributeError:
74937510
raise TypeError('Index must be DatetimeIndex')
74947511

7512+
return self._take(indexer, axis=axis)
7513+
74957514
def resample(self, rule, how=None, axis=0, fill_method=None, closed=None,
74967515
label=None, convention='start', kind=None, loffset=None,
74977516
limit=None, base=0, on=None, level=None):

pandas/tests/frame/test_timeseries.py

+97-44
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
from pandas.tests.frame.common import TestData
2626

2727

28+
@pytest.fixture(params=product([True, False], [True, False]))
29+
def close_open_fixture(request):
30+
return request.param
31+
32+
2833
class TestDataFrameTimeSeriesMethods(TestData):
2934

3035
def test_diff(self):
@@ -638,33 +643,49 @@ def test_at_time_raises(self):
638643
with pytest.raises(TypeError): # index is not a DatetimeIndex
639644
df.at_time('00:00')
640645

641-
def test_between_time(self):
646+
@pytest.mark.parametrize('axis', ['index', 'columns', 0, 1])
647+
def test_at_time_axis(self, axis):
648+
# issue 8839
649+
rng = date_range('1/1/2000', '1/5/2000', freq='5min')
650+
ts = DataFrame(np.random.randn(len(rng), len(rng)))
651+
ts.index, ts.columns = rng, rng
652+
653+
indices = rng[(rng.hour == 9) & (rng.minute == 30) & (rng.second == 0)]
654+
655+
if axis in ['index', 0]:
656+
expected = ts.loc[indices, :]
657+
elif axis in ['columns', 1]:
658+
expected = ts.loc[:, indices]
659+
660+
result = ts.at_time('9:30', axis=axis)
661+
assert_frame_equal(result, expected)
662+
663+
def test_between_time(self, close_open_fixture):
642664
rng = date_range('1/1/2000', '1/5/2000', freq='5min')
643665
ts = DataFrame(np.random.randn(len(rng), 2), index=rng)
644666
stime = time(0, 0)
645667
etime = time(1, 0)
646-
647-
close_open = product([True, False], [True, False])
648-
for inc_start, inc_end in close_open:
649-
filtered = ts.between_time(stime, etime, inc_start, inc_end)
650-
exp_len = 13 * 4 + 1
651-
if not inc_start:
652-
exp_len -= 5
653-
if not inc_end:
654-
exp_len -= 4
655-
656-
assert len(filtered) == exp_len
657-
for rs in filtered.index:
658-
t = rs.time()
659-
if inc_start:
660-
assert t >= stime
661-
else:
662-
assert t > stime
663-
664-
if inc_end:
665-
assert t <= etime
666-
else:
667-
assert t < etime
668+
inc_start, inc_end = close_open_fixture
669+
670+
filtered = ts.between_time(stime, etime, inc_start, inc_end)
671+
exp_len = 13 * 4 + 1
672+
if not inc_start:
673+
exp_len -= 5
674+
if not inc_end:
675+
exp_len -= 4
676+
677+
assert len(filtered) == exp_len
678+
for rs in filtered.index:
679+
t = rs.time()
680+
if inc_start:
681+
assert t >= stime
682+
else:
683+
assert t > stime
684+
685+
if inc_end:
686+
assert t <= etime
687+
else:
688+
assert t < etime
668689

669690
result = ts.between_time('00:00', '01:00')
670691
expected = ts.between_time(stime, etime)
@@ -676,34 +697,66 @@ def test_between_time(self):
676697
stime = time(22, 0)
677698
etime = time(9, 0)
678699

679-
close_open = product([True, False], [True, False])
680-
for inc_start, inc_end in close_open:
681-
filtered = ts.between_time(stime, etime, inc_start, inc_end)
682-
exp_len = (12 * 11 + 1) * 4 + 1
683-
if not inc_start:
684-
exp_len -= 4
685-
if not inc_end:
686-
exp_len -= 4
687-
688-
assert len(filtered) == exp_len
689-
for rs in filtered.index:
690-
t = rs.time()
691-
if inc_start:
692-
assert (t >= stime) or (t <= etime)
693-
else:
694-
assert (t > stime) or (t <= etime)
695-
696-
if inc_end:
697-
assert (t <= etime) or (t >= stime)
698-
else:
699-
assert (t < etime) or (t >= stime)
700+
filtered = ts.between_time(stime, etime, inc_start, inc_end)
701+
exp_len = (12 * 11 + 1) * 4 + 1
702+
if not inc_start:
703+
exp_len -= 4
704+
if not inc_end:
705+
exp_len -= 4
706+
707+
assert len(filtered) == exp_len
708+
for rs in filtered.index:
709+
t = rs.time()
710+
if inc_start:
711+
assert (t >= stime) or (t <= etime)
712+
else:
713+
assert (t > stime) or (t <= etime)
714+
715+
if inc_end:
716+
assert (t <= etime) or (t >= stime)
717+
else:
718+
assert (t < etime) or (t >= stime)
700719

701720
def test_between_time_raises(self):
702721
# GH20725
703722
df = pd.DataFrame([[1, 2, 3], [4, 5, 6]])
704723
with pytest.raises(TypeError): # index is not a DatetimeIndex
705724
df.between_time(start_time='00:00', end_time='12:00')
706725

726+
def test_between_time_axis(self, axis):
727+
# issue 8839
728+
rng = date_range('1/1/2000', periods=100, freq='10min')
729+
ts = DataFrame(np.random.randn(len(rng), len(rng)))
730+
stime, etime = ('08:00:00', '09:00:00')
731+
exp_len = 7
732+
733+
if axis in ['index', 0]:
734+
ts.index = rng
735+
assert len(ts.between_time(stime, etime)) == exp_len
736+
assert len(ts.between_time(stime, etime, axis=0)) == exp_len
737+
738+
if axis in ['columns', 1]:
739+
ts.columns = rng
740+
selected = ts.between_time(stime, etime, axis=1).columns
741+
assert len(selected) == exp_len
742+
743+
def test_between_time_axis_raises(self, axis):
744+
# issue 8839
745+
rng = date_range('1/1/2000', periods=100, freq='10min')
746+
mask = np.arange(0, len(rng))
747+
rand_data = np.random.randn(len(rng), len(rng))
748+
ts = DataFrame(rand_data, index=rng, columns=rng)
749+
stime, etime = ('08:00:00', '09:00:00')
750+
751+
if axis in ['columns', 1]:
752+
ts.index = mask
753+
pytest.raises(TypeError, ts.between_time, stime, etime)
754+
pytest.raises(TypeError, ts.between_time, stime, etime, axis=0)
755+
756+
if axis in ['index', 0]:
757+
ts.columns = mask
758+
pytest.raises(TypeError, ts.between_time, stime, etime, axis=1)
759+
707760
def test_operation_on_NaT(self):
708761
# Both NaT and Timestamp are in DataFrame.
709762
df = pd.DataFrame({'foo': [pd.NaT, pd.NaT,

pandas/tests/series/test_timeseries.py

+11
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,17 @@ def test_between_time_formats(self):
817817
for time_string in strings:
818818
assert len(ts.between_time(*time_string)) == expected_length
819819

820+
def test_between_time_axis(self):
821+
# issue 8839
822+
rng = date_range('1/1/2000', periods=100, freq='10min')
823+
ts = Series(np.random.randn(len(rng)), index=rng)
824+
stime, etime = ('08:00:00', '09:00:00')
825+
expected_length = 7
826+
827+
assert len(ts.between_time(stime, etime)) == expected_length
828+
assert len(ts.between_time(stime, etime, axis=0)) == expected_length
829+
pytest.raises(ValueError, ts.between_time, stime, etime, axis=1)
830+
820831
def test_to_period(self):
821832
from pandas.core.indexes.period import period_range
822833

0 commit comments

Comments
 (0)