The top five, without sorting the other million rows.
Who are the biggest customers? The worst-delayed flights? The countries with the most medals? Every one of those is the same question — give me the top few — and every one of them tempts you into the same answer: sort the column downward and take the head. nlargest is the method that does it in one step, and it turns out to be a different step, not just a shorter one.
The difference is that sorting is a means and a top-five is an end. sort_values puts every value in order and then throws almost all of that work away. nlargest walks the data once, holds on to the n biggest values it has seen, and returns them in order, largest first. Nothing else gets sorted, ever.
It is the mirror image of nsmallest, and the two share a signature. That page leans on what these methods do with missing values; this one leans on ties, on groupby, and on the question everyone asks first — is this really better than sort_values(ascending=False).head(n)?
Official documentation: Series.nlargest and DataFrame.nlargest
The arguments that earn their keep
s.nlargest(n=5, # how many rows you want back
keep='first') # 'first', 'last', or 'all' — how ties are settled
df.nlargest(n=5,
columns='Gold', # required: the column, or list of columns, to rank by
keep='first')
Three arguments, and n defaults to 5 — s.nlargest() with empty parentheses is a valid and fairly common way to write "give me the top five."
columns is the only place the data frame version differs from the series version. A series has one column of values, so there is nothing to name; a data frame has many, so you have to say which one does the ranking, and Pandas makes it required rather than optional. Pass a list and the later names break ties in the earlier ones. They do not combine into a score — that misreading gets its own section below.
keep decides what to do when the cutoff lands in the middle of a run of equal values. 'first' and 'last' take whichever tied row Pandas met earlier or later. 'all' declines to choose and returns every tied row, even though that means handing you back more rows than you asked for. It is the least-used of the three and, for a lot of questions, the only honest one.
A worked example, on real data
Here is the Olympic results file behind Bamboo Weekly #156: Winter Olympics. One row per athlete per event, going back to 1896:
import pandas as pd
url = ('https://github.com/KeithGalli/Olympics-Dataset/raw/'
'refs/heads/master/clean-data/results.csv')
results = pd.read_csv(url, usecols=['year', 'type', 'discipline',
'as', 'noc', 'medal'])
winter = results.loc[lambda df_: (df_['type'] == 'Winter')
& df_['medal'].notna()]
That leaves 7,655 winter medal results across 57 national committees. Two things about that number before we rank it. A row is an athlete, not a podium, so a hockey gold counts once per player. And the file's winter events include the Youth Olympic Games of 2012, 2016 and 2020, which I am leaving in. What follows counts medal rows in this file; it is not the medal table you would find in an almanac.
Which committees have the most? Count them, then take the top five:
medals = winter.groupby('noc').size()
medals.nlargest()
noc
CAN 800
USA 787
NOR 603
GER 562
FIN 516
dtype: int64
Canada leads, and 427 of those 800 are ice hockey — the roster effect, exactly as advertised. Now the data frame version, where columns names the ranking column and the rest of the table comes along:
table = (
winter
.pivot_table(index='noc', columns='medal', values='year', aggfunc='count')
.fillna(0).astype(int)
[['Gold', 'Silver', 'Bronze']]
.assign(total=lambda df_: df_.sum(axis='columns'))
)
table.nlargest(5, columns='total')
medal Gold Silver Bronze total
noc
CAN 360 244 196 800
USA 219 377 191 787
NOR 225 214 164 603
GER 216 214 132 562
FIN 95 156 265 516
A list of columns ranks by the first and breaks ties with the second. Finland took fifth on 516; so, it turns out, did Sweden, and Sweden has 159 golds to Finland's 95:
table.nlargest(5, columns=['total', 'Gold'])
medal Gold Silver Bronze total
noc
CAN 360 244 196 800
USA 219 377 191 787
NOR 225 214 164 603
GER 216 214 132 562
SWE 159 164 193 516
Same five slots, a different country in the last one. Hold on to that, because it is the subject of the first mistake below.
nlargest, or sort_values and head?
They usually give the same answer. medals.sort_values(ascending=False).head(5) returns exactly the frame above. Three things separate them.
The first is ties. sort_values defaults to kind='quicksort', which is not stable, so rows with equal values come back in whatever order the algorithm produced. Germany and Norway both won 214 silvers, and the two methods disagree about which comes first:
table.nlargest(5, columns='Silver').index
table.sort_values('Silver', ascending=False).head(5).index
Index(['USA', 'CAN', 'GER', 'NOR', 'SWE'], dtype='str', name='noc')
Index(['USA', 'CAN', 'NOR', 'GER', 'SWE'], dtype='str', name='noc')
nlargest breaks ties by original position, which is what sort_values(..., kind='stable') does; ask for that and the two agree exactly.
The second is speed, and it is not the simple story I expected. nlargest carries a fixed startup cost of roughly 130 µs, so on a small column it loses badly. It wins, and then keeps winning, once the column is large:
| rows | nlargest(5) |
sort_values(ascending=False).head(5) |
|---|---|---|
| 100 | 129 µs | 27 µs |
| 3,000 | 133 µs | 110 µs |
| 10,000 | 149 µs | 427 µs |
| 100,000 | 0.49 ms | 5.3 ms |
| 1,000,000 | 4.9 ms | 67 ms |
| 10,000,000 | 57 ms | 936 ms |
The crossover sits somewhere near 5,000 rows on my machine. Below it you are paying for a guarantee you do not need; above it a full sort is ordering millions of values to show you five, and by ten million rows that costs sixteen times as much.
Why it is faster: it never sorts
That gap is worth understanding, because it is not a matter of one implementation being better tuned than the other. The two methods are doing different amounts of work.
Sorting a million values puts all million in order. But you asked for five, and the order of the other 999,995 is something you paid for and then discarded.
nlargest does not sort. It partitions, using an algorithm called quickselect: choose a pivot, push everything larger to one side, and then recurse only into the side that still contains your top five. The other side is abandoned unexamined, which is the whole trick — every step throws away work that a sort would have had to do. When it finishes, the top five values are sitting at the front in no particular order, and only then does Pandas sort those five. You can see it in the source: nlargest calls libalgos.kth_smallest to find the cutoff value, takes everything at or beyond it, and sorts that handful.
So the cost is one pass to partition plus a trivial sort of five items, rather than ordering the entire column. That also explains the fixed 130 µs: the setup, the copy that kth_smallest needs, and the index bookkeeping cost the same whether the column holds thirty values or thirty million.
The part that changed in Pandas 3
There is a second effect, and it is larger than the first on the kind of data we work with here. Finding the rows is only half the job — Pandas then has to build the result, and sort_values reorders every column of the whole data frame before .head() discards all but five rows. nlargest moves five rows. The wider the frame, and the more text it carries, the more that asymmetry costs.
Here is the same 100,000-row frame with a growing number of text columns, timed on Pandas 2.2.3 and on Pandas 3.0.5:
| text columns | Pandas 2: nlargest |
Pandas 3: nlargest |
Pandas 3 speedup |
|---|---|---|---|
| 0 | 0.79 ms | 0.70 ms | 1.1x |
| 1 | 1.10 ms | 0.74 ms | 1.5x |
| 4 | 2.37 ms | 0.82 ms | 2.9x |
| 8 | 4.12 ms | 0.85 ms | 4.8x |
On numbers alone, almost nothing changed. On a frame carrying eight text columns, nlargest got nearly five times faster. Neither method is sorting those text columns — the sort key is a number in both cases. What changed is what it costs to move a row of text.
In Pandas 2 a text column was object dtype: an array of pointers, each one leading to a separate Python string somewhere else in memory. Copying rows meant chasing those pointers. In Pandas 3 text is backed by PyArrow, stored as one contiguous block of bytes with an offsets array, and moving rows is close to a straight memory copy. Cheaper row-moving helps whichever method moves fewer rows, which is nlargest, by a factor of twenty thousand.
Set against sort_values, the comparison inverts between the two versions:
| text columns | Pandas 2 | Pandas 3 |
|---|---|---|
| 0 | 9.7x | 10.8x |
| 1 | 7.5x | 12.7x |
| 4 | 5.3x | 18.7x |
| 8 | 4.8x | 27.8x |
Under Pandas 2, adding text columns eroded the advantage — 9.7x down to 4.8x. Under Pandas 3 it multiplies it, to nearly 28x. If you benchmarked this for yourself a year or two ago and concluded that nlargest was not worth the bother on a wide frame, that conclusion has expired.
The third difference is dtype. sort_values will happily sort anything, including a column of numbers that arrived as text, and hand you a confident wrong answer; nlargest refuses. There is a demonstration of that below.
So: sort_values(ascending=False).head(n) on a small frame, or when you wanted the sorted frame anyway. nlargest when the question really is "top N" — it says so in the code, it is honest about dtypes, it gives you keep=, and on a real data set with real text columns in it, it is not a little faster but an order of magnitude faster.
Four mistakes people make
A "top five" that quietly is not five. Look again at that first result. Finland placed fifth on 516 medals — and so did Sweden, which never appears. keep='first' kept whichever of them Pandas met first, and nothing in the output hints that a coin was flipped:
medals.nlargest(5, keep='last')
noc
CAN 800
USA 787
NOR 603
GER 562
SWE 516
dtype: int64
Both answers are defensible and both are arbitrary. If the question was "which countries have won the five highest medal totals," neither is correct:
medals.nlargest(5, keep='all')
noc
CAN 800
USA 787
NOR 603
GER 562
FIN 516
SWE 516
dtype: int64
Six rows from a request for five, which is the point — the method stopped pretending the data had a clean cutoff. Counts, ratings, scores and rounded percentages tie constantly. Either add a tiebreaker column, as ['total', 'Gold'] did above, or pass keep='all' and look at what comes back.
Reading columns= as a combined score. A list of columns sorts hierarchically. It does not rank by their sum, their mean, or anything else built out of them. The two are easy to confuse and they give genuinely different answers:
table.nlargest(5, columns=['Gold', 'Silver'])
medal Gold Silver Bronze total
noc
CAN 360 244 196 800
URS 250 97 93 440
NOR 225 214 164 603
USA 219 377 191 787
GER 216 214 132 562
That is the top five by golds, with silvers ready to settle any tie among them — and there were no ties, so the silver column changed nothing at all. If you wanted the countries with the most gold-and-silver medals combined, you have to build the column first:
(
table
.assign(g_s=lambda df_: df_['Gold'] + df_['Silver'])
.nlargest(5, columns='g_s')
)
medal Gold Silver Bronze total g_s
noc
CAN 360 244 196 800 604
USA 219 377 191 787 596
NOR 225 214 164 603 439
GER 216 214 132 562 430
URS 250 97 93 440 347
The Soviet Union drops from second to fifth, and the United States climbs from fourth to second. Same data, same n, two different questions.
Ranking a column of text. nlargest is arithmetic, not alphabetical, and it says so:
winter['as'].nlargest(3)
TypeError: Cannot use method 'nlargest' with dtype str
The data frame form names the offending column — Column 'as' has dtype str, cannot use method 'nlargest' with this dtype — which is even more useful. The case that matters, though, is not text you meant to be text. It is numbers that arrived as text. Here is the NATO members table from Wikipedia, the second data source in Bamboo Weekly #58:
nato = (
pd
.read_html('https://en.wikipedia.org/wiki/Member_states_of_NATO',
storage_options={'User-Agent': 'Mozilla/5.0'})[0]
.set_index('Name')
.rename(columns={'Area[10]': 'area'})
)
nato['area'].sort_values(ascending=False).head(3)
Name
Hungary 93,028 km2 (35,918 sq mi)
Portugal 92,090 km2 (35,556 sq mi)
Canada 9,984,670 km2 (3,855,103 sq mi)
Name: area, dtype: str
sort_values reports that Hungary is the largest country in NATO. As text, "93,028" really does sort after "9,984,670" — the comma comes before every digit — and nothing asked the sort to think harder. nlargest raises the TypeError instead, which is the better failure. Make the column numeric before you rank it:
(
nato
.assign(km2=lambda df_: pd.to_numeric(
df_['area'].str.extract(r'^([\d,]+)', expand=False).str.replace(',', '')))
['km2'].nlargest(3)
)
Name
Canada 9984670
United States[o] 9833520
Denmark[c] 2210573
Name: km2, dtype: int64
Canada, the United States, and — thanks to Greenland — Denmark. Strip the formatting, then rank.
Reaching for it inside a groupby. Top-N per group is the most common reason people go looking for this method, and it is where the obvious call does not work. Start with medals per discipline per country:
by_discipline = winter.groupby(['discipline', 'noc']).size().reset_index(name='medals')
by_discipline.groupby('discipline').nlargest(3, 'medals')
AttributeError: 'DataFrameGroupBy' object has no attribute 'nlargest'
A grouped data frame has no nlargest; only a grouped series does. So you pick out the column — and that runs, which is the trap:
by_discipline.groupby('discipline')['medals'].nlargest(3)
discipline
3-on-3 Ice Hockey (Ice Hockey) 12 4
13 4
16 4
Alpine Skiing (Skiing) 37 146
56 87
43 57
Biathlon 75 99
78 96
72 71
Name: medals, dtype: int64
The right numbers, and no idea whose they are. The inner index level is the original row number, because that is what the index of by_discipline holds. Move the label you care about into the index before you group, and the same call answers the question:
(
by_discipline
.set_index('noc')
.groupby('discipline')['medals']
.nlargest(3)
)
discipline noc
3-on-3 Ice Hockey (Ice Hockey) FRA 4
GBR 4
HUN 4
Alpine Skiing (Skiing) AUT 146
SUI 87
FRA 57
Biathlon GER 99
NOR 96
FRA 71
Name: medals, dtype: int64
Austria in alpine skiing, Germany in biathlon, and — in 3-on-3 ice hockey, a Youth Games event that ran once — a five-way tie on four medals that keep='first' has already trimmed to the three alphabetically earliest. If you want whole rows rather than one column, groupby(...).apply(lambda df_: df_.nlargest(3, 'medals')) gets you there, at the cost of running a Python function once per group.
Where it shows up in Bamboo Weekly
Bamboo Weekly #75: Refugees calls it nine times in one solution, and shows what nlargest is really for: it is the last step of a chain. Slice a MultiIndex column with pd.IndexSlice, sum across years, subtract with diff, divide by population — and then .nlargest(10) to find out who is at the top of whatever you just computed.
Bamboo Weekly #67: Electric cars is the same shape on a pivot table: pct_change(periods=4), then .loc[2023], then .nlargest(5) for the five regions where battery-EV sales grew fastest over four years. Bamboo Weekly #66: Pittsburgh does it twice over a pivot_table of 311 service requests, once for the summer column and once for the winter one.
Bamboo Weekly #58: NATO is the one to read if you like being argued with. I timed nlargest against sort_values().head() on that data set, found it four times slower, and wrote that my "infatuation with nlargest is coming to an end" — while noting that the result might depend on the dtype or the number of values. It depends on the number of values. That file has 30 rows, which is deep inside the range where the startup cost dominates, and the table above shows where the answer flips.
Practice it
Work through an .nlargest() exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/nlargest/
Go deeper
The nearest neighbor is nsmallest, the same method pointed downward, and sort_values, which is what you want when you need the whole column in order. For the single largest value rather than the top few, that is max and idxmax. The series you rank almost always comes out of groupby or pivot_table, and agg will take 'nlargest' and 'nsmallest' by name if you want both ends of the distribution in one call. To order the index instead of the values, that is sort_index.
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
.max()— when you only need the one biggest value or its label.nsmallest()— when you want the bottom of the ranking instead of the top.head()— when you want the first rows in the current order rather than the largest.sort_values()— when you want the whole frame in order, not only its extremes.idxmax()— when one label is enough and you do not need the rows
See it on real data
Below are the 60 Bamboo Weekly exercises that use nlargest on real-world data — try each one, then study the worked solution.
- Bamboo Weekly #182: Surveillance technology
- Bamboo Weekly #181: Housing costs
- Bamboo Weekly #177: European Summer
- Bamboo Weekly #176: Religious restrictions
- Bamboo Weekly #175: Inflation
- Bamboo Weekly #174: Vacation
- Bamboo Weekly #173: IPOs
- Bamboo Weekly #172: World Cup
- Bamboo Weekly #169: Press freedom
- Bamboo Weekly #168: US gas prices
- Bamboo Weekly #164: Fertilizer
- Bamboo Weekly #163: Daylight saving time
- Bamboo Weekly #159: State of the Union
- Bamboo Weekly #157: Government corruption
- Bamboo Weekly #156: Winter Olympics
- Bamboo Weekly #155: Gold
- Bamboo Weekly #152: Congestion pricing
- Bamboo Weekly #151: PyPI in 2025
- Bamboo Weekly #148: US Manufacturing
- Bamboo Weekly #146: Thanksgiving travel
- Bamboo Weekly #142: Hurricanes
- Bamboo Weekly #140: Stack Overflow survey
- Bamboo Weekly #137: UN Security Council
- Bamboo Weekly #136: Indian vehicles
- Bamboo Weekly #135: Airline seats
- Bamboo Weekly #133: Wind power
- Bamboo Weekly #132: JetBrains survey
- Bamboo Weekly #129: Tom Lehrer
- Bamboo Weekly #126: EV sales
- Bamboo Weekly #124: NATO Spending
- Bamboo Weekly #123: Missiles
- Bamboo Weekly #122: Economic growth
- Bamboo Weekly #121: Research funding
- Bamboo Weekly #120: Pennies
- Bamboo Weekly #119: Python conferences
- Bamboo Weekly #118: Flight delays
- Bamboo Weekly #117: Electricity
- Bamboo Weekly #114: International trade
- Bamboo Weekly #112: Programming jobs
- Bamboo Weekly #111: State taxes
- Bamboo Weekly #109: Cacao nibs
- Bamboo Weekly #106: Flu season
- Bamboo Weekly #102: WordPress
- Bamboo Weekly #100: Sports betting
- Bamboo Weekly #96: Taylor Swift
- Bamboo Weekly #95: Tariffs
- Bamboo Weekly #90: Voter participation
- Bamboo Weekly #84: Central banks
- Bamboo Weekly #83: Gasoline prices
- Bamboo Weekly #82: Broadband
- Bamboo Weekly #81: School
- Bamboo Weekly #75: Refugees
- Bamboo Weekly #67: Electric cars
- Bamboo Weekly #66: Pittsburgh
- Bamboo Weekly #62: Economic report card
- Bamboo Weekly #59: Long covid
- Bamboo Weekly #58: NATO
- Bamboo Weekly #57: International arms trade
- Bamboo Weekly #56: Rent increases
- Bamboo Weekly #53: Airport animals
Part of the Pandas Methods Index. See also practice by skill.