Skip to content

pandas nlargest

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.

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.

Part of the Pandas Methods Index. See also practice by skill.