Skip to content

Commit b7e80a5

Browse files
committed
DOC/ENH: timedelta conversions & docs
TST: add tests/catch for non-absolute DateOffsets in timedelta operations
1 parent 18bb625 commit b7e80a5

File tree

5 files changed

+224
-68
lines changed

5 files changed

+224
-68
lines changed

doc/source/release.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ pandas 0.13
5353
- Add ``rename`` and ``set_names`` methods to ``Index`` as well as
5454
``set_names``, ``set_levels``, ``set_labels`` to ``MultiIndex``.
5555
(:issue:`4039`)
56-
- A Series of dtype ``Timedelta64[ns]`` can now be divided/multiplied
56+
- A Series of dtype ``timedelta64[ns]`` can now be divided/multiplied
5757
by an integer series (:issue`4521`)
58+
- A Series of dtype ``timedelta64[ns]`` can now be divided by another
59+
``timedelta64[ns]`` object to yield a ``float64`` dtyped Series. This
60+
is frequency conversion.
5861

5962
**API Changes**
6063

doc/source/timeseries.rst

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Take care, ``to_datetime`` may not act as you expect on mixed data:
170170

171171
.. ipython:: python
172172
173-
pd.to_datetime([1, '1'])
173+
to_datetime([1, '1'])
174174
175175
.. _timeseries.daterange:
176176

@@ -297,7 +297,7 @@ the year or year and month as strings:
297297
298298
ts['2011-6']
299299
300-
This type of slicing will work on a DataFrame with a ``DateTimeIndex`` as well. Since the
300+
This type of slicing will work on a DataFrame with a ``DateTimeIndex`` as well. Since the
301301
partial string selection is a form of label slicing, the endpoints **will be** included. This
302302
would include matching times on an included date. Here's an example:
303303

@@ -1112,7 +1112,8 @@ Time Deltas
11121112
-----------
11131113

11141114
Timedeltas are differences in times, expressed in difference units, e.g. days,hours,minutes,seconds.
1115-
They can be both positive and negative.
1115+
They can be both positive and negative. :ref:`DateOffsets<timeseries.offsets>` that are absolute in nature
1116+
(``Day, Hour, Minute, Second, Milli, Micro, Nano``) can be used as ``timedeltas``.
11161117

11171118
.. ipython:: python
11181119
@@ -1128,41 +1129,16 @@ They can be both positive and negative.
11281129
s - s.max()
11291130
s - datetime(2011,1,1,3,5)
11301131
s + timedelta(minutes=5)
1132+
s + Minute(5)
1133+
s + Minute(5) + Milli(5)
11311134
11321135
Getting scalar results from a ``timedelta64[ns]`` series
11331136

1134-
.. ipython:: python
1135-
:suppress:
1136-
1137-
from distutils.version import LooseVersion
1138-
11391137
.. ipython:: python
11401138
11411139
y = s - s[0]
11421140
y
11431141
1144-
.. code-block:: python
1145-
1146-
if LooseVersion(np.__version__) <= '1.6.2':
1147-
y.apply(lambda x: x.item().total_seconds())
1148-
y.apply(lambda x: x.item().days)
1149-
else:
1150-
y.apply(lambda x: x / np.timedelta64(1, 's'))
1151-
y.apply(lambda x: x / np.timedelta64(1, 'D'))
1152-
1153-
.. note::
1154-
1155-
As you can see from the conditional statement above, these operations are
1156-
different in numpy 1.6.2 and in numpy >= 1.7. The ``timedelta64[ns]`` scalar
1157-
type in 1.6.2 is much like a ``datetime.timedelta``, while in 1.7 it is a
1158-
nanosecond based integer. A future version of pandas will make this
1159-
transparent.
1160-
1161-
.. note::
1162-
1163-
In numpy >= 1.7 dividing a ``timedelta64`` array by another ``timedelta64``
1164-
array will yield an array with dtype ``np.float64``.
1165-
11661142
Series of timedeltas with ``NaT`` values are supported
11671143

