How to Calculate Standard Deviation in NumPy

Standard deviation measures how spread out your data is from the mean. A low standard deviation means values cluster tightly around the average; a high standard deviation indicates they're scattered...

Key Insights

  • NumPy’s np.std() calculates population standard deviation by default; use ddof=1 for sample standard deviation when working with data samples rather than complete populations.
  • The axis parameter lets you calculate standard deviation along specific dimensions of multi-dimensional arrays, essential for analyzing datasets with rows and columns.
  • Use np.nanstd() instead of np.std() when your data contains missing values to avoid propagating NaN through your calculations.

Introduction

Standard deviation measures how spread out your data is from the mean. A low standard deviation means values cluster tightly around the average; a high standard deviation indicates they’re scattered widely. It’s one of the most fundamental statistical measures you’ll use in data analysis.

You’ll reach for standard deviation when you need to understand variability in your data. Are your website’s response times consistent, or do they swing wildly? How volatile is a stock’s price compared to another? Are test scores in one class more uniform than another? These questions all require measuring spread, and standard deviation gives you a single number to quantify it.

NumPy makes this calculation trivial. Instead of manually computing the mean, squaring differences, averaging them, and taking the square root, you call one function. But there are nuances—population versus sample calculations, handling multiple dimensions, dealing with missing data—that trip up developers who don’t understand what’s happening under the hood.

Quick Start: numpy.std() Basics

The simplest usage of numpy.std() takes an array and returns the standard deviation:

import numpy as np

data = np.array([1, 2, 3, 4, 5])
std_dev = np.std(data)
print(std_dev)  # Output: 1.4142135623730951

That’s it. Pass your data, get the standard deviation. The function works with Python lists too—NumPy converts them internally:

std_dev = np.std([10, 20, 30, 40, 50])
print(std_dev)  # Output: 14.142135623730951

For context on what this number means: the mean of [1, 2, 3, 4, 5] is 3. A standard deviation of ~1.41 tells you that, on average, values deviate from the mean by about 1.41 units. In a normal distribution, roughly 68% of values fall within one standard deviation of the mean.

You can also call std() as a method on NumPy arrays:

data = np.array([1, 2, 3, 4, 5])
print(data.std())  # Output: 1.4142135623730951

Both approaches are equivalent. Use whichever reads better in your code.

Population vs. Sample Standard Deviation

Here’s where most tutorials gloss over a critical detail. NumPy’s default behavior calculates population standard deviation, which assumes your data represents the entire population you care about. But if your data is a sample from a larger population—which is almost always the case in real-world analysis—you need sample standard deviation.

The difference comes down to the denominator. Population standard deviation divides by N (the number of values). Sample standard deviation divides by N-1, applying what’s called Bessel’s correction to reduce bias when estimating population variance from a sample.

NumPy controls this with the ddof parameter (delta degrees of freedom):

data = np.array([2, 4, 4, 4, 5, 5, 7, 9])

# Population standard deviation (ddof=0, the default)
pop_std = np.std(data)
print(f"Population std: {pop_std}")  # Output: 2.0

# Sample standard deviation (ddof=1)
sample_std = np.std(data, ddof=1)
print(f"Sample std: {sample_std}")  # Output: 2.138089935299395

The sample standard deviation is always larger because dividing by a smaller number (N-1 instead of N) produces a bigger result.

When to use which:

  • Use ddof=0 (default) when your data is the complete population—every single data point that exists.
  • Use ddof=1 when your data is a sample and you’re estimating the population’s standard deviation.

In practice, you’ll use ddof=1 far more often. If you’re analyzing survey responses, experimental measurements, or any subset of possible data, that’s a sample. The only time you’d use population standard deviation is when you have truly exhaustive data—like the test scores of every student in a specific class, with no intent to generalize beyond that class.

# Real-world example: analyzing a sample of customer wait times
wait_times = np.array([3.2, 4.1, 2.8, 5.5, 3.9, 4.7, 3.1, 4.4])

# This is a sample, not every customer ever, so use ddof=1
std_wait = np.std(wait_times, ddof=1)
print(f"Sample std of wait times: {std_wait:.2f} minutes")
# Output: Sample std of wait times: 0.88 minutes

Working with Multi-Dimensional Arrays

Real datasets aren’t flat lists. You’ll often work with 2D arrays where rows represent observations and columns represent variables (or vice versa). The axis parameter controls which dimension NumPy calculates standard deviation along.

# Test scores: 4 students, 3 exams each
scores = np.array([
    [85, 90, 88],   # Student 1
    [78, 82, 80],   # Student 2
    [92, 95, 91],   # Student 3
    [70, 75, 73]    # Student 4
])

# Standard deviation across ALL values (flattened)
total_std = np.std(scores, ddof=1)
print(f"Overall std: {total_std:.2f}")  # Output: Overall std: 8.23

# Standard deviation for each exam (down columns, axis=0)
exam_std = np.std(scores, axis=0, ddof=1)
print(f"Std per exam: {exam_std}")  # Output: Std per exam: [9.25 8.54 7.68]

# Standard deviation for each student (across rows, axis=1)
student_std = np.std(scores, axis=1, ddof=1)
print(f"Std per student: {student_std}")  # Output: Std per student: [2.52 2.   2.08 2.52]

