NaN в числовых массивах: обнаружение и nan-устойчивые агрегаты
Содержание курса
В предыдущих уроках мы работали с чистыми числовыми массивами, где каждый элемент содержал реальное значение. В реальных данных это редкость: измерение могло не сработать, поле не заполнено, датчик выдал ошибку. NumPy представляет такие «отсутствующие» числа через специальное значение NaN — и прежде чем научиться с ним работать, нужно понять, что оно собой представляет.
NaN как специальное значение float: природа и ограничения
NaN расшифровывается как Not a Number. Это не ошибка выполнения и не исключение — это легальное значение в стандарте IEEE 754, которое сигнализирует: «здесь должно быть число, но его нет».
В NumPy NaN вводится как np.nan. Это обычный Python-объект типа float:
import numpy as np
print(type(np.nan)) # <class 'float'>
Почему для числовых данных с пропусками используют float-массивы
Целочисленный dtype (int32, int64 и т.д.) не имеет зарезервированного битового паттерна для «отсутствующего значения» — каждая битовая комбинация уже занята конкретным числом. Float-формат устроен иначе: в нём специально отведены паттерны для бесконечностей и NaN.
Поэтому integer-массив не может хранить NaN без смены dtype. Если передать np.nan в такой массив, NumPy либо выбросит ошибку, либо автоматически повысит dtype до float64:
arr_int = np.array([1, 2, 3])
print(arr_int.dtype) # int64
arr_with_nan = np.array([1.0, np.nan, 3.0])
print(arr_with_nan.dtype) # float64
Если у вас есть целочисленный массив и нужно добавить NaN, сначала приведите его к float:
arr_float = arr_int.astype(float)
arr_float[1] = np.nan
print(arr_float) # [ 1. nan 3.]
print(arr_float.dtype) # float64
Технически NumPy допускает NaN и в complex-, и в object-массивах, но в практике числового анализа стандартный выбор — float64. Именно с ним мы будем работать в этом уроке.
Ключевое свойство: NaN не равен ничему, включая себя
Это самое неочевидное в NaN. По стандарту IEEE 754 поведение сравнений с NaN устроено так:
==с NaN всегда даётFalse— NaN не равен ни одному числу и даже самому себе;!=с NaN всегда даётTrue— NaN «не равен» всему, включая себя;- упорядочивающие сравнения
<,>,<=,>=с NaN даютFalse.
print(np.nan == np.nan) # False
print(np.nan != np.nan) # True
print(np.nan < 0) # False
print(np.nan > 0) # False
На уровне массива это означает, что наивная попытка найти пропуски через == не сработает:
arr = np.array([1.0, np.nan, 3.0])
print(arr == np.nan) # [False False False] — ни один элемент не найден!
Все три позиции дают False, хотя вторая — NaN. Именно из-за этого нестандартного поведения для обнаружения пропусков нужна специальная функция, а не операторы сравнения.
