NumPy: вычисления и устройство массивов

NaN в числовых массивах: обнаружение и nan-устойчивые агрегаты

Содержание курса

Nan-устойчивые агрегаты: nansum, nanmean, nanmin, nanmax

Четыре функции — np.nansum, np.nanmean, np.nanmin, np.nanmax — делают ровно то, что их названия обещают: считают агрегат, молча пропуская NaN-позиции и работая только с теми элементами, у которых есть реальное значение.

import numpy as np

arr = np.array([2.0, np.nan, 5.0, np.nan, 8.0])

print(np.nansum(arr))   # 15.0
print(np.nanmean(arr))  # 5.0
print(np.nanmin(arr))   # 2.0
print(np.nanmax(arr))   # 8.0

В массиве пять элементов, два из них — NaN. Функции видят только тройку [2.0, 5.0, 8.0]: сумма 15.0, среднее 5.0, минимум 2.0, максимум 8.0. Именно это значение возвращает каждая из них.

Все четыре функции принимают параметр axis — тот же, что у стандартных агрегатов. Если работаете с двумерным массивом и хотите посчитать среднее по строкам или столбцам, синтаксис не меняется:

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

print(np.nanmean(matrix, axis=0))  # среднее по столбцам: [2.5, 5.0, 3.0]
print(np.nanmean(matrix, axis=1))  # среднее по строкам:  [2.0, 4.5]

По axis=0 каждый столбец схлопывается в одно число: первый столбец [1.0, 4.0] → 2.5, второй [nan, 5.0] → 5.0 (NaN проигнорирован), третий [3.0, nan] → 3.0. Механика «схлопывания оси» та же, что у обычных агрегатов.

Граничный случай: все элементы — NaN

Когда в массиве нет ни одного реального значения, поведение функций расходится:

all_nan = np.array([np.nan, np.nan, np.nan])

print(np.nansum(all_nan))   # 0.0  — нет слагаемых, сумма пустого набора = 0
print(np.nanmean(all_nan))  # nan  — среднее неопределено, RuntimeWarning
print(np.nanmin(all_nan))   # nan  — минимум неопределён, RuntimeWarning
print(np.nanmax(all_nan))   # nan  — максимум неопределён, RuntimeWarning

np.nansum возвращает 0.0 — это математически корректно: сумма пустого набора равна нулю. Остальные три функции возвращают nan и выдают RuntimeWarning, потому что среднее, минимум и максимум пустого набора не определены.

Перед вызовом nan-функций стоит проверить, не состоит ли массив целиком из NaN, если это важно для логики:

if np.all(np.isnan(all_nan)):
    print("Нет ни одного валидного значения — агрегат не имеет смысла")
else:
    print(np.nanmean(all_nan))

Вот полная связка — обнаружение и вычисление вместе:

arr = np.array([2.0, np.nan, 5.0, np.nan, 8.0])

# Сначала проверяем, есть ли вообще пропуски
if np.any(np.isnan(arr)):
    nan_count = np.sum(np.isnan(arr))
    print(f"Пропусков: {nan_count} из {arr.size}")

# Считаем только по известным значениям
print("Сумма:",   np.nansum(arr))   # 15.0
print("Среднее:", np.nanmean(arr))  # 5.0
print("Минимум:", np.nanmin(arr))   # 2.0
print("Максимум:",np.nanmax(arr))   # 8.0

np.sum(np.isnan(arr)) здесь работает корректно: булев массив трактуется как массив нулей и единиц, поэтому сумма даёт количество True — то есть количество NaN.

В итоге паттерн работы с пропусками в NumPy выглядит так: сначала np.isnan + np.any чтобы понять, есть ли проблема, затем nan-устойчивые агрегаты чтобы считать результат без ручной фильтрации массива.