Skip to content

Commit 4437e9c

Browse files
Tux1jreback
authored andcommitted
BUG: Timestamp rounding wrong implementation fixed #11963
1 parent ced5d23 commit 4437e9c

File tree

9 files changed

+156
-49
lines changed

9 files changed

+156
-49
lines changed

doc/source/api.rst

+6
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,8 @@ These can be accessed like ``Series.dt.<property>``.
483483
Series.dt.normalize
484484
Series.dt.strftime
485485
Series.dt.round
486+
Series.dt.floor
487+
Series.dt.ceil
486488

487489
**Timedelta Properties**
488490

@@ -1496,6 +1498,8 @@ Time-specific operations
14961498
DatetimeIndex.tz_convert
14971499
DatetimeIndex.tz_localize
14981500
DatetimeIndex.round
1501+
DatetimeIndex.floor
1502+
DatetimeIndex.ceil
14991503

15001504
Conversion
15011505
~~~~~~~~~~
@@ -1537,6 +1541,8 @@ Conversion
15371541
TimedeltaIndex.to_pytimedelta
15381542
TimedeltaIndex.to_series
15391543
TimedeltaIndex.round
1544+
TimedeltaIndex.floor
1545+
TimedeltaIndex.ceil
15401546

15411547
Window
15421548
------

doc/source/whatsnew/v0.18.0.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Other enhancements
123123
Datetimelike rounding
124124
^^^^^^^^^^^^^^^^^^^^^
125125

126-
``DatetimeIndex``, ``Timestamp``, ``TimedeltaIndex``, ``Timedelta`` have gained the ``.round()`` method for datetimelike rounding. (:issue:`4314`)
126+
``DatetimeIndex``, ``Timestamp``, ``TimedeltaIndex``, ``Timedelta`` have gained the ``.round()``, ``.floor()`` and ``.ceil()`` method for datetimelike rounding, flooring and ceiling. (:issue:`4314`, :issue:`11963`)
127127

128128
Naive datetimes
129129

@@ -137,7 +137,7 @@ Naive datetimes
137137
dr[0]
138138
dr[0].round('10s')
139139

140-
Tz-aware are rounded in local times
140+
Tz-aware are rounded, floored and ceiled in local times
141141

142142
.. ipython:: python
143143

@@ -158,7 +158,7 @@ Timedeltas
158158
t[0].round('2h')
159159

160160

161-
In addition, ``.round()`` will be available thru the ``.dt`` accessor of ``Series``.
161+
In addition, ``.round()``, ``.floor()`` and ``.ceil()`` will be available thru the ``.dt`` accessor of ``Series``.
162162

163163
.. ipython:: python
164164

pandas/tests/test_categorical.py

+2
Original file line numberDiff line numberDiff line change
@@ -3761,6 +3761,8 @@ def test_dt_accessor_api_for_categorical(self):
37613761
('strftime', ("%Y-%m-%d",), {}),
37623762
('tz_convert', ("EST",), {}),
37633763
('round', ("D",), {}),
3764+
('floor', ("D",), {}),
3765+
('ceil', ("D",), {}),
37643766
#('tz_localize', ("UTC",), {}),
37653767
]
37663768
_special_func_names = [f[0] for f in special_func_defs]

pandas/tests/test_series.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ def test_dt_namespace_accessor(self):
8989
ok_for_period_methods = ['strftime']
9090
ok_for_dt = ok_for_base + ['date','time','microsecond','nanosecond', 'is_month_start', 'is_month_end', 'is_quarter_start',
9191
'is_quarter_end', 'is_year_start', 'is_year_end', 'tz']
92-
ok_for_dt_methods = ['to_period','to_pydatetime','tz_localize','tz_convert', 'normalize', 'strftime', 'round']
92+
ok_for_dt_methods = ['to_period','to_pydatetime','tz_localize','tz_convert', 'normalize', 'strftime', 'round', 'floor', 'ceil']
9393
ok_for_td = ['days','seconds','microseconds','nanoseconds']
94-
ok_for_td_methods = ['components','to_pytimedelta','total_seconds','round']
94+
ok_for_td_methods = ['components','to_pytimedelta','total_seconds','round', 'floor', 'ceil']
9595

9696
def get_expected(s, name):
9797
result = getattr(Index(s._values),prop)
@@ -141,14 +141,26 @@ def compare(s, name):
141141
tm.assert_series_equal(result, expected)
142142

