Skip to content

API: add Series.dt delegator for datetimelike methods (GH7207) #7953

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 10, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.<property>``.

.. 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.<function/property>``.

.. currentmodule:: pandas.core.strings

Expand Down
35 changes: 35 additions & 0 deletions doc/source/basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its -> it's

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
Expand Down
1 change: 1 addition & 0 deletions doc/source/timeseries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <basics.dt_accessors>`

DateOffset objects
------------------
Expand Down
32 changes: 32 additions & 0 deletions doc/source/v0.15.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ users upgrade to this version.

- The ``Categorical`` type was integrated as a first-class pandas type, see :ref:`here <whatsnew_0150.cat>`
- Internal refactoring of the ``Index`` class to no longer sub-class ``ndarray``, see :ref:`Internal Refactoring <whatsnew_0150.refactoring>`
- New datetimelike properties accessor ``.dt`` for Series, see :ref:`Dateimelike Properties <whatsnew_0150.dt>`

- :ref:`Other Enhancements <whatsnew_0150.enhancements>`

Expand Down Expand Up @@ -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`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comments here as above

This will return a Series, indexed like the existing Series. See the :ref:`docs <basics.dt_accessors>`

.. 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
Expand Down
145 changes: 56 additions & 89 deletions pandas/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down
7 changes: 6 additions & 1 deletion pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
2 changes: 0 additions & 2 deletions pandas/core/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def _try_get_item(x):
except AttributeError:
return x


def _indexOp(opname):
"""
Wrapper function for index comparison operations, to avoid
Expand Down Expand Up @@ -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=''):
Expand Down
24 changes: 12 additions & 12 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -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

Expand Down
Loading