Put a date and a ticker in the index, pull out one date or one ticker, and switch between the long and wide shapes.
A panel is a table with one row per date and per ticker. Three tickers over 40 days is 120 rows. The date alone does not name a row and the ticker alone does not either, so a panel needs both as its key.
In this lesson I put Date and Ticker into the index together, pull out one date and one ticker, then switch between the long shape and the wide shape and show which calculation each one suits.
The prices are simulated, not real market data.
Step 1. Two keys in the index
prices.csv is already stacked: a date, a ticker, and the numbers for that pair.
One row is named by a pair. The date prints once and the ticker repeats under it, which is display only: every row still carries both values.
Step 2. One date, one ticker, one cell
.loc with the outer level takes a slice of dates. Give it one date and you get that day’s three tickers, with Date used up and only Ticker left in the index.
120 rows of one column became 40 rows of three columns. 40 times 3 is 120, the same closes rearranged.
Step 4. The wide shape reads across a row
A cross-sectional calculation compares the tickers to each other on one date. In the wide shape one date is one row, so it is a calculation along a row.
Ticker AAPL MSFT NVDA
Date
2026-01-02 NaN NaN NaN
2026-01-05 2.0 1.0 3.0
2026-01-06 1.0 2.0 3.0
2026-01-07 3.0 1.0 2.0
2026-01-08 2.0 3.0 1.0
The first row is all NaN because pct_change has no day before 2026-01-02. rets.mean(axis=1) averages along the row the same way, giving the average return of the three on each date.
Step 5. Wide to long with stack
stack() is the reverse. It folds the columns back down into an index level.
(120,)
['Date', 'Ticker']
Date Ticker
2026-01-02 AAPL NaN
MSFT NaN
NVDA NaN
2026-01-05 AAPL -0.0015
dtype: float64
40 by 3 went back to 120 rows keyed by date and ticker. Now reset_index turns those two levels back into ordinary columns, which is the shape groupby from Lesson 22 works on.
Down a column, per ticker, is groupby on the long shape. Across a row, per date, is one call on the wide shape. The long shape can do the cross-section too, by grouping on the other key:
Date Ticker ret rank
12 2026-01-08 AAPL 0.010842 2.0
13 2026-01-08 MSFT -0.006730 3.0
14 2026-01-08 NVDA 0.034838 1.0
Same numbers as day.rank(ascending=False) in Step 4.
Your turn
Using rets from Step 4, find which ticker had the highest return on each day, then count how many days each ticker won. Drop the first row first, because it is all NaN.
16 plus 13 plus 10 is 39, one winner for each of the 39 days that has a return.
NoteWhy sort_index() after set_index?
.loc["2026-01-02":"2026-01-06"] slices a range of dates on the outer level, and a slice needs the index in order. On an unsorted MultiIndex pandas raises UnsortedIndexError. Sorting once after set_index makes every later slice work.
panel.xs("NVDA", level="Ticker") and groupby do not need the sort, since they match values rather than walk a range.
NoteWhich level goes on top?
set_index(["Date", "Ticker"]) puts dates on the outer level. set_index(["Ticker", "Date"]) puts tickers there instead, and then panel.loc["AAPL"] gives you one ticker’s 40 days.
unstack takes the level you name whichever way round it sits, so unstack("Date") gives 3 rows and 40 columns rather than 40 by 3.