Skip to content

Commit c1e52f1

Browse files
committed
Merge pull request #9257 from jreback/td
API: restore full datetime.timedelta compat with Timedelta w.r.t. seconds/microseconds accessors (GH9185, GH9139)
2 parents 73ee031 + 7060deb commit c1e52f1

File tree

8 files changed

+124
-92
lines changed

8 files changed

+124
-92
lines changed

doc/source/timedeltas.rst

+9-21
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,13 @@ yields another ``timedelta64[ns]`` dtypes Series.
251251
Attributes
252252
----------
253253

254-
You can access various components of the ``Timedelta`` or ``TimedeltaIndex`` directly using the attributes ``days,hours,minutes,seconds,milliseconds,microseconds,nanoseconds``.
255-
These operations can be directly accessed via the ``.dt`` property of the ``Series`` as well. These return an integer representing that interval (which is signed according to whether the ``Timedelta`` is signed).
254+
You can access various components of the ``Timedelta`` or ``TimedeltaIndex`` directly using the attributes ``days,seconds,microseconds,nanoseconds``. These are identical to the values returned by ``datetime.timedelta``, in that, for example, the ``.seconds`` attribute represents the number of seconds >= 0 and < 1 day. These are signed according to whether the ``Timedelta`` is signed.
255+
256+
These operations can also be directly accessed via the ``.dt`` property of the ``Series`` as well.
257+
258+
.. note::
259+
260+
Note that the attributes are NOT the displayed values of the ``Timedelta``. Use ``.components`` to retrieve the displayed values.
256261

257262
For a ``Series``
258263

@@ -271,29 +276,12 @@ You can access the component field for a scalar ``Timedelta`` directly.
271276
(-tds).seconds
272277
273278
You can use the ``.components`` property to access a reduced form of the timedelta. This returns a ``DataFrame`` indexed
274-
similarly to the ``Series``
279+
similarly to the ``Series``. These are the *displayed* values of the ``Timedelta``.
275280

276281
.. ipython:: python
277282
278283
td.dt.components
279-
280-
.. _timedeltas.attribues_warn:
281-
282-
.. warning::
283-
284-
``Timedelta`` scalars (and ``TimedeltaIndex``) component fields are *not the same* as the component fields on a ``datetime.timedelta`` object. For example, ``.seconds`` on a ``datetime.timedelta`` object returns the total number of seconds combined between ``hours``, ``minutes`` and ``seconds``. In contrast, the pandas ``Timedelta`` breaks out hours, minutes, microseconds and nanoseconds separately.
285-
286-
.. ipython:: python
287-
288-
# Timedelta accessor
289-
tds = Timedelta('31 days 5 min 3 sec')
290-
tds.minutes
291-
tds.seconds
292-
293-
# datetime.timedelta accessor
294-
# this is 5 minutes * 60 + 3 seconds
295-
tds.to_pytimedelta().seconds
296-
284+
td.dt.components.seconds
297285
298286
.. _timedeltas.index:
299287

doc/source/whatsnew/v0.15.0.txt

+6-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ users upgrade to this version.
2929
- Split out string methods documentation into :ref:`Working with Text Data <text>`
3030

3131
- Check the :ref:`API Changes <whatsnew_0150.api>` and :ref:`deprecations <whatsnew_0150.deprecations>` before updating
32-
32+
3333
- :ref:`Other Enhancements <whatsnew_0150.enhancements>`
3434

3535
- :ref:`Performance Improvements <whatsnew_0150.performance>`
@@ -403,7 +403,7 @@ Rolling/Expanding Moments improvements
403403

404404
rolling_window(s, window=3, win_type='triang', center=True)
405405

406-
- Removed ``center`` argument from all :func:`expanding_ <expanding_apply>` functions (see :ref:`list <api.functions_expanding>`),
406+
- Removed ``center`` argument from all :func:`expanding_ <expanding_apply>` functions (see :ref:`list <api.functions_expanding>`),
407407
as the results produced when ``center=True`` did not make much sense. (:issue:`7925`)
408408

