How to Handle NaN Values in NumPy

NaN—Not a Number—is NumPy's standard representation for missing or undefined numerical data. You'll encounter NaN values when importing datasets with gaps, performing invalid mathematical operations...

Key Insights

  • NaN values propagate through calculations silently, corrupting your results unless you explicitly handle them with detection, removal, or replacement strategies.
  • NumPy’s nan* functions (np.nansum(), np.nanmean(), etc.) provide the cleanest way to perform calculations while ignoring missing data without modifying your original array.
  • Never use == to check for NaN—it always returns False due to IEEE 754 floating-point semantics. Always use np.isnan() instead.

Introduction to NaN in NumPy

NaN—Not a Number—is NumPy’s standard representation for missing or undefined numerical data. You’ll encounter NaN values when importing datasets with gaps, performing invalid mathematical operations (like dividing zero by zero), or explicitly marking data as missing.

The problem with NaN is its infectious nature. A single NaN in your calculation contaminates the entire result:

import numpy as np

data = np.array([1.0, 2.0, np.nan, 4.0, 5.0])
print(np.mean(data))  # Output: nan

That nan output isn’t a bug—it’s NumPy telling you that your result is meaningless because the input contained undefined values. Ignoring this leads to silent data corruption that propagates through your entire analysis pipeline.

Proper NaN handling isn’t optional. It’s the difference between trustworthy results and garbage outputs that look legitimate.

Detecting NaN Values

Before you can handle NaN values, you need to find them. NumPy provides several functions for detection, each serving different purposes.

Basic Detection with np.isnan()

The np.isnan() function returns a boolean array indicating which elements are NaN:

import numpy as np

data = np.array([1.0, np.nan, 3.0, np.nan, 5.0])

# Create boolean mask
nan_mask = np.isnan(data)
print(nan_mask)  # [False  True False  True False]

# Count NaN values
nan_count = np.sum(np.isnan(data))
print(f"NaN count: {nan_count}")  # NaN count: 2

# Find indices of NaN values
nan_indices = np.where(np.isnan(data))[0]
print(f"NaN at indices: {nan_indices}")  # NaN at indices: [1 3]

# Check if any NaN exists
has_nan = np.any(np.isnan(data))
print(f"Contains NaN: {has_nan}")  # Contains NaN: True

Working with 2D Arrays

For multidimensional arrays, you often need to identify which rows or columns contain NaN:

matrix = np.array([
    [1.0, 2.0, 3.0],
    [4.0, np.nan, 6.0],
    [7.0, 8.0, 9.0],
    [np.nan, 11.0, 12.0]
])

# Find rows containing NaN
rows_with_nan = np.any(np.isnan(matrix), axis=1)
print(f"Rows with NaN: {np.where(rows_with_nan)[0]}")  # [1 3]

# Find columns containing NaN
cols_with_nan = np.any(np.isnan(matrix), axis=0)
print(f"Columns with NaN: {np.where(cols_with_nan)[0]}")  # [0 1]

Using np.isfinite() for Broader Checks

When you need to catch both NaN and infinity values, use np.isfinite():

data = np.array([1.0, np.nan, np.inf, -np.inf, 5.0])

finite_mask = np.isfinite(data)
print(finite_mask)  # [ True False False False  True]

Removing NaN Values

Sometimes the cleanest solution is to remove NaN values entirely. The approach differs based on your array’s dimensionality.

Filtering 1D Arrays

For one-dimensional arrays, use boolean indexing with the negated NaN mask:

data = np.array([1.0, np.nan, 3.0, np.nan, 5.0, 6.0])

# Remove NaN values
clean_data = data[~np.isnan(data)]
print(clean_data)  # [1. 3. 5. 6.]

Removing Rows or Columns from 2D Arrays

For matrices, you typically want to remove entire rows or columns containing NaN:

matrix = np.array([
    [1.0, 2.0, 3.0],
    [4.0, np.nan, 6.0],
    [7.0, 8.0, 9.0],
    [np.nan, 11.0, 12.0]
])

# Remove rows containing any NaN
clean_rows = matrix[~np.any(np.isnan(matrix), axis=1)]
print("Rows without NaN:")
print(clean_rows)
# [[1. 2. 3.]
#  [7. 8. 9.]]

# Remove columns containing any NaN
clean_cols = matrix[:, ~np.any(np.isnan(matrix), axis=0)]
print("Columns without NaN:")
print(clean_cols)
# [[ 3.]
#  [ 6.]
#  [ 9.]
#  [12.]]

Be cautious with removal—you’re discarding data. For small datasets or when NaN values are sparse, this can significantly reduce your sample size.

Replacing NaN with Values

Replacement preserves your array’s shape while substituting NaN with meaningful values. NumPy offers several approaches.

Using np.nan_to_num()

The simplest replacement uses np.nan_to_num(), which replaces NaN with zero by default:

data = np.array([1.0, np.nan, 3.0, np.nan, 5.0])

# Replace NaN with zero
zero_filled = np.nan_to_num(data)
print(zero_filled)  # [1. 0. 3. 0. 5.]

# Replace NaN with custom value
custom_filled = np.nan_to_num(data, nan=-999.0)
print(custom_filled)  # [1. -999. 3. -999. 5.]

Using np.where() for Conditional Replacement

For more control, np.where() lets you specify replacement logic:

data = np.array([1.0, np.nan, 3.0, np.nan, 5.0])

# Replace NaN with a specific value
filled = np.where(np.isnan(data), 0.0, data)
print(filled)  # [1. 0. 3. 0. 5.]

