Repository: pydata/xarray

Issue:
DataArray.dt.seconds returns incorrect value for negative `timedelta64[ns]`
**What happened**:

For a negative `timedelta64[ns]` of 42 nanoseconds `DataArray.dt.seconds` returned a non-zero value (the returned value was `86399`). When I pass in a positive 42 nanosecond `timedelta64[ns]` with the the TimeDeltaAccessor correctly returns zero. I would have expected both assertions in the example below to have passed, but the second fails. This seems to be a general issue with negative `timedelta64[ns]`.

```bash
<xarray.DataArray 'seconds' (dim_0: 1)>
array([0])
Dimensions without coordinates: dim_0
<xarray.DataArray 'seconds' (dim_0: 1)>
array([86399])
Dimensions without coordinates: dim_0
Traceback (most recent call last):
  File "bug_dt_seconds.py", line 15, in <module>
    assert da.dt.seconds == 0
AssertionError
```

**What you expected to happen**:
```bash
<xarray.DataArray 'seconds' (dim_0: 1)>
array([0])
Dimensions without coordinates: dim_0
<xarray.DataArray 'seconds' (dim_0: 1)>
array([0])
Dimensions without coordinates: dim_0
```

**Minimal Complete Verifiable Example**:

```python
# coding: utf-8

import xarray as xr
import numpy as np

# number of nanoseconds
value = 42

da = xr.DataArray([np.timedelta64(value, "ns")])
print(da.dt.seconds)
assert da.dt.seconds == 0

da = xr.DataArray([np.timedelta64(-value, "ns")])
print(da.dt.seconds)
assert da.dt.seconds == 0
```

**Anything else we need to know?**:

I've narrowed this down to the call to `pd.Series(values.ravel())` in `xarray.core.accessor_dt._access_through_series`:

```python
ipdb> pd.Series(values.ravel())
0   -1 days +23:59:59.999999958
dtype: timedelta64[ns]
```

I think the issue arises because pandas turns the numpy timedelta64 into a "minus one day plus a time". This actually does have a number of "seconds" in it, but the "total_seconds" has the expected value:

```python
ipdb> pd.Series(values.ravel()).dt.total_seconds()
0   -4.200000e-08
dtype: float64
```

Which would correctly round to zero.

I don't think the issue is in pandas, although the output from pandas is counter-intuitive:

```python
ipdb> pd.Series(values.ravel()).dt.seconds
0    86399
dtype: int64
```

Maybe we should handle this as a special case by taking the absolute value before passing the values to pandas (and then applying the original sign again afterwards)?

**Environment**:

<details><summary>Output of <tt>xr.show_versions()</tt></summary>

```
INSTALLED VERSIONS
------------------
commit: None
python: 3.7.7 (default, May  6 2020, 04:59:01)
[Clang 4.0.1 (tags/RELEASE_401/final)]
python-bits: 64
OS: Darwin
OS-release: 19.6.0
machine: x86_64
processor: i386
byteorder: little
LC_ALL: en_GB.UTF-8
LANG: None
LOCALE: ('en_GB', 'UTF-8')
libhdf5: 1.10.4
libnetcdf: 4.6.2

xarray: 0.18.2
pandas: 1.3.4
numpy: 1.19.1
scipy: 1.5.0
netCDF4: 1.4.2
pydap: installed
h5netcdf: None
h5py: 2.9.0
Nio: None
zarr: 2.10.1
cftime: 1.5.1.1
nc_time_axis: None
PseudoNetCDF: None
rasterio: None
cfgrib: None
iris: None
bottleneck: 1.3.2
dask: 2021.09.1
distributed: 2021.09.1
matplotlib: 3.2.2
cartopy: 0.18.0
seaborn: 0.10.1
numbagg: None
fsspec: 2021.06.1
cupy: None
pint: 0.18
sparse: None
setuptools: 46.4.0.post20200518
pip: 21.1.2
conda: None
pytest: 6.0.1
IPython: 7.16.1
sphinx: None
```
</details>


Discussion:
maresb: Ugh:

```
AssertionError: Left and right DataArray objects are not identical
```

I don't understand why not. It passes locally. Anyone have any ideas?
maresb: Interesting. I installed the `xarray-tests` bare-minimum environment, and `actual - expected` is:
```python
<xarray.DataArray 'total_seconds' (time: 100)>
array([ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
       -1.45519152e-11, -1.45519152e-11, -1.45519152e-11, -1.45519152e-11,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00, -7.27595761e-12, -7.27595761e-12,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00, -3.63797881e-12,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  3.63797881e-12,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  7.27595761e-12,  7.27595761e-12,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  1.45519152e-11,  1.45519152e-11,  1.45519152e-11,
        1.45519152e-11,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00])
Coordinates:
  * time     (time) datetime64[ns] 2000-01-01 ... 2000-01-05T03:00:00
```
maresb: Ya, will do. Just bisected the problem to Pandas <1.5
maresb: Cool, looks like the only tests currently failing are those that were previously failing.
max-sixty: Nice, thanks @maresb !

