Skip to content

Commit 81358e8

Browse files
mroeschkejreback
authored andcommitted
BUG: Avoid Timedelta rounding when specifying unit and integer (#12690) (#19732)
1 parent 508ec3d commit 81358e8

File tree

6 files changed

+79
-47
lines changed

6 files changed

+79
-47
lines changed

doc/source/whatsnew/v0.23.1.txt

+4-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ Strings
5555
^^^^^^^
5656

5757
- Bug in :meth:`Series.str.replace()` where the method throws `TypeError` on Python 3.5.2 (:issue: `21078`)
58-
-
58+
59+
Timedelta
60+
^^^^^^^^^
61+
- Bug in :class:`Timedelta`: where passing a float with a unit would prematurely round the float precision (:issue: `14156`)
5962

6063
Categorical
6164
^^^^^^^^^^^

pandas/_libs/tslibs/timedeltas.pyx

+8-8
Original file line numberDiff line numberDiff line change
@@ -202,22 +202,22 @@ cpdef inline int64_t cast_from_unit(object ts, object unit) except? -1:
202202

203203
if unit == 'D' or unit == 'd':
204204
m = 1000000000L * 86400
205-
p = 6
205+
p = 9
206206
elif unit == 'h':
207207
m = 1000000000L * 3600
208-
p = 6
208+
p = 9
209209
elif unit == 'm':
210210
m = 1000000000L * 60
211-
p = 6
211+
p = 9
212212
elif unit == 's':
213213
m = 1000000000L
214-
p = 6
214+
p = 9
215215
elif unit == 'ms':
216216
m = 1000000L
217-
p = 3
217+
p = 6
218218
elif unit == 'us':
219219
m = 1000L
220-
p = 0
220+
p = 3
221221
elif unit == 'ns' or unit is None:
222222
m = 1L
223223
p = 0
@@ -231,10 +231,10 @@ cpdef inline int64_t cast_from_unit(object ts, object unit) except? -1:
231231
# cast the unit, multiply base/frace separately
232232
# to avoid precision issues from float -> int
233233
base = <int64_t> ts
234-
frac = ts -base
234+
frac = ts - base
235235
if p:
236236
frac = round(frac, p)
237-
return <int64_t> (base *m) + <int64_t> (frac *m)
237+
return <int64_t> (base * m) + <int64_t> (frac * m)
238238

239239

240240
cdef inline _decode_if_necessary(object ts):

pandas/tests/indexes/datetimes/test_tools.py

+8
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,14 @@ def test_unit_mixed(self, cache):
650650
with pytest.raises(ValueError):
651651
pd.to_datetime(arr, errors='raise', cache=cache)
652652

653+
@pytest.mark.parametrize('cache', [True, False])
654+
def test_unit_rounding(self, cache):
655+
# GH 14156: argument will incur floating point errors but no
656+
# premature rounding
657+
result = pd.to_datetime(1434743731.8770001, unit='s', cache=cache)
658+
expected = pd.Timestamp('2015-06-19 19:55:31.877000093')
659+
assert result == expected
660+
653661
@pytest.mark.parametrize('cache', [True, False])
654662
def test_dataframe(self, cache):
655663

pandas/tests/io/sas/test_sas7bdat.py

+2
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ def test_date_time():
182182
fname = os.path.join(dirpath, "datetime.csv")
183183
df0 = pd.read_csv(fname, parse_dates=['Date1', 'Date2', 'DateTime',
184184
'DateTimeHi', 'Taiw'])
185+
# GH 19732: Timestamps imported from sas will incur floating point errors
186+
df.iloc[:, 3] = df.iloc[:, 3].dt.round('us')
185187
tm.assert_frame_equal(df, df0)
186188

187189

pandas/tests/scalar/timedelta/test_timedelta.py

+10
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ def test_compare_timedelta_ndarray(self):
106106

107107
class TestTimedeltas(object):
108108

109+
@pytest.mark.parametrize("unit, value, expected", [
110+
('us', 9.999, 9999), ('ms', 9.999999, 9999999),
111+
('s', 9.999999999, 9999999999)])
112+
def test_rounding_on_int_unit_construction(self, unit, value, expected):
113+
# GH 12690
114+
result = Timedelta(value, unit=unit)
115+
assert result.value == expected
116+
result = Timedelta(str(value) + unit)
117+
assert result.value == expected
118+
109119
def test_total_seconds_scalar(self):
110120
# see gh-10939
111121
rng = Timedelta('1 days, 10:11:12.100123456')

pandas/tests/scalar/timestamp/test_timestamp.py

+47-38
Original file line numberDiff line numberDiff line change
@@ -621,10 +621,51 @@ def test_basics_nanos(self):
621621
assert stamp.microsecond == 145224
622622
assert stamp.nanosecond == 192
623623

624-
def test_unit(self):
625-
626-
def check(val, unit=None, h=1, s=1, us=0):
627-
stamp = Timestamp(val, unit=unit)
624+
@pytest.mark.parametrize('value, check_kwargs', [
625+
[946688461000000000, {}],
626+
[946688461000000000 / long(1000), dict(unit='us')],
627+
[946688461000000000 / long(1000000), dict(unit='ms')],
628+
[946688461000000000 / long(1000000000), dict(unit='s')],
629+
[10957, dict(unit='D', h=0)],
630+
pytest.param((946688461000000000 + 500000) / long(1000000000),
631+
dict(unit='s', us=499, ns=964),
632+
marks=pytest.mark.skipif(not PY3,
633+
reason='using truediv, so these'
634+
' are like floats')),
635+
pytest.param((946688461000000000 + 500000000) / long(1000000000),
636+
dict(unit='s', us=500000),
637+
marks=pytest.mark.skipif(not PY3,
638+
reason='using truediv, so these'
639+
' are like floats')),
640+
pytest.param((946688461000000000 + 500000) / long(1000000),
641+
dict(unit='ms', us=500),
642+
marks=pytest.mark.skipif(not PY3,
643+
reason='using truediv, so these'
644+
' are like floats')),
645+
pytest.param((946688461000000000 + 500000) / long(1000000000),
646+
dict(unit='s'),
647+
marks=pytest.mark.skipif(PY3,
648+
reason='get chopped in py2')),
649+
pytest.param((946688461000000000 + 500000000) / long(1000000000),
650+
dict(unit='s'),
651+
marks=pytest.mark.skipif(PY3,
652+
reason='get chopped in py2')),
653+
pytest.param((946688461000000000 + 500000) / long(1000000),
654+
dict(unit='ms'),
655+
marks=pytest.mark.skipif(PY3,
656+
reason='get chopped in py2')),
657+
[(946688461000000000 + 500000) / long(1000), dict(unit='us', us=500)],
658+
[(946688461000000000 + 500000000) / long(1000000),
659+
dict(unit='ms', us=500000)],
660+
[946688461000000000 / 1000.0 + 5, dict(unit='us', us=5)],
661+
[946688461000000000 / 1000.0 + 5000, dict(unit='us', us=5000)],
662+
[946688461000000000 / 1000000.0 + 0.5, dict(unit='ms', us=500)],
663+
[946688461000000000 / 1000000.0 + 0.005, dict(unit='ms', us=5, ns=5)],
664+
[946688461000000000 / 1000000000.0 + 0.5, dict(unit='s', us=500000)],
665+
[10957 + 0.5, dict(unit='D', h=12)]])
666+
def test_unit(self, value, check_kwargs):
667+
def check(value, unit=None, h=1, s=1, us=0, ns=0):
668+
stamp = Timestamp(value, unit=unit)
628669
assert stamp.year == 2000
629670
assert stamp.month == 1
630671
assert stamp.day == 1
@@ -637,41 +678,9 @@ def check(val, unit=None, h=1, s=1, us=0):
637678
assert stamp.minute == 0
638679
assert stamp.second == 0
639680
assert stamp.microsecond == 0
640-
assert stamp.nanosecond == 0
641-
642-
ts = Timestamp('20000101 01:01:01')
643-
val = ts.value
644-
days = (ts - Timestamp('1970-01-01')).days
645-
646-
check(val)
647-
check(val / long(1000), unit='us')
648-
check(val / long(1000000), unit='ms')
649-
check(val / long(1000000000), unit='s')
650-
check(days, unit='D', h=0)
681+
assert stamp.nanosecond == ns
651682

652-
# using truediv, so these are like floats
653-
if PY3:
654-
check((val + 500000) / long(1000000000), unit='s', us=500)
655-
check((val + 500000000) / long(1000000000), unit='s', us=500000)
656-
check((val + 500000) / long(1000000), unit='ms', us=500)
657-
658-
# get chopped in py2
659-
else:
660-
check((val + 500000) / long(1000000000), unit='s')
661-
check((val + 500000000) / long(1000000000), unit='s')
662-
check((val + 500000) / long(1000000), unit='ms')
663-
664-
# ok
665-
check((val + 500000) / long(1000), unit='us', us=500)
666-
check((val + 500000000) / long(1000000), unit='ms', us=500000)
667-
668-
# floats
669-
check(val / 1000.0 + 5, unit='us', us=5)
670-
check(val / 1000.0 + 5000, unit='us', us=5000)
671-
check(val / 1000000.0 + 0.5, unit='ms', us=500)
672-
check(val / 1000000.0 + 0.005, unit='ms', us=5)
673-
check(val / 1000000000.0 + 0.5, unit='s', us=500000)
674-
check(days + 0.5, unit='D', h=12)
683+
check(value, **check_kwargs)
675684

676685
def test_roundtrip(self):
677686

0 commit comments

Comments
 (0)