1from __future__ import annotations
2
3from datetime import timedelta
4import operator
5from typing import (
6 TYPE_CHECKING,
7 Iterator,
8 cast,
9)
10import warnings
11
12import numpy as np
13
14from pandas._libs import (
15 lib,
16 tslibs,
17)
18from pandas._libs.tslibs import (
19 BaseOffset,
20 NaT,
21 NaTType,
22 Tick,
23 Timedelta,
24 astype_overflowsafe,
25 get_supported_reso,
26 get_unit_from_dtype,
27 iNaT,
28 is_supported_unit,
29 npy_unit_to_abbrev,
30 periods_per_second,
31 to_offset,
32)
33from pandas._libs.tslibs.conversion import precision_from_unit
34from pandas._libs.tslibs.fields import (
35 get_timedelta_days,
36 get_timedelta_field,
37)
38from pandas._libs.tslibs.timedeltas import (
39 array_to_timedelta64,
40 floordiv_object_array,
41 ints_to_pytimedelta,
42 parse_timedelta_unit,
43 truediv_object_array,
44)
45from pandas._typing import (
46 AxisInt,
47 DateTimeErrorChoices,
48 DtypeObj,
49 NpDtype,
50 npt,
51)
52from pandas.compat.numpy import function as nv
53from pandas.util._validators import validate_endpoints
54
55from pandas.core.dtypes.common import (
56 TD64NS_DTYPE,
57 is_dtype_equal,
58 is_extension_array_dtype,
59 is_float_dtype,
60 is_integer_dtype,
61 is_object_dtype,
62 is_scalar,
63 is_string_dtype,
64 is_timedelta64_dtype,
65 pandas_dtype,
66)
67from pandas.core.dtypes.missing import isna
68
69from pandas.core import nanops
70from pandas.core.array_algos import datetimelike_accumulations
71from pandas.core.arrays import datetimelike as dtl
72from pandas.core.arrays._ranges import generate_regular_range
73import pandas.core.common as com
74from pandas.core.ops import roperator
75from pandas.core.ops.common import unpack_zerodim_and_defer
76
77if TYPE_CHECKING:
78 from pandas import DataFrame
79
80
81def _field_accessor(name: str, alias: str, docstring: str):
82 def f(self) -> np.ndarray:
83 values = self.asi8
84 if alias == "days":
85 result = get_timedelta_days(values, reso=self._creso)
86 else:
87 # error: Incompatible types in assignment (
88 # expression has type "ndarray[Any, dtype[signedinteger[_32Bit]]]",
89 # variable has type "ndarray[Any, dtype[signedinteger[_64Bit]]]
90 result = get_timedelta_field(values, alias, reso=self._creso) # type: ignore[assignment] # noqa: E501
91 if self._hasna:
92 result = self._maybe_mask_results(
93 result, fill_value=None, convert="float64"
94 )
95
96 return result
97
98 f.__name__ = name
99 f.__doc__ = f"\n{docstring}\n"
100 return property(f)
101
102
103class TimedeltaArray(dtl.TimelikeOps):
104 """
105 Pandas ExtensionArray for timedelta data.
106
107 .. warning::
108
109 TimedeltaArray is currently experimental, and its API may change
110 without warning. In particular, :attr:`TimedeltaArray.dtype` is
111 expected to change to be an instance of an ``ExtensionDtype``
112 subclass.
113
114 Parameters
115 ----------
116 values : array-like
117 The timedelta data.
118
119 dtype : numpy.dtype
120 Currently, only ``numpy.dtype("timedelta64[ns]")`` is accepted.
121 freq : Offset, optional
122 copy : bool, default False
123 Whether to copy the underlying array of data.
124
125 Attributes
126 ----------
127 None
128
129 Methods
130 -------
131 None
132 """
133
134 _typ = "timedeltaarray"
135 _internal_fill_value = np.timedelta64("NaT", "ns")
136 _recognized_scalars = (timedelta, np.timedelta64, Tick)
137 _is_recognized_dtype = is_timedelta64_dtype
138 _infer_matches = ("timedelta", "timedelta64")
139
140 @property
141 def _scalar_type(self) -> type[Timedelta]:
142 return Timedelta
143
144 __array_priority__ = 1000
145 # define my properties & methods for delegation
146 _other_ops: list[str] = []
147 _bool_ops: list[str] = []
148 _object_ops: list[str] = ["freq"]
149 _field_ops: list[str] = ["days", "seconds", "microseconds", "nanoseconds"]
150 _datetimelike_ops: list[str] = _field_ops + _object_ops + _bool_ops + ["unit"]
151 _datetimelike_methods: list[str] = [
152 "to_pytimedelta",
153 "total_seconds",
154 "round",
155 "floor",
156 "ceil",
157 "as_unit",
158 ]
159
160 # Note: ndim must be defined to ensure NaT.__richcmp__(TimedeltaArray)
161 # operates pointwise.
162
163 def _box_func(self, x: np.timedelta64) -> Timedelta | NaTType:
164 y = x.view("i8")
165 if y == NaT._value:
166 return NaT
167 return Timedelta._from_value_and_reso(y, reso=self._creso)
168
169 @property
170 # error: Return type "dtype" of "dtype" incompatible with return type
171 # "ExtensionDtype" in supertype "ExtensionArray"
172 def dtype(self) -> np.dtype: # type: ignore[override]
173 """
174 The dtype for the TimedeltaArray.
175
176 .. warning::
177
178 A future version of pandas will change dtype to be an instance
179 of a :class:`pandas.api.extensions.ExtensionDtype` subclass,
180 not a ``numpy.dtype``.
181
182 Returns
183 -------
184 numpy.dtype
185 """
186 return self._ndarray.dtype
187
188 # ----------------------------------------------------------------
189 # Constructors
190
191 _freq = None
192 _default_dtype = TD64NS_DTYPE # used in TimeLikeOps.__init__
193
194 @classmethod
195 def _validate_dtype(cls, values, dtype):
196 # used in TimeLikeOps.__init__
197 _validate_td64_dtype(values.dtype)
198 dtype = _validate_td64_dtype(dtype)
199 return dtype
200
201 # error: Signature of "_simple_new" incompatible with supertype "NDArrayBacked"
202 @classmethod
203 def _simple_new( # type: ignore[override]
204 cls, values: np.ndarray, freq: BaseOffset | None = None, dtype=TD64NS_DTYPE
205 ) -> TimedeltaArray:
206 # Require td64 dtype, not unit-less, matching values.dtype
207 assert isinstance(dtype, np.dtype) and dtype.kind == "m"
208 assert not tslibs.is_unitless(dtype)
209 assert isinstance(values, np.ndarray), type(values)
210 assert dtype == values.dtype
211
212 result = super()._simple_new(values=values, dtype=dtype)
213 result._freq = freq
214 return result
215
216 @classmethod
217 def _from_sequence(cls, data, *, dtype=None, copy: bool = False) -> TimedeltaArray:
218 if dtype:
219 dtype = _validate_td64_dtype(dtype)
220
221 data, inferred_freq = sequence_to_td64ns(data, copy=copy, unit=None)
222 freq, _ = dtl.validate_inferred_freq(None, inferred_freq, False)
223
224 if dtype is not None:
225 data = astype_overflowsafe(data, dtype=dtype, copy=False)
226
227 return cls._simple_new(data, dtype=data.dtype, freq=freq)
228
229 @classmethod
230 def _from_sequence_not_strict(
231 cls,
232 data,
233 *,
234 dtype=None,
235 copy: bool = False,
236 freq=lib.no_default,
237 unit=None,
238 ) -> TimedeltaArray:
239 """
240 A non-strict version of _from_sequence, called from TimedeltaIndex.__new__.
241 """
242 if dtype:
243 dtype = _validate_td64_dtype(dtype)
244
245 assert unit not in ["Y", "y", "M"] # caller is responsible for checking
246
247 explicit_none = freq is None
248 freq = freq if freq is not lib.no_default else None
249
250 freq, freq_infer = dtl.maybe_infer_freq(freq)
251
252 data, inferred_freq = sequence_to_td64ns(data, copy=copy, unit=unit)
253 freq, freq_infer = dtl.validate_inferred_freq(freq, inferred_freq, freq_infer)
254 if explicit_none:
255 freq = None
256
257 if dtype is not None:
258 data = astype_overflowsafe(data, dtype=dtype, copy=False)
259
260 result = cls._simple_new(data, dtype=data.dtype, freq=freq)
261
262 if inferred_freq is None and freq is not None:
263 # this condition precludes `freq_infer`
264 cls._validate_frequency(result, freq)
265
266 elif freq_infer:
267 # Set _freq directly to bypass duplicative _validate_frequency
268 # check.
269 result._freq = to_offset(result.inferred_freq)
270
271 return result
272
273 # Signature of "_generate_range" incompatible with supertype
274 # "DatetimeLikeArrayMixin"
275 @classmethod
276 def _generate_range( # type: ignore[override]
277 cls, start, end, periods, freq, closed=None, *, unit: str | None = None
278 ):
279 periods = dtl.validate_periods(periods)
280 if freq is None and any(x is None for x in [periods, start, end]):
281 raise ValueError("Must provide freq argument if no data is supplied")
282
283 if com.count_not_none(start, end, periods, freq) != 3:
284 raise ValueError(
285 "Of the four parameters: start, end, periods, "
286 "and freq, exactly three must be specified"
287 )
288
289 if start is not None:
290 start = Timedelta(start).as_unit("ns")
291
292 if end is not None:
293 end = Timedelta(end).as_unit("ns")
294
295 if unit is not None:
296 if unit not in ["s", "ms", "us", "ns"]:
297 raise ValueError("'unit' must be one of 's', 'ms', 'us', 'ns'")
298 else:
299 unit = "ns"
300
301 if start is not None and unit is not None:
302 start = start.as_unit(unit, round_ok=False)
303 if end is not None and unit is not None:
304 end = end.as_unit(unit, round_ok=False)
305
306 left_closed, right_closed = validate_endpoints(closed)
307
308 if freq is not None:
309 index = generate_regular_range(start, end, periods, freq, unit=unit)
310 else:
311 index = np.linspace(start._value, end._value, periods).astype("i8")
312
313 if not left_closed:
314 index = index[1:]
315 if not right_closed:
316 index = index[:-1]
317
318 td64values = index.view(f"m8[{unit}]")
319 return cls._simple_new(td64values, dtype=td64values.dtype, freq=freq)
320
321 # ----------------------------------------------------------------
322 # DatetimeLike Interface
323
324 def _unbox_scalar(self, value) -> np.timedelta64:
325 if not isinstance(value, self._scalar_type) and value is not NaT:
326 raise ValueError("'value' should be a Timedelta.")
327 self._check_compatible_with(value)
328 if value is NaT:
329 return np.timedelta64(value._value, self.unit)
330 else:
331 return value.as_unit(self.unit).asm8
332
333 def _scalar_from_string(self, value) -> Timedelta | NaTType:
334 return Timedelta(value)
335
336 def _check_compatible_with(self, other) -> None:
337 # we don't have anything to validate.
338 pass
339
340 # ----------------------------------------------------------------
341 # Array-Like / EA-Interface Methods
342
343 def astype(self, dtype, copy: bool = True):
344 # We handle
345 # --> timedelta64[ns]
346 # --> timedelta64
347 # DatetimeLikeArrayMixin super call handles other cases
348 dtype = pandas_dtype(dtype)
349
350 if isinstance(dtype, np.dtype) and dtype.kind == "m":
351 if dtype == self.dtype:
352 if copy:
353 return self.copy()
354 return self
355
356 if is_supported_unit(get_unit_from_dtype(dtype)):
357 # unit conversion e.g. timedelta64[s]
358 res_values = astype_overflowsafe(self._ndarray, dtype, copy=False)
359 return type(self)._simple_new(
360 res_values, dtype=res_values.dtype, freq=self.freq
361 )
362 else:
363 raise ValueError(
364 f"Cannot convert from {self.dtype} to {dtype}. "
365 "Supported resolutions are 's', 'ms', 'us', 'ns'"
366 )
367
368 return dtl.DatetimeLikeArrayMixin.astype(self, dtype, copy=copy)
369
370 def __iter__(self) -> Iterator:
371 if self.ndim > 1:
372 for i in range(len(self)):
373 yield self[i]
374 else:
375 # convert in chunks of 10k for efficiency
376 data = self._ndarray
377 length = len(self)
378 chunksize = 10000
379 chunks = (length // chunksize) + 1
380 for i in range(chunks):
381 start_i = i * chunksize
382 end_i = min((i + 1) * chunksize, length)
383 converted = ints_to_pytimedelta(data[start_i:end_i], box=True)
384 yield from converted
385
386 # ----------------------------------------------------------------
387 # Reductions
388
389 def sum(
390 self,
391 *,
392 axis: AxisInt | None = None,
393 dtype: NpDtype | None = None,
394 out=None,
395 keepdims: bool = False,
396 initial=None,
397 skipna: bool = True,
398 min_count: int = 0,
399 ):
400 nv.validate_sum(
401 (), {"dtype": dtype, "out": out, "keepdims": keepdims, "initial": initial}
402 )
403
404 result = nanops.nansum(
405 self._ndarray, axis=axis, skipna=skipna, min_count=min_count
406 )
407 return self._wrap_reduction_result(axis, result)
408
409 def std(
410 self,
411 *,
412 axis: AxisInt | None = None,
413 dtype: NpDtype | None = None,
414 out=None,
415 ddof: int = 1,
416 keepdims: bool = False,
417 skipna: bool = True,
418 ):
419 nv.validate_stat_ddof_func(
420 (), {"dtype": dtype, "out": out, "keepdims": keepdims}, fname="std"
421 )
422
423 result = nanops.nanstd(self._ndarray, axis=axis, skipna=skipna, ddof=ddof)
424 if axis is None or self.ndim == 1:
425 return self._box_func(result)
426 return self._from_backing_data(result)
427
428 # ----------------------------------------------------------------
429 # Accumulations
430
431 def _accumulate(self, name: str, *, skipna: bool = True, **kwargs):
432 if name == "cumsum":
433 op = getattr(datetimelike_accumulations, name)
434 result = op(self._ndarray.copy(), skipna=skipna, **kwargs)
435
436 return type(self)._simple_new(result, freq=None, dtype=self.dtype)
437 elif name == "cumprod":
438 raise TypeError("cumprod not supported for Timedelta.")
439
440 else:
441 return super()._accumulate(name, skipna=skipna, **kwargs)
442
443 # ----------------------------------------------------------------
444 # Rendering Methods
445
446 def _formatter(self, boxed: bool = False):
447 from pandas.io.formats.format import get_format_timedelta64
448
449 return get_format_timedelta64(self, box=True)
450
451 def _format_native_types(
452 self, *, na_rep: str | float = "NaT", date_format=None, **kwargs
453 ) -> npt.NDArray[np.object_]:
454 from pandas.io.formats.format import get_format_timedelta64
455
456 # Relies on TimeDelta._repr_base
457 formatter = get_format_timedelta64(self._ndarray, na_rep)
458 # equiv: np.array([formatter(x) for x in self._ndarray])
459 # but independent of dimension
460 return np.frompyfunc(formatter, 1, 1)(self._ndarray)
461
462 # ----------------------------------------------------------------
463 # Arithmetic Methods
464
465 def _add_offset(self, other):
466 assert not isinstance(other, Tick)
467 raise TypeError(
468 f"cannot add the type {type(other).__name__} to a {type(self).__name__}"
469 )
470
471 @unpack_zerodim_and_defer("__mul__")
472 def __mul__(self, other) -> TimedeltaArray:
473 if is_scalar(other):
474 # numpy will accept float and int, raise TypeError for others
475 result = self._ndarray * other
476 freq = None
477 if self.freq is not None and not isna(other):
478 freq = self.freq * other
479 return type(self)._simple_new(result, dtype=result.dtype, freq=freq)
480
481 if not hasattr(other, "dtype"):
482 # list, tuple
483 other = np.array(other)
484 if len(other) != len(self) and not is_timedelta64_dtype(other.dtype):
485 # Exclude timedelta64 here so we correctly raise TypeError
486 # for that instead of ValueError
487 raise ValueError("Cannot multiply with unequal lengths")
488
489 if is_object_dtype(other.dtype):
490 # this multiplication will succeed only if all elements of other
491 # are int or float scalars, so we will end up with
492 # timedelta64[ns]-dtyped result
493 arr = self._ndarray
494 result = [arr[n] * other[n] for n in range(len(self))]
495 result = np.array(result)
496 return type(self)._simple_new(result, dtype=result.dtype)
497
498 # numpy will accept float or int dtype, raise TypeError for others
499 result = self._ndarray * other
500 return type(self)._simple_new(result, dtype=result.dtype)
501
502 __rmul__ = __mul__
503
504 def _scalar_divlike_op(self, other, op):
505 """
506 Shared logic for __truediv__, __rtruediv__, __floordiv__, __rfloordiv__
507 with scalar 'other'.
508 """
509 if isinstance(other, self._recognized_scalars):
510 other = Timedelta(other)
511 # mypy assumes that __new__ returns an instance of the class
512 # github.com/python/mypy/issues/1020
513 if cast("Timedelta | NaTType", other) is NaT:
514 # specifically timedelta64-NaT
515 result = np.empty(self.shape, dtype=np.float64)
516 result.fill(np.nan)
517 return result
518
519 # otherwise, dispatch to Timedelta implementation
520 return op(self._ndarray, other)
521
522 else:
523 # caller is responsible for checking lib.is_scalar(other)
524 # assume other is numeric, otherwise numpy will raise
525
526 if op in [roperator.rtruediv, roperator.rfloordiv]:
527 raise TypeError(
528 f"Cannot divide {type(other).__name__} by {type(self).__name__}"
529 )
530
531 result = op(self._ndarray, other)
532 freq = None
533
534 if self.freq is not None:
535 # Note: freq gets division, not floor-division, even if op
536 # is floordiv.
537 freq = self.freq / other
538
539 # TODO: 2022-12-24 test_ufunc_coercions, test_tdi_ops_attributes
540 # get here for truediv, no tests for floordiv
541
542 if op is operator.floordiv:
543 if freq.nanos == 0 and self.freq.nanos != 0:
544 # e.g. if self.freq is Nano(1) then dividing by 2
545 # rounds down to zero
546 # TODO: 2022-12-24 should implement the same check
547 # for truediv case
548 freq = None
549
550 return type(self)._simple_new(result, dtype=result.dtype, freq=freq)
551
552 def _cast_divlike_op(self, other):
553 if not hasattr(other, "dtype"):
554 # e.g. list, tuple
555 other = np.array(other)
556
557 if len(other) != len(self):
558 raise ValueError("Cannot divide vectors with unequal lengths")
559 return other
560
561 def _vector_divlike_op(self, other, op) -> np.ndarray | TimedeltaArray:
562 """
563 Shared logic for __truediv__, __floordiv__, and their reversed versions
564 with timedelta64-dtype ndarray other.
565 """
566 # Let numpy handle it
567 result = op(self._ndarray, np.asarray(other))
568
569 if (is_integer_dtype(other.dtype) or is_float_dtype(other.dtype)) and op in [
570 operator.truediv,
571 operator.floordiv,
572 ]:
573 return type(self)._simple_new(result, dtype=result.dtype)
574
575 if op in [operator.floordiv, roperator.rfloordiv]:
576 mask = self.isna() | isna(other)
577 if mask.any():
578 result = result.astype(np.float64)
579 np.putmask(result, mask, np.nan)
580
581 return result
582
583 @unpack_zerodim_and_defer("__truediv__")
584 def __truediv__(self, other):
585 # timedelta / X is well-defined for timedelta-like or numeric X
586 op = operator.truediv
587 if is_scalar(other):
588 return self._scalar_divlike_op(other, op)
589
590 other = self._cast_divlike_op(other)
591 if (
592 is_timedelta64_dtype(other.dtype)
593 or is_integer_dtype(other.dtype)
594 or is_float_dtype(other.dtype)
595 ):
596 return self._vector_divlike_op(other, op)
597
598 if is_object_dtype(other.dtype):
599 other = np.asarray(other)
600 if self.ndim > 1:
601 res_cols = [left / right for left, right in zip(self, other)]
602 res_cols2 = [x.reshape(1, -1) for x in res_cols]
603 result = np.concatenate(res_cols2, axis=0)
604 else:
605 result = truediv_object_array(self._ndarray, other)
606
607 return result
608
609 else:
610 return NotImplemented
611
612 @unpack_zerodim_and_defer("__rtruediv__")
613 def __rtruediv__(self, other):
614 # X / timedelta is defined only for timedelta-like X
615 op = roperator.rtruediv
616 if is_scalar(other):
617 return self._scalar_divlike_op(other, op)
618
619 other = self._cast_divlike_op(other)
620 if is_timedelta64_dtype(other.dtype):
621 return self._vector_divlike_op(other, op)
622
623 elif is_object_dtype(other.dtype):
624 # Note: unlike in __truediv__, we do not _need_ to do type
625 # inference on the result. It does not raise, a numeric array
626 # is returned. GH#23829
627 result_list = [other[n] / self[n] for n in range(len(self))]
628 return np.array(result_list)
629
630 else:
631 return NotImplemented
632
633 @unpack_zerodim_and_defer("__floordiv__")
634 def __floordiv__(self, other):
635 op = operator.floordiv
636 if is_scalar(other):
637 return self._scalar_divlike_op(other, op)
638
639 other = self._cast_divlike_op(other)
640 if (
641 is_timedelta64_dtype(other.dtype)
642 or is_integer_dtype(other.dtype)
643 or is_float_dtype(other.dtype)
644 ):
645 return self._vector_divlike_op(other, op)
646
647 elif is_object_dtype(other.dtype):
648 other = np.asarray(other)
649 if self.ndim > 1:
650 res_cols = [left // right for left, right in zip(self, other)]
651 res_cols2 = [x.reshape(1, -1) for x in res_cols]
652 result = np.concatenate(res_cols2, axis=0)
653 else:
654 result = floordiv_object_array(self._ndarray, other)
655
656 assert result.dtype == object
657 return result
658
659 else:
660 return NotImplemented
661
662 @unpack_zerodim_and_defer("__rfloordiv__")
663 def __rfloordiv__(self, other):
664 op = roperator.rfloordiv
665 if is_scalar(other):
666 return self._scalar_divlike_op(other, op)
667
668 other = self._cast_divlike_op(other)
669 if is_timedelta64_dtype(other.dtype):
670 return self._vector_divlike_op(other, op)
671
672 elif is_object_dtype(other.dtype):
673 result_list = [other[n] // self[n] for n in range(len(self))]
674 result = np.array(result_list)
675 return result
676
677 else:
678 return NotImplemented
679
680 @unpack_zerodim_and_defer("__mod__")
681 def __mod__(self, other):
682 # Note: This is a naive implementation, can likely be optimized
683 if isinstance(other, self._recognized_scalars):
684 other = Timedelta(other)
685 return self - (self // other) * other
686
687 @unpack_zerodim_and_defer("__rmod__")
688 def __rmod__(self, other):
689 # Note: This is a naive implementation, can likely be optimized
690 if isinstance(other, self._recognized_scalars):
691 other = Timedelta(other)
692 return other - (other // self) * self
693
694 @unpack_zerodim_and_defer("__divmod__")
695 def __divmod__(self, other):
696 # Note: This is a naive implementation, can likely be optimized
697 if isinstance(other, self._recognized_scalars):
698 other = Timedelta(other)
699
700 res1 = self // other
701 res2 = self - res1 * other
702 return res1, res2
703
704 @unpack_zerodim_and_defer("__rdivmod__")
705 def __rdivmod__(self, other):
706 # Note: This is a naive implementation, can likely be optimized
707 if isinstance(other, self._recognized_scalars):
708 other = Timedelta(other)
709
710 res1 = other // self
711 res2 = other - res1 * self
712 return res1, res2
713
714 def __neg__(self) -> TimedeltaArray:
715 freq = None
716 if self.freq is not None:
717 freq = -self.freq
718 return type(self)._simple_new(-self._ndarray, dtype=self.dtype, freq=freq)
719
720 def __pos__(self) -> TimedeltaArray:
721 return type(self)(self._ndarray.copy(), freq=self.freq)
722
723 def __abs__(self) -> TimedeltaArray:
724 # Note: freq is not preserved
725 return type(self)(np.abs(self._ndarray))
726
727 # ----------------------------------------------------------------
728 # Conversion Methods - Vectorized analogues of Timedelta methods
729
730 def total_seconds(self) -> npt.NDArray[np.float64]:
731 """
732 Return total duration of each element expressed in seconds.
733
734 This method is available directly on TimedeltaArray, TimedeltaIndex
735 and on Series containing timedelta values under the ``.dt`` namespace.
736
737 Returns
738 -------
739 ndarray, Index or Series
740 When the calling object is a TimedeltaArray, the return type
741 is ndarray. When the calling object is a TimedeltaIndex,
742 the return type is an Index with a float64 dtype. When the calling object
743 is a Series, the return type is Series of type `float64` whose
744 index is the same as the original.
745
746 See Also
747 --------
748 datetime.timedelta.total_seconds : Standard library version
749 of this method.
750 TimedeltaIndex.components : Return a DataFrame with components of
751 each Timedelta.
752
753 Examples
754 --------
755 **Series**
756
757 >>> s = pd.Series(pd.to_timedelta(np.arange(5), unit='d'))
758 >>> s
759 0 0 days
760 1 1 days
761 2 2 days
762 3 3 days
763 4 4 days
764 dtype: timedelta64[ns]
765
766 >>> s.dt.total_seconds()
767 0 0.0
768 1 86400.0
769 2 172800.0
770 3 259200.0
771 4 345600.0
772 dtype: float64
773
774 **TimedeltaIndex**
775
776 >>> idx = pd.to_timedelta(np.arange(5), unit='d')
777 >>> idx
778 TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'],
779 dtype='timedelta64[ns]', freq=None)
780
781 >>> idx.total_seconds()
782 Index([0.0, 86400.0, 172800.0, 259200.0, 345600.0], dtype='float64')
783 """
784 pps = periods_per_second(self._creso)
785 return self._maybe_mask_results(self.asi8 / pps, fill_value=None)
786
787 def to_pytimedelta(self) -> npt.NDArray[np.object_]:
788 """
789 Return an ndarray of datetime.timedelta objects.
790
791 Returns
792 -------
793 numpy.ndarray
794 """
795 return ints_to_pytimedelta(self._ndarray)
796
797 days = _field_accessor("days", "days", "Number of days for each element.")
798 seconds = _field_accessor(
799 "seconds",
800 "seconds",
801 "Number of seconds (>= 0 and less than 1 day) for each element.",
802 )
803 microseconds = _field_accessor(
804 "microseconds",
805 "microseconds",
806 "Number of microseconds (>= 0 and less than 1 second) for each element.",
807 )
808 nanoseconds = _field_accessor(
809 "nanoseconds",
810 "nanoseconds",
811 "Number of nanoseconds (>= 0 and less than 1 microsecond) for each element.",
812 )
813
814 @property
815 def components(self) -> DataFrame:
816 """
817 Return a DataFrame of the individual resolution components of the Timedeltas.
818
819 The components (days, hours, minutes seconds, milliseconds, microseconds,
820 nanoseconds) are returned as columns in a DataFrame.
821
822 Returns
823 -------
824 DataFrame
825 """
826 from pandas import DataFrame
827
828 columns = [
829 "days",
830 "hours",
831 "minutes",
832 "seconds",
833 "milliseconds",
834 "microseconds",
835 "nanoseconds",
836 ]
837 hasnans = self._hasna
838 if hasnans:
839
840 def f(x):
841 if isna(x):
842 return [np.nan] * len(columns)
843 return x.components
844
845 else:
846
847 def f(x):
848 return x.components
849
850 result = DataFrame([f(x) for x in self], columns=columns)
851 if not hasnans:
852 result = result.astype("int64")
853 return result
854
855
856# ---------------------------------------------------------------------
857# Constructor Helpers
858
859
860def sequence_to_td64ns(
861 data,
862 copy: bool = False,
863 unit=None,
864 errors: DateTimeErrorChoices = "raise",
865) -> tuple[np.ndarray, Tick | None]:
866 """
867 Parameters
868 ----------
869 data : list-like
870 copy : bool, default False
871 unit : str, optional
872 The timedelta unit to treat integers as multiples of. For numeric
873 data this defaults to ``'ns'``.
874 Must be un-specified if the data contains a str and ``errors=="raise"``.
875 errors : {"raise", "coerce", "ignore"}, default "raise"
876 How to handle elements that cannot be converted to timedelta64[ns].
877 See ``pandas.to_timedelta`` for details.
878
879 Returns
880 -------
881 converted : numpy.ndarray
882 The sequence converted to a numpy array with dtype ``timedelta64[ns]``.
883 inferred_freq : Tick or None
884 The inferred frequency of the sequence.
885
886 Raises
887 ------
888 ValueError : Data cannot be converted to timedelta64[ns].
889
890 Notes
891 -----
892 Unlike `pandas.to_timedelta`, if setting ``errors=ignore`` will not cause
893 errors to be ignored; they are caught and subsequently ignored at a
894 higher level.
895 """
896 assert unit not in ["Y", "y", "M"] # caller is responsible for checking
897
898 inferred_freq = None
899 if unit is not None:
900 unit = parse_timedelta_unit(unit)
901
902 data, copy = dtl.ensure_arraylike_for_datetimelike(
903 data, copy, cls_name="TimedeltaArray"
904 )
905
906 if isinstance(data, TimedeltaArray):
907 inferred_freq = data.freq
908
909 # Convert whatever we have into timedelta64[ns] dtype
910 if is_object_dtype(data.dtype) or is_string_dtype(data.dtype):
911 # no need to make a copy, need to convert if string-dtyped
912 data = _objects_to_td64ns(data, unit=unit, errors=errors)
913 copy = False
914
915 elif is_integer_dtype(data.dtype):
916 # treat as multiples of the given unit
917 data, copy_made = _ints_to_td64ns(data, unit=unit)
918 copy = copy and not copy_made
919
920 elif is_float_dtype(data.dtype):
921 # cast the unit, multiply base/frac separately
922 # to avoid precision issues from float -> int
923 if is_extension_array_dtype(data):
924 mask = data._mask
925 data = data._data
926 else:
927 mask = np.isnan(data)
928 # The next few lines are effectively a vectorized 'cast_from_unit'
929 m, p = precision_from_unit(unit or "ns")
930 with warnings.catch_warnings():
931 # Suppress RuntimeWarning about All-NaN slice
932 warnings.filterwarnings(
933 "ignore", "invalid value encountered in cast", RuntimeWarning
934 )
935 base = data.astype(np.int64)
936 frac = data - base
937 if p:
938 frac = np.round(frac, p)
939 with warnings.catch_warnings():
940 warnings.filterwarnings(
941 "ignore", "invalid value encountered in cast", RuntimeWarning
942 )
943 data = (base * m + (frac * m).astype(np.int64)).view("timedelta64[ns]")
944 data[mask] = iNaT
945 copy = False
946
947 elif is_timedelta64_dtype(data.dtype):
948 data_unit = get_unit_from_dtype(data.dtype)
949 if not is_supported_unit(data_unit):
950 # cast to closest supported unit, i.e. s or ns
951 new_reso = get_supported_reso(data_unit)
952 new_unit = npy_unit_to_abbrev(new_reso)
953 new_dtype = np.dtype(f"m8[{new_unit}]")
954 data = astype_overflowsafe(data, dtype=new_dtype, copy=False)
955 copy = False
956
957 else:
958 # This includes datetime64-dtype, see GH#23539, GH#29794
959 raise TypeError(f"dtype {data.dtype} cannot be converted to timedelta64[ns]")
960
961 data = np.array(data, copy=copy)
962
963 assert data.dtype.kind == "m"
964 assert data.dtype != "m8" # i.e. not unit-less
965
966 return data, inferred_freq
967
968
969def _ints_to_td64ns(data, unit: str = "ns"):
970 """
971 Convert an ndarray with integer-dtype to timedelta64[ns] dtype, treating
972 the integers as multiples of the given timedelta unit.
973
974 Parameters
975 ----------
976 data : numpy.ndarray with integer-dtype
977 unit : str, default "ns"
978 The timedelta unit to treat integers as multiples of.
979
980 Returns
981 -------
982 numpy.ndarray : timedelta64[ns] array converted from data
983 bool : whether a copy was made
984 """
985 copy_made = False
986 unit = unit if unit is not None else "ns"
987
988 if data.dtype != np.int64:
989 # converting to int64 makes a copy, so we can avoid
990 # re-copying later
991 data = data.astype(np.int64)
992 copy_made = True
993
994 if unit != "ns":
995 dtype_str = f"timedelta64[{unit}]"
996 data = data.view(dtype_str)
997
998 data = astype_overflowsafe(data, dtype=TD64NS_DTYPE)
999
1000 # the astype conversion makes a copy, so we can avoid re-copying later
1001 copy_made = True
1002
1003 else:
1004 data = data.view("timedelta64[ns]")
1005
1006 return data, copy_made
1007
1008
1009def _objects_to_td64ns(data, unit=None, errors: DateTimeErrorChoices = "raise"):
1010 """
1011 Convert a object-dtyped or string-dtyped array into an
1012 timedelta64[ns]-dtyped array.
1013
1014 Parameters
1015 ----------
1016 data : ndarray or Index
1017 unit : str, default "ns"
1018 The timedelta unit to treat integers as multiples of.
1019 Must not be specified if the data contains a str.
1020 errors : {"raise", "coerce", "ignore"}, default "raise"
1021 How to handle elements that cannot be converted to timedelta64[ns].
1022 See ``pandas.to_timedelta`` for details.
1023
1024 Returns
1025 -------
1026 numpy.ndarray : timedelta64[ns] array converted from data
1027
1028 Raises
1029 ------
1030 ValueError : Data cannot be converted to timedelta64[ns].
1031
1032 Notes
1033 -----
1034 Unlike `pandas.to_timedelta`, if setting `errors=ignore` will not cause
1035 errors to be ignored; they are caught and subsequently ignored at a
1036 higher level.
1037 """
1038 # coerce Index to np.ndarray, converting string-dtype if necessary
1039 values = np.array(data, dtype=np.object_, copy=False)
1040
1041 result = array_to_timedelta64(values, unit=unit, errors=errors)
1042 return result.view("timedelta64[ns]")
1043
1044
1045def _validate_td64_dtype(dtype) -> DtypeObj:
1046 dtype = pandas_dtype(dtype)
1047 if is_dtype_equal(dtype, np.dtype("timedelta64")):
1048 # no precision disallowed GH#24806
1049 msg = (
1050 "Passing in 'timedelta' dtype with no precision is not allowed. "
1051 "Please pass in 'timedelta64[ns]' instead."
1052 )
1053 raise ValueError(msg)
1054
1055 if (
1056 not isinstance(dtype, np.dtype)
1057 or dtype.kind != "m"
1058 or not is_supported_unit(get_unit_from_dtype(dtype))
1059 ):
1060 raise ValueError(f"dtype {dtype} cannot be converted to timedelta64[ns]")
1061
1062 return dtype