Skip to content

pandas std

How far the values sit from their mean — and which of the two formulas you just used.

How spread out is this? Two columns can share a mean and have nothing else in common, and .std() is the one-number answer to the difference. It is the square root of the average squared distance from the mean, in the same units as the data, so a standard deviation of 3 percentage points on an inflation column means percentage points you can reason about.

What earns std its own page is the word "average" in that sentence. Average over what — the number of values, or one less than that? Pandas answers n - 1. NumPy answers n. Neither is wrong, both are defaults, and they are different defaults.

Official documentation: DataFrame.std and Series.std.

The argument that earns its keep

df.std(ddof=1)   # sample standard deviation — the Pandas default
df.std(ddof=0)   # population standard deviation — the NumPy default

ddof is "delta degrees of freedom," and it is subtracted from the count before dividing. ddof=1 treats your rows as a sample drawn from a larger population and divides by n - 1, which keeps the estimate from coming out too small. ddof=0 treats your rows as the whole population and divides by n. On Pandas 3.0.5 the signature reads std(axis=0, skipna=True, ddof=1, numeric_only=False), so if you have never passed ddof you have been computing sample standard deviations. numpy.std takes the same argument and defaults it to 0. The other keyword arguments are shared with the rest of the reducers and are covered on mean.

A worked example, on real data

Here is OECD consumer price inflation, the file behind Bamboo Weekly #175 — one row per country per month, each value a year-on-year percentage change, from January 2020 on:

import pandas as pd

url = 'https://www.bambooweekly.com/content/files/2026/06/bw-175-oecd.csv'

cpi = (
    pd.read_csv(url, low_memory=False,
                usecols=['Reference area', 'Expenditure',
                         'TIME_PERIOD', 'OBS_VALUE'])
    .loc[lambda df_: df_['Expenditure'] == 'Total']
    .rename(columns={'Reference area': 'country', 'TIME_PERIOD': 'month',
                     'OBS_VALUE': 'inflation'})
    [['country', 'month', 'inflation']]
    .loc[lambda df_: df_['month'] >= '2020-01']
)

spread = cpi.groupby('country')['inflation'].agg(['count', 'mean', 'std'])

Sorted the two ways, that gives the countries whose inflation moved most and least:

spread.sort_values('std', ascending=False).head(5).round(2)
           count   mean    std
country
Türkiye       76  41.49  22.63
Lithuania     77   6.45   7.13
Estonia       77   6.67   6.89
Latvia        77   5.67   6.87
Hungary       77   7.70   6.84
spread.sort_values('std').head(5).round(2)
              count  mean   std
country
Austria           4  2.70  0.70
Switzerland      77  0.97  1.27
Saudi Arabia     77  2.44  1.54
Japan            76  1.88  1.56
South Africa     76  4.68  1.64

Switzerland ran at about 1 percent inflation and stayed there, wobbling by 1.3 points. Türkiye averaged 41 percent with a standard deviation of 22.6 — the spread is half the level. One column, and std separates quiet from loud in a way mean never can.

Austria sits at the top of the calm list and does not belong there. Look at the count column: four values, because this file carries Austria's national CPI for only four months of 2026 where the others have six years. Four observations is also where ddof stops being a footnote:

austria = cpi.loc[lambda df_: df_['country'] == 'Austria', 'inflation']

austria.to_list()
[3.4, 2.0, 2.2, 3.2]
austria.std()        # 0.7023769168568492
austria.std(ddof=0)  # 0.6082762530298219
import numpy as np

np.std(austria.to_numpy())  # 0.6082762530298219

Same four numbers, two answers 15 percent apart, and NumPy's matches the one Pandas does not hand you. The ratio is exactly the square root of 4/3: dividing by 3 instead of 4 inflates the result by 15 percent. Do the same on Türkiye's 76 months and the two come out 22.633 and 22.483, a difference of 0.7 percent — which is why nobody notices this until they hit a small group.

Three mistakes people make

Assuming Pandas and NumPy agree. They do not, and neither warns you. When a Pandas figure will not match one from NumPy, scikit-learn or a colleague's notebook, check the ddof before you go looking for a bug — and say which one you used when you publish a number.

Expecting std on a single row to be zero. One value has no spread, so zero feels like the answer. With ddof=1 the denominator is n - 1, which is zero, and Pandas returns NaN rather than dividing by it:

one_month = cpi.loc[lambda df_: (df_['country'] == 'Türkiye')
                                & (df_['month'] == '2022-10')]

one_month.std(numeric_only=True)
inflation   NaN
dtype: float64
one_month.std(numeric_only=True, ddof=0)
inflation    0.0
dtype: float64

This bites hardest inside a groupby that is finer than you meant it to be. Group this frame by country and month and every group holds exactly one row:

cpi.groupby(['country', 'month'])['inflation'].std().head(4)
country  month
Austria  2026-01   NaN
         2026-02   NaN
         2026-03   NaN
         2026-04   NaN
Name: inflation, dtype: float64

A column of NaN is Pandas telling you your grouping keys are too specific, not that your data is missing.

Reading a standard deviation without its count. std skips missing values like the rest of the family, so each column's spread rests on however many values that column happened to have. Austria's 0.70 sits in the table above looking directly comparable to Switzerland's 1.27, and it rests on four months against seventy-seven. Ask agg for count next to std every time.

Where it shows up in Bamboo Weekly

Bamboo Weekly #40: Sovereign bonds uses std as a threshold rather than as a reported number, which is how it most often earns its place. rates_df['Rate'].mean() + rates_df['Rate'].std() defines "an unusually high interest rate," and one .loc[] keeps the countries above it — Argentina and Turkey among them. Free to read.

Bamboo Weekly #88: Hot summers pulls one region out of a MultiIndex with xs and runs .agg(['mean', 'std']) on the temperature anomaly: a Caribbean mean of 0.9516 degrees above normal with a standard deviation of 0.1588. A tight spread on an anomaly column is the interesting part — it means the whole region warmed together.

Bamboo Weekly #167: Oil prices chains .pct_change().resample('1ME').std() to turn daily futures prices into monthly volatility, then pipes the result straight into a Plotly line chart. The plot is not of the price but of how much the price is moving, which is a question only std answers.

Practice it

Work through a std exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/std/

Go deeper

mean is the point this measures the distance from, and agg gets you both plus the count in one call. describe prints std in its eight-row survey, and that row is ddof=1 too. When the spread is not symmetric, a standard deviation flatters it and quantile tells the truth instead. A distribution plot shows at a glance what one spread number can only hint at.

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.

See it on real data

Below are the 3 Bamboo Weekly exercises that use std on real-world data — try each one, then study the worked solution.

Part of the Pandas Methods Index. See also practice by skill.