Mathew K Analytics

Lesson 13 · Pandas Full Course

Sort and Rank Data in Pandas: sort_values, nlargest and rank | Pandas Course #13

In this lesson you'll be able to: Sort rows by one or several columns, in any direction Control where missing values land, and sort by the index 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 13: Sorting and Ranking Data#

In this lesson you'll be able to:

  • Sort rows by one or several columns, in any direction
  • Control where missing values land, and sort by the index
  • Put categories in a logical order instead of A to Z
  • Pull top-N lists and rank values, overall and within groups Data: the Titanic passenger list and Gapminder country statistics

Warm-up: tidy names from last lesson#

In lesson 12 we renamed columns. A quick rename gets us started.

import pandas as pd

df = pd.read_csv("data/pandas/titanic.csv")
df = df.rename(columns={"Pclass": "Class", "SibSp": "Siblings"})
df[["Name", "Class", "Siblings", "Fare"]].head(3)
Name Class Siblings Fare
0 Braund, Mr. Owen Harris 3 1 7.2500
1 Cumings, Mrs. John Bradley (Florence Briggs Th... 1 1 71.2833
2 Heikkinen, Miss. Laina 3 0 7.9250

Sort by one column#

Who paid the least, and who paid the most? Sorting answers that instantly.

cheapest = df.sort_values("Fare")
cheapest[["Name", "Class", "Fare"]].head()
Name Class Fare
815 Fry, Mr. Richard 1 0.0
806 Andrews, Mr. Thomas Jr 1 0.0
413 Cunningham, Mr. Alfred Fleming 2 0.0
481 Frost, Mr. Anthony Wood "Archie" 2 0.0
302 Johnson, Mr. William Cahoone Jr 3 0.0
priciest = df.sort_values("Fare", ascending=False)
priciest[["Name", "Class", "Fare"]].head()
Name Class Fare
679 Cardeza, Mr. Thomas Drake Martinez 1 512.3292
258 Ward, Miss. Anna 1 512.3292
737 Lesurer, Mr. Gustave J 1 512.3292
88 Fortune, Miss. Mabel Helen 1 263.0000
438 Fortune, Mr. Mark 1 263.0000

Quick check: after those two sorts, what order is df in now?

print(df["PassengerId"].head(3).tolist())
print(priciest["PassengerId"].head(3).tolist())
[1, 2, 3]
[680, 259, 738]

Common mistake: a typo in the column name.

df.sort_values("fare")
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_15448\2525601923.py in ?()
----> 1 df.sort_values("fare")

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py in ?(self, by, axis, ascending, inplace, kind, na_position, ignore_index, key)
   8357             )
   8358         elif by:
   8359             # len(by) == 1
   8360 
-> 8361             k = self._get_label_or_level_values(by[0], axis=axis)
   8362 
   8363             # need to rewrap column in Series to apply key function
   8364             if key is not None:

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py in ?(self, key, axis)
   1760             values = self.xs(key, axis=first_other_axes)._values
   1761         elif self._is_level_reference(key, axis=axis):
   1762             values = self.axes[axis].get_level_values(key)._values
   1763         else:
-> 1764             raise KeyError(key)
   1765 
   1766         # Check for duplicates
   1767         if values.ndim > 1:

KeyError: 'fare'
print([c for c in df.columns if "fare" in c.lower()])
df.sort_values("Fare").shape
['Fare']
(891, 12)

Common mistake: saving the result of inplace=True.

by_fare = df.copy()
result = by_fare.sort_values("Fare", inplace=True)
result.head()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[8], line 3
      1 by_fare = df.copy()
      2 result = by_fare.sort_values("Fare", inplace=True)
----> 3 result.head()

AttributeError: 'NoneType' object has no attribute 'head'
by_fare = df.sort_values("Fare", ascending=False, ignore_index=True)
by_fare[["Name", "Fare"]].head(3)
Name Fare
0 Cardeza, Mr. Thomas Drake Martinez 512.3292
1 Ward, Miss. Anna 512.3292
2 Lesurer, Mr. Gustave J 512.3292

Sort by several columns#

Break ties with a second column, and choose a direction for each.