409409
- Added optional ``ddof`` argument to :func:`expanding_cov` and :func:`rolling_cov`.
@@ -574,20 +574,20 @@ for more details):
574574
.. code-block:: python
575575

576576
In [2]: pd.Categorical.from_codes([0,1,0,2,1], categories=['a', 'b', 'c'])
577-
Out[2]:
577+
Out[2]:
578578
[a, b, a, c, b]
579579
Categories (3, object): [a, b, c]
580580

581581
API changes related to the introduction of the ``Timedelta`` scalar (see
582582
:ref:`above <whatsnew_0150.timedeltaindex>` for more details):
583-
583+
584584
- Prior to 0.15.0 :func:`to_timedelta` would return a ``Series`` for list-like/Series input,
585585
and a ``np.timedelta64`` for scalar input. It will now return a ``TimedeltaIndex`` for
586586
list-like input, ``Series`` for Series input, and ``Timedelta`` for scalar input.
587587

588588
For API changes related to the rolling and expanding functions, see detailed overview :ref:`above <whatsnew_0150.roll>`.
589589

590-
Other notable API changes:
590+
Other notable API changes:
591591

592592
- Consistency when indexing with ``.loc`` and a list-like indexer when no values are found.
593593

@@ -872,7 +872,7 @@ Enhancements in the importing/exporting of Stata files:
872872
objects and columns containing missing values have ``object`` data type. (:issue:`8045`)
873873

874874
Enhancements in the plotting functions:
875-
875+
876876
- Added ``layout`` keyword to ``DataFrame.plot``. You can pass a tuple of ``(rows, columns)``, one of which can be ``-1`` to automatically infer (:issue:`6667`, :issue:`8071`).
877877
- Allow to pass multiple axes to ``DataFrame.plot``, ``hist`` and ``boxplot`` (:issue:`5353`, :issue:`6970`, :issue:`7069`)
878878
- Added support for ``c``, ``colormap`` and ``colorbar`` arguments for ``DataFrame.plot`` with ``kind='scatter'`` (:issue:`7780`)

doc/source/whatsnew/v0.16.0.txt

+35
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,41 @@ Backwards incompatible API changes
2727

2828
.. _whatsnew_0160.api_breaking:
2929

30+
- In v0.15.0 a new scalar type ``Timedelta`` was introduced, that is a sub-class of ``datetime.timedelta``. Mentioned :ref:`here <whatsnew_0150.timedeltaindex>` was a notice of an API change w.r.t. the ``.seconds`` accessor. The intent was to provide a user-friendly set of accessors that give the 'natural' value for that unit, e.g. if you had a ``Timedelta('1 day, 10:11:12')``, then ``.seconds`` would return 12. However, this is at odds with the definition of ``datetime.timedelta``, which defines ``.seconds`` as ``10 * 3600 + 11 * 60 + 12 == 36672``.
31+
32+
So in v0.16.0, we are restoring the API to match that of ``datetime.timedelta``. However, the component values are still available through the ``.components`` accessor. This affects the ``.seconds`` and ``.microseconds`` accessors, and removes the ``.hours``, ``.minutes``, ``.milliseconds`` accessors. These changes affect ``TimedeltaIndex`` and the Series ``.dt`` accessor as well. (:issue:`9185`, :issue:`9139`)
33+
34+
Previous Behavior
35+
36+
.. code-block:: python
37+
38+
In [2]: t = pd.Timedelta('1 day, 10:11:12.100123')
39+
40+
In [3]: t.days
41+
Out[3]: 1
42+
43+
In [4]: t.seconds
44+
Out[4]: 12
45+
46+
In [5]: t.microseconds
47+
Out[5]: 123
48+
49+
New Behavior
50+
51+
.. ipython:: python
52+
53+
t = pd.Timedelta('1 day, 10:11:12.100123')
54+
t.days
55+
t.seconds
56+
t.microseconds
57+
58+
Using ``.components`` allows the full component access
59+
60+
.. ipython:: python
61+
62+
t.components
63+
t.components.seconds
64+
3065
- ``Index.duplicated`` now returns `np.array(dtype=bool)` rather than `Index(dtype=object)` containing `bool` values. (:issue:`8875`)
3166
- ``DataFrame.to_json`` now returns accurate type serialisation for each column for frames of mixed dtype (:issue:`9037`)
3267

