Skip to content

Commit db8533f

Browse files
committed
Merge pull request #7953 from jreback/dt
API: add Series.dt delegator for datetimelike methods (GH7207)
2 parents e800fe1 + aa1c120 commit db8533f

File tree

13 files changed

+458
-150
lines changed

13 files changed

+458
-150
lines changed

doc/source/api.rst

+36-1
Original file line numberDiff line numberDiff line change
@@ -436,12 +436,47 @@ Time series-related
436436
Series.tz_convert
437437
Series.tz_localize
438438

439+
Datetimelike Properties
440+
~~~~~~~~~~~~~~~~~~~~~~~
441+
``Series.dt`` can be used to access the values of the series as
442+
datetimelike and return several properties.
443+
Due to implementation details the methods show up here as methods of the
444+
``DatetimeProperties/PeriodProperties`` classes. These can be accessed like ``Series.dt.<property>``.
445+
446+
.. currentmodule:: pandas.tseries.common
447+
448+
.. autosummary::
449+
:toctree: generated/
450+
451+
DatetimeProperties.date
452+
DatetimeProperties.time
453+
DatetimeProperties.year
454+
DatetimeProperties.month
455+
DatetimeProperties.day
456+
DatetimeProperties.hour
457+
DatetimeProperties.minute
458+
DatetimeProperties.second
459+
DatetimeProperties.microsecond
460+
DatetimeProperties.nanosecond
461+
DatetimeProperties.second
462+
DatetimeProperties.weekofyear
463+
DatetimeProperties.dayofweek
464+
DatetimeProperties.weekday
465+
DatetimeProperties.dayofyear
466+
DatetimeProperties.quarter
467+
DatetimeProperties.is_month_start
468+
DatetimeProperties.is_month_end
469+
DatetimeProperties.is_quarter_start
470+
DatetimeProperties.is_quarter_end
471+
DatetimeProperties.is_year_start
472+
DatetimeProperties.is_year_end
473+
439474
String handling
440475
~~~~~~~~~~~~~~~
441476
``Series.str`` can be used to access the values of the series as
442477
strings and apply several methods to it. Due to implementation
443478
details the methods show up here as methods of the
444-
``StringMethods`` class.
479+
``StringMethods`` class. These can be acccessed like ``Series.str.<function/property>``.
445480

446481
.. currentmodule:: pandas.core.strings
447482

doc/source/basics.rst

+35
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,41 @@ For instance,
10991099
for r in df2.itertuples():
11001100
print(r)
11011101
1102+
.. _basics.dt_accessors:
1103+
1104+
.dt accessor
1105+
~~~~~~~~~~~~
1106+
1107+
``Series`` has an accessor to succinctly return datetime like properties for the *values* of the Series, if its a datetime/period like Series.
1108+
This will return a Series, indexed like the existing Series.
1109+
1110+
.. ipython:: python
1111+
1112+
# datetime
1113+
s = Series(date_range('20130101 09:10:12',periods=4))
1114+
s
1115+
s.dt.hour
1116+
s.dt.second
1117+
s.dt.day
1118+
1119+
This enables nice expressions like this:
1120+
1121+
.. ipython:: python
1122+
1123+
s[s.dt.day==2]
1124+
1125+
.. ipython:: python
1126+
1127+
# period
1128+
s = Series(period_range('20130101',periods=4,freq='D').asobject)
1129+
s
1130+
s.dt.year
1131+
s.dt.day
1132+
1133+
.. note::
1134+
1135+
``Series.dt`` will raise a ``TypeError`` if you access with a non-datetimelike values
1136+
11021137
.. _basics.string_methods:
11031138

11041139
Vectorized string methods

doc/source/timeseries.rst

+1
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ There are several time/date properties that one can access from ``Timestamp`` or
444444
is_year_start,"Logical indicating if first day of year (defined by frequency)"
445445
is_year_end,"Logical indicating if last day of year (defined by frequency)"
446446

