Turn values into their positions in the ordering.
Where does this country come in the list? sort_values puts the rows in order so you can count down them, and nlargest hands you the top few. Neither gives you the number itself — 1st, 12th, 32nd — attached to the row it belongs to. That is .rank(): it replaces each value with its position in the sorted order and keeps the index intact, which makes it composable in a way that sorting is not.
The interesting part is what it does about ties, and that is entirely up to you.
Official documentation: Series.rank and DataFrame.rank
The arguments that earn their keep
s.rank(method='average', # 'average', 'min', 'max', 'first', 'dense'
ascending=True, # True means rank 1 is the SMALLEST value
na_option='keep', # 'keep', 'top', 'bottom'
pct=False) # report the rank as a fraction of the count
method is the page. ascending=False is the argument you will type most often, because "rank 1" almost always means the biggest. pct=True divides by the number of ranked values, giving a percentile between 0 and 1. And na_option decides where missing values go — by default nowhere, which is to say they get a rank of NaN.
A worked example, on real data
The Global Coal Plant Tracker lists every coal-fired generating unit on earth, the workbook behind Bamboo Weekly #64. Counting the operating units in each European country gives us plenty of ties, which is exactly what we need:
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',
'Annual CO2 (million tonnes / annum)'])
europe = (
df
.loc[(pd.col('Region') == 'Europe') & (pd.col('Status') == 'operating')]
.groupby('Country').size()
)
(
europe
.sort_values(ascending=False)
.to_frame('units')
.assign(average=pd.col('units').rank(ascending=False),
min=pd.col('units').rank(ascending=False, method='min'),
dense=pd.col('units').rank(ascending=False, method='dense'),
first=pd.col('units').rank(ascending=False, method='first'))
.iloc[6:13]
)
units average min dense first
Country
Serbia 17 7.0 7.0 7.0 7.0
Italy 12 8.5 8.0 8.0 8.0
Romania 12 8.5 8.0 8.0 9.0
France 11 10.0 10.0 9.0 10.0
Bosnia and Herzegovina 10 11.5 11.0 10.0 11.0
Spain 10 11.5 11.0 10.0 12.0
Greece 8 13.0 13.0 11.0 13.0
Serbia is unambiguously 7th. Everything below it shows the methods disagreeing, and the whole vocabulary is in those seven rows.
Italy and Romania both have 12 units, competing for positions 8 and 9. average, the default, splits the difference and gives both 8.5. min gives both the better of the two, 8. first breaks the tie by row order, so Italy takes 8 and Romania takes 9 — arbitrary, but the ranks are whole numbers and no two rows share one. There is a max as well, the mirror of min, handing both tied rows the worse position.
France is where dense separates from the pack. Under average, min and first it is 10th, because two countries sat above it at 12. Under dense it is 9th: dense ranks distinct values rather than rows, so having spent 8 on the tie it moves straight to 9 and never skips a number. The gap keeps growing down the table. Pick dense for "the ninth-largest unit count" and min for "ninth place."
Percentile rank is one more keyword:
europe.rank(ascending=False, pct=True).loc[['Russia', 'France', 'Ireland']].round(2)
Country
Russia 0.04
France 0.40
Ireland 0.92
dtype: float64
Twenty-five countries, so rank 1 scores 0.04. With ascending=False the small numbers are the good ones, the opposite of how percentiles usually read.
Three mistakes people make
Expecting integers. The default is method='average', so any tie produces a fraction — and no method gives you an integer dtype, since min, dense and first all return float64 too. In my own solution to Bamboo Weekly #64, ranking countries by coal emissions puts France at 32.5. If the halves bother you, method='min' gives whole numbers in a float column, and .astype(int) afterwards makes them integers — once you are sure no NaN ranks are left.
Getting the direction backwards. ascending=True is the default, so rank 1 goes to the smallest value. That is right for a race time and wrong for almost everything else. If your biggest number came back with the biggest rank, you wanted ascending=False.
Forgetting what happens to missing values. The default na_option='keep' gives every NaN a rank of NaN, so the ranked column has holes and the ranks that remain do not run 1 through n:
pd.Series([10.0, None, 30.0, 10.0]).rank()
0 1.5
1 NaN
2 3.0
3 1.5
dtype: float64
na_option='bottom' puts the missing values last and ranks them 4.0, 'top' puts them first. Choose deliberately, because a NaN rank propagates silently through everything downstream.
Where it shows up in Bamboo Weekly
Bamboo Weekly #64: Coal power is the classic shape: group by country, sum the annual CO2, .rank(ascending=False), then .loc[] a list of G7 countries to see where each lands among all 107. The United States comes 3rd, Germany 6th, Japan 9th — and France 32.5th.
Bamboo Weekly #60: Iceland shows the other pattern, a ranking that stays attached to its rows. Two Wikipedia tables get .assign(rank=lambda df_: df_['area'].rank(ascending=False)), and .loc['Iceland', 'rank'] then reads off where Iceland places by area and by population. Ranking inside assign keeps every other column, which is the advantage over sorting. Bamboo Weekly #136: Indian vehicles does the same to state registration totals, then asks .loc['Karnataka'].
Bamboo Weekly #181: Housing costs is the most interesting use. It ranks countries by nominal house prices, ranks them again by real prices, joins the two rankings side by side and takes .diff(axis='columns') — so the answer is how far a country moves once you adjust for inflation. Ranks are comparable across data sets in a way raw values are not, and that is what this buys you.
Practice it
Work through a rank exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/rank/
Go deeper
sort_values is what you want when the order itself is the output, and nlargest when only the top few matter; idxmax names the winner directly. pct=True puts you next door to quantile, which goes the other way, and the ranked series usually comes out of groupby.
How to sort in Pandas covers the neighborhood in a few minutes. 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_values()— when you want the rows in order rather than a column of positions.nlargest()— when only the top few matter and ranking everything is wasted work