From e2c6727b0031285deb6202bfff0a59f3877636da Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 11 Oct 2018 08:43:39 -0700 Subject: [PATCH 01/13] Move repeat up to array mixin --- pandas/core/arrays/datetimelike.py | 13 ++++++++++++ pandas/core/indexes/datetimelike.py | 12 ----------- pandas/tests/arrays/test_datetimelike.py | 26 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index e4ace2bfe1509..c0ea67975599b 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -13,6 +13,7 @@ from pandas.errors import NullFrequencyError, PerformanceWarning from pandas import compat +from pandas.compat.numpy import function as nv from pandas.tseries import frequencies from pandas.tseries.offsets import Tick, DateOffset @@ -207,6 +208,18 @@ def astype(self, dtype, copy=True): return self._box_values(self.asi8) return super(DatetimeLikeArrayMixin, self).astype(dtype, copy) + def repeat(self, repeats, *args, **kwargs): + """ + Analogous to ndarray.repeat + """ + nv.validate_repeat(args, kwargs) + if is_period_dtype(self): + freq = self.freq + else: + freq = None + return self._shallow_copy(self.asi8.repeat(repeats), + freq=freq) + # ------------------------------------------------------------------ # Null Handling diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 1ec30ecbb3a3b..d32bf8b22e7dd 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -613,18 +613,6 @@ def isin(self, values): return algorithms.isin(self.asi8, values.asi8) - def repeat(self, repeats, *args, **kwargs): - """ - Analogous to ndarray.repeat - """ - nv.validate_repeat(args, kwargs) - if is_period_dtype(self): - freq = self.freq - else: - freq = None - return self._shallow_copy(self.asi8.repeat(repeats), - freq=freq) - @Appender(_index_shared_docs['where'] % _index_doc_kwargs) def where(self, cond, other=None): other = _ensure_datetimelike_to_i8(other, to_utc=True) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index 6bb4241451b3f..4a6bd2b3b78d1 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -67,6 +67,19 @@ def test_astype_object(self, tz_naive_fixture): assert asobj.dtype == 'O' assert list(asobj) == list(dti) + # TODO: share this between Datetime/Timedelta/Period Array tests + def test_repeat(self, datetime_index): + dti = datetime_index + arr = DatetimeArrayMixin(dti) + + expected = dti.repeat(3) + result = arr.repeat(3) + assert isinstance(result, DatetimeArrayMixin) + + # placeholder until these become actual EA subclasses and we can use + # an EA-specific tm.assert_ function + tm.assert_index_equal(pd.Index(result), pd.Index(expected)) + @pytest.mark.parametrize('freqstr', ['D', 'B', 'W', 'M', 'Q', 'Y']) def test_to_period(self, datetime_index, freqstr): dti = datetime_index @@ -156,6 +169,19 @@ def test_to_timestamp(self, how, period_index): # an EA-specific tm.assert_ function tm.assert_index_equal(pd.Index(result), pd.Index(expected)) + # TODO: share this between Datetime/Timedelta/Period Array tests + def test_repeat(self, period_index): + pi = period_index + arr = PeriodArrayMixin(pi) + + expected = pi.repeat(3) + result = arr.repeat(3) + assert isinstance(result, PeriodArrayMixin) + + # placeholder until these become actual EA subclasses and we can use + # an EA-specific tm.assert_ function + tm.assert_index_equal(pd.Index(result), pd.Index(expected)) + @pytest.mark.parametrize('propname', pd.PeriodIndex._bool_ops) def test_bool_properties(self, period_index, propname): # in this case _bool_ops is just `is_leap_year` From 5c32348c8ac8419c9e898a4adea561e1a8076a0e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 11 Oct 2018 08:45:13 -0700 Subject: [PATCH 02/13] Move tolist up --- pandas/core/arrays/datetimelike.py | 6 ++++++ pandas/core/indexes/datetimelike.py | 6 ------ pandas/tests/arrays/test_datetimelike.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index c0ea67975599b..fb118ebabdae0 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -220,6 +220,12 @@ def repeat(self, repeats, *args, **kwargs): return self._shallow_copy(self.asi8.repeat(repeats), freq=freq) + def tolist(self): + """ + return a list of the underlying data + """ + return list(self.astype(object)) + # ------------------------------------------------------------------ # Null Handling diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index d32bf8b22e7dd..864f289edc645 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -415,12 +415,6 @@ def _convert_tolerance(self, tolerance, target): 'target index size') return tolerance - def tolist(self): - """ - return a list of the underlying data - """ - return list(self.astype(object)) - def min(self, axis=None, *args, **kwargs): """ Return the minimum value of the Index or minimum along diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index 4a6bd2b3b78d1..6e313fa115ed6 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -80,6 +80,14 @@ def test_repeat(self, datetime_index): # an EA-specific tm.assert_ function tm.assert_index_equal(pd.Index(result), pd.Index(expected)) + def test_tolist(self, datetime_index): + dti = datetime_index + arr = DatetimeArrayMixin(dti) + + expected = dti.tolist() + result = arr.tolist() + assert expected == result + @pytest.mark.parametrize('freqstr', ['D', 'B', 'W', 'M', 'Q', 'Y']) def test_to_period(self, datetime_index, freqstr): dti = datetime_index @@ -182,6 +190,14 @@ def test_repeat(self, period_index): # an EA-specific tm.assert_ function tm.assert_index_equal(pd.Index(result), pd.Index(expected)) + def test_tolist(self, period_index): + pi = period_index + arr = PeriodArrayMixin(pi) + + expected = pi.tolist() + result = arr.tolist() + assert expected == result + @pytest.mark.parametrize('propname', pd.PeriodIndex._bool_ops) def test_bool_properties(self, period_index, propname): # in this case _bool_ops is just `is_leap_year` From ba5c2c13b3cfe0eb485b047829baa4b43dbbace0 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 11 Oct 2018 08:50:05 -0700 Subject: [PATCH 03/13] Move to_perioddelta up --- pandas/core/arrays/datetimes.py | 18 ++++++++++++++++++ pandas/core/indexes/datetimes.py | 19 ++++--------------- pandas/tests/arrays/test_datetimelike.py | 13 +++++++++++++ 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 7daaa8de1734f..c3cc37509337f 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -829,6 +829,24 @@ def to_period(self, freq=None): return PeriodArrayMixin(self.values, freq=freq) + def to_perioddelta(self, freq): + """ + Calculate TimedeltaArray of difference between index + values and index converted to PeriodArray at specified + freq. Used for vectorized offsets + + Parameters + ---------- + freq: Period frequency + + Returns + ------- + TimedeltaArray/Index + """ + from pandas.core.arrays.timedeltas import TimedeltaArrayMixin + i8delta = self.asi8 - self.to_period(freq).to_timestamp().asi8 + return TimedeltaArrayMixin(i8delta) + # ----------------------------------------------------------------- # Properties - Vectorized Timestamp Properties/Methods diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index e40ceadc1a083..e4d56005c559d 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -752,22 +752,11 @@ def union(self, other): result.freq = to_offset(result.inferred_freq) return result + @Appender(DatetimeArrayMixin.to_perioddelta.__doc__) def to_perioddelta(self, freq): - """ - Calculate TimedeltaIndex of difference between index - values and index converted to periodIndex at specified - freq. Used for vectorized offsets - - Parameters - ---------- - freq: Period frequency - - Returns - ------- - y: TimedeltaIndex - """ - return to_timedelta(self.asi8 - self.to_period(freq) - .to_timestamp().asi8) + from pandas import TimedeltaIndex + result = DatetimeArrayMixin.to_perioddelta(self, freq) + return TimedeltaIndex(result) def union_many(self, others): """ diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index 6e313fa115ed6..f9046e737c894 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -88,6 +88,19 @@ def test_tolist(self, datetime_index): result = arr.tolist() assert expected == result + @pytest.mark.parametrize('freqstr', ['D', 'B', 'W', 'M', 'Q', 'Y']) + def test_to_perioddelta(self, freqstr) + dti = datetime_index + arr = DatetimeArrayMixin(dti) + + expected = dti.to_perioddelta(freq=freqstr) + result = arr.to_perioddelta(freq=freqstr) + assert isinstance(result, TimedeltaArrayMixin) + + # placeholder until these become actual EA subclasses and we can use + # an EA-specific tm.assert_ function + tm.assert_index_equal(pd.Index(result), pd.Index(expected)) + @pytest.mark.parametrize('freqstr', ['D', 'B', 'W', 'M', 'Q', 'Y']) def test_to_period(self, datetime_index, freqstr): dti = datetime_index From e1e05a9ace4304dbea9b63887afba655aa232d84 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 11 Oct 2018 08:54:41 -0700 Subject: [PATCH 04/13] Avoid passing object dtype to simple_new --- pandas/core/indexes/period.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index f151389b02463..bdf84feca1fd7 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -327,9 +327,9 @@ def __array_wrap__(self, result, context=None): """ if isinstance(context, tuple) and len(context) > 0: func = context[0] - if (func is np.add): + if func is np.add: pass - elif (func is np.subtract): + elif func is np.subtract: name = self.name left = context[1][0] right = context[1][1] @@ -350,7 +350,7 @@ def __array_wrap__(self, result, context=None): return result # the result is object dtype array of Period # cannot pass _simple_new as it is - return self._shallow_copy(result, freq=self.freq, name=self.name) + return type(self)(result, freq=self.freq, name=self.name) @property def size(self): From d3ca5e9cbda36d77321ec553b3320bca6bd5a709 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 11 Oct 2018 17:02:50 -0700 Subject: [PATCH 05/13] de-duplicate wrapping code --- pandas/core/indexes/datetimelike.py | 56 +++++++++++ pandas/core/indexes/datetimes.py | 114 +++++++---------------- pandas/core/indexes/period.py | 48 ++++------ pandas/core/indexes/timedeltas.py | 33 ++----- pandas/tests/arrays/test_datetimelike.py | 2 +- 5 files changed, 115 insertions(+), 138 deletions(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 864f289edc645..b58dd8d7854e0 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -726,3 +726,59 @@ def wrap_arithmetic_op(self, other, result): res_name = ops.get_op_result_name(self, other) result.name = res_name return result + + +def wrap_array_method(method, pin_name=False): + """ + Wrap a DatetimeArray/TimedeltaArray/PeriodArray method so that the + returned object is an Index subclass instead of ndarray or ExtensionArray + subclass. + + Parameters + ---------- + method : method of Datetime/Timedelta/Period Array class + pin_name : bool + Whether to set name=self.name on the output Index + + Returns + ------- + method + """ + def index_method(self, *args, **kwargs): + result = method(self, *args, **kwargs) + + # Index.__new__ will choose the appropriate subclass to return + result = Index(result) + if pin_name: + result.name = self.name + return result + + index_method.__name__ = method.__name__ + index_method.__doc__ = method.__doc__ + return index_method + + +def wrap_field_accessor(prop): + """ + Wrap a DatetimeArray/TimedeltaArray/PeriodArray array-returning property + to return an Index subclass instead of ndarray or ExtensionArray subclass. + + Parameters + ---------- + prop : property + + Returns + ------- + new_prop : property + """ + fget = prop.fget + + def f(self): + result = fget(self) + if is_bool_dtype(result): + return result + return Index(result, name=self.name) + + f.__name__ = fget.__name__ + f.__doc__ = fget.__doc__ + return property(f) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index e4d56005c559d..7d0eaf020871b 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -38,7 +38,8 @@ import pandas.compat as compat from pandas.tseries.frequencies import to_offset, Resolution from pandas.core.indexes.datetimelike import ( - DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin) + DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, + wrap_array_method, wrap_field_accessor) from pandas.tseries.offsets import ( generate_range, CDay, prefix_mapping) @@ -56,32 +57,7 @@ # -------- some conversion wrapper functions -def _wrap_field_accessor(name): - fget = getattr(DatetimeArrayMixin, name).fget - - def f(self): - result = fget(self) - if is_bool_dtype(result): - return result - return Index(result, name=self.name) - - f.__name__ = name - f.__doc__ = fget.__doc__ - return property(f) - - -def _wrap_in_index(name): - meth = getattr(DatetimeArrayMixin, name) - - def func(self, *args, **kwargs): - result = meth(self, *args, **kwargs) - return Index(result, name=self.name) - - func.__doc__ = meth.__doc__ - func.__name__ = name - return func - - +# TODO: can we get away without this? I think we still need to call compat.set_function_name def _dt_index_cmp(cls, op): """ Wrap comparison operations to convert datetime-like to datetime64 @@ -90,9 +66,7 @@ def _dt_index_cmp(cls, op): def wrapper(self, other): result = getattr(DatetimeArrayMixin, opname)(self, other) - if is_bool_dtype(result): - return result - return Index(result) + return result return compat.set_function_name(wrapper, opname, cls) @@ -674,13 +648,6 @@ def to_series(self, keep_tz=False, index=None, name=None): return Series(values, index=index, name=name) - @Appender(DatetimeArrayMixin.to_period.__doc__) - def to_period(self, freq=None): - from pandas.core.indexes.period import PeriodIndex - - result = DatetimeArrayMixin.to_period(self, freq=freq) - return PeriodIndex(result, name=self.name) - def snap(self, freq='S'): """ Snap time stamps to nearest occurring frequency @@ -752,12 +719,6 @@ def union(self, other): result.freq = to_offset(result.inferred_freq) return result - @Appender(DatetimeArrayMixin.to_perioddelta.__doc__) - def to_perioddelta(self, freq): - from pandas import TimedeltaIndex - result = DatetimeArrayMixin.to_perioddelta(self, freq) - return TimedeltaIndex(result) - def union_many(self, others): """ A bit of a hack to accelerate unioning a collection of indexes @@ -1262,38 +1223,32 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): else: raise - year = _wrap_field_accessor('year') - month = _wrap_field_accessor('month') - day = _wrap_field_accessor('day') - hour = _wrap_field_accessor('hour') - minute = _wrap_field_accessor('minute') - second = _wrap_field_accessor('second') - microsecond = _wrap_field_accessor('microsecond') - nanosecond = _wrap_field_accessor('nanosecond') - weekofyear = _wrap_field_accessor('weekofyear') + year = wrap_field_accessor(DatetimeArrayMixin.year) + month = wrap_field_accessor(DatetimeArrayMixin.month) + day = wrap_field_accessor(DatetimeArrayMixin.day) + hour = wrap_field_accessor(DatetimeArrayMixin.hour) + minute = wrap_field_accessor(DatetimeArrayMixin.minute) + second = wrap_field_accessor(DatetimeArrayMixin.second) + microsecond = wrap_field_accessor(DatetimeArrayMixin.microsecond) + nanosecond = wrap_field_accessor(DatetimeArrayMixin.nanosecond) + weekofyear = wrap_field_accessor(DatetimeArrayMixin.weekofyear) week = weekofyear - dayofweek = _wrap_field_accessor('dayofweek') + dayofweek = wrap_field_accessor(DatetimeArrayMixin.dayofweek) weekday = dayofweek - weekday_name = _wrap_field_accessor('weekday_name') + weekday_name = wrap_field_accessor(DatetimeArrayMixin.weekday_name) - dayofyear = _wrap_field_accessor('dayofyear') - quarter = _wrap_field_accessor('quarter') - days_in_month = _wrap_field_accessor('days_in_month') + dayofyear = wrap_field_accessor(DatetimeArrayMixin.dayofyear) + quarter = wrap_field_accessor(DatetimeArrayMixin.quarter) + days_in_month = wrap_field_accessor(DatetimeArrayMixin.days_in_month) daysinmonth = days_in_month - is_month_start = _wrap_field_accessor('is_month_start') - is_month_end = _wrap_field_accessor('is_month_end') - is_quarter_start = _wrap_field_accessor('is_quarter_start') - is_quarter_end = _wrap_field_accessor('is_quarter_end') - is_year_start = _wrap_field_accessor('is_year_start') - is_year_end = _wrap_field_accessor('is_year_end') - is_leap_year = _wrap_field_accessor('is_leap_year') - - @Appender(DatetimeArrayMixin.normalize.__doc__) - def normalize(self): - result = DatetimeArrayMixin.normalize(self) - result.name = self.name - return result + is_month_start = wrap_field_accessor(DatetimeArrayMixin.is_month_start) + is_month_end = wrap_field_accessor(DatetimeArrayMixin.is_month_end) + is_quarter_start = wrap_field_accessor(DatetimeArrayMixin.is_quarter_start) + is_quarter_end = wrap_field_accessor(DatetimeArrayMixin.is_quarter_end) + is_year_start = wrap_field_accessor(DatetimeArrayMixin.is_year_start) + is_year_end = wrap_field_accessor(DatetimeArrayMixin.is_year_end) + is_leap_year = wrap_field_accessor(DatetimeArrayMixin.is_leap_year) @Substitution(klass='DatetimeIndex') @Appender(_shared_docs['searchsorted']) @@ -1481,17 +1436,14 @@ def indexer_between_time(self, start_time, end_time, include_start=True, return mask.nonzero()[0] - def to_julian_date(self): - """ - Convert DatetimeIndex to Float64Index of Julian Dates. - 0 Julian date is noon January 1, 4713 BC. - http://en.wikipedia.org/wiki/Julian_day - """ - result = DatetimeArrayMixin.to_julian_date(self) - return Float64Index(result) - - month_name = _wrap_in_index("month_name") - day_name = _wrap_in_index("day_name") + to_perioddelta = wrap_array_method(DatetimeArrayMixin.to_perioddelta, + False) + to_period = wrap_array_method(DatetimeArrayMixin.to_period, True) + normalize = wrap_array_method(DatetimeArrayMixin.normalize, True) + to_julian_date = wrap_array_method(DatetimeArrayMixin.to_julian_date, + False) + month_name = wrap_array_method(DatetimeArrayMixin.month_name, True) + day_name = wrap_array_method(DatetimeArrayMixin.day_name, True) DatetimeIndex._add_comparison_methods() diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index bdf84feca1fd7..9ba8c15d1756b 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -20,7 +20,9 @@ from pandas.tseries.frequencies import get_freq_code as _gfc from pandas.core.indexes.datetimes import DatetimeIndex, Int64Index, Index -from pandas.core.indexes.datetimelike import DatelikeOps, DatetimeIndexOpsMixin +from pandas.core.indexes.datetimelike import ( + DatelikeOps, DatetimeIndexOpsMixin, + wrap_array_method, wrap_field_accessor) from pandas.core.tools.datetimes import parse_time_string from pandas._libs.lib import infer_dtype @@ -43,19 +45,6 @@ _index_doc_kwargs.update( dict(target_klass='PeriodIndex or list of Periods')) - -def _wrap_field_accessor(name): - fget = getattr(PeriodArrayMixin, name).fget - - def f(self): - result = fget(self) - return Index(result, name=self.name) - - f.__name__ = name - f.__doc__ = fget.__doc__ - return property(f) - - # --- Period index sketch @@ -431,22 +420,24 @@ def is_full(self): values = self.asi8 return ((values[1:] - values[:-1]) < 2).all() - year = _wrap_field_accessor('year') - month = _wrap_field_accessor('month') - day = _wrap_field_accessor('day') - hour = _wrap_field_accessor('hour') - minute = _wrap_field_accessor('minute') - second = _wrap_field_accessor('second') - weekofyear = _wrap_field_accessor('week') + year = wrap_field_accessor(PeriodArrayMixin.year) + month = wrap_field_accessor(PeriodArrayMixin.month) + day = wrap_field_accessor(PeriodArrayMixin.day) + hour = wrap_field_accessor(PeriodArrayMixin.hour) + minute = wrap_field_accessor(PeriodArrayMixin.minute) + second = wrap_field_accessor(PeriodArrayMixin.second) + weekofyear = wrap_field_accessor(PeriodArrayMixin.week) week = weekofyear - dayofweek = _wrap_field_accessor('dayofweek') + dayofweek = wrap_field_accessor(PeriodArrayMixin.dayofweek) weekday = dayofweek - dayofyear = day_of_year = _wrap_field_accessor('dayofyear') - quarter = _wrap_field_accessor('quarter') - qyear = _wrap_field_accessor('qyear') - days_in_month = _wrap_field_accessor('days_in_month') + dayofyear = day_of_year = wrap_field_accessor(PeriodArrayMixin.dayofyear) + quarter = wrap_field_accessor(PeriodArrayMixin.quarter) + qyear = wrap_field_accessor(PeriodArrayMixin.qyear) + days_in_month = wrap_field_accessor(PeriodArrayMixin.days_in_month) daysinmonth = days_in_month + to_timestamp = wrap_array_method(PeriodArrayMixin.to_timestamp, True) + @property @Appender(PeriodArrayMixin.start_time.__doc__) def start_time(self): @@ -461,11 +452,6 @@ def _mpl_repr(self): # how to represent ourselves to matplotlib return self.astype(object).values - @Appender(PeriodArrayMixin.to_timestamp.__doc__) - def to_timestamp(self, freq=None, how='start'): - result = PeriodArrayMixin.to_timestamp(self, freq=freq, how=how) - return DatetimeIndex(result, name=self.name) - @property def inferred_type(self): # b/c data is represented as ints make sure we can't have ambiguous diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index ee604f44b98e0..69c9b003e1e8b 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -31,25 +31,14 @@ import pandas.core.dtypes.concat as _concat from pandas.util._decorators import Appender, Substitution from pandas.core.indexes.datetimelike import ( - TimelikeOps, DatetimeIndexOpsMixin, wrap_arithmetic_op) + TimelikeOps, DatetimeIndexOpsMixin, wrap_arithmetic_op, + wrap_array_method, wrap_field_accessor) from pandas.core.tools.timedeltas import ( to_timedelta, _coerce_scalar_to_timedelta_type) from pandas._libs import (lib, index as libindex, join as libjoin, Timedelta, NaT) -def _wrap_field_accessor(name): - fget = getattr(TimedeltaArrayMixin, name).fget - - def f(self): - result = fget(self) - return Index(result, name=self.name) - - f.__name__ = name - f.__doc__ = fget.__doc__ - return property(f) - - def _td_index_cmp(cls, op): """ Wrap comparison operations to convert timedelta-like to timedelta64 @@ -58,10 +47,7 @@ def _td_index_cmp(cls, op): def wrapper(self, other): result = getattr(TimedeltaArrayMixin, opname)(self, other) - if is_bool_dtype(result): - # support of bool dtype indexers - return result - return Index(result) + return result return compat.set_function_name(wrapper, opname, cls) @@ -269,15 +255,12 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): nat_rep=na_rep, justify='all').get_result() - days = _wrap_field_accessor("days") - seconds = _wrap_field_accessor("seconds") - microseconds = _wrap_field_accessor("microseconds") - nanoseconds = _wrap_field_accessor("nanoseconds") + days = wrap_field_accessor(TimedeltaArrayMixin.days) + seconds = wrap_field_accessor(TimedeltaArrayMixin.seconds) + microseconds = wrap_field_accessor(TimedeltaArrayMixin.microseconds) + nanoseconds = wrap_field_accessor(TimedeltaArrayMixin.nanoseconds) - @Appender(TimedeltaArrayMixin.total_seconds.__doc__) - def total_seconds(self): - result = TimedeltaArrayMixin.total_seconds(self) - return Index(result, name=self.name) + total_seconds = wrap_array_method(TimedeltaArrayMixin.total_seconds, True) @Appender(_index_shared_docs['astype']) def astype(self, dtype, copy=True): diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index f9046e737c894..f8ee59e2eddfe 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -89,7 +89,7 @@ def test_tolist(self, datetime_index): assert expected == result @pytest.mark.parametrize('freqstr', ['D', 'B', 'W', 'M', 'Q', 'Y']) - def test_to_perioddelta(self, freqstr) + def test_to_perioddelta(self, freqstr): dti = datetime_index arr = DatetimeArrayMixin(dti) From b4c2496896e71803cace22d3de186583c0adfd5f Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 12 Oct 2018 09:09:25 -0700 Subject: [PATCH 06/13] de-duplicate wrapping code --- pandas/core/arrays/datetimelike.py | 22 ++++++---------------- pandas/core/indexes/datetimes.py | 28 +--------------------------- pandas/core/indexes/timedeltas.py | 25 +------------------------ 3 files changed, 8 insertions(+), 67 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index fb118ebabdae0..710873ce6fc9b 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -42,7 +42,7 @@ from pandas.util._decorators import deprecate_kwarg -def _make_comparison_op(op, cls): +def _make_comparison_op(cls, op): # TODO: share code with indexes.base version? Main difference is that # the block for MultiIndex was removed here. def cmp_method(self, other): @@ -759,6 +759,9 @@ def __isub__(self, other): # -------------------------------------------------------------- # Comparison Methods + # Called by ExtensionOpsMixin._add_comparison_ops + _create_comparison_method = classmethod(_make_comparison_op) + def _evaluate_compare(self, other, op): """ We have been called because a comparison between @@ -792,21 +795,8 @@ def _evaluate_compare(self, other, op): result[mask] = filler return result - # TODO: get this from ExtensionOpsMixin - @classmethod - def _add_comparison_methods(cls): - """ add in comparison methods """ - # DatetimeArray and TimedeltaArray comparison methods will - # call these as their super(...) methods - cls.__eq__ = _make_comparison_op(operator.eq, cls) - cls.__ne__ = _make_comparison_op(operator.ne, cls) - cls.__lt__ = _make_comparison_op(operator.lt, cls) - cls.__gt__ = _make_comparison_op(operator.gt, cls) - cls.__le__ = _make_comparison_op(operator.le, cls) - cls.__ge__ = _make_comparison_op(operator.ge, cls) - - -DatetimeLikeArrayMixin._add_comparison_methods() + +DatetimeLikeArrayMixin._add_comparison_ops() # ------------------------------------------------------------------- diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 7d0eaf020871b..273f629b63a25 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -54,22 +54,6 @@ from pandas._libs.tslibs import (timezones, conversion, fields, parsing, ccalendar) -# -------- some conversion wrapper functions - - -# TODO: can we get away without this? I think we still need to call compat.set_function_name -def _dt_index_cmp(cls, op): - """ - Wrap comparison operations to convert datetime-like to datetime64 - """ - opname = '__{name}__'.format(name=op.__name__) - - def wrapper(self, other): - result = getattr(DatetimeArrayMixin, opname)(self, other) - return result - - return compat.set_function_name(wrapper, opname, cls) - def _new_DatetimeIndex(cls, d): """ This is called upon unpickling, rather than the default which doesn't @@ -207,16 +191,6 @@ def _join_i8_wrapper(joinf, **kwargs): _left_indexer_unique = _join_i8_wrapper( libjoin.left_join_indexer_unique_int64, with_indexers=False) - @classmethod - def _add_comparison_methods(cls): - """ add in comparison methods """ - cls.__eq__ = _dt_index_cmp(cls, operator.eq) - cls.__ne__ = _dt_index_cmp(cls, operator.ne) - cls.__lt__ = _dt_index_cmp(cls, operator.lt) - cls.__gt__ = _dt_index_cmp(cls, operator.gt) - cls.__le__ = _dt_index_cmp(cls, operator.le) - cls.__ge__ = _dt_index_cmp(cls, operator.ge) - _engine_type = libindex.DatetimeEngine tz = None @@ -1446,7 +1420,7 @@ def indexer_between_time(self, start_time, end_time, include_start=True, day_name = wrap_array_method(DatetimeArrayMixin.day_name, True) -DatetimeIndex._add_comparison_methods() +DatetimeIndex._add_comparison_ops() DatetimeIndex._add_numeric_methods_disabled() DatetimeIndex._add_logical_methods_disabled() DatetimeIndex._add_datetimelike_methods() diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 69c9b003e1e8b..a93516ed678e2 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -39,19 +39,6 @@ join as libjoin, Timedelta, NaT) -def _td_index_cmp(cls, op): - """ - Wrap comparison operations to convert timedelta-like to timedelta64 - """ - opname = '__{name}__'.format(name=op.__name__) - - def wrapper(self, other): - result = getattr(TimedeltaArrayMixin, opname)(self, other) - return result - - return compat.set_function_name(wrapper, opname, cls) - - class TimedeltaIndex(TimedeltaArrayMixin, DatetimeIndexOpsMixin, TimelikeOps, Int64Index): """ @@ -139,16 +126,6 @@ def _join_i8_wrapper(joinf, **kwargs): _datetimelike_methods = ["to_pytimedelta", "total_seconds", "round", "floor", "ceil"] - @classmethod - def _add_comparison_methods(cls): - """ add in comparison methods """ - cls.__eq__ = _td_index_cmp(cls, operator.eq) - cls.__ne__ = _td_index_cmp(cls, operator.ne) - cls.__lt__ = _td_index_cmp(cls, operator.lt) - cls.__gt__ = _td_index_cmp(cls, operator.gt) - cls.__le__ = _td_index_cmp(cls, operator.le) - cls.__ge__ = _td_index_cmp(cls, operator.ge) - _engine_type = libindex.TimedeltaEngine _comparables = ['name', 'freq'] @@ -691,7 +668,7 @@ def delete(self, loc): return TimedeltaIndex(new_tds, name=self.name, freq=freq) -TimedeltaIndex._add_comparison_methods() +TimedeltaIndex._add_comparison_ops() TimedeltaIndex._add_numeric_methods() TimedeltaIndex._add_logical_methods_disabled() TimedeltaIndex._add_datetimelike_methods() From b8c26475c5b06a964c112e4c20655c8fbe206654 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 12 Oct 2018 11:25:03 -0700 Subject: [PATCH 07/13] test fixup --- pandas/tests/arrays/test_datetimelike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index f8ee59e2eddfe..5890cea3f8645 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -89,7 +89,7 @@ def test_tolist(self, datetime_index): assert expected == result @pytest.mark.parametrize('freqstr', ['D', 'B', 'W', 'M', 'Q', 'Y']) - def test_to_perioddelta(self, freqstr): + def test_to_perioddelta(self, datetime_index, freqstr): dti = datetime_index arr = DatetimeArrayMixin(dti) From a99b1f1e9d614718b8b25516bd4ecafa65bbe70c Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 12 Oct 2018 13:16:33 -0700 Subject: [PATCH 08/13] Fixup unused import --- pandas/core/indexes/datetimes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index cade41dfff2e3..504941d2128ca 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -42,7 +42,6 @@ from pandas.tseries.offsets import ( generate_range, CDay, prefix_mapping) -from pandas.core.tools.timedeltas import to_timedelta from pandas.util._decorators import Appender, cache_readonly, Substitution import pandas.core.common as com import pandas.tseries.offsets as offsets From eaf364fb686f82341bdbc84473001441928c241a Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 14 Oct 2018 08:31:40 -0700 Subject: [PATCH 09/13] privatize to_perioddelta in Array class --- pandas/core/arrays/datetimes.py | 2 +- pandas/core/indexes/datetimes.py | 2 +- pandas/tests/arrays/test_datetimelike.py | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 6759332f89e09..4cd0b39452d26 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -835,7 +835,7 @@ def to_period(self, freq=None): return PeriodArrayMixin(self.values, freq=freq) - def to_perioddelta(self, freq): + def _to_perioddelta(self, freq): """ Calculate TimedeltaArray of difference between index values and index converted to PeriodArray at specified diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index a7bbfc1b3f1a8..97f1cafb49b03 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -1218,7 +1218,7 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): is_year_end = wrap_field_accessor(DatetimeArrayMixin.is_year_end) is_leap_year = wrap_field_accessor(DatetimeArrayMixin.is_leap_year) - to_perioddelta = wrap_array_method(DatetimeArrayMixin.to_perioddelta, + to_perioddelta = wrap_array_method(DatetimeArrayMixin._to_perioddelta, False) to_period = wrap_array_method(DatetimeArrayMixin.to_period, True) normalize = wrap_array_method(DatetimeArrayMixin.normalize, True) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index 5890cea3f8645..a1c75357e31eb 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -69,6 +69,7 @@ def test_astype_object(self, tz_naive_fixture): # TODO: share this between Datetime/Timedelta/Period Array tests def test_repeat(self, datetime_index): + # GH#23113 dti = datetime_index arr = DatetimeArrayMixin(dti) @@ -81,6 +82,7 @@ def test_repeat(self, datetime_index): tm.assert_index_equal(pd.Index(result), pd.Index(expected)) def test_tolist(self, datetime_index): + # GH#23113 dti = datetime_index arr = DatetimeArrayMixin(dti) @@ -90,11 +92,14 @@ def test_tolist(self, datetime_index): @pytest.mark.parametrize('freqstr', ['D', 'B', 'W', 'M', 'Q', 'Y']) def test_to_perioddelta(self, datetime_index, freqstr): + # GH#23113 dti = datetime_index arr = DatetimeArrayMixin(dti) + # Note: _to_perioddelta is private on the PeriodArray class but + # public on the PeriodIndex class expected = dti.to_perioddelta(freq=freqstr) - result = arr.to_perioddelta(freq=freqstr) + result = arr._to_perioddelta(freq=freqstr) assert isinstance(result, TimedeltaArrayMixin) # placeholder until these become actual EA subclasses and we can use @@ -192,6 +197,7 @@ def test_to_timestamp(self, how, period_index): # TODO: share this between Datetime/Timedelta/Period Array tests def test_repeat(self, period_index): + # GH#23113 pi = period_index arr = PeriodArrayMixin(pi) @@ -204,6 +210,7 @@ def test_repeat(self, period_index): tm.assert_index_equal(pd.Index(result), pd.Index(expected)) def test_tolist(self, period_index): + # GH#23113 pi = period_index arr = PeriodArrayMixin(pi) From 0a5bb77b1340a921aa2b4c9f74aae966e02cd830 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 14 Oct 2018 15:27:15 -0700 Subject: [PATCH 10/13] revert move of repeat, tolist --- pandas/core/arrays/datetimelike.py | 19 ------------------- pandas/core/arrays/datetimes.py | 2 +- pandas/core/indexes/datetimelike.py | 18 ++++++++++++++++++ pandas/core/indexes/datetimes.py | 2 +- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 0805dd76e76ab..37fc451ba2a2b 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -13,7 +13,6 @@ from pandas.errors import NullFrequencyError, PerformanceWarning from pandas import compat -from pandas.compat.numpy import function as nv from pandas.tseries import frequencies from pandas.tseries.offsets import Tick, DateOffset @@ -209,24 +208,6 @@ def astype(self, dtype, copy=True): return self._box_values(self.asi8) return super(DatetimeLikeArrayMixin, self).astype(dtype, copy) - def repeat(self, repeats, *args, **kwargs): - """ - Analogous to ndarray.repeat - """ - nv.validate_repeat(args, kwargs) - if is_period_dtype(self): - freq = self.freq - else: - freq = None - return self._shallow_copy(self.asi8.repeat(repeats), - freq=freq) - - def tolist(self): - """ - return a list of the underlying data - """ - return list(self.astype(object)) - # ------------------------------------------------------------------ # Null Handling diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 4cd0b39452d26..6759332f89e09 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -835,7 +835,7 @@ def to_period(self, freq=None): return PeriodArrayMixin(self.values, freq=freq) - def _to_perioddelta(self, freq): + def to_perioddelta(self, freq): """ Calculate TimedeltaArray of difference between index values and index converted to PeriodArray at specified diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index e5869423eb1ab..8e919ba3599fc 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -415,6 +415,12 @@ def _convert_tolerance(self, tolerance, target): 'target index size') return tolerance + def tolist(self): + """ + return a list of the underlying data + """ + return list(self.astype(object)) + def min(self, axis=None, *args, **kwargs): """ Return the minimum value of the Index or minimum along @@ -607,6 +613,18 @@ def isin(self, values): return algorithms.isin(self.asi8, values.asi8) + def repeat(self, repeats, *args, **kwargs): + """ + Analogous to ndarray.repeat + """ + nv.validate_repeat(args, kwargs) + if is_period_dtype(self): + freq = self.freq + else: + freq = None + return self._shallow_copy(self.asi8.repeat(repeats), + freq=freq) + @Appender(_index_shared_docs['where'] % _index_doc_kwargs) def where(self, cond, other=None): other = _ensure_datetimelike_to_i8(other, to_utc=True) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 97f1cafb49b03..a7bbfc1b3f1a8 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -1218,7 +1218,7 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): is_year_end = wrap_field_accessor(DatetimeArrayMixin.is_year_end) is_leap_year = wrap_field_accessor(DatetimeArrayMixin.is_leap_year) - to_perioddelta = wrap_array_method(DatetimeArrayMixin._to_perioddelta, + to_perioddelta = wrap_array_method(DatetimeArrayMixin.to_perioddelta, False) to_period = wrap_array_method(DatetimeArrayMixin.to_period, True) normalize = wrap_array_method(DatetimeArrayMixin.normalize, True) From b38ad9b7f9e6b36ef957d44b66339c89067a7e94 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 14 Oct 2018 15:28:15 -0700 Subject: [PATCH 11/13] remove tests for removed methods --- pandas/tests/arrays/test_datetimelike.py | 46 ------------------------ 1 file changed, 46 deletions(-) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index a1c75357e31eb..bff0b2c1caeed 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -67,29 +67,6 @@ def test_astype_object(self, tz_naive_fixture): assert asobj.dtype == 'O' assert list(asobj) == list(dti) - # TODO: share this between Datetime/Timedelta/Period Array tests - def test_repeat(self, datetime_index): - # GH#23113 - dti = datetime_index - arr = DatetimeArrayMixin(dti) - - expected = dti.repeat(3) - result = arr.repeat(3) - assert isinstance(result, DatetimeArrayMixin) - - # placeholder until these become actual EA subclasses and we can use - # an EA-specific tm.assert_ function - tm.assert_index_equal(pd.Index(result), pd.Index(expected)) - - def test_tolist(self, datetime_index): - # GH#23113 - dti = datetime_index - arr = DatetimeArrayMixin(dti) - - expected = dti.tolist() - result = arr.tolist() - assert expected == result - @pytest.mark.parametrize('freqstr', ['D', 'B', 'W', 'M', 'Q', 'Y']) def test_to_perioddelta(self, datetime_index, freqstr): # GH#23113 @@ -195,29 +172,6 @@ def test_to_timestamp(self, how, period_index): # an EA-specific tm.assert_ function tm.assert_index_equal(pd.Index(result), pd.Index(expected)) - # TODO: share this between Datetime/Timedelta/Period Array tests - def test_repeat(self, period_index): - # GH#23113 - pi = period_index - arr = PeriodArrayMixin(pi) - - expected = pi.repeat(3) - result = arr.repeat(3) - assert isinstance(result, PeriodArrayMixin) - - # placeholder until these become actual EA subclasses and we can use - # an EA-specific tm.assert_ function - tm.assert_index_equal(pd.Index(result), pd.Index(expected)) - - def test_tolist(self, period_index): - # GH#23113 - pi = period_index - arr = PeriodArrayMixin(pi) - - expected = pi.tolist() - result = arr.tolist() - assert expected == result - @pytest.mark.parametrize('propname', pd.PeriodIndex._bool_ops) def test_bool_properties(self, period_index, propname): # in this case _bool_ops is just `is_leap_year` From 28bcd5a0e24111577152215a3a4e45714871e8bd Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 14 Oct 2018 16:55:15 -0700 Subject: [PATCH 12/13] Revert privatization in test --- pandas/tests/arrays/test_datetimelike.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index bff0b2c1caeed..af59a57163d62 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -73,10 +73,8 @@ def test_to_perioddelta(self, datetime_index, freqstr): dti = datetime_index arr = DatetimeArrayMixin(dti) - # Note: _to_perioddelta is private on the PeriodArray class but - # public on the PeriodIndex class expected = dti.to_perioddelta(freq=freqstr) - result = arr._to_perioddelta(freq=freqstr) + result = arr.to_perioddelta(freq=freqstr) assert isinstance(result, TimedeltaArrayMixin) # placeholder until these become actual EA subclasses and we can use From 91aaef413ab19f197c622ac55ce6ba01eea3dcf2 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 15 Oct 2018 07:56:18 -0700 Subject: [PATCH 13/13] add comment [ci skip] --- pandas/core/arrays/datetimes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index dbd84d6fd2bd8..1da43b03dac46 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -834,6 +834,7 @@ def to_perioddelta(self, freq): ------- TimedeltaArray/Index """ + # TODO: consider privatizing (discussion in GH#23113) from pandas.core.arrays.timedeltas import TimedeltaArrayMixin i8delta = self.asi8 - self.to_period(freq).to_timestamp().asi8 return TimedeltaArrayMixin(i8delta)