The smallest value, and the label of the row it came from.
How bad did it get? That is one question. Where and when did it get that bad? That is a second one, and .min() cannot answer it. .min() hands you a number and throws away everything else about the row. Its partner .idxmin() hands you the index label instead, and once you have a label you can go get the whole row back:
df.loc[df['column'].idxmin()]
That pairing is the whole reason to learn min and idxmin together rather than separately.
.min() shares its keyword arguments — axis, skipna, numeric_only — with the other reducers, and those are explained on mean. What is worth your attention here is that min is happy to work on things that are not numbers. Dates have an order, so the minimum of a date column is the earliest date in your file. Strings have an order too, and that is where the trouble starts.
Official documentation: DataFrame.min and DataFrame.idxmin.
A worked example, on real data
Here is the Bank for International Settlements residential property database, the file behind Bamboo Weekly #181. I am keeping the real, inflation-adjusted series, expressed as a year-on-year percentage change, so every value answers "how much did house prices move in this country over the previous twelve months":
import pandas as pd
url = ('https://www.bambooweekly.com/content/files/2026/07/'
'bw-181-bis-data-1.csv')
prices = (
pd.read_csv(url, low_memory=False,
usecols=['Reference area', 'Value', 'Unit of measure',
'TIME_PERIOD', 'OBS_VALUE'])
.loc[lambda df_: (df_['Value'] == 'Real')
& (df_['Unit of measure']
== 'Year-on-year changes, in per cent')]
.rename(columns={'Reference area': 'country',
'TIME_PERIOD': 'quarter',
'OBS_VALUE': 'change'})
[['country', 'quarter', 'change']]
)
prices['change'].min()
-45.8416
The worst twelve months anywhere in the file saw real house prices fall by nearly 46 percent. Where? .min() will never say. .idxmin() will:
prices['change'].idxmin()
10199
prices.loc[prices['change'].idxmin()]
country Serbia
quarter 2001-Q3
change -45.8416
Name: 10199, dtype: object
Serbia, in the third quarter of 2001. Both methods take the same trip through the column; only one of them remembers where it stopped.
The same pairing inside a groupby gives every country its own worst quarter, with the labels you need to look them up:
(
prices
.groupby('country')['change']
.agg(['count', 'min', 'idxmin'])
.sort_values('min')
.head(6)
)
count min idxmin
country
Serbia 101 -45.8416 10199
Latvia 77 -44.8497 33255
Estonia 81 -41.5677 16392
Hong Kong SAR 182 -40.5393 31541
Lithuania 105 -34.7990 6721
Russia 97 -28.5699 4261
And turning the table sideways asks the question the other way around. Pivot to one column per country, and axis='columns' runs across each row, naming the country that fell furthest in each quarter:
wide = prices.pivot(index='quarter', columns='country', values='change')
wide.idxmin(axis='columns').loc[['2009-Q2', '2023-Q1', '2024-Q3']]
quarter
2009-Q2 Latvia
2023-Q1 Canada
2024-Q3 Hong Kong SAR
dtype: str
Three different crashes, three different countries, one method call.
Three mistakes people make
The minimum of a text column is alphabetical, not shortest or smallest. Ask a whole data frame for its minimum and Pandas answers for every column it can, including the text ones:
prices.min()
country Advanced economies
quarter 1948-Q1
change -45.8416
dtype: object
1948-Q1 is genuinely the earliest quarter, because ISO-style strings sort in date order. Advanced economies is not a country at all. It is the aggregate row the BIS ships alongside the individual countries, and it wins because A sorts first. This is worse for min than for max: aggregate labels tend to begin with words like "Advanced", "Africa" and "All", so they land at the top of an alphabetical sort far more often than at the bottom. Pass numeric_only=True when you mean numbers, and check for aggregate rows before you group.
Reading min as "smallest in size" when the values go negative. Every number in this file is a percentage change, and -45.84 is the minimum because it is the most negative, not because it is the smallest movement. If you want the quarter in which prices barely budged, you want the smallest absolute value — prices['change'].abs().idxmin() — which is a different question with a different answer.
Forgetting that each column's minimum has its own denominator. min skips missing values, and the BIS series start in different decades, so the counts underneath those minima are not comparable:
wide[['Serbia', 'Japan', 'World']].agg(['count', 'min']).round(2)
country Serbia Japan World
count 101.00 280.00 69.0
min -45.84 -16.82 -5.9
Japan's minimum is drawn from 280 quarters, the World aggregate's from 69. Ask for count alongside min and the comparison stays honest.
Where it shows up in Bamboo Weekly
Bamboo Weekly #65: Microplastics is the clearest demonstration of the pairing. .agg(['min', 'max']) on latitude gives -26.238 and 79.96 — the southernmost and northernmost samples — and then .agg(['min', 'idxmin', 'max', 'idxmax']) on the same column, indexed by date, adds that they were taken in 2011 and 2016. Same call, two more strings, and the answer goes from "where" to "where and when." Free to read.
Bamboo Weekly #7: Bank failures is min on dates, which is how I most often use it: df['FAILDATE'].min() returns April 19th, 1934, and tells me at a glance where the FDIC's records begin. Free to read.
Bamboo Weekly #51: Academy Awards puts min and max in one agg per performer and subtracts them, which turns two extremes into a career span. Robert De Niro leads with 49 years between his first and last acting nod, Katharine Hepburn follows with 48. Free to read.
Bamboo Weekly #71: Holidays uses groupby(['country', 'holiday']).idxmin() on a date-indexed frame to find, for every country-and-holiday pair, the first date it was ever celebrated — which is idxmin doing something min structurally cannot. Free to read.
Practice it
Work through a min exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/min/
Go deeper
max, min, idxmax and idxmin is the full treatment of the pair, including the difference between a label and a position, and what happens when values tie. idxmin is the "where" half on its own. If you want the bottom few rather than the bottom one, that is nsmallest, which beats sorting the whole frame with sort_values. And since every idxmin ends in a lookup, loc is the method to be fluent in first.
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
.idxmin()— when you want to know which row holds the smallest value.max()— for the top of the range rather than the bottom
See it on real data
Below are the 9 Bamboo Weekly exercises that use min on real-world data — try each one, then study the worked solution.
- Bamboo Weekly #177: European Summer
- Bamboo Weekly #173: IPOs
- Bamboo Weekly #145: Economic indicators
- Bamboo Weekly #142: Hurricanes
- Bamboo Weekly #115: Sahm rule
- Bamboo Weekly #100: Sports betting
- Bamboo Weekly #56: Rent increases
- Bamboo Weekly #45: Netflix
- Bamboo Weekly #7: Bank failures
Part of the Pandas Methods Index. See also practice by skill.