Skip to content

Commit 0a6b205

Browse files
jbrockmendelKevin D Smith
authored and
Kevin D Smith
committed
BUG: .loc with MultiIndex with names[1] = 0 (pandas-dev#37194)
1 parent 24f8a0d commit 0a6b205

File tree

5 files changed

+59
-13
lines changed

5 files changed

+59
-13
lines changed

doc/source/whatsnew/v1.2.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ Indexing
432432
- Bug in indexing with boolean masks on datetime-like values sometimes returning a view instead of a copy (:issue:`36210`)
433433
- Bug in :meth:`DataFrame.__getitem__` and :meth:`DataFrame.loc.__getitem__` with :class:`IntervalIndex` columns and a numeric indexer (:issue:`26490`)
434434
- Bug in :meth:`Series.loc.__getitem__` with a non-unique :class:`MultiIndex` and an empty-list indexer (:issue:`13691`)
435+
- Bug in indexing on a :class:`Series` or :class:`DataFrame` with a :class:`MultiIndex` with a level named "0" (:issue:`37194`)
435436

436437
Missing
437438
^^^^^^^

pandas/core/generic.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -3684,7 +3684,9 @@ class animal locomotion
36843684
index = self.index
36853685
if isinstance(index, MultiIndex):
36863686
try:
3687-
loc, new_index = self.index.get_loc_level(key, drop_level=drop_level)
3687+
loc, new_index = self.index._get_loc_level(
3688+
key, level=0, drop_level=drop_level
3689+
)
36883690
except TypeError as e:
36893691
raise TypeError(f"Expected label or tuple of labels, got {key}") from e
36903692
else:

pandas/core/indexes/base.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -1556,12 +1556,19 @@ def droplevel(self, level=0):
15561556

15571557
levnums = sorted(self._get_level_number(lev) for lev in level)[::-1]
15581558

1559-
if len(level) == 0:
1559+
return self._drop_level_numbers(levnums)
1560+
1561+
def _drop_level_numbers(self, levnums: List[int]):
1562+
"""
1563+
Drop MultiIndex levels by level _number_, not name.
1564+
"""
1565+
1566+
if len(levnums) == 0:
15601567
return self
1561-
if len(level) >= self.nlevels:
1568+
if len(levnums) >= self.nlevels:
15621569
raise ValueError(
1563-
f"Cannot remove {len(level)} levels from an index with {self.nlevels} "
1564-
"levels: at least one level must be left."
1570+
f"Cannot remove {len(levnums)} levels from an index with "
1571+
f"{self.nlevels} levels: at least one level must be left."
15651572
)
15661573
# The two checks above guarantee that here self is a MultiIndex
15671574
self = cast("MultiIndex", self)

pandas/core/indexes/multi.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -2864,16 +2864,29 @@ def get_loc_level(self, key, level=0, drop_level: bool = True):
28642864
>>> mi.get_loc_level(['b', 'e'])
28652865
(1, None)
28662866
"""
2867+
if not isinstance(level, (list, tuple)):
2868+
level = self._get_level_number(level)
2869+
else:
2870+
level = [self._get_level_number(lev) for lev in level]
2871+
return self._get_loc_level(key, level=level, drop_level=drop_level)
2872+
2873+
def _get_loc_level(
2874+
self, key, level: Union[int, List[int]] = 0, drop_level: bool = True
2875+
):
2876+
"""
2877+
get_loc_level but with `level` known to be positional, not name-based.
2878+
"""
2879+
28672880
# different name to distinguish from maybe_droplevels
28682881
def maybe_mi_droplevels(indexer, levels, drop_level: bool):
28692882
if not drop_level:
28702883
return self[indexer]
28712884
# kludge around
28722885
orig_index = new_index = self[indexer]
2873-
levels = [self._get_level_number(i) for i in levels]
2886+
28742887
for i in sorted(levels, reverse=True):
28752888
try:
2876-
new_index = new_index.droplevel(i)
2889+
new_index = new_index._drop_level_numbers([i])
28772890
except ValueError:
28782891

28792892
# no dropping here
@@ -2887,7 +2900,7 @@ def maybe_mi_droplevels(indexer, levels, drop_level: bool):
28872900
)
28882901
result = None
28892902
for lev, k in zip(level, key):
2890-
loc, new_index = self.get_loc_level(k, level=lev)
2903+
loc, new_index = self._get_loc_level(k, level=lev)
28912904
if isinstance(loc, slice):
28922905
mask = np.zeros(len(self), dtype=bool)
28932906
mask[loc] = True
@@ -2897,8 +2910,6 @@ def maybe_mi_droplevels(indexer, levels, drop_level: bool):
28972910

28982911
return result, maybe_mi_droplevels(result, level, drop_level)
28992912

2900-
level = self._get_level_number(level)
2901-
29022913
# kludge for #1796
29032914
if isinstance(key, list):
29042915
key = tuple(key)
@@ -2963,7 +2974,8 @@ def partial_selection(key, indexer=None):
29632974
indexer = self._get_level_indexer(key, level=level)
29642975
return indexer, maybe_mi_droplevels(indexer, [level], drop_level)
29652976

2966-
def _get_level_indexer(self, key, level=0, indexer=None):
2977+
def _get_level_indexer(self, key, level: int = 0, indexer=None):
2978+
# `level` kwarg is _always_ positional, never name
29672979
# return an indexer, boolean array or a slice showing where the key is
29682980
# in the totality of values
29692981
# if the indexer is provided, then use this
@@ -3767,13 +3779,13 @@ def maybe_droplevels(index, key):
37673779
if isinstance(key, tuple):
37683780
for _ in key:
37693781
try:
3770-
index = index.droplevel(0)
3782+
index = index._drop_level_numbers([0])
37713783
except ValueError:
37723784
# we have dropped too much, so back out
37733785
return original_index
37743786
else:
37753787
try:
3776-
index = index.droplevel(0)
3788+
index = index._drop_level_numbers([0])
37773789
except ValueError:
37783790
pass
37793791

pandas/tests/indexing/multiindex/test_loc.py

+24
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,30 @@ def test_loc_with_mi_indexer():
524524
tm.assert_frame_equal(result, expected)
525525

526526

527+
def test_loc_mi_with_level1_named_0():
528+
# GH#37194
529+
dti = pd.date_range("2016-01-01", periods=3, tz="US/Pacific")
530+
531+
ser = Series(range(3), index=dti)
532+
df = ser.to_frame()
533+
df[1] = dti
534+
535+
df2 = df.set_index(0, append=True)
536+
assert df2.index.names == (None, 0)
537+
df2.index.get_loc(dti[0]) # smoke test
538+
539+
result = df2.loc[dti[0]]
540+
expected = df2.iloc[[0]].droplevel(None)
541+
tm.assert_frame_equal(result, expected)
542+
543+
ser2 = df2[1]
544+
assert ser2.index.names == (None, 0)
545+
546+
result = ser2.loc[dti[0]]
547+
expected = ser2.iloc[[0]].droplevel(None)
548+
tm.assert_series_equal(result, expected)
549+
550+
527551
def test_getitem_str_slice(datapath):
528552
# GH#15928
529553
path = datapath("reshape", "merge", "data", "quotes2.csv")

0 commit comments

Comments
 (0)