From aa1c120d6c457f01a2c4eb131ad2e5cdc06cb566 Mon Sep 17 00:00:00 2001 From: jreback Date: Thu, 7 Aug 2014 12:54:19 -0400 Subject: [PATCH] API: add Series.dt delegator for datetimelike methods (GH7207) CLN: make _add_delegate_accessors generic --- doc/source/api.rst | 37 ++++++++- doc/source/basics.rst | 35 +++++++++ doc/source/timeseries.rst | 1 + doc/source/v0.15.0.txt | 32 ++++++++ pandas/core/base.py | 145 ++++++++++++++---------------------- pandas/core/generic.py | 7 +- pandas/core/index.py | 2 - pandas/core/series.py | 24 +++--- pandas/tests/test_base.py | 56 ++++++++++++-- pandas/tests/test_series.py | 71 +++++++++++++++++- pandas/tseries/common.py | 115 ++++++++++++++++++++++++++++ pandas/tseries/index.py | 50 +++++++------ pandas/tseries/period.py | 33 ++++---- 13 files changed, 458 insertions(+), 150 deletions(-) create mode 100644 pandas/tseries/common.py diff --git a/doc/source/api.rst b/doc/source/api.rst index 62518bf0d9ffd..ec6e2aff870c6 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -436,12 +436,47 @@ Time series-related Series.tz_convert Series.tz_localize +Datetimelike Properties +~~~~~~~~~~~~~~~~~~~~~~~ +``Series.dt`` can be used to access the values of the series as +datetimelike and return several properties. +Due to implementation details the methods show up here as methods of the +``DatetimeProperties/PeriodProperties`` classes. These can be accessed like ``Series.dt.``. + +.. currentmodule:: pandas.tseries.common + +.. autosummary:: + :toctree: generated/ + + DatetimeProperties.date + DatetimeProperties.time + DatetimeProperties.year + DatetimeProperties.month + DatetimeProperties.day + DatetimeProperties.hour + DatetimeProperties.minute + DatetimeProperties.second + DatetimeProperties.microsecond + DatetimeProperties.nanosecond + DatetimeProperties.second + DatetimeProperties.weekofyear + DatetimeProperties.dayofweek + DatetimeProperties.weekday + DatetimeProperties.dayofyear + DatetimeProperties.quarter + DatetimeProperties.is_month_start + DatetimeProperties.is_month_end + DatetimeProperties.is_quarter_start + DatetimeProperties.is_quarter_end + DatetimeProperties.is_year_start + DatetimeProperties.is_year_end + String handling ~~~~~~~~~~~~~~~ ``Series.str`` can be used to access the values of the series as strings and apply several methods to it. Due to implementation details the methods show up here as methods of the -``StringMethods`` class. +``StringMethods`` class. These can be acccessed like ``Series.str.``. .. currentmodule:: pandas.core.strings diff --git a/doc/source/basics.rst b/doc/source/basics.rst index 93933140ab11c..e880bb2d6b952 100644 --- a/doc/source/basics.rst +++ b/doc/source/basics.rst @@ -1099,6 +1099,41 @@ For instance, for r in df2.itertuples(): print(r) +.. _basics.dt_accessors: + +.dt accessor +~~~~~~~~~~~~ + +``Series`` has an accessor to succinctly return datetime like properties for the *values* of the Series, if its a datetime/period like Series. +This will return a Series, indexed like the existing Series. + +.. ipython:: python + + # datetime + s = Series(date_range('20130101 09:10:12',periods=4)) + s + s.dt.hour + s.dt.second + s.dt.day + +This enables nice expressions like this: + +.. ipython:: python + + s[s.dt.day==2] + +.. ipython:: python + + # period + s = Series(period_range('20130101',periods=4,freq='D').asobject) + s + s.dt.year + s.dt.day + +.. note:: + + ``Series.dt`` will raise a ``TypeError`` if you access with a non-datetimelike values + .. _basics.string_methods: Vectorized string methods diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 7a912361d0e14..60e32f8db5305 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -444,6 +444,7 @@ There are several time/date properties that one can access from ``Timestamp`` or is_year_start,"Logical indicating if first day of year (defined by frequency)" is_year_end,"Logical indicating if last day of year (defined by frequency)" +Furthermore, if you have a ``Series`` with datetimelike values, then you can access these properties via the ``.dt`` accessor, see the :ref:`docs ` DateOffset objects ------------------ diff --git a/doc/source/v0.15.0.txt b/doc/source/v0.15.0.txt index 5824e5824e8b5..8cdad6a872f49 100644 --- a/doc/source/v0.15.0.txt +++ b/doc/source/v0.15.0.txt @@ -11,6 +11,7 @@ users upgrade to this version. - The ``Categorical`` type was integrated as a first-class pandas type, see :ref:`here ` - Internal refactoring of the ``Index`` class to no longer sub-class ``ndarray``, see :ref:`Internal Refactoring ` + - New datetimelike properties accessor ``.dt`` for Series, see :ref:`Dateimelike Properties ` - :ref:`Other Enhancements ` @@ -165,6 +166,37 @@ previously results in ``Exception`` or ``TypeError`` (:issue:`7812`) - ``DataFrame.tz_localize`` and ``DataFrame.tz_convert`` now accepts an optional ``level`` argument for localizing a specific level of a MultiIndex (:issue:`7846`) +.. _whatsnew_0150.dt: + +.dt accessor +~~~~~~~~~~~~ + +``Series`` has gained an accessor to succinctly return datetime like properties for the *values* of the Series, if its a datetime/period like Series. (:issue:`7207`) +This will return a Series, indexed like the existing Series. See the :ref:`docs ` + +.. ipython:: python + + # datetime + s = Series(date_range('20130101 09:10:12',periods=4)) + s + s.dt.hour + s.dt.second + s.dt.day + +This enables nice expressions like this: + +.. ipython:: python + + s[s.dt.day==2] + +.. ipython:: python + + # period + s = Series(period_range('20130101',periods=4,freq='D').asobject) + s + s.dt.year + s.dt.day + .. _whatsnew_0150.refactoring: Internal Refactoring diff --git a/pandas/core/base.py b/pandas/core/base.py index c04872ab74bb0..021f4474130bd 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -100,6 +100,62 @@ def _reset_cache(self, key=None): else: self._cache.pop(key, None) +class PandasDelegate(PandasObject): + """ an abstract base class for delegating methods/properties """ + + def _delegate_property_get(self, name, *args, **kwargs): + raise TypeError("You cannot access the property {name}".format(name=name)) + + def _delegate_property_set(self, name, value, *args, **kwargs): + raise TypeError("The property {name} cannot be set".format(name=name)) + + def _delegate_method(self, name, *args, **kwargs): + raise TypeError("You cannot call method {name}".format(name=name)) + + @classmethod + def _add_delegate_accessors(cls, delegate, accessors, typ): + """ + add accessors to cls from the delegate class + + Parameters + ---------- + cls : the class to add the methods/properties to + delegate : the class to get methods/properties & doc-strings + acccessors : string list of accessors to add + typ : 'property' or 'method' + + """ + + def _create_delegator_property(name): + + def _getter(self): + return self._delegate_property_get(name) + def _setter(self, new_values): + return self._delegate_property_set(name, new_values) + + _getter.__name__ = name + _setter.__name__ = name + + return property(fget=_getter, fset=_setter, doc=getattr(delegate,name).__doc__) + + def _create_delegator_method(name): + + def f(self, *args, **kwargs): + return self._delegate_method(name, *args, **kwargs) + + f.__name__ = name + f.__doc__ = getattr(delegate,name).__doc__ + + return f + + for name in accessors: + + if typ == 'property': + f = _create_delegator_property(name) + else: + f = _create_delegator_method(name) + + setattr(cls,name,f) class FrozenList(PandasObject, list): @@ -221,36 +277,6 @@ def f(self, *args, **kwargs): class IndexOpsMixin(object): """ common ops mixin to support a unified inteface / docs for Series / Index """ - def _is_allowed_index_op(self, name): - if not self._allow_index_ops: - raise TypeError("cannot perform an {name} operations on this type {typ}".format( - name=name,typ=type(self._get_access_object()))) - - def _ops_compat(self, name, op_accessor): - - obj = self._get_access_object() - try: - return self._wrap_access_object(getattr(obj,op_accessor)) - except AttributeError: - raise TypeError("cannot perform an {name} operations on this type {typ}".format( - name=name,typ=type(obj))) - - def _get_access_object(self): - if isinstance(self, com.ABCSeries): - return self.index - return self - - def _wrap_access_object(self, obj): - # we may need to coerce the input as we don't want non int64 if - # we have an integer result - if hasattr(obj,'dtype') and com.is_integer_dtype(obj): - obj = obj.astype(np.int64) - - if isinstance(self, com.ABCSeries): - return self._constructor(obj,index=self.index).__finalize__(self) - - return obj - # ndarray compatibility __array_priority__ = 1000 @@ -449,68 +475,9 @@ def searchsorted(self, key, side='left'): all = _unbox(np.ndarray.all) any = _unbox(np.ndarray.any) -# facilitate the properties on the wrapped ops -def _field_accessor(name, docstring=None): - op_accessor = '_{0}'.format(name) - def f(self): - return self._ops_compat(name,op_accessor) - - f.__name__ = name - f.__doc__ = docstring - return property(f) - class DatetimeIndexOpsMixin(object): """ common ops mixin to support a unified inteface datetimelike Index """ - def _is_allowed_datetime_index_op(self, name): - if not self._allow_datetime_index_ops: - raise TypeError("cannot perform an {name} operations on this type {typ}".format( - name=name,typ=type(self._get_access_object()))) - - def _is_allowed_period_index_op(self, name): - if not self._allow_period_index_ops: - raise TypeError("cannot perform an {name} operations on this type {typ}".format( - name=name,typ=type(self._get_access_object()))) - - def _ops_compat(self, name, op_accessor): - - from pandas.tseries.index import DatetimeIndex - from pandas.tseries.period import PeriodIndex - obj = self._get_access_object() - if isinstance(obj, DatetimeIndex): - self._is_allowed_datetime_index_op(name) - elif isinstance(obj, PeriodIndex): - self._is_allowed_period_index_op(name) - try: - return self._wrap_access_object(getattr(obj,op_accessor)) - except AttributeError: - raise TypeError("cannot perform an {name} operations on this type {typ}".format( - name=name,typ=type(obj))) - - date = _field_accessor('date','Returns numpy array of datetime.date. The date part of the Timestamps') - time = _field_accessor('time','Returns numpy array of datetime.time. The time part of the Timestamps') - year = _field_accessor('year', "The year of the datetime") - month = _field_accessor('month', "The month as January=1, December=12") - day = _field_accessor('day', "The days of the datetime") - hour = _field_accessor('hour', "The hours of the datetime") - minute = _field_accessor('minute', "The minutes of the datetime") - second = _field_accessor('second', "The seconds of the datetime") - microsecond = _field_accessor('microsecond', "The microseconds of the datetime") - nanosecond = _field_accessor('nanosecond', "The nanoseconds of the datetime") - weekofyear = _field_accessor('weekofyear', "The week ordinal of the year") - week = weekofyear - dayofweek = _field_accessor('dayofweek', "The day of the week with Monday=0, Sunday=6") - weekday = dayofweek - dayofyear = _field_accessor('dayofyear', "The ordinal day of the year") - quarter = _field_accessor('quarter', "The quarter of the date") - qyear = _field_accessor('qyear') - is_month_start = _field_accessor('is_month_start', "Logical indicating if first day of month (defined by frequency)") - is_month_end = _field_accessor('is_month_end', "Logical indicating if last day of month (defined by frequency)") - is_quarter_start = _field_accessor('is_quarter_start', "Logical indicating if first day of quarter (defined by frequency)") - is_quarter_end = _field_accessor('is_quarter_end', "Logical indicating if last day of quarter (defined by frequency)") - is_year_start = _field_accessor('is_year_start', "Logical indicating if first day of year (defined by frequency)") - is_year_end = _field_accessor('is_year_end', "Logical indicating if last day of year (defined by frequency)") - def __iter__(self): return (self._box_func(v) for v in self.asi8) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 90c3fa207e3bb..7b8b609fe0f2a 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1193,12 +1193,17 @@ def _check_setitem_copy(self, stacklevel=4, t='setting'): except: pass - if t == 'referant': + # a custom message + if isinstance(self.is_copy, string_types): + t = self.is_copy + + elif t == 'referant': t = ("\n" "A value is trying to be set on a copy of a slice from a " "DataFrame\n\n" "See the the caveats in the documentation: " "http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy") + else: t = ("\n" "A value is trying to be set on a copy of a slice from a " diff --git a/pandas/core/index.py b/pandas/core/index.py index 4f4fe092a3606..a58a3331f9759 100644 --- a/pandas/core/index.py +++ b/pandas/core/index.py @@ -37,7 +37,6 @@ def _try_get_item(x): except AttributeError: return x - def _indexOp(opname): """ Wrapper function for index comparison operations, to avoid @@ -4281,7 +4280,6 @@ def isin(self, values, level=None): return np.lib.arraysetops.in1d(labs, sought_labels) MultiIndex._add_numeric_methods_disabled() - # For utility purposes def _sparsify(label_list, start=0, sentinel=''): diff --git a/pandas/core/series.py b/pandas/core/series.py index 22284df337d97..3901e19968841 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -107,18 +107,6 @@ class Series(base.IndexOpsMixin, generic.NDFrame): _metadata = ['name'] _allow_index_ops = True - @property - def _allow_datetime_index_ops(self): - # disabling to invalidate datetime index ops (GH7206) - # return self.index.is_all_dates and isinstance(self.index, DatetimeIndex) - return False - - @property - def _allow_period_index_ops(self): - # disabling to invalidate period index ops (GH7206) - # return self.index.is_all_dates and isinstance(self.index, PeriodIndex) - return False - def __init__(self, data=None, index=None, dtype=None, name=None, copy=False, fastpath=False): @@ -2405,6 +2393,18 @@ def to_period(self, freq=None, copy=True): new_index = self.index.to_period(freq=freq) return self._constructor(new_values, index=new_index).__finalize__(self) + + #------------------------------------------------------------------------------ + # Datetimelike delegation methods + + @cache_readonly + def dt(self): + from pandas.tseries.common import maybe_to_datetimelike + try: + return maybe_to_datetimelike(self) + except (Exception): + raise TypeError("Can only use .dt accessor with datetimelike values") + #------------------------------------------------------------------------------ # Categorical methods diff --git a/pandas/tests/test_base.py b/pandas/tests/test_base.py index 356984ea88f43..179dc4d2948d9 100644 --- a/pandas/tests/test_base.py +++ b/pandas/tests/test_base.py @@ -4,8 +4,9 @@ import pandas.compat as compat import pandas as pd from pandas.compat import u, StringIO -from pandas.core.base import FrozenList, FrozenNDArray, DatetimeIndexOpsMixin +from pandas.core.base import FrozenList, FrozenNDArray, PandasDelegate, DatetimeIndexOpsMixin from pandas.util.testing import assertRaisesRegexp, assert_isinstance +from pandas.tseries.common import is_datetimelike from pandas import Series, Index, Int64Index, DatetimeIndex, PeriodIndex from pandas import _np_version_under1p7 import pandas.tslib as tslib @@ -127,6 +128,53 @@ def test_values(self): self.assert_numpy_array_equal(self.container, original) self.assertEqual(vals[0], n) +class TestPandasDelegate(tm.TestCase): + + def setUp(self): + pass + + def test_invalida_delgation(self): + # these show that in order for the delegation to work + # the _delegate_* methods need to be overriden to not raise a TypeError + + class Delegator(object): + _properties = ['foo'] + _methods = ['bar'] + + def _set_foo(self, value): + self.foo = value + + def _get_foo(self): + return self.foo + + foo = property(_get_foo, _set_foo, doc="foo property") + + def bar(self, *args, **kwargs): + """ a test bar method """ + pass + + class Delegate(PandasDelegate): + def __init__(self, obj): + self.obj = obj + Delegate._add_delegate_accessors(delegate=Delegator, + accessors=Delegator._properties, + typ='property') + Delegate._add_delegate_accessors(delegate=Delegator, + accessors=Delegator._methods, + typ='method') + + delegate = Delegate(Delegator()) + + def f(): + delegate.foo + self.assertRaises(TypeError, f) + def f(): + delegate.foo = 5 + self.assertRaises(TypeError, f) + def f(): + delegate.foo() + self.assertRaises(TypeError, f) + class Ops(tm.TestCase): def setUp(self): self.int_index = tm.makeIntIndex(10) @@ -526,13 +574,12 @@ def test_factorize(self): class TestDatetimeIndexOps(Ops): - _allowed = '_allow_datetime_index_ops' tz = [None, 'UTC', 'Asia/Tokyo', 'US/Eastern', 'dateutil/Asia/Singapore', 'dateutil/US/Pacific'] def setUp(self): super(TestDatetimeIndexOps, self).setUp() - mask = lambda x: x._allow_datetime_index_ops or x._allow_period_index_ops + mask = lambda x: isinstance(x, DatetimeIndex) or isinstance(x, PeriodIndex) or is_datetimelike(x) self.is_valid_objs = [ o for o in self.objs if mask(o) ] self.not_valid_objs = [ o for o in self.objs if not mask(o) ] @@ -784,11 +831,10 @@ def test_value_counts_unique(self): class TestPeriodIndexOps(Ops): - _allowed = '_allow_period_index_ops' def setUp(self): super(TestPeriodIndexOps, self).setUp() - mask = lambda x: x._allow_datetime_index_ops or x._allow_period_index_ops + mask = lambda x: isinstance(x, DatetimeIndex) or isinstance(x, PeriodIndex) or is_datetimelike(x) self.is_valid_objs = [ o for o in self.objs if mask(o) ] self.not_valid_objs = [ o for o in self.objs if not mask(o) ] diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py index 3e5fe1f392445..aa718a11d97cf 100644 --- a/pandas/tests/test_series.py +++ b/pandas/tests/test_series.py @@ -16,7 +16,7 @@ import pandas as pd from pandas import (Index, Series, DataFrame, isnull, notnull, - bdate_range, date_range, _np_version_under1p7) + bdate_range, date_range, period_range, _np_version_under1p7) from pandas.core.index import MultiIndex from pandas.core.indexing import IndexingError from pandas.tseries.index import Timestamp, DatetimeIndex @@ -71,6 +71,75 @@ def test_append_preserve_name(self): result = self.ts[:5].append(self.ts[5:]) self.assertEqual(result.name, self.ts.name) + def test_dt_namespace_accessor(self): + + # GH 7207 + # test .dt namespace accessor + + ok_for_base = ['year','month','day','hour','minute','second','weekofyear','week','dayofweek','weekday','dayofyear','quarter'] + ok_for_period = ok_for_base + ['qyear'] + ok_for_dt = ok_for_base + ['date','time','microsecond','nanosecond', 'is_month_start', 'is_month_end', 'is_quarter_start', + 'is_quarter_end', 'is_year_start', 'is_year_end'] + ok_for_both = ok_for_dt + + def get_expected(s, name): + result = getattr(Index(s.values),prop) + if isinstance(result, np.ndarray): + if com.is_integer_dtype(result): + result = result.astype('int64') + return Series(result,index=s.index) + + # invalids + for s in [Series(np.arange(5)), + Series(list('abcde')), + Series(np.random.randn(5))]: + self.assertRaises(TypeError, lambda : s.dt) + + # datetimeindex + for s in [Series(date_range('20130101',periods=5)), + Series(date_range('20130101',periods=5,freq='s')), + Series(date_range('20130101 00:00:00',periods=5,freq='ms'))]: + + for prop in ok_for_dt: + tm.assert_series_equal(getattr(s.dt,prop),get_expected(s,prop)) + + # both + index = date_range('20130101',periods=3,freq='D') + s = Series(date_range('20140204',periods=3,freq='s'),index=index) + tm.assert_series_equal(s.dt.year,Series(np.array([2014,2014,2014],dtype='int64'),index=index)) + tm.assert_series_equal(s.dt.month,Series(np.array([2,2,2],dtype='int64'),index=index)) + tm.assert_series_equal(s.dt.second,Series(np.array([0,1,2],dtype='int64'),index=index)) + + # periodindex + for s in [Series(period_range('20130101',periods=5,freq='D').asobject)]: + + for prop in ok_for_period: + tm.assert_series_equal(getattr(s.dt,prop),get_expected(s,prop)) + + # test limited display api + def get_dir(s): + results = [ r for r in s.dt.__dir__() if not r.startswith('_') ] + return list(sorted(set(results))) + + s = Series(date_range('20130101',periods=5,freq='D')) + results = get_dir(s) + tm.assert_almost_equal(results,list(sorted(set(ok_for_dt)))) + + s = Series(period_range('20130101',periods=5,freq='D').asobject) + results = get_dir(s) + tm.assert_almost_equal(results,list(sorted(set(ok_for_period)))) + + # no setting allowed + s = Series(date_range('20130101',periods=5,freq='D')) + with tm.assertRaisesRegexp(ValueError, "modifications"): + s.dt.hour = 5 + + # trying to set a copy + with pd.option_context('chained_assignment','raise'): + def f(): + s.dt.hour[0] = 5 + self.assertRaises(com.SettingWithCopyError, f) + def test_binop_maybe_preserve_name(self): # names match, preserve diff --git a/pandas/tseries/common.py b/pandas/tseries/common.py new file mode 100644 index 0000000000000..92ccd1248fac9 --- /dev/null +++ b/pandas/tseries/common.py @@ -0,0 +1,115 @@ +## datetimelike delegation ## + +import numpy as np +from pandas.core.base import PandasDelegate +from pandas.core import common as com +from pandas import Series, DatetimeIndex, PeriodIndex +from pandas import lib, tslib + +def is_datetimelike(data): + """ return a boolean if we can be successfully converted to a datetimelike """ + try: + maybe_to_datetimelike(data) + return True + except (Exception): + pass + return False + +def maybe_to_datetimelike(data, copy=False): + """ + return a DelegatedClass of a Series that is datetimelike (e.g. datetime64[ns] dtype or a Series of Periods) + raise TypeError if this is not possible. + + Parameters + ---------- + data : Series + copy : boolean, default False + copy the input data + + Returns + ------- + DelegatedClass + + """ + + if not isinstance(data, Series): + raise TypeError("cannot convert an object of type {0} to a datetimelike index".format(type(data))) + + index = data.index + if issubclass(data.dtype.type, np.datetime64): + return DatetimeProperties(DatetimeIndex(data, copy=copy), index) + else: + + if isinstance(data, PeriodIndex): + return PeriodProperties(PeriodIndex(data, copy=copy), index) + + data = com._values_from_object(data) + inferred = lib.infer_dtype(data) + if inferred == 'period': + return PeriodProperties(PeriodIndex(data), index) + + raise TypeError("cannot convert an object of type {0} to a datetimelike index".format(type(data))) + +class Properties(PandasDelegate): + + def __init__(self, values, index): + self.values = values + self.index = index + + def _delegate_property_get(self, name): + result = getattr(self.values,name) + + # maybe need to upcast (ints) + if isinstance(result, np.ndarray): + if com.is_integer_dtype(result): + result = result.astype('int64') + + # return the result as a Series, which is by definition a copy + result = Series(result, index=self.index) + + # setting this object will show a SettingWithCopyWarning/Error + result.is_copy = ("modifications to a property of a datetimelike object are not " + "supported and are discarded. Change values on the original.") + + return result + + def _delegate_property_set(self, name, value, *args, **kwargs): + raise ValueError("modifications to a property of a datetimelike object are not " + "supported. Change values on the original.") + + +class DatetimeProperties(Properties): + """ + Accessor object for datetimelike properties of the Series values. + + Examples + -------- + >>> s.dt.hour + >>> s.dt.second + >>> s.dt.quarter + + Returns a Series indexed like the original Series. + Raises TypeError if the Series does not contain datetimelike values. + """ + +DatetimeProperties._add_delegate_accessors(delegate=DatetimeIndex, + accessors=DatetimeIndex._datetimelike_ops, + typ='property') + +class PeriodProperties(Properties): + """ + Accessor object for datetimelike properties of the Series values. + + Examples + -------- + >>> s.dt.hour + >>> s.dt.second + >>> s.dt.quarter + + Returns a Series indexed like the original Series. + Raises TypeError if the Series does not contain datetimelike values. + """ + +PeriodProperties._add_delegate_accessors(delegate=PeriodIndex, + accessors=PeriodIndex._datetimelike_ops, + typ='property') diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py index 3b8cebcb51684..2acdcfffb7d9a 100644 --- a/pandas/tseries/index.py +++ b/pandas/tseries/index.py @@ -174,7 +174,10 @@ class DatetimeIndex(DatetimeIndexOpsMixin, Int64Index): offset = None _comparables = ['name','freqstr','tz'] _attributes = ['name','freq','tz'] - _allow_datetime_index_ops = True + _datetimelike_ops = ['year','month','day','hour','minute','second', + 'weekofyear','week','dayofweek','weekday','dayofyear','quarter', + 'date','time','microsecond','nanosecond','is_month_start','is_month_end', + 'is_quarter_start','is_quarter_end','is_year_start','is_year_end'] _is_numeric_dtype = False def __new__(cls, data=None, @@ -1428,30 +1431,31 @@ def freqstr(self): return None return self.offset.freqstr - _year = _field_accessor('year', 'Y') - _month = _field_accessor('month', 'M', "The month as January=1, December=12") - _day = _field_accessor('day', 'D') - _hour = _field_accessor('hour', 'h') - _minute = _field_accessor('minute', 'm') - _second = _field_accessor('second', 's') - _microsecond = _field_accessor('microsecond', 'us') - _nanosecond = _field_accessor('nanosecond', 'ns') - _weekofyear = _field_accessor('weekofyear', 'woy') - _week = _weekofyear - _dayofweek = _field_accessor('dayofweek', 'dow', + year = _field_accessor('year', 'Y', "The year of the datetime") + month = _field_accessor('month', 'M', "The month as January=1, December=12") + day = _field_accessor('day', 'D', "The days of the datetime") + hour = _field_accessor('hour', 'h', "The hours of the datetime") + minute = _field_accessor('minute', 'm', "The minutes of the datetime") + second = _field_accessor('second', 's', "The seconds of the datetime") + millisecond = _field_accessor('millisecond', 'ms', "The milliseconds of the datetime") + microsecond = _field_accessor('microsecond', 'us', "The microseconds of the datetime") + nanosecond = _field_accessor('nanosecond', 'ns', "The nanoseconds of the datetime") + weekofyear = _field_accessor('weekofyear', 'woy', "The week ordinal of the year") + week = weekofyear + dayofweek = _field_accessor('dayofweek', 'dow', "The day of the week with Monday=0, Sunday=6") - _weekday = _dayofweek - _dayofyear = _field_accessor('dayofyear', 'doy') - _quarter = _field_accessor('quarter', 'q') - _is_month_start = _field_accessor('is_month_start', 'is_month_start') - _is_month_end = _field_accessor('is_month_end', 'is_month_end') - _is_quarter_start = _field_accessor('is_quarter_start', 'is_quarter_start') - _is_quarter_end = _field_accessor('is_quarter_end', 'is_quarter_end') - _is_year_start = _field_accessor('is_year_start', 'is_year_start') - _is_year_end = _field_accessor('is_year_end', 'is_year_end') + weekday = dayofweek + dayofyear = _field_accessor('dayofyear', 'doy', "The ordinal day of the year") + quarter = _field_accessor('quarter', 'q', "The quarter of the date") + is_month_start = _field_accessor('is_month_start', 'is_month_start', "Logical indicating if first day of month (defined by frequency)") + is_month_end = _field_accessor('is_month_end', 'is_month_end', "Logical indicating if last day of month (defined by frequency)") + is_quarter_start = _field_accessor('is_quarter_start', 'is_quarter_start', "Logical indicating if first day of quarter (defined by frequency)") + is_quarter_end = _field_accessor('is_quarter_end', 'is_quarter_end', "Logical indicating if last day of quarter (defined by frequency)") + is_year_start = _field_accessor('is_year_start', 'is_year_start', "Logical indicating if first day of year (defined by frequency)") + is_year_end = _field_accessor('is_year_end', 'is_year_end', "Logical indicating if last day of year (defined by frequency)") @property - def _time(self): + def time(self): """ Returns numpy array of datetime.time. The time part of the Timestamps. """ @@ -1460,7 +1464,7 @@ def _time(self): return _algos.arrmap_object(self.asobject.values, lambda x: x.time()) @property - def _date(self): + def date(self): """ Returns numpy array of datetime.date. The date part of the Timestamps. """ diff --git a/pandas/tseries/period.py b/pandas/tseries/period.py index e80fdf28c4089..b8b97a35cba15 100644 --- a/pandas/tseries/period.py +++ b/pandas/tseries/period.py @@ -33,14 +33,14 @@ def f(self): return property(f) -def _field_accessor(name, alias): +def _field_accessor(name, alias, docstring=None): def f(self): base, mult = _gfc(self.freq) return tslib.get_period_field_arr(alias, self.values, base) f.__name__ = name + f.__doc__ = docstring return property(f) - class Period(PandasObject): """ Represents an period of time @@ -572,8 +572,9 @@ class PeriodIndex(DatetimeIndexOpsMixin, Int64Index): >>> idx2 = PeriodIndex(start='2000', end='2010', freq='A') """ _box_scalars = True - _allow_period_index_ops = True _attributes = ['name','freq'] + _datetimelike_ops = ['year','month','day','hour','minute','second', + 'weekofyear','week','dayofweek','weekday','dayofyear','quarter', 'qyear'] _is_numeric_dtype = False __eq__ = _period_index_cmp('__eq__') @@ -786,19 +787,19 @@ def asfreq(self, freq=None, how='E'): def to_datetime(self, dayfirst=False): return self.to_timestamp() - _year = _field_accessor('year', 0) - _month = _field_accessor('month', 3) - _day = _field_accessor('day', 4) - _hour = _field_accessor('hour', 5) - _minute = _field_accessor('minute', 6) - _second = _field_accessor('second', 7) - _weekofyear = _field_accessor('week', 8) - _week = _weekofyear - _dayofweek = _field_accessor('dayofweek', 10) - _weekday = _dayofweek - _dayofyear = day_of_year = _field_accessor('dayofyear', 9) - _quarter = _field_accessor('quarter', 2) - _qyear = _field_accessor('qyear', 1) + year = _field_accessor('year', 0, "The year of the period") + month = _field_accessor('month', 3, "The month as January=1, December=12") + day = _field_accessor('day', 4, "The days of the period") + hour = _field_accessor('hour', 5, "The hour of the period") + minute = _field_accessor('minute', 6, "The minute of the period") + second = _field_accessor('second', 7, "The second of the period") + weekofyear = _field_accessor('week', 8, "The week ordinal of the year") + week = weekofyear + dayofweek = _field_accessor('dayofweek', 10, "The day of the week with Monday=0, Sunday=6") + weekday = dayofweek + dayofyear = day_of_year = _field_accessor('dayofyear', 9, "The ordinal day of the year") + quarter = _field_accessor('quarter', 2, "The quarter of the date") + qyear = _field_accessor('qyear', 1) # Try to run function on index first, and then on elements of index # Especially important for group-by functionality