pandas·Lesson 20·12 min·0/4 exercises

MultiIndex, stack and unstack

An index whose labels are tuples — and two methods for moving levels sideways.

MultiIndex.unstack().stack().xs()swaplevelget_level_values

Watch it happen

Play it through, or step back and forth yourself.

orders.groupby(["city", "item"])["cups"].sum()
city / item
cups
Delhi / ginger
95
Delhi / masala
230
Mumbai / ginger
80
Mumbai / masala
60
Pune / masala
150

Group by two keys and you get a MultiIndex — an index whose labels are tuples rather than single values. It isn't a new kind of object; it's the same index with more than one level.

The idea

Group by two keys and pandas hands you a MultiIndex. It looks exotic and isn't: it's an index whose labels are tuples instead of single values.

s = orders.groupby(["city", "item"])["cups"].sum()
s.index          # MultiIndex([("Delhi", "elaichi"), ("Delhi", "ginger"), ...])
s.loc[("Delhi", "ginger")]

You'll meet them from groupby with several keys, from pivot_table, from concat(keys=...), and from agg with a list of functions.

unstack moves a level up

s.unstack()             # innermost level becomes the columns
s.unstack(level=0)      # or name which one
s.unstack(fill_value=0) # for combinations that never happened

Two levels down the side become one down and one across — which is exactly what pivot_table produced, arrived at from the other direction. They're the same operation with different entry points.

Going wide always materialises the combinations that never occurred. Pune never sold ginger, so unstacking invents that cell and fills it with NaN — and promotes the column to float in the process.

stack moves it back

wide.stack()                  # columns fold down into the index
wide.stack(dropna=False)      # keep the empty combinations

By default stack drops the NaN cells on the way down. That's usually helpful and occasionally surprising: a round trip through unstack().stack() can come back shorter than it went, having quietly removed combinations that were never there.

Selecting

s.loc[("Delhi", "ginger")]        # one cell — the label is a tuple
s.loc["Delhi"]                     # the whole outer group
s.loc["Delhi":"Mumbai"]            # a slice of the outer level
s.xs("masala", level="item")       # cut ACROSS — every city's masala
s.index.get_level_values("city")   # one level as a flat Index

.xs is the one worth remembering, because plain .loc can't express "every city's masala" — that cuts across the outer level rather than down it.

Almost all level slicing wants a sorted index. If you get UnsortedIndexError, or lexsort warnings, the fix is .sort_index() — do it as soon as you build the MultiIndex.

Rearranging levels

s.swaplevel().sort_index()     # put item on the outside instead
s.reorder_levels(["item", "city"])
s.groupby(level="city").sum()  # aggregate one level away

Or just flatten it

s.reset_index()                # levels become ordinary columns

Honestly, this is the right move most of the time. A MultiIndex earns its keep when you'll genuinely slice by level or the hierarchy is real — otherwise flat columns are easier to think about, easier to merge, and easier for the next person to read.

The same goes for MultiIndexed columns, which agg with a list of functions produces. Flatten them and move on:

df.columns = ["_".join(c).strip("_") for c in df.columns]

See it run

The lesson's code, ready to run and to fiddle with.

Putting the kettle on…

Starting up…

Worked example

not graded

Already written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.

trys.unstack().stack() and compare its length with s — the round trip can lose rows.

Press Run — the output appears here.

Your turn

4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

Group orders by city and item and return the total cups — a Series with a MultiIndex.

your answer

Take that Series and unstack it, filling the empty combinations with 0.

your answer

From the grouped Series, return every city's masala total — cutting across the inner level with .xs().

your answer

Flatten the grouped Series back into an ordinary frame with city, item and cups as columns.

your answer