143143
# round
144-
s = Series(date_range('20130101 09:10:11',periods=5))
144+
s = Series(pd.to_datetime(['2012-01-01 13:00:00', '2012-01-01 12:01:00', '2012-01-01 08:00:00']))
145145
result = s.dt.round('D')
146-
expected = Series(date_range('20130101',periods=5))
146+
expected = Series(pd.to_datetime(['2012-01-02', '2012-01-02', '2012-01-01']))
147147
tm.assert_series_equal(result, expected)
148148

149149
# round with tz
150150
result = s.dt.tz_localize('UTC').dt.tz_convert('US/Eastern').dt.round('D')
151-
expected = Series(date_range('20130101',periods=5)).dt.tz_localize('US/Eastern')
151+
expected = Series(pd.to_datetime(['2012-01-01', '2012-01-01', '2012-01-01']).tz_localize('US/Eastern'))
152+
tm.assert_series_equal(result, expected)
153+
154+
# floor
155+
s = Series(pd.to_datetime(['2012-01-01 13:00:00', '2012-01-01 12:01:00', '2012-01-01 08:00:00']))
156+
result = s.dt.floor('D')
157+
expected = Series(pd.to_datetime(['2012-01-01', '2012-01-01', '2012-01-01']))
158+
tm.assert_series_equal(result, expected)
159+
160+
# ceil
161+
s = Series(pd.to_datetime(['2012-01-01 13:00:00', '2012-01-01 12:01:00', '2012-01-01 08:00:00']))
162+
result = s.dt.ceil('D')
163+
expected = Series(pd.to_datetime(['2012-01-02', '2012-01-02', '2012-01-02']))
152164
tm.assert_series_equal(result, expected)
153165

154166
# datetimeindex with tz

pandas/tseries/base.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ def strftime(self, date_format):
4444
class TimelikeOps(object):
4545
""" common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex """
4646

