Mathew K Analytics

Lesson 14 · Pandas Full Course

Advanced Slicing in Pandas: MultiIndex, xs and IndexSlice Explained | Pandas Course #14

In this lesson you'll be able to: Slice rows and columns by label and by position, without off-by-one bugs Build a MultiIndex and slice it by country and…

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 14: Advanced Slicing and Cross-Section Operations#

In this lesson you'll be able to:

  • Slice rows and columns by label and by position, without off-by-one bugs
  • Build a MultiIndex and slice it by country and year range
  • Pull cross-sections out of any level with xs and IndexSlice
  • Slice wide tables with year columns, then flatten them again Data: Gapminder, 142 countries from 1952 to 2007

Warm-up: sorting from last lesson#

In lesson 13 we sorted and ranked. Who led in 2007?

import pandas as pd

gap = pd.read_csv("data/pandas/gapminder.csv")
print(gap.shape)
gap[gap["year"] == 2007].nlargest(3, "lifeExp")
(1704, 6)
country year pop continent lifeExp gdpPercap
803 Japan 2007 127467972.0 Asia 82.603 31656.06806
671 Hong Kong, China 2007 6980412.0 Asia 82.208 39724.97867
695 Iceland 2007 301931.0 Europe 81.757 36180.78919

Label slices versus position slices#

loc includes the end label; iloc stops before the end position.

Quick check: how many rows do loc[0:4] and iloc[0:4] return?

print(len(gap.loc[0:4]), "rows with loc")
print(len(gap.iloc[0:4]), "rows with iloc")
5 rows with loc
4 rows with iloc
gap.loc[0:2, "country":"year"]
country year
0 Afghanistan 1952
1 Afghanistan 1957
2 Afghanistan 1962
gap.iloc[-3:, [0, 2, 4]]
country pop lifeExp
1701 Zimbabwe 11404948.0 46.809
1702 Zimbabwe 11926563.0 39.989
1703 Zimbabwe 12311143.0 43.487
first_years = gap.iloc[::12]
print(len(first_years), "rows")
first_years[["country", "year", "lifeExp"]].head(3)
142 rows
country year lifeExp
0 Afghanistan 1952 28.801
12 Albania 1952 55.230
24 Algeria 1952 43.077
recent = gap.loc[gap["year"] >= 2002, "country":"pop"]
print(recent.shape)
recent.head(3)
(284, 3)
country year pop
10 Afghanistan 2002 25268405.0
11 Afghanistan 2007 31889923.0
22 Albania 2002 3508512.0
print(gap.at[0, "lifeExp"])
print(gap.iat[0, 4])
28.801
28.801

Common mistake: giving iloc a column name.

gap.iloc[0:3, "country"]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:993, in _LocationIndexer._validate_tuple_indexer(self, key)
    992 try:
--> 993     self._validate_key(k, i)
    994 except ValueError as err:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1635, in _iLocIndexer._validate_key(self, key, axis)
   1634 else:
-> 1635     raise ValueError(f"Can only index by location with a [{self._valid_types}]")

ValueError: Can only index by location with a [integer, integer slice (START point is INCLUDED, END point is EXCLUDED), listlike of integers, boolean array]

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

ValueError                                Traceback (most recent call last)
Cell In[9], line 1
----> 1 gap.iloc[0:3, "country"]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1200, in _LocationIndexer.__getitem__(self, key)
   1198     if self._is_scalar_access(key):
   1199         return self.obj._get_value(*key, takeable=self._takeable)
-> 1200     return self._getitem_tuple(key)
   1201 else:
   1202     # we by definition only have the 0th axis
   1203     axis = self.axis or 0

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1711, in _iLocIndexer._getitem_tuple(self, tup)
   1710 def _getitem_tuple(self, tup: tuple):
-> 1711     tup = self._validate_tuple_indexer(tup)
   1712     with suppress(IndexingError):
   1713         return self._getitem_lowerdim(tup)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:995, in _LocationIndexer._validate_tuple_indexer(self, key)
    993         self._validate_key(k, i)
    994     except ValueError as err:
--> 995         raise ValueError(
    996             f"Location based indexing can only have [{self._valid_types}] types"
    997         ) from err
    998 return key

