Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ In cases with mixed-resolution inputs, the highest resolution is used:

Similarly, the :class:`Timedelta` constructor and :func:`to_timedelta` with a string input now defaults to a microsecond unit, using nanosecond unit only in cases that actually have nanosecond precision.

Moreover, passing an integer to the :class:`Timedelta` constructor or :func:`to_timedelta` along with a ``unit`` will now return an object with that unit when possible, or the closest-supported unit for non-supported units ("W", "D", "h", "m").

.. _whatsnew_300.api_breaking.concat_datetime_sorting:

:func:`concat` no longer ignores ``sort`` when all objects have a :class:`DatetimeIndex`
Expand Down Expand Up @@ -1131,6 +1133,7 @@ Timedelta
- Accuracy improvement in :meth:`Timedelta.to_pytimedelta` to round microseconds consistently for large nanosecond based Timedelta (:issue:`57841`)
- Bug in :class:`Timedelta` constructor failing to raise when passed an invalid keyword (:issue:`53801`)
- Bug in :meth:`DataFrame.cumsum` which was raising ``IndexError`` if dtype is ``timedelta64[ns]`` (:issue:`57956`)
- Bug in adding or subtracting a :class:`Timedelta` object with non-nanosecond unit to a python ``datetime.datetime`` object giving incorrect results; this now works correctly for Timedeltas inside the ``datetime.timedelta`` implementation bounds (:issue:`53643`)
- Bug in multiplication operations with ``timedelta64`` dtype failing to raise ``TypeError`` when multiplying by ``bool`` objects or dtypes (:issue:`58054`)
- Bug in multiplication operations with ``timedelta64`` dtype incorrectly raising when multiplying by numpy-nullable dtypes or pyarrow integer dtypes (:issue:`58054`)