447+
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>`
447448

448449
DateOffset objects
449450
------------------

doc/source/v0.15.0.txt

+32
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ users upgrade to this version.
1111

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

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

@@ -165,6 +166,37 @@ previously results in ``Exception`` or ``TypeError`` (:issue:`7812`)
165166
- ``DataFrame.tz_localize`` and ``DataFrame.tz_convert`` now accepts an optional ``level`` argument
166167
for localizing a specific level of a MultiIndex (:issue:`7846`)
167168

169+
.. _whatsnew_0150.dt:
170+
171+
.dt accessor
172+
~~~~~~~~~~~~
173+
174+
``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`)
175+
This will return a Series, indexed like the existing Series. See the :ref:`docs <basics.dt_accessors>`
176+
177+
.. ipython:: python
178+
179+
# datetime
180+
s = Series(date_range('20130101 09:10:12',periods=4))
181+
s
182+
s.dt.hour
183+
s.dt.second
184+
s.dt.day
185+
186+
This enables nice expressions like this:
187+
188+
.. ipython:: python
189+
190+
s[s.dt.day==2]
191+
192+
.. ipython:: python
193+
194+
# period
195+
s = Series(period_range('20130101',periods=4,freq='D').asobject)
196+
s
197+
s.dt.year
198+
s.dt.day
199+
168200
.. _whatsnew_0150.refactoring:
169201

170202
Internal Refactoring

pandas/core/base.py

+56-89
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,62 @@ def _reset_cache(self, key=None):
100100
else:
101101
self._cache.pop(key, None)
102102

103+
class PandasDelegate(PandasObject):
104+
""" an abstract base class for delegating methods/properties """
105+
106+
def _delegate_property_get(self, name, *args, **kwargs):
107+
raise TypeError("You cannot access the property {name}".format(name=name))
108+
109+
def _delegate_property_set(self, name, value, *args, **kwargs):
110+
raise TypeError("The property {name} cannot be set".format(name=name))
111+
112+
def _delegate_method(self, name, *args, **kwargs):
113+
raise TypeError("You cannot call method {name}".format(name=name))
114+
115+
@classmethod
116+
def _add_delegate_accessors(cls, delegate, accessors, typ):
117+
"""
118+
add accessors to cls from the delegate class
119+
120+
Parameters
121+
----------
122+
cls : the class to add the methods/properties to
123+
delegate : the class to get methods/properties & doc-strings
124+
acccessors : string list of accessors to add
125+
typ : 'property' or 'method'
126+
127+
"""
128+
129+
def _create_delegator_property(name):
130+
131+
def _getter(self):
132+
return self._delegate_property_get(name)
133+
def _setter(self, new_values):
134+
return self._delegate_property_set(name, new_values)
135+
136+
_getter.__name__ = name
137+
_setter.__name__ = name
138+
139+
return property(fget=_getter, fset=_setter, doc=getattr(delegate,name).__doc__)
140+
141+
def _create_delegator_method(name):
142+
143+
def f(self, *args, **kwargs):
144+
return self._delegate_method(name, *args, **kwargs)
145+
146+
f.__name__ = name
147+
f.__doc__ = getattr(delegate,name).__doc__
148+
149+
return f
150+
151+
for name in accessors:
152+
153+
if typ == 'property':
154+
f = _create_delegator_property(name)
155+
else:
156+
f = _create_delegator_method(name)
157+
158+
setattr(cls,name,f)
103159

104160
class FrozenList(PandasObject, list):
105161

@@ -221,36 +277,6 @@ def f(self, *args, **kwargs):
221277
class IndexOpsMixin(object):
222278
""" common ops mixin to support a unified inteface / docs for Series / Index """
223279

