sort_values, nlargest and rank - and where the missing values end up.
Overview
sort_values
Sorting is one of the most common operations in data analysis. You sort to find the largest values, to put records in chronological order, to group related rows visually, or to prepare data for a report. pandas provides sort_values for sorting by column values and sort_index for sorting by the index labels.
The distinction between stable and unstable sorting matters more than most people realize. An unstable sort can rearrange rows that have equal values in the sort column. That rearrangement is not random, but it is not guaranteed to preserve the original order either. If you sort by region and then by date within each region, an unstable sort on date after a sort on region can scramble the regions. Passing both keys in a single sort_values call avoids the issue.
Missing values go to the end of the sorted result by default, regardless of the sort direction. That behaviour is usually what you want, but it is worth knowing about. The na_position parameter lets you move them to the beginning if your use case requires it.
The result of sort_values is a new DataFrame with the original index labels preserved. That means the index after sorting is not 0, 1, 2. It is whatever the labels were before. Positional code that assumes sorted order starts at index 0 will break. Use reset_index if you need positional access after sorting.
rank assigns ordinal positions without rearranging the data. It is useful when you need a ranking column alongside the original order. The method parameter controls how ties are handled: average, min, max, first, or dense. Each gives a different sequence for tied values.
This module covers sort_values with single and multiple keys, ascending and descending directions, stable sorting, na_position, sort_index, nlargest, nsmallest, and rank. These are the tools you reach for whenever order matters.
df.sort_values("sales") sorts by one column. A list sorts by a few, in order. ascending takes a matching list so each key can have its own direction:
That is "by city A to Z. Within each city by sales high to low". The shape of most reporting orders.
The result is a new frame carrying the first index labels. If downstream code thinks positionally, reset_index(drop=True) later.
kind selects the method. The default isn't stable. Equal rows can come out in any order. Pass kind="stable" when ties must keep their first scaled order. Which matters if you're sorting by a second key after already sorting by a first, rather than passing both keys at once.
Worth knowing
sort_values takes one or several columns and an ascending list per key; it returns a new frame carrying the original labels.
Missing values sort last in both directions, so reversing an ascending sort is not the same as sorting descending.
sort_index orders by label, sort_values by data; sort_index(axis=1) orders the columns.
nlargest(n, col) is much faster than sorting everything and taking the head.
rank defaults to averaging ties; min gives competition ranking and dense leaves no gaps.
Sorting is n log n and copies — use a reduction for an extreme, nlargest for a top n, and sort only when order is the output.
Sorting and Ranking
sort_values, nlargest and rank, and where the missing values end up.
sort_values takes one column or several
And returns a new frame, keeping the original index labels.
example_01.pypandas
import pandas as pd
df = pd.DataFrame({
"city": ["pune", "delhi", "pune", "goa"],
"sales": [30, 20, 10, 20],
})
print("by sales:")
print(df.sort_values("sales"))
print()
print("descending:")
print(df.sort_values("sales", ascending=False))
print()
print("two keys, with different directions:")
print(df.sort_values(["city", "sales"], ascending=[True, False]))
print()
print("the index came along - reset it if position matters:")
print(list(df.sort_values("sales").index))
Output
Missing values go last, whatever the direction
Which means descending order does not simply reverse ascending order.
example_02.pypandas
import pandas as pd
import numpy as np
s = pd.Series([3.0, np.nan, 1.0, 2.0])
print("ascending :", list(s.sort_values()))
print("descending:", list(s.sort_values(ascending=False)))
print(" NaN is last in BOTH - it is not treated as large or small")
print()
print("na_position='first' moves it:")
print(" ", list(s.sort_values(na_position="first")))
print()
print("so reversing a sorted result is not the same as sorting descending:")
print(" reversed ascending:", list(s.sort_values()[::-1]))
print(" sorted descending :", list(s.sort_values(ascending=False)))
Output
sort_index versus sort_values
One orders by the labels, the other by the data.
example_03.pypandas
import pandas as pd
s = pd.Series([10, 30, 20], index=["c", "a", "b"])
print("original :", dict(s))
print("sort_index() :", dict(s.sort_index()), "<- by LABEL")
print("sort_values() :", dict(s.sort_values()), "<- by DATA")
print()
df = pd.DataFrame({"b": [1, 2], "a": [3, 4]})
print("sort the columns alphabetically:")
print(df.sort_index(axis=1))
print()
print("sort_index matters after a groupby, which returns")
print("groups in sorted key order already.")
Output
nlargest beats sorting the whole frame
When you only want the top few, do not order the rest.
example_04.pypandas
import pandas as pd
import numpy as np
import time
rng = np.random.default_rng(0)
df = pd.DataFrame({"v": rng.random(200_000)})
t = time.perf_counter(); a = df.sort_values("v", ascending=False).head(5); t1 = time.perf_counter() - t
t = time.perf_counter(); b = df.nlargest(5, "v"); t2 = time.perf_counter() - t
print("sort then head : %.4f s" % t1)
print("nlargest : %.4f s" % t2)
print("ratio : %.1fx" % (t1 / max(t2, 1e-9)))
print("same rows :", a.index.equals(b.index))
print()
print("nsmallest is the other direction, and both take a column list")
print("for tie-breaking.")
Output
rank, and what to do with ties
The default averages tied ranks, which is often not what a leaderboard wants.
example_05.pypandas
import pandas as pd
s = pd.Series([10, 20, 20, 40], index=["a", "b", "c", "d"])
for how in ["average", "min", "max", "dense", "first"]:
print("%-8s :" % how, list(s.rank(method=how).astype(float)))
print()
print("average : the two 20s share rank 2.5")
print("min : both get 2, and 3 is skipped (competition ranking)")
print("dense : both get 2, and the next is 3 (no gaps)")
print("first : ties broken by order of appearance")
print()
print("descending ranks:", list(s.rank(ascending=False).astype(float)))
print("as a percentage :", [round(v, 2) for v in s.rank(pct=True)])
Output
Sorting is not free
It is n log n and it copies, so do it once and late.
example_06.pypandas
import pandas as pd
import numpy as np
import time
rng = np.random.default_rng(0)
df = pd.DataFrame({"k": rng.integers(0, 1000, 300_000),
"v": rng.random(300_000)})
t = time.perf_counter(); df.sort_values("v"); full = time.perf_counter() - t
t = time.perf_counter(); df["v"].max(); mx = time.perf_counter() - t
print("sort 300k rows : %.4f s" % full)
print("max of a column: %.6f s" % mx)
print("sorting is %.0fx the cost of the reduction" % (full / max(mx, 1e-9)))
print()
print("If you only need the extreme, do not sort. If you need the top n,")
print("use nlargest. Sort when the ORDER itself is the output.")
Output
Missing values sort last
NaN goes to the end in both ascending and descending order. It isn't treated as very large or very small. It's in short aside.
The result catches people: reversing an ascending sort is not the same as sorting descending. The NaNs move scaled to all else.
na_position="first" puts them at the front instead. This Is handy when the missing rows are the ones you want to look at.
sort_index
sort_index() orders by the index labels rather than the data. It's what you want after steps that leave the index shuffled. It's needed for label-range slicing on a non-unique index.
sort_index(axis=1) orders the columns alphabetically. This Is a quick way to make two frames close.
Group-by results already come back sorted by key. An extra sort_index there's often redundant.
nlargest and nsmallest
Sorting 200,000 rows to look at five is wasted work.
df.nlargest(5, "v") finds the top five without sorting the rest. The last-but-one editor measures the gap.
Both take a list of columns for tie-breaking. A keep arg controlling what happens when the boundary value is tied: "first", "last" or "all". keep="all" can return more than n rows. This Is at times what you want and worth knowing before it surprises you.
For a single extreme value, neither is needed: df["v"].max() is a reduction and far cheaper than any sort. idxmax() gives the label of the row that holds it. This Is how you get "the row with the highest value" in one step.
rank
rank converts values into spots, and the notable part is what it does with ties.
method="mean" (the default) gives tied values the mean of the ranks they span. Two values tied for 2nd and 3rd both get 2.5. That is the statistically standard choice and it makes fractional ranks. This Surprises people expecting ints.
method="min" gives both 2 and skips 3. This is contest ranking, the "joint second" of a leaderboard.
method="max" gives both 3.
method="dense" gives both 2 and makes the next value 3. No gaps. This Is what you want for grouping into levels.
method="first" breaks ties by order of appearance, giving strictly distinct int ranks.
ascending=False ranks from the top. pct=True returns percentiles rather than spots. This Is the quick route to "what fraction of rows are below this one".
rank also takes a na_option, and by default missing values get NaN ranks rather than being placed anywhere.
The cost
Sorting is O(n log n) and allocates a new frame. On a large table it's one of the more costly things you can do.
Three cheaper options cover most reasons people sort:
An extreme value: max, min, idxmax, idxmin — a single linear pass.
A top n: nlargest / nsmallest.
Deduplication order: sorting is genuinely required here, and it is the one case where sorting before drop_duplicates is not optional.
Sort when the order is the actual output. A report, a chart, a file someone will read. else there's often a reduction that answers the same question for a fraction of the cost.
Sorting by something that is not a column
key= applies a function to the sort keys before checking, without changing the data:
groupby(...).head(3) also works and takes the first three rows of each group in current order. It needs a sort first to mean "the top three".
Stability, in practice
The default sort isn't stable. So Equal elements can come out in any order.
That matters in two situations.
A multi-pass sort. Sorting by one column, then another, only works if the second sort preserves the first sorting. Pass kind="stable", or better, sort by both keys in one call with a list.
Reproducible output. Two runs on the same data can order ties differently. This Makes diffs noisy and tests flaky. kind="stable" fixes it.
The cost is small enough that using kind="stable" by default when the output is checked or written to a file is a fair habit.
Sorting large frames
Sorting is O(n log n) and copies the frame. On a large table it's often the most costly single step in a script.
The options, in order of preference:
A reduction — max, min, idxmax, idxmin for an extreme.
**nlargest / nsmallest** for a top n.
**sort_index** if the data is already nearly sorted by that key. It's cheaper than a full value sort.
Sorting once and reusing the result, rather than sorting inside a loop or a function called again and again.
And when the sort *is* the output. A report, a leaderboard, a file someone reads. It isn't cost, it's the deliverable.
Ranking, and what to do with ties
The five methods differ only in tie handling. The right choice depends on what the rank is for.
For a leaderboard, min gives the familiar "joint second, then fourth".
For levels or bands, dense avoids gaps.
For a statistic — a rank link, a percentile — mean is the standard choice and is the default for that reason.
For a fixed sorting where ties must be broken, first uses order of appearance. This Means the result depends on how the frame is sorted.
pct=True gives percentile ranks in [0, 1]. This Is the quick route to "what fraction of rows are at or below this one" and is close across datasets of other sizes.
Sorting the columns rather than the rows
df.sort_index(axis=1) orders columns alphabetically. This Makes two frames easier to compare and diffs easier to read.
For a deliberate order, pick is clearer: df[["id", "date", "value"]].
For a partial order — pin some columns to the front, keep the rest — build the list:
first = ["id", "date"]
df = df[first + [c for c in df.columns if c not in first]]
That lives a schema change. Here A hard-coded full list doesn't.
Sorting for output
When the sort is the deliverable, a few details matter that don't else.
na_position="first" when missing rows are what the reader should notice.
key= for case-insensitive or natural sorting.
An sorted cat for a domain order. Small/medium/large. So each later step uses it too.
reset_index(drop=True) if row numbers will be displayed. They read 0, 1, 2 rather than the first labels.
kind="stable" so that re-running makes byte-same output. This Matters if the result is committed or diffed.
Ties, restated
The choice of tie-handling is a choice about meaning, not a detail:
mean — the statistical rule. Makes fractional ranks.
min — contest ranking. Joint second, then fourth.
dense — no gaps. For bands and levels.
max — the pessimistic reading.
first — any but fixed, and dependent on the current row order.
If ranks are shown to people, min or dense often matches their expectations. If ranks feed a statistic, mean is the right default.
A summary
sort_values for data, sort_index for labels.
A list of keys with a matching list of directions handles multi-key sorts in one call.
Missing values go last in both directions. So Reversing isn't the same as descending.
nlargest/nsmallest for a top n. idxmax/idxmin for a single extreme.
key= for derived orderings. Sorted cats for domain orderings.
kind="stable" when ties must keep their order or output must be reproducible.
And sorting is costly — do it once, late. Only when the order is part of the answer.
A closing note on Sorting and Ranking
Sorting is costly and often not needed. This Makes it worth asking what the order is for.
If the answer is "to find the largest", a reduction does it in one pass. If it's "to find the top ten", nlargest does it without sorting the rest. If it's "so deduplication keeps the right row", the sort is key and skipping it makes the result any. If it's "because the output is a report", the sort is the deliverable and its cost is the point.
Ranking is the same step asked differently. Its only real subtlety is ties. The default means them. This Makes fractional ranks and surprises people expecting ints. Leaderboards often want min, and bands often want dense.
And missing values sort last in both directions. This Means reversing an ascending sort isn't the same as sorting descending. A small skew that makes a wrong answer at the ends.
One more thing
sort_values accepts ignore_index=True, which renumbers the result rather than carrying the first labels along. That saves a split reset_index(drop=True) when the sorted output is going to be displayed or written with row numbers.
And nlargest and nsmallest exist on groupby objects too. "the top three per group" is df.groupby("city")["sales"].nlargest(3). Which returns a MultiIndexed Series with the group and the first label. Often wants reset_index before going anywhere else.
Questions people ask about Sorting and Ranking
What is the difference between sort_values and sort_index? sort_values orders rows by the contents of one or more columns, and sort_index orders them by index labels.
How do I sort by two columns in different directions? Pass lists: df.sort_values(['a', 'b'], ascending=[True, False]).
Where do missing values go when sorting? They are placed last by default. Use na_position='first' to move them to the top.
What is the difference between sort_values and rank? sort_values reorders the rows. rank leaves the rows in place and adds a number showing each value's position in the ordering.
How does rank handle ties? The default method is 'average', which gives tied values the mean of their positions. Use 'min', 'dense' or 'first' for other behaviours.
What is faster for the top few rows, nlargest or sort_values? nlargest(n, 'col') is usually faster, because it does not sort the whole column.
A note on reproducibility
Output that will be committed to a store, diffed, or checked between runs should be fixed. Sorting is where non-determinism creeps in.
The default sort isn't stable. Tied rows can appear in other orders on other runs or other pandas versions. A file written from that output makes spurious diffs. A test checking it fails intermittently.
kind="stable" fixes it, at a cost small enough to skip.
The same applies to group-by. This Sorts keys by default but says nothing about the order of rows within a group. To drop_duplicates, whose survivor depends fully on the current order.
For anything whose output is checked, the rule is. Sort by name, sort stably. Make the sort keys enough to find the order uniquely.
Check yourself
0 of 4
Answer without scrolling back up.
Where do NaN values go when sorting descending?
They are set aside rather than treated as large or small - which is why reversing an ascending sort is not the same as sorting descending.
You need the 5 highest rows from 200,000. What is the right call?
It finds the top five without ordering the rest. For a single extreme, idxmax is cheaper still.
Two values tie. What does the default `rank()` give them?
The default is 'average', which produces fractional ranks. Use method='min' for leaderboard-style joint second, or 'dense' for no gaps.
When is sorting genuinely required rather than avoidable?
drop_duplicates keeps the first row in current order, so without an explicit sort the survivor is arbitrary.
Cheat sheet
Sorting and Ranking
Sort rows by one column or several, find the top N values with nlargest, and see where NaN lands in each method.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.