by_class_age = df.sort_values(["Class", "Age"])
by_class_age[["Name", "Class", "Age"]].head()
Name Class Age
305 Allison, Master. Hudson Trevor 1 0.92
297 Allison, Miss. Helen Loraine 1 2.00
445 Dodge, Master. Washington 1 4.00
802 Carter, Master. William Thornton II 1 11.00
435 Carter, Miss. Lucile Polk 1 14.00
mixed = df.sort_values(["Class", "Fare"], ascending=[True, False])
mixed[["Name", "Class", "Fare"]].head()
Name Class Fare
258 Ward, Miss. Anna 1 512.3292
679 Cardeza, Mr. Thomas Drake Martinez 1 512.3292
737 Lesurer, Mr. Gustave J 1 512.3292
27 Fortune, Mr. Charles Alexander 1 263.0000
88 Fortune, Miss. Mabel Helen 1 263.0000

Common mistake: giving ascending fewer values than by.

df.sort_values(["Class", "Fare"], ascending=[True])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_15448\3292645834.py in ?()
----> 1 df.sort_values(["Class", "Fare"], ascending=[True])

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py in ?(self, by, axis, ascending, inplace, kind, na_position, ignore_index, key)
   8333             len(by) != len(ascending)  # type: ignore[arg-type]
   8334         ):
   8335             # error: Argument 1 to "len" has incompatible type "Union[bool,
   8336             # List[bool]]"; expected "Sized"
-> 8337             raise ValueError(
   8338                 f"Length of ascending ({len(ascending)})"  # type: ignore[arg-type]
   8339                 f" != length of by ({len(by)})"
   8340             )

ValueError: Length of ascending (1) != length of by (2)
fixed = df.sort_values(["Class", "Fare"], ascending=[True, False])
fixed[["Class", "Fare"]].head(3)
Class Fare
258 1 512.3292
679 1 512.3292
737 1 512.3292

Your turn: list the oldest survivors first, showing name and age.

oldest_survivors = df.sort_values(["Survived", "Age"],
                                  ascending=[False, False])
oldest_survivors[["Name", "Survived", "Age"]].head()
Name Survived Age
630 Barkworth, Mr. Algernon Henry Wilson 1 80.0
275 Andrews, Miss. Kornelia Theodosia 1 63.0
483 Turkula, Mrs. (Hedwig) 1 63.0
570 Harris, Mr. George 1 62.0
829 Stone, Mrs. George Nelson (Martha Evelyn) 1 62.0

Missing values, the index and custom keys#

Choose where gaps go, restore the original order, and sort by a rule.

