Mathew K Analytics

Lesson 11 · Pandas Full Course

Master the Pandas Index: set_index, reset_index and sort_index | Pandas Course #11

In this lesson you'll be able to: Turn a column into row labels with setindex, and look rows up Undo it with resetindex, and repair gappy row numbers Put…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Pandas 11: Setting, Resetting, and Sorting Indexes#

In this lesson you'll be able to:

  • Turn a column into row labels with set_index, and look rows up
  • Undo it with reset_index, and repair gappy row numbers
  • Put rows in label order with sort_index
  • Build a two-level index of country and year
  • Let the index line up data from two tables for you Dataset: Gapminder, 142 countries from 1952 to 2007 (data/pandas/gapminder.csv)

Warm-up: query and isin from lesson 10#

Lesson 10 filtered rows with query and isin. Same tools, new data.

import pandas as pd
df = pd.read_csv("data/pandas/gapminder.csv")
print(df.shape)
df.head()
(1704, 6)
country year pop continent lifeExp gdpPercap
0 Afghanistan 1952 8425333.0 Asia 28.801 779.445314
1 Afghanistan 1957 9240934.0 Asia 30.332 820.853030
2 Afghanistan 1962 10267083.0 Asia 31.997 853.100710
3 Afghanistan 1967 11537966.0 Asia 34.020 836.197138
4 Afghanistan 1972 13079460.0 Asia 36.088 739.981106
nordic = ["Norway", "Sweden", "Denmark", "Finland", "Iceland"]
recent = df.query("year == 2007")
recent[recent["country"].isin(nordic)]
country year pop continent lifeExp gdpPercap
419 Denmark 2007 5468120.0 Europe 78.332 35278.41874
527 Finland 2007 5238460.0 Europe 79.313 33207.08440
695 Iceland 2007 301931.0 Europe 81.757 36180.78919
1151 Norway 2007 4627926.0 Europe 80.196 49357.19017
1475 Sweden 2007 9031088.0 Europe 80.884 33859.74835

Look rows up by name with set_index#

Problem: row numbers like 803 mean nothing. Country names do.

print(df.index)
RangeIndex(start=0, stop=1704, step=1)

Common mistake: looking up a name the index doesn't contain