ValueError: Location based indexing can only have [integer, integer slice (START point is INCLUDED, END point is EXCLUDED), listlike of integers, boolean array] types
col = gap.columns.get_loc("country")
print(gap.iloc[0:3, col].tolist())
print(gap.loc[0:2, "country"].tolist())
['Afghanistan', 'Afghanistan', 'Afghanistan']
['Afghanistan', 'Afghanistan', 'Afghanistan']

Your turn: with iloc, show the last 2 rows and the first 3 columns.

gap.iloc[-2:, :3]
country year pop
1702 Zimbabwe 2002 11926563.0
1703 Zimbabwe 2007 12311143.0

Build a MultiIndex and slice it#

Two keys, country and year, become a two-level row index.

m = gap.set_index(["country", "year"]).sort_index()
print(m.index.nlevels, "levels")
m.head(3)
2 levels
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
m.loc["Canada"].tail(3)
pop continent lifeExp gdpPercap
year
1997 30305843.0 Americas 78.610 28954.92589
2002 31902268.0 Americas 79.770 33328.96507
2007 33390141.0 Americas 80.653 36319.23501
block = m.loc["Canada":"Chile"]
block.index.get_level_values("country").unique().tolist()
['Canada', 'Central African Republic', 'Chad', 'Chile']
m.loc[("Canada", 2007)]
pop           33390141.0
continent       Americas
lifeExp           80.653
gdpPercap    36319.23501
Name: (Canada, 2007), dtype: object
years = slice(1977, 2002)
m.loc[("Canada", years), ["lifeExp", "gdpPercap"]]
lifeExp gdpPercap
country year
Canada 1977 74.21 22090.88306
1982 75.76 22898.79214
1987 76.86 26626.51503
1992 77.95 26342.88426
1997 78.61 28954.92589
2002 79.77 33328.96507

Common mistake: slicing a MultiIndex that isn't sorted.

by_year = gap.set_index(["year", "country"])
by_year.loc[(slice(1990, 2000), "Canada"), :]
---------------------------------------------------------------------------
UnsortedIndexError                        Traceback (most recent call last)
Cell In[17], line 2
      1 by_year = gap.set_index(["year", "country"])
----> 2 by_year.loc[(slice(1990, 2000), "Canada"), :]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1200, in _LocationIndexer.__getitem__(self, key)
   1198     if self._is_scalar_access(key):
   1199         return self.obj._get_value(*key, takeable=self._takeable)
-> 1200     return self._getitem_tuple(key)
   1201 else:
   1202     # we by definition only have the 0th axis
   1203     axis = self.axis or 0

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1386, in _LocIndexer._getitem_tuple(self, tup)
   1384 with suppress(IndexingError):
   1385     tup = self._expand_ellipsis(tup)
-> 1386     return self._getitem_lowerdim(tup)
   1388 # no multi-index, so validate all of the indexers
   1389 tup = self._validate_tuple_indexer(tup)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1067, in _LocationIndexer._getitem_lowerdim(self, tup)
   1065 # we may have a nested tuples indexer here
   1066 if self._is_nested_tuple_indexer(tup):
-> 1067     return self._getitem_nested_tuple(tup)
   1069 # we maybe be using a tuple to represent multiple dimensions here
   1070 ax0 = self.obj._get_axis(0)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1172, in _LocationIndexer._getitem_nested_tuple(self, tup)
   1169     axis -= 1
   1170     continue
-> 1172 obj = getattr(obj, self.name)._getitem_axis(key, axis=axis)
   1173 axis -= 1
   1175 # if we have a scalar, we are done

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1442, in _LocIndexer._getitem_axis(self, key, axis)
   1440 # nested tuple slicing
   1441 if is_nested_tuple(key, labels):
-> 1442     locs = labels.get_locs(key)
   1443     indexer: list[slice | npt.NDArray[np.intp]] = [slice(None)] * self.ndim
   1444     indexer[axis] = locs

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3953, in MultiIndex.get_locs(self, seq)
   3951 true_slices = [i for (i, s) in enumerate(com.is_true_slices(seq)) if s]
   3952 if true_slices and true_slices[-1] >= self._lexsort_depth:
