Same query, different day, different row — if you do not control the tie-break, you do not control your report.
Two products sold exactly 90 units. Which one lands in your top-3? If you can't answer, your report is a coin flip.
df.nlargest(n, col) returns the n rows with the largest values in col. What almost nobody reads past: the keep parameter decides which row survives a TIE for the last slot. 'first' (the default) keeps the first occurrence, 'last' keeps the final one, 'all' keeps every tied row — so you may get back more than n rows.
Ties are common in real data: identical revenue rounded to cents, identical counts, identical sensor readings. With the default keep='first', the winner is decided by row ORDER — a data-entry accident, not a business rule. If a bonus goes to the top-3 reps, a silent tie-break can decide who gets paid.
import pandas as pd
sales = pd.DataFrame({
"id": [1, 2, 3, 4, 5, 6],
"region": ["EMEA", "APAC", "EMEA", "AMER", "EMEA", "APAC"],
"revenue": [120.5, 90.0, 135.4, 150.2, 90.0, 88.7],
})
# ids 2 and 5 are tied at 90.0 — who gets the 3rd slot?
print(sales.nlargest(3, "revenue"))id region revenue 4 AMER 150.2 2 EMEA 135.4 1 EMEA 120.5
keep='first' (default) keeps id 2 — the row that appears first. Correct per the docs, but 'correct' here just means row order won.
# the same query, one keyword later — a different rep gets the slot print(sales.nlargest(3, "revenue", keep="last"))
id region revenue 4 AMER 150.2 2 EMEA 135.4 0 EMEA 120.5
keep='last' hands the last slot to id 5 and drops id 2. Same data, same n — different business outcome.
# 'all' keeps every tied row: you asked for 3, you get 4 print(sales.nlargest(3, "revenue", keep="all").shape)
(4, 3)
keep='all' never hides a tie behind a coin flip — ideal for 'top performers' lists where everyone tied must appear.
# Series form: same keep semantics on a value column
s = sales.set_index("id")["revenue"]
print(s.nlargest(3))id 4 150.2 3 135.4 1 120.5
Series.nlargest takes just n and keep — handy after a groupby or on a cleaned value column.
| Flag | Meaning |
|---|---|
keep='first' | Default: of tied rows, keep the one that appears first in the frame. |
keep='last' | Of tied rows, keep the one that appears last — flips the winner without touching data. |
keep='all' | Keep every row tied for the cutoff; result can exceed n rows. |
n + columns | df.nlargest(n, 'col') picks rows; Series.nlargest(n) picks values — same keep rules. |
dtype caveat | NaN is treated as smaller than any value, so NaN rows never make a top-N. |
Series.nlargest/nsmallest and the DataFrame equivalents arrived together with a stable 'first' tie-break. The docs have promised since then that ties are resolved by keeping the FIRST occurrence.
The 'all' option was added later (0.24 era) after users asked for a way to show every tied row instead of an arbitrary one — ranking APIs in R and SQL grew the same escape hatch.
For small n, pandas doesn't sort: it maintains a heap of size n (one push per row, O(m log n) for m rows) and then reorders the winners back into their original frame order — that's why output rows come out sorted by position, not by value. Ties fall out of the heap comparison, which is exactly where 'first vs last' decides the winner. Docs claim an O(m log n) worst case for keep='all' versus O(m) for the default path.