Move data between the index and the columns — and replace the labels outright.
Have you ever run a groupby, gotten back exactly the numbers you wanted, and then found that you could not merge them, could not chart them, and could not write them out, because the things you grouped by were not columns any more?
That is the single most common reason people reach for reset_index, and it is worth leading with. groupby does not throw the grouping keys away. It promotes them into the index, which is why they print at the left of the result, in bold in a notebook, without a column name above them. reset_index demotes them back into ordinary columns and puts a plain RangeIndex — 0, 1, 2, 3 — in their place.
Once you see the index as a place where data can live, its partner falls out of the same idea. set_index moves in the opposite direction, promoting one or more columns up into the index. Two methods, one question: where do the labels live now, and where should they live instead?
Official documentation: DataFrame.reset_index, and DataFrame.set_index
The arguments that earn their keep
df.reset_index(level=None, # only these levels; default is all of them
drop=False, # True throws the old labels away
names=None) # names for the new columns
df.set_index(keys, # a column name, or a list of them
drop=True, # False keeps the column as a column too
append=False) # True adds a level instead of replacing
drop= is the argument you will use most, and the one worth pausing over. reset_index() on its own preserves the old labels by turning them into a column; drop=True discards them. Neither is the safe default, which is the whole problem — you have to know which you meant.
level= resets only part of a MultiIndex and leaves the rest alone. names= renames the resulting columns in the same call, which matters more than it sounds like it should, because Pandas invents names like index and level_1 when the index levels are unnamed. One wrinkle: on a series the argument is singular, name=, and it names the column holding the values. Ask a series for names= and you get TypeError: Series.reset_index() got an unexpected keyword argument 'names'. Did you mean 'name'? — one of the friendlier error messages in Pandas.
set_axis has an axis argument with the same trap, and its own page.
A worked example, on real data
The Global Coal Plant Tracker lists every coal-fired generating unit on earth, one per row, with its country, region, capacity, status, and start year. It is the workbook behind Bamboo Weekly #64.
import pandas as pd
url = ('https://www.bambooweekly.com/content/files/wp-content/uploads/2024/02/'
'global-coal-plant-tracker-january-2024.xlsx')
df = pd.read_excel(url, sheet_name='Units',
usecols=['Country', 'Region', 'Status', 'Start year',
'Capacity (MW)'])
Start with the archetype. Group operating capacity by region and country, and both keys land in the index:
capacity = (
df
.loc[lambda df_: df_['Status'] == 'operating']
.groupby(['Region', 'Country'])['Capacity (MW)'].sum()
)
capacity.head(4)
Region Country
Africa Botswana 732.0
Madagascar 120.0
Mauritius 195.0
Morocco 4257.0
Name: Capacity (MW), dtype: float64
That is a series with a two-level index, and it is correct. It is also not a table. reset_index makes it one:
capacity.reset_index().head(4)
Region Country Capacity (MW)
0 Africa Botswana 732.0
1 Africa Madagascar 120.0
2 Africa Mauritius 195.0
3 Africa Morocco 4257.0
Three columns, a fresh RangeIndex, and something px.bar or merge will accept without argument. Because the index levels were named, the new columns inherited those names. The values column keeps the series name, and name= is how you change it in the same breath:
capacity.nlargest(5).reset_index(name='operating_mw')
Region Country operating_mw
0 Asia China 1136731.0
1 Asia India 237148.2
2 Americas United States 200090.2
3 Asia Japan 55123.0
4 Asia Indonesia 51556.6
You do not have to reset the whole thing. level= takes one level down and leaves the other where it is, which is often exactly what you want when the inner level is the interesting one:
capacity.reset_index(level='Region').head(4)
Region Capacity (MW)
Country
Botswana Africa 732.0
Madagascar Africa 120.0
Mauritius Africa 195.0
Morocco Africa 4257.0
Now the other direction. set_index takes a column name, or a list of them for a MultiIndex:
df.set_index(['Region', 'Status']).head(3)
Country Capacity (MW) Start year
Region Status
Europe cancelled Albania 800.0 NaN
Americas operating Argentina 120.0 2022.0
construction Argentina 120.0 2023.0
append=True says "add this to what is already there" rather than "replace it," so the same MultiIndex can be built in two steps, which is what you need when the second column only becomes available later in the chain:
df.set_index('Region').set_index('Status', append=True).head(3)
Country Capacity (MW) Start year
Region Status
Europe cancelled Albania 800.0 NaN
Americas operating Argentina 120.0 2022.0
construction Argentina 120.0 2023.0
And drop=False leaves the column in place while also making it the index, which is how you index a frame for a join without losing the column that a chart still needs to label by:
df.set_index('Country', drop=False).head(3)
Country Capacity (MW) Status Start year Region
Country
Albania Albania 800.0 cancelled NaN Europe
Argentina Argentina 120.0 operating 2022.0 Americas
Argentina Argentina 120.0 construction 2023.0 Americas
There is a fuller treatment of set_index — including why a DatetimeIndex lets you slice with .loc['2024-04'] — on the set_index page.
set_axis, which has its own page
There is a third way to change labels, for the case where you have a list of correct names and no mapping to get there from — often because the current names are Unnamed: 1 and Unnamed: 2 and there is nothing to map from. That is set_axis, and it has enough of its own arguments and traps to deserve a page: set_axis.
Four mistakes people make
Calling reset_index() on a filtered frame without drop=True. This is the one that follows you around. Filter a frame and the surviving rows keep their original row numbers, so a bare reset_index turns those numbers into a column called index:
poland = df.loc[lambda df_: df_['Country'] == 'Poland']
poland.reset_index().head(3)
index Country Capacity (MW) Status Start year Region
0 10643 Poland 120.0 retired 1964.0 Europe
1 10644 Poland 120.0 retired 1964.0 Europe
2 10645 Poland 120.0 retired 1964.0 Europe
Nothing complains. The column rides along into to_csv, where it shows up in somebody's spreadsheet as a mystery first column, and into merge, where two frames that were both reset this way produce index_x and index_y alongside your real data. When the old row numbers are meaningless, say drop=True and mean it.
Reaching for drop=True reflexively, and losing the labels. The opposite mistake, and it is quieter, because there is no leftover column to notice. Sometimes the index is the data. Bamboo Weekly #54 is the case in point: the NY Fed's household debt report puts the year and quarter in the index, so the solution calls reset_index(), slices the year out of the resulting index column, groups by it, and only then drops the column. drop=True at the top of that chain would have thrown away the only copy of the dates.
Setting an index on a column with duplicates. set_index does not object, and it should not — a non-unique index is legal and sometimes deliberate. But .loc[] changes shape underneath you. Countries repeat in this file, once per generating unit:
df.set_index('Country').index.is_unique
False
df.set_index('Country').loc['Poland'].shape
(231, 4)
The reader who wrote .loc['Poland'] expecting one row got 231, and a data frame where they were counting on a series. Check before you rely on it. Note that verify_integrity=True is not the answer any more: in Pandas 3 it raises Pandas4Warning: The 'verify_integrity' keyword in DataFrame.set_index is deprecated and will be removed in a future version. Directly check the result.index.is_unique instead. Take the advice — is_unique is a one-line assertion and it reads better anyway.
Where it shows up in Bamboo Weekly
Fifty-two Bamboo Weekly solutions call reset_index. Four worth studying, all of them free to read:
Bamboo Weekly #60: Iceland is the round trip in a single chain: .loc[2019].reset_index().set_index('Country') takes a year out of a MultiIndex, flattens what is left into columns, and then re-indexes on the column the next join needs to align on. That pattern — reset, then set again on a different key — is how most joins between two awkwardly shaped frames actually get done.
Bamboo Weekly #70: Moon missions is the clearest level= example on the site. The table of 21st-century moon missions has a two-part index, and df.reset_index(level=0)['Mission.1'] moves only the outer level down into a column, leaving the spacecraft names in the index so that a filter(regex=...) can match on them.
Bamboo Weekly #54: Household debt is the one described above, and the best argument against reflexive drop=True. The index holds year-and-quarter labels from a badly formatted Excel export; reset_index is what makes them reachable with .str.
Practice it
Work through a reset_index exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/reset-index/
Go deeper
The index is the thing that most other Pandas methods quietly depend on, so this page has a lot of neighbors. set_index goes into why a good index makes .loc[] and time-based slicing easy; sort_index puts those labels in order, which a MultiIndex needs before it will slice; xs selects on an inner level of one; and unstack moves a level sideways into the columns rather than down into the data. For changing labels rather than moving them, rename handles the mapping case and covers rename_axis as well. The Pandas user guide on indexing and selecting data 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
.sort_index()— when the labels are right and only their order is wrong.xs()— when you want one slice of a MultiIndex rather than to flatten it.set_axis()— when you are replacing every label from a list rather than moving them.set_index()— when a column should become the index rather than the other way round
See it on real data
Below are the 52 Bamboo Weekly exercises that use reset_index on real-world data — try each one, then study the worked solution.
- Bamboo Weekly #184: Parmesan cheese
- Bamboo Weekly #180: Movies
- Bamboo Weekly #178: Harmful algal bloom
- Bamboo Weekly #177: European Summer
- Bamboo Weekly #174: Vacation
- Bamboo Weekly #172: World Cup
- Bamboo Weekly #171: Hantavirus
- Bamboo Weekly #170: Port of Long Beach
- Bamboo Weekly #169: Press freedom
- Bamboo Weekly #168: US gas prices
- Bamboo Weekly #166: Income tax
- Bamboo Weekly #164: Fertilizer
- Bamboo Weekly #163: Daylight saving time
- Bamboo Weekly #162: Spotify and car accidents
- Bamboo Weekly #161: Missiles in Israel
- Bamboo Weekly #159: State of the Union
- Bamboo Weekly #158: University endowments
- Bamboo Weekly #156: Winter Olympics
- Bamboo Weekly #155: Gold
- Bamboo Weekly #154: University rankings
- Bamboo Weekly #152: Congestion pricing
- Bamboo Weekly #150: Kalshi
- Bamboo Weekly #147: Presidential pardons
- Bamboo Weekly #146: Thanksgiving travel
- Bamboo Weekly #142: Hurricanes
- Bamboo Weekly #138: Federal workers
- Bamboo Weekly #129: Tom Lehrer
- Bamboo Weekly #128: Extreme heat
- Bamboo Weekly #125: Shrinking dollars
- Bamboo Weekly #123: Missiles
- Bamboo Weekly #122: Economic growth
- Bamboo Weekly #113: US airport traffic
- Bamboo Weekly #108: Measles
- Bamboo Weekly #106: Flu season
- Bamboo Weekly #93: Anti-politics
- Bamboo Weekly #92: Climate disaster costs
- Bamboo Weekly #90: Voter participation
- Bamboo Weekly #89: Housing
- Bamboo Weekly #83: Gasoline prices
- Bamboo Weekly #76: Aging legislators
- Bamboo Weekly #72: City travel
- Bamboo Weekly #71: Holidays
- Bamboo Weekly #70: Moon missions
- Bamboo Weekly #67: Electric cars
- Bamboo Weekly #62: Economic report card
- Bamboo Weekly #60: Iceland
- Bamboo Weekly #59: Long covid
- Bamboo Weekly #54: Household debt
- Bamboo Weekly #36: Nobel Prize
- Bamboo Weekly #23: Misery index
- Bamboo Weekly #15: Eurovision
- Bamboo Weekly #6: End of the humanities?
Part of the Pandas Methods Index. See also practice by skill.