by_age = df.sort_values("Age")
by_age[["Name", "Age"]].tail(3)
Name Age
868 van Melkebeke, Mr. Philemon NaN
878 Laleff, Mr. Kristo NaN
888 Johnston, Miss. Catherine Helen "Carrie" NaN
gaps_first = df.sort_values("Age", na_position="first")
gaps_first[["Name", "Age"]].head(3)
Name Age
5 Moran, Mr. James NaN
17 Williams, Mr. Charles Eugene NaN
19 Masselmani, Mrs. Fatima NaN
restored = by_age.sort_index()
restored[["PassengerId", "Name"]].head(3)
PassengerId Name
0 1 Braund, Mr. Owen Harris
1 2 Cumings, Mrs. John Bradley (Florence Briggs Th...
2 3 Heikkinen, Miss. Laina
df["Embarked"].value_counts().sort_index()
Embarked
C    168
Q     77
S    644
Name: count, dtype: int64
df.sort_index(axis=1).columns[:6].tolist()
['Age', 'Cabin', 'Class', 'Embarked', 'Fare', 'Name']
by_name_len = df.sort_values("Name", key=lambda s: s.str.len())
short = by_name_len["Name"].head(3)
pd.DataFrame({"name": short, "chars": short.str.len()})
name chars
692 Lam, Mr. Ali 12
826 Lam, Mr. Len 12
74 Bing, Mr. Lee 13

Quick check: which passenger has the longest name on board?

longest = by_name_len["Name"].tail(1).item()
print(longest)
print(len(longest), "characters")
Penasco y Castellana, Mrs. Victor de Satode (Maria Josefa Perez de Soto y Vallejo)
82 characters

Sort categories in a logical order#

The Titanic called at Southampton, Cherbourg, then Queenstown. A to Z gets that wrong.

route = ["S", "C", "Q"]
port_type = pd.CategoricalDtype(categories=route, ordered=True)
df["Port"] = df["Embarked"].astype(port_type)
df["Port"].dtype
CategoricalDtype(categories=['S', 'C', 'Q'], ordered=True, categories_dtype=str)
df.groupby("Port", observed=True)["Fare"].mean().round(2)
Port
S    27.08
C    59.95
Q    13.28
Name: Fare, dtype: float64
by_port = df.sort_values(["Port", "Fare"], ascending=[True, False])
by_port[["Name", "Port", "Fare"]].head(3)
Name Port Fare
27 Fortune, Mr. Charles Alexander S 263.0
88 Fortune, Miss. Mabel Helen S 263.0
341 Fortune, Miss. Alice Elizabeth S 263.0

Your turn: sort the classes as words, Upper, Middle, Lower, using a key.

names = {1: "Upper", 2: "Middle", 3: "Lower"}
order = {"Upper": 0, "Middle": 1, "Lower": 2}
df["Deck"] = df["Class"].map(names)
by_deck = df.sort_values("Deck", key=lambda s: s.map(order))
by_deck["Deck"].drop_duplicates().tolist()
['Upper', 'Middle', 'Lower']

Top-N lists with nlargest and nsmallest#

When you only need the top few rows, skip the full sort.

df.nlargest(5, "Age")[["Name", "Class", "Age"]]
Name Class Age
630 Barkworth, Mr. Algernon Henry Wilson 1 80.0
851 Svensson, Mr. Johan 3 74.0
96 Goldschmidt, Mr. George B 1 71.0
493 Artagaveytia, Mr. Ramon 1 71.0
116 Connors, Mr. Patrick 3 70.5
df.nsmallest(3, "Fare")[["Name", "Fare"]]
Name Fare
179 Leonard, Mr. Lionel 0.0
263 Harrison, Mr. William 0.0
271 Tornquist, Mr. William Henry 0.0
print((df["Fare"] == 0).sum(), "passengers paid 0")
free = df.nsmallest(3, "Fare", keep="all")
print(len(free), "rows with keep=all")
15 passengers paid 0
15 rows with keep=all

Your turn: which 3 passengers paid the most within third class?

third = df[df["Class"] == 3]
third.nlargest(3, "Fare")[["Name", "Siblings", "Fare"]]
Name Siblings Fare
159 Sage, Master. Thomas Henry 8 69.55
180 Sage, Miss. Constance Gladys 8 69.55
201 Sage, Mr. Frederick 8 69.55

Rank values and handle ties#

Ranking gives every row a position number. Ties need a rule.

ages = df["Age"].head(5)
pd.DataFrame({"age": ages,
              "average": ages.rank(ascending=False),
              "min": ages.rank(ascending=False, method="min"),
              "dense": ages.rank(ascending=False, method="dense"),
              "first": ages.rank(ascending=False, method="first")})
age average min dense first
0 22.0 5.0 5.0 4.0 5.0
1 38.0 1.0 1.0 1.0 1.0
2 26.0 4.0 4.0 3.0 4.0
3 35.0 2.5 2.0 2.0 2.0
4 35.0 2.5 2.0 2.0 3.0
df["Fare_rank"] = df["Fare"].rank(ascending=False, method="min")
cols = ["Name", "Fare", "Fare_rank"]
df.sort_values("Fare_rank")[cols].head()
Name Fare Fare_rank
679 Cardeza, Mr. Thomas Drake Martinez 512.3292 1.0
737 Lesurer, Mr. Gustave J 512.3292 1.0
258 Ward, Miss. Anna 512.3292 1.0
341 Fortune, Miss. Alice Elizabeth 263.0000 4.0
88 Fortune, Miss. Mabel Helen 263.0000 4.0
dense = df["Fare"].rank(ascending=False, method="dense")
print(dense.max(), "dense rank levels")
print(df["Fare"].nunique(), "different fares")
248.0 dense rank levels
248 different fares
df["Fare_pct"] = df["Fare"].rank(pct=True).round(2)
df[["Name", "Fare", "Fare_pct"]].head()
Name Fare Fare_pct
0 Braund, Mr. Owen Harris 7.2500 0.09
1 Cumings, Mrs. John Bradley (Florence Briggs Th... 71.2833 0.89
2 Heikkinen, Miss. Laina 7.9250 0.26
3 Futrelle, Mrs. Jacques Heath (Lily May Peel) 53.1000 0.84
4 Allen, Mr. William Henry 8.0500 0.30
grp = df.groupby("Class")["Fare"]
df["Class_rank"] = grp.rank(ascending=False, method="min")
top1 = df[df["Class_rank"] == 1]
print(len(top1), "passengers share a rank of 1")
top1.drop_duplicates("Class")[["Name", "Class", "Fare"]]
15 passengers share a rank of 1
Name Class Fare
72 Hood, Mr. Ambrose Jr 2 73.5000
159 Sage, Master. Thomas Henry 3 69.5500
258 Ward, Miss. Anna 1 512.3292

Your turn: rank ages within each sex. Who is the oldest woman?

age_rank = df.groupby("Sex")["Age"].rank(ascending=False,
                                         method="min")
women = df[(df["Sex"] == "female") & (age_rank == 1)]
women[["Name", "Class", "Age"]]
Name Class Age
275 Andrews, Miss. Kornelia Theodosia 1 63.0
483 Turkula, Mrs. (Hedwig) 3 63.0

Quick check: is rank 1 in third class a cheap or an expensive ticket overall?

top1.drop_duplicates("Class")[["Class", "Fare", "Fare_pct"]]
Class Fare Fare_pct
72 2 73.5000 0.89
159 3 69.5500 0.88
258 1 512.3292 1.00

Mini-project: who climbed the life expectancy table?#

Rank 142 countries by life expectancy in 1952 and 2007. Who rose furthest?

gap = pd.read_csv("data/pandas/gapminder.csv")
wide = gap.pivot(index="country", columns="year", values="lifeExp")
life = wide[[1952, 2007]]
life.sort_values(2007, ascending=False).head(3).round(1)
year 1952 2007
country
Japan 63.0 82.6
Hong Kong, China 61.0 82.2
Iceland 72.5 81.8
ranks = life.rank(ascending=False, method="min").astype(int)
ranks["climb"] = ranks[1952] - ranks[2007]
ranks.sort_values(2007).head(3)
year 1952 2007 climb
country
Japan 28 1 27
Hong Kong, China 34 2 32
Iceland 2 3 -1
ranks.nlargest(5, "climb")
year 1952 2007 climb
country
Oman 115 40 75
Vietnam 101 49 52
Korea, Rep. 66 25 41
Saudi Arabia 104 63 41
Gambia 141 102 39
ranks.nsmallest(5, "climb")
year 1952 2007 climb
country
Zimbabwe 62 136 -74
Botswana 65 121 -56
South Africa 72 125 -53
Lesotho 88 138 -50
Zambia 90 140 -50
top = ranks.nlargest(10, "climb")
cont = gap.drop_duplicates("country").set_index("country")
cont.loc[top.index, "continent"].value_counts()
continent
Asia      8
Africa    2
Name: count, dtype: int64
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 4))
top["climb"].sort_values().plot.barh(ax=ax, color="seagreen")
ax.set_title("Biggest climbs in life expectancy rank, 1952-2007")
ax.set_xlabel("Places gained")
ax.set_ylabel("Country")
plt.show()
No description has been provided for this image
latest = gap[gap["year"] == 2007].copy()
by_cont = latest.groupby("continent")["lifeExp"]
latest["cont_rank"] = by_cont.rank(ascending=False, method="min")
leaders = latest[latest["cont_rank"] == 1]
leaders[["continent", "country", "lifeExp"]].sort_values("lifeExp")
continent country lifeExp
1271 Africa Reunion 76.442
251 Americas Canada 80.653
71 Oceania Australia 81.235
695 Europe Iceland 81.757
803 Asia Japan 82.603

Finding: 8 of the 10 biggest climbers are in Asia; the biggest fallers are in southern Africa.

Recap#

  • sort_values returns a new, sorted copy; ascending sets the direction
  • Pass lists to by and ascending to sort on several columns
  • na_position, sort_index and key control gaps, order and rules
  • Ordered categoricals sort in a logical order, not A to Z
  • nlargest/nsmallest for top lists; rank for positions and ties

Challenges#

  1. Rank Gapminder countries by GDP per person within each continent in 2007.
  2. Sort the Titanic by Port, then Class, then Age oldest first.
  3. Which 5 countries had the largest population in 1952 and in 2007?

What's next#

Lesson 14: Advanced Slicing and Cross-Section Operations. Sorted data is ready for slicing by label ranges and index levels.

Found this useful?

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