diff --git a/pandas/compat/__init__.py b/pandas/compat/__init__.py index a2531ebd43c82..47a5bb8f56a4f 100644 --- a/pandas/compat/__init__.py +++ b/pandas/compat/__init__.py @@ -180,8 +180,13 @@ class to receive bound method def u(s): return s + def u_safe(s): return s + + def function_object(f): + return f + else: string_types = basestring, integer_types = (int, long) @@ -198,6 +203,9 @@ def u_safe(s): except: return s + def function_object(f): + return f.im_func + string_and_binary_types = string_types + (binary_type,) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 799d96f46a15b..80b8f5825599c 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4716,7 +4716,7 @@ def combineMult(self, other): DataFrame._setup_axes( ['index', 'columns'], info_axis=1, stat_axis=0, axes_are_reversed=True) - +DataFrame._setup_generic_methods() _EMPTY_SERIES = Series([]) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 4553e4804e98b..91f2cbcfcdefa 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -2,6 +2,8 @@ import warnings import operator import weakref +import copy + import numpy as np import pandas.lib as lib @@ -16,7 +18,8 @@ import pandas.core.common as com import pandas.core.datetools as datetools from pandas import compat, _np_version_under1p7 -from pandas.compat import map, zip, lrange +from pandas.compat import map, zip, lrange, function_object +from pandas.util.decorators import Generic from pandas.core.common import (isnull, notnull, is_list_like, _values_from_object, _infer_dtype_from_scalar, _maybe_promote, @@ -145,13 +148,17 @@ def _setup_axes( # indexing support cls._ix = None - if info_axis is not None: - cls._info_axis_number = info_axis - cls._info_axis_name = axes[info_axis] + if info_axis is None: + info_axis = 0 + + cls._info_axis_number = info_axis + cls._info_axis_name = axes[info_axis] - if stat_axis is not None: - cls._stat_axis_number = stat_axis - cls._stat_axis_name = axes[stat_axis] + if stat_axis is None: + stat_axis = 0 + + cls._stat_axis_number = stat_axis + cls._stat_axis_name = axes[stat_axis] # setup the actual axis if build_axes: @@ -172,6 +179,49 @@ def set_axis(a, i): for k, v in ns.items(): setattr(cls, k, v) + @classmethod + def _setup_generic_methods(cls, allow_matching=False): + """ + setup any defined generic functions + note that this will override any defined local method + unless allow_matching=True + """ + + for name, g in compat.iteritems(Generic.functions): + + # allow an overriden function to propogate + func = g.func + if not allow_matching: + if function_object(getattr(cls,name)) != func: + continue + + doc = copy.copy(func.__doc__) + subs = copy.copy(g.subs) + subs['return_type'] = cls.__name__ + subs['info_axis'] = cls._info_axis_name + subs['stat_axis'] = cls._stat_axis_name + subs['allowed_axes'] = "({0})".format(', '.join(cls._AXIS_ORDERS)) + + if 'axes' in subs: + + def fmt_axes(i, axis): + lines = subs['axes'].split('\n') + s = '\n'.join([ " {0}".format(l) for l in lines[1:] ]) + extra = '\n ' if i > 0 else '' + return "{0}{1} : {2}\n{3}".format(extra,axis,lines[0],s) + subs['axes'] = ''.join([ fmt_axes(i, axis) for i, axis in enumerate(cls._AXIS_ORDERS) ]) + + # create out new function, setting the docstring and name + def new_scope(func,name,doc): + def new_func(self, *args, **kwargs): + return func(self, *args, **kwargs) + new_func.__doc__ = doc + new_func.__name__ = name + return new_func + + new_func = new_scope(func,name,doc.format(**subs)) + setattr(cls,name,new_func) + def _construct_axes_dict(self, axes=None, **kwargs): """ return an axes dictionary for myself """ d = dict([(a, self._get_axis(a)) for a in (axes or self._AXIS_ORDERS)]) @@ -310,6 +360,7 @@ def _set_axis(self, axis, labels): self._data.set_axis(axis, labels) self._clear_item_cache() + @Generic() def transpose(self, *args, **kwargs): """ Permute the dimensions of the Object @@ -328,7 +379,7 @@ def transpose(self, *args, **kwargs): Returns ------- - y : same as input + y : {return_type} """ # construct the args @@ -350,14 +401,17 @@ def transpose(self, *args, **kwargs): new_values = new_values.copy() return self._constructor(new_values, **new_axes) + @Generic() def swapaxes(self, axis1, axis2, copy=True): """ Interchange axes and swap values axes appropriately Returns ------- - y : same as input + y : {return_type} """ + if self.ndim == 1: + axis1 = axis2 = 0 i = self._get_axis_number(axis1) j = self._get_axis_number(axis2) @@ -376,10 +430,14 @@ def swapaxes(self, axis1, axis2, copy=True): return self._constructor(new_values, *new_axes) + @Generic() def pop(self, item): """ - Return item and drop from frame. Raise KeyError if not found. + Return the item from the {info_axis} and drop from object. + + Raise KeyError if not found. """ + result = self[item] del self[item] return result @@ -391,6 +449,7 @@ def squeeze(self): except: return self + @Generic() def swaplevel(self, i, j, axis=0): """ Swap levels i and j in a MultiIndex on a particular axis @@ -402,7 +461,7 @@ def swaplevel(self, i, j, axis=0): Returns ------- - swapped : type of caller (new object) + swapped : {return_type} """ axis = self._get_axis_number(axis) result = self.copy() @@ -413,6 +472,7 @@ def swaplevel(self, i, j, axis=0): #---------------------------------------------------------------------- # Rename + @Generic({ 'axes' : "dict-like or function, optional\nTransformation to apply to that axis values" }) def rename(self, *args, **kwargs): """ Alter axes input function or @@ -421,22 +481,15 @@ def rename(self, *args, **kwargs): Parameters ---------- - axis keywords for this object - (e.g. index for Series, - index,columns for DataFrame, - items,major_axis,minor_axis for Panel) - : dict-like or function, optional - Transformation to apply to that axis values - - copy : boolean, default True - Also copy underlying data + {axes} + copy : boolean, default True, copy underlying data inplace : boolean, default False - Whether to return a new PandasObject. If True then value of copy is - ignored. + Whether to return a new {return_type}. + If True then value of copy is ignored. Returns ------- - renamed : PandasObject (new object) + renamed : {return_type} """ axes, kwargs = self._construct_axes_from_arguments(args, kwargs) @@ -480,9 +533,10 @@ def f(x): else: return result._propogate_attributes(self) + @Generic() def rename_axis(self, mapper, axis=0, copy=True, inplace=False): """ - Alter index and / or columns using input function or functions. + Alter a specific axes using input function or functions. Function / dict values must be unique (1-to-1). Labels not contained in a dict / Series will be left as-is. @@ -496,7 +550,7 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False): Returns ------- - renamed : type of caller + renamed : {return_type} """ axis = self._get_axis_name(axis) d = { 'copy' : copy, 'inplace' : inplace } @@ -877,6 +931,7 @@ def __delitem__(self, key): except KeyError: pass + @Generic() def take(self, indices, axis=0, convert=True): """ Analogous to ndarray.take @@ -889,7 +944,7 @@ def take(self, indices, axis=0, convert=True): Returns ------- - taken : type of caller + taken : {return_type} """ # check/convert indicies here @@ -907,6 +962,7 @@ def take(self, indices, axis=0, convert=True): new_data = self._data.take(indices, axis=baxis) return self._constructor(new_data) + @Generic() def select(self, crit, axis=0): """ Return data corresponding to axis labels matching criteria @@ -919,7 +975,7 @@ def select(self, crit, axis=0): Returns ------- - selection : type of caller + selection : {return_type} """ axis = self._get_axis_number(axis) axis_name = self._get_axis_name(axis) @@ -933,12 +989,13 @@ def select(self, crit, axis=0): return self.reindex(**{axis_name: new_axis}) + @Generic() def reindex_like(self, other, method=None, copy=True, limit=None): """ return an object with matching indicies to myself Parameters ---------- - other : Object + other : PandasObject method : string or None copy : boolean, default True limit : int, default None @@ -951,11 +1008,12 @@ def reindex_like(self, other, method=None, copy=True, limit=None): Returns ------- - reindexed : same as input + reindexed : {return_type} """ d = other._construct_axes_dict(method=method) return self.reindex(**d) + @Generic() def drop(self, labels, axis=0, level=None): """ Return new object with labels in requested axis removed @@ -969,7 +1027,7 @@ def drop(self, labels, axis=0, level=None): Returns ------- - dropped : type of caller + dropped : {return_type} """ axis_name = self._get_axis_name(axis) axis, axis_ = self._get_axis(axis), axis @@ -1002,6 +1060,7 @@ def drop(self, labels, axis=0, level=None): return self.ix[tuple(slicer)] + @Generic() def add_prefix(self, prefix): """ Concatenate prefix string with panel items names. @@ -1012,11 +1071,12 @@ def add_prefix(self, prefix): Returns ------- - with_prefix : type of caller + with_prefix : {return_type} """ new_data = self._data.add_prefix(prefix) return self._constructor(new_data) + @Generic() def add_suffix(self, suffix): """ Concatenate suffix string with panel items names @@ -1027,25 +1087,25 @@ def add_suffix(self, suffix): Returns ------- - with_suffix : type of caller + with_suffix : {return_type} """ new_data = self._data.add_suffix(suffix) return self._constructor(new_data) + @Generic() def sort_index(self, axis=0, ascending=True): """ Sort object by labels (along an axis) Parameters ---------- - axis : {0, 1} - Sort index/rows versus columns + axis : {allowed_axes} ascending : boolean, default True Sort ascending vs. descending Returns ------- - sorted_obj : type of caller + sorted_obj : {return_type} """ axis = self._get_axis_number(axis) axis_name = self._get_axis_name(axis) @@ -1058,18 +1118,20 @@ def sort_index(self, axis=0, ascending=True): new_axis = labels.take(sort_index) return self.reindex(**{axis_name: new_axis}) + @Generic({ 'axes' : + "array-like, optional\nNew labels / index to conform to. " + "Preferably an Index object to\navoid duplicating data" }) def reindex(self, *args, **kwargs): - """Conform DataFrame to new index with optional filling logic, placing + """ + Conform {return_type} to new index with optional filling logic, placing NA/NaN in locations having no value in the previous index. A new object is produced unless the new index is equivalent to the current one and copy=False Parameters ---------- - axes : array-like, optional (can be specified in order, or as keywords) - New labels / index to conform to. Preferably an Index object to - avoid duplicating data - method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None + {axes} + method : {{'backfill', 'bfill', 'pad', 'ffill', None}}, default None Method to use for filling holes in reindexed DataFrame pad / ffill: propagate last valid observation forward to next valid backfill / bfill: use NEXT valid observation to fill gap @@ -1092,7 +1154,7 @@ def reindex(self, *args, **kwargs): Returns ------- - reindexed : same type as calling instance + reindexed : {return_type} """ # construct the args @@ -1151,6 +1213,7 @@ def _needs_reindex_multi(self, axes, method, level): def _reindex_multi(self, axes, copy, fill_value): return NotImplemented + @Generic() def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True, limit=None, fill_value=np.nan): """Conform input object to new index with optional filling logic, placing @@ -1164,7 +1227,7 @@ def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True, New labels / index to conform to. Preferably an Index object to avoid duplicating data axis : allowed axis for the input - method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None + method : {{'backfill', 'bfill', 'pad', 'ffill', None}}, default None Method to use for filling holes in reindexed DataFrame pad / ffill: propagate last valid observation forward to next valid backfill / bfill: use NEXT valid observation to fill gap @@ -1186,7 +1249,7 @@ def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True, Returns ------- - reindexed : same type as calling instance + reindexed : {return_type} """ self._consolidate_inplace() @@ -1245,6 +1308,7 @@ def _reindex_axis(self, new_index, fill_method, axis, copy): else: return self._constructor(new_data) + @Generic() def filter(self, items=None, like=None, regex=None, axis=None): """ Restrict the info axis to set of items or wildcard @@ -1252,16 +1316,21 @@ def filter(self, items=None, like=None, regex=None, axis=None): Parameters ---------- items : list-like - List of info axis to restrict to (must not all be present) + List of values to restrict to (must not all be present) like : string Keep info axis where "arg in col == True" regex : string (regular expression) Keep info axis with re.search(regex, col) == True + axis : the axis to filter Notes ----- Arguments are mutually exclusive, but this is not checked for + Returns + ------- + {return_type} + """ import re @@ -1326,6 +1395,7 @@ def _consolidate_inplace(self): f = lambda: self._data.consolidate() self._data = self._protect_consolidate(f) + @Generic() def consolidate(self, inplace=False): """ Compute NDFrame with "consolidated" internals (data of each dtype @@ -1339,7 +1409,7 @@ def consolidate(self, inplace=False): Returns ------- - consolidated : type of caller + consolidated : {return_type} """ if inplace: self._consolidate_inplace() @@ -1455,6 +1525,7 @@ def as_blocks(self, columns=None): def blocks(self): return self.as_blocks() + @Generic() def astype(self, dtype, copy=True, raise_on_error=True): """ Cast object to input numpy.dtype @@ -1467,13 +1538,14 @@ def astype(self, dtype, copy=True, raise_on_error=True): Returns ------- - casted : type of caller + casted : {return_type} """ mgr = self._data.astype( dtype, copy=copy, raise_on_error=raise_on_error) return self._constructor(mgr)._propogate_attributes(self) + @Generic() def copy(self, deep=True): """ Make a copy of this object @@ -1485,7 +1557,7 @@ def copy(self, deep=True): Returns ------- - copy : type of caller + copy : {return_type} """ data = self._data if deep: @@ -1511,6 +1583,7 @@ def convert_objects(self, convert_dates=True, convert_numeric=False, copy=True): #---------------------------------------------------------------------- # Filling NA's + @Generic() def fillna(self, value=None, method=None, axis=0, inplace=False, limit=None, downcast=None): """ @@ -1518,7 +1591,7 @@ def fillna(self, value=None, method=None, axis=0, inplace=False, Parameters ---------- - method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None + method : {{'backfill', 'bfill', 'pad', 'ffill', None}}, default None Method to use for filling holes in reindexed Series pad / ffill: propagate last valid observation forward to next valid backfill / bfill: use NEXT valid observation to fill gap @@ -1526,13 +1599,13 @@ def fillna(self, value=None, method=None, axis=0, inplace=False, Value to use to fill holes (e.g. 0), alternately a dict of values specifying which value to use for each column (columns not in the dict will not be filled). This value cannot be a list. - axis : {0, 1}, default 0 + axis : {{0, 1}}, default 0 0: fill column-by-column 1: fill row-by-row inplace : boolean, default False - If True, fill the DataFrame in place. Note: this will modify any - other views on this DataFrame, like if you took a no-copy slice of - an existing DataFrame, for example a column in a DataFrame. Returns + If True, fill the {return_type} in place. Note: this will modify any + other views on this {return_type}, like if you took a no-copy slice of + an existing {return_type}, for example a column in a {return_type}. Returns a reference to the filled object, which is self if inplace=True limit : int, default None Maximum size gap to forward or backward fill @@ -1546,7 +1619,7 @@ def fillna(self, value=None, method=None, axis=0, inplace=False, Returns ------- - filled : DataFrame + filled : {return_type} """ if isinstance(value, (list, tuple)): raise TypeError('"value" parameter must be a scalar or dict, but ' @@ -1625,6 +1698,7 @@ def bfill(self, axis=0, inplace=False, limit=None, downcast=None): return self.fillna(method='bfill', axis=axis, inplace=inplace, limit=limit, downcast=downcast) + @Generic() def replace(self, to_replace=None, value=None, inplace=False, limit=None, regex=False, method='pad', axis=None): """ @@ -1653,7 +1727,7 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None, * dict: - - Nested dictionaries, e.g., {'a': {'b': nan}}, are read as + - Nested dictionaries, e.g., {{'a': {{'b': nan}} }}, are read as follows: look in column 'a' for the value 'b' and replace it with nan. You can nest regular expressions as well. Note that column names (the top-level dictionary keys in a nested @@ -1688,7 +1762,7 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None, string. Otherwise, `to_replace` must be ``None`` because this parameter will be interpreted as a regular expression or a list, dict, or array of regular expressions. - method : string, optional, {'pad', 'ffill', 'bfill'} + method : string, optional, {{'pad', 'ffill', 'bfill'}} The method to use when for replacement, when ``to_replace`` is a ``list``. @@ -1700,7 +1774,7 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None, Returns ------- - filled : NDFrame + filled : {return_type} Raises ------ @@ -1913,6 +1987,7 @@ def interpolate(self, to_replace, method='pad', axis=0, inplace=False, #---------------------------------------------------------------------- # Action Methods + @Generic() def abs(self): """ Return an object with absolute value taken. Only applicable to objects @@ -1920,7 +1995,7 @@ def abs(self): Returns ------- - abs: type of caller + abs: {return_type} """ obj = np.abs(self) @@ -2053,6 +2128,7 @@ def groupby(self, by=None, axis=0, level=None, as_index=True, sort=True, return groupby(self, by, axis=axis, level=level, as_index=as_index, sort=sort, group_keys=group_keys, squeeze=squeeze) + @Generic() def asfreq(self, freq, method=None, how=None, normalize=False): """ Convert all TimeSeries inside to specified frequency using DateOffset @@ -2061,23 +2137,24 @@ def asfreq(self, freq, method=None, how=None, normalize=False): Parameters ---------- freq : DateOffset object, or string - method : {'backfill', 'bfill', 'pad', 'ffill', None} + method : {{'backfill', 'bfill', 'pad', 'ffill', None}} Method to use for filling holes in reindexed Series pad / ffill: propagate last valid observation forward to next valid backfill / bfill: use NEXT valid observation to fill methdo - how : {'start', 'end'}, default end + how : {{'start', 'end'}}, default end For PeriodIndex only, see PeriodIndex.asfreq normalize : bool, default False Whether to reset output index to midnight Returns ------- - converted : type of caller + converted : {return_type} """ from pandas.tseries.resample import asfreq return asfreq(self, freq, method=method, how=how, normalize=normalize) + @Generic() def at_time(self, time, asof=False): """ Select values at particular time of day (e.g. 9:30AM) @@ -2088,7 +2165,7 @@ def at_time(self, time, asof=False): Returns ------- - values_at_time : type of caller + values_at_time : {return_type} """ try: indexer = self.index.indexer_at_time(time, asof=asof) @@ -2096,6 +2173,7 @@ def at_time(self, time, asof=False): except AttributeError: raise TypeError('Index must be DatetimeIndex') + @Generic() def between_time(self, start_time, end_time, include_start=True, include_end=True): """ @@ -2110,7 +2188,7 @@ def between_time(self, start_time, end_time, include_start=True, Returns ------- - values_between_time : type of caller + values_between_time : {return_type} """ try: indexer = self.index.indexer_between_time( @@ -2157,6 +2235,7 @@ def resample(self, rule, how=None, axis=0, fill_method=None, limit=limit, base=base) return sampler.resample(self) + @Generic() def first(self, offset): """ Convenience method for subsetting initial periods of time series data @@ -2172,7 +2251,7 @@ def first(self, offset): Returns ------- - subset : type of caller + subset : {return_type} """ from pandas.tseries.frequencies import to_offset if not isinstance(self.index, DatetimeIndex): @@ -2191,6 +2270,7 @@ def first(self, offset): return self.ix[:end] + @Generic() def last(self, offset): """ Convenience method for subsetting final periods of time series data @@ -2206,7 +2286,7 @@ def last(self, offset): Returns ------- - subset : type of caller + subset : {return_type} """ from pandas.tseries.frequencies import to_offset if not isinstance(self.index, DatetimeIndex): @@ -2221,6 +2301,7 @@ def last(self, offset): start = self.index.searchsorted(start_date, side='right') return self.ix[start:] + @Generic() def align(self, other, join='outer', axis=None, level=None, copy=True, fill_value=None, method=None, limit=None, fill_axis=0): """ @@ -2229,8 +2310,8 @@ def align(self, other, join='outer', axis=None, level=None, copy=True, Parameters ---------- - other : DataFrame or Series - join : {'outer', 'inner', 'left', 'right'}, default 'outer' + other : PandasObject + join : {{'outer', 'inner', 'left', 'right'}}, default 'outer' axis : allowed axis of the other object, default None Align on index (0), columns (1), or both (None) level : int or name @@ -2244,12 +2325,12 @@ def align(self, other, join='outer', axis=None, level=None, copy=True, "compatible" value method : str, default None limit : int, default None - fill_axis : {0, 1}, default 0 + fill_axis : {{0, 1}}, default 0 Filling axis, method and limit Returns ------- - (left, right) : (type of input, type of other) + (left, right) : ({return_type}, type of other) Aligned objects """ from pandas import DataFrame, Series @@ -2362,6 +2443,7 @@ def _align_series(self, other, join='outer', axis=None, level=None, else: return left_result, right_result + @Generic() def where(self, cond, other=np.nan, inplace=False, axis=None, level=None, try_cast=False, raise_on_error=True): """ @@ -2370,8 +2452,8 @@ def where(self, cond, other=np.nan, inplace=False, axis=None, level=None, Parameters ---------- - cond : boolean DataFrame or array - other : scalar or DataFrame + cond : boolean {return_type} or array + other : scalar or PandasObject inplace : boolean, default False Whether to perform the operation in place on the data axis : alignment axis if needed, default None @@ -2384,7 +2466,7 @@ def where(self, cond, other=np.nan, inplace=False, axis=None, level=None, Returns ------- - wh : DataFrame + wh : {return_type} """ if isinstance(cond, NDFrame): cond = cond.reindex(**self._construct_axes_dict()) @@ -2499,6 +2581,7 @@ def where(self, cond, other=np.nan, inplace=False, axis=None, level=None, return self._constructor(new_data) + @Generic() def mask(self, cond): """ Returns copy of self whose values are replaced with nan if the @@ -2510,7 +2593,7 @@ def mask(self, cond): Returns ------- - wh: same as input + wh: {return_type} """ return self.where(~cond, np.nan) @@ -2544,21 +2627,21 @@ def pct_change(self, periods=1, fill_method='pad', limit=None, freq=None, np.putmask(rs.values, mask, np.nan) return rs + @Generic() def cumsum(self, axis=None, skipna=True): """ Return DataFrame of cumulative sums over requested axis. Parameters ---------- - axis : {0, 1} - 0 for row-wise, 1 for column-wise + axis : the axis to operate, defaults to [{stat_axis}] skipna : boolean, default True Exclude NA/null values. If an entire row/column is NA, the result will be NA Returns ------- - y : DataFrame + y : {return_type} """ if axis is None: axis = self._stat_axis_number @@ -2580,21 +2663,21 @@ def cumsum(self, axis=None, skipna=True): result = y.cumsum(axis) return self._wrap_array(result, self.axes, copy=False) + @Generic() def cumprod(self, axis=None, skipna=True): """ Return cumulative product over requested axis as DataFrame Parameters ---------- - axis : {0, 1} - 0 for row-wise, 1 for column-wise + axis : the axis to operate, defaults to [{stat_axis}] skipna : boolean, default True Exclude NA/null values. If an entire row/column is NA, the result will be NA Returns ------- - y : DataFrame + y : {return_type} """ if axis is None: axis = self._stat_axis_number @@ -2615,21 +2698,21 @@ def cumprod(self, axis=None, skipna=True): result = y.cumprod(axis) return self._wrap_array(result, self.axes, copy=False) + @Generic() def cummax(self, axis=None, skipna=True): """ Return DataFrame of cumulative max over requested axis. Parameters ---------- - axis : {0, 1} - 0 for row-wise, 1 for column-wise + axis : the axis to operate, defaults to [{stat_axis}] skipna : boolean, default True Exclude NA/null values. If an entire row/column is NA, the result will be NA Returns ------- - y : DataFrame + y : {return_type} """ if axis is None: axis = self._stat_axis_number @@ -2651,21 +2734,21 @@ def cummax(self, axis=None, skipna=True): result = np.maximum.accumulate(y, axis) return self._wrap_array(result, self.axes, copy=False) + @Generic() def cummin(self, axis=None, skipna=True): """ Return DataFrame of cumulative min over requested axis. Parameters ---------- - axis : {0, 1} - 0 for row-wise, 1 for column-wise + axis : the axis to operate, defaults to [{stat_axis}] skipna : boolean, default True Exclude NA/null values. If an entire row/column is NA, the result will be NA Returns ------- - y : DataFrame + y : {return_type} """ if axis is None: axis = self._stat_axis_number @@ -2781,25 +2864,34 @@ def tshift(self, periods=1, freq=None, axis=0, **kwds): return self._constructor(new_data) - def truncate(self, before=None, after=None, copy=True): - """Function truncate a sorted DataFrame / Series before and/or after + @Generic() + def truncate(self, before=None, after=None, axis=None, copy=True): + """Function truncate a sorted {return_type} before and/or after some particular dates. Parameters ---------- before : date - Truncate before date + Truncate before date after : date - Truncate after date + Truncate after date + axis : the axis to operate, defaults to [{stat_axis}] + copy : optional, return a copy of the truncation Returns ------- - truncated : type of caller + truncated : {return_type} """ + if axis is None: + axis = self._stat_axis_number + axis = self._get_axis_number(axis) + axis_name = self._get_axis_name(axis) + # if we have a date index, convert to dates, otherwise # treat like a slice - if self.index.is_all_dates: + ax = self._get_axis(axis) + if ax.is_all_dates: from pandas.tseries.tools import to_datetime before = to_datetime(before) after = to_datetime(after) @@ -2809,10 +2901,12 @@ def truncate(self, before=None, after=None, copy=True): raise AssertionError('Truncate: %s must be after %s' % (before, after)) - result = self.ix[before:after] + slicer = [ slice(None,None) ] * self._AXIS_LEN + slicer[axis] = slice(before,after) + result = self.ix[tuple(slicer)] - if isinstance(self.index, MultiIndex): - result.index = self.index.truncate(before, after) + if isinstance(ax, MultiIndex): + setattr(result,axis_name,ax.truncate(before, after)) if copy: result = result.copy() diff --git a/pandas/core/panel.py b/pandas/core/panel.py index 5f90eb9fa31a7..d644968811a28 100644 --- a/pandas/core/panel.py +++ b/pandas/core/panel.py @@ -1016,7 +1016,7 @@ def shift(self, lags, freq=None, axis='major'): def tshift(self, periods=1, freq=None, axis='major', **kwds): return super(Panel, self).tshift(periods, freq, axis, **kwds) - def truncate(self, before=None, after=None, axis='major'): + def truncate(self, before=None, after=None, axis='major', copy=False): """Function truncates a sorted Panel before and/or after some particular values on the requested axis @@ -1370,6 +1370,7 @@ def min(self, axis='major', skipna=True): slicers={'major_axis': 'index', 'minor_axis': 'columns'}) Panel._add_aggregate_operations() +Panel._setup_generic_methods() WidePanel = Panel LongPanel = DataFrame diff --git a/pandas/core/panelnd.py b/pandas/core/panelnd.py index 8f427568a4102..c168965d38e8f 100644 --- a/pandas/core/panelnd.py +++ b/pandas/core/panelnd.py @@ -108,5 +108,6 @@ def func(self, *args, **kwargs): # add the aggregate operations klass._add_aggregate_operations() + klass._setup_generic_methods(allow_matching=True) return klass diff --git a/pandas/core/series.py b/pandas/core/series.py index 9f7ab0cb0346b..d1cff145b2855 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -3124,6 +3124,7 @@ def to_period(self, freq=None, copy=True): return self._constructor(new_values, index=new_index, name=self.name) Series._setup_axes(['index'], info_axis=0) +Series._setup_generic_methods() _INDEX_TYPES = ndarray, Index, list, tuple # reinstall the SeriesIndexer diff --git a/pandas/util/decorators.py b/pandas/util/decorators.py index d83b1eb778763..7271110f69468 100644 --- a/pandas/util/decorators.py +++ b/pandas/util/decorators.py @@ -3,7 +3,6 @@ import sys import warnings - def deprecate(name, alternative, alt_name=None): alt_name = alt_name or alternative.__name__ @@ -106,6 +105,24 @@ def __call__(self, func): return func +class Generic(object): + """ + A function decorator that will hold specific functions for the named classes + of the target function. + """ + functions = {} + + def __init__(self, subs=None): + if subs is None: + subs = dict() + self.subs = subs + self.func = None + + def __call__(self, func): + self.func = func + self.functions[func.__name__] = self + return func + def indent(text, indents=1): if not text or not isinstance(text, str): return ''