Push the columns down into the row index — and, since Pandas 3, keep the holes.
Which direction are you moving? unstack lifts an index level up into the columns. stack does the opposite: it takes a column level and folds it down into the index, turning a rectangular table into a long series with one row per cell. That page covers the wide-versus-long idea and the pair as inverses. This one is about the thing that makes stack different in Pandas 3, which is that it no longer throws anything away.
Official documentation: DataFrame.stack.
The arguments that earn their keep
df.stack() # fold the innermost column level down (the default, -1)
df.stack(0) # fold the outermost level instead
df.stack('metric') # same thing, by name
There is one argument now: level. The other three are gone in all but name. dropna= and sort= raise ValueError if you pass them, and future_stack= still exists but defaults to True and does nothing — which matters, because older Bamboo Weekly solutions pass future_stack=True to silence a Pandas 2 warning and still run fine today.
A worked example, on real data
Bamboo Weekly #125 uses a FRED download of daily exchange rates: one row per date, one column per currency, going back to 1971. Series start at different times, so the table is full of gaps.
import pandas as pd
url = 'https://www.bambooweekly.com/content/files/2025/07/bw-125.csv'
df = pd.read_csv(url, index_col='observation_date',
parse_dates=['observation_date'])
df.shape
(14215, 12)
Stack it, and every cell becomes a row keyed by date and currency:
df.stack().tail(4)
observation_date
2025-06-27 DEXVZUS 106.5948
DTWEXAFEGS 110.3921
DEXINUS 85.4500
DEXBZUS 5.4801
dtype: float64
Here is the change, in two numbers:
df.stack().shape, df.stack().dropna().shape
((170580,), (112299,))
14,215 × 12 is 170,580 cells, and that is exactly what Pandas 3 gives back. Pandas 2 gave you 112,299 — the non-empty ones. Fifty-eight thousand rows appeared in an upgrade, and nothing warned anybody.
Why fold a table down at all? Because a series has methods a data frame does not. There is no two-dimensional idxmax, so the question "when did any currency move most in a single day, and which one?" has no answer until the table is one-dimensional:
df.pct_change().stack().abs().idxmax()
(Timestamp('2018-02-05 00:00:00'), 'DEXVZUS')
A Venezuelan redenomination, which is not really a market move. Drop that column and the list is history:
df.drop(columns='DEXVZUS').pct_change().stack().abs().nlargest(3).round(3)
observation_date
1994-01-03 DEXCHUS 0.500
1989-12-18 DEXCHUS 0.269
1994-12-22 DEXMXUS 0.213
dtype: float64
China's 1994 devaluation of the yuan, and Mexico's peso crisis eleven months later.
level matters once the columns have more than one level:
q = df.loc['2024', ['DEXUSEU', 'DEXJPUS']].resample('QE').agg(['min', 'max'])
q.stack(0).head(4)
min max
observation_date
2024-03-31 DEXUSEU 1.0720 1.0976
DEXJPUS 141.8900 151.6600
2024-06-30 DEXUSEU 1.0628 1.0890
DEXJPUS 151.5500 160.8800
Bare q.stack() folds min/max down instead and leaves the currencies as columns. Same data, two different questions — pick the level deliberately.
Three mistakes people make
Expecting a round trip to be symmetric. unstack builds a full rectangle, inventing NaN for combinations that never occurred; stack now hands every one of them back. So s.unstack().stack() returns more rows than s had. That is honest arithmetic, not a bug. Add .dropna() when you want the old shape.
Passing dropna=False to keep the missing values. You already have them, and the argument raises: ValueError: dropna must be unspecified as the new implementation does not introduce rows of NA values. The same goes for sort=, whose message tells you to chain sort_index instead.
Trusting a tutorial written before Pandas 3. Nearly everything published about stack says it drops missing values, because for a decade it did. If a row count you have relied on suddenly grew, this is why.
Where it shows up in Bamboo Weekly
Bamboo Weekly #72: City travel is the clearest use on the site, and free to read: three transit-mode columns get folded into the index so a scatter plot can color by mode.
Bamboo Weekly #75: Refugees, also free, stacks the inner level of a column MultiIndex so that destination and population sit side by side and can be divided.
Bamboo Weekly #175: Inflation stacks a correlation matrix into a series, then calls idxmax on it — the one-dimensional trick above, applied to a square table.
Bamboo Weekly #146: Thanksgiving travel uses stack(1) to pull the inner level of an agg result down out of the columns.
Practice it
Work through a stack exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/stack/
Go deeper
unstack is the other half of this idea and the page to read first. melt does a similar wide-to-long move but returns flat columns rather than an index level, which is usually what a plotting library wants. The Pandas user guide on reshaping and pivot tables is the canonical treatment.
More Pandas videos on Python and Pandas with Reuven Lerner.
Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.
Related methods
.unstack()— for the other direction: an index level becomes columns.melt()— when the columns should become an ordinary column rather than an index level
See it on real data
Below are the 5 Bamboo Weekly exercises that use stack on real-world data — try each one, then study the worked solution.
- Bamboo Weekly #175: Inflation
- Bamboo Weekly #146: Thanksgiving travel
- Bamboo Weekly #82: Broadband
- Bamboo Weekly #75: Refugees
- Bamboo Weekly #72: City travel
Part of the Pandas Methods Index. See also practice by skill.