Pandas idxmax & idxmin
Reviewed & published by Brayan K
max() tells you the biggest number in a column. It does not tell you who it belongs to. idxmax() answers that second question — it returns the index label of the row holding that maximum, which you can hand straight to .loc to pull back the whole record.
Part of the free Pandas course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
This is the everyday "which product sold most?" question, and getting it wrong — by sorting the entire frame, or by confusing idxmax with argmax — is one of the most common beginner mistakes in pandas.
What You'll Learn in This Lesson
- Why idxmax returns a label, not a value
- Pull the full row with df.loc[...idxmax()]
- Use idxmin for the smallest value
- Find the best row per group with groupby
- Scan across columns with axis=1
- Handle ties, NaN, and empty Series safely
1 The Value vs. the Location
These two methods answer different questions, and mixing them up is where the confusion starts:
- df["sales"].max() — what is the largest number? → 980
- df["sales"].idxmax() — where is it? → 2 (an index label)
On its own that label is not very interesting. Its power is that .loc accepts it, so the two together give you the complete winning row:
idxmin() is the exact mirror image and takes all the same arguments, so everything below applies to finding minimums too.
import pandas as pd
df = pd.DataFrame({
"product": ["Laptop", "Mouse", "Monitor", "Keyboard"],
"sales": [420, 155, 980, 310],
})
print("Biggest number :", df["sales"].max())
print("Its row label :", df["sales"].idxmax())
print()
print("The whole row:")
print(df.loc[df["sales"].idxmax()])
# ✅ Expected output:
# Biggest number : 980
# Its row label : 2
#
# The whole row:
# product Monitor
# sales 980
# Name: 2, dtype: object2 idxmax vs. argmax — the Bug That Hides
argmax() returns the position (0, 1, 2 …). idxmax() returns the label. On a fresh DataFrame these are the same number, so code using the wrong one appears to work — until you filter the rows or set a custom index, and the labels stop matching the positions.
After a filter, the surviving rows keep their original labels — so a frame of three rows might have labels 1, 2 and 5. That is exactly when position and label part company.
import pandas as pd
df = pd.DataFrame({
"product": ["Laptop", "Mouse", "Monitor", "Keyboard"],
"sales": [420, 155, 980, 310],
})
# Keep only the pricier items — labels 0, 2, 3 survive
big = df[df["sales"] > 300]
print(big)
print()
print("idxmax (label) :", big["sales"].idxmax())
print("argmax (position):", big["sales"].argmax())
print()
print("Correct row with .loc :", df.loc[big["sales"].idxmax(), "product"])
print("Correct row with .iloc:", big.iloc[big["sales"].argmax()]["product"])
# ✅ Expected output:
# product sales
# 0 Laptop 420
# 2 Monitor 980
# 3 Keyboard 310
#
# idxmax (label) : 2
# argmax (position): 1
#
# Correct row with .loc : Monitor
# Correct row with .iloc: Monitor3 The Best Row in Every Group
This is where idxmax really earns its place. Suppose you want the top-selling product per region. A plain groupby("region").max() will not do it — that aggregates every column independently, so you can end up with one region's highest sales sitting next to a different product's name. A row that never existed.
groupby(...)["sales"].idxmax() instead returns one label per group. Feed those labels to .loc and you get real, intact rows:
import pandas as pd
df = pd.DataFrame({
"region": ["North", "North", "South", "South", "South"],
"product": ["Laptop", "Mouse", "Monitor", "Keyboard", "Webcam"],
"sales": [420, 155, 980, 310, 640],
})
# One index label per region, then fetch those rows
best = df.loc[df.groupby("region")["sales"].idxmax()]
print(best)
print()
# The misleading alternative — note the mismatched product names
print(df.groupby("region").max())
# ✅ Expected output:
# region product sales
# 0 North Laptop 420
# 2 South Monitor 980
#
# product sales
# region
# North Mouse 420
# South Webcam 9804 Edge Cases: Ties, NaN, Empty & axis=1
- Ties: only the first matching label comes back. If you need every winner, compare instead: df[df["sales"] == df["sales"].max()].
- NaN: ignored by default (skipna=True). Set skipna=False and any NaN present makes the result NaN.
- Empty or all-NaN: raises ValueError: attempt to get argmax of an empty sequence. Check s.notna().any() first.
- axis=1: scans across columns and returns the column name holding each row's maximum — handy for "which month was this shop's best?".
import pandas as pd
import numpy as np
s = pd.Series([10, np.nan, 30, 30])
print("skipna=True (default):", s.idxmax()) # NaN ignored, first 30 wins
print("all tied labels :", list(s[s == s.max()].index))
print()
# axis=1: which month was each shop's best?
sales = pd.DataFrame({
"jan": [120, 300],
"feb": [450, 210],
"mar": [380, 260],
}, index=["Shop A", "Shop B"])
print(sales.idxmax(axis=1))
print()
# Guard against the empty case
empty = s[s > 999]
print("safe:", empty.idxmax() if empty.notna().any() else "no values to compare")
# ✅ Expected output:
# skipna=True (default): 2
# all tied labels : [2, 3]
#
# Shop A feb
# Shop B jan
# dtype: object
#
# safe: no values to compare5 🎯 Your Turn
Given a table of city temperature readings, find the single hottest reading, the single coldest, and the hottest reading in each country — keeping every column of those rows intact.
import pandas as pd
df = pd.DataFrame({
"country": ["UK", "UK", "Spain", "Spain", "Spain"],
"city": ["London", "Leeds", "Madrid", "Seville", "Bilbao"],
"temp_c": [24, 21, 35, 39, 27],
})
print("Hottest overall:")
print(df.loc[df["temp_c"].idxmax()])
print()
print("Coldest overall:")
print(df.loc[df["temp_c"].idxmin()])
print()
print("Hottest per country:")
print(df.loc[df.groupby("country")["temp_c"].idxmax()])
# ✅ Expected output:
# Hottest overall:
# country Spain
# city Seville
# temp_c 39
# Name: 3, dtype: object
#
# Coldest overall:
# country UK
# city Leeds
# temp_c 21
# Name: 1, dtype: object
#
# Hottest per country:
# country city temp_c
# 3 Spain Seville 39
# 0 UK London 24❓ Frequently Asked Questions
Lesson complete — you can now name the winner, not just the number!
You know that idxmax returns a label, that .loc turns that label into a full row, and that groupby(...).idxmax() is the honest way to get the best record per group.
Practice quiz
What does df["sales"].idxmax() return?
- The largest sales value
- The index label of the largest value
- The row as a Series
- The column name
Answer: The index label of the largest value. idxmax returns the index LABEL where the maximum sits — not the value itself.
How do you get the whole row containing the largest value?
- df.max()
- df.loc[df["sales"].idxmax()]
- df["sales"].max()
- df.iloc[df["sales"].max()]
Answer: df.loc[df["sales"].idxmax()]. idxmax gives you the label; feed that label to .loc to pull back the entire row.
What is the difference between .max() and .idxmax()?
- They are identical
- max returns the value, idxmax returns where it is
- max is faster
- idxmax only works on integers
Answer: max returns the value, idxmax returns where it is. max answers 'what is the biggest number', idxmax answers 'which row holds it'.
If two rows tie for the maximum, what does idxmax return?
- Both labels
- The last one
- The first one it meets
- NaN
Answer: The first one it meets. Ties are broken by position — idxmax returns the first occurrence only.
By default, how does idxmax treat NaN values?
- Returns NaN
- Raises an error
- Skips them (skipna=True)
- Treats them as zero
Answer: Skips them (skipna=True). skipna defaults to True, so missing values are ignored when finding the maximum.
What happens if you call idxmax() on an empty Series?
- Returns None
- Returns 0
- Returns NaN
- Raises a ValueError
Answer: Raises a ValueError. There is no label to return, so pandas raises ValueError: attempt to get argmax of an empty sequence.
Which argument makes idxmax scan across columns instead of down rows?
- axis=1
- across=True
- rows=False
- orient='columns'
Answer: axis=1. axis=1 works row-wise, returning the column NAME holding each row's maximum.
How do you find the best-selling row within every region?
- df.groupby('region').max()
- df.loc[df.groupby('region')['sales'].idxmax()]
- df.groupby('region').idxmax()
- df.sort_values('sales')
Answer: df.loc[df.groupby('region')['sales'].idxmax()]. groupby(...).idxmax() gives one label per group; .loc turns those labels back into full rows.
How does argmax differ from idxmax?
- They are the same method
- argmax returns a position, idxmax returns a label
- argmax works on strings only
- argmax is deprecated
Answer: argmax returns a position, idxmax returns a label. argmax gives the integer POSITION (use with .iloc); idxmax gives the index LABEL (use with .loc).
Why is df.loc[df['x'].idxmax()] better than sorting the whole frame?
- It is not, sorting is always better
- It reads more clearly and avoids sorting every row
- It handles NaN differently
- Sorting cannot find a maximum
Answer: It reads more clearly and avoids sorting every row. Sorting rearranges every row just to read the top one; idxmax goes straight to it and states your intent.
Continue this course
- Previous: Sorting and Ranking
- Next: apply, map & applymap — Run custom functions across Series and DataFrames element by element
- Quick reference: Pandas cheat sheet
Frequently asked questions
What does idxmax() return in pandas?
idxmax() returns the index label of the first occurrence of the maximum value — not the maximum value itself. If your DataFrame uses the default integer index it looks like a position, but it is genuinely a label, which is why you pair it with .loc rather than .iloc.
How do I get the entire row with the maximum value?
Pass the label idxmax gives you straight into .loc: df.loc[df['sales'].idxmax()]. That returns the whole row as a Series. To keep it as a one-row DataFrame instead, wrap the label in a list: df.loc[[df['sales'].idxmax()]].
What is the difference between idxmax and argmax?
idxmax returns the index LABEL and is used with .loc. argmax returns the integer POSITION and is used with .iloc. On a default RangeIndex they happen to be the same number, which hides bugs — after filtering or setting a custom index they diverge, and only idxmax still points at the right row.
Why does idxmax raise 'attempt to get argmax of an empty sequence'?
The Series has no values left to compare — usually because a filter matched nothing, or because every value was NaN and skipna removed them all. Guard it with a check such as: if not s.empty and s.notna().any(): s.idxmax().
How do I find the maximum row for each group?
Use groupby with idxmax to get one label per group, then feed those labels to .loc: df.loc[df.groupby('region')['sales'].idxmax()]. This keeps every column of the winning row, which a plain groupby().max() cannot do because it aggregates each column independently.