Skip to content

Commit 819e0ad

Browse files
committed
Merge pull request #2955 from jreback/timedelta_issues
BUG: negative timedeltas not printing correctly
2 parents 10dd532 + ad7a399 commit 819e0ad

File tree

10 files changed

+272
-69
lines changed

10 files changed

+272
-69
lines changed

RELEASE.rst

+4-2
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,12 @@ pandas 0.11.0
115115
- Series ops with a Timestamp on the rhs was throwing an exception (GH2898_)
116116
added tests for Series ops with datetimes,timedeltas,Timestamps, and datelike
117117
Series on both lhs and rhs
118-
- Series will now set its dtype automatically to ``timedelta64[ns]``
119-
if all passed objects are timedelta objects
118+
- Fixed subtle timedelta64 inference issue on py3
119+
- Fixed some formatting issues on timedelta when negative
120120
- Support null checking on timedelta64, representing (and formatting) with NaT
121121
- Support setitem with np.nan value, converts to NaT
122+
- Support min/max ops in a Dataframe (abs not working, nor do we error on non-supported ops)
123+
- Support idxmin/idxmax in a Series (but with no NaT)
122124

123125
- Bug on in-place putmasking on an ``integer`` series that needs to be converted to ``float`` (GH2746_)
124126

doc/source/timeseries.rst

