pandas nsmallest() — grab the bottom n rows in one call

Worst offenders, cheapest stores, coldest sensors — the n rows you actually wanted from sort_values().head().

Your manager asks for the three worst-performing stores — and most people still sort a thousand rows to keep three.

What it does

df.nsmallest(n, 'col') returns the n rows with the smallest values in the column you name, sorted ascending — the exact mirror of nlargest(). NaN rows are skipped by default, ties at the cutoff follow the keep rule, and passing a list of columns gives you a tie-break chain: sort by the first column, settle ties on the second. The Series version takes no column argument, so s.nsmallest(5) is a one-call 'five lowest readings'.

Why it matters

Bottom-k questions are everywhere once you look for them: which stores are bleeding revenue, which sensors read coldest, which API endpoints are fastest to answer. The default idiom sort_values().head() pays full price — a complete O(n log n) sort of every row to keep three. nsmallest() was added (pandas 0.17.0, September 2015) so you would not have to: it partitions with quickselect instead of sorting, which on my machine is roughly 24x faster on 10 million rows. Sorting a table to keep three rows is paying for a full ranking you never asked for.

Examples

import pandas as pd

sales = pd.DataFrame({
    "store": ["Berlin", "Hamburg", "Munich", "Cologne", "Frankfurt",
              "Stuttgart", "Düsseldorf", "Leipzig"],
    "region": ["east", "north", "south", "west", "west", "south", "west", "east"],
    "revenue": [48200, 51100, 39400, 47650, 21000, 41200, 45900, 22800],
})
sales.nsmallest(3, "revenue")
       store region  revenue
4  Frankfurt   west    21000
7    Leipzig   east    22800
2    Munich  south    39400

The daily-driver move: the three worst stores by revenue, ranked rows included, in one call.

sales['at_risk'] = sales.index.isin(sales.nsmallest(3, "revenue").index)
sales
       store region  revenue  at_risk
0      Berlin   east    48200    False
1     Hamburg  north    51100    False
2      Munich  south    39400     True
3     Cologne   west    47650    False
4   Frankfurt   west    21000     True
5    Stuttgart  south    41200    False
6  Düsseldorf   west    45900    False
7     Leipzig   east    22800     True

The real workflow: use the index nsmallest() returns to flag at-risk rows on the full report.

readings = pd.DataFrame({
    "sensor": ["S1", "S2", "S3", "S4", "S5"],
    "reading": [10, 22, 22, 31, 10],
    "drift": [0.4, 1.1, 0.9, 2.5, 0.2],
})
readings.nsmallest(3, ["reading", "drift"])
  sensor  reading  drift
4     S5       10    0.2
0     S1       10    0.4
2     S3       22    0.9

Multi-column tie-break: both S5 and S1 read 10, so the lower drift settles it; S3 wins the third slot at 22.

dups = pd.DataFrame({"city": ["A", "B", "C", "D"], "temp_c": [5.2, 5.2, 9.0, 3.0]})
dups.nsmallest(2, "temp_c", keep="all")
  city  temp_c
3    D     3.0
0    A     5.2
1    B     5.2

Ties handled SQL-style: keep='first' (the default) would keep only A and drop B — 'all' keeps every row tied at the cutoff, even more than n rows.

s = pd.Series({"Mon": 41, "Tue": 12, "Wed": 88, "Thu": 12, "Fri": 55})
s.nsmallest(2)
Tue    12
Thu    12
dtype: int64

The Series version takes no column argument. keep='first' kept Tue and dropped the tied Thu; keep='all' would return both.

Flags

FlagMeaning
df.nsmallest(n, 'col')the core call: n smallest rows by one column, returned sorted ascending
df.nsmallest(n, ['col1', 'col2'])tie-break chain: settle ties on the second column when the first is equal
df.nsmallest(n, 'col', keep='all')keep every row tied at the cutoff, even if that means more than n rows back
df.nsmallest(n, 'col', keep='last')on ties keep the later row instead of the first
s.nsmallest(n)Series version: no column argument, nsmallest defaults to n=5
df.index.isin(df.nsmallest(3, 'col').index)turn the selection into a boolean mask to flag rows on the full frame
df.sort_values('col').groupby('region').head(2)per-group bottom-k: nsmallest has no groupby method, so sort + groupby head is the pattern

Added in pandas 0.17.0 (September 2015)

nsmallest() and nlargest() arrived together in 0.17.0 (GH 10393) — the DataFrame methods, that is. Series.nsmallest() predates them: it landed back in 0.14.0 (May 2014, GH 3960). The 0.17.0 changelog also deprecated Series.nsmallest's old take_last keyword in favor of keep, unifying the tie rule across the pair.

Design note: the tie rule

keep='first' | 'last' | 'all' was in the method from the start — the same options as drop_duplicates(). The rule mirrors SQL's ORDER BY + LIMIT semantics: without keep='all', a tie at the cutoff silently drops rows, which is exactly what reports want when they say 'bottom 3' — and exactly what surprises people in code review.

Under the hood: quickselect, not a sort

Inside pandas (core/methods/selectn.py), SelectNFrame checks dtypes first, then delegates to the Series implementation, which partitions the column's numpy values instead of sorting them — an O(n) average quickselect that guarantees the n smallest values land on one side of a pivot. Only the n winning rows get re-sorted; the rest of the frame is stitched back with take() using the winning positions. For a multi-column tie-break, SelectNFrame actually runs nsmallest() once per column, narrowing the candidate set at each step. I measured 10M rows: sort_values().head(10) took 2.5s, nsmallest(10) took 0.1s on this machine — that is where the ~24x figure comes from.

Fun facts

Pros

Cons

Takeaways