diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 3609c68a26c0f..6c9462ff4fa4d 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -14,6 +14,7 @@ precision_from_unit, ) import pandas.compat as compat +from pandas.compat.numpy import function as nv from pandas.util._decorators import Appender from pandas.core.dtypes.common import ( @@ -41,6 +42,7 @@ ) from pandas.core.dtypes.missing import isna +from pandas.core import nanops from pandas.core.algorithms import checked_add_with_arr import pandas.core.common as com from pandas.core.ops.invalid import invalid_comparison @@ -384,6 +386,62 @@ def astype(self, dtype, copy=True): return self return dtl.DatetimeLikeArrayMixin.astype(self, dtype, copy=copy) + def sum( + self, + axis=None, + dtype=None, + out=None, + keepdims: bool = False, + initial=None, + skipna: bool = True, + min_count: int = 0, + ): + nv.validate_sum( + (), dict(dtype=dtype, out=out, keepdims=keepdims, initial=initial) + ) + if not len(self): + return NaT + if not skipna and self._hasnans: + return NaT + + result = nanops.nansum( + self._data, axis=axis, skipna=skipna, min_count=min_count + ) + return Timedelta(result) + + def std( + self, + axis=None, + dtype=None, + out=None, + ddof: int = 1, + keepdims: bool = False, + skipna: bool = True, + ): + nv.validate_stat_ddof_func( + (), dict(dtype=dtype, out=out, keepdims=keepdims), fname="std" + ) + if not len(self): + return NaT + if not skipna and self._hasnans: + return NaT + + result = nanops.nanstd(self._data, axis=axis, skipna=skipna, ddof=ddof) + return Timedelta(result) + + def median( + self, + axis=None, + out=None, + overwrite_input: bool = False, + keepdims: bool = False, + skipna: bool = True, + ): + nv.validate_median( + (), dict(out=out, overwrite_input=overwrite_input, keepdims=keepdims) + ) + return nanops.nanmedian(self._data, axis=axis, skipna=skipna) + # ---------------------------------------------------------------- # Rendering Methods diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 49dcea4da5760..2ecb66bc8f1e4 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -30,6 +30,7 @@ from pandas.core.indexes.datetimelike import ( DatetimeIndexOpsMixin, DatetimelikeDelegateMixin, + ea_passthrough, ) from pandas.core.indexes.numeric import Int64Index from pandas.core.ops import get_op_result_name @@ -173,6 +174,9 @@ def _join_i8_wrapper(joinf, **kwargs): _datetimelike_ops = TimedeltaArray._datetimelike_ops _datetimelike_methods = TimedeltaArray._datetimelike_methods _other_ops = TimedeltaArray._other_ops + sum = ea_passthrough(TimedeltaArray.sum) + std = ea_passthrough(TimedeltaArray.std) + median = ea_passthrough(TimedeltaArray.median) # ------------------------------------------------------------------- # Constructors diff --git a/pandas/tests/arrays/test_timedeltas.py b/pandas/tests/arrays/test_timedeltas.py index 540c3343b2a1b..42e7bee97e671 100644 --- a/pandas/tests/arrays/test_timedeltas.py +++ b/pandas/tests/arrays/test_timedeltas.py @@ -143,6 +143,18 @@ def test_setitem_objects(self, obj): class TestReductions: + @pytest.mark.parametrize("name", ["sum", "std", "min", "max", "median"]) + @pytest.mark.parametrize("skipna", [True, False]) + def test_reductions_empty(self, name, skipna): + tdi = pd.TimedeltaIndex([]) + arr = tdi.array + + result = getattr(tdi, name)(skipna=skipna) + assert result is pd.NaT + + result = getattr(arr, name)(skipna=skipna) + assert result is pd.NaT + def test_min_max(self): arr = TimedeltaArray._from_sequence(["3H", "3H", "NaT", "2H", "5H", "4H"]) @@ -160,11 +172,87 @@ def test_min_max(self): result = arr.max(skipna=False) assert result is pd.NaT - @pytest.mark.parametrize("skipna", [True, False]) - def test_min_max_empty(self, skipna): - arr = TimedeltaArray._from_sequence([]) - result = arr.min(skipna=skipna) + def test_sum(self): + tdi = pd.TimedeltaIndex(["3H", "3H", "NaT", "2H", "5H", "4H"]) + arr = tdi.array + + result = arr.sum(skipna=True) + expected = pd.Timedelta(hours=17) + assert isinstance(result, pd.Timedelta) + assert result == expected + + result = tdi.sum(skipna=True) + assert isinstance(result, pd.Timedelta) + assert result == expected + + result = arr.sum(skipna=False) + assert result is pd.NaT + + result = tdi.sum(skipna=False) + assert result is pd.NaT + + result = arr.sum(min_count=9) + assert result is pd.NaT + + result = tdi.sum(min_count=9) + assert result is pd.NaT + + result = arr.sum(min_count=1) + assert isinstance(result, pd.Timedelta) + assert result == expected + + result = tdi.sum(min_count=1) + assert isinstance(result, pd.Timedelta) + assert result == expected + + def test_npsum(self): + # GH#25335 np.sum should return a Timedelta, not timedelta64 + tdi = pd.TimedeltaIndex(["3H", "3H", "2H", "5H", "4H"]) + arr = tdi.array + + result = np.sum(tdi) + expected = pd.Timedelta(hours=17) + assert isinstance(result, pd.Timedelta) + assert result == expected + + result = np.sum(arr) + assert isinstance(result, pd.Timedelta) + assert result == expected + + def test_std(self): + tdi = pd.TimedeltaIndex(["0H", "4H", "NaT", "4H", "0H", "2H"]) + arr = tdi.array + + result = arr.std(skipna=True) + expected = pd.Timedelta(hours=2) + assert isinstance(result, pd.Timedelta) + assert result == expected + + result = tdi.std(skipna=True) + assert isinstance(result, pd.Timedelta) + assert result == expected + + result = arr.std(skipna=False) + assert result is pd.NaT + + result = tdi.std(skipna=False) + assert result is pd.NaT + + def test_median(self): + tdi = pd.TimedeltaIndex(["0H", "3H", "NaT", "5H06m", "0H", "2H"]) + arr = tdi.array + + result = arr.median(skipna=True) + expected = pd.Timedelta(hours=2) + assert isinstance(result, pd.Timedelta) + assert result == expected + + result = tdi.median(skipna=True) + assert isinstance(result, pd.Timedelta) + assert result == expected + + result = arr.std(skipna=False) assert result is pd.NaT - result = arr.max(skipna=skipna) + result = tdi.std(skipna=False) assert result is pd.NaT