BUG/TST: timedelta-like with Index/Series/DataFrame ops
State: closed

Broken off from #23308, with additional fixes for problems found along the way.


```
val = np.timedelta64(4*10**9, 'ns')
ser = pd.Series([val]) * 1.5

>>> ser % val   # <-- raises TypeError in master
0   00:00:02
dtype: timedelta64[ns] 
```

```
ri = pd.RangeIndex(1)
di = pd.DataFrame(ri)

>>> ri + df  # <-- raises TypeError in master
    0
0   0
```

```
td64 = np.timedelta64(4*10**9, 'ns')
tdnat = np.timedelta64("NaT", "D")
ser = pd.Series([0, 1, 2])

>>> ser + td64   # <-- fails to raise in master, now raises TypeError; ditto with tdnat
# ditto if for radd, sub, and rsub

>>> pd.Index(ser) + tdnat  # <-- in master returns all-NaT DatetimeIndex, now raises
# ditto for sub; reversed ops DO raise TypeError in master, but for wrong reason
>>> pd.DataFrame(ser) + tdnat # <-- in msater returns all-NaT timedelta64 DataFrame, now raises
# ditto for radd, sub, and rsub
```

```
off = pd.offsets.Minute()
ser = pd.Series([1, 2, 3])

>>> off / ser  # <--  raises TypeError in master
0   00:01:00
1   00:00:30
2  00:00:20
dtype: timedelta64[ns]
```




One more that isn't a bug in master, but where the new behavior is better:

```
off = pd.offsets.Minute()
ser = pd.Series([1, 2, 3])

# master
>>> ser * off
0        <Minute>
1   <2 * Minutes>
2   <3 * Minutes>
dtype: object

# PR
>>> ser * off
0   00:01:00
1   00:02:00
2   00:03:00
dtype: timedelta64[ns]
```

Labels:
name: Numeric Operations
description: Arithmetic, Comparison, and Logical operations
name: Timedelta
description: Timedelta data type

