Skip to content

Commit f76a3f3

Browse files
reidy-pjreback
authored andcommitted
BUG: Adjust time values with Period objects in Series.dt.end_time (#18952)
1 parent 7c67d9c commit f76a3f3

File tree

12 files changed

+127
-16
lines changed

12 files changed

+127
-16
lines changed

doc/source/whatsnew/v0.24.0.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,43 @@ that the dates have been converted to UTC
281281
.. ipython:: python
282282
pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"], utc=True)
283283

284+
.. _whatsnew_0240.api_breaking.period_end_time:
285+
286+
Time values in ``dt.end_time`` and ``to_timestamp(how='end')``
287+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
288+
289+
The time values in :class:`Period` and :class:`PeriodIndex` objects are now set
290+
to '23:59:59.999999999' when calling :attr:`Series.dt.end_time`, :attr:`Period.end_time`,
291+
:attr:`PeriodIndex.end_time`, :func:`Period.to_timestamp()` with ``how='end'``,
292+
or :func:`PeriodIndex.to_timestamp()` with ``how='end'`` (:issue:`17157`)
293+
294+
Previous Behavior:
295+
296+
.. code-block:: ipython
297+
298+
In [2]: p = pd.Period('2017-01-01', 'D')
299+
In [3]: pi = pd.PeriodIndex([p])
300+
301+
In [4]: pd.Series(pi).dt.end_time[0]
302+
Out[4]: Timestamp(2017-01-01 00:00:00)
303+
304+
In [5]: p.end_time
305+
Out[5]: Timestamp(2017-01-01 23:59:59.999999999)
306+
307+
Current Behavior:
308+
309+
Calling :attr:`Series.dt.end_time` will now result in a time of '23:59:59.999999999' as
310+
is the case with :attr:`Period.end_time`, for example
311+
312+
.. ipython:: python
313+
314+
p = pd.Period('2017-01-01', 'D')
315+
pi = pd.PeriodIndex([p])
316+
317+
pd.Series(pi).dt.end_time[0]
318+
319+
p.end_time
320+
284321
.. _whatsnew_0240.api.datetimelike.normalize:
285322

286323
Tick DateOffset Normalize Restrictions

pandas/_libs/tslibs/period.pyx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ cdef extern from "../src/datetime/np_datetime.h":
3434
cimport util
3535
from util cimport is_period_object, is_string_object, INT32_MIN
3636

37+
from pandas._libs.tslibs.timedeltas import Timedelta
3738
from timestamps import Timestamp
3839
from timezones cimport is_utc, is_tzlocal, get_dst_info
3940
from timedeltas cimport delta_to_nanoseconds
@@ -1221,6 +1222,10 @@ cdef class _Period(object):
12211222
freq = self._maybe_convert_freq(freq)
12221223
how = _validate_end_alias(how)
12231224

1225+
end = how == 'E'
1226+
if end:
1227+
return (self + 1).to_timestamp(how='start') - Timedelta(1, 'ns')
1228+
12241229
if freq is None:
12251230
base, mult = get_freq_code(self.freq)
12261231
freq = get_to_timestamp_base(base)

pandas/core/arrays/datetimes.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,11 +1235,9 @@ def _generate_regular_range(cls, start, end, periods, freq):
12351235
tz = None
12361236
if isinstance(start, Timestamp):
12371237
tz = start.tz
1238-
start = start.to_pydatetime()
12391238

12401239
if isinstance(end, Timestamp):
12411240
tz = end.tz
1242-
end = end.to_pydatetime()
12431241

12441242
xdr = generate_range(start=start, end=end,
12451243
periods=periods, offset=freq)

pandas/core/indexes/period.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from pandas.core.tools.datetimes import parse_time_string
2626

2727
from pandas._libs.lib import infer_dtype
28-
from pandas._libs import tslib, index as libindex
28+
from pandas._libs import tslib, index as libindex, Timedelta
2929
from pandas._libs.tslibs.period import (Period, IncompatibleFrequency,
3030
DIFFERENT_FREQ_INDEX,
3131
_validate_end_alias)
@@ -501,6 +501,16 @@ def to_timestamp(self, freq=None, how='start'):
501501
"""
502502
how = _validate_end_alias(how)
503503

504+
end = how == 'E'
505+
if end:
506+
if freq == 'B':
507+
# roll forward to ensure we land on B date
508+
adjust = Timedelta(1, 'D') - Timedelta(1, 'ns')
509+
return self.to_timestamp(how='start') + adjust
510+
else:
511+
adjust = Timedelta(1, 'ns')
512+
return (self + 1).to_timestamp(how='start') - adjust
513+
504514
if freq is None:
505515
base, mult = _gfc(self.freq)
506516
freq = frequencies.get_to_timestamp_base(base)

pandas/tests/frame/test_period.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pandas as pd
66
import pandas.util.testing as tm
77
from pandas import (PeriodIndex, period_range, DataFrame, date_range,
8-
Index, to_datetime, DatetimeIndex)
8+
Index, to_datetime, DatetimeIndex, Timedelta)
99

1010

1111
def _permute(obj):
@@ -51,6 +51,7 @@ def test_frame_to_time_stamp(self):
5151
df['mix'] = 'a'
5252

5353
exp_index = date_range('1/1/2001', end='12/31/2009', freq='A-DEC')
54+
exp_index = exp_index + Timedelta(1, 'D') - Timedelta(1, 'ns')
5455
result = df.to_timestamp('D', 'end')
5556
tm.assert_index_equal(result.index, exp_index)
5657
tm.assert_numpy_array_equal(result.values, df.values)
@@ -66,22 +67,26 @@ def _get_with_delta(delta, freq='A-DEC'):
6667
delta = timedelta(hours=23)
6768
result = df.to_timestamp('H', 'end')
6869
exp_index = _get_with_delta(delta)
70+
exp_index = exp_index + Timedelta(1, 'h') - Timedelta(1, 'ns')
6971
tm.assert_index_equal(result.index, exp_index)
7072

7173
delta = timedelta(hours=23, minutes=59)
7274
result = df.to_timestamp('T', 'end')
7375
exp_index = _get_with_delta(delta)
76+
exp_index = exp_index + Timedelta(1, 'm') - Timedelta(1, 'ns')
7477
tm.assert_index_equal(result.index, exp_index)
7578

7679
result = df.to_timestamp('S', 'end')
7780
delta = timedelta(hours=23, minutes=59, seconds=59)
7881
exp_index = _get_with_delta(delta)
82+
exp_index = exp_index + Timedelta(1, 's') - Timedelta(1, 'ns')
7983
tm.assert_index_equal(result.index, exp_index)
8084

8185
# columns
8286
df = df.T
8387

8488
exp_index = date_range('1/1/2001', end='12/31/2009', freq='A-DEC')
89+
exp_index = exp_index + Timedelta(1, 'D') - Timedelta(1, 'ns')
8590
result = df.to_timestamp('D', 'end', axis=1)
8691
tm.assert_index_equal(result.columns, exp_index)
8792
tm.assert_numpy_array_equal(result.values, df.values)
@@ -93,16 +98,19 @@ def _get_with_delta(delta, freq='A-DEC'):
9398
delta = timedelta(hours=23)
9499
result = df.to_timestamp('H', 'end', axis=1)
95100
exp_index = _get_with_delta(delta)
101+
exp_index = exp_index + Timedelta(1, 'h') - Timedelta(1, 'ns')
96102
tm.assert_index_equal(result.columns, exp_index)
97103

98104
delta = timedelta(hours=23, minutes=59)
99105
result = df.to_timestamp('T', 'end', axis=1)
100106
exp_index = _get_with_delta(delta)
107+
exp_index = exp_index + Timedelta(1, 'm') - Timedelta(1, 'ns')
101108
tm.assert_index_equal(result.columns, exp_index)
102109

103110
result = df.to_timestamp('S', 'end', axis=1)
104111
delta = timedelta(hours=23, minutes=59, seconds=59)
105112
exp_index = _get_with_delta(delta)
113+
exp_index = exp_index + Timedelta(1, 's') - Timedelta(1, 'ns')
106114
tm.assert_index_equal(result.columns, exp_index)
107115

108116
# invalid axis

pandas/tests/indexes/period/test_period.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,19 @@ def test_periods_number_check(self):
366366
with pytest.raises(ValueError):
367367
period_range('2011-1-1', '2012-1-1', 'B')
368368

369+
def test_start_time(self):
370+
# GH 17157
371+
index = PeriodIndex(freq='M', start='2016-01-01', end='2016-05-31')
372+
expected_index = date_range('2016-01-01', end='2016-05-31', freq='MS')
373+
tm.assert_index_equal(index.start_time, expected_index)
374+
375+
def test_end_time(self):
376+
# GH 17157
377+
index = PeriodIndex(freq='M', start='2016-01-01', end='2016-05-31')
378+
expected_index = date_range('2016-01-01', end='2016-05-31', freq='M')
379+
expected_index = expected_index.shift(1, freq='D').shift(-1, freq='ns')
380+
tm.assert_index_equal(index.end_time, expected_index)
381+
369382
def test_index_duplicate_periods(self):
370383
# monotonic
371384
idx = PeriodIndex([2000, 2007, 2007, 2009, 2009], freq='A-JUN')

pandas/tests/indexes/period/test_scalar_compat.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
"""Tests for PeriodIndex behaving like a vectorized Period scalar"""
33

4-
from pandas import PeriodIndex, date_range
4+
from pandas import PeriodIndex, date_range, Timedelta
55
import pandas.util.testing as tm
66

77

@@ -14,4 +14,5 @@ def test_start_time(self):
1414
def test_end_time(self):
1515
index = PeriodIndex(freq='M', start='2016-01-01', end='2016-05-31')
1616
expected_index = date_range('2016-01-01', end='2016-05-31', freq='M')
17+
expected_index += Timedelta(1, 'D') - Timedelta(1, 'ns')
1718
tm.assert_index_equal(index.end_time, expected_index)

pandas/tests/indexes/period/test_tools.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
import pandas as pd
6+
from pandas import Timedelta
67
import pandas.util.testing as tm
78
import pandas.core.indexes.period as period
89
from pandas.compat import lrange
@@ -60,6 +61,7 @@ def test_to_timestamp(self):
6061

6162
exp_index = date_range('1/1/2001', end='12/31/2009', freq='A-DEC')
6263
result = series.to_timestamp(how='end')
64+
exp_index = exp_index + Timedelta(1, 'D') - Timedelta(1, 'ns')
6365
tm.assert_index_equal(result.index, exp_index)
6466
assert result.name == 'foo'
6567

@@ -74,16 +76,19 @@ def _get_with_delta(delta, freq='A-DEC'):
7476
delta = timedelta(hours=23)
7577
result = series.to_timestamp('H', 'end')
7678
exp_index = _get_with_delta(delta)
79+
exp_index = exp_index + Timedelta(1, 'h') - Timedelta(1, 'ns')
7780
tm.assert_index_equal(result.index, exp_index)
7881

7982
delta = timedelta(hours=23, minutes=59)
8083
result = series.to_timestamp('T', 'end')
8184
exp_index = _get_with_delta(delta)
85+
exp_index = exp_index + Timedelta(1, 'm') - Timedelta(1, 'ns')
8286
tm.assert_index_equal(result.index, exp_index)
8387

8488
result = series.to_timestamp('S', 'end')
8589
delta = timedelta(hours=23, minutes=59, seconds=59)
8690
exp_index = _get_with_delta(delta)
91+
exp_index = exp_index + Timedelta(1, 's') - Timedelta(1, 'ns')
8792
tm.assert_index_equal(result.index, exp_index)
8893

8994
index = PeriodIndex(freq='H', start='1/1/2001', end='1/2/2001')
@@ -92,6 +97,7 @@ def _get_with_delta(delta, freq='A-DEC'):
9297
exp_index = date_range('1/1/2001 00:59:59', end='1/2/2001 00:59:59',
9398
freq='H')
9499
result = series.to_timestamp(how='end')
100+
exp_index = exp_index + Timedelta(1, 's') - Timedelta(1, 'ns')
95101
tm.assert_index_equal(result.index, exp_index)
96102
assert result.name == 'foo'
97103

@@ -284,6 +290,7 @@ def test_to_timestamp_pi_mult(self):
284290
result = idx.to_timestamp(how='E')
285291
expected = DatetimeIndex(['2011-02-28', 'NaT', '2011-03-31'],
286292
name='idx')
293+
expected = expected + Timedelta(1, 'D') - Timedelta(1, 'ns')
287294
tm.assert_index_equal(result, expected)
288295

289296
def test_to_timestamp_pi_combined(self):
@@ -298,11 +305,13 @@ def test_to_timestamp_pi_combined(self):
298305
expected = DatetimeIndex(['2011-01-02 00:59:59',
299306
'2011-01-03 01:59:59'],
300307
name='idx')
308+
expected = expected + Timedelta(1, 's') - Timedelta(1, 'ns')
301309
tm.assert_index_equal(result, expected)
302310

303311
result = idx.to_timestamp(how='E', freq='H')
304312
expected = DatetimeIndex(['2011-01-02 00:00', '2011-01-03 01:00'],
305313
name='idx')
314+
expected = expected + Timedelta(1, 'h') - Timedelta(1, 'ns')
306315
tm.assert_index_equal(result, expected)
307316

308317
def test_period_astype_to_timestamp(self):
@@ -312,6 +321,7 @@ def test_period_astype_to_timestamp(self):
312321
tm.assert_index_equal(pi.astype('datetime64[ns]'), exp)
313322

314323
exp = pd.DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31'])
324+
exp = exp + Timedelta(1, 'D') - Timedelta(1, 'ns')
315325
tm.assert_index_equal(pi.astype('datetime64[ns]', how='end'), exp)
316326

317327
exp = pd.DatetimeIndex(['2011-01-01', '2011-02-01', '2011-03-01'],
@@ -321,6 +331,7 @@ def test_period_astype_to_timestamp(self):
321331

322332
exp = pd.DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31'],
323333
tz='US/Eastern')
334+
exp = exp + Timedelta(1, 'D') - Timedelta(1, 'ns')
324335
res = pi.astype('datetime64[ns, US/Eastern]', how='end')
325336
tm.assert_index_equal(res, exp)
326337

pandas/tests/scalar/period/test_period.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from datetime import datetime, date, timedelta
66

77
import pandas as pd
8+
from pandas import Timedelta
89
import pandas.util.testing as tm
910
import pandas.core.indexes.period as period
1011
from pandas.compat import text_type, iteritems
@@ -274,12 +275,14 @@ def test_timestamp_tz_arg_dateutil_from_string(self):
274275

275276
def test_timestamp_mult(self):
276277
p = pd.Period('2011-01', freq='M')
277-
assert p.to_timestamp(how='S') == pd.Timestamp('2011-01-01')
278-
assert p.to_timestamp(how='E') == pd.Timestamp('2011-01-31')
278+
assert p.to_timestamp(how='S') == Timestamp('2011-01-01')
279+
expected = Timestamp('2011-02-01') - Timedelta(1, 'ns')
280+
assert p.to_timestamp(how='E') == expected
279281

280282
p = pd.Period('2011-01', freq='3M')
281-
assert p.to_timestamp(how='S') == pd.Timestamp('2011-01-01')
282-
assert p.to_timestamp(how='E') == pd.Timestamp('2011-03-31')
283+
assert p.to_timestamp(how='S') == Timestamp('2011-01-01')
284+
expected = Timestamp('2011-04-01') - Timedelta(1, 'ns')
285+
assert p.to_timestamp(how='E') == expected
283286

284287
def test_construction(self):
285288
i1 = Period('1/1/2005', freq='M')
@@ -611,19 +614,19 @@ def _ex(p):
611614
p = Period('1985', freq='A')
612615

613616
result = p.to_timestamp('H', how='end')
614-
expected = datetime(1985, 12, 31, 23)
617+
expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns')
615618
assert result == expected
616619
result = p.to_timestamp('3H', how='end')
617620
assert result == expected
618621

619622
result = p.to_timestamp('T', how='end')
620-
expected = datetime(1985, 12, 31, 23, 59)
623+
expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns')
621624
assert result == expected
622625
result = p.to_timestamp('2T', how='end')
623626
assert result == expected
624627

625628
result = p.to_timestamp(how='end')
626-
expected = datetime(1985, 12, 31)
629+
expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns')
627630
assert result == expected
628631

629632
expected = datetime(1985, 1, 1)

pandas/tests/series/test_period.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import pandas as pd
44
import pandas.util.testing as tm
55
import pandas.core.indexes.period as period
6-
from pandas import Series, period_range, DataFrame
6+
from pandas import Series, period_range, DataFrame, Period
7+
import pytest
78

89

910
def _permute(obj):
@@ -167,3 +168,23 @@ def test_truncate(self):
167168
pd.Period('2017-09-02')
168169
])
169170
tm.assert_series_equal(result2, pd.Series([2], index=expected_idx2))
171+
172+
@pytest.mark.parametrize('input_vals', [
173+
[Period('2016-01', freq='M'), Period('2016-02', freq='M')],
174+
[Period('2016-01-01', freq='D'), Period('2016-01-02', freq='D')],
175+
[Period('2016-01-01 00:00:00', freq='H'),
176+
Period('2016-01-01 01:00:00', freq='H')],
177+
[Period('2016-01-01 00:00:00', freq='M'),
178+
Period('2016-01-01 00:01:00', freq='M')],
179+
[Period('2016-01-01 00:00:00', freq='S'),
180+
Period('2016-01-01 00:00:01', freq='S')]
181+
])
182+
def test_end_time_timevalues(self, input_vals):
183+
# GH 17157
184+
# Check that the time part of the Period is adjusted by end_time
185+
# when using the dt accessor on a Series
186+
187+
s = Series(input_vals)
188+
result = s.dt.end_time
189+
expected = s.apply(lambda x: x.end_time)
190+
tm.assert_series_equal(result, expected)

0 commit comments

Comments
 (0)