Skip to content

Commit c4f6960

Browse files
mroeschkeAnkurDedania
authored andcommitted
BUG: Timestamp.round precision error for ns (pandas-dev#15578)
closes pandas-dev#15578 Author: Matt Roeschke <[email protected]> Closes pandas-dev#15588 from mroeschke/fix_15578 and squashes the following commits: af95baa [Matt Roeschke] BUG: Timestamp.round precision error for ns (pandas-dev#15578)
1 parent 69902bd commit c4f6960

File tree

5 files changed

+51
-7
lines changed

5 files changed

+51
-7
lines changed

doc/source/whatsnew/v0.20.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ Bug Fixes
652652
- Bug in ``Index`` power operations with reversed operands (:issue:`14973`)
653653
- Bug in ``TimedeltaIndex`` addition where overflow was being allowed without error (:issue:`14816`)
654654
- Bug in ``TimedeltaIndex`` raising a ``ValueError`` when boolean indexing with ``loc`` (:issue:`14946`)
655-
- Bug in ``DatetimeIndex.round()`` and ``Timestamp.round()`` floating point accuracy when rounding by milliseconds (:issue: `14440`)
655+
- Bug in ``DatetimeIndex.round()`` and ``Timestamp.round()`` floating point accuracy when rounding by milliseconds or less (:issue: `14440`, :issue:`15578`)
656656
- Bug in ``astype()`` where ``inf`` values were incorrectly converted to integers. Now raises error now with ``astype()`` for Series and DataFrames (:issue:`14265`)
657657
- Bug in ``DataFrame(..).apply(to_numeric)`` when values are of type decimal.Decimal. (:issue:`14827`)
658658
- Bug in ``describe()`` when passing a numpy array which does not contain the median to the ``percentiles`` keyword argument (:issue:`14908`)

pandas/tests/indexes/datetimes/test_ops.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,29 @@ def test_round(self):
175175
tm.assertRaisesRegexp(ValueError, msg, rng.round, freq='M')
176176
tm.assertRaisesRegexp(ValueError, msg, elt.round, freq='M')
177177

178-
# GH 14440
178+
# GH 14440 & 15578
179179
index = pd.DatetimeIndex(['2016-10-17 12:00:00.0015'], tz=tz)
180180
result = index.round('ms')
181181
expected = pd.DatetimeIndex(['2016-10-17 12:00:00.002000'], tz=tz)
182182
tm.assert_index_equal(result, expected)
183183

184+
for freq in ['us', 'ns']:
185+
tm.assert_index_equal(index, index.round(freq))
186+
184187
index = pd.DatetimeIndex(['2016-10-17 12:00:00.00149'], tz=tz)
185188
result = index.round('ms')
186189
expected = pd.DatetimeIndex(['2016-10-17 12:00:00.001000'], tz=tz)
187190
tm.assert_index_equal(result, expected)
188191

192+
index = pd.DatetimeIndex(['2016-10-17 12:00:00.001501031'])
193+
result = index.round('10ns')
194+
expected = pd.DatetimeIndex(['2016-10-17 12:00:00.001501030'])
195+
tm.assert_index_equal(result, expected)
196+
197+
with tm.assert_produces_warning():
198+
ts = '2016-10-17 12:00:00.001501031'
199+
pd.DatetimeIndex([ts]).round('1010ns')
200+
189201
def test_repeat_range(self):
190202
rng = date_range('1/1/2000', '1/1/2001')
191203

pandas/tests/scalar/test_timestamp.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ def test_round(self):
732732
for freq in ['Y', 'M', 'foobar']:
733733
self.assertRaises(ValueError, lambda: dti.round(freq))
734734

735-
# GH 14440
735+
# GH 14440 & 15578
736736
result = pd.Timestamp('2016-10-17 12:00:00.0015').round('ms')
737737
expected = pd.Timestamp('2016-10-17 12:00:00.002000')
738738
self.assertEqual(result, expected)
@@ -741,6 +741,17 @@ def test_round(self):
741741
expected = pd.Timestamp('2016-10-17 12:00:00.001000')
742742
self.assertEqual(result, expected)
743743

744+
ts = pd.Timestamp('2016-10-17 12:00:00.0015')
745+
for freq in ['us', 'ns']:
746+
self.assertEqual(ts, ts.round(freq))
747+
748+
result = pd.Timestamp('2016-10-17 12:00:00.001501031').round('10ns')
749+
expected = pd.Timestamp('2016-10-17 12:00:00.001501030')
750+
self.assertEqual(result, expected)
751+
752+
with tm.assert_produces_warning():
753+
pd.Timestamp('2016-10-17 12:00:00.001501031').round('1010ns')
754+
744755
def test_class_ops_pytz(self):
745756
tm._skip_if_no_pytz()
746757
from pytz import timezone

pandas/tseries/base.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Base and utility classes for tseries type pandas objects.
33
"""
4+
import warnings
45

56
from datetime import datetime, timedelta
67

@@ -79,11 +80,20 @@ def _round(self, freq, rounder):
7980

8081
from pandas.tseries.frequencies import to_offset
8182
unit = to_offset(freq).nanos
82-
8383
# round the local times
8484
values = _ensure_datetimelike_to_i8(self)
85-
86-
result = (unit * rounder(values / float(unit)).astype('i8'))
85+
if unit < 1000 and unit % 1000 != 0:
86+
# for nano rounding, work with the last 6 digits separately
87+
# due to float precision
88+
buff = 1000000
89+
result = (buff * (values // buff) + unit *
90+
(rounder((values % buff) / float(unit))).astype('i8'))
91+
elif unit >= 1000 and unit % 1000 != 0:
92+
msg = 'Precision will be lost using frequency: {}'
93+
warnings.warn(msg.format(freq))
94+
result = (unit * rounder(values / float(unit)).astype('i8'))
95+
else:
96+
result = (unit * rounder(values / float(unit)).astype('i8'))
8797
result = self._maybe_mask_results(result, fill_value=tslib.NaT)
8898
attribs = self._get_attributes_dict()
8999
if 'freq' in attribs:

pandas/tslib.pyx

+12-1
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,18 @@ class Timestamp(_Timestamp):
421421
value = self.tz_localize(None).value
422422
else:
423423
value = self.value
424-
result = (unit * rounder(value / float(unit)).astype('i8'))
424+
if unit < 1000 and unit % 1000 != 0:
425+
# for nano rounding, work with the last 6 digits separately
426+
# due to float precision
427+
buff = 1000000
428+
result = (buff * (value // buff) + unit *
429+
(rounder((value % buff) / float(unit))).astype('i8'))
430+
elif unit >= 1000 and unit % 1000 != 0:
431+
msg = 'Precision will be lost using frequency: {}'
432+
warnings.warn(msg.format(freq))
433+
result = (unit * rounder(value / float(unit)).astype('i8'))
434+
else:
435+
result = (unit * rounder(value / float(unit)).astype('i8'))
425436
result = Timestamp(result, unit='ns')
426437
if self.tz is not None:
427438
result = result.tz_localize(self.tz)

0 commit comments

Comments
 (0)