Think of axis=0 as collapsing rows (computing down each column) and axis=1 as collapsing columns (computing across each row). The result’s shape loses the dimension you specified.

This becomes powerful when analyzing datasets:

# Monthly sales data: 12 months x 5 products
sales = np.random.randint(100, 500, size=(12, 5))

# Which product has the most volatile sales?
product_volatility = np.std(sales, axis=0, ddof=1)
most_volatile = np.argmax(product_volatility)
print(f"Product {most_volatile + 1} has highest sales volatility")

# Which month had the most uneven sales across products?
month_spread = np.std(sales, axis=1, ddof=1)
uneven_month = np.argmax(month_spread)
print(f"Month {uneven_month + 1} had most uneven product performance")

Handling Missing Data (NaN Values)

Missing data is inevitable. Sensors fail, users skip survey questions, records get corrupted. NumPy represents missing values as NaN (Not a Number), and the default np.std() propagates NaN through calculations:

data_with_nan = np.array([1, 2, np.nan, 4, 5])

# Regular std() returns NaN if any value is NaN
result = np.std(data_with_nan)
print(result)  # Output: nan

Use np.nanstd() to ignore NaN values:

data_with_nan = np.array([1, 2, np.nan, 4, 5])

# nanstd() ignores NaN values
result = np.nanstd(data_with_nan, ddof=1)
print(result)  # Output: 1.8257418583505538

# Equivalent to removing NaN and calculating
clean_data = np.array([1, 2, 4, 5])
print(np.std(clean_data, ddof=1))  # Output: 1.8257418583505538

The nanstd() function accepts all the same parameters as std():

# 2D array with missing values
data = np.array([
    [1.0, 2.0, np.nan],
    [4.0, np.nan, 6.0],
    [7.0, 8.0, 9.0]
])

# Std per column, ignoring NaN
col_std = np.nanstd(data, axis=0, ddof=1)
print(f"Column std (ignoring NaN): {col_std}")
# Output: Column std (ignoring NaN): [3.   4.24 2.12]

Important caveat: nanstd() adjusts the count used in calculations. If a column has 3 values but one is NaN, it calculates standard deviation using N=2. This is usually what you want, but be aware that columns with many missing values will have less reliable statistics.

Practical Example: Analyzing Real Data

Let’s put everything together with a realistic scenario. You’re analyzing daily stock returns for a portfolio and need to understand the volatility of each asset.

import numpy as np

# Simulated daily returns (%) for 4 stocks over 20 trading days
# In practice, you'd load this from a CSV or API
np.random.seed(42)
returns = np.array([
    np.random.normal(0.1, 1.5, 20),   # Stock A: moderate volatility
    np.random.normal(0.05, 0.8, 20),  # Stock B: low volatility
    np.random.normal(0.15, 2.5, 20),  # Stock C: high volatility
    np.random.normal(0.08, 1.2, 20),  # Stock D: moderate volatility
]).T  # Transpose so rows=days, columns=stocks

# Introduce some missing data (simulating days when a stock didn't trade)
returns[5, 1] = np.nan
returns[12, 2] = np.nan

stock_names = ['Stock A', 'Stock B', 'Stock C', 'Stock D']

# Calculate volatility (std of returns) for each stock
volatility = np.nanstd(returns, axis=0, ddof=1)

print("Portfolio Volatility Analysis")
print("-" * 40)
for name, vol in zip(stock_names, volatility):
    print(f"{name}: {vol:.2f}% daily volatility")

# Find the most and least volatile stocks
most_volatile_idx = np.argmax(volatility)
least_volatile_idx = np.argmin(volatility)

print(f"\nMost volatile: {stock_names[most_volatile_idx]}")
print(f"Least volatile: {stock_names[least_volatile_idx]}")

# Calculate overall portfolio volatility (assuming equal weights)
# This is simplified; real portfolio volatility considers correlations
portfolio_returns = np.nanmean(returns, axis=1)
portfolio_vol = np.nanstd(portfolio_returns, ddof=1)
print(f"\nEqual-weight portfolio volatility: {portfolio_vol:.2f}%")

# Identify days with unusually high spread across stocks
daily_spread = np.nanstd(returns, axis=1, ddof=1)
high_spread_days = np.where(daily_spread > np.nanmean(daily_spread) + np.nanstd(daily_spread, ddof=1))[0]
print(f"\nDays with unusually high cross-stock spread: {high_spread_days}")

This example demonstrates the key patterns: using ddof=1 for sample data, applying axis to aggregate across the right dimension, and using nanstd() to handle missing values gracefully.

Summary and Key Takeaways

Here’s your quick reference for calculating standard deviation in NumPy:

Function Purpose
np.std(data) Population standard deviation
np.std(data, ddof=1) Sample standard deviation
np.std(data, axis=0) Std along columns
np.std(data, axis=1) Std along rows
np.nanstd(data) Std ignoring NaN values

For most real-world analysis, your default should be np.nanstd(data, ddof=1). This handles missing values and uses the sample formula appropriate for data that represents a subset of a larger population. Only deviate from this when you have specific reasons—complete population data or guaranteed clean data.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.