-> 3953     raise UnsortedIndexError(
   3954         "MultiIndex slicing requires the index to be lexsorted: slicing "
   3955         f"on levels {true_slices}, lexsort depth {self._lexsort_depth}"
   3956     )
   3958 if any(x is Ellipsis for x in seq):
   3959     raise NotImplementedError(
   3960         "MultiIndex does not support indexing with Ellipsis"
   3961     )

UnsortedIndexError: 'MultiIndex slicing requires the index to be lexsorted: slicing on levels [0], lexsort depth 0'
by_year = by_year.sort_index()
by_year.loc[(slice(1990, 2000), "Canada"), "lifeExp"]
year  country
1992  Canada     77.95
1997  Canada     78.61
Name: lifeExp, dtype: float64

Your turn: get Mexico's population from 1987 to 2007.

m.loc[("Mexico", slice(1987, 2007)), "pop"]
country  year
Mexico   1987     80122492.0
         1992     88111030.0
         1997     95895146.0
         2002    102479927.0
         2007    108700891.0
Name: pop, dtype: float64

Cross-sections with xs#

xs grabs every row that matches one value at any level.

Common mistake: calling xs without saying which level.

m.xs(2007)
---------------------------------------------------------------------------
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:176, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:583, in pandas._libs.index.StringObjectEngine._check_type()

KeyError: 2007

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

KeyError                                  Traceback (most recent call last)
Cell In[20], line 1
----> 1 m.xs(2007)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:4233, in NDFrame.xs(self, key, axis, level, drop_level)
   4230     index = self.index
   4232 if isinstance(index, MultiIndex):
-> 4233     loc, new_index = index._get_loc_level(key, level=0)
   4234     if not drop_level:
   4235         if lib.is_integer(loc):
   4236             # Slice index must be an integer or None

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3784, in MultiIndex._get_loc_level(self, key, level)
   3782         return indexer, maybe_mi_droplevels(indexer, ilevels)
   3783 else:
-> 3784     indexer = self._get_level_indexer(key, level=level)
   3785     if (
   3786         isinstance(key, str)
   3787         and self.levels[level]._supports_partial_string_indexing
   3788     ):
   3789         # check to see if we did an exact lookup vs sliced
   3790         check = self.levels[level].get_loc(key)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3885, in MultiIndex._get_level_indexer(self, key, level, indexer)
   3882         return slice(i, j, step)
   3884 else:
-> 3885     idx = self._get_loc_single_level_index(level_index, key)
   3887     if level > 0 or self._lexsort_depth == 0:
   3888         # Desired level is not sorted
   3889         if isinstance(idx, slice):
   3890             # test_get_loc_partial_timestamp_multiindex

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3458, in MultiIndex._get_loc_single_level_index(self, level_index, key)
   3456     return -1
   3457 else:
-> 3458     return level_index.get_loc(key)

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: 2007
in_2007 = m.xs(2007, level="year")
print(in_2007.shape)
in_2007.head(3)
(142, 4)
pop continent lifeExp gdpPercap
country
Afghanistan 31889923.0 Asia 43.828 974.580338
Albania 3600523.0 Europe 76.423 5937.029526
Algeria 33333216.0 Africa 72.301 6223.367465
in_2007.groupby("continent")["lifeExp"].median().round(1)
continent
Africa      52.9
Americas    72.9
Asia        72.4
Europe      78.6
Oceania     80.7
Name: lifeExp, dtype: float64
keep = m.xs(2007, level="year", drop_level=False)
keep.index[:2].tolist()
[('Afghanistan', 2007), ('Albania', 2007)]

Quick check: is m.xs("Japan", level="country") the same as m.loc["Japan"]?

via_xs = m.xs("Japan", level="country")
print(via_xs.equals(m.loc["Japan"]))
True

Your turn: take the 1952 cross-section. Which 3 countries had the lowest life expectancy?

in_1952 = m.xs(1952, level="year")
in_1952.nsmallest(3, "lifeExp")[["continent", "lifeExp"]]
continent lifeExp
country
Afghanistan Asia 28.801
Gambia Africa 30.000
Angola Africa 30.015

Precise selections with IndexSlice#

pd.IndexSlice writes multi-level slices that read like a sentence.

