Skip to content

Commit 1d7c9e5

Browse files
committed
ENH: allow multiple axes to be passed to axis on a Panel to process slabs
1 parent f1a7f2a commit 1d7c9e5

File tree

3 files changed

+95
-19
lines changed

3 files changed

+95
-19
lines changed

doc/source/release.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Improvements to existing features
7575
- Series.str.contains now has a `regex=False` keyword which can be faster for plain (non-regex) string patterns. (:issue: `5879`)
7676
- support ``dtypes`` on ``Panel``
7777
- extend ``Panel.apply`` to allow arbitrary functions (rather than only ufuncs) (:issue:`1148`)
78+
allow multiple axes to be used to operate on slabs of a ``Panel``
7879

7980
.. _release.bug_fixes-0.13.1:
8081

pandas/core/panel.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,7 @@ def to_frame(self, filter_observations=True):
841841
to_long = deprecate('to_long', to_frame)
842842
toLong = deprecate('toLong', to_frame)
843843

844-
def apply(self, func, axis='major', args=(), **kwargs):
844+
def apply(self, func, axis='major', **kwargs):
845845
"""
846846
Applies function along input axis of the Panel
847847
@@ -852,9 +852,6 @@ def apply(self, func, axis='major', args=(), **kwargs):
852852
e.g. if axis = 'items', then the combination of major_axis/minor_axis
853853
will be passed a Series
854854
axis : {'major', 'minor', 'items'}
855-
args : tuple
856-
Positional arguments to pass to function in addition to the
857-
array/series
858855
Additional keyword arguments will be passed as keywords to the function
859856
860857
Examples
@@ -868,25 +865,36 @@ def apply(self, func, axis='major', args=(), **kwargs):
868865
-------
869866
result : Pandas Object
870867
"""
871-
axis = self._get_axis_number(axis)
872-
axis_name = self._get_axis_name(axis)
873-
ax = self._get_axis(axis)
874-
values = self.values
875-
ndim = self.ndim
876868

877-
if args or kwargs and not isinstance(func, np.ufunc):
878-
f = lambda x: func(x, *args, **kwargs)
869+
if kwargs and not isinstance(func, np.ufunc):
870+
f = lambda x: func(x, **kwargs)
879871
else:
880872
f = func
881873

874+
# 2d-slabs
875+
if isinstance(axis, (tuple,list)) and len(axis) == 2:
876+
return self._apply_2d(f, axis=axis)
877+
878+
axis = self._get_axis_number(axis)
879+
882880
# try ufunc like
883881
if isinstance(f, np.ufunc):
884882
try:
885-
result = np.apply_along_axis(func, axis, values)
883+
result = np.apply_along_axis(func, axis, self.values)
886884
return self._wrap_result(result, axis=axis)
887885
except (AttributeError):
888886
pass
889887

888+
# 1d
889+
return self._apply_1d(f, axis=axis)
890+
891+
def _apply_1d(self, func, axis):
892+
893+
axis_name = self._get_axis_name(axis)
894+
ax = self._get_axis(axis)
895+
ndim = self.ndim
896+
values = self.values
897+
890898
# iter thru the axes
891899
slice_axis = self._get_axis(axis)
892900
slice_indexer = [0]*(ndim-1)
@@ -902,14 +910,14 @@ def apply(self, func, axis='major', args=(), **kwargs):
902910
points = cartesian_product(planes)
903911

904912
results = []
905-
for i in xrange(np.prod(shape)):
913+
for i in range(np.prod(shape)):
906914

907915
# construct the object
908916
pts = tuple([ p[i] for p in points ])
909917
indexer.put(indlist, slice_indexer)
910918

911919
obj = Series(values[tuple(indexer)],index=slice_axis,name=pts)
912-
result = func(obj, *args, **kwargs)
920+
result = func(obj)
913921

914922
results.append(result)
915923

@@ -940,6 +948,32 @@ def apply(self, func, axis='major', args=(), **kwargs):
940948
planes = planes[::-1]
941949
return self._construct_return_type(results,planes)
942950

