From 6ec1f08379dcf59c487493fef3789d43a2d8762d Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 20 Nov 2018 18:31:24 -0800 Subject: [PATCH 01/14] implement truediv, rtruediv directly in TimedeltaArray; tests --- pandas/core/arrays/timedeltas.py | 75 ++++++++++++++++++- pandas/core/indexes/timedeltas.py | 36 ++++++++- pandas/tests/arithmetic/test_timedelta64.py | 81 +++++++++++++++++++-- 3 files changed, 181 insertions(+), 11 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index d1e6d979b554c..74e524414fa74 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -7,7 +7,7 @@ import numpy as np -from pandas._libs import tslibs +from pandas._libs import lib, tslibs from pandas._libs.tslibs import NaT, Timedelta, Timestamp, iNaT from pandas._libs.tslibs.fields import get_timedelta_field from pandas._libs.tslibs.timedeltas import ( @@ -324,12 +324,83 @@ def _evaluate_with_timedelta_like(self, other, op): __mul__ = _wrap_tdi_op(operator.mul) __rmul__ = __mul__ - __truediv__ = _wrap_tdi_op(operator.truediv) __floordiv__ = _wrap_tdi_op(operator.floordiv) __rfloordiv__ = _wrap_tdi_op(ops.rfloordiv) + def __truediv__(self, other): + # TODO: should we unbox zero_dim? + if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)): + return NotImplemented + + if isinstance(other, (timedelta, np.timedelta64, Tick)): + other = Timedelta(other) + if other is NaT: + # specifically timedelta64-NaT + result = np.empty(self.shape, dtype=np.float64) + result.fill(np.nan) + return result + + # otherwise, dispatch to Timedelta implementation + return self._data / other + + elif lib.is_scalar(other): + # assume it is numeric + result = self._data / other + freq = None + if self.freq is not None: + # Tick division is not implemented, so operate on Timedelta + freq = self.freq.delta / other + return type(self)(result, freq=freq) + + elif is_timedelta64_dtype(other): + # let numpy handle it + return self._data / other + + elif is_object_dtype(other): + result = [self[n] / other[n] for n in range(len(self))] + result = np.array(result) + if lib.infer_dtype(result) == 'timedelta': + # TODO: keep inferred freq? + result, _ = sequence_to_td64ns(result) + return type(self)(result) + + return np.array(result) + + else: + result = self._data / other + return type(self)(result) + + def __rtruediv__(self, other): + if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)): + return NotImplemented + + if isinstance(other, (timedelta, np.timedelta64, Tick)): + other = Timedelta(other) + if other is NaT: + # specifically timedelta64-NaT + result = np.empty(self.shape, dtype=np.float64) + result.fill(np.nan) + return result + + # otherwise, dispatch to Timedelta implementation + return other / self._data + + elif is_timedelta64_dtype(other): + # let numpy handle it + return other / self._data + + elif is_object_dtype(other): + result = [other[n] / self[n] for n in range(len(self))] + return np.array(result) + + else: + # object-dtype *may* be OK here, but generally this will raise + result = other / self._data + return type(self)(result) + if compat.PY2: __div__ = __truediv__ + __rdiv__ = __rtruediv__ # Note: TimedeltaIndex overrides this in call to cls._add_numeric_methods def __neg__(self): diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 8f50b40a20738..ddf49271fada2 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -229,11 +229,8 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): __mul__ = Index.__mul__ __rmul__ = Index.__rmul__ - __truediv__ = Index.__truediv__ __floordiv__ = Index.__floordiv__ __rfloordiv__ = Index.__rfloordiv__ - if compat.PY2: - __div__ = Index.__div__ days = wrap_field_accessor(TimedeltaArray.days) seconds = wrap_field_accessor(TimedeltaArray.seconds) @@ -242,6 +239,38 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): total_seconds = wrap_array_method(TimedeltaArray.total_seconds, True) + @classmethod + def _override_div_mod_methods(cls): + """ + Define __div__, __mod__, etc by wrapping TimedeltaArray implementations + rather than the versions defined by _add_numeric_methods. Because + _add_numeric_methods is called after class creation, we have to define + these methods even later. + """ + + def __truediv__(self, other): + oth = other + if isinstance(other, Index): + # TimedeltaArray defers, so we need to unwrap + oth = other._values + result = TimedeltaArray.__truediv__(self, oth) + return wrap_arithmetic_op(self, other, result) + + def __rtruediv__(self, other): + oth = other + if isinstance(other, Index): + # TimedeltaArray defers, so we need to unwrap + oth = other._values + result = TimedeltaArray.__rtruediv__(self, oth) + return wrap_arithmetic_op(self, other, result) + + cls.__truediv__ = __truediv__ + cls.__rtruediv__ = __rtruediv__ + + if compat.PY2: + cls.__div__ = __truediv__ + cls.__rdiv__ = __rtruediv__ + # ------------------------------------------------------------------- @Appender(_index_shared_docs['astype']) @@ -642,6 +671,7 @@ def delete(self, loc): TimedeltaIndex._add_numeric_methods() TimedeltaIndex._add_logical_methods_disabled() TimedeltaIndex._add_datetimelike_methods() +TimedeltaIndex._override_div_mod_methods() def _is_convertible_to_index(other): diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 58c7216f0eece..62b98e1883586 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1115,7 +1115,7 @@ def test_tdi_rmul_arraylike(self, other, box_with_array): tm.assert_equal(commute, expected) # ------------------------------------------------------------------ - # __div__ + # __div__, __rdiv__ def test_td64arr_div_nat_invalid(self, box_with_array): # don't allow division by NaT (maybe could in the future) @@ -1123,6 +1123,23 @@ def test_td64arr_div_nat_invalid(self, box_with_array): rng = tm.box_expected(rng, box_with_array) with pytest.raises(TypeError): rng / pd.NaT + with pytest.raises(TypeError): + pd.NaT / rng + + def test_td64arr_div_td64nat(self, box_with_array): + rng = timedelta_range('1 days', '10 days',) + rng = tm.box_expected(rng, box_with_array) + + other = np.timedelta64('NaT') + + expected = np.array([np.nan] * 10) + expected = tm.box_expected(expected, box_with_array) + + result = rng / other + tm.assert_equal(result, expected) + + result = other / rng + tm.assert_equal(result, expected) def test_td64arr_div_int(self, box_with_array): idx = TimedeltaIndex(np.arange(5, dtype='int64')) @@ -1131,7 +1148,10 @@ def test_td64arr_div_int(self, box_with_array): result = idx / 1 tm.assert_equal(result, idx) - def test_tdi_div_tdlike_scalar(self, two_hours, box_with_array): + with pytest.raises(TypeError): + 1 / idx + + def test_td64arr_div_tdlike_scalar(self, two_hours, box_with_array): # GH#20088, GH#22163 ensure DataFrame returns correct dtype rng = timedelta_range('1 days', '10 days', name='foo') expected = pd.Float64Index((np.arange(10) + 1) * 12, name='foo') @@ -1142,7 +1162,12 @@ def test_tdi_div_tdlike_scalar(self, two_hours, box_with_array): result = rng / two_hours tm.assert_equal(result, expected) - def test_tdi_div_tdlike_scalar_with_nat(self, two_hours, box_with_array): + result = two_hours / rng + expected = 1 / expected + tm.assert_equal(result, expected) + + def test_td64arr_div_tdlike_scalar_with_nat(self, two_hours, + box_with_array): rng = TimedeltaIndex(['1 days', pd.NaT, '2 days'], name='foo') expected = pd.Float64Index([12, np.nan, 24], name='foo') @@ -1152,6 +1177,31 @@ def test_tdi_div_tdlike_scalar_with_nat(self, two_hours, box_with_array): result = rng / two_hours tm.assert_equal(result, expected) + result = two_hours / rng + expected = 1 / expected + tm.assert_equal(result, expected) + + def test_td64arr_div_td64_ndarray(self, box_with_array): + rng = TimedeltaIndex(['1 days', pd.NaT, '2 days'], name='foo') + expected = pd.Float64Index([12, np.nan, 24], name='foo') + + rng = tm.box_expected(rng, box_with_array) + expected = tm.box_expected(expected, box_with_array) + + other = np.array([2, 4, 2], dtype='m8[h]') + result = rng / other + tm.assert_equal(result, expected) + + result = rng / other.astype(object) + tm.assert_equal(result, expected) + + expected = 1 / expected + result = other / rng + tm.assert_equal(result, expected) + + result = other.astype(object) / rng + tm.assert_equal(result, expected) + # ------------------------------------------------------------------ # __floordiv__, __rfloordiv__ @@ -1203,6 +1253,9 @@ def test_td64arr_floordiv_int(self, box_with_array): result = idx // 1 tm.assert_equal(result, idx) + with pytest.raises(TypeError): + 1 // idx + def test_td64arr_floordiv_tdlike_scalar(self, two_hours, box_with_array): tdi = timedelta_range('1 days', '10 days', name='foo') expected = pd.Int64Index((np.arange(10) + 1) * 12, name='foo') @@ -1309,6 +1362,9 @@ def test_td64arr_div_numeric_scalar(self, box_with_array, two): result = tdser / two tm.assert_equal(result, expected) + with pytest.raises(TypeError): + two / tdser + @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', 'uint64', 'uint32', 'uint16', 'uint8', 'float64', 'float32', 'float16']) @@ -1361,6 +1417,14 @@ def test_td64arr_div_numeric_array(self, box_with_array, vector, dtype): with pytest.raises(TypeError): vector / tdser + if not isinstance(vector, pd.Index): + # Index.__rdiv__ won't try to operate elementwise, just raises + result = tdser / vector.astype(object) + tm.assert_equal(result, expected) + + with pytest.raises(TypeError): + vector.astype(object) / tdser + @pytest.mark.parametrize('names', [(None, None, None), ('Egon', 'Venkman', None), ('NCC1701D', 'NCC1701D', 'NCC1701D')]) @@ -1391,20 +1455,25 @@ def test_td64arr_mul_int_series(self, box_df_fail, names): @pytest.mark.parametrize('names', [(None, None, None), ('Egon', 'Venkman', None), ('NCC1701D', 'NCC1701D', 'NCC1701D')]) - def test_float_series_rdiv_td64arr(self, box, names): + def test_float_series_rdiv_td64arr(self, box_with_array, names): # GH#19042 test for correct name attachment # TODO: the direct operation TimedeltaIndex / Series still # needs to be fixed. + box = box_with_array tdi = TimedeltaIndex(['0days', '1day', '2days', '3days', '4days'], name=names[0]) ser = Series([1.5, 3, 4.5, 6, 7.5], dtype=np.float64, name=names[1]) + xname = names[2] if box is not tm.to_array else names[1] expected = Series([tdi[n] / ser[n] for n in range(len(ser))], dtype='timedelta64[ns]', - name=names[2]) + name=xname) + + xbox = box + if box in [pd.Index, tm.to_array] and type(ser) is Series: + xbox = Series tdi = tm.box_expected(tdi, box) - xbox = Series if (box is pd.Index and type(ser) is Series) else box expected = tm.box_expected(expected, xbox) result = ser.__rdiv__(tdi) From 4c2cc5961d44c3b0f2ce9f087ea130d37a50fcf7 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 20 Nov 2018 18:46:07 -0800 Subject: [PATCH 02/14] test tdi/tdi specifically --- pandas/tests/arithmetic/test_timedelta64.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 62b98e1883586..4a78304da121d 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1182,6 +1182,7 @@ def test_td64arr_div_tdlike_scalar_with_nat(self, two_hours, tm.assert_equal(result, expected) def test_td64arr_div_td64_ndarray(self, box_with_array): + # GH#22631 rng = TimedeltaIndex(['1 days', pd.NaT, '2 days'], name='foo') expected = pd.Float64Index([12, np.nan, 24], name='foo') @@ -1192,6 +1193,9 @@ def test_td64arr_div_td64_ndarray(self, box_with_array): result = rng / other tm.assert_equal(result, expected) + result = rng / tm.box_expected(other, box_with_array) + tm.assert_equal(result, expected, check_names=False) + result = rng / other.astype(object) tm.assert_equal(result, expected) @@ -1199,6 +1203,9 @@ def test_td64arr_div_td64_ndarray(self, box_with_array): result = other / rng tm.assert_equal(result, expected) + result = tm.box_expected(other, box_with_array) / rng + tm.assert_equal(result, expected, check_names=False) + result = other.astype(object) / rng tm.assert_equal(result, expected) From bd2ee969627a93ef32c8462ace74ff8539f24330 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 20 Nov 2018 20:20:11 -0800 Subject: [PATCH 03/14] more checks and test cases --- pandas/core/arrays/timedeltas.py | 26 ++++++++++++++++++- pandas/tests/arithmetic/test_timedelta64.py | 28 ++++++++++++++++++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 74e524414fa74..58b1f929b0774 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -165,6 +165,7 @@ def _simple_new(cls, values, freq=None, dtype=_TD_DTYPE): def __new__(cls, values, freq=None, dtype=_TD_DTYPE): freq, freq_infer = dtl.maybe_infer_freq(freq) + values, inferred_freq = sequence_to_td64ns(values) values = np.array(values, copy=False) if values.dtype == np.object_: @@ -328,7 +329,9 @@ def _evaluate_with_timedelta_like(self, other, op): __rfloordiv__ = _wrap_tdi_op(ops.rfloordiv) def __truediv__(self, other): - # TODO: should we unbox zero_dim? + # TODO: Decimals? + other = lib.item_from_zerodim(other) + if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)): return NotImplemented @@ -352,6 +355,13 @@ def __truediv__(self, other): freq = self.freq.delta / other return type(self)(result, freq=freq) + if not hasattr(other, "dtype"): + # e.g. list, tuple + other = np.array(other) + + if len(other) != len(self): + raise ValueError("Cannot divide vectors with unequal lengths") + elif is_timedelta64_dtype(other): # let numpy handle it return self._data / other @@ -371,6 +381,8 @@ def __truediv__(self, other): return type(self)(result) def __rtruediv__(self, other): + other = lib.item_from_zerodim(other) + if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)): return NotImplemented @@ -385,6 +397,18 @@ def __rtruediv__(self, other): # otherwise, dispatch to Timedelta implementation return other / self._data + elif lib.is_scalar(other): + raise TypeError("Cannot divide {typ} by {cls}" + .format(typ=type(other).__name__, + cls=type(self).__name__)) + + if not hasattr(other, "dtype"): + # e.g. list, tuple + other = np.array(other) + + if len(other) != len(self): + raise ValueError("Cannot divide vectors with unequal lengths") + elif is_timedelta64_dtype(other): # let numpy handle it return other / self._data diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 4a78304da121d..cfafe4c7c1d8d 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1183,8 +1183,8 @@ def test_td64arr_div_tdlike_scalar_with_nat(self, two_hours, def test_td64arr_div_td64_ndarray(self, box_with_array): # GH#22631 - rng = TimedeltaIndex(['1 days', pd.NaT, '2 days'], name='foo') - expected = pd.Float64Index([12, np.nan, 24], name='foo') + rng = TimedeltaIndex(['1 days', pd.NaT, '2 days']) + expected = pd.Float64Index([12, np.nan, 24]) rng = tm.box_expected(rng, box_with_array) expected = tm.box_expected(expected, box_with_array) @@ -1194,21 +1194,41 @@ def test_td64arr_div_td64_ndarray(self, box_with_array): tm.assert_equal(result, expected) result = rng / tm.box_expected(other, box_with_array) - tm.assert_equal(result, expected, check_names=False) + tm.assert_equal(result, expected) result = rng / other.astype(object) tm.assert_equal(result, expected) + result = rng / list(other) + tm.assert_equal(result, expected) + + # reversed op expected = 1 / expected result = other / rng tm.assert_equal(result, expected) result = tm.box_expected(other, box_with_array) / rng - tm.assert_equal(result, expected, check_names=False) + tm.assert_equal(result, expected) result = other.astype(object) / rng tm.assert_equal(result, expected) + result = list(other) / rng + tm.assert_equal(result, expected) + + def test_tdarr_div_length_mismatch(self, box_with_array): + rng = TimedeltaIndex(['1 days', pd.NaT, '2 days']) + mismatched = [1, 2, 3, 4] + + rng = tm.box_expected(rng, box_with_array) + for obj in [mismatched, mismatched[:2]]: + # one shorter, one longer + for other in [obj, np.array(obj), pd.Index(obj)]: + with pytest.raises(ValueError): + rng / other + with pytest.raises(ValueError): + other / rng + # ------------------------------------------------------------------ # __floordiv__, __rfloordiv__ From 79901f595a45557663f146659b333a7b3d019a78 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 21 Nov 2018 09:37:27 -0800 Subject: [PATCH 04/14] dont define _override_div_mod_methods, matches for pytest.raises --- pandas/core/arrays/timedeltas.py | 1 - pandas/core/indexes/base.py | 13 +++--- pandas/core/indexes/timedeltas.py | 49 ++++++++------------- pandas/tests/arithmetic/test_timedelta64.py | 19 +++++--- 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 58b1f929b0774..82e9c52369e1b 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -329,7 +329,6 @@ def _evaluate_with_timedelta_like(self, other, op): __rfloordiv__ = _wrap_tdi_op(ops.rfloordiv) def __truediv__(self, other): - # TODO: Decimals? other = lib.item_from_zerodim(other) if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)): diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 93af7b9933782..12450f3d94028 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -4896,11 +4896,14 @@ def _add_numeric_methods_binary(cls): cls.__mod__ = _make_arithmetic_op(operator.mod, cls) cls.__floordiv__ = _make_arithmetic_op(operator.floordiv, cls) cls.__rfloordiv__ = _make_arithmetic_op(ops.rfloordiv, cls) - cls.__truediv__ = _make_arithmetic_op(operator.truediv, cls) - cls.__rtruediv__ = _make_arithmetic_op(ops.rtruediv, cls) - if not compat.PY3: - cls.__div__ = _make_arithmetic_op(operator.div, cls) - cls.__rdiv__ = _make_arithmetic_op(ops.rdiv, cls) + + if not issubclass(cls, ABCTimedeltaIndex): + # GH#23829 TimedeltaIndex defines these directly + cls.__truediv__ = _make_arithmetic_op(operator.truediv, cls) + cls.__rtruediv__ = _make_arithmetic_op(ops.rtruediv, cls) + if not compat.PY3: + cls.__div__ = _make_arithmetic_op(operator.div, cls) + cls.__rdiv__ = _make_arithmetic_op(ops.rdiv, cls) cls.__divmod__ = _make_arithmetic_op(divmod, cls) diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index ddf49271fada2..173af8a9a0f5a 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -239,37 +239,25 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): total_seconds = wrap_array_method(TimedeltaArray.total_seconds, True) - @classmethod - def _override_div_mod_methods(cls): - """ - Define __div__, __mod__, etc by wrapping TimedeltaArray implementations - rather than the versions defined by _add_numeric_methods. Because - _add_numeric_methods is called after class creation, we have to define - these methods even later. - """ + def __truediv__(self, other): + oth = other + if isinstance(other, Index): + # TimedeltaArray defers, so we need to unwrap + oth = other._values + result = TimedeltaArray.__truediv__(self, oth) + return wrap_arithmetic_op(self, other, result) + + def __rtruediv__(self, other): + oth = other + if isinstance(other, Index): + # TimedeltaArray defers, so we need to unwrap + oth = other._values + result = TimedeltaArray.__rtruediv__(self, oth) + return wrap_arithmetic_op(self, other, result) - def __truediv__(self, other): - oth = other - if isinstance(other, Index): - # TimedeltaArray defers, so we need to unwrap - oth = other._values - result = TimedeltaArray.__truediv__(self, oth) - return wrap_arithmetic_op(self, other, result) - - def __rtruediv__(self, other): - oth = other - if isinstance(other, Index): - # TimedeltaArray defers, so we need to unwrap - oth = other._values - result = TimedeltaArray.__rtruediv__(self, oth) - return wrap_arithmetic_op(self, other, result) - - cls.__truediv__ = __truediv__ - cls.__rtruediv__ = __rtruediv__ - - if compat.PY2: - cls.__div__ = __truediv__ - cls.__rdiv__ = __rtruediv__ + if compat.PY2: + __div__ = __truediv__ + __rdiv__ = __rtruediv__ # ------------------------------------------------------------------- @@ -671,7 +659,6 @@ def delete(self, loc): TimedeltaIndex._add_numeric_methods() TimedeltaIndex._add_logical_methods_disabled() TimedeltaIndex._add_datetimelike_methods() -TimedeltaIndex._override_div_mod_methods() def _is_convertible_to_index(other): diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index cfafe4c7c1d8d..b7a1d04e82b54 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1121,9 +1121,10 @@ def test_td64arr_div_nat_invalid(self, box_with_array): # don't allow division by NaT (maybe could in the future) rng = timedelta_range('1 days', '10 days', name='foo') rng = tm.box_expected(rng, box_with_array) - with pytest.raises(TypeError): + + with pytest.raises(TypeError, match='true_divide cannot use operands'): rng / pd.NaT - with pytest.raises(TypeError): + with pytest.raises(TypeError, match='Cannot divide NaTType by'): pd.NaT / rng def test_td64arr_div_td64nat(self, box_with_array): @@ -1148,7 +1149,7 @@ def test_td64arr_div_int(self, box_with_array): result = idx / 1 tm.assert_equal(result, idx) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match='Cannot divide'): 1 / idx def test_td64arr_div_tdlike_scalar(self, two_hours, box_with_array): @@ -1280,7 +1281,7 @@ def test_td64arr_floordiv_int(self, box_with_array): result = idx // 1 tm.assert_equal(result, idx) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match='floor_divide cannot use operands'): 1 // idx def test_td64arr_floordiv_tdlike_scalar(self, two_hours, box_with_array): @@ -1389,7 +1390,7 @@ def test_td64arr_div_numeric_scalar(self, box_with_array, two): result = tdser / two tm.assert_equal(result, expected) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match='Cannot divide'): two / tdser @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', @@ -1441,7 +1442,11 @@ def test_td64arr_div_numeric_array(self, box_with_array, vector, dtype): result = tdser / vector tm.assert_equal(result, expected) - with pytest.raises(TypeError): + pattern = ('true_divide cannot use operands|' + 'cannot perform __div__|' + 'unsupported operand|' + 'Cannot divide') + with pytest.raises(TypeError, match=pattern): vector / tdser if not isinstance(vector, pd.Index): @@ -1449,7 +1454,7 @@ def test_td64arr_div_numeric_array(self, box_with_array, vector, dtype): result = tdser / vector.astype(object) tm.assert_equal(result, expected) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=pattern): vector.astype(object) / tdser @pytest.mark.parametrize('names', [(None, None, None), From adea273e0a129307c5035500909dd0642b6c7bf2 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 21 Nov 2018 09:42:30 -0800 Subject: [PATCH 05/14] change comment --- pandas/core/arrays/timedeltas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 82e9c52369e1b..c2c66b5076ddc 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -417,7 +417,7 @@ def __rtruediv__(self, other): return np.array(result) else: - # object-dtype *may* be OK here, but generally this will raise + # numeric-dtyped other result = other / self._data return type(self)(result) From da9f7438ecb0b0c887f8ec7bc1f3cb5369a20c1c Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 21 Nov 2018 09:46:30 -0800 Subject: [PATCH 06/14] whatsnew, GH references --- doc/source/whatsnew/v0.24.0.rst | 2 +- pandas/tests/arithmetic/test_timedelta64.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.rst b/doc/source/whatsnew/v0.24.0.rst index 4ff3cc728f7f7..19f3b4042ba68 100644 --- a/doc/source/whatsnew/v0.24.0.rst +++ b/doc/source/whatsnew/v0.24.0.rst @@ -1224,7 +1224,7 @@ Timedelta - Bug in :class:`TimedeltaIndex` where adding a timezone-aware datetime scalar incorrectly returned a timezone-naive :class:`DatetimeIndex` (:issue:`23215`) - Bug in :class:`TimedeltaIndex` where adding ``np.timedelta64('NaT')`` incorrectly returned an all-`NaT` :class:`DatetimeIndex` instead of an all-`NaT` :class:`TimedeltaIndex` (:issue:`23215`) - Bug in :class:`Timedelta` and :func:`to_timedelta()` have inconsistencies in supported unit string (:issue:`21762`) - +- Bug in :class:`TimedeltaIndex` division where dividing by another :class:`TimedeltaIndex` raised ``TypeError`` instead of returning a :class:`Float64Index` (:issue:`23829`) Timezones ^^^^^^^^^ diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index b7a1d04e82b54..5d07a6d89743a 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1128,6 +1128,7 @@ def test_td64arr_div_nat_invalid(self, box_with_array): pd.NaT / rng def test_td64arr_div_td64nat(self, box_with_array): + # GH#23829 rng = timedelta_range('1 days', '10 days',) rng = tm.box_expected(rng, box_with_array) @@ -1150,6 +1151,7 @@ def test_td64arr_div_int(self, box_with_array): tm.assert_equal(result, idx) with pytest.raises(TypeError, match='Cannot divide'): + # GH#23829 1 / idx def test_td64arr_div_tdlike_scalar(self, two_hours, box_with_array): From ba9e490b1573844b304519fb0754339a6eda75dd Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 21 Nov 2018 11:30:43 -0800 Subject: [PATCH 07/14] error msg py3 compat --- pandas/tests/arithmetic/test_timedelta64.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 5d07a6d89743a..3d8d90460eecd 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1446,6 +1446,7 @@ def test_td64arr_div_numeric_array(self, box_with_array, vector, dtype): pattern = ('true_divide cannot use operands|' 'cannot perform __div__|' + 'cannot perform __truediv__|' 'unsupported operand|' 'Cannot divide') with pytest.raises(TypeError, match=pattern): From 8f276ae8e889e011e2953b1a70ba62d63fc8e679 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 21 Nov 2018 13:27:08 -0800 Subject: [PATCH 08/14] flake8 fixup, raise directly --- pandas/core/arrays/timedeltas.py | 6 +++--- pandas/tests/arithmetic/test_timedelta64.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index c2c66b5076ddc..2f378e8d2d39b 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -417,9 +417,9 @@ def __rtruediv__(self, other): return np.array(result) else: - # numeric-dtyped other - result = other / self._data - return type(self)(result) + raise TypeError("Cannot divide {dtype} data by {cls}" + .format(dtype=other.dtype, + cls=type(self).__name__)) if compat.PY2: __div__ = __truediv__ diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 3d8d90460eecd..a506bcea6315e 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1283,7 +1283,8 @@ def test_td64arr_floordiv_int(self, box_with_array): result = idx // 1 tm.assert_equal(result, idx) - with pytest.raises(TypeError, match='floor_divide cannot use operands'): + pattern = 'floor_divide cannot use operands' + with pytest.raises(TypeError, match=pattern): 1 // idx def test_td64arr_floordiv_tdlike_scalar(self, two_hours, box_with_array): From 2037be8e8acdcfc8cc3b0036743b6f621cad96fe Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 26 Nov 2018 09:35:06 -0800 Subject: [PATCH 09/14] sidestep object conversion --- pandas/core/arrays/timedeltas.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 2f378e8d2d39b..d5b39042c0d6a 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -368,12 +368,11 @@ def __truediv__(self, other): elif is_object_dtype(other): result = [self[n] / other[n] for n in range(len(self))] result = np.array(result) - if lib.infer_dtype(result) == 'timedelta': - # TODO: keep inferred freq? - result, _ = sequence_to_td64ns(result) + + if all(isinstance(x, Timedelta) or x is NaT for x in result): return type(self)(result) - return np.array(result) + return result else: result = self._data / other From 2fc44aa3bfe47c84dfa1e1ea894ba06bcf9f28a5 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 26 Nov 2018 19:07:21 -0800 Subject: [PATCH 10/14] dont case result when operating against object dtype --- pandas/core/arrays/timedeltas.py | 6 ++---- pandas/tests/arithmetic/test_timedelta64.py | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index d5b39042c0d6a..7013d4851d16f 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -329,6 +329,7 @@ def _evaluate_with_timedelta_like(self, other, op): __rfloordiv__ = _wrap_tdi_op(ops.rfloordiv) def __truediv__(self, other): + # timedelta / X is well-defined for timedelta-like or numeric X other = lib.item_from_zerodim(other) if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)): @@ -368,10 +369,6 @@ def __truediv__(self, other): elif is_object_dtype(other): result = [self[n] / other[n] for n in range(len(self))] result = np.array(result) - - if all(isinstance(x, Timedelta) or x is NaT for x in result): - return type(self)(result) - return result else: @@ -379,6 +376,7 @@ def __truediv__(self, other): return type(self)(result) def __rtruediv__(self, other): + # X / timedelta is defined only for timedelta-like X other = lib.item_from_zerodim(other) if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)): diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index a506bcea6315e..a7bc0fe5bf86f 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1453,9 +1453,13 @@ def test_td64arr_div_numeric_array(self, box_with_array, vector, dtype): with pytest.raises(TypeError, match=pattern): vector / tdser - if not isinstance(vector, pd.Index): + if (not isinstance(vector, pd.Index) and + box_with_array is not pd.DataFrame): # Index.__rdiv__ won't try to operate elementwise, just raises + # DataFrame casts just-NaT object column to datetime64 result = tdser / vector.astype(object) + expected = [tdser[n] / vector[n] for n in range(len(tdser))] + expected = tm.box_expected(expected, xbox) tm.assert_equal(result, expected) with pytest.raises(TypeError, match=pattern): From 641ad20721b426480571c5c98b2ceefa2397e0a7 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 28 Nov 2018 10:52:54 -0800 Subject: [PATCH 11/14] another GH reference --- doc/source/whatsnew/v0.24.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.rst b/doc/source/whatsnew/v0.24.0.rst index cb71f1a44cb45..5a0f8bb9256d0 100644 --- a/doc/source/whatsnew/v0.24.0.rst +++ b/doc/source/whatsnew/v0.24.0.rst @@ -1255,7 +1255,7 @@ Timedelta - Bug in :class:`TimedeltaIndex` where adding a timezone-aware datetime scalar incorrectly returned a timezone-naive :class:`DatetimeIndex` (:issue:`23215`) - Bug in :class:`TimedeltaIndex` where adding ``np.timedelta64('NaT')`` incorrectly returned an all-`NaT` :class:`DatetimeIndex` instead of an all-`NaT` :class:`TimedeltaIndex` (:issue:`23215`) - Bug in :class:`Timedelta` and :func:`to_timedelta()` have inconsistencies in supported unit string (:issue:`21762`) -- Bug in :class:`TimedeltaIndex` division where dividing by another :class:`TimedeltaIndex` raised ``TypeError`` instead of returning a :class:`Float64Index` (:issue:`23829`) +- Bug in :class:`TimedeltaIndex` division where dividing by another :class:`TimedeltaIndex` raised ``TypeError`` instead of returning a :class:`Float64Index` (:issue:`23829`, :issue:`22631`) Timezones ^^^^^^^^^ From 7d9e677b32cd602ea757de85327ce4f6fdee843f Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 28 Nov 2018 15:26:24 -0800 Subject: [PATCH 12/14] comment --- pandas/core/arrays/timedeltas.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 2a24225f45f0b..0408f301f2fb7 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -369,6 +369,9 @@ def __truediv__(self, other): return self._data / other elif is_object_dtype(other): + # Note: we do not do type inference on the result, so either + # an object array or numeric-dtyped (if numpy does inference) + # will be returned. GH#23829 result = [self[n] / other[n] for n in range(len(self))] result = np.array(result) return result @@ -412,6 +415,9 @@ def __rtruediv__(self, other): return other / self._data elif is_object_dtype(other): + # Note: unlike in __truediv__, we do not _need_ to do type# + # inference on the result. It does not raise, a numeric array + # is returned. GH#23829 result = [other[n] / self[n] for n in range(len(self))] return np.array(result) From 55cad6b17d70dd12bb088266b1fe03fa401d18bf Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 28 Nov 2018 16:00:56 -0800 Subject: [PATCH 13/14] Fixup rebase mixup, un-skip part of a test that isnt broken after all --- pandas/core/arrays/timedeltas.py | 3 +-- pandas/tests/arithmetic/test_timedelta64.py | 13 +++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 7163248d96924..3f14b61c1ea6c 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -165,7 +165,6 @@ def _simple_new(cls, values, freq=None, dtype=_TD_DTYPE): def __new__(cls, values, freq=None, dtype=_TD_DTYPE, copy=False): freq, freq_infer = dtl.maybe_infer_freq(freq) - values, inferred_freq = sequence_to_td64ns(values) values, inferred_freq = sequence_to_td64ns( values, copy=copy, unit=None) @@ -178,7 +177,7 @@ def __new__(cls, values, freq=None, dtype=_TD_DTYPE, copy=False): passed=freq.freqstr)) elif freq is None: freq = inferred_freq - freq_infer = False + freq_infer = False result = cls._simple_new(values, freq=freq) # check that we are matching freqs diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 6afb4d3efd380..020877d67f291 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -455,10 +455,10 @@ def test_td64arr_add_sub_timestamp(self, box_with_array): ts = Timestamp('2012-01-01') # TODO: parametrize over types of datetime scalar? - tdarr = timedelta_range('1 day', periods=3) + tdi = timedelta_range('1 day', periods=3) expected = pd.date_range('2012-01-02', periods=3) - tdarr = tm.box_expected(tdarr, box_with_array) + tdarr = tm.box_expected(tdi, box_with_array) expected = tm.box_expected(expected, box_with_array) tm.assert_equal(ts + tdarr, expected) @@ -1450,12 +1450,13 @@ def test_td64arr_div_numeric_array(self, box_with_array, vector, dtype): with pytest.raises(TypeError, match=pattern): vector / tdser - if (not isinstance(vector, pd.Index) and - box_with_array is not pd.DataFrame): + if not isinstance(vector, pd.Index): # Index.__rdiv__ won't try to operate elementwise, just raises - # DataFrame casts just-NaT object column to datetime64 result = tdser / vector.astype(object) - expected = [tdser[n] / vector[n] for n in range(len(tdser))] + if box_with_array is pd.DataFrame: + expected = [tdser.iloc[0, n] / vector[n] for n in range(len(vector))] + else: + expected = [tdser[n] / vector[n] for n in range(len(tdser))] expected = tm.box_expected(expected, xbox) tm.assert_equal(result, expected) From d72bf9053803d1a9239682fd47b4a0ca7aaa0c06 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 28 Nov 2018 18:49:07 -0800 Subject: [PATCH 14/14] flake8 fixup --- pandas/tests/arithmetic/test_timedelta64.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 020877d67f291..81e7062c23fbe 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1454,7 +1454,8 @@ def test_td64arr_div_numeric_array(self, box_with_array, vector, dtype): # Index.__rdiv__ won't try to operate elementwise, just raises result = tdser / vector.astype(object) if box_with_array is pd.DataFrame: - expected = [tdser.iloc[0, n] / vector[n] for n in range(len(vector))] + expected = [tdser.iloc[0, n] / vector[n] + for n in range(len(vector))] else: expected = [tdser[n] / vector[n] for n in range(len(tdser))] expected = tm.box_expected(expected, xbox)