11681144
.. ipython:: python
@@ -1218,3 +1194,55 @@ issues). ``idxmin, idxmax`` are supported as well.
12181194
12191195
df.min().idxmax()
12201196
df.min(axis=1).idxmin()
1197+
1198+
.. _timeseries.timedeltas_convert:
1199+
1200+
Time Deltas & Conversions
1201+
-------------------------
1202+
1203+
.. versionadded:: 0.13
1204+
1205+
Timedeltas can be converted to other 'frequencies' by dividing by another timedelta.
1206+
These operations yield ``float64`` dtyped Series.
1207+
1208+
.. ipython:: python
1209+
1210+
td = Series(date_range('20130101',periods=4))-Series(date_range('20121201',periods=4))
1211+
td[2] += np.timedelta64(timedelta(minutes=5,seconds=3))
1212+
td[3] = np.nan
1213+
td
1214+
1215+
# to days
1216+
td / np.timedelta64(1,'D')
1217+
1218+
# to seconds
1219+
td / np.timedelta64(1,'s')
1220+
1221+
Dividing or multiplying a ``timedelta64[ns]`` Series by an integer or integer Series
1222+
yields another ``timedelta64[ns]`` dtypes Series.
1223+
1224+
.. ipython:: python
1225+
1226+
td * -1
1227+
td * Series([1,2,3,4])
1228+
1229+
Numpy < 1.7 Compatibility
1230+
~~~~~~~~~~~~~~~~~~~~~~~~~
1231+
1232+
Numpy < 1.7 has a broken ``timedelta64`` type that does not correctly work
1233+
for arithmetic. Pandas bypasses this, but for frequency conversion as above,
1234+
you need to create the divisor yourself. The ``np.timetimedelta64`` type only
1235+
has 1 argument, the number of **micro** seconds.
1236+
1237+
The following are equivalent statements in the two versions of numpy.
1238+
1239+
.. code-block:: python
1240+
1241+
from distutils.version import LooseVersion
1242+
if LooseVersion(np.__version__) <= '1.6.2':
1243+
y / np.timedelta(86400*int(1e6))
1244+
y / np.timedelta(int(1e6))
1245+
else:
1246+
y / np.timedelta64(1,'D')
1247+
y / np.timedelta64(1,'s')
1248+

doc/source/v0.13.0.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,40 @@ Enhancements
100100
- Added a more informative error message when plot arguments contain
101101
overlapping color and style arguments (:issue:`4402`)
102102

103+
- ``timedelta64[ns]`` operations
104+
105+
- A Series of dtype ``timedelta64[ns]`` can now be divided by another
106+
``timedelta64[ns]`` object to yield a ``float64`` dtyped Series. This
107+
is frequency conversion. See :ref:`here<timeseries.timedeltas_convert>` for the docs.
108+
109+
.. ipython:: python
110+
111+
from datetime import timedelta
112+
td = Series(date_range('20130101',periods=4))-Series(date_range('20121201',periods=4))
113+
td[2] += np.timedelta64(timedelta(minutes=5,seconds=3))
114+
td[3] = np.nan
115+
td
116+
117+
# to days
118+
td / np.timedelta64(1,'D')
119+
120+
# to seconds
121+
td / np.timedelta64(1,'s')
122+
123+
- Dividing or multiplying a ``timedelta64[ns]`` Series by an integer or integer Series
124+
125+
.. ipython:: python
126+
127+
td * -1
128+
td * Series([1,2,3,4])
129+
130+
- Absolute ``DateOffset`` objects can act equivalenty to ``timedeltas``
131+
132+
.. ipython:: python
133+
134+
from pandas import offsets
135+
td + offsets.Minute(5) + offsets.Milli(5)
136+
103137
Bug Fixes
104138
~~~~~~~~~
105139

pandas/core/series.py

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,14 @@ def na_op(x, y):
8585
def wrapper(self, other, name=name):
8686
from pandas.core.frame import DataFrame
8787
dtype = None
88+
fill_value = tslib.iNaT
8889
wrap_results = lambda x: x
8990

9091
lvalues, rvalues = self, other
9192

9293
is_timedelta_lhs = com.is_timedelta64_dtype(self)
9394
is_datetime_lhs = com.is_datetime64_dtype(self)
94-
is_integer_lhs = lvalues.dtype == np.int64
95+
is_integer_lhs = lvalues.dtype.kind in ['i','u']
9596

9697
if is_datetime_lhs or is_timedelta_lhs:
9798

@@ -116,14 +117,19 @@ def convert_to_array(values):
116117
# py3 compat where dtype is 'm' but is an integer
117118
if values.dtype.kind == 'm':
118119
values = values.astype('timedelta64[ns]')
119-
elif name not in ['__div__','__mul__']:
120-
raise TypeError("incompatible type for a datetime/timedelta operation")
120+
elif name not in ['__truediv__','__div__','__mul__']:
121+
raise TypeError("incompatible type for a datetime/timedelta operation [{0}]".format(name))
121122
elif isinstance(values[0],DateOffset):
122123
# handle DateOffsets
123-
values = pa.array([ v.delta for v in values ])
124-
values = com._possibly_cast_to_timedelta(values, coerce=coerce)
124+
os = pa.array([ getattr(v,'delta',None) for v in values ])
125+
mask = isnull(os)
126+
if mask.any():
127+
raise TypeError("cannot use a non-absolute DateOffset in "
128+
"datetime/timedelta operations [{0}]".format(','.join([ com.pprint_thing(v) for v in values[mask] ])))
129+
values = com._possibly_cast_to_timedelta(os, coerce=coerce)
125130
else:
126-
values = pa.array(values)
131+
raise TypeError("incompatible type [{0}] for a datetime/timedelta operation".format(pa.array(values).dtype))
132+
127133
return values
128134