951+
def _apply_2d(self, func, axis):
952+
""" handle 2-d slices, equiv to iterating over the other axis """
953+
954+
ndim = self.ndim
955+
axis = [ self._get_axis_number(a) for a in axis ]
956+
957+
# construct slabs, in 2-d this is a DataFrame result
958+
indexer_axis = list(range(ndim))
959+
for a in axis:
960+
indexer_axis.remove(a)
961+
indexer_axis = indexer_axis[0]
962+
963+
slicer = [ slice(None,None) ] * ndim
964+
ax = self._get_axis(indexer_axis)
965+
966+
results = []
967+
for i, e in enumerate(ax):
968+
969+
slicer[indexer_axis] = i
970+
sliced = self.iloc[tuple(slicer)]
971+
972+
obj = func(sliced)
973+
results.append((e,obj))
974+
975+
return self._construct_return_type(dict(results))
976+
943977
def _reduce(self, op, axis=0, skipna=True, numeric_only=None,
944978
filter_type=None, **kwds):
945979
axis_name = self._get_axis_name(axis)
@@ -961,7 +995,7 @@ def _construct_return_type(self, result, axes=None, **kwargs):
961995
# need to assume they are the same
962996
if ndim is None:
963997
if isinstance(result,dict):
964-
ndim = getattr(result.values()[0],'ndim',None)
998+
ndim = getattr(list(compat.itervalues(result))[0],'ndim',None)
965999

9661000
# a saclar result
9671001
if ndim is None:

pandas/tests/test_panel.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,15 +1110,56 @@ def test_apply(self):
11101110
result = self.panel.apply(lambda x: x.sum(), axis='minor_axis')
11111111
assert_frame_equal(result,expected)
11121112

1113-
# pass args
1114-
result = self.panel.apply(lambda x, y: x.sum() + y, axis='items', args=[5])
1113+
# pass kwargs
1114+
result = self.panel.apply(lambda x, y: x.sum() + y, axis='items', y=5)
11151115
expected = self.panel.sum(0) + 5
11161116
assert_frame_equal(result,expected)
11171117

1118-
result = self.panel.apply(lambda x, y: x.sum() + y, axis='items', y=5)
1119-
expected = self.panel.sum(0) + 5
1118+
def test_apply_slabs(self):
1119+
1120+
# same shape as original
1121+
result = self.panel.apply(lambda x: x*2, axis = ['items','major_axis'])
1122+
expected = (self.panel*2).transpose('minor_axis','major_axis','items')
1123+
assert_panel_equal(result,expected)
1124+
result = self.panel.apply(lambda x: x*2, axis = ['major_axis','items'])
1125+
assert_panel_equal(result,expected)
1126+
1127+
result = self.panel.apply(lambda x: x*2, axis = ['items','minor_axis'])
1128+
expected = (self.panel*2).transpose('major_axis','minor_axis','items')
1129+
assert_panel_equal(result,expected)
1130+
result = self.panel.apply(lambda x: x*2, axis = ['minor_axis','items'])
1131+
assert_panel_equal(result,expected)
1132+
1133+
result = self.panel.apply(lambda x: x*2, axis = ['major_axis','minor_axis'])
1134+
expected = self.panel*2
1135+
assert_panel_equal(result,expected)
1136+
result = self.panel.apply(lambda x: x*2, axis = ['minor_axis','major_axis'])
1137+
assert_panel_equal(result,expected)
1138+
1139+
# reductions
1140+
result = self.panel.apply(lambda x: x.sum(0), axis = ['items','major_axis'])
1141+
expected = self.panel.sum(1).T
11201142
assert_frame_equal(result,expected)
11211143

1144+
result = self.panel.apply(lambda x: x.sum(1), axis = ['items','major_axis'])
1145+
expected = self.panel.sum(0)
1146+
assert_frame_equal(result,expected)
1147+
1148+
# transforms
1149+
f = lambda x: (x-x.mean(1)/x.std(1))
1150+
1151+
result = self.panel.apply(f, axis = ['items','major_axis'])
1152+
expected = Panel(dict([ (ax,f(self.panel.loc[:,:,ax])) for ax in self.panel.minor_axis ]))
1153+
assert_panel_equal(result,expected)
1154+
1155+
result = self.panel.apply(f, axis = ['major_axis','minor_axis'])
1156+
expected = Panel(dict([ (ax,f(self.panel.loc[ax])) for ax in self.panel.items ]))
1157+
assert_panel_equal(result,expected)
1158+
1159+
result = self.panel.apply(f, axis = ['minor_axis','items'])
1160+
expected = Panel(dict([ (ax,f(self.panel.loc[:,ax])) for ax in self.panel.major_axis ]))
1161+
assert_panel_equal(result,expected)
1162+
11221163
def test_reindex(self):
11231164
ref = self.panel['ItemB']
11241165

0 commit comments

Comments
 (0)