pandas/io/tests/test_excel.py

+23
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,29 @@ def test_swapped_columns(self):
11511151
tm.assert_series_equal(write_frame['A'], read_frame['A'])
11521152
tm.assert_series_equal(write_frame['B'], read_frame['B'])
11531153

1154+
def test_datetimes(self):
1155+
1156+
# Test writing and reading datetimes. For issue #9139. (xref #9185)
1157+
_skip_if_no_xlrd()
1158+
1159+
datetimes = [datetime(2013, 1, 13, 1, 2, 3),
1160+
datetime(2013, 1, 13, 2, 45, 56),
1161+
datetime(2013, 1, 13, 4, 29, 49),
1162+
datetime(2013, 1, 13, 6, 13, 42),
1163+
datetime(2013, 1, 13, 7, 57, 35),
1164+
datetime(2013, 1, 13, 9, 41, 28),
1165+
datetime(2013, 1, 13, 11, 25, 21),
1166+
datetime(2013, 1, 13, 13, 9, 14),
1167+
datetime(2013, 1, 13, 14, 53, 7),
1168+
datetime(2013, 1, 13, 16, 37, 0),
1169+
datetime(2013, 1, 13, 18, 20, 52)]
1170+
1171+
with ensure_clean(self.ext) as path:
1172+
write_frame = DataFrame.from_items([('A', datetimes)])
1173+
write_frame.to_excel(path, 'Sheet1')
1174+
read_frame = read_excel(path, 'Sheet1', header=0)
1175+
1176+
tm.assert_series_equal(write_frame['A'], read_frame['A'])
11541177

11551178
def raise_wrapper(major_ver):
11561179
def versioned_raise_wrapper(orig_method):

pandas/tests/test_series.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def test_dt_namespace_accessor(self):
8484
ok_for_dt = ok_for_base + ['date','time','microsecond','nanosecond', 'is_month_start', 'is_month_end', 'is_quarter_start',
8585
'is_quarter_end', 'is_year_start', 'is_year_end', 'tz']
8686
ok_for_dt_methods = ['to_period','to_pydatetime','tz_localize','tz_convert']
87-
ok_for_td = ['days','hours','minutes','seconds','milliseconds','microseconds','nanoseconds']
87+
ok_for_td = ['days','seconds','microseconds','nanoseconds']
8888
ok_for_td_methods = ['components','to_pytimedelta']
8989

9090
def get_expected(s, name):

pandas/tseries/tdi.py

+6-21
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ def _join_i8_wrapper(joinf, **kwargs):
118118
_left_indexer_unique = _join_i8_wrapper(
119119
_algos.left_join_indexer_unique_int64, with_indexers=False)
120120
_arrmap = None
121-
_datetimelike_ops = ['days','hours','minutes','seconds','milliseconds','microseconds',
122-
'nanoseconds','freq','components']
121+
_datetimelike_ops = ['days','seconds','microseconds','nanoseconds',
122+
'freq','components']
123123

124124
__eq__ = _td_index_cmp('__eq__')
125125
__ne__ = _td_index_cmp('__ne__', nat_result=True)
@@ -349,37 +349,22 @@ def _get_field(self, m):
349349

350350
@property
351351
def days(self):
352-
""" The number of integer days for each element """
352+
""" Number of days for each element. """
353353
return self._get_field('days')
354354