47-
def round(self, freq):
47+
_round_doc = (
4848
"""
49-
Round the index to the specified freq; this is a floor type of operation
49+
%s the index to the specified freq
5050
5151
Parameters
5252
----------
@@ -59,7 +59,8 @@ def round(self, freq):
5959
Raises
6060
------
6161
ValueError if the freq cannot be converted
62-
"""
62+
""")
63+
def _round(self, freq, rounder):
6364

6465
from pandas.tseries.frequencies import to_offset
6566
unit = to_offset(freq).nanos
@@ -69,7 +70,7 @@ def round(self, freq):
6970
values = self.tz_localize(None).asi8
7071
else:
7172
values = self.asi8
72-
result = (unit*np.floor(values/unit)).astype('i8')
73+
result = (unit*rounder(values/float(unit))).astype('i8')
7374
attribs = self._get_attributes_dict()
7475
if 'freq' in attribs:
7576
attribs['freq'] = None
@@ -81,6 +82,18 @@ def round(self, freq):
8182
if getattr(self,'tz',None) is not None:
8283
result = result.tz_localize(self.tz)
8384
return result
85+
86+
@Appender(_round_doc % "round")
87+
def round(self, freq):
88+
return self._round(freq, np.round)
89+
90+
@Appender(_round_doc % "floor")
91+
def floor(self, freq):
92+
return self._round(freq, np.floor)
93+
94+
@Appender(_round_doc % "floor")
95+
def ceil(self, freq):
96+
return self._round(freq, np.ceil)
8497

8598
class DatetimeIndexOpsMixin(object):
8699
""" common ops mixin to support a unified inteface datetimelike Index """

pandas/tseries/common.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def to_pydatetime(self):
146146
typ='property')
147147
DatetimeProperties._add_delegate_accessors(delegate=DatetimeIndex,
148148
accessors=["to_period","tz_localize","tz_convert",
149-
"normalize","strftime","round"],
149+
"normalize","strftime","round", "floor", "ceil"],
150150
typ='method')
151151

152152
class TimedeltaProperties(Properties):
@@ -182,7 +182,7 @@ def components(self):
182182
accessors=TimedeltaIndex._datetimelike_ops,
183183
typ='property')
184184
TimedeltaProperties._add_delegate_accessors(delegate=TimedeltaIndex,
185-
accessors=["to_pytimedelta", "total_seconds", "round"],
185+
accessors=["to_pytimedelta", "total_seconds", "round", "floor", "ceil"],
186186
typ='method')
187187

188188
class PeriodProperties(Properties):

pandas/tseries/tests/test_timedeltas.py

+13-20
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ def test_round(self):
193193
Timedelta('-1 days 02:34:56.789000000')
194194
),
195195
('S',
196-
Timedelta('1 days 02:34:56'),
197-
Timedelta('-1 days 02:34:56')
196+
Timedelta('1 days 02:34:57'),
197+
Timedelta('-1 days 02:34:57')
198198
),
199199
('2S',
200200
Timedelta('1 days 02:34:56'),
@@ -205,15 +205,15 @@ def test_round(self):
205205
Timedelta('-1 days 02:34:55')
206206
),
207207
('T',
208-
Timedelta('1 days 02:34:00'),
209-
Timedelta('-1 days 02:34:00')
208+
Timedelta('1 days 02:35:00'),
209+
Timedelta('-1 days 02:35:00')
210210
),
211211
('12T',
212-
Timedelta('1 days 02:24:00'),
213-
Timedelta('-1 days 02:24:00')),
212+
Timedelta('1 days 02:36:00'),
213+
Timedelta('-1 days 02:36:00')),
214214
('H',
215-
Timedelta('1 days 02:00:00'),
216-
Timedelta('-1 days 02:00:00')
215+
Timedelta('1 days 03:00:00'),
216+
Timedelta('-1 days 03:00:00')
217217
),
218218
('d',
219219
Timedelta('1 days'),
@@ -237,22 +237,15 @@ def test_round(self):
237237
# note that negative times round DOWN! so don't give whole numbers
238238
for (freq, s1, s2) in [('N', t1, t2),
239239
('U', t1, t2),
240-
('L', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:57.999000',
241-
'-2 days +23:57:55.999000'],
240+
('L', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:58', '-2 days +23:57:56'],
242241
dtype='timedelta64[ns]', freq=None)),
243-
('S', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:57', '-2 days +23:57:55'],
242+
('S', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:58', '-2 days +23:57:56'],
244243
dtype='timedelta64[ns]', freq=None)),
245-
('2S', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:56', '-2 days +23:57:54'],
246-
dtype='timedelta64[ns]', freq=None)),
247-
('5S', t1b, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:55', '-2 days +23:57:55'],
248-
dtype='timedelta64[ns]', freq=None)),
249-
('T', t1b, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:00', '-2 days +23:57:00'],
250-
dtype='timedelta64[ns]', freq=None)),
251-
('12T', t1c, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:48:00', '-2 days +23:48:00'],
244+
('12T', t1c, TimedeltaIndex(['-1 days', '-1 days', '-1 days'],
252245
dtype='timedelta64[ns]', freq=None)),
253-
('H', t1c, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:00:00', '-2 days +23:00:00'],
246+
('H', t1c, TimedeltaIndex(['-1 days', '-1 days', '-1 days'],
254247
dtype='timedelta64[ns]', freq=None)),
255-
('d', t1c, pd.TimedeltaIndex([-1,-2,-2],unit='D'))]:
248+
('d', t1c, pd.TimedeltaIndex([-1,-1,-1],unit='D'))]:
256249
r1 = t1.round(freq)
257250
tm.assert_index_equal(r1, s1)
258251
r2 = t2.round(freq)

pandas/tseries/tests/test_timeseries.py

+32
Original file line numberDiff line numberDiff line change
@@ -2785,11 +2785,43 @@ def test_round(self):
27852785
expected = Timestamp('20130101')
27862786
self.assertEqual(result, expected)
27872787

2788+
dt = Timestamp('20130101 19:10:11')
2789+
result = dt.round('D')
2790+
expected = Timestamp('20130102')
2791+
self.assertEqual(result, expected)
2792+
2793+
dt = Timestamp('20130201 12:00:00')
2794+
result = dt.round('D')
2795+
expected = Timestamp('20130202')
2796+
self.assertEqual(result, expected)
2797+
2798+
dt = Timestamp('20130104 12:00:00')
2799+
result = dt.round('D')
2800+
expected = Timestamp('20130105')
2801+
self.assertEqual(result, expected)
2802+
2803+
dt = Timestamp('20130104 12:32:00')
2804+
result = dt.round('30Min')
2805+
expected = Timestamp('20130104 12:30:00')
2806+
self.assertEqual(result, expected)
2807+
27882808
dti = date_range('20130101 09:10:11',periods=5)
27892809
result = dti.round('D')
27902810
expected = date_range('20130101',periods=5)
27912811
tm.assert_index_equal(result, expected)
27922812

2813+
# floor
2814+
dt = Timestamp('20130101 09:10:11')
2815+
result = dt.floor('D')
2816+
expected = Timestamp('20130101')
2817+
self.assertEqual(result, expected)
2818+
2819+
# ceil
2820+
dt = Timestamp('20130101 09:10:11')
2821+
result = dt.ceil('D')
2822+
expected = Timestamp('20130102')
2823+
self.assertEqual(result, expected)
2824+
27932825
# round with tz
27942826
dt = Timestamp('20130101 09:10:11',tz='US/Eastern')
27952827
result = dt.round('D')

pandas/tslib.pyx

+64-15
Original file line numberDiff line numberDiff line change
@@ -361,14 +361,8 @@ class Timestamp(_Timestamp):
361361
def _repr_base(self):
362362
return '%s %s' % (self._date_repr, self._time_repr)
363363

364-
def round(self, freq):
365-
"""
366-
return a new Timestamp rounded to this resolution
364+
def _round(self, freq, rounder):
367365

368-
Parameters
369-
----------
370-
freq : a freq string indicating the rouding resolution
371-
"""
372366
cdef int64_t unit
373367
cdef object result, value
374368

@@ -378,11 +372,41 @@ class Timestamp(_Timestamp):
378372
value = self.tz_localize(None).value
379373
else:
380374
value = self.value
381-
result = Timestamp(unit*np.floor(value/unit),unit='ns')
375+
result = Timestamp(unit*rounder(value/float(unit)),unit='ns')
382376
if self.tz is not None:
383377
result = result.tz_localize(self.tz)
384378
return result
385379

380+
def round(self, freq):
381+
"""
382+
return a new Timestamp rounded to this resolution
383+
384+
Parameters
385+
----------
386+
freq : a freq string indicating the rounding resolution
387+
"""
388+
return self._round(freq, np.round)
389+
390+
def floor(self, freq):
391+
"""
392+
return a new Timestamp floored to this resolution
393+
394+
Parameters
395+
----------
396+
freq : a freq string indicating the flooring resolution
397+
"""
398+
return self._round(freq, np.floor)
399+
400+
def ceil(self, freq):
401+
"""
402+
return a new Timestamp ceiled to this resolution
403+
404+
Parameters
405+
----------
406+
freq : a freq string indicating the ceiling resolution
407+
"""
408+
return self._round(freq, np.ceil)
409+
386410
@property
387411
def tz(self):
388412
"""
@@ -2388,20 +2412,45 @@ class Timedelta(_Timedelta):
23882412
else:
23892413
return "D"
23902414

2415+
def _round(self, freq, rounder):
2416+
2417+
cdef int64_t result, unit
2418+
2419+
from pandas.tseries.frequencies import to_offset
2420+
unit = to_offset(freq).nanos
2421+
result = unit*rounder(self.value/float(unit))
2422+
return Timedelta(result,unit='ns')
2423+
23912424
def round(self, freq):
23922425
"""
2393-
return a new Timedelta rounded to this resolution
2426+
return a new Timedelta rounded to this resolution.
2427+
23942428
23952429
Parameters
23962430
----------
2397-
freq : a freq string indicating the rouding resolution
2431+
freq : a freq string indicating the rounding resolution
23982432
"""
2399-
cdef int64_t result, unit
2433+
return self._round(freq, np.round)
24002434

2401-
from pandas.tseries.frequencies import to_offset
2402-
unit = to_offset(freq).nanos
2403-
result = unit*np.floor(self.value/unit)
2404-
return Timedelta(result,unit='ns')
2435+
def floor(self, freq):
2436+
"""
2437+
return a new Timedelta floored to this resolution
2438+
2439+
Parameters
2440+
----------
2441+
freq : a freq string indicating the flooring resolution
2442+
"""
2443+
return self._round(freq, np.floor)
2444+
2445+
def ceil(self, freq):
2446+
"""
2447+
return a new Timedelta ceiled to this resolution
2448+
2449+
Parameters
2450+
----------
2451+
freq : a freq string indicating the ceiling resolution
2452+
"""
2453+
return self._round(freq, np.ceil)
24052454

24062455
def _repr_base(self, format=None):
24072456
"""

0 commit comments

Comments
 (0)