Replacing with Statistical Values

A common strategy replaces NaN with the mean or median of the non-NaN values:

data = np.array([1.0, np.nan, 3.0, np.nan, 5.0, 7.0])

# Calculate mean of non-NaN values
mean_value = np.nanmean(data)
print(f"Mean: {mean_value}")  # Mean: 4.0

# Replace NaN with mean
mean_filled = np.where(np.isnan(data), mean_value, data)
print(mean_filled)  # [1. 4. 3. 4. 5. 7.]

# Replace NaN with median
median_value = np.nanmedian(data)
median_filled = np.where(np.isnan(data), median_value, data)
print(median_filled)  # [1. 4. 3. 4. 5. 7.]

Direct Assignment with Boolean Indexing

You can also modify arrays in place:

data = np.array([1.0, np.nan, 3.0, np.nan, 5.0])

# Modify in place
data[np.isnan(data)] = 0.0
print(data)  # [1. 0. 3. 0. 5.]

This approach mutates the original array. Use data.copy() first if you need to preserve the original.

NaN-Safe Mathematical Operations

NumPy provides a family of functions prefixed with nan that automatically ignore NaN values during computation. These are your best tools for working with incomplete data.

Comparing Regular vs. NaN-Safe Functions

data = np.array([1.0, 2.0, np.nan, 4.0, 5.0, np.nan, 7.0])

# Regular functions return NaN
print(f"np.sum():  {np.sum(data)}")    # nan
print(f"np.mean(): {np.mean(data)}")   # nan
print(f"np.std():  {np.std(data)}")    # nan
print(f"np.max():  {np.max(data)}")    # nan

print("---")

# NaN-safe functions ignore NaN
print(f"np.nansum():  {np.nansum(data)}")    # 19.0
print(f"np.nanmean(): {np.nanmean(data)}")   # 3.8
print(f"np.nanstd():  {np.nanstd(data)}")    # 2.04...
print(f"np.nanmax():  {np.nanmax(data)}")    # 7.0

Available NaN-Safe Functions

NumPy provides these NaN-ignoring functions:

data = np.array([2.0, np.nan, 4.0, 1.0, np.nan, 5.0, 3.0])

# Aggregation functions
print(f"nansum:    {np.nansum(data)}")      # 15.0
print(f"nanprod:   {np.nanprod(data)}")     # 120.0
print(f"nanmean:   {np.nanmean(data)}")     # 3.0
print(f"nanmedian: {np.nanmedian(data)}")   # 3.0
print(f"nanstd:    {np.nanstd(data)}")      # 1.414...
print(f"nanvar:    {np.nanvar(data)}")      # 2.0
print(f"nanmin:    {np.nanmin(data)}")      # 1.0
print(f"nanmax:    {np.nanmax(data)}")      # 5.0

# Cumulative functions
print(f"nancumsum:  {np.nancumsum(data)}")  # [ 2.  2.  6.  7.  7. 12. 15.]
print(f"nancumprod: {np.nancumprod(data)}") # [ 2.  2.  8.  8.  8. 40. 120.]

# Percentile and quantile
print(f"nanpercentile(50): {np.nanpercentile(data, 50)}")  # 3.0

These functions treat NaN as “missing” rather than “invalid,” which is usually what you want for statistical analysis.

Common Pitfalls and Best Practices

The NaN Equality Trap

The most common mistake is testing for NaN with equality operators:

value = np.nan

# This NEVER works
print(value == np.nan)      # False
print(np.nan == np.nan)     # False

# This is correct
print(np.isnan(value))      # True

This behavior comes from IEEE 754 floating-point standards. NaN is defined as not equal to anything, including itself. Always use np.isnan().

NaN Requires Float dtype

NaN only exists in floating-point arrays. Integer arrays cannot contain NaN:

# This silently converts to integer (loses NaN)
int_array = np.array([1, np.nan, 3], dtype=np.int64)
print(int_array)  # May produce unexpected results or error

# Always use float for data that might contain NaN
float_array = np.array([1, np.nan, 3], dtype=np.float64)
print(float_array)  # [ 1. nan  3.]

If you’re reading data that might have missing values, explicitly specify a float dtype.

When to Use Masked Arrays

For complex NaN handling, consider NumPy’s masked arrays:

data = np.array([1.0, 2.0, -999.0, 4.0, -999.0])

# Create masked array where -999 indicates missing
masked = np.ma.masked_equal(data, -999.0)
print(masked)        # [1.0 2.0 -- 4.0 --]
print(masked.mean()) # 2.333...

Masked arrays are useful when:

  • You have multiple sentinel values indicating missing data
  • You need to temporarily mask values without modifying the array
  • You’re working with data where NaN isn’t appropriate (like integers)

Conclusion

Handling NaN values in NumPy comes down to three strategies: detect, remove, or replace. Use np.isnan() for detection, boolean indexing for removal, and np.where() or np.nan_to_num() for replacement.

For calculations, prefer the nan* family of functions. They’re cleaner than filtering data before computation and don’t modify your original arrays.

Choose your approach based on context:

  • Remove NaN when missing data would invalidate your analysis and you have sufficient remaining samples
  • Replace with mean/median when you need to preserve array shape and the replacement is statistically reasonable
  • Use nan-safe functions when you want to compute statistics without modifying data
  • Replace with zero only when zero is a meaningful default in your domain

The worst thing you can do is ignore NaN values. They’ll silently corrupt your results, and you won’t know until the damage is done.

Liked this? There's more.

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