Skip to content

Commit 4b18c90

Browse files
authored
REF: avoid try/except in maybe_mi_droplevel (#42326)
1 parent 7d05180 commit 4b18c90

File tree

4 files changed

+56
-31
lines changed

4 files changed

+56
-31
lines changed

pandas/core/generic.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -3767,8 +3767,10 @@ class animal locomotion
37673767
if isinstance(index, MultiIndex):
37683768
try:
37693769
loc, new_index = index._get_loc_level(key, level=0)
3770-
except TypeError as e:
3771-
raise TypeError(f"Expected label or tuple of labels, got {key}") from e
3770+
except TypeError as err:
3771+
raise TypeError(
3772+
f"Expected label or tuple of labels, got {key}"
3773+
) from err
37723774
else:
37733775
if not drop_level:
37743776
if lib.is_integer(loc):

pandas/core/indexes/multi.py

+29-24
Original file line numberDiff line numberDiff line change
@@ -2949,16 +2949,11 @@ def _get_loc_level(self, key, level: int | list[int] = 0):
29492949

29502950
# different name to distinguish from maybe_droplevels
29512951
def maybe_mi_droplevels(indexer, levels):
2952-
# kludge around
2953-
orig_index = new_index = self[indexer]
2952+
new_index = self[indexer]
29542953

29552954
for i in sorted(levels, reverse=True):
2956-
try:
2957-
new_index = new_index._drop_level_numbers([i])
2958-
except ValueError:
2955+
new_index = new_index._drop_level_numbers([i])
29592956

2960-
# no dropping here
2961-
return orig_index
29622957
return new_index
29632958

29642959
if isinstance(level, (tuple, list)):
@@ -2973,10 +2968,18 @@ def maybe_mi_droplevels(indexer, levels):
29732968
mask = np.zeros(len(self), dtype=bool)
29742969
mask[loc] = True
29752970
loc = mask
2976-
29772971
result = loc if result is None else result & loc
29782972

2979-
return result, maybe_mi_droplevels(result, level)
2973+
try:
2974+
# FIXME: we should be only dropping levels on which we are
2975+
# scalar-indexing
2976+
mi = maybe_mi_droplevels(result, level)
2977+
except ValueError:
2978+
# droplevel failed because we tried to drop all levels,
2979+
# i.e. len(level) == self.nlevels
2980+
mi = self[result]
2981+
2982+
return result, mi
29802983

29812984
# kludge for #1796
29822985
if isinstance(key, list):
@@ -2994,24 +2997,26 @@ def maybe_mi_droplevels(indexer, levels):
29942997

29952998
if not any(isinstance(k, slice) for k in key):
29962999

2997-
# partial selection
2998-
# optionally get indexer to avoid re-calculation
2999-
def partial_selection(key, indexer=None):
3000-
if indexer is None:
3001-
indexer = self.get_loc(key)
3002-
ilevels = [
3003-
i for i in range(len(key)) if key[i] != slice(None, None)
3004-
]
3005-
return indexer, maybe_mi_droplevels(indexer, ilevels)
3006-
30073000
if len(key) == self.nlevels and self.is_unique:
30083001
# Complete key in unique index -> standard get_loc
30093002
try:
30103003
return (self._engine.get_loc(key), None)
3011-
except KeyError as e:
3012-
raise KeyError(key) from e
3013-
else:
3014-
return partial_selection(key)
3004+
except KeyError as err:
3005+
raise KeyError(key) from err
3006+
3007+
# partial selection
3008+
indexer = self.get_loc(key)
3009+
ilevels = [i for i in range(len(key)) if key[i] != slice(None, None)]
3010+
if len(ilevels) == self.nlevels:
3011+
if is_integer(indexer):
3012+
# we are dropping all levels
3013+
return indexer, None
3014+
3015+
# TODO: in some cases we still need to drop some levels,
3016+
# e.g. test_multiindex_perf_warn
3017+
ilevels = []
3018+
return indexer, maybe_mi_droplevels(indexer, ilevels)
3019+
30153020
else:
30163021
indexer = None
30173022
for i, k in enumerate(key):
@@ -3032,7 +3037,7 @@ def partial_selection(key, indexer=None):
30323037

30333038
if indexer is None:
30343039
indexer = k_index
3035-
else: # pragma: no cover
3040+
else:
30363041
indexer &= k_index
30373042
if indexer is None:
30383043
indexer = slice(None, None)

pandas/tests/frame/indexing/test_xs.py

+17
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,23 @@ def test_xs_view(self, using_array_manager):
129129

130130

131131
class TestXSWithMultiIndex:
132+
def test_xs_doc_example(self):
133+
# TODO: more descriptive name
134+
# based on example in advanced.rst
135+
arrays = [
136+
["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
137+
["one", "two", "one", "two", "one", "two", "one", "two"],
138+
]
139+
tuples = list(zip(*arrays))
140+
141+
index = MultiIndex.from_tuples(tuples, names=["first", "second"])
142+
df = DataFrame(np.random.randn(3, 8), index=["A", "B", "C"], columns=index)
143+
144+
result = df.xs(("one", "bar"), level=("second", "first"), axis=1)
145+
146+
expected = df.iloc[:, [0]]
147+
tm.assert_frame_equal(result, expected)
148+
132149
def test_xs_integer_key(self):
133150
# see GH#2107
134151
dates = range(20111201, 20111205)

pandas/tests/indexes/datetimes/test_partial_slicing.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -343,22 +343,23 @@ def test_partial_slicing_with_multiindex(self):
343343
with pytest.raises(IndexingError, match=msg):
344344
df_multi.loc[("2013-06-19", "ACCT1", "ABC")]
345345

346+
def test_partial_slicing_with_multiindex_series(self):
346347
# GH 4294
347348
# partial slice on a series mi
348-
s = DataFrame(
349+
ser = DataFrame(
349350
np.random.rand(1000, 1000), index=date_range("2000-1-1", periods=1000)
350351
).stack()
351352

352-
s2 = s[:-1].copy()
353+
s2 = ser[:-1].copy()
353354
expected = s2["2000-1-4"]
354355
result = s2[Timestamp("2000-1-4")]
355356
tm.assert_series_equal(result, expected)
356357

357-
result = s[Timestamp("2000-1-4")]
358-
expected = s["2000-1-4"]
358+
result = ser[Timestamp("2000-1-4")]
359+
expected = ser["2000-1-4"]
359360
tm.assert_series_equal(result, expected)
360361

361-
df2 = DataFrame(s)
362+
df2 = DataFrame(ser)
362363
expected = df2.xs("2000-1-4")
363364
result = df2.loc[Timestamp("2000-1-4")]
364365
tm.assert_frame_equal(result, expected)

0 commit comments

Comments
 (0)