Use nlargest() to select records with the largest values in supported ordering columns. Define the eligible population and decide whether a tie at the cutoff should return exactly n records or every tied record.
DataFrame.nlargest(n, columns, keep="first") returns a DataFrame, retaining other fields in each selected record. Series.nlargest(n, keep="first") returns a Series and defaults n to 5; the DataFrame version requires n and columns. Both order selected values descending. keep chooses first, last or all tied occurrences; all may return more than n rows. This selects records, not distinct score levels.
The synthetic student table has one score of 80 and three scores of 70. Default keep="first" chooses Alex at the tied cutoff. Source index labels remain intact.
import pandas as pd
def student_data():
return pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex", "Ron", "King", "Jack"], "ID": [1, 2, 3, 4, 5, 6], "MATH": [80, 40, 70, 70, 70, 30], "CLASS1": ["Four", "Three", "Three", "Four", "Five", "Three"]})
df = student_data()
result = df.nlargest(n=2, columns="MATH", keep="first")
print(result)
assert result["ID"].tolist() == [1, 3]
assert df["ID"].tolist() == [1, 2, 3, 4, 5, 6]Expected output
NAME ID MATH CLASS1
0 Ravi 1 80 Four
2 Alex 3 70 Threekeep="last" selects King instead of Alex at the cutoff. First and last refer to occurrence order in the input, not smallest or largest ID.
df = student_data()
result = df.nlargest(2, "MATH", keep="last")
print(result)
assert result["ID"].tolist() == [1, 5]Expected output
NAME ID MATH CLASS1
0 Ravi 1 80 Four
4 King 5 70 Fivekeep="all" includes all three students scoring 70 along with Ravi scoring 80. The result has four rows even though n=2. It is not a request for two distinct score levels in general.
df = student_data()
result = df.nlargest(2, "MATH", keep="all")
print(result)
print("Returned records:", len(result))
assert result["ID"].tolist() == [1, 3, 4, 5]
assert len(result) == 4Expected output
NAME ID MATH CLASS1
0 Ravi 1 80 Four
2 Alex 3 70 Three
3 Ron 4 70 Four
4 King 5 70 Five
Returned records: 4A Series result contains values and their original index labels, without student fields. Use the DataFrame version when the output must contain full records. See Series and iloc for related concepts.
df = student_data()
scores = df["MATH"].nlargest(2)
print(scores)
assert scores.tolist() == [80, 70]
assert scores.index.tolist() == [0, 2]Expected output
0 80
2 70
Name: MATH, dtype: int64All columns supplied to nlargest are ordered descending. Here ENGLISH breaks the MATH tie: King’s English score of 60 wins over Ron’s 50 and Alex’s 40. Use sort_values() for mixed ascending and descending directions.
df = student_data()
df["ENGLISH"] = [80, 70, 40, 50, 60, 30]
result = df.nlargest(2, ["MATH", "ENGLISH"])
print(result)
mixed = df.sort_values(["MATH", "ID"], ascending=[False, True]).head(2)
print("Score descending, ID ascending:")
print(mixed)
assert result["ID"].tolist() == [1, 5]
assert mixed["ID"].tolist() == [1, 3]Expected output
NAME ID MATH CLASS1 ENGLISH
0 Ravi 1 80 Four 80
4 King 5 70 Five 60
Score descending, ID ascending:
NAME ID MATH CLASS1 ENGLISH
0 Ravi 1 80 Four 80
2 Alex 3 70 Three 40Text or categorical ordering columns are unsupported by DataFrame.nlargest and raise TypeError. Parse numbers explicitly, then review failures rather than treating them as zero. Other non-ordering text columns can remain in the result.
raw = pd.DataFrame({"id": [1, 2, 3, 4], "amount": ["100", "20", "3", "bad"]})
try:
raw.nlargest(2, "amount")
except TypeError:
print("Parse the amount column before selecting largest values.")
cleaned = raw.copy()
cleaned["amount"] = pd.to_numeric(raw["amount"], errors="coerce")
known = cleaned.loc[cleaned["amount"].notna()].copy()
print(known.nlargest(2, "amount"))
print("Unparseable source values:")
print(raw.loc[cleaned["amount"].isna()])
assert known.nlargest(2, "amount")["id"].tolist() == [1, 2]Expected output
Parse the amount column before selecting largest values.
id amount
0 1 100.0
1 2 20.0
Unparseable source values:
id amount
3 4 badSelect known values explicitly when every selected row must have a usable measure. If fewer than n eligible records exist, return those available records and report the count. Validate n as a positive integer for a user-facing report.
records = pd.DataFrame({"id": [1, 2, 3], "score": [80, None, 70]})
eligible = records.loc[records["score"].notna()].copy()
n = 5
assert isinstance(n, int) and n > 0
result = eligible.nlargest(n, "score")
print(result)
print("Requested:", n, "Available:", len(result))
assert result["id"].tolist() == [1, 3]
assert result["score"].notna().all()Expected output
id score
0 1 80.0
2 3 70.0
Requested: 5 Available: 2For mixed-direction tie-breaking and full rows, sort by class, mark descending and ID ascending, then use groupby().head(). This selects up to two student records in each class; it is different from selecting two records globally.
df = student_data()
ordered = df.sort_values(["CLASS1", "MATH", "ID"], ascending=[True, False, True])
top_by_class = ordered.groupby("CLASS1", sort=False).head(2)
print(top_by_class)
assert top_by_class.groupby("CLASS1").size().max() <= 2
assert set(top_by_class["ID"]) == {1, 3, 4, 5, 2}Expected output
NAME ID MATH CLASS1
4 King 5 70 Five
0 Ravi 1 80 Four
3 Ron 4 70 Four
2 Alex 3 70 Three
1 Raju 2 40 ThreeThe synthetic extract includes a test order, invalid text, tied paid amounts and an ordinary small paid order. Define paid orders with known non-negative amounts before selecting the top two. keep="all" includes both orders tied at the cutoff.
raw = pd.DataFrame({"order_id": [101, 102, 103, 104, 105, 106], "status": ["paid", "paid", "paid", "test", "paid", "paid"], "amount": ["100", "70", "70", "500", "bad", "20"]})
cleaned = raw.copy()
cleaned["amount"] = pd.to_numeric(raw["amount"], errors="coerce")
eligible_mask = cleaned["status"].eq("paid") & cleaned["amount"].ge(0).fillna(False)
eligible = cleaned.loc[eligible_mask].copy()
review = raw.loc[~eligible_mask].copy()
top = eligible.nlargest(2, "amount", keep="all")
print("Top orders including cutoff ties:")
print(top)
print("Excluded source records:")
print(review)
assert top["order_id"].tolist() == [101, 102, 103]
assert len(top) == 3
assert review["order_id"].tolist() == [104, 105]Expected output
Top orders including cutoff ties:
order_id status amount
0 101 paid 100.0
1 102 paid 70.0
2 103 paid 70.0
Excluded source records:
order_id status amount
3 104 test 500
4 105 paid badThe top-record subtotal is not total eligible revenue. State the population and tie policy alongside the selected count. This sample’s selected revenue is 240 out of 260 eligible revenue. Unique customers or products require a separate grouping rule.
total = eligible["amount"].sum()
selected = top["amount"].sum()
print("Eligible orders:", len(eligible))
print("Selected orders:", len(top))
print("Eligible revenue:", total)
print("Selected revenue:", selected)
print("Selected revenue share:", round(selected / total * 100, 2))
print(top.to_csv(index=False).rstrip())
assert total == 260 and selected == 240
# Optional Colab export:
# top.to_csv("top_paid_orders.csv", index=False)Expected output
Eligible orders: 4
Selected orders: 3
Eligible revenue: 260.0
Selected revenue: 240.0
Selected revenue share: 92.31
order_id,status,amount
101,paid,100.0
102,paid,70.0
103,paid,70.0If the question is which products have the most revenue, aggregate by product first and select the largest summary rows. Use groupby() for the summary and cut() for bands rather than top records.
sales = pd.DataFrame({"product": ["Pen", "Pen", "Book", "Notebook"], "revenue": [60, 60, 100, 30]})
summary = sales.groupby("product", as_index=False).agg(revenue=("revenue", "sum"))
top_products = summary.nlargest(2, "revenue", keep="all")
print(top_products)
assert top_products["product"].tolist() == ["Pen", "Book"]
assert summary["revenue"].sum() == sales["revenue"].sum()Expected output
product revenue
2 Pen 120
0 Book 100Why are more than n rows returned? keep="all" includes ties at the cutoff. Does it return n unique values? No, it selects records. Why did a different tied ID win? first and last depend on input order; use explicit secondary sort keys when IDs decide ties. Does it reset the index? No; use reset_index(drop=True) after selection if required. Can it order names? Use sort_values() for text. Does it select one record per customer? No; define a customer-level summary or per-group selection first. Is it faster? It can avoid fully sorting a supported numeric column for a small top-n selection; measure representative data if performance matters. For a report with mixed directions use sort_values().
Use IDs 1, 2, 3 and 4 with amounts 90, 60, 60 and 10. Select the largest two records with keep="all". Predict the number of selected records and their subtotal.
Three records qualify because two share the cutoff amount of 60.
practice = pd.DataFrame({"id": [1, 2, 3, 4], "amount": [90, 60, 60, 10]})
answer = practice.nlargest(2, "amount", keep="all")
print(answer)
print("Subtotal:", answer["amount"].sum())
assert answer["id"].tolist() == [1, 2, 3]
assert len(answer) == 3
assert answer["amount"].sum() == 210Expected output
id amount
0 1 90
1 2 60
2 3 60
Subtotal: 210Open in Google Colab View on GitHub
Run the examples in order, change n and tie policies, then inspect selected records and report denominators. Save your own copy to keep edits. All sample tables are included in the notebook.
Continue with the Data Cleaning hub, where(), loc, at, NumPy where(), fillna() and iloc.
Reference: Pandas DataFrame.nlargest documentation.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.