Repository: pandas-dev/pandas

Issue:
np.sum(TimedeltaIndex) should return a Timedelta, not np.timedelta64
ditto TimedeltaArray.  xref #25329.

I think the relevant method is `__array_ufunc__`, @jorisvandenbossche any idea about this?


Discussion:
> I think the relevant method is `__array_ufunc__`, @jorisvandenbossche any idea about this?

The datetimelike indexes and arrays don't implement `__array_ufunc__` yet, do they? Or are you saying that the fix is to implement it?

The old (pre-numpy 1.13) way of doing this would be with `__array_ufunc__`, which is defined on our indexes.
> The old (pre-numpy 1.13) way of doing this would be with `__array_ufunc__`, which is defined on our indexes.

I suppose you mean `__array_wrap__` ?
See also https://github.com/pandas-dev/pandas/issues/22798 and https://github.com/pandas-dev/pandas/pull/23293 (that last one is a PR that started implementing this, I hope to find some time soon to pick it up again).
Yes, sorry.

On Fri, Feb 15, 2019 at 10:22 AM Joris Van den Bossche <
notifications@github.com> wrote:

> The old (pre-numpy 1.13) way of doing this would be with __array_ufunc__,
> which is defined on our indexes.
>
> I suppose you mean __array_wrap__ ?
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/25335#issuecomment-464109769>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABQHIgyck90AcKRJeb708fzL_WyJSEXOks5vNt65gaJpZM4a98EU>
> .
>


Resolving patch:
diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py
--- 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
--- 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


Test patch:
diff --git a/pandas/tests/arrays/test_timedeltas.py b/pandas/tests/arrays/test_timedeltas.py
--- 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