Comments:
Hello @jbrockmendel! Thanks for submitting the PR.

 - There are no PEP8 issues in the file [`pandas/core/frame.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/core/frame.py) !

 - There are no PEP8 issues in the file [`pandas/core/indexes/base.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/core/indexes/base.py) !

 - There are no PEP8 issues in the file [`pandas/core/indexes/range.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/core/indexes/range.py) !

 - There are no PEP8 issues in the file [`pandas/core/ops.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/core/ops.py) !

 - There are no PEP8 issues in the file [`pandas/core/sparse/frame.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/core/sparse/frame.py) !

 - There are no PEP8 issues in the file [`pandas/tests/arithmetic/conftest.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/tests/arithmetic/conftest.py) !

 - There are no PEP8 issues in the file [`pandas/tests/arithmetic/test_numeric.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/tests/arithmetic/test_numeric.py) !

 - There are no PEP8 issues in the file [`pandas/tests/arithmetic/test_timedelta64.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/tests/arithmetic/test_timedelta64.py) !

 - There are no PEP8 issues in the file [`pandas/tests/indexes/test_range.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/tests/indexes/test_range.py) !

 - There are no PEP8 issues in the file [`pandas/tests/internals/test_internals.py`](https://github.com/jbrockmendel/pandas/blob/b3a316847d201c23759c5ff1795bdb04a0b46bec/pandas/tests/internals/test_internals.py) !


Might be out of the loops, but ops with `datetime.timedelta` and `TimedeltaIndex` operate as intened too with `Index`, `Series`, etc?

# [Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/23320?src=pr&el=h1) Report
> Merging [#23320](https://codecov.io/gh/pandas-dev/pandas/pull/23320?src=pr&el=desc) into [master](https://codecov.io/gh/pandas-dev/pandas/commit/a2e599499667b256bc5b8b13a75f0601eccfd432?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/pandas-dev/pandas/pull/23320/graphs/tree.svg?width=650&token=eZ4WkYLtcO&height=150&src=pr)](https://codecov.io/gh/pandas-dev/pandas/pull/23320?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master   #23320      +/-   ##
==========================================
+ Coverage   92.18%   92.18%   +<.01%     
==========================================
  Files         161      161              
  Lines       51184    51190       +6     
==========================================
+ Hits        47185    47191       +6     
  Misses       3999     3999
```

| Flag | Coverage Δ | |
|---|---|---|
| #multiple | `90.62% <100%> (ø)` | :arrow_up: |
| #single | `42.23% <50%> (-0.01%)` | :arrow_down: |

| [Impacted Files](https://codecov.io/gh/pandas-dev/pandas/pull/23320?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pandas/core/sparse/frame.py](https://codecov.io/gh/pandas-dev/pandas/pull/23320/diff?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvc3BhcnNlL2ZyYW1lLnB5) | `94.86% <100%> (ø)` | :arrow_up: |
| [pandas/core/ops.py](https://codecov.io/gh/pandas-dev/pandas/pull/23320/diff?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvb3BzLnB5) | `94.25% <100%> (+0.01%)` | :arrow_up: |
| [pandas/core/frame.py](https://codecov.io/gh/pandas-dev/pandas/pull/23320/diff?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvZnJhbWUucHk=) | `97.03% <100%> (ø)` | :arrow_up: |
| [pandas/core/indexes/base.py](https://codecov.io/gh/pandas-dev/pandas/pull/23320/diff?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvaW5kZXhlcy9iYXNlLnB5) | `96.62% <100%> (ø)` | :arrow_up: |
| [pandas/core/indexes/range.py](https://codecov.io/gh/pandas-dev/pandas/pull/23320/diff?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvaW5kZXhlcy9yYW5nZS5weQ==) | `95.79% <100%> (+0.02%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/23320?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/23320?src=pr&el=footer). Last update [a2e5994...481bb90](https://codecov.io/gh/pandas-dev/pandas/pull/23320?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

> Might be out of the loops, but ops with datetime.timedelta and TimedeltaIndex operate as intened too with Index, Series, etc?

Yes.  The "broken" case is one where we don't override numpy's behavior, which is only with a Integer Series/DataFrame operating specifically with np.timedelta64 scalar.
does any of this need a whatsnew?
> does any of this need a whatsnew?

The fixed bugs are sufficiently small that no one has opened an Issue.  I can write a couple lines, but it wouldn't be a grave omission if skipped.
@jbrockmendel About the offset behaviour you mention in the first post:

```
off = pd.offsets.Minute()
ser = pd.Series([1, 2, 3])

>>> off / ser  # <--  raises TypeError in master
0   00:01:00
1   00:00:30
2  00:00:20
dtype: timedelta64[ns]
```

Why not keep it as offsets? I find this conversion to timedelta's a bit strange (if somebody really wants this, they can use a timedelta to start with)

Also, the same operation with a scalar also raises:

```
In [107]: off / 2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-107-52397db32693> in <module>()
----> 1 off / 2

TypeError: unsupported operand type(s) for /: 'Minute' and 'int'
```

shouldn't that be consistent? 


> Why not keep it as offsets? I find this conversion to timedelta's a bit strange (if somebody really wants this, they can use a timedelta to start with)

Tick is basically redundant with Timedelta, so basically all that is accomplished by returning a Series of Ticks is getting object-dtype performance.
> Also, the same operation with a scalar also raises:

Yes, the Tick methods needs some fleshing-out.
Yes, but if you care about that, you can use Timedelta's ? 

Eg, now when working with Periods, you can get an object array/series of offsets:

```
In [117]: s = pd.Series(pd.core.arrays.period_array(['2012', '2012'], freq='A'))

In [118]: s
Out[118]: 
0    2012
1    2012
dtype: period[A-DEC]

In [119]: s - s[0]
Out[119]: 
0    <0 * YearEnds: month=12>
1    <0 * YearEnds: month=12>
dtype: object
```

Since you end up with that here, why not when doing arithmetic with offsets?
> Eg, now when working with Periods, you can get an object array/series of offsets:

For "A" freq that is unavoidable, but for "D" and lower I think we should change that to return timedelta64.  (separate Issue/PR)

> Since you end up with that here, why not when doing arithmetic with offsets?

Immediate reason: because that is the easiest way to fix the extant bug(s).
Big-picture reason: I am not aware of any good reason why a user would want the object array, so see it as a "why not" rather than a "why".
Another thing, in the OP you posted:

```
off = pd.offsets.Minute()
ser = pd.Series([1, 2, 3])

# master
>>> ser * off
0        <Minute>
1   <2 * Minutes>
2   <3 * Minutes>
dtype: object

# PR
>>> ser * off
0   00:01:00
1   00:02:00
2   00:03:00
dtype: timedelta64[ns]
```

But will then also the following change?

```
In [124]: pd.Series([off, off, off]) * 3
Out[124]: 
0    <3 * Minutes>
1    <3 * Minutes>
2    <3 * Minutes>
dtype: object
```

and 

```
In [125]: off * 3
Out[125]: <3 * Minutes>
```

I think those operations should be consistent throughout the cases (whether left/right operands are scalars, ndarray or Series)

> Big-picture reason: I am not aware of any good reason why a user would want the object array, so see it as a "why not" rather than a "why".

A good "why" IMO is consistency with the scalar operations?
> I think those operations should be consistent throughout the cases

This one I fully agree on; that is a downside to the change in this PR.  I'd rather address it in an upcoming pass (I want to clear out my datetimelike PRs before jumping into DatetimeArray) but understand the opposite viewpoint.
Yes, I don't think we should introduce the inconsistency here in this PR, if we would later decide we actually want to keep it consistent. 
Is the offset change essential for the other bug fixes?
> Is the offset change essential for the other bug fixes?

No.  I'll revert it, both because you make a reasonable point and because I can't keep up with your commenting speed.
Reverted.
Rebased.  This is the last one I'd like to close out before making a big push on DatetimeArray
lgtm. over to you @jorisvandenbossche 
Thanks @jbrockmendel 

Do you want to open an issue to further discuss the Offset arithmetic?