+16
Original file line numberDiff line numberDiff line change
@@ -961,3 +961,19 @@ Operands can also appear in a reversed order (a singluar object operated with a
961961
s.max() - s
962962
datetime(2011,1,1,3,5) - s
963963
timedelta(minutes=5) + s
964+
965+
Some timedelta numeric like operations are supported.
966+
967+
.. ipython:: python
968+
969+
s = Series(date_range('2012-1-1', periods=3, freq='D'))
970+
df = DataFrame(dict(A = s - Timestamp('20120101')-timedelta(minutes=5,seconds=5),
971+
B = s - Series(date_range('2012-1-2', periods=3, freq='D'))))
972+
df
973+
974+
# timedelta arithmetic
975+
td - timedelta(minutes=5,seconds=5,microseconds=5)
976+
977+
# min/max operations
978+
df.min()
979+
df.min(axis=1)

pandas/core/common.py

+7
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,13 @@ def _possibly_convert_platform(values):
918918

919919
def _possibly_cast_to_timedelta(value):
920920
""" try to cast to timedelta64 w/o coercion """
921+
922+
# deal with numpy not being able to handle certain timedelta operations
923+
if isinstance(value,np.ndarray) and value.dtype.kind == 'm':
924+
if value.dtype != 'timedelta64[ns]':
925+
value = value.astype('timedelta64[ns]')
926+
return value
927+
921928
new_value = tslib.array_to_timedelta64(value.astype(object), coerce=False)
922929
if new_value.dtype == 'i8':
923930
value = np.array(new_value,dtype='timedelta64[ns]')

pandas/core/nanops.py

+39-22
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def f(values, axis=None, skipna=True, **kwds):
5555

5656
def _bn_ok_dtype(dt):
5757
# Bottleneck chokes on datetime64
58-
return dt != np.object_ and not issubclass(dt.type, np.datetime64)
58+
return dt != np.object_ and not issubclass(dt.type, (np.datetime64,np.timedelta64))
5959

6060

6161
def _has_infs(result):
@@ -69,6 +69,34 @@ def _has_infs(result):
6969
else:
7070
return np.isinf(result) or np.isneginf(result)
7171

72+
def _isfinite(values):
73+
if issubclass(values.dtype.type, np.timedelta64):
74+
return isnull(values)
75+
return -np.isfinite(values)
76+
77+
def _na_ok_dtype(dtype):
78+
return not issubclass(dtype.type, (np.integer, np.datetime64, np.timedelta64))
79+
80+
def _view_if_needed(values):
81+
if issubclass(values.dtype.type, (np.datetime64,np.timedelta64)):
82+
return values.view(np.int64)
83+
return values
84+
85+
def _wrap_results(result,dtype):
86+
""" wrap our results if needed """
87+
88+
if issubclass(dtype.type, np.datetime64):
89+
if not isinstance(result, np.ndarray):
90+
result = lib.Timestamp(result)
91+
else:
92+
result = result.view(dtype)
93+
elif issubclass(dtype.type, np.timedelta64):
94+
if not isinstance(result, np.ndarray):
95+
pass
96+
else:
97+
result = result.view(dtype)
98+
99+
return result
72100

73101
def nanany(values, axis=None, skipna=True):
74102
mask = isnull(values)
@@ -162,13 +190,11 @@ def _nanmin(values, axis=None, skipna=True):
162190

163191
dtype = values.dtype
164192

165-
if skipna and not issubclass(dtype.type,
166-
(np.integer, np.datetime64)):
193+
if skipna and _na_ok_dtype(dtype):
167194
values = values.copy()
168195
np.putmask(values, mask, np.inf)
169196

170-
if issubclass(dtype.type, np.datetime64):
171-
values = values.view(np.int64)
197+
values = _view_if_needed(values)
172198

173199
# numpy 1.6.1 workaround in Python 3.x
174200
if (values.dtype == np.object_
@@ -187,12 +213,7 @@ def _nanmin(values, axis=None, skipna=True):
187213
else:
188214
result = values.min(axis)
189215

190-
if issubclass(dtype.type, np.datetime64):
191-
if not isinstance(result, np.ndarray):
192-
result = lib.Timestamp(result)
193-
else:
194-
result = result.view(dtype)
195-
216+
result = _wrap_results(result,dtype)
196217
return _maybe_null_out(result, axis, mask)
197218

198219

@@ -201,12 +222,11 @@ def _nanmax(values, axis=None, skipna=True):
201222

202223
dtype = values.dtype
203224

204-
if skipna and not issubclass(dtype.type, (np.integer, np.datetime64)):
225+
if skipna and _na_ok_dtype(dtype):
205226
values = values.copy()
206227
np.putmask(values, mask, -np.inf)
207228

208-
if issubclass(dtype.type, np.datetime64):
209-
values = values.view(np.int64)
229+
values = _view_if_needed(values)
210230

211231
# numpy 1.6.1 workaround in Python 3.x
212232
if (values.dtype == np.object_
@@ -226,20 +246,16 @@ def _nanmax(values, axis=None, skipna=True):
226246
else:
227247
result = values.max(axis)
228248

229-
if issubclass(dtype.type, np.datetime64):
230-
if not isinstance(result, np.ndarray):
231-
result = lib.Timestamp(result)
232-
else:
233-
result = result.view(dtype)
234-
249+
result = _wrap_results(result,dtype)
235250
return _maybe_null_out(result, axis, mask)
236251

237252

238253
def nanargmax(values, axis=None, skipna=True):
239254
"""
240255
Returns -1 in the NA case
241256
"""
242-
mask = -np.isfinite(values)
257+
mask = _isfinite(values)
258+
values = _view_if_needed(values)
243259
if not issubclass(values.dtype.type, np.integer):
244260
values = values.copy()
245261
np.putmask(values, mask, -np.inf)
@@ -252,7 +268,8 @@ def nanargmin(values, axis=None, skipna=True):
252268
"""
253269
Returns -1 in the NA case
254270
"""
255-
mask = -np.isfinite(values)
271+
mask = _isfinite(values)
272+
values = _view_if_needed(values)
256273
if not issubclass(values.dtype.type, np.integer):
257274
values = values.copy()
258275
np.putmask(values, mask, np.inf)

pandas/core/series.py

+21-16
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ def wrapper(self, other):
8282

8383
lvalues, rvalues = self, other
8484

85-
is_timedelta = com.is_timedelta64_dtype(self)
86-
is_datetime = com.is_datetime64_dtype(self)
85+
is_timedelta_lhs = com.is_timedelta64_dtype(self)
86+
is_datetime_lhs = com.is_datetime64_dtype(self)
8787

88-
if is_datetime or is_timedelta:
88+
if is_datetime_lhs or is_timedelta_lhs:
8989

9090
# convert the argument to an ndarray
9191
def convert_to_array(values):
@@ -97,26 +97,27 @@ def convert_to_array(values):
9797
pass
9898
else:
9999
values = tslib.array_to_datetime(values)
100+
elif inferred_type in set(['timedelta','timedelta64']):
101+
# need to convert timedelta to ns here
102+
# safest to convert it to an object arrany to process
103+
if isinstance(values, pa.Array) and com.is_timedelta64_dtype(values):
104+
pass
105+
else:
106+
values = com._possibly_cast_to_timedelta(values)
100107
else:
101108
values = pa.array(values)
102109
return values
103110

104-
# swap the valuesor com.is_timedelta64_dtype(self):
105-
if is_timedelta:
106-
lvalues, rvalues = rvalues, lvalues
107-
lvalues = convert_to_array(lvalues)
108-
is_timedelta = False
109-
111+
# convert lhs and rhs
112+
lvalues = convert_to_array(lvalues)
110113
rvalues = convert_to_array(rvalues)
111114

112-
# rhs is either a timedelta or a series/ndarray
113-
if lib.is_timedelta_or_timedelta64_array(rvalues):
115+
is_timedelta_rhs = com.is_timedelta64_dtype(rvalues)
116+
is_datetime_rhs = com.is_datetime64_dtype(rvalues)
114117

115-
# need to convert timedelta to ns here
116-
# safest to convert it to an object arrany to process
117-
rvalues = tslib.array_to_timedelta64(rvalues.astype(object))
118-
dtype = 'M8[ns]'
119-
elif com.is_datetime64_dtype(rvalues):
118+
# 2 datetimes or 2 timedeltas
119+
if (is_timedelta_lhs and is_timedelta_rhs) or (is_datetime_lhs and is_datetime_rhs):
120+
120121
dtype = 'timedelta64[ns]'
121122

122123
# we may have to convert to object unfortunately here
@@ -127,6 +128,10 @@ def wrap_results(x):
127128
np.putmask(x,mask,tslib.iNaT)
128129
return x
129130

131+
# datetime and timedelta
132+
elif (is_timedelta_lhs and is_datetime_rhs) or (is_timedelta_rhs and is_datetime_lhs):
133+
dtype = 'M8[ns]'
134+
130135
else:
131136
raise ValueError('cannot operate on a series with out a rhs '
132137
'of a series/ndarray of type datetime64[ns] '

pandas/src/inference.pyx

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ try:
2424
_TYPE_MAP[np.complex256] = 'complex'
2525
_TYPE_MAP[np.float16] = 'floating'
2626
_TYPE_MAP[np.datetime64] = 'datetime64'
27+
_TYPE_MAP[np.timedelta64] = 'timedelta64'
2728
except AttributeError:
2829
pass
2930

pandas/tests/test_format.py

+39-6
Original file line numberDiff line numberDiff line change
@@ -1209,24 +1209,57 @@ def test_float_trim_zeros(self):
12091209
def test_timedelta64(self):
12101210

12111211
from pandas import date_range
1212-
from datetime import datetime
1212+
from datetime import datetime, timedelta
12131213

12141214
Series(np.array([1100, 20], dtype='timedelta64[s]')).to_string()
1215-
# check this works
1215+
1216+
s = Series(date_range('2012-1-1', periods=3, freq='D'))
1217+
12161218
# GH2146
12171219

12181220
# adding NaTs
1219-
s = Series(date_range('2012-1-1', periods=3, freq='D'))
12201221
y = s-s.shift(1)
12211222
result = y.to_string()
12221223
self.assertTrue('1 days, 00:00:00' in result)
12231224
self.assertTrue('NaT' in result)
12241225

12251226
# with frac seconds
1226-
s = Series(date_range('2012-1-1', periods=3, freq='D'))
1227-
y = s-datetime(2012,1,1,microsecond=150)
1227+
o = Series([datetime(2012,1,1,microsecond=150)]*3)
1228+
y = s-o
1229+
result = y.to_string()
1230+
self.assertTrue('-00:00:00.000150' in result)
1231+
1232+
# rounding?
1233+
o = Series([datetime(2012,1,1,1)]*3)
1234+
y = s-o
1235+
result = y.to_string()
1236+
self.assertTrue('-01:00:00' in result)
1237+
self.assertTrue('1 days, 23:00:00' in result)
1238+
1239+
o = Series([datetime(2012,1,1,1,1)]*3)
1240+
y = s-o
1241+
result = y.to_string()
1242+
self.assertTrue('-01:01:00' in result)
1243+
self.assertTrue('1 days, 22:59:00' in result)
1244+
1245+
o = Series([datetime(2012,1,1,1,1,microsecond=150)]*3)
1246+
y = s-o
1247+
result = y.to_string()
1248+
self.assertTrue('-01:01:00.000150' in result)
1249+
self.assertTrue('1 days, 22:58:59.999850' in result)
1250+
1251+
# neg time
1252+
td = timedelta(minutes=5,seconds=3)
1253+
s2 = Series(date_range('2012-1-1', periods=3, freq='D')) + td
1254+
y = s - s2
1255+
result = y.to_string()
1256+
self.assertTrue('-00:05:03' in result)
1257+
1258+
td = timedelta(microseconds=550)
1259+
s2 = Series(date_range('2012-1-1', periods=3, freq='D')) + td
1260+
y = s - td
12281261
result = y.to_string()
1229-
self.assertTrue('00:00:00.000150' in result)
1262+
self.assertTrue('2012-01-01 23:59:59.999450' in result)
12301263

12311264
def test_mixed_datetime64(self):
12321265
df = DataFrame({'A': [1, 2],

pandas/tests/test_frame.py

+47
Original file line numberDiff line numberDiff line change
@@ -2884,6 +2884,53 @@ def test_timedeltas(self):
28842884
expected.sort()
28852885
assert_series_equal(result, expected)
28862886

2887+
def test_operators_timedelta64(self):
2888+
2889+
from pandas import date_range
2890+
from datetime import datetime, timedelta
2891+
df = DataFrame(dict(A = date_range('2012-1-1', periods=3, freq='D'),
2892+
B = date_range('2012-1-2', periods=3, freq='D'),
2893+
C = Timestamp('20120101')-timedelta(minutes=5,seconds=5)))
2894+
2895+
diffs = DataFrame(dict(A = df['A']-df['C'],
2896+
B = df['A']-df['B']))
2897+
2898+
2899+
# min
2900+
result = diffs.min()
2901+
self.assert_(result[0] == diffs.ix[0,'A'])
2902+
self.assert_(result[1] == diffs.ix[0,'B'])
2903+
2904+
result = diffs.min(axis=1)
2905+
self.assert_((result == diffs.ix[0,'B']).all() == True)
2906+
2907+
# max
2908+
result = diffs.max()
2909+
self.assert_(result[0] == diffs.ix[2,'A'])
2910+
self.assert_(result[1] == diffs.ix[2,'B'])
2911+
2912+
result = diffs.max(axis=1)
2913+
self.assert_((result == diffs['A']).all() == True)
2914+
2915+
# abs ###### THIS IS BROKEN NOW ###### (results are dtype=timedelta64[us]
2916+
result = np.abs(df['A']-df['B'])
2917+
result = diffs.abs()
2918+
expected = DataFrame(dict(A = df['A']-df['C'],
2919+
B = df['B']-df['A']))
2920+
#assert_frame_equal(result,expected)
2921+
2922+
# mixed frame
2923+
mixed = diffs.copy()
2924+
mixed['C'] = 'foo'
2925+
mixed['D'] = 1
2926+
mixed['E'] = 1.
2927+
2928+
# this is ok
2929+
result = mixed.min()
2930+
2931+
# this is not
2932+
result = mixed.min(axis=1)
2933+
28872934
def test_new_empty_index(self):
28882935
df1 = DataFrame(randn(0, 3))
28892936
df2 = DataFrame(randn(0, 3))

0 commit comments

Comments
 (0)