355-
@property
356-
def hours(self):
357-
""" The number of integer hours for each element """
358-
return self._get_field('hours')
359-
360-
@property
361-
def minutes(self):
362-
""" The number of integer minutes for each element """
363-
return self._get_field('minutes')
364-
365355
@property
366356
def seconds(self):
367-
""" The number of integer seconds for each element """
357+
""" Number of seconds (>= 0 and less than 1 day) for each element. """
368358
return self._get_field('seconds')
369359

370-
@property
371-
def milliseconds(self):
372-
""" The number of integer milliseconds for each element """
373-
return self._get_field('milliseconds')
374-
375360
@property
376361
def microseconds(self):
377-
""" The number of integer microseconds for each element """
362+
""" Number of microseconds (>= 0 and less than 1 second) for each element. """
378363
return self._get_field('microseconds')
379364

380365
@property
381366
def nanoseconds(self):
382-
""" The number of integer nanoseconds for each element """
367+
""" Number of nanoseconds (>= 0 and less than 1 microsecond) for each element. """
383368
return self._get_field('nanoseconds')
384369

385370
@property

pandas/tseries/tests/test_timedeltas.py

+23-20
Original file line numberDiff line numberDiff line change
@@ -301,30 +301,33 @@ class Other:
301301
self.assertTrue(td.__floordiv__(td) is NotImplemented)
302302

303303
def test_fields(self):
304+
305+
# compat to datetime.timedelta
304306
rng = to_timedelta('1 days, 10:11:12')
305307
self.assertEqual(rng.days,1)
306-
self.assertEqual(rng.hours,10)
307-
self.assertEqual(rng.minutes,11)
308-
self.assertEqual(rng.seconds,12)
309-
self.assertEqual(rng.milliseconds,0)
308+
self.assertEqual(rng.seconds,10*3600+11*60+12)
310309
self.assertEqual(rng.microseconds,0)
311310
self.assertEqual(rng.nanoseconds,0)
312311

312+
self.assertRaises(AttributeError, lambda : rng.hours)
313+
self.assertRaises(AttributeError, lambda : rng.minutes)
314+
self.assertRaises(AttributeError, lambda : rng.milliseconds)
315+
313316
td = Timedelta('-1 days, 10:11:12')
314317
self.assertEqual(abs(td),Timedelta('13:48:48'))
315318
self.assertTrue(str(td) == "-1 days +10:11:12")
316319
self.assertEqual(-td,Timedelta('0 days 13:48:48'))
317320
self.assertEqual(-Timedelta('-1 days, 10:11:12').value,49728000000000)
318321
self.assertEqual(Timedelta('-1 days, 10:11:12').value,-49728000000000)
319322

320-
rng = to_timedelta('-1 days, 10:11:12')
323+
rng = to_timedelta('-1 days, 10:11:12.100123456')
321324
self.assertEqual(rng.days,-1)
322-
self.assertEqual(rng.hours,10)
323-
self.assertEqual(rng.minutes,11)
324-
self.assertEqual(rng.seconds,12)
325-
self.assertEqual(rng.milliseconds,0)
326-
self.assertEqual(rng.microseconds,0)
327-
self.assertEqual(rng.nanoseconds,0)
325+
self.assertEqual(rng.seconds,10*3600+11*60+12)
326+
self.assertEqual(rng.microseconds,100*1000+123)
327+
self.assertEqual(rng.nanoseconds,456)
328+
self.assertRaises(AttributeError, lambda : rng.hours)
329+
self.assertRaises(AttributeError, lambda : rng.minutes)
330+
self.assertRaises(AttributeError, lambda : rng.milliseconds)
328331

329332
# components
330333
tup = pd.to_timedelta(-1, 'us').components
@@ -830,22 +833,22 @@ def test_astype(self):
830833
self.assert_numpy_array_equal(result, rng.asi8)
831834

