Skip to content

Commit 9db86be

Browse files
authored
BUG: .loc with MultiIndex with tuple in level GH#27591 (#42329)
1 parent 0a44a51 commit 9db86be

File tree

3 files changed

+85
-27
lines changed

3 files changed

+85
-27
lines changed

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ Interval
233233
Indexing
234234
^^^^^^^^
235235
- Bug in :meth:`DataFrame.truncate` and :meth:`Series.truncate` when the object's Index has a length greater than one but only one unique value (:issue:`42365`)
236+
- Bug in :meth:`Series.loc` and :meth:`DataFrame.loc` with a :class:`MultiIndex` when indexing with a tuple in which one of the levels is also a tuple (:issue:`27591`)
236237
- Bug in :meth:`Series.loc` when with a :class:`MultiIndex` whose first level contains only ``np.nan`` values (:issue:`42055`)
237238
- Bug in indexing on a :class:`Series` or :class:`DataFrame` with a :class:`DatetimeIndex` when passing a string, the return type depended on whether the index was monotonic (:issue:`24892`)
238239
- Bug in indexing on a :class:`MultiIndex` failing to drop scalar levels when the indexer is a tuple containing a datetime-like string (:issue:`42476`)

pandas/core/indexes/multi.py

+54-27
Original file line numberDiff line numberDiff line change
@@ -3288,35 +3288,62 @@ def _update_indexer(idxr: Index, indexer: Index) -> Index:
32883288
# are or'd)
32893289

32903290
indexers: Int64Index | None = None
3291-
for x in k:
3292-
try:
3293-
# Argument "indexer" to "_get_level_indexer" of "MultiIndex"
3294-
# has incompatible type "Index"; expected "Optional[Int64Index]"
3295-
item_lvl_indexer = self._get_level_indexer(
3296-
x, level=i, indexer=indexer # type: ignore[arg-type]
3297-
)
3298-
except KeyError:
3299-
# ignore not founds; see discussion in GH#39424
3300-
warnings.warn(
3301-
"The behavior of indexing on a MultiIndex with a nested "
3302-
"sequence of labels is deprecated and will change in a "
3303-
"future version. `series.loc[label, sequence]` will "
3304-
"raise if any members of 'sequence' or not present in "
3305-
"the index's second level. To retain the old behavior, "
3306-
"use `series.index.isin(sequence, level=1)`",
3307-
# TODO: how to opt in to the future behavior?
3308-
# TODO: how to handle IntervalIndex level? (no test cases)
3309-
FutureWarning,
3310-
stacklevel=7,
3311-
)
3312-
continue
3313-
else:
3314-
idxrs = _convert_to_indexer(item_lvl_indexer)
33153291

3316-
if indexers is None:
3317-
indexers = idxrs
3292+
# GH#27591 check if this is a single tuple key in the level
3293+
try:
3294+
# Argument "indexer" to "_get_level_indexer" of "MultiIndex"
3295+
# has incompatible type "Index"; expected "Optional[Int64Index]"
3296+
lev_loc = self._get_level_indexer(
3297+
k, level=i, indexer=indexer # type: ignore[arg-type]
3298+
)
3299+
except (InvalidIndexError, TypeError, KeyError) as err:
3300+
# InvalidIndexError e.g. non-hashable, fall back to treating
3301+
# this as a sequence of labels
3302+
# KeyError it can be ambiguous if this is a label or sequence
3303+
# of labels
3304+
# github.com/pandas-dev/pandas/issues/39424#issuecomment-871626708
3305+
for x in k:
3306+
if not is_hashable(x):
3307+
# e.g. slice
3308+
raise err
3309+
try:
3310+
# Argument "indexer" to "_get_level_indexer" of "MultiIndex"
3311+
# has incompatible type "Index"; expected
3312+
# "Optional[Int64Index]"
3313+
item_lvl_indexer = self._get_level_indexer(
3314+
x, level=i, indexer=indexer # type: ignore[arg-type]
3315+
)
3316+
except KeyError:
3317+
# ignore not founds; see discussion in GH#39424
3318+
warnings.warn(
3319+
"The behavior of indexing on a MultiIndex with a "
3320+
"nested sequence of labels is deprecated and will "
3321+
"change in a future version. "
3322+
"`series.loc[label, sequence]` will raise if any "
3323+
"members of 'sequence' or not present in "
3324+
"the index's second level. To retain the old "
3325+
"behavior, use `series.index.isin(sequence, level=1)`",
3326+
# TODO: how to opt in to the future behavior?
3327+
# TODO: how to handle IntervalIndex level?
3328+
# (no test cases)
3329+
FutureWarning,
3330+
stacklevel=7,
3331+
)
3332+
continue
33183333
else:
3319-
indexers = indexers.union(idxrs, sort=False)
3334+
idxrs = _convert_to_indexer(item_lvl_indexer)
3335+
3336+
if indexers is None:
3337+
indexers = idxrs
3338+
else:
3339+
indexers = indexers.union(idxrs, sort=False)
3340+
3341+
else:
3342+
idxrs = _convert_to_indexer(lev_loc)
3343+
if indexers is None:
3344+
indexers = idxrs
3345+
else:
3346+
indexers = indexers.union(idxrs, sort=False)
33203347

33213348
if indexers is not None:
33223349
indexer = _update_indexer(indexers, indexer=indexer)

pandas/tests/indexing/test_loc.py

+30
Original file line numberDiff line numberDiff line change
@@ -2583,6 +2583,36 @@ def test_loc_with_period_index_indexer():
25832583
tm.assert_frame_equal(df, df.loc[list(idx)])
25842584

25852585

2586+
def test_loc_getitem_multiindex_tuple_level():
2587+
# GH#27591
2588+
lev1 = ["a", "b", "c"]
2589+
lev2 = [(0, 1), (1, 0)]
2590+
lev3 = [0, 1]
2591+
cols = MultiIndex.from_product([lev1, lev2, lev3], names=["x", "y", "z"])
2592+
df = DataFrame(6, index=range(5), columns=cols)
2593+
2594+
# the lev2[0] here should be treated as a single label, not as a sequence
2595+
# of labels
2596+
result = df.loc[:, (lev1[0], lev2[0], lev3[0])]
2597+
2598+
# TODO: i think this actually should drop levels
2599+
expected = df.iloc[:, :1]
2600+
tm.assert_frame_equal(result, expected)
2601+
2602+
alt = df.xs((lev1[0], lev2[0], lev3[0]), level=[0, 1, 2], axis=1)
2603+
tm.assert_frame_equal(alt, expected)
2604+
2605+
# same thing on a Series
2606+
ser = df.iloc[0]
2607+
expected2 = ser.iloc[:1]
2608+
2609+
alt2 = ser.xs((lev1[0], lev2[0], lev3[0]), level=[0, 1, 2], axis=0)
2610+
tm.assert_series_equal(alt2, expected2)
2611+
2612+
result2 = ser.loc[lev1[0], lev2[0], lev3[0]]
2613+
assert result2 == 6
2614+
2615+
25862616
class TestLocSeries:
25872617
@pytest.mark.parametrize("val,expected", [(2 ** 63 - 1, 3), (2 ** 63, 4)])
25882618
def test_loc_uint64(self, val, expected):

0 commit comments

Comments
 (0)