Pull out the smallest few values, without sorting everything else.
Which five countries burn the least coal? Which five products sell worst? You already know how to answer that: sort the column, take the top five. So why does Pandas ship a separate method for it?
Because sorting is the wrong tool for the job in three separate ways. It is slower than it needs to be, it has no opinion about missing values beyond shoving them to one end, and — the one that actually bites — it does not promise you anything about the order of rows that tie. nsmallest fixes all three. It walks the data once, keeps the n smallest values it has seen, and hands them back in order, smallest first. Nothing else in the column ever gets sorted.
It is the mirror image of nlargest, and everything on this page is true of that method with the signs reversed.
Official documentation: Series.nsmallest and DataFrame.nsmallest
The arguments that earn their keep
s.nsmallest(n=5, # how many rows you want back
keep='first') # 'first', 'last', or 'all' — how ties are settled
df.nsmallest(n=5,
columns='Price', # required: the column, or list of columns, to rank by
keep='first')
There are only three arguments, and one of them is the number 5, which is also the default — s.nsmallest() with no arguments at all gives you five rows, and I write it that way in more than one Bamboo Weekly solution.
columns is where the series version and the data frame version part company. A series has one column of values, so there is nothing to name. A data frame has many, so you must say which one to rank by, and it is required rather than optional. Pass a list of names and the later ones break ties in the earlier ones, exactly the way sort_values treats a list.
keep decides what happens when the cutoff lands in the middle of a group of equal values. 'first' and 'last' take whichever tied row appeared earlier or later in the data. 'all' refuses to choose and returns every tied row, even though that means handing back more than n rows. That third option is the one almost nobody knows about, and it is the only honest answer to a lot of questions.
A worked example, on real data
The Global Coal Plant Tracker lists every coal-fired generating unit on earth, one row each. It is the workbook behind Bamboo Weekly #64.
import pandas as pd
url = ('https://www.bambooweekly.com/content/files/wp-content/uploads/2024/02/'
'global-coal-plant-tracker-january-2024.xlsx')
df = pd.read_excel(url, sheet_name='Units',
usecols=['Country', 'Plant name', 'Capacity (MW)',
'Status', 'Start year'])
Which countries have the smallest operating coal fleets? Total the capacity by country, then ask for the bottom five:
capacity = (
df
.loc[lambda df_: df_['Status'] == 'operating']
.groupby('Country')['Capacity (MW)'].sum()
)
capacity.nsmallest(5)
Country
Guadeloupe 64.0
Honduras 105.0
Madagascar 120.0
Namibia 120.0
Senegal 155.0
Name: Capacity (MW), dtype: float64
Look at rows three and four. Madagascar and Namibia both run exactly 120 MW. That is a tie, and it is sitting inside the answer harmlessly — until I ask for three instead of five, at which point the cutoff falls right through it:
capacity.nsmallest(3)
Country
Guadeloupe 64.0
Honduras 105.0
Madagascar 120.0
Name: Capacity (MW), dtype: float64
Namibia has vanished, and nothing in the output tells me it was ever a contender. keep='last' drops Madagascar instead:
capacity.nsmallest(3, keep='last')
Country
Guadeloupe 64.0
Honduras 105.0
Namibia 120.0
Name: Capacity (MW), dtype: float64
Both of those answers are arbitrary, and if the question was "which countries have the three smallest coal fleets," both of them are wrong. keep='all' is the one that tells the truth:
capacity.nsmallest(3, keep='all')
Country
Guadeloupe 64.0
Honduras 105.0
Madagascar 120.0
Namibia 120.0
Name: Capacity (MW), dtype: float64
Four rows from a request for three. That is the point: the method stopped pretending the data had a clean answer.
Now the data frame version, where columns says which column does the ranking and every other column comes along for the ride:
operating = (
df
.loc[lambda df_: df_['Status'] == 'operating',
['Country', 'Plant name', 'Capacity (MW)', 'Start year']]
)
operating.nsmallest(5, columns='Capacity (MW)')
Country Plant name Capacity (MW) Start year
390 Bulgaria Sliven power station 30.0 1969.0
592 China Anhui Xinyuan power station 30.0 1989.0
908 China Changle Shengshi power station 30.0 2009.0
909 China Changle Shengshi power station 30.0 2010.0
910 China Changle Shengshi power station 30.0 2019.0
Five rows, all at 30 MW, and the tie is much worse than it looks:
operating.nsmallest(5, columns='Capacity (MW)', keep='all').shape
(220, 4)
Two hundred and twenty units in this data set are tied for smallest. There is no such thing as "the five smallest operating coal units," and the plain call answered a question that has no answer. A list of columns is the way out — rank by capacity, then break the tie by age:
operating.nsmallest(5, columns=['Capacity (MW)', 'Start year'])
Country Plant name Capacity (MW) Start year
11094 Russia Khabarovsk-1 power station 30.0 1955.0
13873 Zimbabwe Harare power station 30.0 1956.0
7180 Hungary Bakony power station 30.0 1957.0
7181 Hungary Bakony power station 30.0 1957.0
11210 Russia Novosibirsk-4 power station 30.0 1957.0
The oldest of the smallest. Now the five rows mean something.
One more difference worth seeing, this time about missing values. Seventeen of the 107 countries in this file have no start year on record at all — mostly because almost every unit ever listed for them was cancelled. nsmallest does not care:
first_year = df.groupby('Country')['Start year'].min()
first_year.nsmallest(5)
Country
Germany 1927.0
United States 1935.0
India 1939.0
Kazakhstan 1942.0
Russia 1942.0
Name: Start year, dtype: float64
sort_values merely parks the missing values at the far end, which is fine going down and useless coming back up:
first_year.sort_values().tail(5)
Country
Niger NaN
Nigeria NaN
Papua New Guinea NaN
Sudan NaN
Venezuela NaN
Name: Start year, dtype: float64
nlargest(5) on the same series gives you 2026, 2026, 2025, 2024, 2024 — real years, no dropna required. Looking at both ends of a sorted column is the most common reason to reach for these two methods at once.
Finally, speed, since this is the reason the methods exist at all. On the 13,906 rows of this file, nsmallest(5) takes 0.21 ms against 0.45 ms for sort_values().head(5). Stack the column a hundred times to reach 1.39 million rows and the gap widens to 6.8 ms against 57 ms — a bit over eight times faster, because a full sort has to order 1.39 million values to show you five.
Four mistakes people make
Treating it as shorthand for sort_values().head(n). The two agree on the values. They do not always agree on the rows. sort_values defaults to kind='quicksort', which is not stable, so tied rows come back in whatever order the algorithm happened to produce:
operating.nsmallest(5, columns='Capacity (MW)').index
operating.sort_values('Capacity (MW)').head(5).index
Index([390, 592, 908, 909, 910], dtype='int64')
Index([9466, 6014, 6013, 1458, 1459], dtype='int64')
Same data, same question, two disjoint sets of five rows — and the sort_values version quietly picked four units with no recorded start year. nsmallest breaks ties by original position, which is what sort_values(kind='stable') does; ask for that and the two agree exactly.
Forgetting columns on a data frame. On a series nsmallest takes no column name, so the habit carries over and the data frame refuses:
operating.nsmallest(5)
TypeError: DataFrame.nsmallest() missing 1 required positional argument: 'columns'
That is a good error, as errors go — it names the argument you left out. Note also that columns is positional, so df.nsmallest(5, 'Capacity (MW)') works fine. Just remember which is which: the bare number is n, the bare string is columns.
Expecting the tie to be broken the way you would break it. keep='first' does not mean "smallest," "alphabetically first," or "most important." It means "whichever row Pandas met first," which is an artifact of how the file was written. With counts, ratings, integer scores, or rounded numbers, ties are the normal case rather than the exception: add a tiebreaker column, or pass keep='all' and look at what comes back.
Using it on text. nsmallest is arithmetic, not alphabetical, and it says so plainly:
operating['Plant name'].nsmallest(5)
TypeError: Cannot use method 'nsmallest' with dtype str
The data frame form names the offending column: Column 'Plant name' has dtype str, cannot use method 'nsmallest' with this dtype. For the first five names alphabetically, use sort_values and head. The same error also catches a numeric column that was never parsed as numbers — a price column read in as "1,200" strings fails loudly here rather than ranking wrong in silence.
Where it shows up in Bamboo Weekly
Bamboo Weekly #56: Rent increases is the plain case, and shows why nsmallest on a series is so often what you want: after a groupby on metro area, the index holds the place names, so .nsmallest(5) returns the five metros with the smallest rent increases together with their names, in one step.
Bamboo Weekly #62: Economic report card is where I spell the choice out. The IMF inflation figures are full of missing values, so sort_values would mean running dropna first — it is easier in many ways just to run nsmallest. Note the call in that solution: .nsmallest(), no arguments, taking the default five.
Bamboo Weekly #53: Airport animals and Bamboo Weekly #57: International arms trade both use the trick worth stealing. Wanting the biggest and smallest changes looks like two queries, but agg will run both at once if you pass their names as strings:
capacity.agg(['nsmallest', 'nlargest'])
nsmallest nlargest
Country
Guadeloupe 64.0 NaN
Honduras 105.0 NaN
Madagascar 120.0 NaN
Namibia 120.0 NaN
Senegal 155.0 NaN
China NaN 1136731.0
India NaN 237148.2
United States NaN 200090.2
Japan NaN 55123.0
Indonesia NaN 51556.6
One data frame, both ends of the distribution. The NaN values are simply the rows where the two answers do not overlap.
Practice it
Work through an .nsmallest() exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/nsmallest/
Go deeper
The obvious next stop is nlargest, the same method pointed the other way, and sort_values, which is what you want the moment you need the whole column in order rather than a slice of it. The series you are ranking nearly always comes out of groupby or pct_change, and agg is how you ask for both ends at once. To order the index rather than the values, that is sort_index.
How to sort in Pandas covers the neighborhood in a few minutes. More Pandas videos on Python and Pandas with Reuven Lerner.
Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.
Related methods
.nlargest()— when you want the top of the ranking instead of the bottom.sort_index()— when you want the whole frame ordered rather than its bottom few rows
See it on real data
Below are the 16 Bamboo Weekly exercises that use nsmallest on real-world data — try each one, then study the worked solution.
- Bamboo Weekly #177: European Summer
- Bamboo Weekly #176: Religious restrictions
- Bamboo Weekly #175: Inflation
- Bamboo Weekly #169: Press freedom
- Bamboo Weekly #163: Daylight saving time
- Bamboo Weekly #157: Government corruption
- Bamboo Weekly #151: PyPI in 2025
- Bamboo Weekly #148: US Manufacturing
- Bamboo Weekly #139: Chinese exports
- Bamboo Weekly #135: Airline seats
- Bamboo Weekly #129: Tom Lehrer
- Bamboo Weekly #127: European comparisons
- Bamboo Weekly #122: Economic growth
- Bamboo Weekly #117: Electricity
- Bamboo Weekly #62: Economic report card
- Bamboo Weekly #56: Rent increases
Part of the Pandas Methods Index. See also practice by skill.