diff --git a/doc/source/api.rst b/doc/source/api.rst index 7e863a4429487..a377fa3960d4c 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -730,6 +730,7 @@ Reindexing / Selection / Label manipulation Panel.reindex Panel.reindex_axis Panel.reindex_like + Panel.rename Panel.select Panel.take Panel.truncate diff --git a/doc/source/release.rst b/doc/source/release.rst index 9be9a03b0346e..0ca8437bb53d8 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -172,21 +172,19 @@ See :ref:`Internal Refactoring` - added ``ftypes`` method to Series/DataFame, similar to ``dtypes``, but indicates if the underlying is sparse/dense (as well as the dtype) - - All ``NDFrame`` objects now have a ``_prop_attributes``, which can be used to indcated various values to propogate to a new object from an existing (e.g. name in ``Series`` will follow more automatically now) - - Internal type checking is now done via a suite of generated classes, allowing ``isinstance(value, klass)`` without having to directly import the klass, courtesy of @jtratner - - Bug in Series update where the parent frame is not updating its cache based on changes (:issue:`4080`) or types (:issue:`3217`), fillna (:issue:`3386`) - - Indexing with dtype conversions fixed (:issue:`4463`, :issue:`4204`) - -- Refactor Series.reindex to core/generic.py (:issue:`4604`, :issue:`4618`), allow ``method=`` in reindexing +- Refactor ``Series.reindex`` to core/generic.py (:issue:`4604`, :issue:`4618`), allow ``method=`` in reindexing on a Series to work +- ``Series.copy`` no longer accepts the ``order`` parameter and is now consistent with ``NDFrame`` copy +- Refactor ``rename`` methods to core/generic.py; fixes ``Series.rename`` for (:issue`4605`), and adds ``rename`` + with the same signature for ``Panel`` **Experimental Features** diff --git a/doc/source/v0.13.0.txt b/doc/source/v0.13.0.txt index ffa71cbe97ce0..67d676618deb3 100644 --- a/doc/source/v0.13.0.txt +++ b/doc/source/v0.13.0.txt @@ -226,21 +226,19 @@ and behaviors. Series formerly subclassed directly from ``ndarray``. (:issue:`40 - added ``ftypes`` method to Series/DataFame, similar to ``dtypes``, but indicates if the underlying is sparse/dense (as well as the dtype) - - All ``NDFrame`` objects now have a ``_prop_attributes``, which can be used to indcated various values to propogate to a new object from an existing (e.g. name in ``Series`` will follow more automatically now) - - Internal type checking is now done via a suite of generated classes, allowing ``isinstance(value, klass)`` without having to directly import the klass, courtesy of @jtratner - - Bug in Series update where the parent frame is not updating its cache based on changes (:issue:`4080`) or types (:issue:`3217`), fillna (:issue:`3386`) - - Indexing with dtype conversions fixed (:issue:`4463`, :issue:`4204`) - -- Refactor Series.reindex to core/generic.py (:issue:`4604`, :issue:`4618`), allow ``method=`` in reindexing +- Refactor ``Series.reindex`` to core/generic.py (:issue:`4604`, :issue:`4618`), allow ``method=`` in reindexing on a Series to work +- ``Series.copy`` no longer accepts the ``order`` parameter and is now consistent with ``NDFrame`` copy +- Refactor ``rename`` methods to core/generic.py; fixes ``Series.rename`` for (:issue`4605`), and adds ``rename`` + with the same signature for ``Panel`` Bug Fixes ~~~~~~~~~ diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 413cb0b6ef3d0..fce6896027867 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -2897,64 +2897,6 @@ def reorder_levels(self, order, axis=0): result.columns = result.columns.reorder_levels(order) return result - #---------------------------------------------------------------------- - # Rename - - def rename(self, index=None, columns=None, copy=True, inplace=False): - """ - Alter index and / or columns 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. - - Parameters - ---------- - index : dict-like or function, optional - Transformation to apply to index values - columns : dict-like or function, optional - Transformation to apply to column values - copy : boolean, default True - Also copy underlying data - inplace : boolean, default False - Whether to return a new DataFrame. If True then value of copy is - ignored. - - See also - -------- - Series.rename - - Returns - ------- - renamed : DataFrame (new object) - """ - from pandas.core.series import _get_rename_function - - if index is None and columns is None: - raise Exception('must pass either index or columns') - - index_f = _get_rename_function(index) - columns_f = _get_rename_function(columns) - - self._consolidate_inplace() - - result = self if inplace else self.copy(deep=copy) - - if index is not None: - result._rename_index_inplace(index_f) - - if columns is not None: - result._rename_columns_inplace(columns_f) - - if not inplace: - return result - - def _rename_index_inplace(self, mapper): - self._data = self._data.rename_axis(mapper, axis=1) - self._clear_item_cache() - - def _rename_columns_inplace(self, mapper): - self._data = self._data.rename_items(mapper, copydata=False) - self._clear_item_cache() - #---------------------------------------------------------------------- # Arithmetic / combination related diff --git a/pandas/core/generic.py b/pandas/core/generic.py index dccf3c9b8d36a..56c37ff3c7a0a 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -13,10 +13,11 @@ from pandas.core.internals import BlockManager import pandas.core.common as com from pandas import compat -from pandas.compat import map, zip +from pandas.compat import map, zip, lrange from pandas.core.common import (isnull, notnull, is_list_like, _values_from_object, - _infer_dtype_from_scalar, _maybe_promote) + _infer_dtype_from_scalar, _maybe_promote, + ABCSeries) class NDFrame(PandasObject): @@ -382,7 +383,77 @@ def swaplevel(self, i, j, axis=0): result._data.set_axis(axis, labels.swaplevel(i, j)) return result - def rename_axis(self, mapper, axis=0, copy=True): + #---------------------------------------------------------------------- + # Rename + + def rename(self, *args, **kwargs): + """ + Alter axes 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. + + 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 + inplace : boolean, default False + Whether to return a new PandasObject. If True then value of copy is + ignored. + + Returns + ------- + renamed : PandasObject (new object) + """ + + axes, kwargs = self._construct_axes_from_arguments(args, kwargs) + copy = kwargs.get('copy', True) + inplace = kwargs.get('inplace', False) + + if (com._count_not_none(*axes.values()) == 0): + raise Exception('must pass an index to rename') + + # renamer function if passed a dict + def _get_rename_function(mapper): + if isinstance(mapper, (dict, ABCSeries)): + def f(x): + if x in mapper: + return mapper[x] + else: + return x + else: + f = mapper + + return f + + + self._consolidate_inplace() + result = self if inplace else self.copy(deep=copy) + + # start in the axis order to eliminate too many copies + for axis in lrange(self._AXIS_LEN): + v = axes.get(self._AXIS_NAMES[axis]) + if v is None: continue + f = _get_rename_function(v) + + baxis = self._get_block_manager_axis(axis) + result._data = result._data.rename(f, axis=baxis, copy=copy) + result._clear_item_cache() + + if inplace: + self._data = result._data + self._clear_item_cache() + + else: + return result._propogate_attributes(self) + + def rename_axis(self, mapper, axis=0, copy=True, inplace=False): """ Alter index and / or columns using input function or functions. Function / dict values must be unique (1-to-1). Labels not contained in @@ -394,24 +465,16 @@ def rename_axis(self, mapper, axis=0, copy=True): axis : int, default 0 copy : boolean, default True Also copy underlying data + inplace : boolean, default False Returns ------- renamed : type of caller """ - # should move this at some point - from pandas.core.series import _get_rename_function - - mapper_f = _get_rename_function(mapper) - - if axis == 0: - new_data = self._data.rename_items(mapper_f, copydata=copy) - else: - new_data = self._data.rename_axis(mapper_f, axis=axis) - if copy: - new_data = new_data.copy() - - return self._constructor(new_data) + axis = self._AXIS_NAMES[axis] + d = { 'copy' : copy, 'inplace' : inplace } + d[axis] = mapper + return self.rename(**d) #---------------------------------------------------------------------- # Comparisons @@ -1373,7 +1436,7 @@ def copy(self, deep=True): data = self._data if deep: data = data.copy() - return self._constructor(data) + return self._constructor(data)._propogate_attributes(self) def convert_objects(self, convert_dates=True, convert_numeric=False, copy=True): """ diff --git a/pandas/core/internals.py b/pandas/core/internals.py index ecce508284fc1..82877d1ddae7e 100644 --- a/pandas/core/internals.py +++ b/pandas/core/internals.py @@ -2758,8 +2758,8 @@ def rrenamer(x): return '%s%s' % (x, rsuffix) return x - this = self.rename_items(lrenamer, copydata=copydata) - other = other.rename_items(rrenamer, copydata=copydata) + this = self.rename_items(lrenamer, copy=copydata) + other = other.rename_items(rrenamer, copy=copydata) else: this = self @@ -2777,6 +2777,13 @@ def _is_indexed_like(self, other): return False return True + def rename(self, mapper, axis, copy=False): + """ generic rename """ + + if axis == 0: + return self.rename_items(mapper, copy=copy) + return self.rename_axis(mapper, axis=axis) + def rename_axis(self, mapper, axis=1): index = self.axes[axis] @@ -2793,7 +2800,7 @@ def rename_axis(self, mapper, axis=1): new_axes[axis] = new_axis return self.__class__(self.blocks, new_axes) - def rename_items(self, mapper, copydata=True): + def rename_items(self, mapper, copy=True): if isinstance(self.items, MultiIndex): items = [tuple(mapper(y) for y in x) for x in self.items] new_items = MultiIndex.from_tuples(items, names=self.items.names) @@ -2803,7 +2810,7 @@ def rename_items(self, mapper, copydata=True): new_blocks = [] for block in self.blocks: - newb = block.copy(deep=copydata) + newb = block.copy(deep=copy) newb.set_ref_items(new_items, maybe_rename=True) new_blocks.append(newb) new_axes = list(self.axes) diff --git a/pandas/core/series.py b/pandas/core/series.py index 051b445638f5b..0fc3341c52117 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -1437,32 +1437,6 @@ def values(self): """ return self._data.values - def copy(self, order='C', deep=False): - """ - Return new Series with copy of underlying values - - Parameters - ---------- - deep : boolean, default False - deep copy index along with data - order : boolean, default 'C' - order for underlying numpy array - - Returns - ------- - cp : Series - """ - if deep: - from copy import deepcopy - index = self.index.copy(deep=deep) - name = deepcopy(self.name) - else: - index = self.index - name = self.name - - return Series(self.values.copy(order), index=index, - name=name) - def get_values(self): """ same as values (but handles sparseness conversions); is a view """ return self._data.values @@ -3090,48 +3064,6 @@ def interpolate(self, method='linear'): return self._constructor(result, index=self.index, name=self.name) - def rename(self, mapper, inplace=False): - """ - Alter Series index using dict or function - - Parameters - ---------- - mapper : dict-like or function - Transformation to apply to each index - - Notes - ----- - Function / dict values must be unique (1-to-1) - - Examples - -------- - >>> x - foo 1 - bar 2 - baz 3 - - >>> x.rename(str.upper) - FOO 1 - BAR 2 - BAZ 3 - - >>> x.rename({'foo' : 'a', 'bar' : 'b', 'baz' : 'c'}) - a 1 - b 2 - c 3 - - Returns - ------- - renamed : Series (new object) - """ - mapper_f = _get_rename_function(mapper) - result = self if inplace else self.copy() - result.index = Index([mapper_f(x) - for x in self.index], name=self.index.name) - - if not inplace: - return result - @property def weekday(self): return self._constructor([d.weekday() for d in self.index], index=self.index) @@ -3379,20 +3311,6 @@ def _try_cast(arr, take_fast_path): return subarr - -def _get_rename_function(mapper): - if isinstance(mapper, (dict, Series)): - def f(x): - if x in mapper: - return mapper[x] - else: - return x - else: - f = mapper - - return f - - def _resolve_offset(freq, kwds): if 'timeRule' in kwds or 'offset' in kwds: offset = kwds.get('offset', None) diff --git a/pandas/tests/test_generic.py b/pandas/tests/test_generic.py new file mode 100644 index 0000000000000..9a147b4e69f38 --- /dev/null +++ b/pandas/tests/test_generic.py @@ -0,0 +1,95 @@ +# pylint: disable-msg=E1101,W0612 + +from datetime import datetime, timedelta +import operator +import unittest +import nose + +import numpy as np +import pandas as pd + +from pandas import (Index, Series, DataFrame, Panel, + isnull, notnull,date_range) +from pandas.core.index import Index, MultiIndex +from pandas.tseries.index import Timestamp, DatetimeIndex + +import pandas.core.common as com + +from pandas.compat import StringIO, lrange, range, zip, u, OrderedDict, long +from pandas import compat +from pandas.util.testing import (assert_series_equal, + assert_frame_equal, + assert_panel_equal, + assert_almost_equal, + ensure_clean) +import pandas.util.testing as tm + +#------------------------------------------------------------------------------ +# Generic types test cases + + +class Generic(object): + + _multiprocess_can_split_ = True + + def setUp(self): + import warnings + warnings.filterwarnings(action='ignore', category=FutureWarning) + + def _axes(self): + """ return the axes for my object typ """ + return self._typ._AXIS_ORDERS + + def _construct(self, shape=None, **kwargs): + """ construct an object for the given shape """ + + if isinstance(shape,int): + shape = tuple([shape] * self._typ._AXIS_LEN) + return self._typ(np.random.randn(*shape),**kwargs) + + def _compare(self, result, expected): + self._comparator(result,expected) + + def test_rename(self): + + # single axis + for axis in self._axes(): + kwargs = { axis : list('ABCD') } + o = self._construct(4,**kwargs) + + # no values passed + #self.assertRaises(Exception, o.rename(str.lower)) + + # rename a single axis + result = o.rename(**{ axis : str.lower }) + expected = o.copy() + setattr(expected,axis,list('abcd')) + self._compare(result, expected) + + # multiple axes at once + +class TestSeries(unittest.TestCase, Generic): + _typ = Series + _comparator = lambda self, x, y: assert_series_equal(x,y) + + def test_rename_mi(self): + s = Series([11,21,31], + index=MultiIndex.from_tuples([("A",x) for x in ["a","B","c"]])) + result = s.rename(str.lower) + +class TestDataFrame(unittest.TestCase, Generic): + _typ = DataFrame + _comparator = lambda self, x, y: assert_frame_equal(x,y) + + def test_rename_mi(self): + df = DataFrame([11,21,31], + index=MultiIndex.from_tuples([("A",x) for x in ["a","B","c"]])) + result = df.rename(str.lower) + +class TestPanel(unittest.TestCase, Generic): + _typ = Panel + _comparator = lambda self, x, y: assert_panel_equal(x,y) + +if __name__ == '__main__': + nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'], + exit=False)