Expand Down
71 changes: 57 additions & 14 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ from pandas._libs.tslibs.conversion cimport (
cast_from_unit,
)
from pandas._libs.tslibs.dtypes cimport (
abbrev_to_npy_unit,
c_DEPR_UNITS,
get_supported_reso,
is_supported_unit,
Expand Down Expand Up @@ -290,22 +291,24 @@ cpdef int64_t delta_to_nanoseconds(
) from err


cdef _numeric_to_td64ns(object item, str unit):
cdef int64_t _numeric_to_td64ns(
object item, str unit, NPY_DATETIMEUNIT out_reso=NPY_FR_ns
):
# caller is responsible for checking
# assert unit not in ["Y", "y", "M"]
# assert is_integer_object(item) or is_float_object(item)
if is_integer_object(item) and item == NPY_NAT:
return np.timedelta64(NPY_NAT, "ns")
return NPY_NAT

try:
item = cast_from_unit(item, unit)
ival = cast_from_unit(item, unit, out_reso)
except OutOfBoundsDatetime as err:
abbrev = npy_unit_to_abbrev(out_reso)
raise OutOfBoundsTimedelta(
f"Cannot cast {item} from {unit} to 'ns' without overflow."
f"Cannot cast {item} from {unit} to '{abbrev}' without overflow."
) from err

ts = np.timedelta64(item, "ns")
return ts
return ival


# TODO: de-duplicate with DatetimeParseState
Expand Down Expand Up @@ -352,16 +355,21 @@ def array_to_timedelta64(
cdef:
Py_ssize_t i, n = values.size
ndarray result = np.empty((<object>values).shape, dtype="m8[ns]")
object item, td64ns_obj
object item
int64_t ival
cnp.broadcast mi = cnp.PyArray_MultiIterNew2(result, values)
cnp.flatiter it
str parsed_unit = parse_timedelta_unit(unit or "ns")
NPY_DATETIMEUNIT item_reso
NPY_DATETIMEUNIT item_reso, int_reso
ResoState state = ResoState(creso)
bint infer_reso = creso == NPY_DATETIMEUNIT.NPY_FR_GENERIC
ndarray iresult = result.view("i8")

if unit is None:
int_reso = NPY_FR_ns
else:
int_reso = get_supported_reso(abbrev_to_npy_unit(parsed_unit))

if values.descr.type_num != cnp.NPY_OBJECT:
# raise here otherwise we segfault below
raise TypeError("array_to_timedelta64 'values' must have object dtype")
Expand Down Expand Up @@ -470,9 +478,19 @@ def array_to_timedelta64(
creso = state.creso
ival = delta_to_nanoseconds(item, reso=creso)

elif is_integer_object(item) or is_float_object(item):
td64ns_obj = _numeric_to_td64ns(item, parsed_unit)
ival = cnp.get_timedelta64_value(td64ns_obj)
elif is_integer_object(item):
if item == NPY_NAT:
ival = NPY_NAT
else:
ival = _numeric_to_td64ns(item, parsed_unit, int_reso)
item_reso = int_reso

state.update_creso(item_reso)
if infer_reso:
creso = state.creso

elif is_float_object(item):
ival = _numeric_to_td64ns(item, parsed_unit, NPY_FR_ns)

item_reso = NPY_FR_ns
state.update_creso(item_reso)
Expand Down Expand Up @@ -1022,9 +1040,23 @@ cdef _timedelta_from_value_and_reso(cls, int64_t value, NPY_DATETIMEUNIT reso):
elif reso == NPY_DATETIMEUNIT.NPY_FR_us:
td_base = _Timedelta.__new__(cls, microseconds=int(value))
elif reso == NPY_DATETIMEUNIT.NPY_FR_ms:
td_base = _Timedelta.__new__(cls, milliseconds=0)
if value > -86_399_999_913_600_000 and value < 86_400_000_000_000_000:
# i.e. we are in range for pytimedelta. By passing the
# 'correct' value here we can
# make pydatetime + Timedelta operations work correctly,
# xref GH#53643
td_base = _Timedelta.__new__(cls, milliseconds=value)
else:
td_base = _Timedelta.__new__(cls, milliseconds=0)
elif reso == NPY_DATETIMEUNIT.NPY_FR_s:
td_base = _Timedelta.__new__(cls, seconds=0)
if value > -86_399_999_913_600 and value < 86_400_000_000_000:
# i.e. we are in range for pytimedelta. By passing the
# 'correct' value here we can
# make pydatetime + Timedelta operations work correctly,
# xref GH#53643
td_base = _Timedelta.__new__(cls, seconds=value)
else:
td_base = _Timedelta.__new__(cls, seconds=0)
# Other resolutions are disabled but could potentially be implemented here:
# elif reso == NPY_DATETIMEUNIT.NPY_FR_m:
# td_base = _Timedelta.__new__(Timedelta, minutes=int(value))
Expand Down Expand Up @@ -2216,7 +2248,18 @@ class Timedelta(_Timedelta):
elif checknull_with_nat_and_na(value):
return NaT

elif is_integer_object(value) or is_float_object(value):
elif is_integer_object(value):
# unit=None is de-facto 'ns'
if value != NPY_NAT:
unit = parse_timedelta_unit(unit)
if unit != "ns":
# Return with the closest-to-supported unit by going through
# the timedelta64 path
td = np.timedelta64(value, unit)
return cls(td)
value = _numeric_to_td64ns(value, unit)

elif is_float_object(value):
# unit=None is de-facto 'ns'
unit = parse_timedelta_unit(unit)
value = _numeric_to_td64ns(value, unit)
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -1656,7 +1656,7 @@ def mean(self, *, skipna: bool = True, axis: AxisInt | None = 0):
>>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
>>> tdelta_idx
TimedeltaIndex(['1 days', '2 days', '3 days'],
dtype='timedelta64[ns]', freq=None)
dtype='timedelta64[s]', freq=None)
>>> tdelta_idx.mean()
Timedelta('2 days 00:00:00')
"""
Expand Down
16 changes: 9 additions & 7 deletions pandas/core/arrays/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
>>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
>>> tdelta_idx
TimedeltaIndex(['1 days', '2 days', '3 days'],
dtype='timedelta64[ns]', freq=None)
dtype='timedelta64[s]', freq=None)
>>> tdelta_idx.to_pytimedelta()
array([datetime.timedelta(days=1), datetime.timedelta(days=2),
datetime.timedelta(days=3)], dtype=object)
Expand Down Expand Up @@ -880,7 +880,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
0 1 days
1 2 days
2 3 days
dtype: timedelta64[ns]
dtype: timedelta64[s]
>>> ser.dt.days
0 1
1 2
Expand Down Expand Up @@ -915,7 +915,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
0 0 days 00:00:01
1 0 days 00:00:02
2 0 days 00:00:03
dtype: timedelta64[ns]
dtype: timedelta64[s]
>>> ser.dt.seconds
0 1
1 2
Expand All @@ -927,7 +927,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
>>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit='s')
>>> tdelta_idx
TimedeltaIndex(['0 days 00:00:01', '0 days 00:00:02', '0 days 00:00:03'],
dtype='timedelta64[ns]', freq=None)
dtype='timedelta64[s]', freq=None)
>>> tdelta_idx.seconds
Index([1, 2, 3], dtype='int32')"""
)
Expand Down Expand Up @@ -955,7 +955,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
0 0 days 00:00:00.000001
1 0 days 00:00:00.000002
2 0 days 00:00:00.000003
dtype: timedelta64[ns]
dtype: timedelta64[us]
>>> ser.dt.microseconds
0 1
1 2
Expand All @@ -968,7 +968,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
>>> tdelta_idx
TimedeltaIndex(['0 days 00:00:00.000001', '0 days 00:00:00.000002',
'0 days 00:00:00.000003'],
dtype='timedelta64[ns]', freq=None)
dtype='timedelta64[us]', freq=None)
>>> tdelta_idx.microseconds
Index([1, 2, 3], dtype='int32')"""
)
Expand Down Expand Up @@ -1205,7 +1205,9 @@ def _ints_to_td64ns(data, unit: str = "ns") -> tuple[np.ndarray, bool]:
dtype_str = f"timedelta64[{unit}]"
data = data.view(dtype_str)

data = astype_overflowsafe(data, dtype=TD64NS_DTYPE)
new_dtype = get_supported_dtype(data.dtype)
if new_dtype != data.dtype:
data = astype_overflowsafe(data, dtype=new_dtype)

# the astype conversion makes a copy, so we can avoid re-copying later
copy_made = True
Expand Down
4 changes: 2 additions & 2 deletions pandas/core/indexes/accessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ def to_pytimedelta(self) -> np.ndarray:
2 2 days
3 3 days
4 4 days
dtype: timedelta64[ns]
dtype: timedelta64[s]

>>> s.dt.to_pytimedelta()
array([datetime.timedelta(0), datetime.timedelta(days=1),
Expand Down Expand Up @@ -535,7 +535,7 @@ def components(self) -> DataFrame:
2 0 days 00:00:02
3 0 days 00:00:03
4 0 days 00:00:04
dtype: timedelta64[ns]
dtype: timedelta64[s]
>>> s.dt.components
days hours minutes seconds milliseconds microseconds nanoseconds
0 0 0 0 0 0 0 0
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def mean(self, *, skipna: bool = True, axis: int | None = 0):
>>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
>>> tdelta_idx
TimedeltaIndex(['1 days', '2 days', '3 days'],
dtype='timedelta64[ns]', freq=None)
dtype='timedelta64[s]', freq=None)
>>> tdelta_idx.mean()
Timedelta('2 days 00:00:00')
"""
Expand Down
4 changes: 2 additions & 2 deletions pandas/core/tools/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ def to_timedelta(
>>> pd.to_timedelta(np.arange(5), unit="s")
TimedeltaIndex(['0 days 00:00:00', '0 days 00:00:01', '0 days 00:00:02',
'0 days 00:00:03', '0 days 00:00:04'],
dtype='timedelta64[ns]', freq=None)
dtype='timedelta64[s]', freq=None)
>>> pd.to_timedelta(np.arange(5), unit="D")
TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'],
dtype='timedelta64[ns]', freq=None)
dtype='timedelta64[s]', freq=None)
"""
if unit is not None:
unit = parse_timedelta_unit(unit)
Expand Down
10 changes: 3 additions & 7 deletions pandas/tests/arithmetic/test_timedelta64.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

from pandas._libs.tslibs import timezones
from pandas.compat import WASM
from pandas.errors import OutOfBoundsDatetime
import pandas.util._test_decorators as td

import pandas as pd
Expand Down Expand Up @@ -728,17 +727,14 @@ def test_tdi_add_overflow(self):
# See GH#14068
# preliminary test scalar analogue of vectorized tests below
# TODO: Make raised error message more informative and test
with pytest.raises(OutOfBoundsDatetime, match="10155196800000000000"):
pd.to_timedelta(106580, "D") + Timestamp("2000")
with pytest.raises(OutOfBoundsDatetime, match="10155196800000000000"):
Timestamp("2000") + pd.to_timedelta(106580, "D")

_NaT = NaT._value + 1
td = pd.to_timedelta([106580], "D").as_unit("ns")
msg = "Overflow in int64 addition"
with pytest.raises(OverflowError, match=msg):
pd.to_timedelta([106580], "D") + Timestamp("2000")
td + Timestamp("2000")
with pytest.raises(OverflowError, match=msg):
Timestamp("2000") + pd.to_timedelta([106580], "D")
Timestamp("2000") + td
with pytest.raises(OverflowError, match=msg):
pd.to_timedelta([_NaT]) - Timedelta("1 days")
with pytest.raises(OverflowError, match=msg):
Expand Down
7 changes: 4 additions & 3 deletions pandas/tests/frame/indexing/test_mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,15 @@ def test_mask_where_dtype_timedelta():
# https://github.com/pandas-dev/pandas/issues/39548
df = DataFrame([Timedelta(i, unit="D") for i in range(5)])

expected = DataFrame(np.full(5, np.nan, dtype="timedelta64[ns]"))
expected = DataFrame(np.full(5, np.nan, dtype="timedelta64[s]"))
tm.assert_frame_equal(df.mask(df.notna()), expected)

expected = DataFrame(
[np.nan, np.nan, np.nan, Timedelta("3 day"), Timedelta("4 day")],
dtype="m8[ns]",
dtype="m8[s]",
)
tm.assert_frame_equal(df.where(df > Timedelta(2, unit="D")), expected)
result = df.where(df > Timedelta(2, unit="D"))
tm.assert_frame_equal(result, expected)


def test_mask_return_dtype():
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/frame/indexing/test_setitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,8 +1000,8 @@ def test_loc_expansion_with_timedelta_type(self):
index=Index([0]),
columns=(["a", "b", "c"]),
)
expected["a"] = expected["a"].astype("m8[ns]")
expected["b"] = expected["b"].astype("m8[ns]")
expected["a"] = expected["a"].astype("m8[s]")
expected["b"] = expected["b"].astype("m8[s]")
tm.assert_frame_equal(result, expected)

def test_setitem_tuple_key_in_empty_frame(self):
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/frame/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ def create_data(constructor):
[
(lambda x: np.timedelta64(x, "D"), "m8[s]"),
(lambda x: timedelta(days=x), "m8[us]"),
(lambda x: Timedelta(x, "D"), "m8[ns]"),
(lambda x: Timedelta(x, "D"), "m8[s]"),
(lambda x: Timedelta(x, "D").as_unit("s"), "m8[s]"),
],
)
Expand Down
9 changes: 4 additions & 5 deletions pandas/tests/groupby/methods/test_quantile.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,15 +363,14 @@ def test_groupby_quantile_allNA_column(dtype):

def test_groupby_timedelta_quantile():
# GH: 29485
df = DataFrame(
{"value": pd.to_timedelta(np.arange(4), unit="s"), "group": [1, 1, 2, 2]}
)
tdi = pd.to_timedelta(np.arange(4), unit="s").as_unit("us")
df = DataFrame({"value": tdi, "group": [1, 1, 2, 2]})
result = df.groupby("group").quantile(0.99)
expected = DataFrame(
{
"value": [
pd.Timedelta("0 days 00:00:00.990000").as_unit("ns"),
pd.Timedelta("0 days 00:00:02.990000").as_unit("ns"),
pd.Timedelta("0 days 00:00:00.990000"),
pd.Timedelta("0 days 00:00:02.990000"),
]
},
index=Index([1, 2], name="group"),
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/indexes/timedeltas/methods/test_shift.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_tdi_shift_int(self):
"5 days 01:00:00",
],
freq="D",
dtype="m8[ns]",
dtype="m8[s]",
)
tm.assert_index_equal(result, expected)

Expand All @@ -67,7 +67,7 @@ def test_tdi_shift_nonstandard_freq(self):
"10 days 01:00:03",
],
freq="D",
dtype="m8[ns]",
dtype="m8[s]",
)
tm.assert_index_equal(result, expected)

Expand Down
3 changes: 2 additions & 1 deletion pandas/tests/indexes/timedeltas/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,5 @@ def test_unit_deprecated(self, unit, unit_depr):

with tm.assert_produces_warning(Pandas4Warning, match=msg):
tdi = to_timedelta([1, 2], unit=unit_depr)
tm.assert_index_equal(tdi, expected.as_unit("ns"))
exp_unit = unit if unit in ["s", "ms", "us"] else "s"
tm.assert_index_equal(tdi, expected.as_unit(exp_unit))
Loading
Loading