Skip to content

Commit 172ab7a

Browse files
toobazjreback
authored andcommitted
Idx droplevel (#21116)
1 parent ac32ce8 commit 172ab7a

File tree

8 files changed

+104
-58
lines changed

8 files changed

+104
-58
lines changed

doc/source/api.rst

+9-2
Original file line numberDiff line numberDiff line change
@@ -1459,7 +1459,6 @@ Modifying and Computations
14591459
Index.is_floating
14601460
Index.is_integer
14611461
Index.is_interval
1462-
Index.is_lexsorted_for_tuple
14631462
Index.is_mixed
14641463
Index.is_numeric
14651464
Index.is_object
@@ -1471,11 +1470,19 @@ Modifying and Computations
14711470
Index.where
14721471
Index.take
14731472
Index.putmask
1474-
Index.set_names
14751473
Index.unique
14761474
Index.nunique
14771475
Index.value_counts
14781476

1477+
Compatibility with MultiIndex
1478+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1479+
.. autosummary::
1480+
:toctree: generated/
1481+
1482+
Index.set_names
1483+
Index.is_lexsorted_for_tuple
1484+
Index.droplevel
1485+
14791486
Missing Values
14801487
~~~~~~~~~~~~~~
14811488
.. autosummary::

doc/source/whatsnew/v0.23.1.txt

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Indexing
7575
^^^^^^^^
7676

7777
- Bug in :meth:`Series.reset_index` where appropriate error was not raised with an invalid level name (:issue:`20925`)
78+
- :meth:`Index.droplevel` is now implemented also for flat indexes, for compatibility with MultiIndex (:issue:`21115`)
7879
-
7980

8081
I/O

pandas/core/frame.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -4096,9 +4096,8 @@ def _maybe_casted_values(index, labels=None):
40964096
if not isinstance(level, (tuple, list)):
40974097
level = [level]
40984098
level = [self.index._get_level_number(lev) for lev in level]
4099-
if isinstance(self.index, MultiIndex):
4100-
if len(level) < self.index.nlevels:
4101-
new_index = self.index.droplevel(level)
4099+
if len(level) < self.index.nlevels:
4100+
new_index = self.index.droplevel(level)
41024101

41034102
if not drop:
41044103
if isinstance(self.index, MultiIndex):

pandas/core/indexes/base.py

+54
Original file line numberDiff line numberDiff line change
@@ -3158,6 +3158,60 @@ def _get_level_values(self, level):
31583158

31593159
get_level_values = _get_level_values
31603160

3161+
def droplevel(self, level=0):
3162+
"""
3163+
Return index with requested level(s) removed. If resulting index has
3164+
only 1 level left, the result will be of Index type, not MultiIndex.
3165+
3166+
.. versionadded:: 0.23.1 (support for non-MultiIndex)
3167+
3168+
Parameters
3169+
----------
3170+
level : int, str, or list-like, default 0
3171+
If a string is given, must be the name of a level
3172+
If list-like, elements must be names or indexes of levels.
3173+
3174+
Returns
3175+
-------
3176+
index : Index or MultiIndex
3177+
"""
3178+
if not isinstance(level, (tuple, list)):
3179+
level = [level]
3180+
3181+
levnums = sorted(self._get_level_number(lev) for lev in level)[::-1]
3182+
3183+
if len(level) == 0:
3184+
return self
3185+
if len(level) >= self.nlevels:
3186+
raise ValueError("Cannot remove {} levels from an index with {} "
3187+
"levels: at least one level must be "
3188+
"left.".format(len(level), self.nlevels))
3189+
# The two checks above guarantee that here self is a MultiIndex
3190+
3191+
new_levels = list(self.levels)
3192+
new_labels = list(self.labels)
3193+
new_names = list(self.names)
3194+
3195+
for i in levnums:
3196+
new_levels.pop(i)
3197+
new_labels.pop(i)
3198+
new_names.pop(i)
3199+
3200+
if len(new_levels) == 1:
3201+
3202+
# set nan if needed
3203+
mask = new_labels[0] == -1
3204+
result = new_levels[0].take(new_labels[0])
3205+
if mask.any():
3206+
result = result.putmask(mask, np.nan)
3207+
3208+
result.name = new_names[0]
3209+
return result
3210+
else:
3211+
from .multi import MultiIndex
3212+
return MultiIndex(levels=new_levels, labels=new_labels,
3213+
names=new_names, verify_integrity=False)
3214+
31613215
_index_shared_docs['get_indexer'] = """
31623216
Compute indexer and mask for new index given the current index. The
31633217
indexer should be then used as an input to ndarray.take to align the

pandas/core/indexes/multi.py

-46
Original file line numberDiff line numberDiff line change
@@ -1761,52 +1761,6 @@ def _drop_from_level(self, labels, level):
17611761

17621762
return self[mask]
17631763

1764-
def droplevel(self, level=0):
1765-
"""
1766-
Return Index with requested level removed. If MultiIndex has only 2
1767-
levels, the result will be of Index type not MultiIndex.
1768-
1769-
Parameters
1770-
----------
1771-
level : int/level name or list thereof
1772-
1773-
Notes
1774-
-----
1775-
Does not check if result index is unique or not
1776-
1777-
Returns
1778-
-------
1779-
index : Index or MultiIndex
1780-
"""
1781-
levels = level
1782-
if not isinstance(levels, (tuple, list)):
1783-
levels = [level]
1784-
1785-
new_levels = list(self.levels)
1786-
new_labels = list(self.labels)
1787-
new_names = list(self.names)
1788-
1789-
levnums = sorted(self._get_level_number(lev) for lev in levels)[::-1]
1790-
1791-
for i in levnums:
1792-
new_levels.pop(i)
1793-
new_labels.pop(i)
1794-
new_names.pop(i)
1795-
1796-
if len(new_levels) == 1:
1797-
1798-
# set nan if needed
1799-
mask = new_labels[0] == -1
1800-
result = new_levels[0].take(new_labels[0])
1801-
if mask.any():
1802-
result = result.putmask(mask, np.nan)
1803-
1804-
result.name = new_names[0]
1805-
return result
1806-
else:
1807-
return MultiIndex(levels=new_levels, labels=new_labels,
1808-
names=new_names, verify_integrity=False)
1809-
18101764
def swaplevel(self, i=-2, j=-1):
18111765
"""
18121766
Swap level i with level j.

pandas/core/series.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1199,9 +1199,8 @@ def reset_index(self, level=None, drop=False, name=None, inplace=False):
11991199
if not isinstance(level, (tuple, list)):
12001200
level = [level]
12011201
level = [self.index._get_level_number(lev) for lev in level]
1202-
if isinstance(self.index, MultiIndex):
1203-
if len(level) < self.index.nlevels:
1204-
new_index = self.index.droplevel(level)
1202+
if len(level) < self.index.nlevels:
1203+
new_index = self.index.droplevel(level)
12051204

12061205
if inplace:
12071206
self.index = new_index
@@ -3177,7 +3176,8 @@ def apply(self, func, convert_dtype=True, args=(), **kwds):
31773176

31783177
# handle ufuncs and lambdas
31793178
if kwds or args and not isinstance(func, np.ufunc):
3180-
f = lambda x: func(x, *args, **kwds)
3179+
def f(x):
3180+
return func(x, *args, **kwds)
31813181
else:
31823182
f = func
31833183

pandas/tests/indexes/test_base.py

+19
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,25 @@ def test_constructor_int_dtype_nan(self):
245245
result = Index(data, dtype='float')
246246
tm.assert_index_equal(result, expected)
247247

248+
def test_droplevel(self, indices):
249+
# GH 21115
250+
if isinstance(indices, MultiIndex):
251+
# Tested separately in test_multi.py
252+
return
253+
254+
assert indices.droplevel([]).equals(indices)
255+
256+
for level in indices.name, [indices.name]:
257+
if isinstance(indices.name, tuple) and level is indices.name:
258+
# GH 21121 : droplevel with tuple name
259+
continue
260+
with pytest.raises(ValueError):
261+
indices.droplevel(level)
262+
263+
for level in 'wrong', ['wrong']:
264+
with pytest.raises(KeyError):
265+
indices.droplevel(level)
266+
248267
@pytest.mark.parametrize("dtype", ['int64', 'uint64'])
249268
def test_constructor_int_dtype_nan_raises(self, dtype):
250269
# see gh-15187

pandas/tests/indexes/test_multi.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ def test_where_array_like(self):
9999
cond = [False, True]
100100

101101
for klass in klasses:
102-
f = lambda: i.where(klass(cond))
102+
def f():
103+
return i.where(klass(cond))
103104
pytest.raises(NotImplementedError, f)
104105

105106
def test_repeat(self):
@@ -2078,7 +2079,7 @@ def test_droplevel_with_names(self):
20782079
expected = index.droplevel(1)
20792080
assert dropped.equals(expected)
20802081

2081-
def test_droplevel_multiple(self):
2082+
def test_droplevel_list(self):
20822083
index = MultiIndex(
20832084
levels=[Index(lrange(4)), Index(lrange(4)), Index(lrange(4))],
20842085
labels=[np.array([0, 0, 1, 2, 2, 2, 3, 3]), np.array(
@@ -2089,6 +2090,16 @@ def test_droplevel_multiple(self):
20892090
expected = index[:2].droplevel(2).droplevel(0)
20902091
assert dropped.equals(expected)
20912092

2093+
dropped = index[:2].droplevel([])
2094+
expected = index[:2]
2095+
assert dropped.equals(expected)
2096+
2097+
with pytest.raises(ValueError):
2098+
index[:2].droplevel(['one', 'two', 'three'])
2099+
2100+
with pytest.raises(KeyError):
2101+
index[:2].droplevel(['one', 'four'])
2102+
20922103
def test_drop_not_lexsorted(self):
20932104
# GH 12078
20942105

@@ -2405,7 +2416,8 @@ def check(nlevels, with_nulls):
24052416

24062417
# with a dup
24072418
if with_nulls:
2408-
f = lambda a: np.insert(a, 1000, a[0])
2419+
def f(a):
2420+
return np.insert(a, 1000, a[0])
24092421
labels = list(map(f, labels))
24102422
index = MultiIndex(levels=levels, labels=labels)
24112423
else:

0 commit comments

Comments
 (0)