Missing data: NaN and masked arrays

NumPy · Advanced · 12 min

Real data has gaps; you need to know how they propagate and how to skip them correctly.

NumPy has no dedicated missing-value type for every dtype. The common convention for floats is NaN: any arithmetic with NaN produces NaN, so a single missing value turns a.mean() into nan. That propagation is a feature; it stops gaps from silently skewing results.

To skip missing values deliberately:

  • Use the nan-functions: np.nansum, np.nanmean, np.nanstd, np.nanmax, np.nanargmax, np.nanpercentile. Note that nanmean of an all-NaN slice returns NaN with a RuntimeWarning, and nansum returns 0.
  • Filter first: a[~np.isnan(a)].
  • Fill: np.nan_to_num(a, nan=0.0) or np.where(np.isnan(a), fill, a); for 1-D series, np.interp can interpolate across gaps.

Integer and boolean arrays cannot hold NaN. Putting NaN into an integer array raises, and np.array([1, None]) silently becomes an object array, which is slow. Convert to float or keep a separate mask.

That separate mask is what masked arrays formalise. np.ma.masked_array(data, mask=...) pairs values with a boolean mask, and operations ignore masked entries: m.mean() averages only valid values, and results stay masked where inputs were masked. Helpers include np.ma.masked_invalid (mask NaN/inf), np.ma.masked_where(cond, a), m.filled(value) to get a plain array back, and m.compressed() for the valid values. They work for any dtype, including integers, and keep the original value under the mask.

Masked arrays are slower and less widely supported than plain arrays; many libraries just call np.asarray and drop the mask. In tabular work pandas' nullable dtypes are usually the better answer.

Example

import numpy as np

t = np.array([21.5, np.nan, 23.0, 22.1, np.nan])
print(t.mean(), np.nanmean(t))       # nan 22.2
print(np.isnan(t).sum())             # 2
print(t[~np.isnan(t)])               # [21.5 23.  22.1]

days = np.arange(t.size)
ok = ~np.isnan(t)
print(np.interp(days, days[ok], t[ok]))   # gaps linearly filled

counts = np.array([5, -1, 7, -1, 3])      # -1 = sensor offline
m = np.ma.masked_equal(counts, -1)
print(m)                             # [5 -- 7 -- 3]
print(m.mean(), m.sum())             # 5.0 15
print(m.filled(0))                   # [5 0 7 0 3]

bad = np.ma.masked_invalid(np.array([1.0, np.inf, np.nan, 4.0]))
print(bad.compressed())              # [1. 4.]

Remember

  • NaN propagates through arithmetic; use nan* functions to skip it
  • Integer arrays can't hold NaN; mixing in None makes slow object arrays
  • np.ma pairs data with a mask and works for any dtype
  • filled() and compressed() convert masked arrays back to plain ones

Watch out for

  • Testing for missing values with a == np.nan, which is always False.

Check yourself

  1. `m = np.ma.masked_array([1, 2, 3, 4], mask=[0, 1, 0, 1])`. What is `m.sum()`?
  2. What is `np.nanmax(np.array([np.nan, np.nan]))`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: numpy.org/doc/stable/reference/maskedarray.html

Builds on

Unlocks

Nothing builds on this yet.