Skip to content

Commit 3ab8623

Browse files
jbrockmendeljreback
authored andcommitted
Fix Index __mul__-like ops with timedelta scalars (#19333)
1 parent 3b135c3 commit 3ab8623

File tree

5 files changed

+85
-6
lines changed

5 files changed

+85
-6
lines changed

doc/source/whatsnew/v0.23.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ Timedelta
765765
- Bug in :func:`Timedelta.__floordiv__`, :func:`Timedelta.__rfloordiv__` where operating with a ``Tick`` object would raise a ``TypeError`` instead of returning a numeric value (:issue:`19738`)
766766
- Bug in :func:`Period.asfreq` where periods near ``datetime(1, 1, 1)`` could be converted incorrectly (:issue:`19643`)
767767
- Bug in :func:`Timedelta.total_seconds()` causing precision errors i.e. ``Timedelta('30S').total_seconds()==30.000000000000004`` (:issue:`19458`)
768+
- Multiplication of :class:`TimedeltaIndex` by ``TimedeltaIndex`` will now raise ``TypeError`` instead of raising ``ValueError`` in cases of length mis-match (:issue`19333`)
768769
-
769770

770771
Timezones
@@ -799,6 +800,7 @@ Numeric
799800
- Bug in the :class:`DataFrame` constructor in which data containing very large positive or very large negative numbers was causing ``OverflowError`` (:issue:`18584`)
800801
- Bug in :class:`Index` constructor with ``dtype='uint64'`` where int-like floats were not coerced to :class:`UInt64Index` (:issue:`18400`)
801802
- Bug in :class:`DataFrame` flex arithmetic (e.g. ``df.add(other, fill_value=foo)``) with a ``fill_value`` other than ``None`` failed to raise ``NotImplementedError`` in corner cases where either the frame or ``other`` has length zero (:issue:`19522`)
803+
- Multiplication and division of numeric-dtyped :class:`Index` objects with timedelta-like scalars returns ``TimedeltaIndex`` instead of raising ``TypeError`` (:issue:`19333`)
802804

803805

804806
Indexing

pandas/core/indexes/base.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66
from pandas._libs import (lib, index as libindex, tslib as libts,
77
algos as libalgos, join as libjoin,
8-
Timestamp)
8+
Timestamp, Timedelta)
99
from pandas._libs.lib import is_datetime_array
1010

1111
from pandas.compat import range, u, set_function_name
@@ -16,7 +16,7 @@
1616
from pandas.core.dtypes.generic import (
1717
ABCSeries, ABCDataFrame,
1818
ABCMultiIndex,
19-
ABCPeriodIndex,
19+
ABCPeriodIndex, ABCTimedeltaIndex,
2020
ABCDateOffset)
2121
from pandas.core.dtypes.missing import isna, array_equivalent
2222
from pandas.core.dtypes.common import (
@@ -3918,7 +3918,21 @@ def dropna(self, how='any'):
39183918
return self._shallow_copy()
39193919

39203920
def _evaluate_with_timedelta_like(self, other, op, opstr, reversed=False):
3921-
raise TypeError("can only perform ops with timedelta like values")
3921+
# Timedelta knows how to operate with np.array, so dispatch to that
3922+
# operation and then wrap the results
3923+
other = Timedelta(other)
3924+
values = self.values
3925+
if reversed:
3926+
values, other = other, values
3927+
3928+
with np.errstate(all='ignore'):
3929+
result = op(values, other)
3930+
3931+
attrs = self._get_attributes_dict()
3932+
attrs = self._maybe_update_attributes(attrs)
3933+
if op == divmod:
3934+
return Index(result[0], **attrs), Index(result[1], **attrs)
3935+
return Index(result, **attrs)
39223936

39233937
def _evaluate_with_datetime_like(self, other, op, opstr):
39243938
raise TypeError("can only perform ops with datetime like values")
@@ -4061,6 +4075,9 @@ def _make_evaluate_binop(op, opstr, reversed=False, constructor=Index):
40614075
def _evaluate_numeric_binop(self, other):
40624076
if isinstance(other, (ABCSeries, ABCDataFrame)):
40634077
return NotImplemented
4078+
elif isinstance(other, ABCTimedeltaIndex):
4079+
# Defer to subclass implementation
4080+
return NotImplemented
40644081

40654082
other = self._validate_for_numeric_binop(other, op, opstr)
40664083

pandas/core/indexes/range.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from sys import getsizeof
22
import operator
3+
from datetime import timedelta
34

45
import numpy as np
56
from pandas._libs import index as libindex
@@ -8,7 +9,7 @@
89
is_integer,
910
is_scalar,
1011
is_int64_dtype)
11-
from pandas.core.dtypes.generic import ABCSeries
12+
from pandas.core.dtypes.generic import ABCSeries, ABCTimedeltaIndex
1213