832835
def test_fields(self):
833-
rng = timedelta_range('1 days, 10:11:12', periods=2, freq='s')
836+
rng = timedelta_range('1 days, 10:11:12.100123456', periods=2, freq='s')
834837
self.assert_numpy_array_equal(rng.days, np.array([1,1],dtype='int64'))
835-
self.assert_numpy_array_equal(rng.hours, np.array([10,10],dtype='int64'))
836-
self.assert_numpy_array_equal(rng.minutes, np.array([11,11],dtype='int64'))
837-
self.assert_numpy_array_equal(rng.seconds, np.array([12,13],dtype='int64'))
838-
self.assert_numpy_array_equal(rng.milliseconds, np.array([0,0],dtype='int64'))
839-
self.assert_numpy_array_equal(rng.microseconds, np.array([0,0],dtype='int64'))
840-
self.assert_numpy_array_equal(rng.nanoseconds, np.array([0,0],dtype='int64'))
838+
self.assert_numpy_array_equal(rng.seconds, np.array([10*3600+11*60+12,10*3600+11*60+13],dtype='int64'))
839+
self.assert_numpy_array_equal(rng.microseconds, np.array([100*1000+123,100*1000+123],dtype='int64'))
840+
self.assert_numpy_array_equal(rng.nanoseconds, np.array([456,456],dtype='int64'))
841+
842+
self.assertRaises(AttributeError, lambda : rng.hours)
843+
self.assertRaises(AttributeError, lambda : rng.minutes)
844+
self.assertRaises(AttributeError, lambda : rng.milliseconds)
841845

842846
# with nat
843847
s = Series(rng)
844848
s[1] = np.nan
845849

846850
tm.assert_series_equal(s.dt.days,Series([1,np.nan],index=[0,1]))
847-
tm.assert_series_equal(s.dt.hours,Series([10,np.nan],index=[0,1]))
848-
tm.assert_series_equal(s.dt.milliseconds,Series([0,np.nan],index=[0,1]))
851+
tm.assert_series_equal(s.dt.seconds,Series([10*3600+11*60+12,np.nan],index=[0,1]))
849852

850853
def test_components(self):
851854
rng = timedelta_range('1 days, 10:11:12', periods=2, freq='s')

pandas/tslib.pyx

+21-23
Original file line numberDiff line numberDiff line change
@@ -1896,45 +1896,43 @@ class Timedelta(_Timedelta):
18961896

18971897
@property
18981898
def days(self):
1899-
""" The days for the Timedelta """
1899+
"""
1900+
Number of Days
1901+
1902+
.components will return the shown components
1903+
"""
19001904
self._ensure_components()
19011905
if self._sign < 0:
19021906
return -1*self._d
19031907
return self._d
19041908

1905-
@property
1906-
def hours(self):
1907-
""" The hours for the Timedelta """
1908-
self._ensure_components()
1909-
return self._h
1910-
1911-
@property
1912-
def minutes(self):
1913-
""" The minutes for the Timedelta """
1914-
self._ensure_components()
1915-
return self._m
1916-
19171909
@property
19181910
def seconds(self):
1919-
""" The seconds for the Timedelta """
1920-
self._ensure_components()
1921-
return self._s
1911+
"""
1912+
Number of seconds (>= 0 and less than 1 day).
19221913
1923-
@property
1924-
def milliseconds(self):
1925-
""" The milliseconds for the Timedelta """
1914+
.components will return the shown components
1915+
"""
19261916
self._ensure_components()
1927-
return self._ms
1917+
return self._h*3600 + self._m*60 + self._s
19281918

19291919
@property
19301920
def microseconds(self):
1931-
""" The microseconds for the Timedelta """
1921+
"""
1922+
Number of microseconds (>= 0 and less than 1 second).
1923+
1924+
.components will return the shown components
1925+
"""
19321926
self._ensure_components()
1933-
return self._us
1927+
return self._ms*1000 + self._us
19341928

19351929
@property
19361930
def nanoseconds(self):
1937-
""" The nanoseconds for the Timedelta """
1931+
"""
1932+
Number of nanoseconds (>= 0 and less than 1 microsecond).
1933+
1934+
.components will return the shown components
1935+
"""
19381936
self._ensure_components()
19391937
return self._ns
19401938

0 commit comments

Comments
 (0)