diff --git a/RELEASE.rst b/RELEASE.rst index a5f9a9aad9447..db68a9590f329 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -287,6 +287,7 @@ pandas 0.11.0 - Fix Python ascii file parsing when integer falls outside of floating point spacing (GH3258_) - fixed pretty priniting of sets (GH3294_) + - Panel() and Panel.from_dict() now respects ordering when give OrderedDict (GH3303_) .. _GH3294: https://github.com/pydata/pandas/issues/3294 .. _GH622: https://github.com/pydata/pandas/issues/622 diff --git a/pandas/core/panel.py b/pandas/core/panel.py index d1f87e4e7c932..8e18e93e955ef 100644 --- a/pandas/core/panel.py +++ b/pandas/core/panel.py @@ -13,7 +13,7 @@ from pandas.core.index import (Index, MultiIndex, _ensure_index, _get_combined_index) from pandas.core.indexing import _maybe_droplevels, _is_list_like -from pandas.core.internals import (BlockManager, +from pandas.core.internals import (BlockManager, create_block_manager_from_arrays, create_block_manager_from_blocks) from pandas.core.series import Series @@ -274,14 +274,18 @@ def _from_axes(cls, data, axes): return cls(data, **d) def _init_dict(self, data, axes, dtype=None): + from pandas.util.compat import OrderedDict haxis = axes.pop(self._het_axis) # prefilter if haxis passed if haxis is not None: haxis = _ensure_index(haxis) - data = dict((k, v) for k, v in data.iteritems() if k in haxis) + data = OrderedDict((k, v) for k, v in data.iteritems() if k in haxis) else: - haxis = Index(_try_sort(data.keys())) + ks = data.keys() + if not isinstance(data,OrderedDict): + ks = _try_sort(ks) + haxis = Index(ks) for k, v in data.iteritems(): if isinstance(v, dict): @@ -341,11 +345,11 @@ def from_dict(cls, data, intersect=False, orient='items', dtype=None): ------- Panel """ - from collections import defaultdict + from pandas.util.compat import OrderedDict,OrderedDefaultdict orient = orient.lower() if orient == 'minor': - new_data = defaultdict(dict) + new_data = OrderedDefaultdict(dict) for col, df in data.iteritems(): for item, s in df.iteritems(): new_data[item][col] = s @@ -354,7 +358,10 @@ def from_dict(cls, data, intersect=False, orient='items', dtype=None): raise ValueError('only recognize items or minor for orientation') d = cls._homogenize_dict(cls, data, intersect=intersect, dtype=dtype) - d[cls._info_axis] = Index(sorted(d['data'].keys())) + ks = d['data'].keys() + if not isinstance(d['data'],OrderedDict): + ks = list(sorted(ks)) + d[cls._info_axis] = Index(ks) return cls(**d) def __getitem__(self, key): @@ -1491,9 +1498,13 @@ def _homogenize_dict(self, frames, intersect=True, dtype=None): ------- dict of aligned results & indicies """ - result = {} + from pandas.util.compat import OrderedDict - adj_frames = {} + result = dict() + if isinstance(frames,OrderedDict): # caller differs dict/ODict, presered type + result = OrderedDict() + + adj_frames = OrderedDict() for k, v in frames.iteritems(): if isinstance(v, dict): adj_frames[k] = self._constructor_sliced(v) diff --git a/pandas/tests/test_panel.py b/pandas/tests/test_panel.py index 921097e3408fd..437f8b7279824 100644 --- a/pandas/tests/test_panel.py +++ b/pandas/tests/test_panel.py @@ -19,7 +19,9 @@ assert_frame_equal, assert_series_equal, assert_almost_equal, - ensure_clean) + ensure_clean, + makeCustomDataframe as mkdf + ) import pandas.core.panel as panelm import pandas.util.testing as tm @@ -904,6 +906,16 @@ def test_constructor_dict_mixed(self): data['ItemB'] = self.panel['ItemB'].values[:, :-1] self.assertRaises(Exception, Panel, data) + def test_ctor_orderedDict(self): + from pandas.util.compat import OrderedDict + keys = list(set(np.random.randint(0,5000,100)))[:50] # unique random int keys + d = OrderedDict([(k,mkdf(10,5)) for k in keys]) + p = Panel(d) + self.assertTrue(list(p.items) == keys) + + p = Panel.from_dict(d) + self.assertTrue(list(p.items) == keys) + def test_constructor_resize(self): data = self.panel._data items = self.panel.items[:-1] diff --git a/pandas/util/compat.py b/pandas/util/compat.py index 41055f48c2fac..c18044fc6c492 100644 --- a/pandas/util/compat.py +++ b/pandas/util/compat.py @@ -475,3 +475,28 @@ def __and__(self, other): Counter = _Counter else: from collections import OrderedDict, Counter + +# http://stackoverflow.com/questions/4126348 +# Thanks to @martineau at SO + +class OrderedDefaultdict(OrderedDict): + def __init__(self, *args, **kwargs): + newdefault = None + newargs = () + if args: + newdefault = args[0] + if not (newdefault is None or callable(newdefault)): + raise TypeError('first argument must be callable or None') + newargs = args[1:] + self.default_factory = newdefault + super(self.__class__, self).__init__(*newargs, **kwargs) + + def __missing__ (self, key): + if self.default_factory is None: + raise KeyError(key) + self[key] = value = self.default_factory() + return value + + def __reduce__(self): # optional, for pickle support + args = self.default_factory if self.default_factory else tuple() + return type(self), args, None, None, self.items()