df.loc["Japan"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[5], line 1
----> 1 df.loc["Japan"]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1207, in _LocationIndexer.__getitem__(self, key)
   1205 maybe_callable = com.apply_if_callable(key, self.obj)
   1206 maybe_callable = self._raise_callable_usage(key, maybe_callable)
-> 1207 return self._getitem_axis(maybe_callable, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1449, in _LocIndexer._getitem_axis(self, key, axis)
   1447 # fall thru to straight lookup
   1448 self._validate_key(key, axis)
-> 1449 return self._get_label(key, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1399, in _LocIndexer._get_label(self, label, axis)
   1397 def _get_label(self, label, axis: AxisInt):
   1398     # GH#5567 this will fail if the label is not present in the axis.
-> 1399     return self.obj.xs(label, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:4241, in NDFrame.xs(self, key, axis, level, drop_level)
   4239             new_index = index[loc]
   4240 else:
-> 4241     loc = index.get_loc(key)
   4243     if isinstance(loc, np.ndarray):
   4244         if loc.dtype == np.bool_:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\range.py:535, in RangeIndex.get_loc(self, key)
    533         raise KeyError(key) from err
    534 if isinstance(key, Hashable):
--> 535     raise KeyError(key)
    536 self._check_indexing_error(key)
    537 raise KeyError(key)

KeyError: 'Japan'
by_country = recent.set_index("country")
by_country.loc["Japan"]
year                2007
pop          127467972.0
continent           Asia
lifeExp           82.603
gdpPercap    31656.06806
Name: Japan, dtype: object
print(by_country.index[:5].tolist())
print(by_country.shape)
['Afghanistan', 'Albania', 'Algeria', 'Angola', 'Argentina']
(142, 5)
pick = ["Kenya", "Ghana", "Norway"]
by_country.loc[pick, ["lifeExp", "gdpPercap"]].round(1)
lifeExp gdpPercap
country
Kenya 54.1 1463.2
Ghana 60.0 1327.6
Norway 80.2 49357.2
by_country.loc["Kenya":"Lesotho", ["lifeExp"]]
lifeExp
country
Kenya 54.110
Korea, Dem. Rep. 67.297
Korea, Rep. 78.623
Kuwait 77.588
Lebanon 71.993
Lesotho 42.592
both = recent.set_index("country", drop=False)
both[["country", "lifeExp"]].head(2)
country lifeExp
country
Afghanistan Afghanistan 43.828
Albania Albania 76.423
print(df["country"].is_unique)
print(by_country.index.is_unique)
False
True

Quick check: what was Brazil's life expectancy in 2007?

print(by_country.loc["Brazil", "lifeExp"])
72.39

Quick check: after recent.set_index("country"), does recent change?

recent.set_index("country")
print(recent.columns.tolist())
['country', 'year', 'pop', 'continent', 'lifeExp', 'gdpPercap']

Undo it with reset_index#

Problem: labels need to become a normal column again, or be renumbered.

flat = by_country.reset_index()
flat.head(3)
country year pop continent lifeExp gdpPercap
0 Afghanistan 2007 31889923.0 Asia 43.828 974.580338
1 Albania 2007 3600523.0 Europe 76.423 5937.029526
2 Algeria 2007 33333216.0 Africa 72.301 6223.367465
gone = by_country.reset_index(drop=True)
print(gone.columns.tolist())
['year', 'pop', 'continent', 'lifeExp', 'gdpPercap']
life = recent.groupby("continent")["lifeExp"].mean().round(1)
life.reset_index()
continent lifeExp
0 Africa 54.8
1 Americas 73.6
2 Asia 70.7
3 Europe 77.6
4 Oceania 80.7
europe = df.query("continent == 'Europe' and year == 2007")
print(europe.index[:5].tolist())
[23, 83, 119, 155, 191]

Common mistake: asking for row 0 after a filter

europe.loc[0]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3641, in Index.get_loc(self, key)
   3640 try:
-> 3641     return self._engine.get_loc(casted_key)
   3642 except KeyError as err:

File pandas/_libs/index.pyx:168, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:197, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:2761, in pandas._libs.hashtable.Int64HashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:2785, in pandas._libs.hashtable.Int64HashTable.get_item()

KeyError: 0

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[18], line 1
----> 1 europe.loc[0]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1207, in _LocationIndexer.__getitem__(self, key)
   1205 maybe_callable = com.apply_if_callable(key, self.obj)
   1206 maybe_callable = self._raise_callable_usage(key, maybe_callable)
-> 1207 return self._getitem_axis(maybe_callable, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1449, in _LocIndexer._getitem_axis(self, key, axis)
   1447 # fall thru to straight lookup
   1448 self._validate_key(key, axis)
-> 1449 return self._get_label(key, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1399, in _LocIndexer._get_label(self, label, axis)
   1397 def _get_label(self, label, axis: AxisInt):
   1398     # GH#5567 this will fail if the label is not present in the axis.
-> 1399     return self.obj.xs(label, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:4241, in NDFrame.xs(self, key, axis, level, drop_level)
   4239             new_index = index[loc]
   4240 else:
-> 4241     loc = index.get_loc(key)
   4243     if isinstance(loc, np.ndarray):
   4244         if loc.dtype == np.bool_:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3648, in Index.get_loc(self, key)
   3643     if isinstance(casted_key, slice) or (
   3644         isinstance(casted_key, abc.Iterable)
   3645         and any(isinstance(x, slice) for x in casted_key)
   3646     ):
   3647         raise InvalidIndexError(key) from err
-> 3648     raise KeyError(key) from err
   3649 except TypeError:
   3650     # If we have a listlike key, _check_indexing_error will raise
   3651     #  InvalidIndexError. Otherwise we fall through and re-raise
   3652     #  the TypeError.
   3653     self._check_indexing_error(key)

KeyError: 0
europe = europe.reset_index(drop=True)
europe.loc[0, ["country", "lifeExp"]]
country    Albania
lifeExp     76.423
Name: 0, dtype: object

Your turn#

Take the 2007 African rows, number them from 0, and show the last 3.

africa = df.query("continent == 'Africa' and year == 2007")
africa = africa.reset_index(drop=True)
africa[["country", "lifeExp"]].tail(3)
country lifeExp
49 Uganda 51.542
50 Zambia 42.384
51 Zimbabwe 43.487

Put rows in label order with sort_index#

Problem: rows end up shuffled after sorting by values.

richest = by_country.sort_values("gdpPercap", ascending=False)
richest[["gdpPercap"]].head().round(0)
gdpPercap
country
Norway 49357.0
Kuwait 47307.0
Singapore 47143.0
United States 42952.0
Ireland 40676.0
alpha = richest.sort_index()
alpha[["gdpPercap"]].head(3).round(0)
gdpPercap
country
Afghanistan 975.0
Albania 5937.0
Algeria 6223.0
print(alpha.index.is_monotonic_increasing)
print(richest.index.is_monotonic_increasing)
True
False
by_country.sort_index(axis=1).head(2)
continent gdpPercap lifeExp pop year
country
Afghanistan Asia 974.580338 43.828 31889923.0 2007
Albania Europe 5937.029526 76.423 3600523.0 2007

Quick check: which country comes last alphabetically?

by_country.sort_index(ascending=False).index[0]
'Zimbabwe'

Your turn#

Index the 2007 Asian rows by country, sort the labels, and show the last 3.

asia = df.query("continent == 'Asia' and year == 2007")
asia = asia.set_index("country").sort_index()
asia[["lifeExp"]].tail(3)
lifeExp
country
Vietnam 74.249
West Bank and Gaza 73.422
Yemen, Rep. 62.698
years = df.set_index("year")
print(years.index[:3].tolist())
[1952, 1957, 1962]

Common mistake: slicing a range of unsorted labels

years.loc[1952:1957]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[28], line 1
----> 1 years.loc[1952:1957]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1207, in _LocationIndexer.__getitem__(self, key)
   1205 maybe_callable = com.apply_if_callable(key, self.obj)
   1206 maybe_callable = self._raise_callable_usage(key, maybe_callable)
-> 1207 return self._getitem_axis(maybe_callable, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1429, in _LocIndexer._getitem_axis(self, key, axis)
   1427 if isinstance(key, slice):
   1428     self._validate_key(key, axis)
-> 1429     return self._get_slice_axis(key, axis=axis)
   1430 elif com.is_bool_indexer(key):
   1431     return self._getbool_axis(key, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1461, in _LocIndexer._get_slice_axis(self, slice_obj, axis)
   1458     return obj.copy(deep=False)
   1460 labels = obj._get_axis(axis)
-> 1461 indexer = labels.slice_indexer(slice_obj.start, slice_obj.stop, slice_obj.step)
   1463 if isinstance(indexer, slice):
   1464     return self.obj._slice(indexer, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:6804, in Index.slice_indexer(self, start, end, step)
   6753 def slice_indexer(
   6754     self,
   6755     start: Hashable | None = None,
   6756     end: Hashable | None = None,
   6757     step: int | None = None,
   6758 ) -> slice:
   6759     """
   6760     Compute the slice indexer for input labels and step.
   6761 
   (...)
   6802     slice(1, 3, None)
   6803     """
-> 6804     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6806     # return a slice
   6807     if not is_scalar(start_slice):

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:7062, in Index.slice_locs(self, start, end, step)
   7060 start_slice = None
   7061 if start is not None:
-> 7062     start_slice = self.get_slice_bound(start, "left")
   7063 if start_slice is None:
   7064     start_slice = 0

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:6985, in Index.get_slice_bound(self, label, side)
   6983     slc = lib.maybe_booleans_to_slice(slc.view("u1"))
   6984     if isinstance(slc, np.ndarray):
-> 6985         raise KeyError(
   6986             f"Cannot get {side} slice bound for non-unique "
   6987             f"label: {original_label!r}"
   6988         )
   6990 if isinstance(slc, slice):
   6991     if side == "left":

KeyError: 'Cannot get left slice bound for non-unique label: 1952'
years = years.sort_index()
print(years.loc[1952:1957].shape)
(284, 5)

Build a two-level index of country and year#

Problem: one country has 12 years, so country alone isn't unique.

panel = df.set_index(["country", "year"]).sort_index()
print(panel.index.is_unique)
panel.head(3)
True
pop continent lifeExp gdpPercap
country year
Afghanistan 1952 8425333.0 Asia 28.801 779.445314
1957 9240934.0 Asia 30.332 820.853030
1962 10267083.0 Asia 31.997 853.100710
panel.loc["Kenya"].head(3)
pop continent lifeExp gdpPercap
year
1952 6464046.0 Africa 42.270 853.540919
1957 7454779.0 Africa 44.686 944.438315
1962 8678557.0 Africa 47.949 896.966373

Quick check: how many rows does panel.loc["Kenya"] return?

print(len(panel.loc["Kenya"]))
12
print(panel.loc[("Kenya", 2007), "lifeExp"])
54.11
ice_ire = panel.loc["Iceland":"Ireland", "lifeExp"]
ice_ire.unstack()[[1952, 2007]].round(1)
year 1952 2007
country
Iceland 72.5 81.8
India 37.4 64.7
Indonesia 37.5 70.6
Iran 44.9 71.0
Iraq 45.3 59.5
Ireland 66.9 78.9
by_year = panel.sort_index(level="year")
by_year[["lifeExp"]].head(3)
lifeExp
country year
Afghanistan 1952 28.801
Albania 1952 55.230
Algeria 1952 43.077
newest = panel.sort_index(ascending=False)
newest[["lifeExp"]].head(3)
lifeExp
country year
Zimbabwe 2007 43.487
2002 39.989
1997 46.809
back = panel.reset_index(level="year")
back.head(3)
year pop continent lifeExp gdpPercap
country
Afghanistan 1952 8425333.0 Asia 28.801 779.445314
Afghanistan 1957 9240934.0 Asia 30.332 820.853030
Afghanistan 1962 10267083.0 Asia 31.997 853.100710

Common mistake: slicing an unsorted two-level index

messy = df.set_index(["continent", "country"])
messy.loc[("Asia", "China"):("Asia", "Japan")]
---------------------------------------------------------------------------
UnsortedIndexError                        Traceback (most recent call last)
Cell In[38], line 2
      1 messy = df.set_index(["continent", "country"])
----> 2 messy.loc[("Asia", "China"):("Asia", "Japan")]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1207, in _LocationIndexer.__getitem__(self, key)
   1205 maybe_callable = com.apply_if_callable(key, self.obj)
   1206 maybe_callable = self._raise_callable_usage(key, maybe_callable)
-> 1207 return self._getitem_axis(maybe_callable, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1429, in _LocIndexer._getitem_axis(self, key, axis)
   1427 if isinstance(key, slice):
   1428     self._validate_key(key, axis)
-> 1429     return self._get_slice_axis(key, axis=axis)
   1430 elif com.is_bool_indexer(key):
   1431     return self._getbool_axis(key, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1461, in _LocIndexer._get_slice_axis(self, slice_obj, axis)
   1458     return obj.copy(deep=False)
   1460 labels = obj._get_axis(axis)
-> 1461 indexer = labels.slice_indexer(slice_obj.start, slice_obj.stop, slice_obj.step)
   1463 if isinstance(indexer, slice):
   1464     return self.obj._slice(indexer, axis=axis)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:6804, in Index.slice_indexer(self, start, end, step)
   6753 def slice_indexer(
   6754     self,
   6755     start: Hashable | None = None,
   6756     end: Hashable | None = None,
   6757     step: int | None = None,
   6758 ) -> slice:
   6759     """
   6760     Compute the slice indexer for input labels and step.
   6761 
   (...)
   6802     slice(1, 3, None)
   6803     """
-> 6804     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6806     # return a slice
   6807     if not is_scalar(start_slice):

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3382, in MultiIndex.slice_locs(self, start, end, step)
   3329 """
   3330 For an ordered MultiIndex, compute the slice locations for input
   3331 labels.
   (...)
   3378                       sequence of such.
   3379 """
   3380 # This function adds nothing to its parent implementation (the magic
   3381 # happens in get_slice_bound method), but it adds meaningful doc.
-> 3382 return super().slice_locs(start, end, step)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:7062, in Index.slice_locs(self, start, end, step)
   7060 start_slice = None
   7061 if start is not None:
-> 7062     start_slice = self.get_slice_bound(start, "left")
   7063 if start_slice is None:
   7064     start_slice = 0

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3324, in MultiIndex.get_slice_bound(self, label, side)
   3322 if not isinstance(label, tuple):
   3323     label = (label,)
-> 3324 result = self._partial_tup_index(label, side=side)
   3325 result = maybe_unbox_numpy_scalar(result)
   3326 return result

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3386, in MultiIndex._partial_tup_index(self, tup, side)
   3384 def _partial_tup_index(self, tup: tuple, side: Literal["left", "right"] = "left"):
   3385     if len(tup) > self._lexsort_depth:
-> 3386         raise UnsortedIndexError(
   3387             f"Key length ({len(tup)}) was greater than MultiIndex lexsort depth "
   3388             f"({self._lexsort_depth})"
   3389         )
   3391     n = len(tup)
   3392     start, end = 0, len(self)

UnsortedIndexError: 'Key length (2) was greater than MultiIndex lexsort depth (0)'
messy = messy.sort_index()
print(messy.loc[("Asia", "China"):("Asia", "Japan")].shape)
(96, 4)

Your turn#

With panel, show India's population in 1952 and in 2007.

india = panel.loc["India", "pop"]
india.loc[[1952, 2007]].astype("int64")
year
1952     372000000
2007    1110396331
Name: pop, dtype: int64

Let the index line up two tables#

Problem: subtract 1952 from 2007, country by country.

y52 = df.query("year == 1952").set_index("country")["lifeExp"]
y07 = df.query("year == 2007").set_index("country")["lifeExp"]
print(y52.loc["Kenya"], y07.loc["Kenya"])
42.27 54.11
print(y52.index.equals(y07.index))
True
shuffled = y07.sort_values()
gain = shuffled - y52
print(round(gain.loc["Kenya"], 3))
11.84

Quick check: what if the labels don't match?

a = pd.Series([1, 2], index=["UK", "Peru"])
b = pd.Series([10, 20], index=["Peru", "Chad"])
a + b
Chad     NaN
Peru    12.0
UK       NaN
dtype: float64

Your turn#

Align 1952 and 2007 population. How many times bigger did Nigeria get?

p52 = df.query("year == 1952").set_index("country")["pop"]
p07 = df.query("year == 2007").set_index("country")["pop"]
growth = p07 / p52
print(round(growth.loc["Nigeria"], 1))
4.1

Mini-project: who gained the most years of life?#

Plan: align the two years, sort the gains, check by continent.

gain = gain.sort_values(ascending=False)
print(gain.head().round(1))
print(gain.tail().round(1))
country
Oman            38.1
Vietnam         33.8
Indonesia       33.2
Saudi Arabia    32.9
Libya           31.2
Name: lifeExp, dtype: float64
country
Botswana     3.1
Lesotho      0.5
Zambia       0.3
Swaziland   -1.8
Zimbabwe    -5.0
Name: lifeExp, dtype: float64
print((gain > 30).sum())
print(round(gain.mean(), 1), round(gain.median(), 1))
9
17.9 18.4
cont = recent.set_index("country")["continent"]
table = pd.DataFrame({"gain": gain, "continent": cont})
table.groupby("continent")["gain"].median().round(1)
continent
Africa      16.0
Americas    21.6
Asia        24.7
Europe      12.6
Oceania     11.5
Name: gain, dtype: float64
table.loc[["Oman", "Norway", "Zimbabwe"]].round(1)
gain continent
country
Oman 38.1 Asia
Norway 7.5 Europe
Zimbabwe -5.0 Africa
eu = table.query("continent == 'Europe'")
eu.sort_values("gain").head(3).round(1)
gain continent
country
Norway 7.5 Europe
Denmark 7.6 Europe
Netherlands 7.6 Europe
import matplotlib.pyplot as plt
ends = pd.concat([gain.head(5), gain.tail(5)]).sort_values()
ax = ends.plot(kind="barh", figsize=(7, 4))
ax.set_title("Change in life expectancy, 1952 to 2007")
ax.set_xlabel("Years gained")
ax.set_ylabel("Country")
plt.show()
No description has been provided for this image
print((gain < 0).sum())
print(gain[gain < 0].round(1))
2
country
Swaziland   -1.8
Zimbabwe    -5.0
Name: lifeExp, dtype: float64

Recap#

  • set_index turns a column into row labels for loc lookups
  • reset_index moves labels back; drop=True renumbers from 0
  • Filters keep old row numbers, so reset before using loc[0]
  • sort_index orders by labels; sort before slicing a range
  • Maths between Series lines up by index labels

Challenges#

  1. Index 2007 by country and show the 5 most populous countries.
  2. With panel, show Japan's lifeExp for 1952 to 1972 only.
  3. Align 1952 and 2007 gdpPercap to find the fastest growth.

What's next#

Lesson 12: Renaming Columns and Indexes Clean up messy column names and relabel rows.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.