129135
# convert lhs and rhs
@@ -132,32 +138,51 @@ def convert_to_array(values):
132138

133139
is_datetime_rhs = com.is_datetime64_dtype(rvalues)
134140
is_timedelta_rhs = com.is_timedelta64_dtype(rvalues) or (not is_datetime_rhs and _np_version_under1p7)
135-
is_integer_rhs = rvalues.dtype == np.int64
141+
is_integer_rhs = rvalues.dtype.kind in ['i','u']
136142
mask = None
137143

138144
# timedelta and integer mul/div
139145
if (is_timedelta_lhs and is_integer_rhs) or (is_integer_lhs and is_timedelta_rhs):
140146

141-
if name not in ['__div__','__mul__']:
147+
if name not in ['__truediv__','__div__','__mul__']:
142148
raise TypeError("can only operate on a timedelta and an integer for "
143149
"division, but the operator [%s] was passed" % name)
144150
dtype = 'timedelta64[ns]'
145151
mask = isnull(lvalues) | isnull(rvalues)
146152
lvalues = lvalues.astype(np.int64)
147153
rvalues = rvalues.astype(np.int64)
148154

149-
# 2 datetimes or 2 timedeltas
150-
elif (is_timedelta_lhs and is_timedelta_rhs) or (is_datetime_lhs and
151-
is_datetime_rhs):
152-
if is_datetime_lhs and name != '__sub__':
155+
# 2 datetimes
156+
elif is_datetime_lhs and is_datetime_rhs:
157+
if name != '__sub__':
153158
raise TypeError("can only operate on a datetimes for subtraction, "
154159
"but the operator [%s] was passed" % name)
155-
elif is_timedelta_lhs and name not in ['__add__','__sub__']:
156-
raise TypeError("can only operate on a timedeltas for "
157-
"addition and subtraction, but the operator [%s] was passed" % name)
158160

159161
dtype = 'timedelta64[ns]'
160162
mask = isnull(lvalues) | isnull(rvalues)
163+
lvalues = lvalues.view('i8')
164+
rvalues = rvalues.view('i8')
165+
166+
# 2 timedeltas
167+
elif is_timedelta_lhs and is_timedelta_rhs:
168+
mask = isnull(lvalues) | isnull(rvalues)
169+
170+
# time delta division -> unit less
171+
if name in ['__div__','__truediv__']:
172+
dtype = 'float64'
173+
fill_value = np.nan
174+
lvalues = lvalues.astype(np.int64).astype(np.float64)
175+
rvalues = rvalues.astype(np.int64).astype(np.float64)
176+
177+
# another timedelta
178+
elif name in ['__add__','__sub__']:
179+
dtype = 'timedelta64[ns]'
180+
lvalues = lvalues.astype(np.int64)
181+
rvalues = rvalues.astype(np.int64)
182+
183+
else:
184+
raise TypeError("can only operate on a timedeltas for "
185+
"addition, subtraction, and division, but the operator [%s] was passed" % name)
161186

162187
# datetime and timedelta
163188
elif is_timedelta_rhs and is_datetime_lhs:
@@ -166,13 +191,17 @@ def convert_to_array(values):
166191
raise TypeError("can only operate on a datetime with a rhs of a timedelta for "
167192
"addition and subtraction, but the operator [%s] was passed" % name)
168193
dtype = 'M8[ns]'
194+
lvalues = lvalues.view('i8')
195+
rvalues = rvalues.view('i8')
169196

170197
elif is_timedelta_lhs and is_datetime_rhs:
171198

172199
if name not in ['__add__']:
173200
raise TypeError("can only operate on a timedelta and a datetime for "
174201
"addition, but the operator [%s] was passed" % name)
175202
dtype = 'M8[ns]'
203+
lvalues = lvalues.view('i8')
204+
rvalues = rvalues.view('i8')
176205

177206
else:
178207
raise TypeError('cannot operate on a series with out a rhs '
@@ -183,14 +212,11 @@ def convert_to_array(values):
183212
if mask is not None:
184213
if mask.any():
185214
def f(x):
186-
x = pa.array(x,dtype='timedelta64[ns]')
187-
np.putmask(x,mask,tslib.iNaT)
215+
x = pa.array(x,dtype=dtype)
216+
np.putmask(x,mask,fill_value)
188217
return x
189218
wrap_results = f
190219

191-
lvalues = lvalues.view('i8')
192-
rvalues = rvalues.view('i8')
193-
194220
if isinstance(rvalues, Series):
195221

196222
if hasattr(lvalues,'values'):

0 commit comments

Comments
 (0)