Patch:
diff --git a/doc/api.rst b/doc/api.rst
index 96b48648..f2ff809e 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -557,6 +557,7 @@ Datetimelike properties
    DataArray.dt.seconds
    DataArray.dt.microseconds
    DataArray.dt.nanoseconds
+   DataArray.dt.total_seconds
 
 **Timedelta methods**:
 
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index b6bad62d..e2817781 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -24,6 +24,8 @@ New Features
 
 - Use `opt_einsum <https://optimized-einsum.readthedocs.io/en/stable/>`_ for :py:func:`xarray.dot` by default if installed.
   By `Deepak Cherian <https://github.com/dcherian>`_. (:issue:`7764`, :pull:`8373`).
+- Add ``DataArray.dt.total_seconds()`` method to match the Pandas API. (:pull:`8435`).
+  By `Ben Mares <https://github.com/maresb>`_.
 
 Breaking changes
 ~~~~~~~~~~~~~~~~
diff --git a/xarray/core/accessor_dt.py b/xarray/core/accessor_dt.py
index 0d4a402c..b57c2f38 100644
--- a/xarray/core/accessor_dt.py
+++ b/xarray/core/accessor_dt.py
@@ -74,6 +74,8 @@ def _access_through_series(values, name):
     if name == "season":
         months = values_as_series.dt.month.values
         field_values = _season_from_months(months)
+    elif name == "total_seconds":
+        field_values = values_as_series.dt.total_seconds().values
     elif name == "isocalendar":
         # special NaT-handling can be removed when
         # https://github.com/pandas-dev/pandas/issues/54657 is resolved
@@ -574,6 +576,13 @@ class TimedeltaAccessor(TimeAccessor[T_DataArray]):
            43200, 64800])
     Coordinates:
       * time     (time) timedelta64[ns] 1 days 00:00:00 ... 5 days 18:00:00
+    >>> ts.dt.total_seconds()
+    <xarray.DataArray 'total_seconds' (time: 20)>
+    array([ 86400., 108000., 129600., 151200., 172800., 194400., 216000.,
+           237600., 259200., 280800., 302400., 324000., 345600., 367200.,
+           388800., 410400., 432000., 453600., 475200., 496800.])
+    Coordinates:
+      * time     (time) timedelta64[ns] 1 days 00:00:00 ... 5 days 18:00:00
     """
 
     @property
@@ -596,6 +605,11 @@ class TimedeltaAccessor(TimeAccessor[T_DataArray]):
         """Number of nanoseconds (>= 0 and less than 1 microsecond) for each element"""
         return self._date_field("nanoseconds", np.int64)
 
+    # Not defined as a property in order to match the Pandas API
+    def total_seconds(self) -> T_DataArray:
+        """Total duration of each element expressed in seconds."""
+        return self._date_field("total_seconds", np.float64)
+
 
 class CombinedDatetimelikeAccessor(
     DatetimeAccessor[T_DataArray], TimedeltaAccessor[T_DataArray]


Test patch:
diff --git a/xarray/tests/test_accessor_dt.py b/xarray/tests/test_accessor_dt.py
index 64b48762..a8d5e722 100644
--- a/xarray/tests/test_accessor_dt.py
+++ b/xarray/tests/test_accessor_dt.py
@@ -6,6 +6,7 @@ import pytest
 
 import xarray as xr
 from xarray.tests import (
+    assert_allclose,
     assert_array_equal,
     assert_chunks_equal,
     assert_equal,
@@ -100,6 +101,19 @@ class TestDatetimeAccessor:
         assert expected.dtype == actual.dtype
         assert_identical(expected, actual)
 
+    def test_total_seconds(self) -> None:
+        # Subtract a value in the middle of the range to ensure that some values
+        # are negative
+        delta = self.data.time - np.datetime64("2000-01-03")
+        actual = delta.dt.total_seconds()
+        expected = xr.DataArray(
+            np.arange(-48, 52, dtype=np.float64) * 3600,
+            name="total_seconds",
+            coords=[self.data.time],
+        )
+        # This works with assert_identical when pandas is >=1.5.0.
+        assert_allclose(expected, actual)
+
     @pytest.mark.parametrize(
         "field, pandas_field",
         [