PYTHON / PANDAS
Sorting and ranking
Reorder rows by column values or index labels, control tie-breaking and NaN placement, and attach 1-based rank columns with the rank rule you need.
What you will learn
- Reorder rows with df.sort_values('col') and renumber via ignore_index=True
- Sort on several keys with per-key directions: ascending=[True, False]
- Pick a tie rule for rank: average, min, max, dense, or first
- Place missing values with na_position, which is independent of ascending
Understanding Sorting and ranking
sort_values rearranges rows and returns a new object; the original DataFrame is untouched unless you reassign it. Crucially, the index travels with each row, so after sorting the label 0 is still on whichever row it started on. That means df.loc[0] gives you the original first row, not the new top row — use .iloc[0] for position, or pass ignore_index=True to throw the old labels away and renumber 0..n-1. sort_index() is the sibling operation: it orders rows by their labels rather than by any column's values.
With several keys, sort_values(['a','b']) sorts by a and breaks ties with b, and ascending can be a list so each key gets its own direction. Missing values are handled separately from direction: NaN goes to the end regardless of ascending, and only na_position='first' moves it to the top. Tie order among equal keys is stable in practice for multi-column sorts (pandas uses a lexsort under the hood), but the default kind='quicksort' for a single column carries no such promise — pass kind='stable' when the relative order of tied rows must be reproducible.
rank() answers a different question: it keeps every value where it is and reports what position it would occupy, counting from 1. The method argument decides what happens to ties: 'average' (the default) splits the contested positions, so two values tied for 2nd and 3rd both get 2.5; 'min' gives both 2.0 and leaves a gap at 3 (competition ranking); 'dense' gives both 2.0 and continues at 3 with no gap; 'first' breaks the tie by original order. NaN receives NaN and consumes no position, and pct=True divides by the number of ranked values to get a percentile between 0 and 1.
import pandas as pd
df = pd.DataFrame({
"team": ["Red", "Blue", "Red", "Blue", "Red"],
"player": ["Ada", "Ben", "Cleo", "Dan", "Eve"],
"points": [18, 24, 24, 9, None],
})
print(df.sort_values("points", ascending=False))
print()
df["rank"] = df["points"].rank(ascending=False, method="min")
print(df[["player", "points", "rank"]])Sorting moves rows and drags their index labels along, while ranking leaves rows in place and assigns each value a 1-based position under a tie rule you choose.
Worked examples
Two keys, opposite directions
Sorts by department alphabetically and by salary from high to low inside each department, then renumbers the index.
import pandas as pd
df = pd.DataFrame({
"dept": ["eng", "sales", "eng", "sales", "eng"],
"name": ["Ada", "Ben", "Cleo", "Dan", "Eve"],
"salary": [120, 90, 120, 95, 110],
})
print(df.sort_values(["dept", "salary"], ascending=[True, False], ignore_index=True))Example explained
Line 1The list ['dept','salary'] makes dept the primary key and salary the tie-breaker.
Line 2ascending=[True, False] pairs one direction with each key, so dept climbs while salary falls.
Line 3Ada and Cleo both earn 120; the multi-key sort is stable, so Ada keeps her earlier original position.
Line 4ignore_index=True discards the old labels 0,2,4,3,1 and writes a fresh 0..4 index.
Every rank method side by side
Shows how the five tie rules and pct differ on a single tied pair.
import pandas as pd
s = pd.Series([10, 20, 20, 40], index=["a", "b", "c", "d"])
print(pd.DataFrame({
"average": s.rank(),
"min": s.rank(method="min"),
"max": s.rank(method="max"),
"dense": s.rank(method="dense"),
"first": s.rank(method="first"),
"pct": s.rank(pct=True),
}))Example explained
Line 1The two 20s contest positions 2 and 3, so 'average' reports 2.5 for both.
Line 2'min' gives both 2.0 and skips to 4.0 for the value 40; 'dense' gives both 2.0 and continues at 3.0.
Line 3'first' is the only method that separates the tie, using the original order b before c.
Line 4pct=True divides the average rank by 4, so the largest value maps to 1.000.
Top-n values versus sorting by label
Contrasts nlargest, which returns only the highest rows already ordered, with sort_index, which orders by the index labels.
import pandas as pd
sales = pd.Series([5, 12, 3, 12, 8],
index=["fig", "apple", "date", "cherry", "berry"])
print(sales.nlargest(3))
print()
print(sales.sort_index().head(3))Example explained
Line 1nlargest(3) is shorthand for sort_values(ascending=False).head(3) and keeps the earlier row first on ties.
Line 2sort_index() ignores the values entirely and orders the labels alphabetically.
Line 3head(3) after sort_index therefore returns the three alphabetically first fruits, not the three biggest numbers.
Important notes
rank() always returns float64, even with no ties, because 'average' can produce values like 2.5; convert with .astype('Int64') if you need whole numbers alongside NaN.
sort_index(axis=1) sorts the column labels instead of the rows, which is handy for making two frames comparable before printing.
Common mistakes
Calling df.sort_values('col') and then reading df again: sorting returns a new frame, so the unassigned result is discarded and df is still unsorted.
Assuming ascending=False lifts NaN to the top; NaN stays at the bottom until you pass na_position='first'.
Using df.loc[0] to read the top row after sorting: label 0 still belongs to its original row, so you get the wrong record unless you use .iloc[0] or ignore_index=True.
Try it yourself
Change, predict, then run
Build a Series of five prices that includes one None, then print it sorted descending with the missing value first, and print a second Series of its dense ranks where the highest price ranks 1.
Open the Python workspaceCheck your understanding
A Series holds [30, 10, 30, NaN]. What does s.rank(method='dense', ascending=False) produce?
- [1.0, 2.0, 1.0, NaN]
- [1.5, 3.0, 1.5, 4.0]
- [1.0, 3.0, 1.0, NaN]
- [2.0, 1.0, 2.0, NaN]
Show answer
ascending=False makes 30 the top value, so both 30s get rank 1; 'dense' never leaves gaps, so the next distinct value 10 gets 2, and NaN ranks as NaN. [1.0, 3.0, 1.0, NaN] is what method='min' gives, because it skips rank 2 to account for the two-way tie.