1314
from pandas import compat
1415
from pandas.compat import lrange, range, get_range_parameters
@@ -587,6 +588,15 @@ def _make_evaluate_binop(op, opstr, reversed=False, step=False):
587588
def _evaluate_numeric_binop(self, other):
588589
if isinstance(other, ABCSeries):
589590
return NotImplemented
591+
elif isinstance(other, ABCTimedeltaIndex):
592+
# Defer to TimedeltaIndex implementation
593+
return NotImplemented
594+
elif isinstance(other, (timedelta, np.timedelta64)):
595+
# GH#19333 is_integer evaluated True on timedelta64,
596+
# so we need to catch these explicitly
597+
if reversed:
598+
return op(other, self._int64index)
599+
return op(self._int64index, other)
590600

591601
other = self._validate_for_numeric_binop(other, op, opstr)
592602
attrs = self._get_attributes_dict()

pandas/tests/indexes/test_numeric.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import pandas.util.testing as tm
1414

1515
import pandas as pd
16-
from pandas._libs.tslib import Timestamp
16+
from pandas._libs.tslib import Timestamp, Timedelta
1717

1818
from pandas.tests.indexes.common import Base
1919

@@ -26,6 +26,42 @@ def full_like(array, value):
2626
return ret
2727

2828

29+
class TestIndexArithmeticWithTimedeltaScalar(object):
30+
31+
@pytest.mark.parametrize('index', [
32+
Int64Index(range(1, 11)),
33+
UInt64Index(range(1, 11)),
34+
Float64Index(range(1, 11)),
35+
RangeIndex(1, 11)])
36+
@pytest.mark.parametrize('scalar_td', [Timedelta(days=1),
37+
Timedelta(days=1).to_timedelta64(),
38+
Timedelta(days=1).to_pytimedelta()])
39+
def test_index_mul_timedelta(self, scalar_td, index):
40+
# GH#19333
41+
expected = pd.timedelta_range('1 days', '10 days')
42+
43+
result = index * scalar_td
44+
tm.assert_index_equal(result, expected)
45+
commute = scalar_td * index
46+
tm.assert_index_equal(commute, expected)
47+
48+
@pytest.mark.parametrize('index', [Int64Index(range(1, 3)),
49+
UInt64Index(range(1, 3)),
50+
Float64Index(range(1, 3)),
51+
RangeIndex(1, 3)])
52+
@pytest.mark.parametrize('scalar_td', [Timedelta(days=1),
53+
Timedelta(days=1).to_timedelta64(),
54+
Timedelta(days=1).to_pytimedelta()])
55+
def test_index_rdiv_timedelta(self, scalar_td, index):
56+
expected = pd.TimedeltaIndex(['1 Day', '12 Hours'])
57+
58+
result = scalar_td / index
59+
tm.assert_index_equal(result, expected)
60+
61+
with pytest.raises(TypeError):
62+
index / scalar_td
63+
64+
2965
class Numeric(Base):
3066

3167
def test_numeric_compat(self):

pandas/tests/indexes/timedeltas/test_arithmetic.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def test_dti_mul_dti_raises(self):
368368

369369
def test_dti_mul_too_short_raises(self):
370370
idx = self._holder(np.arange(5, dtype='int64'))
371-
with pytest.raises(ValueError):
371+
with pytest.raises(TypeError):
372372
idx * self._holder(np.arange(3))
373373
with pytest.raises(ValueError):
374374
idx * np.array([1, 2])
@@ -544,6 +544,20 @@ def test_tdi_div_tdlike_scalar_with_nat(self, delta):
544544
result = rng / delta
545545
tm.assert_index_equal(result, expected)
546546

547+
@pytest.mark.parametrize('other', [np.arange(1, 11),
548+
pd.Int64Index(range(1, 11)),
549+
pd.UInt64Index(range(1, 11)),
550+
pd.Float64Index(range(1, 11)),
551+
pd.RangeIndex(1, 11)])
552+
def test_tdi_rmul_arraylike(self, other):
553+
tdi = TimedeltaIndex(['1 Day'] * 10)
554+
expected = timedelta_range('1 days', '10 days')
555+
556+
result = other * tdi
557+
tm.assert_index_equal(result, expected)
558+
commute = tdi * other
559+
tm.assert_index_equal(commute, expected)
560+
547561
def test_subtraction_ops(self):
548562
# with datetimes/timedelta and tdi/dti
549563
tdi = TimedeltaIndex(['1 days', pd.NaT, '2 days'], name='foo')

0 commit comments

Comments
 (0)