224-
def _is_allowed_index_op(self, name):
225-
if not self._allow_index_ops:
226-
raise TypeError("cannot perform an {name} operations on this type {typ}".format(
227-
name=name,typ=type(self._get_access_object())))
228-
229-
def _ops_compat(self, name, op_accessor):
230-
231-
obj = self._get_access_object()
232-
try:
233-
return self._wrap_access_object(getattr(obj,op_accessor))
234-
except AttributeError:
235-
raise TypeError("cannot perform an {name} operations on this type {typ}".format(
236-
name=name,typ=type(obj)))
237-
238-
def _get_access_object(self):
239-
if isinstance(self, com.ABCSeries):
240-
return self.index
241-
return self
242-
243-
def _wrap_access_object(self, obj):
244-
# we may need to coerce the input as we don't want non int64 if
245-
# we have an integer result
246-
if hasattr(obj,'dtype') and com.is_integer_dtype(obj):
247-
obj = obj.astype(np.int64)
248-
249-
if isinstance(self, com.ABCSeries):
250-
return self._constructor(obj,index=self.index).__finalize__(self)
251-
252-
return obj
253-
254280
# ndarray compatibility
255281
__array_priority__ = 1000
256282

@@ -449,68 +475,9 @@ def searchsorted(self, key, side='left'):
449475
all = _unbox(np.ndarray.all)
450476
any = _unbox(np.ndarray.any)
451477

452-
# facilitate the properties on the wrapped ops
453-
def _field_accessor(name, docstring=None):
454-
op_accessor = '_{0}'.format(name)
455-
def f(self):
456-
return self._ops_compat(name,op_accessor)
457-
458-
f.__name__ = name
459-
f.__doc__ = docstring
460-
return property(f)
461-
462478
class DatetimeIndexOpsMixin(object):
463479
""" common ops mixin to support a unified inteface datetimelike Index """
464480

465-
def _is_allowed_datetime_index_op(self, name):
466-
if not self._allow_datetime_index_ops:
467-
raise TypeError("cannot perform an {name} operations on this type {typ}".format(
468-
name=name,typ=type(self._get_access_object())))
469-
470-
def _is_allowed_period_index_op(self, name):
471-
if not self._allow_period_index_ops:
472-
raise TypeError("cannot perform an {name} operations on this type {typ}".format(
473-
name=name,typ=type(self._get_access_object())))
474-
475-
def _ops_compat(self, name, op_accessor):
476-
477-
from pandas.tseries.index import DatetimeIndex
478-
from pandas.tseries.period import PeriodIndex
479-
obj = self._get_access_object()
480-
if isinstance(obj, DatetimeIndex):
481-
self._is_allowed_datetime_index_op(name)
482-
elif isinstance(obj, PeriodIndex):
483-
self._is_allowed_period_index_op(name)
484-
try:
485-
return self._wrap_access_object(getattr(obj,op_accessor))
486-
except AttributeError:
487-
raise TypeError("cannot perform an {name} operations on this type {typ}".format(
488-
name=name,typ=type(obj)))
489-
490-
date = _field_accessor('date','Returns numpy array of datetime.date. The date part of the Timestamps')
491-
time = _field_accessor('time','Returns numpy array of datetime.time. The time part of the Timestamps')
492-
year = _field_accessor('year', "The year of the datetime")
493-
month = _field_accessor('month', "The month as January=1, December=12")
494-
day = _field_accessor('day', "The days of the datetime")
495-
hour = _field_accessor('hour', "The hours of the datetime")
496-
minute = _field_accessor('minute', "The minutes of the datetime")
497-
second = _field_accessor('second', "The seconds of the datetime")
498-
microsecond = _field_accessor('microsecond', "The microseconds of the datetime")
499-
nanosecond = _field_accessor('nanosecond', "The nanoseconds of the datetime")
500-
weekofyear = _field_accessor('weekofyear', "The week ordinal of the year")
501-
week = weekofyear
502-
dayofweek = _field_accessor('dayofweek', "The day of the week with Monday=0, Sunday=6")
503-
weekday = dayofweek
504-
dayofyear = _field_accessor('dayofyear', "The ordinal day of the year")
505-
quarter = _field_accessor('quarter', "The quarter of the date")
506-
qyear = _field_accessor('qyear')
507-
is_month_start = _field_accessor('is_month_start', "Logical indicating if first day of month (defined by frequency)")
508-
is_month_end = _field_accessor('is_month_end', "Logical indicating if last day of month (defined by frequency)")
509-
is_quarter_start = _field_accessor('is_quarter_start', "Logical indicating if first day of quarter (defined by frequency)")
510-
is_quarter_end = _field_accessor('is_quarter_end', "Logical indicating if last day of quarter (defined by frequency)")
511-
is_year_start = _field_accessor('is_year_start', "Logical indicating if first day of year (defined by frequency)")
512-
is_year_end = _field_accessor('is_year_end', "Logical indicating if last day of year (defined by frequency)")
513-
514481
def __iter__(self):
515482
return (self._box_func(v) for v in self.asi8)
516483

