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-устойчивые агрегаты чтобы считать результат без ручной фильтрации массива.
