Skip to content

Commit dbb4552

Browse files
committed
Merge PR #2407
2 parents 6376011 + 66140d2 commit dbb4552

File tree

8 files changed

+409
-136
lines changed

8 files changed

+409
-136
lines changed

RELEASE.rst

+8-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ pandas 0.10.0
4343
`describe_option`, and `reset_option`. Deprecate `set_printoptions` and
4444
`reset_printoptions` (#2393)
4545

46+
**Experimental Features**
47+
- Add support for Panel4D, a named 4 Dimensional stucture
48+
- Add support for ndpanel factory functions, to create custom, domain-specific
49+
N Dimensional containers
50+
4651
**API Changes**
4752

4853
- The default binning/labeling behavior for ``resample`` has been changed to
@@ -91,7 +96,9 @@ pandas 0.10.0
9196
structures, which should do the right thing on both py2.x and py3.x. (#2224)
9297
- Reduce groupby.apply overhead substantially by low-level manipulation of
9398
internal NumPy arrays in DataFrames (#535)
94-
- Implement ``value_vars`` in ``melt`` and add ``melt`` to pandas namespace (#2412)
99+
- Implement ``value_vars`` in ``melt`` and add ``melt`` to pandas namespace
100+
(#2412)
101+
- Added boolean comparison operators to Panel
95102

96103
**Bug fixes**
97104

doc/source/dsintro.rst

+119
Original file line numberDiff line numberDiff line change
@@ -798,3 +798,122 @@ method:
798798
major_axis=date_range('1/1/2000', periods=5),
799799
minor_axis=['a', 'b', 'c', 'd'])
800800
panel.to_frame()
801+
802+
Panel4D (Experimental)
803+
----------------------
804+
805+
``Panel4D`` is a 4-Dimensional named container very much like a ``Panel``, but
806+
having 4 named dimensions. It is intended as a test bed for more N-Dimensional named
807+
containers.
808+
809+
- **labels**: axis 0, each item corresponds to a Panel contained inside
810+
- **items**: axis 1, each item corresponds to a DataFrame contained inside
811+
- **major_axis**: axis 2, it is the **index** (rows) of each of the
812+
DataFrames
813+
- **minor_axis**: axis 3, it is the **columns** of each of the DataFrames
814+
815+
816+
``Panel4D`` is a sub-class of ``Panel``, so most methods that work on Panels are
817+
applicable to Panel4D. The following methods are disabled:
818+
819+
- ``join , to_frame , to_excel , to_sparse , groupby``
820+
821+
Construction of Panel4D works in a very similar manner to a ``Panel``
822+
823+
From 4D ndarray with optional axis labels
824+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
825+
826+
.. ipython:: python
827+
828+
p4d = Panel4D(randn(2, 2, 5, 4),
829+
labels=['Label1','Label2'],
830+
items=['Item1', 'Item2'],
831+
major_axis=date_range('1/1/2000', periods=5),
832+
minor_axis=['A', 'B', 'C', 'D'])
833+
p4d
834+
835+
836+
From dict of Panel objects
837+
~~~~~~~~~~~~~~~~~~~~~~~~~~
838+
839+
.. ipython:: python
840+
841+
data = { 'Label1' : Panel({ 'Item1' : DataFrame(randn(4, 3)) }),
842+
'Label2' : Panel({ 'Item2' : DataFrame(randn(4, 2)) }) }
843+
Panel4D(data)
844+
845+
Note that the values in the dict need only be **convertible to Panels**.
846+
Thus, they can be any of the other valid inputs to Panel as per above.
847+
848+
Slicing
849+
~~~~~~~
850+
851+
Slicing works in a similar manner to a Panel. ``[]`` slices the first dimension.
852+
``.ix`` allows you to slice abitrarily and get back lower dimensional objects
853+
854+
.. ipython:: python
855+
856+
p4d['Label1']
857+
858+
4D -> Panel
859+
860+
.. ipython:: python
861+
862+
p4d.ix[:,:,:,'A']
863+
864+
4D -> DataFrame
865+
866+
.. ipython:: python
867+
868+
p4d.ix[:,:,0,'A']
869+
870+
4D -> Series
871+
872+
.. ipython:: python
873+
874+
p4d.ix[:,0,0,'A']
875+
876+
Transposing
877+
~~~~~~~~~~~
878+
879+
A Panel4D can be rearranged using its ``transpose`` method (which does not make a
880+
copy by default unless the data are heterogeneous):
881+
882+
.. ipython:: python
883+
884+
p4d.transpose(3, 2, 1, 0)
885+
886+
PanelND (Experimental)
887+
----------------------
888+
889+
PanelND is a module with a set of factory functions to enable a user to construct N-dimensional named
890+
containers like Panel4D, with a custom set of axis labels. Thus a domain-specific container can easily be
891+
created.
892+
893+
The following creates a Panel5D. A new panel type object must be sliceable into a lower dimensional object.
894+
Here we slice to a Panel4D.
895+
896+
.. ipython:: python
897+
898+
from pandas.core import panelnd
899+
Panel5D = panelnd.create_nd_panel_factory(
900+
klass_name = 'Panel5D',
901+
axis_orders = [ 'cool', 'labels','items','major_axis','minor_axis'],
902+
axis_slices = { 'labels' : 'labels', 'items' : 'items',
903+
'major_axis' : 'major_axis', 'minor_axis' : 'minor_axis' },
904+
slicer = Panel4D,
905+
axis_aliases = { 'major' : 'major_axis', 'minor' : 'minor_axis' },
906+
stat_axis = 2)
907+
908+
p5d = Panel5D(dict(C1 = p4d))
909+
p5d
910+
911+
# print a slice of our 5D
912+
p5d.ix['C1',:,:,0:3,:]
913+
914+
# transpose it
915+
p5d.transpose(1,2,3,4,0)
916+
917+
# look at the shape & dim
918+
p5d.shape
919+
p5d.ndim

doc/source/v0.10.0.txt

+17
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,23 @@ Updated PyTables Support
113113

114114
- Implement ``value_vars`` in ``melt`` and add ``melt`` to pandas namespace (GH2412_)
115115

116+
N Dimensional Panels (Experimental)
117+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
118+
119+
Adding experimental support for Panel4D and factory functions to create n-dimensional named panels.
120+
:ref:`Docs <dsintro-panel4d>` for NDim. Here is a taste of what to expect.
121+
122+
.. ipython:: python
123+
124+
p4d = Panel4D(randn(2, 2, 5, 4),
125+
labels=['Label1','Label2'],
126+
items=['Item1', 'Item2'],
127+
major_axis=date_range('1/1/2000', periods=5),
128+
minor_axis=['A', 'B', 'C', 'D'])
129+
p4d
130+
131+
132+
116133
API changes
117134
~~~~~~~~~~~
118135

pandas/core/panel.py

+102-16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
_get_combined_index)
1414
from pandas.core.indexing import _NDFrameIndexer, _maybe_droplevels
1515
from pandas.core.internals import BlockManager, make_block, form_blocks
16+
from pandas.core.series import Series
1617
from pandas.core.frame import DataFrame
1718
from pandas.core.generic import NDFrame
1819
from pandas.util import py3compat
@@ -104,7 +105,7 @@ def f(self, other):
104105

105106
def _panel_arith_method(op, name):
106107
@Substitution(op)
107-
def f(self, other, axis='items'):
108+
def f(self, other, axis = 0):
108109
"""
109110
Wrapper method for %s
110111
@@ -123,6 +124,44 @@ def f(self, other, axis='items'):
123124
f.__name__ = name
124125
return f
125126

127+
def _comp_method(func, name):
128+
129+
def na_op(x, y):
130+
try:
131+
result = func(x, y)
132+
except TypeError:
133+
xrav = x.ravel()
134+
result = np.empty(x.size, dtype=x.dtype)
135+
if isinstance(y, np.ndarray):
136+
yrav = y.ravel()
137+
mask = notnull(xrav) & notnull(yrav)
138+
result[mask] = func(np.array(list(xrav[mask])),
139+
np.array(list(yrav[mask])))
140+
else:
141+
mask = notnull(xrav)
142+
result[mask] = func(np.array(list(xrav[mask])), y)
143+
144+
if func == operator.ne: # pragma: no cover
145+
np.putmask(result, -mask, True)
146+
else:
147+
np.putmask(result, -mask, False)
148+
result = result.reshape(x.shape)
149+
150+
return result
151+
152+
@Appender('Wrapper for comparison method %s' % name)
153+
def f(self, other):
154+
if isinstance(other, self._constructor):
155+
return self._compare_constructor(other, func)
156+
elif isinstance(other, (self._constructor_sliced, DataFrame, Series)):
157+
raise Exception("input needs alignment for this object [%s]" % self._constructor)
158+
else:
159+
return self._combine_const(other, na_op)
160+
161+
162+
f.__name__ = name
163+
164+
return f
126165

127166
_agg_doc = """
128167
Return %(desc)s over requested axis
@@ -280,7 +319,6 @@ def _init_dict(self, data, axes, dtype=None):
280319

281320
# shallow copy
282321
arrays = []
283-
reshaped_data = data.copy()
284322
haxis_shape = [ len(a) for a in raxes ]
285323
for h in haxis:
286324
v = values = data.get(h)
@@ -401,6 +439,51 @@ def __array_wrap__(self, result):
401439
d['copy'] = False
402440
return self._constructor(result, **d)
403441

442+
#----------------------------------------------------------------------
443+
# Comparison methods
444+
445+
def _indexed_same(self, other):
446+
return all([ getattr(self,a).equals(getattr(other,a)) for a in self._AXIS_ORDERS ])
447+
448+
def _compare_constructor(self, other, func):
449+
if not self._indexed_same(other):
450+
raise Exception('Can only compare identically-labeled '
451+
'same type objects')
452+
453+
new_data = {}
454+
for col in getattr(self,self._info_axis):
455+
new_data[col] = func(self[col], other[col])
456+
457+
d = self._construct_axes_dict()
458+
d['copy'] = False
459+
return self._constructor(data=new_data, **d)
460+
461+
# boolean operators
462+
__and__ = _arith_method(operator.and_, '__and__')
463+
__or__ = _arith_method(operator.or_, '__or__')
464+
__xor__ = _arith_method(operator.xor, '__xor__')
465+
466+
def __neg__(self):
467+
return -1 * self
468+
469+
def __invert__(self):
470+
return -1 * self
471+
472+
# Comparison methods
473+
__eq__ = _comp_method(operator.eq, '__eq__')
474+
__ne__ = _comp_method(operator.ne, '__ne__')
475+
__lt__ = _comp_method(operator.lt, '__lt__')
476+
__gt__ = _comp_method(operator.gt, '__gt__')
477+
__le__ = _comp_method(operator.le, '__le__')
478+
__ge__ = _comp_method(operator.ge, '__ge__')
479+
480+
eq = _comp_method(operator.eq, 'eq')
481+
ne = _comp_method(operator.ne, 'ne')
482+
gt = _comp_method(operator.gt, 'gt')
483+
lt = _comp_method(operator.lt, 'lt')
484+
ge = _comp_method(operator.ge, 'ge')
485+
le = _comp_method(operator.le, 'le')
486+
404487
#----------------------------------------------------------------------
405488
# Magic methods
406489

@@ -435,14 +518,14 @@ def __unicode__(self):
435518
class_name = str(self.__class__)
436519

437520
shape = self.shape
438-
dims = 'Dimensions: %s' % ' x '.join([ "%d (%s)" % (s, a) for a,s in zip(self._AXIS_ORDERS,shape) ])
521+
dims = u'Dimensions: %s' % ' x '.join([ "%d (%s)" % (s, a) for a,s in zip(self._AXIS_ORDERS,shape) ])
439522

440523
def axis_pretty(a):
441524
v = getattr(self,a)
442525
if len(v) > 0:
443-
return '%s axis: %s to %s' % (a.capitalize(),v[0],v[-1])
526+
return u'%s axis: %s to %s' % (a.capitalize(),com.pprint_thing(v[0]),com.pprint_thing(v[-1]))
444527
else:
445-
return '%s axis: None' % a.capitalize()
528+
return u'%s axis: None' % a.capitalize()
446529

447530

448531
output = '\n'.join([class_name, dims] + [axis_pretty(a) for a in self._AXIS_ORDERS])
@@ -496,9 +579,9 @@ def ix(self):
496579
return self._ix
497580

498581
def _wrap_array(self, arr, axes, copy=False):
499-
items, major, minor = axes
500-
return self._constructor(arr, items=items, major_axis=major,
501-
minor_axis=minor, copy=copy)
582+
d = dict([ (a,ax) for a,ax in zip(self._AXIS_ORDERS,axes) ])
583+
d['copy'] = False
584+
return self._constructor(arr, **d)
502585

503586
fromDict = from_dict
504587

@@ -742,7 +825,10 @@ def reindex(self, major=None, minor=None, method=None,
742825
if (method is None and not self._is_mixed_type and al <= 3):
743826
items = kwargs.get('items')
744827
if com._count_not_none(items, major, minor) == 3:
745-
return self._reindex_multi(items, major, minor)
828+
try:
829+
return self._reindex_multi(items, major, minor)
830+
except:
831+
pass
746832

747833
if major is not None:
748834
result = result._reindex_axis(major, method, al-2, copy)
@@ -874,12 +960,12 @@ def _combine(self, other, func, axis=0):
874960
elif isinstance(other, DataFrame):
875961
return self._combine_frame(other, func, axis=axis)
876962
elif np.isscalar(other):
877-
new_values = func(self.values, other)
878-
d = self._construct_axes_dict()
879-
return self._constructor(new_values, **d)
963+
return self._combine_const(other, func)
880964

881-
def __neg__(self):
882-
return -1 * self
965+
def _combine_const(self, other, func):
966+
new_values = func(self.values, other)
967+
d = self._construct_axes_dict()
968+
return self._constructor(new_values, **d)
883969

884970
def _combine_frame(self, other, func, axis=0):
885971
index, columns = self._get_plane_axes(axis)
@@ -1434,8 +1520,8 @@ def update(self, other, join='left', overwrite=True, filter_func=None,
14341520
contain data in the same place.
14351521
"""
14361522

1437-
if not isinstance(other, Panel):
1438-
other = Panel(other)
1523+
if not isinstance(other, self._constructor):
1524+
other = self._constructor(other)
14391525

14401526
other = other.reindex(items=self.items)
14411527

0 commit comments

Comments
 (0)