idx = pd.IndexSlice
pair = m.loc[idx[["Canada", "Mexico"], 1997:2007], "lifeExp"]
pair.unstack("country")
country Canada Mexico
year
1997 78.610 73.670
2002 79.770 74.902
2007 80.653 76.195
every_2007 = m.loc[idx[:, 2007], ["lifeExp", "pop"]]
print(every_2007.shape)
(142, 2)
m3 = gap.set_index(["continent", "country", "year"]).sort_index()
eu = m3.xs(("Europe", 2007), level=["continent", "year"])
print(len(eu), "European countries")
eu.nlargest(3, "gdpPercap")[["gdpPercap"]].round(0)
30 European countries
gdpPercap
country
Norway 49357.0
Ireland 40676.0
Switzerland 37506.0

Quick check: how would you get every Asian country in 1952 from m3?

asia_52 = m3.loc[idx["Asia", :, 1952], "lifeExp"]
print(len(asia_52), "countries")
print(asia_52.round(1).sort_values().tail(3))
33 countries
continent  country           year
Asia       Hong Kong, China  1952    61.0
           Japan             1952    63.0
           Israel            1952    65.4
Name: lifeExp, dtype: float64
m.query("year == 2007 and lifeExp > 81")[["lifeExp"]]
lifeExp
country year
Australia 2007 81.235
Hong Kong, China 2007 82.208
Iceland 2007 81.757
Japan 2007 82.603
Switzerland 2007 81.701
yr_first = m.swaplevel().sort_index()
yr_first.loc[2007].head(3)
pop continent lifeExp gdpPercap
country
Afghanistan 31889923.0 Asia 43.828 974.580338
Albania 3600523.0 Europe 76.423 5937.029526
Algeria 33333216.0 Africa 72.301 6223.367465

Slice wide tables, then flatten#

When years become columns, slice them like any other labels.

wide = m["lifeExp"].unstack("year")
print(wide.shape)
wide.loc["Canada":"Chile", 1987:2007]
(142, 12)
year 1987 1992 1997 2002 2007
country
Canada 76.860 77.950 78.610 79.770 80.653
Central African Republic 50.485 49.396 46.066 43.308 44.741
Chad 51.051 51.724 51.573 50.525 50.651
Chile 72.492 74.126 75.816 77.860 78.553

Common mistake: typing a year column as text.

wide.loc["Canada", "2007"]
---------------------------------------------------------------------------
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:176, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index_class_helper.pxi:70, in pandas._libs.index.Int64Engine._check_type()

KeyError: '2007'

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

KeyError                                  Traceback (most recent call last)
Cell In[33], line 1
----> 1 wide.loc["Canada", "2007"]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexing.py:1199, in _LocationIndexer.__getitem__(self, key)
   1197     key = tuple(com.apply_if_callable(x, self.obj) for x in key)
   1198     if self._is_scalar_access(key):
-> 1199         return self.obj._get_value(*key, takeable=self._takeable)
   1200     return self._getitem_tuple(key)
   1201 else:
   1202     # we by definition only have the 0th axis

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4489, in DataFrame._get_value(self, index, col, takeable)
   4486     series = self._ixs(col, axis=1)
   4487     return series._values[index]
-> 4489 series = self._get_item(col)
   4491 if not isinstance(self.index, MultiIndex):
   4492     # CategoricalIndex: Trying to use the engine fastpath may give incorrect
   4493     #  results if our categories are integers that dont match our codes
   4494     # IntervalIndex: IntervalTree has no get_loc
   4495     row = self.index.get_loc(index)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4969, in DataFrame._get_item(self, item)
   4968 def _get_item(self, item: Hashable) -> Series:
-> 4969     loc = self.columns.get_loc(item)
   4970     return self._ixs(loc, axis=1)

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: '2007'
print(wide.columns.dtype)
print(wide.loc["Canada", 2007])
int64
80.653

Your turn: make a wide table of gdpPercap and slice Japan and South Korea, 1997-2007.

