How do I slice across levels of a MultiIndex in pandas?
Short answer
Use pd.IndexSlice to select on inner levels while leaving outer ones open, and sort the index first. Without sorting, partial slicing is slow and can raise.
The short version
Use pd.IndexSlice to write readable selections across levels, with : meaning "every value at this level". Sort the index first with sort_index(), because partial slicing on an unsorted MultiIndex is slow and can raise UnsortedIndexError.
The syntax
import pandas as pd
df = pd.DataFrame({
"city": ["Leeds", "Leeds", "Bath"],
"year": [2020, 2021, 2020],
"value": [1, 2, 3],
}).set_index(["city", "year"]).sort_index()
idx = pd.IndexSlice
print(df.loc[idx[:, 2020], :]["value"].tolist()) # [3, 1]
The : in the first position means every city, and 2020 pins the year. The trailing , : selects every column, and it is required: .loc needs both a row and a column selector when you use IndexSlice.
Why not just loc
A plain .loc["Leeds"] works on the outer level, but there is no way to pin an inner level without naming the outer one. IndexSlice is what lets you leave outer levels open.
For a single inner level, xs is simpler and reads well.
df.xs(2020, level="year")
Use xs for one level, and IndexSlice when you need to combine constraints across several levels or use ranges.
Ranges across levels
IndexSlice accepts slices, which is where it really earns its keep.
df.loc[idx["Bath":"Leeds", 2020:2021], :]
Note that label slices are inclusive at both ends, unlike positional slicing.
Sort the index, always
df = df.sort_index()
An unsorted MultiIndex makes lookups slow, and partial slicing raises a performance-related error. Sorting once after you build the frame fixes both. If a selection unexpectedly errors, this is nearly always why.
Swapping levels
If you constantly select on the inner level, the index is probably the wrong way round. Swap it so the level you filter on most is outermost.
df.swaplevel().sort_index()
Remember to re-sort after swapping, because the order that made the old arrangement efficient does not carry over.
The honest advice
MultiIndex slicing is powerful but hard to read, and many teams find it a net loss. If you are fighting the syntax, reset_index() turns the levels back into ordinary columns and plain boolean filtering does the same job with far less ceremony.
flat = df.reset_index()
flat[flat["year"] == 2020]
Reserve the MultiIndex for cases where you genuinely benefit from it, such as the output of a grouped aggregation you are about to unstack, rather than as a default way of holding data.