Skip to content

BUG: Timestamp rounding wrong implementation fixed #11963 #11964

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,8 @@ These can be accessed like ``Series.dt.<property>``.
Series.dt.normalize
Series.dt.strftime
Series.dt.round
Series.dt.floor
Series.dt.ceil

**Timedelta Properties**

Expand Down Expand Up @@ -1496,6 +1498,8 @@ Time-specific operations
DatetimeIndex.tz_convert
DatetimeIndex.tz_localize
DatetimeIndex.round
DatetimeIndex.floor
DatetimeIndex.ceil

Conversion
~~~~~~~~~~
Expand Down Expand Up @@ -1537,6 +1541,8 @@ Conversion
TimedeltaIndex.to_pytimedelta
TimedeltaIndex.to_series
TimedeltaIndex.round
TimedeltaIndex.floor
TimedeltaIndex.ceil

Window
------
Expand Down
6 changes: 3 additions & 3 deletions doc/source/whatsnew/v0.18.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Other enhancements
Datetimelike rounding
^^^^^^^^^^^^^^^^^^^^^

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

Naive datetimes

Expand All @@ -137,7 +137,7 @@ Naive datetimes
dr[0]
dr[0].round('10s')

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

.. ipython:: python

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


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

.. ipython:: python

Expand Down
2 changes: 2 additions & 0 deletions pandas/tests/test_categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -3761,6 +3761,8 @@ def test_dt_accessor_api_for_categorical(self):
('strftime', ("%Y-%m-%d",), {}),
('tz_convert', ("EST",), {}),
('round', ("D",), {}),
('floor', ("D",), {}),
('ceil', ("D",), {}),
#('tz_localize', ("UTC",), {}),
]
_special_func_names = [f[0] for f in special_func_defs]
Expand Down
22 changes: 17 additions & 5 deletions pandas/tests/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ def test_dt_namespace_accessor(self):
ok_for_period_methods = ['strftime']
ok_for_dt = ok_for_base + ['date','time','microsecond','nanosecond', 'is_month_start', 'is_month_end', 'is_quarter_start',
'is_quarter_end', 'is_year_start', 'is_year_end', 'tz']
ok_for_dt_methods = ['to_period','to_pydatetime','tz_localize','tz_convert', 'normalize', 'strftime', 'round']
ok_for_dt_methods = ['to_period','to_pydatetime','tz_localize','tz_convert', 'normalize', 'strftime', 'round', 'floor', 'ceil']
ok_for_td = ['days','seconds','microseconds','nanoseconds']
ok_for_td_methods = ['components','to_pytimedelta','total_seconds','round']
ok_for_td_methods = ['components','to_pytimedelta','total_seconds','round', 'floor', 'ceil']

def get_expected(s, name):
result = getattr(Index(s._values),prop)
Expand Down Expand Up @@ -141,14 +141,26 @@ def compare(s, name):
tm.assert_series_equal(result, expected)

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

# round with tz
result = s.dt.tz_localize('UTC').dt.tz_convert('US/Eastern').dt.round('D')
expected = Series(date_range('20130101',periods=5)).dt.tz_localize('US/Eastern')
expected = Series(pd.to_datetime(['2012-01-01', '2012-01-01', '2012-01-01']).tz_localize('US/Eastern'))
tm.assert_series_equal(result, expected)

# floor
s = Series(pd.to_datetime(['2012-01-01 13:00:00', '2012-01-01 12:01:00', '2012-01-01 08:00:00']))
result = s.dt.floor('D')
expected = Series(pd.to_datetime(['2012-01-01', '2012-01-01', '2012-01-01']))
tm.assert_series_equal(result, expected)

# ceil
s = Series(pd.to_datetime(['2012-01-01 13:00:00', '2012-01-01 12:01:00', '2012-01-01 08:00:00']))
result = s.dt.ceil('D')
expected = Series(pd.to_datetime(['2012-01-02', '2012-01-02', '2012-01-02']))
tm.assert_series_equal(result, expected)

# datetimeindex with tz
Expand Down
21 changes: 17 additions & 4 deletions pandas/tseries/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ def strftime(self, date_format):
class TimelikeOps(object):
""" common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex """

def round(self, freq):
_round_doc = (
"""
Round the index to the specified freq; this is a floor type of operation
%s the index to the specified freq

Parameters
----------
Expand All @@ -59,7 +59,8 @@ def round(self, freq):
Raises
------
ValueError if the freq cannot be converted
"""
""")
def _round(self, freq, rounder):

from pandas.tseries.frequencies import to_offset
unit = to_offset(freq).nanos
Expand All @@ -69,7 +70,7 @@ def round(self, freq):
values = self.tz_localize(None).asi8
else:
values = self.asi8
result = (unit*np.floor(values/unit)).astype('i8')
result = (unit*rounder(values/float(unit))).astype('i8')
attribs = self._get_attributes_dict()
if 'freq' in attribs:
attribs['freq'] = None
Expand All @@ -81,6 +82,18 @@ def round(self, freq):
if getattr(self,'tz',None) is not None:
result = result.tz_localize(self.tz)
return result

@Appender(_round_doc % "round")
def round(self, freq):
return self._round(freq, np.round)

@Appender(_round_doc % "floor")
def floor(self, freq):
return self._round(freq, np.floor)

@Appender(_round_doc % "floor")
def ceil(self, freq):
return self._round(freq, np.ceil)

class DatetimeIndexOpsMixin(object):
""" common ops mixin to support a unified inteface datetimelike Index """
Expand Down
4 changes: 2 additions & 2 deletions pandas/tseries/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def to_pydatetime(self):
typ='property')
DatetimeProperties._add_delegate_accessors(delegate=DatetimeIndex,
accessors=["to_period","tz_localize","tz_convert",
"normalize","strftime","round"],
"normalize","strftime","round", "floor", "ceil"],
typ='method')

class TimedeltaProperties(Properties):
Expand Down Expand Up @@ -182,7 +182,7 @@ def components(self):
accessors=TimedeltaIndex._datetimelike_ops,
typ='property')
TimedeltaProperties._add_delegate_accessors(delegate=TimedeltaIndex,
accessors=["to_pytimedelta", "total_seconds", "round"],
accessors=["to_pytimedelta", "total_seconds", "round", "floor", "ceil"],
typ='method')

class PeriodProperties(Properties):
Expand Down
33 changes: 13 additions & 20 deletions pandas/tseries/tests/test_timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ def test_round(self):
Timedelta('-1 days 02:34:56.789000000')
),
('S',
Timedelta('1 days 02:34:56'),
Timedelta('-1 days 02:34:56')
Timedelta('1 days 02:34:57'),
Timedelta('-1 days 02:34:57')
),
('2S',
Timedelta('1 days 02:34:56'),
Expand All @@ -205,15 +205,15 @@ def test_round(self):
Timedelta('-1 days 02:34:55')
),
('T',
Timedelta('1 days 02:34:00'),
Timedelta('-1 days 02:34:00')
Timedelta('1 days 02:35:00'),
Timedelta('-1 days 02:35:00')
),
('12T',
Timedelta('1 days 02:24:00'),
Timedelta('-1 days 02:24:00')),
Timedelta('1 days 02:36:00'),
Timedelta('-1 days 02:36:00')),
('H',
Timedelta('1 days 02:00:00'),
Timedelta('-1 days 02:00:00')
Timedelta('1 days 03:00:00'),
Timedelta('-1 days 03:00:00')
),
('d',
Timedelta('1 days'),
Expand All @@ -237,22 +237,15 @@ def test_round(self):
# note that negative times round DOWN! so don't give whole numbers
for (freq, s1, s2) in [('N', t1, t2),
('U', t1, t2),
('L', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:57.999000',
'-2 days +23:57:55.999000'],
('L', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:58', '-2 days +23:57:56'],
dtype='timedelta64[ns]', freq=None)),
('S', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:57', '-2 days +23:57:55'],
('S', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:58', '-2 days +23:57:56'],
dtype='timedelta64[ns]', freq=None)),
('2S', t1a, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:56', '-2 days +23:57:54'],
dtype='timedelta64[ns]', freq=None)),
('5S', t1b, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:55', '-2 days +23:57:55'],
dtype='timedelta64[ns]', freq=None)),
('T', t1b, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:58:00', '-2 days +23:57:00'],
dtype='timedelta64[ns]', freq=None)),
('12T', t1c, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:48:00', '-2 days +23:48:00'],
('12T', t1c, TimedeltaIndex(['-1 days', '-1 days', '-1 days'],
dtype='timedelta64[ns]', freq=None)),
('H', t1c, TimedeltaIndex(['-1 days +00:00:00', '-2 days +23:00:00', '-2 days +23:00:00'],
('H', t1c, TimedeltaIndex(['-1 days', '-1 days', '-1 days'],
dtype='timedelta64[ns]', freq=None)),
('d', t1c, pd.TimedeltaIndex([-1,-2,-2],unit='D'))]:
('d', t1c, pd.TimedeltaIndex([-1,-1,-1],unit='D'))]:
r1 = t1.round(freq)
tm.assert_index_equal(r1, s1)
r2 = t2.round(freq)
Expand Down
32 changes: 32 additions & 0 deletions pandas/tseries/tests/test_timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2785,11 +2785,43 @@ def test_round(self):
expected = Timestamp('20130101')
self.assertEqual(result, expected)

dt = Timestamp('20130101 19:10:11')
result = dt.round('D')
expected = Timestamp('20130102')
self.assertEqual(result, expected)

dt = Timestamp('20130201 12:00:00')
result = dt.round('D')
expected = Timestamp('20130202')
self.assertEqual(result, expected)

dt = Timestamp('20130104 12:00:00')
result = dt.round('D')
expected = Timestamp('20130105')
self.assertEqual(result, expected)

dt = Timestamp('20130104 12:32:00')
result = dt.round('30Min')
expected = Timestamp('20130104 12:30:00')
self.assertEqual(result, expected)

dti = date_range('20130101 09:10:11',periods=5)
result = dti.round('D')
expected = date_range('20130101',periods=5)
tm.assert_index_equal(result, expected)

# floor
dt = Timestamp('20130101 09:10:11')
result = dt.floor('D')
expected = Timestamp('20130101')
self.assertEqual(result, expected)

# ceil
dt = Timestamp('20130101 09:10:11')
result = dt.ceil('D')
expected = Timestamp('20130102')
self.assertEqual(result, expected)

# round with tz
dt = Timestamp('20130101 09:10:11',tz='US/Eastern')
result = dt.round('D')
Expand Down
79 changes: 64 additions & 15 deletions pandas/tslib.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,8 @@ class Timestamp(_Timestamp):
def _repr_base(self):
return '%s %s' % (self._date_repr, self._time_repr)

def round(self, freq):
"""
return a new Timestamp rounded to this resolution
def _round(self, freq, rounder):

Parameters
----------
freq : a freq string indicating the rouding resolution
"""
cdef int64_t unit
cdef object result, value

Expand All @@ -378,11 +372,41 @@ class Timestamp(_Timestamp):
value = self.tz_localize(None).value
else:
value = self.value
result = Timestamp(unit*np.floor(value/unit),unit='ns')
result = Timestamp(unit*rounder(value/float(unit)),unit='ns')
if self.tz is not None:
result = result.tz_localize(self.tz)
return result

def round(self, freq):
"""
return a new Timestamp rounded to this resolution

Parameters
----------
freq : a freq string indicating the rounding resolution
"""
return self._round(freq, np.round)

def floor(self, freq):
"""
return a new Timestamp floored to this resolution

Parameters
----------
freq : a freq string indicating the flooring resolution
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, use a private method and simply call from the other 3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can I use Appender in pyx file ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, not really sure how to do that. you can just repeat the doc-string (but for sure don't repeat code, use a private method for that)

return self._round(freq, np.floor)

def ceil(self, freq):
"""
return a new Timestamp ceiled to this resolution

Parameters
----------
freq : a freq string indicating the ceiling resolution
"""
return self._round(freq, np.ceil)

@property
def tz(self):
"""
Expand Down Expand Up @@ -2388,20 +2412,45 @@ class Timedelta(_Timedelta):
else:
return "D"

def _round(self, freq, rounder):

cdef int64_t result, unit

from pandas.tseries.frequencies import to_offset
unit = to_offset(freq).nanos
result = unit*rounder(self.value/float(unit))
return Timedelta(result,unit='ns')

def round(self, freq):
"""
return a new Timedelta rounded to this resolution
return a new Timedelta rounded to this resolution.


Parameters
----------
freq : a freq string indicating the rouding resolution
freq : a freq string indicating the rounding resolution
"""
cdef int64_t result, unit
return self._round(freq, np.round)

from pandas.tseries.frequencies import to_offset
unit = to_offset(freq).nanos
result = unit*np.floor(self.value/unit)
return Timedelta(result,unit='ns')
def floor(self, freq):
"""
return a new Timedelta floored to this resolution

Parameters
----------
freq : a freq string indicating the flooring resolution
"""
return self._round(freq, np.floor)

def ceil(self, freq):
"""
return a new Timedelta ceiled to this resolution

Parameters
----------
freq : a freq string indicating the ceiling resolution
"""
return self._round(freq, np.ceil)

def _repr_base(self, format=None):
"""
Expand Down