gdp = m["gdpPercap"].unstack("year")
gdp.loc[["Japan", "Korea, Rep."], 1997:2007].round(0)
year 1997 2002 2007
country
Japan 28817.0 28605.0 31656.0
Korea, Rep. 15994.0 19234.0 23348.0
both = m[["lifeExp", "gdpPercap"]].unstack("year")
print(both.columns.nlevels, "column levels")
both.xs(2007, axis=1, level="year").head(3).round(1)
2 column levels
lifeExp gdpPercap
country
Afghanistan 43.8 974.6
Albania 76.4 5937.0
Algeria 72.3 6223.4
flat = in_2007.reset_index()
print(flat.columns.tolist())
flat.head(2)
['country', 'pop', 'continent', 'lifeExp', 'gdpPercap']
country pop continent lifeExp gdpPercap
0 Afghanistan 31889923.0 Asia 43.828 974.580338
1 Albania 3600523.0 Europe 76.423 5937.029526
import os
os.makedirs("output/pandas", exist_ok=True)
flat.to_csv("output/pandas/l14_gap2007.csv", index=False)
back = pd.read_csv("output/pandas/l14_gap2007.csv")
print(back.shape)
(142, 5)

Mini-project: which continent closed the gap?#

Compare 1952 with 2007 using cross-sections. Who gained the most years of life?

start = m.xs(1952, level="year")
end = m.xs(2007, level="year")
gain = (end["lifeExp"] - start["lifeExp"]).rename("gain")
gain.sort_values(ascending=False).head(3).round(1)
country
Oman         38.1
Vietnam      33.8
Indonesia    33.2
Name: gain, dtype: float64
by_cont = gain.groupby(end["continent"])
by_cont.agg(["mean", "min", "max"]).round(1)
mean min max
continent
Africa 15.7 -5.0 31.2
Americas 20.3 9.1 30.6
Asia 24.4 14.2 38.1
Europe 13.2 7.5 28.2
Oceania 11.5 10.8 12.1
backwards = gain[gain < 0].sort_values().round(1)
print(len(backwards), "countries went backwards")
backwards
2 countries went backwards
country
Zimbabwe    -5.0
Swaziland   -1.8
Name: gain, dtype: float64
south = ["Botswana", "Lesotho", "Swaziland", "Zimbabwe"]
peak = m.loc[idx[south, 1987:2007], "lifeExp"].unstack("year")
peak[[1987, 1992, 2002, 2007]].round(1)
year 1987 1992 2002 2007
country
Botswana 63.6 62.7 46.6 50.7
Lesotho 57.2 59.7 44.6 42.6
Swaziland 57.7 58.5 43.9 39.6
Zimbabwe 62.4 60.4 40.0 43.5
med = pd.DataFrame({
    "1952": start.groupby("continent")["lifeExp"].median(),
    "2007": end.groupby("continent")["lifeExp"].median()})
med.round(1)
1952 2007
continent
Africa 38.8 52.9
Americas 54.7 72.9
Asia 44.9 72.4
Europe 65.9 78.6
Oceania 69.3 80.7
africa = m3.loc[idx["Africa", :, 1987:2007], "lifeExp"]
trend = africa.groupby(level="year").mean()
trend.round(1)
year
1987    53.3
1992    53.6
1997    53.6
2002    53.3
2007    54.8
Name: lifeExp, dtype: float64
import matplotlib.pyplot as plt

four = ["China", "India", "Zimbabwe", "Botswana"]
paths = m.loc[idx[four, :], "lifeExp"].unstack("country")
ax = paths.plot(figsize=(8, 4), marker="o")
ax.set_title("Life expectancy, 1952-2007")
ax.set_xlabel("Year")
ax.set_ylabel("Life expectancy (years)")
plt.show()
No description has been provided for this image

Finding: Asia gained the most years; Africa's gains stalled after 1987 as southern Africa fell back.

Recap#

  • loc slices include the end label; iloc slices exclude the end
  • set_index([...]).sort_index() builds a sliceable MultiIndex
  • xs(value, level=...) takes a cross-section at any level
  • pd.IndexSlice writes clear multi-level slices with colons
  • unstack puts a level into columns; reset_index flattens again

Challenges#

  1. With m3, get every African country in 2007, sorted by lifeExp.
  2. Take the 1977 cross-section and find the richest country per continent.
  3. Make a wide table of pop and slice India and China, 1952-1977.

What's next#

Lesson 15: Mini Project - Analysing Titanic Dataset Basics. Selecting, sorting and slicing, all in one real analysis.

Found this useful?

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