diff --git a/pandas/core/common.py b/pandas/core/common.py index c34dfedc7130c..80ee9cd34779d 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -2554,7 +2554,8 @@ def save(obj, path): # TODO remove in 0.13 def _maybe_match_name(a, b): - name = None - if a.name == b.name: - name = a.name - return name + a_name = getattr(a,'name',None) + b_name = getattr(b,'name',None) + if a_name == b_name: + return a_name + return None diff --git a/pandas/core/generic.py b/pandas/core/generic.py index a5da0b4f23c9a..4dfe0a55fce28 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -47,8 +47,7 @@ def _single_replace(self, to_replace, method, inplace, limit): if values.dtype == orig_dtype and inplace: return - result = pd.Series(values, index=self.index, name=self.name, - dtype=self.dtype) + result = pd.Series(values, index=self.index, dtype=self.dtype).__finalize__(self) if inplace: self._data = result._data @@ -72,7 +71,7 @@ class NDFrame(PandasObject): _internal_names = [ '_data', 'name', '_cacher', '_subtyp', '_index', '_default_kind', '_default_fill_value'] _internal_names_set = set(_internal_names) - _prop_attributes = [] + _metadata = [] def __init__(self, data, axes=None, copy=False, dtype=None, fastpath=False): @@ -413,7 +412,7 @@ def transpose(self, *args, **kwargs): new_values = self.values.transpose(axes_numbers) if kwargs.get('copy') or (len(args) and args[-1]): new_values = new_values.copy() - return self._constructor(new_values, **new_axes) + return self._constructor(new_values, **new_axes).__finalize__(self) def swapaxes(self, axis1, axis2, copy=True): """ @@ -439,7 +438,7 @@ def swapaxes(self, axis1, axis2, copy=True): if copy: new_values = new_values.copy() - return self._constructor(new_values, *new_axes) + return self._constructor(new_values, *new_axes).__finalize__(self) def pop(self, item): """ @@ -543,7 +542,7 @@ def f(x): self._clear_item_cache() else: - return result._propogate_attributes(self) + return result.__finalize__(self) rename.__doc__ = _shared_docs['rename'] @@ -655,14 +654,14 @@ def __abs__(self): def _wrap_array(self, arr, axes, copy=False): d = self._construct_axes_dict_from(self, axes, copy=copy) - return self._constructor(arr, **d) + return self._constructor(arr, **d).__finalize__(self) def __array__(self, dtype=None): return _values_from_object(self) def __array_wrap__(self, result): d = self._construct_axes_dict(self._AXIS_ORDERS, copy=False) - return self._constructor(result, **d) + return self._constructor(result, **d).__finalize__(self) def to_dense(self): # compat @@ -1024,7 +1023,7 @@ def take(self, indices, axis=0, convert=True): new_data = self._data.reindex_axis(new_items, indexer=indices, axis=0) else: new_data = self._data.take(indices, axis=baxis) - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) # TODO: Check if this was clearer in 0.12 def select(self, crit, axis=0): @@ -1135,7 +1134,7 @@ def add_prefix(self, prefix): with_prefix : type of caller """ new_data = self._data.add_prefix(prefix) - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def add_suffix(self, suffix): """ @@ -1150,7 +1149,7 @@ def add_suffix(self, suffix): with_suffix : type of caller """ new_data = self._data.add_suffix(suffix) - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def sort_index(self, axis=0, ascending=True): """ @@ -1244,7 +1243,8 @@ def reindex(self, *args, **kwargs): return self # perform the reindex on the axes - return self._reindex_axes(axes, level, limit, method, fill_value, copy, takeable=takeable)._propogate_attributes(self) + return self._reindex_axes(axes, level, limit, + method, fill_value, copy, takeable=takeable).__finalize__(self) def _reindex_axes(self, axes, level, limit, method, fill_value, copy, takeable=False): """ perform the reinxed for all the axes """ @@ -1332,7 +1332,7 @@ def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True, new_index, indexer = axis_values.reindex(labels, method, level, limit=limit, copy_if_needed=True) return self._reindex_with_indexers({axis: [new_index, indexer]}, method=method, fill_value=fill_value, - limit=limit, copy=copy)._propogate_attributes(self) + limit=limit, copy=copy).__finalize__(self) def _reindex_with_indexers(self, reindexers, method=None, fill_value=np.nan, limit=None, copy=False, allow_dups=False): """ allow_dups indicates an internal call here """ @@ -1370,7 +1370,7 @@ def _reindex_with_indexers(self, reindexers, method=None, fill_value=np.nan, lim if copy and new_data is self._data: new_data = new_data.copy() - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def _reindex_axis(self, new_index, fill_method, axis, copy): new_data = self._data.reindex_axis(new_index, axis=axis, @@ -1379,7 +1379,7 @@ def _reindex_axis(self, new_index, fill_method, axis, copy): if new_data is self._data and not copy: return self else: - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def filter(self, items=None, like=None, regex=None, axis=None): """ @@ -1421,9 +1421,18 @@ def filter(self, items=None, like=None, regex=None, axis=None): #---------------------------------------------------------------------- # Attribute access - def _propogate_attributes(self, other): - """ propogate attributes from other to self""" - for name in self._prop_attributes: + def __finalize__(self, other, method=None, **kwargs): + """ + propagate metadata from other to self + + Parameters + ---------- + other : the object from which to get the attributes that we are going to propagate + method : optional, a passed method name ; possibily to take different types + of propagation actions based on this + + """ + for name in self._metadata: object.__setattr__(self, name, getattr(other, name, None)) return self @@ -1484,7 +1493,7 @@ def consolidate(self, inplace=False): cons_data = self._protect_consolidate(f) if cons_data is self._data: cons_data = cons_data.copy() - return self._constructor(cons_data) + return self._constructor(cons_data).__finalize__(self) @property def _is_mixed_type(self): @@ -1504,10 +1513,10 @@ def _protect_consolidate(self, f): return result def _get_numeric_data(self): - return self._constructor(self._data.get_numeric_data()) + return self._constructor(self._data.get_numeric_data()).__finalize__(self) def _get_bool_data(self): - return self._constructor(self._data.get_bool_data()) + return self._constructor(self._data.get_bool_data()).__finalize__(self) #---------------------------------------------------------------------- # Internal Interface Methods @@ -1584,7 +1593,7 @@ def as_blocks(self, columns=None): for b in self._data.blocks: b = b.reindex_items_from(columns or b.items) bd[str(b.dtype)] = self._constructor( - BlockManager([b], [b.items, self.index])) + BlockManager([b], [b.items, self.index])).__finalize__(self) return bd @property @@ -1608,7 +1617,7 @@ def astype(self, dtype, copy=True, raise_on_error=True): mgr = self._data.astype( dtype, copy=copy, raise_on_error=raise_on_error) - return self._constructor(mgr)._propogate_attributes(self) + return self._constructor(mgr).__finalize__(self) def copy(self, deep=True): """ @@ -1626,7 +1635,7 @@ def copy(self, deep=True): data = self._data if deep: data = data.copy() - return self._constructor(data)._propogate_attributes(self) + return self._constructor(data).__finalize__(self) def convert_objects(self, convert_dates=True, convert_numeric=False, copy=True): """ @@ -1642,7 +1651,9 @@ def convert_objects(self, convert_dates=True, convert_numeric=False, copy=True): ------- converted : asm as input object """ - return self._constructor(self._data.convert(convert_dates=convert_dates, convert_numeric=convert_numeric, copy=copy)) + return self._constructor(self._data.convert(convert_dates=convert_dates, + convert_numeric=convert_numeric, + copy=copy)).__finalize__(self) #---------------------------------------------------------------------- # Filling NA's @@ -1713,7 +1724,7 @@ def fillna(self, value=None, method=None, axis=0, inplace=False, # fill in 2d chunks result = dict([ (col,s.fillna(method=method, value=value)) for col, s in compat.iteritems(self) ]) - return self._constructor.from_dict(result) + return self._constructor.from_dict(result).__finalize__(self) # 2d or less method = com._clean_fill_method(method) @@ -1750,7 +1761,7 @@ def fillna(self, value=None, method=None, axis=0, inplace=False, if inplace: self._data = new_data else: - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def ffill(self, axis=0, inplace=False, limit=None, downcast=None): return self.fillna(method='ffill', axis=axis, inplace=inplace, @@ -1991,7 +2002,7 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None, if inplace: self._data = new_data else: - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def interpolate(self, method='linear', axis=0, limit=None, inplace=False, downcast='infer', **kwargs): @@ -2101,7 +2112,7 @@ def interpolate(self, method='linear', axis=0, limit=None, inplace=False, else: self._data = new_data else: - res = self._constructor(new_data, index=self.index) + res = self._constructor(new_data).__finalize__(self) if axis == 1: res = res.T return res @@ -2113,13 +2124,13 @@ def isnull(self): """ Return a boolean same-sized object indicating if the values are null """ - return self.__class__(isnull(self),**self._construct_axes_dict())._propogate_attributes(self) + return self.__class__(isnull(self),**self._construct_axes_dict()).__finalize__(self) def notnull(self): """ Return a boolean same-sized object indicating if the values are not null """ - return self.__class__(notnull(self),**self._construct_axes_dict())._propogate_attributes(self) + return self.__class__(notnull(self),**self._construct_axes_dict()).__finalize__(self) def clip(self, lower=None, upper=None, out=None): """ @@ -2484,7 +2495,7 @@ def _align_frame(self, other, join='outer', axis=None, level=None, left = left.fillna(axis=fill_axis, method=method, limit=limit) right = right.fillna(axis=fill_axis, method=method, limit=limit) - return left, right + return left.__finalize__(self), right.__finalize__(other) def _align_series(self, other, join='outer', axis=None, level=None, copy=True, fill_value=None, method=None, limit=None, @@ -2543,7 +2554,7 @@ def _align_series(self, other, join='outer', axis=None, level=None, right_result.fillna(fill_value, method=method, limit=limit)) else: - return left_result, right_result + return left_result.__finalize__(self), right_result.__finalize__(other) def where(self, cond, other=np.nan, inplace=False, axis=None, level=None, try_cast=False, raise_on_error=True): @@ -2680,7 +2691,7 @@ def where(self, cond, other=np.nan, inplace=False, axis=None, level=None, new_data = self._data.where( other, cond, align=axis is None, raise_on_error=raise_on_error, try_cast=try_cast) - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def mask(self, cond): """ @@ -2728,7 +2739,7 @@ def shift(self, periods=1, freq=None, axis=0, **kwds): else: return self.tshift(periods, freq, **kwds) - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def tshift(self, periods=1, freq=None, axis=0, **kwds): """ @@ -2789,7 +2800,7 @@ def tshift(self, periods=1, freq=None, axis=0, **kwds): new_data = self._data.copy() new_data.axes[block_axis] = index.shift(periods, offset) - return self._constructor(new_data) + return self._constructor(new_data).__finalize__(self) def truncate(self, before=None, after=None, copy=True): """Truncates a sorted NDFrame before and/or after some particular @@ -2864,7 +2875,7 @@ def tz_convert(self, tz, axis=0, copy=True): new_obj._set_axis(0, new_ax) self._clear_item_cache() - return new_obj + return new_obj.__finalize__(self) def tz_localize(self, tz, axis=0, copy=True, infer_dst=False): """ @@ -2902,7 +2913,7 @@ def tz_localize(self, tz, axis=0, copy=True, infer_dst=False): new_obj._set_axis(0, new_ax) self._clear_item_cache() - return new_obj + return new_obj.__finalize__(self) #---------------------------------------------------------------------- # Numeric Methods @@ -3128,7 +3139,7 @@ def func(self, axis=None, dtype=None, out=None, skipna=True, **kwargs): d = self._construct_axes_dict() d['copy'] = False - return self._constructor(result, **d)._propogate_attributes(self) + return self._constructor(result, **d).__finalize__(self) func.__name__ = name return func diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 17d524978158b..f8ab35656d99c 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -515,7 +515,7 @@ def wrapper(self, other): if len(self) != len(other): raise ValueError('Lengths must match to compare') return self._constructor(na_op(self.values, np.asarray(other)), - index=self.index, name=self.name) + index=self.index).__finalize__(self) else: mask = isnull(self) @@ -590,7 +590,7 @@ def wrapper(self, other): else: # scalars return self._constructor(na_op(self.values, other), - index=self.index, name=self.name).fillna(False).astype(bool) + index=self.index).fillna(False).astype(bool).__finalize__(self) return wrapper @@ -643,8 +643,8 @@ def f(self, other, level=None, fill_value=None): return self._binop(self._constructor(other, self.index), op, level=level, fill_value=fill_value) else: - return self._constructor(op(self.values, other), self.index, - name=self.name) + return self._constructor(op(self.values, other), + self.index).__finalize__(self) f.__name__ = name return f diff --git a/pandas/core/series.py b/pandas/core/series.py index fba3e946de0b0..526355b0f4dc3 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -116,7 +116,7 @@ class Series(generic.NDFrame): If None, dtype will be inferred copy : boolean, default False, copy input data """ - _prop_attributes = ['name'] + _metadata = ['name'] def __init__(self, data=None, index=None, dtype=None, name=None, copy=False, fastpath=False): @@ -334,8 +334,7 @@ def __len__(self): return len(self._data) def view(self, dtype=None): - return self._constructor(self.values.view(dtype), index=self.index, - name=self.name) + return self._constructor(self.values.view(dtype), index=self.index).__finalize__(self) def __array__(self, result=None): """ the array interface, return my values """ @@ -345,8 +344,7 @@ def __array_wrap__(self, result): """ Gets called prior to a ufunc (and after) """ - return self._constructor(result, index=self.index, name=self.name, - copy=False) + return self._constructor(result, index=self.index, copy=False).__finalize__(self) def __contains__(self, key): return key in self.index @@ -473,8 +471,7 @@ def _slice(self, slobj, axis=0, raise_on_error=False, typ=None): if raise_on_error: _check_slice_bounds(slobj, self.values) slobj = self.index._convert_slice_indexer(slobj,typ=typ or 'getitem') - return self._constructor(self.values[slobj], index=self.index[slobj], - name=self.name) + return self._constructor(self.values[slobj], index=self.index[slobj]).__finalize__(self) def __getitem__(self, key): try: @@ -565,12 +562,11 @@ def _get_values_tuple(self, key): # If key is contained, would have returned by now indexer, new_index = self.index.get_loc_level(key) - return self._constructor(self.values[indexer], index=new_index, name=self.name) + return self._constructor(self.values[indexer], index=new_index).__finalize__(self) def _get_values(self, indexer): try: - return self._constructor(self._data.get_slice(indexer), - name=self.name, fastpath=True) + return self._constructor(self._data.get_slice(indexer), fastpath=True).__finalize__(self) except Exception: return self.values[indexer] @@ -704,7 +700,7 @@ def repeat(self, reps): """ new_index = self.index.repeat(reps) new_values = self.values.repeat(reps) - return self._constructor(new_values, index=new_index, name=self.name) + return self._constructor(new_values, index=new_index).__finalize__(self) def reshape(self, *args, **kwargs): """ @@ -823,8 +819,7 @@ def reset_index(self, level=None, drop=False, name=None, inplace=False): # set name if it was passed, otherwise, keep the previous name self.name = name or self.name else: - return self._constructor(self.values.copy(), index=new_index, - name=self.name) + return self._constructor(self.values.copy(), index=new_index).__finalize__(self) elif inplace: raise TypeError('Cannot reset_index inplace on a Series ' 'to create a DataFrame') @@ -977,11 +972,11 @@ def iteritems(self): # inversion def __neg__(self): arr = operator.neg(self.values) - return self._constructor(arr, self.index, name=self.name) + return self._constructor(arr, self.index).__finalize__(self) def __invert__(self): arr = operator.inv(self.values) - return self._constructor(arr, self.index, name=self.name) + return self._constructor(arr, self.index).__finalize__(self) #---------------------------------------------------------------------- # unbox reductions @@ -1060,8 +1055,8 @@ def to_sparse(self, kind='block', fill_value=None): sp : SparseSeries """ from pandas.core.sparse import SparseSeries - return SparseSeries(self, kind=kind, fill_value=fill_value, - name=self.name) + return SparseSeries(self, kind=kind, + fill_value=fill_value).__finalize__(self) def head(self, n=5): """Returns first n rows of Series @@ -1101,14 +1096,14 @@ def count(self, level=None): level_index = self.index.levels[level] if len(self) == 0: - return self._constructor(0, index=level_index) + return self._constructor(0, index=level_index).__finalize__(self) # call cython function max_bin = len(level_index) labels = com._ensure_int64(self.index.labels[level]) counts = lib.count_level_1d(mask.view(pa.uint8), labels, max_bin) - return self._constructor(counts, index=level_index) + return self._constructor(counts, index=level_index).__finalize__(self) return notnull(_values_from_object(self)).sum() @@ -1191,7 +1186,7 @@ def duplicated(self, take_last=False): """ keys = _ensure_object(self.values) duplicated = lib.duplicated(keys, take_last=take_last) - return self._constructor(duplicated, index=self.index, name=self.name) + return self._constructor(duplicated, index=self.index).__finalize__(self) def idxmin(self, axis=None, out=None, skipna=True): """ @@ -1256,8 +1251,7 @@ def round(self, decimals=0, out=None): """ result = _values_from_object(self).round(decimals, out=out) if out is None: - result = self._constructor( - result, index=self.index, name=self.name) + result = self._constructor(result, index=self.index).__finalize__(self) return result @@ -1351,7 +1345,7 @@ def pretty_name(x): lb), self.median(), self.quantile(ub), self.max()] - return self._constructor(data, index=names) + return self._constructor(data, index=names).__finalize__(self) def corr(self, other, method='pearson', min_periods=None): @@ -1415,7 +1409,7 @@ def diff(self, periods=1): diffed : Series """ result = com.diff(_values_from_object(self), periods) - return self._constructor(result, self.index, name=self.name) + return self._constructor(result, index=self.index).__finalize__(self) def autocorr(self): """ @@ -1460,7 +1454,7 @@ def dot(self, other): if isinstance(other, DataFrame): return self._constructor(np.dot(lvals, rvals), - index=other.columns) + index=other.columns).__finalize__(self) elif isinstance(other, Series): return np.dot(lvals, rvals) elif isinstance(rvals, np.ndarray): @@ -1539,7 +1533,7 @@ def _binop(self, other, func, level=None, fill_value=None): result = func(this_vals, other_vals) name = _maybe_match_name(self, other) - return self._constructor(result, index=new_index, name=name) + return self._constructor(result, index=new_index).__finalize__(self) def combine(self, other, func, fill_value=nan): """ @@ -1589,7 +1583,7 @@ def combine_first(self, other): other = other.reindex(new_index, copy=False) name = _maybe_match_name(self, other) rs_vals = com._where_compat(isnull(this), other.values, this.values) - return self._constructor(rs_vals, index=new_index, name=name) + return self._constructor(rs_vals, index=new_index).__finalize__(self) def update(self, other): """ @@ -1673,7 +1667,7 @@ def sort_index(self, ascending=True): ascending=ascending) new_values = self.values.take(indexer) - return self._constructor(new_values, new_labels, name=self.name) + return self._constructor(new_values, index=new_labels).__finalize__(self) def argsort(self, axis=0, kind='quicksort', order=None): """ @@ -1701,11 +1695,11 @@ def argsort(self, axis=0, kind='quicksort', order=None): -1, index=self.index, name=self.name, dtype='int64') notmask = -mask result[notmask] = np.argsort(values[notmask], kind=kind) - return self._constructor(result, index=self.index, name=self.name) + return self._constructor(result, index=self.index).__finalize__(self) else: return self._constructor( np.argsort(values, kind=kind), index=self.index, - name=self.name, dtype='int64') + dtype='int64').__finalize__(self) def rank(self, method='average', na_option='keep', ascending=True): """ @@ -1731,7 +1725,7 @@ def rank(self, method='average', na_option='keep', ascending=True): from pandas.core.algorithms import rank ranks = rank(self.values, method=method, na_option=na_option, ascending=ascending) - return self._constructor(ranks, index=self.index, name=self.name) + return self._constructor(ranks, index=self.index).__finalize__(self) def order(self, na_last=True, ascending=True, kind='mergesort'): """ @@ -1783,8 +1777,8 @@ def _try_kind_sort(arr): sortedIdx[n:] = idx[good][argsorted] sortedIdx[:n] = idx[bad] - return self._constructor(arr[sortedIdx], index=self.index[sortedIdx], - name=self.name) + return self._constructor(arr[sortedIdx], + index=self.index[sortedIdx]).__finalize__(self) def sortlevel(self, level=0, ascending=True): """ @@ -1806,7 +1800,7 @@ def sortlevel(self, level=0, ascending=True): new_index, indexer = self.index.sortlevel(level, ascending=ascending) new_values = self.values.take(indexer) - return self._constructor(new_values, index=new_index, name=self.name) + return self._constructor(new_values, index=new_index).__finalize__(self) def swaplevel(self, i, j, copy=True): """ @@ -1822,7 +1816,8 @@ def swaplevel(self, i, j, copy=True): swapped : Series """ new_index = self.index.swaplevel(i, j) - return self._constructor(self.values, index=new_index, copy=copy, name=self.name) + return self._constructor(self.values, index=new_index, + copy=copy).__finalize__(self) def reorder_levels(self, order): """ @@ -1934,10 +1929,10 @@ def map_f(values, f): indexer = arg.index.get_indexer(values) new_values = com.take_1d(arg.values, indexer) - return self._constructor(new_values, index=self.index, name=self.name) + return self._constructor(new_values, index=self.index).__finalize__(self) else: mapped = map_f(values, arg) - return self._constructor(mapped, index=self.index, name=self.name) + return self._constructor(mapped, index=self.index).__finalize__(self) def apply(self, func, convert_dtype=True, args=(), **kwds): """ @@ -1980,7 +1975,7 @@ def apply(self, func, convert_dtype=True, args=(), **kwds): from pandas.core.frame import DataFrame return DataFrame(mapped.tolist(), index=self.index) else: - return self._constructor(mapped, index=self.index, name=self.name) + return self._constructor(mapped, index=self.index).__finalize__(self) def _reduce(self, op, axis=0, skipna=True, numeric_only=None, filter_type=None, **kwds): @@ -1995,7 +1990,7 @@ def _reindex_indexer(self, new_index, indexer, copy): # be subclass-friendly new_values = com.take_1d(self.get_values(), indexer) - return self._constructor(new_values, new_index, name=self.name) + return self._constructor(new_values, index=new_index) def _needs_reindex_multi(self, axes, method, level): """ check if we do need a multi reindex; this is for compat with higher dims """ @@ -2037,7 +2032,7 @@ def take(self, indices, axis=0, convert=True): indices = com._ensure_platform_int(indices) new_index = self.index.take(indices) new_values = self.values.take(indices) - return self._constructor(new_values, index=new_index, name=self.name) + return self._constructor(new_values, index=new_index).__finalize__(self) def isin(self, values): """ @@ -2084,7 +2079,7 @@ def isin(self, values): "{0!r}".format(type(values).__name__)) value_set = set(values) result = lib.ismember(_values_from_object(self), value_set) - return self._constructor(result, self.index, name=self.name) + return self._constructor(result, index=self.index).__finalize__(self) def between(self, left, right, inclusive=True): """ @@ -2270,11 +2265,11 @@ def asof(self, where): locs = self.index.asof_locs(where, notnull(values)) new_values = com.take_1d(values, locs) - return self._constructor(new_values, index=where, name=self.name) + return self._constructor(new_values, index=where).__finalize__(self) @property def weekday(self): - return self._constructor([d.weekday() for d in self.index], index=self.index) + return self._constructor([d.weekday() for d in self.index], index=self.index).__finalize__(self) def tz_convert(self, tz, copy=True): """ @@ -2296,7 +2291,7 @@ def tz_convert(self, tz, copy=True): if copy: new_values = new_values.copy() - return self._constructor(new_values, index=new_index, name=self.name) + return self._constructor(new_values, index=new_index).__finalize__(self) def tz_localize(self, tz, copy=True, infer_dst=False): """ @@ -2333,7 +2328,7 @@ def tz_localize(self, tz, copy=True, infer_dst=False): if copy: new_values = new_values.copy() - return self._constructor(new_values, index=new_index, name=self.name) + return self._constructor(new_values, index=new_index).__finalize__(self) @cache_readonly def str(self): @@ -2361,7 +2356,7 @@ def to_timestamp(self, freq=None, how='start', copy=True): new_values = new_values.copy() new_index = self.index.to_timestamp(freq=freq, how=how) - return self._constructor(new_values, index=new_index, name=self.name) + return self._constructor(new_values, index=new_index).__finalize__(self) def to_period(self, freq=None, copy=True): """ @@ -2383,7 +2378,7 @@ def to_period(self, freq=None, copy=True): if freq is None: freq = self.index.freqstr or self.index.inferred_freq new_index = self.index.to_period(freq=freq) - return self._constructor(new_values, index=new_index, name=self.name) + return self._constructor(new_values, index=new_index).__finalize__(self) Series._setup_axes(['index'], info_axis=0, stat_axis=0) Series._add_numeric_operations() diff --git a/pandas/sparse/frame.py b/pandas/sparse/frame.py index ce29b6974de86..88464d683d543 100644 --- a/pandas/sparse/frame.py +++ b/pandas/sparse/frame.py @@ -124,7 +124,7 @@ def __init__(self, data=None, index=None, columns=None, @property def _constructor(self): - def wrapper(data, index=None, columns=None, default_fill_value=None, kind=None, fill_value=None, copy=False): + def wrapper(data=None, index=None, columns=None, default_fill_value=None, kind=None, fill_value=None, copy=False): result = SparseDataFrame(data, index=index, columns=columns, default_fill_value=fill_value, default_kind=kind, @@ -205,7 +205,7 @@ def _init_matrix(self, data, index, columns, dtype=None): def __array_wrap__(self, result): return SparseDataFrame(result, index=self.index, columns=self.columns, default_kind=self._default_kind, - default_fill_value=self._default_fill_value) + default_fill_value=self._default_fill_value).__finalize__(self) def __getstate__(self): # pickling @@ -420,7 +420,7 @@ def _combine_frame(self, other, func, fill_value=None, level=None): raise NotImplementedError if self.empty and other.empty: - return SparseDataFrame(index=new_index) + return SparseDataFrame(index=new_index).__finalize__(self) new_data = {} new_fill_value = None @@ -452,7 +452,7 @@ def _combine_frame(self, other, func, fill_value=None, level=None): index=new_index, columns=new_columns, default_fill_value=new_fill_value, - fill_value=new_fill_value) + fill_value=new_fill_value).__finalize__(self) def _combine_match_index(self, other, func, fill_value=None): new_data = {} @@ -482,7 +482,7 @@ def _combine_match_index(self, other, func, fill_value=None): index=new_index, columns=self.columns, default_fill_value=fill_value, - fill_value=self.default_fill_value) + fill_value=self.default_fill_value).__finalize__(self) def _combine_match_columns(self, other, func, fill_value): # patched version of DataFrame._combine_match_columns to account for @@ -508,7 +508,7 @@ def _combine_match_columns(self, other, func, fill_value): index=self.index, columns=union, default_fill_value=self.default_fill_value, - fill_value=self.default_fill_value) + fill_value=self.default_fill_value).__finalize__(self) def _combine_const(self, other, func): new_data = {} @@ -519,7 +519,7 @@ def _combine_const(self, other, func): index=self.index, columns=self.columns, default_fill_value=self.default_fill_value, - fill_value=self.default_fill_value) + fill_value=self.default_fill_value).__finalize__(self) def _reindex_index(self, index, method, copy, level, fill_value=np.nan, limit=None, takeable=False): @@ -598,7 +598,7 @@ def _reindex_with_indexers(self, reindexers, method=None, fill_value=None, limit else: new_arrays[col] = self[col] - return self._constructor(new_arrays, index=index, columns=columns) + return SparseDataFrame(new_arrays, index=index, columns=columns).__finalize__(self) def _join_compat(self, other, on=None, how='left', lsuffix='', rsuffix='', sort=False): @@ -656,7 +656,7 @@ def transpose(self): return SparseDataFrame(self.values.T, index=self.columns, columns=self.index, default_fill_value=self._default_fill_value, - default_kind=self._default_kind) + default_kind=self._default_kind).__finalize__(self) T = property(transpose) @Appender(DataFrame.count.__doc__) @@ -705,10 +705,10 @@ def apply(self, func, axis=0, broadcast=False, reduce=False): applied = func(v) applied.fill_value = func(applied.fill_value) new_series[k] = applied - return SparseDataFrame(new_series, index=self.index, - columns=self.columns, - default_fill_value=self._default_fill_value, - default_kind=self._default_kind) + return self._constructor(new_series, index=self.index, + columns=self.columns, + default_fill_value=self._default_fill_value, + kind=self._default_kind).__finalize__(self) else: if not broadcast: return self._apply_standard(func, axis, reduce=reduce) diff --git a/pandas/sparse/series.py b/pandas/sparse/series.py index eb97eec75be36..4d8b2578426ec 100644 --- a/pandas/sparse/series.py +++ b/pandas/sparse/series.py @@ -289,7 +289,7 @@ def __array_wrap__(self, result): index=self.index, sparse_index=self.sp_index, fill_value=self.fill_value, - copy=False) + copy=False).__finalize__(self) def __array_finalize__(self, obj): """ @@ -368,7 +368,7 @@ def __getitem__(self, key): key = _values_from_object(key) dataSlice = self.values[key] new_index = Index(self.index.view(ndarray)[key]) - return self._constructor(dataSlice, index=new_index, name=self.name) + return self._constructor(dataSlice, index=new_index).__finalize__(self) def _set_with_engine(self, key, value): return self.set_value(key, value) @@ -383,9 +383,9 @@ def abs(self): abs: type of caller """ res_sp_values = np.abs(self.sp_values) - return SparseSeries(res_sp_values, index=self.index, - sparse_index=self.sp_index, - fill_value=self.fill_value) + return self._constructor(res_sp_values, index=self.index, + sparse_index=self.sp_index, + fill_value=self.fill_value) def get(self, label, default=None): """ @@ -501,7 +501,7 @@ def copy(self, deep=True): return self._constructor(new_data, sparse_index=self.sp_index, - fill_value=self.fill_value, name=self.name) + fill_value=self.fill_value).__finalize__(self) def reindex(self, index=None, method=None, copy=True, limit=None): """ @@ -520,7 +520,8 @@ def reindex(self, index=None, method=None, copy=True, limit=None): return self.copy() else: return self - return self._constructor(self._data.reindex(new_index, method=method, limit=limit, copy=copy), index=new_index, name=self.name) + return self._constructor(self._data.reindex(new_index, method=method, limit=limit, copy=copy), + index=new_index).__finalize__(self) def sparse_reindex(self, new_index): """ @@ -541,7 +542,7 @@ def sparse_reindex(self, new_index): new_data = SingleBlockManager(block, block.ref_items) return self._constructor(new_data, index=self.index, sparse_index=new_index, - fill_value=self.fill_value) + fill_value=self.fill_value).__finalize__(self) def take(self, indices, axis=0, convert=True): """ @@ -553,7 +554,7 @@ def take(self, indices, axis=0, convert=True): """ new_values = SparseArray.take(self.values, indices) new_index = self.index.take(indices) - return self._constructor(new_values, index=new_index) + return self._constructor(new_values, index=new_index).__finalize__(self) def cumsum(self, axis=0, dtype=None, out=None): """ @@ -565,8 +566,8 @@ def cumsum(self, axis=0, dtype=None, out=None): """ new_array = SparseArray.cumsum(self.values) if isinstance(new_array, SparseArray): - return self._constructor(new_array, index=self.index, sparse_index=new_array.sp_index, name=self.name) - return Series(new_array, index=self.index, name=self.name) + return self._constructor(new_array, index=self.index, sparse_index=new_array.sp_index).__finalize__(self) + return Series(new_array, index=self.index).__finalize__(self) def dropna(self): """ @@ -602,7 +603,7 @@ def shift(self, periods, freq=None, **kwds): return self._constructor(self.sp_values, sparse_index=self.sp_index, index=self.index.shift(periods, offset), - fill_value=self.fill_value) + fill_value=self.fill_value).__finalize__(self) int_index = self.sp_index.to_int_index() new_indices = int_index.indices + periods @@ -617,7 +618,7 @@ def shift(self, periods, freq=None, **kwds): return self._constructor(self.sp_values[start:end].copy(), index=self.index, sparse_index=new_sp_index, - fill_value=self.fill_value) + fill_value=self.fill_value).__finalize__(self) def combine_first(self, other): """ diff --git a/pandas/tests/test_generic.py b/pandas/tests/test_generic.py index ce0cb909cf1c5..b3e216526d0f6 100644 --- a/pandas/tests/test_generic.py +++ b/pandas/tests/test_generic.py @@ -224,6 +224,99 @@ def f(dtype): f('float64') f('M8[ns]') + def check_metadata(self, x, y=None): + for m in x._metadata: + v = getattr(x,m,None) + if y is None: + self.assert_(v is None) + else: + self.assert_(v == getattr(y,m,None)) + + def test_metadata_propagation(self): + # check that the metadata matches up on the resulting ops + + o = self._construct(shape=3) + o.name = 'foo' + o2 = self._construct(shape=3) + o2.name = 'bar' + + # TODO + # Once panel can do non-trivial combine operations + # (currently there is an a raise in the Panel arith_ops to prevent + # this, though it actually does work) + # can remove all of these try: except: blocks on the actual operations + + + # ---------- + # preserving + # ---------- + + # simple ops with scalars + for op in [ '__add__','__sub__','__truediv__','__mul__' ]: + result = getattr(o,op)(1) + self.check_metadata(o,result) + + # ops with like + for op in [ '__add__','__sub__','__truediv__','__mul__' ]: + try: + result = getattr(o,op)(o) + self.check_metadata(o,result) + except (ValueError, AttributeError): + pass + + # simple boolean + for op in [ '__eq__','__le__', '__ge__' ]: + v1 = getattr(o,op)(o) + self.check_metadata(o,v1) + + try: + self.check_metadata(o, v1 & v1) + except (ValueError): + pass + + try: + self.check_metadata(o, v1 | v1) + except (ValueError): + pass + + # combine_first + try: + result = o.combine_first(o2) + self.check_metadata(o,result) + except (AttributeError): + pass + + + # --------------------------- + # non-preserving (by default) + # --------------------------- + + # add non-like + try: + result = o + o2 + self.check_metadata(result) + except (ValueError, AttributeError): + pass + + # simple boolean + for op in [ '__eq__','__le__', '__ge__' ]: + + # this is a name matching op + v1 = getattr(o,op)(o) + + v2 = getattr(o,op)(o2) + self.check_metadata(v2) + + try: + self.check_metadata(v1 & v2) + except (ValueError): + pass + + try: + self.check_metadata(v1 | v2) + except (ValueError): + pass + class TestSeries(unittest.TestCase, Generic): _typ = Series _comparator = lambda self, x, y: assert_series_equal(x,y) @@ -292,6 +385,17 @@ def test_nonzero_single_element(self): self.assertRaises(ValueError, lambda : bool(s)) self.assertRaises(ValueError, lambda : s.bool()) + def test_metadata_propagation_indiv(self): + # check that the metadata matches up on the resulting ops + + o = Series(range(3),range(3)) + o.name = 'foo' + o2 = Series(range(3),range(3)) + o2.name = 'bar' + + result = o.T + self.check_metadata(o,result) + def test_interpolate(self): ts = Series(np.arange(len(self.ts), dtype=float), self.ts.index) diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py index ee496673921f7..3715de6dffeb9 100644 --- a/pandas/tests/test_series.py +++ b/pandas/tests/test_series.py @@ -83,6 +83,7 @@ def test_append_preserve_name(self): self.assertEquals(result.name, self.ts.name) def test_binop_maybe_preserve_name(self): + # names match, preserve result = self.ts * self.ts self.assertEquals(result.name, self.ts.name)