diff --git a/doc/source/whatsnew/v0.17.0.txt b/doc/source/whatsnew/v0.17.0.txt index c0b26ad0c03d7..f88e5c0a11f9f 100644 --- a/doc/source/whatsnew/v0.17.0.txt +++ b/doc/source/whatsnew/v0.17.0.txt @@ -309,6 +309,8 @@ Other enhancements - ``DataFrame.apply`` will return a Series of dicts if the passed function returns a dict and ``reduce=True`` (:issue:`8735`). +- ``PeriodIndex`` now supports arithmetic with ``np.ndarray`` (:issue:`10638`) + - ``concat`` will now use existing Series names if provided (:issue:`10698`). .. ipython:: python @@ -333,6 +335,7 @@ Other enhancements pd.concat([foo, bar, baz], 1) + .. _whatsnew_0170.api: .. _whatsnew_0170.api_breaking: @@ -1005,3 +1008,5 @@ Bug Fixes - Bug when constructing ``DataFrame`` where passing a dictionary with only scalar values and specifying columns did not raise an error (:issue:`10856`) - Bug in ``.var()`` causing roundoff errors for highly similar values (:issue:`10242`) - Bug in ``DataFrame.plot(subplots=True)`` with duplicated columns outputs incorrect result (:issue:`10962`) +- Bug in ``Index`` arithmetic may result in incorrect class (:issue:`10638`) + diff --git a/pandas/core/index.py b/pandas/core/index.py index ef167489435b3..c64e181f4c721 100644 --- a/pandas/core/index.py +++ b/pandas/core/index.py @@ -273,7 +273,12 @@ def __array_wrap__(self, result, context=None): """ Gets called after a ufunc """ - return self._shallow_copy(result) + if is_bool_dtype(result): + return result + + attrs = self._get_attributes_dict() + attrs = self._maybe_update_attributes(attrs) + return Index(result, **attrs) @cache_readonly def dtype(self): @@ -2809,6 +2814,10 @@ def invalid_op(self, other=None): cls.__abs__ = _make_invalid_op('__abs__') cls.__inv__ = _make_invalid_op('__inv__') + def _maybe_update_attributes(self, attrs): + """ Update Index attributes (e.g. freq) depending on op """ + return attrs + @classmethod def _add_numeric_methods(cls): """ add in numeric methods """ @@ -2849,7 +2858,9 @@ def _evaluate_numeric_binop(self, other): if reversed: values, other = other, values - return self._shallow_copy(op(values, other)) + attrs = self._get_attributes_dict() + attrs = self._maybe_update_attributes(attrs) + return Index(op(values, other), **attrs) return _evaluate_numeric_binop @@ -2861,8 +2872,9 @@ def _evaluate_numeric_unary(self): if not self._is_numeric_dtype: raise TypeError("cannot evaluate a numeric op {opstr} for type: {typ}".format(opstr=opstr, typ=type(self))) - - return self._shallow_copy(op(self.values)) + attrs = self._get_attributes_dict() + attrs = self._maybe_update_attributes(attrs) + return Index(op(self.values), **attrs) return _evaluate_numeric_unary diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 9b0d6e9db1106..8a879a4de248b 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -613,7 +613,8 @@ def wrapper(left, right, name=name, na_op=na_op): else: # scalars if hasattr(lvalues, 'values') and not isinstance(lvalues, pd.DatetimeIndex): - lvalues = lvalues.values + lvalues = lvalues.values + return left._constructor(wrap_results(na_op(lvalues, rvalues)), index=left.index, name=left.name, dtype=dtype) diff --git a/pandas/tests/test_index.py b/pandas/tests/test_index.py index 6e7a72360ab67..36bc0755f9a6a 100644 --- a/pandas/tests/test_index.py +++ b/pandas/tests/test_index.py @@ -509,6 +509,56 @@ def test_equals_op(self): tm.assert_numpy_array_equal(index_a == item, expected3) tm.assert_numpy_array_equal(series_a == item, expected3) + def test_numpy_ufuncs(self): + # test ufuncs of numpy 1.9.2. see: + # http://docs.scipy.org/doc/numpy/reference/ufuncs.html + + # some functions are skipped because it may return different result + # for unicode input depending on numpy version + + for name, idx in compat.iteritems(self.indices): + for func in [np.exp, np.exp2, np.expm1, np.log, np.log2, np.log10, + np.log1p, np.sqrt, np.sin, np.cos, + np.tan, np.arcsin, np.arccos, np.arctan, + np.sinh, np.cosh, np.tanh, np.arcsinh, np.arccosh, + np.arctanh, np.deg2rad, np.rad2deg]: + if isinstance(idx, pd.tseries.base.DatetimeIndexOpsMixin): + # raise TypeError or ValueError (PeriodIndex) + # PeriodIndex behavior should be changed in future version + with tm.assertRaises(Exception): + func(idx) + elif isinstance(idx, (Float64Index, Int64Index)): + # coerces to float (e.g. np.sin) + result = func(idx) + exp = Index(func(idx.values), name=idx.name) + self.assert_index_equal(result, exp) + self.assertIsInstance(result, pd.Float64Index) + else: + # raise AttributeError or TypeError + if len(idx) == 0: + continue + else: + with tm.assertRaises(Exception): + func(idx) + + for func in [np.isfinite, np.isinf, np.isnan, np.signbit]: + if isinstance(idx, pd.tseries.base.DatetimeIndexOpsMixin): + # raise TypeError or ValueError (PeriodIndex) + with tm.assertRaises(Exception): + func(idx) + elif isinstance(idx, (Float64Index, Int64Index)): + # results in bool array + result = func(idx) + exp = func(idx.values) + self.assertIsInstance(result, np.ndarray) + tm.assertNotIsInstance(result, Index) + else: + if len(idx) == 0: + continue + else: + with tm.assertRaises(Exception): + func(idx) + class TestIndex(Base, tm.TestCase): _holder = Index @@ -2848,6 +2898,41 @@ def test_slice_keep_name(self): idx = Int64Index([1, 2], name='asdf') self.assertEqual(idx.name, idx[1:].name) + def test_ufunc_coercions(self): + idx = pd.Int64Index([1, 2, 3, 4, 5], name='x') + + result = np.sqrt(idx) + tm.assertIsInstance(result, Float64Index) + exp = pd.Float64Index(np.sqrt(np.array([1, 2, 3, 4, 5])), name='x') + tm.assert_index_equal(result, exp) + + result = np.divide(idx, 2.) + tm.assertIsInstance(result, Float64Index) + exp = pd.Float64Index([0.5, 1., 1.5, 2., 2.5], name='x') + tm.assert_index_equal(result, exp) + + # _evaluate_numeric_binop + result = idx + 2. + tm.assertIsInstance(result, Float64Index) + exp = pd.Float64Index([3., 4., 5., 6., 7.], name='x') + tm.assert_index_equal(result, exp) + + result = idx - 2. + tm.assertIsInstance(result, Float64Index) + exp = pd.Float64Index([-1., 0., 1., 2., 3.], name='x') + tm.assert_index_equal(result, exp) + + result = idx * 1. + tm.assertIsInstance(result, Float64Index) + exp = pd.Float64Index([1., 2., 3., 4., 5.], name='x') + tm.assert_index_equal(result, exp) + + result = idx / 2. + tm.assertIsInstance(result, Float64Index) + exp = pd.Float64Index([0.5, 1., 1.5, 2., 2.5], name='x') + tm.assert_index_equal(result, exp) + + class DatetimeLike(Base): def test_str(self): @@ -3101,7 +3186,9 @@ def test_get_loc(self): tolerance=timedelta(1)), 1) with tm.assertRaisesRegexp(ValueError, 'must be convertible'): idx.get_loc('2000-01-10', method='nearest', tolerance='foo') - with tm.assertRaisesRegexp(ValueError, 'different freq'): + + msg = 'Input has different freq from PeriodIndex\\(freq=D\\)' + with tm.assertRaisesRegexp(ValueError, msg): idx.get_loc('2000-01-10', method='nearest', tolerance='1 hour') with tm.assertRaises(KeyError): idx.get_loc('2000-01-10', method='nearest', tolerance='1 day') @@ -3119,7 +3206,8 @@ def test_get_indexer(self): idx.get_indexer(target, 'nearest', tolerance='1 hour'), [0, -1, 1]) - with self.assertRaisesRegexp(ValueError, 'different freq'): + msg = 'Input has different freq from PeriodIndex\\(freq=H\\)' + with self.assertRaisesRegexp(ValueError, msg): idx.get_indexer(target, 'nearest', tolerance='1 minute') tm.assert_numpy_array_equal( @@ -3215,6 +3303,44 @@ def test_numeric_compat(self): def test_pickle_compat_construction(self): pass + def test_ufunc_coercions(self): + # normal ops are also tested in tseries/test_timedeltas.py + idx = TimedeltaIndex(['2H', '4H', '6H', '8H', '10H'], + freq='2H', name='x') + + for result in [idx * 2, np.multiply(idx, 2)]: + tm.assertIsInstance(result, TimedeltaIndex) + exp = TimedeltaIndex(['4H', '8H', '12H', '16H', '20H'], + freq='4H', name='x') + tm.assert_index_equal(result, exp) + self.assertEqual(result.freq, '4H') + + for result in [idx / 2, np.divide(idx, 2)]: + tm.assertIsInstance(result, TimedeltaIndex) + exp = TimedeltaIndex(['1H', '2H', '3H', '4H', '5H'], + freq='H', name='x') + tm.assert_index_equal(result, exp) + self.assertEqual(result.freq, 'H') + + idx = TimedeltaIndex(['2H', '4H', '6H', '8H', '10H'], + freq='2H', name='x') + for result in [ - idx, np.negative(idx)]: + tm.assertIsInstance(result, TimedeltaIndex) + exp = TimedeltaIndex(['-2H', '-4H', '-6H', '-8H', '-10H'], + freq='-2H', name='x') + tm.assert_index_equal(result, exp) + self.assertEqual(result.freq, None) + + idx = TimedeltaIndex(['-2H', '-1H', '0H', '1H', '2H'], + freq='H', name='x') + for result in [ abs(idx), np.absolute(idx)]: + tm.assertIsInstance(result, TimedeltaIndex) + exp = TimedeltaIndex(['2H', '1H', '0H', '1H', '2H'], + freq=None, name='x') + tm.assert_index_equal(result, exp) + self.assertEqual(result.freq, None) + + class TestMultiIndex(Base, tm.TestCase): _holder = MultiIndex _multiprocess_can_split_ = True diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py index 4ba15d319dc62..966bd5c8d0ab5 100644 --- a/pandas/tseries/index.py +++ b/pandas/tseries/index.py @@ -1077,15 +1077,6 @@ def _fast_union(self, other): end=max(left_end, right_end), freq=left.offset) - def __array_finalize__(self, obj): - if self.ndim == 0: # pragma: no cover - return self.item() - - self.offset = getattr(obj, 'offset', None) - self.tz = getattr(obj, 'tz', None) - self.name = getattr(obj, 'name', None) - self._reset_identity() - def __iter__(self): """ Return an iterator over the boxed values diff --git a/pandas/tseries/period.py b/pandas/tseries/period.py index 832791fc6933c..888c50e86b7b2 100644 --- a/pandas/tseries/period.py +++ b/pandas/tseries/period.py @@ -19,7 +19,8 @@ import pandas.core.common as com from pandas.core.common import (isnull, _INT64_DTYPE, _maybe_box, _values_from_object, ABCSeries, - is_integer, is_float, is_object_dtype) + is_integer, is_float, is_object_dtype, + is_float_dtype) from pandas import compat from pandas.util.decorators import cache_readonly @@ -307,6 +308,30 @@ def __contains__(self, key): return False return key.ordinal in self._engine + def __array_wrap__(self, result, context=None): + """ + Gets called after a ufunc. Needs additional handling as + PeriodIndex stores internal data as int dtype + + Replace this to __numpy_ufunc__ in future version + """ + if isinstance(context, tuple) and len(context) > 0: + func = context[0] + if (func is np.add): + return self._add_delta(context[1][1]) + elif (func is np.subtract): + return self._add_delta(-context[1][1]) + elif isinstance(func, np.ufunc): + if 'M->M' not in func.types: + msg = "ufunc '{0}' not supported for the PeriodIndex" + # This should be TypeError, but TypeError cannot be raised + # from here because numpy catches. + raise ValueError(msg.format(func.__name__)) + + if com.is_bool_dtype(result): + return result + return PeriodIndex(result, freq=self.freq, name=self.name) + @property def _box_func(self): return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq) @@ -522,7 +547,18 @@ def _maybe_convert_timedelta(self, other): base = frequencies.get_base_alias(freqstr) if base == self.freq.rule_code: return other.n - raise ValueError("Input has different freq from PeriodIndex(freq={0})".format(self.freq)) + elif isinstance(other, np.ndarray): + if com.is_integer_dtype(other): + return other + elif com.is_timedelta64_dtype(other): + offset = frequencies.to_offset(self.freq) + if isinstance(offset, offsets.Tick): + nanos = tslib._delta_to_nanoseconds(other) + offset_nanos = tslib._delta_to_nanoseconds(offset) + if (nanos % offset_nanos).all() == 0: + return nanos // offset_nanos + msg = "Input has different freq from PeriodIndex(freq={0})" + raise ValueError(msg.format(self.freqstr)) def _add_delta(self, other): ordinal_delta = self._maybe_convert_timedelta(other) @@ -775,14 +811,6 @@ def _format_native_types(self, na_rep=u('NaT'), date_format=None, **kwargs): values[imask] = np.array([formatter(dt) for dt in values[imask]]) return values - def __array_finalize__(self, obj): - if not self.ndim: # pragma: no cover - return self.item() - - self.freq = getattr(obj, 'freq', None) - self.name = getattr(obj, 'name', None) - self._reset_identity() - def take(self, indices, axis=0): """ Analogous to ndarray.take diff --git a/pandas/tseries/tdi.py b/pandas/tseries/tdi.py index 984f2a1cec706..0f6355ec93554 100644 --- a/pandas/tseries/tdi.py +++ b/pandas/tseries/tdi.py @@ -278,6 +278,14 @@ def __setstate__(self, state): raise Exception("invalid pickle state") _unpickle_compat = __setstate__ + def _maybe_update_attributes(self, attrs): + """ Update Index attributes (e.g. freq) depending on op """ + freq = attrs.get('freq', None) + if freq is not None: + # no need to infer if freq is None + attrs['freq'] = 'infer' + return attrs + def _add_delta(self, delta): if isinstance(delta, (Tick, timedelta, np.timedelta64)): new_values = self._add_delta_td(delta) @@ -560,14 +568,6 @@ def _fast_union(self, other): else: return left - def __array_finalize__(self, obj): - if self.ndim == 0: # pragma: no cover - return self.item() - - self.name = getattr(obj, 'name', None) - self.freq = getattr(obj, 'freq', None) - self._reset_identity() - def _wrap_union_result(self, other, result): name = self.name if self.name == other.name else None return self._simple_new(result, name=name, freq=None) diff --git a/pandas/tseries/tests/test_base.py b/pandas/tseries/tests/test_base.py index 4c9726bbcf80d..4a72b094917b5 100644 --- a/pandas/tseries/tests/test_base.py +++ b/pandas/tseries/tests/test_base.py @@ -1391,6 +1391,7 @@ def test_add_iadd(self): for o in [pd.offsets.YearBegin(2), pd.offsets.MonthBegin(1), pd.offsets.Minute(), np.timedelta64(365, 'D'), timedelta(365), Timedelta(days=365)]: + msg = 'Input has different freq from PeriodIndex\\(freq=A-DEC\\)' with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): rng + o @@ -1404,7 +1405,8 @@ def test_add_iadd(self): for o in [pd.offsets.YearBegin(2), pd.offsets.MonthBegin(1), pd.offsets.Minute(), np.timedelta64(365, 'D'), timedelta(365), Timedelta(days=365)]: rng = pd.period_range('2014-01', '2016-12', freq='M') - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + msg = 'Input has different freq from PeriodIndex\\(freq=M\\)' + with tm.assertRaisesRegexp(ValueError, msg): rng + o # Tick @@ -1422,7 +1424,8 @@ def test_add_iadd(self): for o in [pd.offsets.YearBegin(2), pd.offsets.MonthBegin(1), pd.offsets.Minute(), np.timedelta64(4, 'h'), timedelta(hours=23), Timedelta('23:00:00')]: rng = pd.period_range('2014-05-01', '2014-05-15', freq='D') - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + msg = 'Input has different freq from PeriodIndex\\(freq=D\\)' + with tm.assertRaisesRegexp(ValueError, msg): rng + o offsets = [pd.offsets.Hour(2), timedelta(hours=2), np.timedelta64(2, 'h'), @@ -1439,9 +1442,10 @@ def test_add_iadd(self): for delta in [pd.offsets.YearBegin(2), timedelta(minutes=30), np.timedelta64(30, 's'), Timedelta(seconds=30)]: rng = pd.period_range('2014-01-01 10:00', '2014-01-05 10:00', freq='H') - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + msg = 'Input has different freq from PeriodIndex\\(freq=H\\)' + with tm.assertRaisesRegexp(ValueError, msg): result = rng + delta - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + with tm.assertRaisesRegexp(ValueError, msg): rng += delta # int @@ -1502,7 +1506,8 @@ def test_sub_isub(self): for o in [pd.offsets.YearBegin(2), pd.offsets.MonthBegin(1), pd.offsets.Minute(), np.timedelta64(365, 'D'), timedelta(365)]: rng = pd.period_range('2014', '2024', freq='A') - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + msg = 'Input has different freq from PeriodIndex\\(freq=A-DEC\\)' + with tm.assertRaisesRegexp(ValueError, msg): rng - o rng = pd.period_range('2014-01', '2016-12', freq='M') @@ -1515,7 +1520,8 @@ def test_sub_isub(self): for o in [pd.offsets.YearBegin(2), pd.offsets.MonthBegin(1), pd.offsets.Minute(), np.timedelta64(365, 'D'), timedelta(365)]: rng = pd.period_range('2014-01', '2016-12', freq='M') - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + msg = 'Input has different freq from PeriodIndex\\(freq=M\\)' + with tm.assertRaisesRegexp(ValueError, msg): rng - o # Tick @@ -1532,7 +1538,8 @@ def test_sub_isub(self): for o in [pd.offsets.YearBegin(2), pd.offsets.MonthBegin(1), pd.offsets.Minute(), np.timedelta64(4, 'h'), timedelta(hours=23)]: rng = pd.period_range('2014-05-01', '2014-05-15', freq='D') - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + msg = 'Input has different freq from PeriodIndex\\(freq=D\\)' + with tm.assertRaisesRegexp(ValueError, msg): rng - o offsets = [pd.offsets.Hour(2), timedelta(hours=2), np.timedelta64(2, 'h'), @@ -1547,9 +1554,10 @@ def test_sub_isub(self): for delta in [pd.offsets.YearBegin(2), timedelta(minutes=30), np.timedelta64(30, 's')]: rng = pd.period_range('2014-01-01 10:00', '2014-01-05 10:00', freq='H') - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + msg = 'Input has different freq from PeriodIndex\\(freq=H\\)' + with tm.assertRaisesRegexp(ValueError, msg): result = rng + delta - with tm.assertRaisesRegexp(ValueError, 'Input has different freq from Period'): + with tm.assertRaisesRegexp(ValueError, msg): rng += delta # int diff --git a/pandas/tseries/tests/test_period.py b/pandas/tseries/tests/test_period.py index 4b5d5dfedeee7..951bb803ef793 100644 --- a/pandas/tseries/tests/test_period.py +++ b/pandas/tseries/tests/test_period.py @@ -2321,6 +2321,17 @@ def test_shift_nat(self): self.assertTrue(result.equals(expected)) self.assertEqual(result.name, expected.name) + def test_shift_ndarray(self): + idx = PeriodIndex(['2011-01', '2011-02', 'NaT', '2011-04'], freq='M', name='idx') + result = idx.shift(np.array([1, 2, 3, 4])) + expected = PeriodIndex(['2011-02', '2011-04', 'NaT', '2011-08'], freq='M', name='idx') + self.assertTrue(result.equals(expected)) + + idx = PeriodIndex(['2011-01', '2011-02', 'NaT', '2011-04'], freq='M', name='idx') + result = idx.shift(np.array([1, -2, 3, -4])) + expected = PeriodIndex(['2011-02', '2010-12', 'NaT', '2010-12'], freq='M', name='idx') + self.assertTrue(result.equals(expected)) + def test_asfreq(self): pi1 = PeriodIndex(freq='A', start='1/1/2001', end='1/1/2001') pi2 = PeriodIndex(freq='Q', start='1/1/2001', end='1/1/2001') @@ -3337,6 +3348,53 @@ def test_pi_ops_nat(self): with tm.assertRaisesRegexp(TypeError, msg): idx + "str" + def test_pi_ops_array(self): + idx = PeriodIndex(['2011-01', '2011-02', 'NaT', '2011-04'], freq='M', name='idx') + result = idx + np.array([1, 2, 3, 4]) + exp = PeriodIndex(['2011-02', '2011-04', 'NaT', '2011-08'], freq='M', name='idx') + self.assert_index_equal(result, exp) + + result = np.add(idx, np.array([4, -1, 1, 2])) + exp = PeriodIndex(['2011-05', '2011-01', 'NaT', '2011-06'], freq='M', name='idx') + self.assert_index_equal(result, exp) + + result = idx - np.array([1, 2, 3, 4]) + exp = PeriodIndex(['2010-12', '2010-12', 'NaT', '2010-12'], freq='M', name='idx') + self.assert_index_equal(result, exp) + + result = np.subtract(idx, np.array([3, 2, 3, -2])) + exp = PeriodIndex(['2010-10', '2010-12', 'NaT', '2011-06'], freq='M', name='idx') + self.assert_index_equal(result, exp) + + # incompatible freq + msg = "Input has different freq from PeriodIndex\(freq=M\)" + with tm.assertRaisesRegexp(ValueError, msg): + idx + np.array([np.timedelta64(1, 'D')] * 4) + + idx = PeriodIndex(['2011-01-01 09:00', '2011-01-01 10:00', 'NaT', + '2011-01-01 12:00'], freq='H', name='idx') + result = idx + np.array([np.timedelta64(1, 'D')] * 4) + exp = PeriodIndex(['2011-01-02 09:00', '2011-01-02 10:00', 'NaT', + '2011-01-02 12:00'], freq='H', name='idx') + self.assert_index_equal(result, exp) + + result = idx - np.array([np.timedelta64(1, 'h')] * 4) + exp = PeriodIndex(['2011-01-01 08:00', '2011-01-01 09:00', 'NaT', + '2011-01-01 11:00'], freq='H', name='idx') + self.assert_index_equal(result, exp) + + msg = "Input has different freq from PeriodIndex\(freq=H\)" + with tm.assertRaisesRegexp(ValueError, msg): + idx + np.array([np.timedelta64(1, 's')] * 4) + + idx = PeriodIndex(['2011-01-01 09:00:00', '2011-01-01 10:00:00', 'NaT', + '2011-01-01 12:00:00'], freq='S', name='idx') + result = idx + np.array([np.timedelta64(1, 'h'), np.timedelta64(30, 's'), + np.timedelta64(2, 'h'), np.timedelta64(15, 'm')]) + exp = PeriodIndex(['2011-01-01 10:00:00', '2011-01-01 10:00:30', 'NaT', + '2011-01-01 12:15:00'], freq='S', name='idx') + self.assert_index_equal(result, exp) + class TestPeriodRepresentation(tm.TestCase): """ diff --git a/pandas/tseries/tests/test_timedeltas.py b/pandas/tseries/tests/test_timedeltas.py index a2a8f1484f70e..d3d09356648b0 100644 --- a/pandas/tseries/tests/test_timedeltas.py +++ b/pandas/tseries/tests/test_timedeltas.py @@ -960,7 +960,7 @@ def test_total_seconds(self): rng = timedelta_range('1 days, 10:11:12.100123456', periods=2, freq='s') expt = [1*86400+10*3600+11*60+12+100123456./1e9,1*86400+10*3600+11*60+13+100123456./1e9] assert_allclose(rng.total_seconds(), expt, atol=1e-10, rtol=0) - + # test Series s = Series(rng) s_expt = Series(expt,index=[0,1]) @@ -970,7 +970,7 @@ def test_total_seconds(self): s[1] = np.nan s_expt = Series([1*86400+10*3600+11*60+12+100123456./1e9,np.nan],index=[0,1]) tm.assert_series_equal(s.dt.total_seconds(),s_expt) - + # with both nat s = Series([np.nan,np.nan], dtype='timedelta64[ns]') tm.assert_series_equal(s.dt.total_seconds(),Series([np.nan,np.nan],index=[0,1])) @@ -980,7 +980,7 @@ def test_total_seconds_scalar(self): rng = Timedelta('1 days, 10:11:12.100123456') expt = 1*86400+10*3600+11*60+12+100123456./1e9 assert_allclose(rng.total_seconds(), expt, atol=1e-10, rtol=0) - + rng = Timedelta(np.nan) self.assertTrue(np.isnan(rng.total_seconds())) @@ -1513,6 +1513,44 @@ def test_slice_with_zero_step_raises(self): self.assertRaisesRegexp(ValueError, 'slice step cannot be zero', lambda: ts.ix[::0]) + def test_tdi_ops_attributes(self): + rng = timedelta_range('2 days', periods=5, freq='2D', name='x') + + result = rng + 1 + exp = timedelta_range('4 days', periods=5, freq='2D', name='x') + tm.assert_index_equal(result, exp) + self.assertEqual(result.freq, '2D') + + result = rng -2 + exp = timedelta_range('-2 days', periods=5, freq='2D', name='x') + tm.assert_index_equal(result, exp) + self.assertEqual(result.freq, '2D') + + result = rng * 2 + exp = timedelta_range('4 days', periods=5, freq='4D', name='x') + tm.assert_index_equal(result, exp) + self.assertEqual(result.freq, '4D') + + result = rng / 2 + exp = timedelta_range('1 days', periods=5, freq='D', name='x') + tm.assert_index_equal(result, exp) + self.assertEqual(result.freq, 'D') + + result = - rng + exp = timedelta_range('-2 days', periods=5, freq='-2D', name='x') + tm.assert_index_equal(result, exp) + # tdi doesn't infer negative freq + self.assertEqual(result.freq, None) + + rng = pd.timedelta_range('-2 days', periods=5, freq='D', name='x') + + result = abs(rng) + exp = TimedeltaIndex(['2 days', '1 days', '0 days', '1 days', + '2 days'], name='x') + tm.assert_index_equal(result, exp) + # tdi doesn't infer negative freq + self.assertEqual(result.freq, None) + if __name__ == '__main__': nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'], diff --git a/pandas/tslib.pyx b/pandas/tslib.pyx index 7741747103c55..def3764c1113c 100644 --- a/pandas/tslib.pyx +++ b/pandas/tslib.pyx @@ -1088,6 +1088,8 @@ cdef class _NaT(_Timestamp): def _delta_to_nanoseconds(delta): + if isinstance(delta, np.ndarray): + return delta.astype('m8[ns]').astype('int64') if hasattr(delta, 'nanos'): return delta.nanos if hasattr(delta, 'delta'):