Skip to content

Commit 6b660ba

Browse files
committed
ENH/API: DataFrame.stack() support for level=ALL_LEVELS and sequentially=True/False.
1 parent 484f668 commit 6b660ba

File tree

5 files changed

+369
-116
lines changed

5 files changed

+369
-116
lines changed

pandas/core/frame.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -3392,7 +3392,7 @@ def pivot(self, index=None, columns=None, values=None):
33923392
from pandas.core.reshape import pivot
33933393
return pivot(self, index=index, columns=columns, values=values)
33943394

3395-
def stack(self, level=-1, dropna=True):
3395+
def stack(self, level=-1, dropna=True, sequentially=True):
33963396
"""
33973397
Pivot a level of the (possibly hierarchical) column labels, returning a
33983398
DataFrame (or Series in the case of an object with a single level of
@@ -3403,10 +3403,14 @@ def stack(self, level=-1, dropna=True):
34033403
Parameters
34043404
----------
34053405
level : int, string, or list of these, default last level
3406-
Level(s) to stack, can pass level name
3406+
Level(s) to stack, can pass level name(s).
3407+
May pass Index.ALL_LEVELS to specify list(range(columns.nlevels)).
34073408
dropna : boolean, default True
34083409
Whether to drop rows in the resulting Frame/Series with no valid
34093410
values
3411+
sequentially : boolean, default True
3412+
When level is a list (or ALL_LEVELS), whether the multiple column levels
3413+
should be stacked sequentially (if True) or simultaneously (if False).
34103414
34113415
Examples
34123416
----------
@@ -3425,12 +3429,13 @@ def stack(self, level=-1, dropna=True):
34253429
-------
34263430
stacked : DataFrame or Series
34273431
"""
3428-
from pandas.core.reshape import stack, stack_multiple
3432+
from pandas.core.reshape import stack_levels_sequentially, stack_multi_levels_simultaneously
34293433

3430-
if isinstance(level, (tuple, list)):
3431-
return stack_multiple(self, level, dropna=dropna)
3434+
level_num = self.columns._get_level_numbers(level, allow_mixed_names_and_numbers=False)
3435+
if isinstance(level_num, (tuple, list, set)) and isinstance(self.columns, MultiIndex) and (not sequentially):
3436+
return stack_multi_levels_simultaneously(self, level_num, dropna=dropna)
34323437
else:
3433-
return stack(self, level, dropna=dropna)
3438+
return stack_levels_sequentially(self, level_num, dropna=dropna)
34343439

34353440
def unstack(self, level=-1):
34363441
"""

pandas/core/index.py

+43-23
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ class Index(IndexOpsMixin, PandasObject):
123123

124124
_engine_type = _index.ObjectEngine
125125

126+
ALL_LEVELS = -1000
127+
126128
def __new__(cls, data=None, dtype=None, copy=False, name=None, fastpath=False,
127129
tupleize_cols=True, **kwargs):
128130

@@ -824,10 +826,26 @@ def _validate_index_level(self, level):
824826
raise KeyError('Level %s must be same as name (%s)'
825827
% (level, self.name))
826828

827-
def _get_level_number(self, level):
829+
def _get_level_number(self, level, ignore_names=False):
830+
if ignore_names and (not isinstance(level, int)):
831+
raise KeyError('Level %s not found' % str(level))
828832
self._validate_index_level(level)
829833
return 0
830834

835+
def _get_level_numbers(self, levels, allow_mixed_names_and_numbers=False):
836+
if levels == self.ALL_LEVELS:
837+
return list(range(self.nlevels))
838+
elif isinstance(levels, (list, tuple, set)):
839+
if (not allow_mixed_names_and_numbers) and (not all(lev in self.names for lev in levels)):
840+
if all(isinstance(lev, int) for lev in levels):
841+
return type(levels)(self._get_level_number(level, ignore_names=True) for level in levels)
842+
else:
843+
raise ValueError("level should contain all level names or all level numbers, "
844+
"not a mixture of the two.")
845+
return type(levels)(self._get_level_number(level) for level in levels)
846+
else:
847+
return self._get_level_number(levels)
848+
831849
@cache_readonly
832850
def inferred_type(self):
833851
""" return a string of the type inferred from the values """
@@ -3161,28 +3179,30 @@ def _from_elements(values, labels=None, levels=None, names=None,
31613179
sortorder=None):
31623180
return MultiIndex(levels, labels, names, sortorder=sortorder)
31633181

3164-
def _get_level_number(self, level):
3165-
try:
3182+
def _get_level_number(self, level, ignore_names=False):
3183+
if not ignore_names:
31663184
count = self.names.count(level)
31673185
if count > 1:
31683186
raise ValueError('The name %s occurs multiple times, use a '
31693187
'level number' % level)
3170-
level = self.names.index(level)
3171-
except ValueError:
3172-
if not isinstance(level, int):
3173-
raise KeyError('Level %s not found' % str(level))
3174-
elif level < 0:
3175-
level += self.nlevels
3176-
if level < 0:
3177-
orig_level = level - self.nlevels
3178-
raise IndexError(
3179-
'Too many levels: Index has only %d levels, '
3180-
'%d is not a valid level number' % (self.nlevels, orig_level)
3181-
)
3182-
# Note: levels are zero-based
3183-
elif level >= self.nlevels:
3184-
raise IndexError('Too many levels: Index has only %d levels, '
3185-
'not %d' % (self.nlevels, level + 1))
3188+
try:
3189+
return self.names.index(level)
3190+
except ValueError:
3191+
pass
3192+
if not isinstance(level, int):
3193+
raise KeyError('Level %s not found' % str(level))
3194+
elif level < 0:
3195+
level += self.nlevels
3196+
if level < 0:
3197+
orig_level = level - self.nlevels
3198+
raise IndexError(
3199+
'Too many levels: Index has only %d levels, '
3200+
'%d is not a valid level number' % (self.nlevels, orig_level)
3201+
)
3202+
# Note: levels are zero-based
3203+
elif level >= self.nlevels:
3204+
raise IndexError('Too many levels: Index has only %d levels, '
3205+
'not %d' % (self.nlevels, level + 1))
31863206
return level
31873207

31883208
_tuples = None
@@ -4852,7 +4872,7 @@ def _trim_front(strings):
48524872

48534873

48544874
def _sanitize_and_check(indexes):
4855-
kinds = list(set([type(index) for index in indexes]))
4875+
kinds = list(set(type(index) for index in indexes))
48564876

48574877
if list in kinds:
48584878
if len(kinds) > 1:
@@ -4873,11 +4893,11 @@ def _get_consensus_names(indexes):
48734893

48744894
# find the non-none names, need to tupleify to make
48754895
# the set hashable, then reverse on return
4876-
consensus_names = set([
4896+
consensus_names = set(
48774897
tuple(i.names) for i in indexes if all(n is not None for n in i.names)
4878-
])
4898+
)
48794899
if len(consensus_names) == 1:
4880-
return list(list(consensus_names)[0])
4900+
return list(consensus_names.pop())
48814901
return [None] * indexes[0].nlevels
48824902

48834903

0 commit comments

Comments
 (0)