pandas/core/generic.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1193,12 +1193,17 @@ def _check_setitem_copy(self, stacklevel=4, t='setting'):
11931193
except:
11941194
pass
11951195

1196-
if t == 'referant':
1196+
# a custom message
1197+
if isinstance(self.is_copy, string_types):
1198+
t = self.is_copy
1199+
1200+
elif t == 'referant':
11971201
t = ("\n"
11981202
"A value is trying to be set on a copy of a slice from a "
11991203
"DataFrame\n\n"
12001204
"See the the caveats in the documentation: "
12011205
"http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy")
1206+
12021207
else:
12031208
t = ("\n"
12041209
"A value is trying to be set on a copy of a slice from a "

pandas/core/index.py

-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ def _try_get_item(x):
3737
except AttributeError:
3838
return x
3939

40-
4140
def _indexOp(opname):
4241
"""
4342
Wrapper function for index comparison operations, to avoid
@@ -4281,7 +4280,6 @@ def isin(self, values, level=None):
42814280
return np.lib.arraysetops.in1d(labs, sought_labels)
42824281
MultiIndex._add_numeric_methods_disabled()
42834282

4284-
42854283
# For utility purposes
42864284

42874285
def _sparsify(label_list, start=0, sentinel=''):

pandas/core/series.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,6 @@ class Series(base.IndexOpsMixin, generic.NDFrame):
107107
_metadata = ['name']
108108
_allow_index_ops = True
109109

110-
@property
111-
def _allow_datetime_index_ops(self):
112-
# disabling to invalidate datetime index ops (GH7206)
113-
# return self.index.is_all_dates and isinstance(self.index, DatetimeIndex)
114-
return False
115-
116-
@property
117-
def _allow_period_index_ops(self):
118-
# disabling to invalidate period index ops (GH7206)
119-
# return self.index.is_all_dates and isinstance(self.index, PeriodIndex)
120-
return False
121-
122110
def __init__(self, data=None, index=None, dtype=None, name=None,
123111
copy=False, fastpath=False):
124112

@@ -2405,6 +2393,18 @@ def to_period(self, freq=None, copy=True):
24052393
new_index = self.index.to_period(freq=freq)
24062394
return self._constructor(new_values,
24072395
index=new_index).__finalize__(self)
2396+
2397+
#------------------------------------------------------------------------------
2398+
# Datetimelike delegation methods
2399+
2400+
@cache_readonly
2401+
def dt(self):
2402+
from pandas.tseries.common import maybe_to_datetimelike
2403+
try:
2404+
return maybe_to_datetimelike(self)
2405+
except (Exception):
2406+
raise TypeError("Can only use .dt accessor with datetimelike values")
2407+
24082408
#------------------------------------------------------------------------------
24092409
# Categorical methods
24102410

0 commit comments

Comments
 (0)