Урок курса
NaN в числовых массивах: обнаружение и nan-устойчивые агрегаты
NumPy и pandas: практический тренажёрКак проверить NaN и посчитать значения без пропусков
np.isnan(a) возвращает маску пропусков в числовом массиве. Обычные np.mean и np.sum распространяют NaN; np.nanmean и np.nansum пропускают его при вычислении.
import numpy as np
a = np.array([2.0, np.nan, 6.0])
print(np.isnan(a).tolist())
print(np.mean(a))
print(np.nanmean(a))
print(np.nansum(a))Результат:
[False, True, False]
nan
4.0
8.0NaN нельзя искать сравнением a == np.nan. Если все значения NaN, nanmean возвращает NaN с предупреждением, а nansum — 0. Проверяйте число непропущенных значений, чтобы не принять отсутствие измерений за реальный нулевой результат.
В предыдущих уроках мы работали с чистыми числовыми массивами, где каждый элемент содержал реальное значение. В реальных данных это редкость: измерение могло не сработать, поле не заполнено, датчик выдал ошибку. 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. Именно из-за этого нестандартного поведения для обнаружения пропусков нужна специальная функция, а не операторы сравнения.
Обнаружение NaN: np.isnan, np.any и np.all
Раз сравнение через == не работает, NumPy предоставляет специальную функцию — np.isnan. Она обходит каждый элемент массива и возвращает True ровно там, где стоит NaN:
import numpy as np
arr = np.array([1.0, np.nan, 3.0, np.nan, 5.0])
mask = np.isnan(arr)
print(mask) # [False True False True False]
print(mask.shape) # (5,)
print(mask.dtype) # bool
Результат — булев массив той же формы, что входной. Его можно сразу использовать для индексации: arr[~mask] даст только элементы без NaN.
Есть ли в массиве хотя бы один NaN?
Просматривать булев массив глазами неудобно. Для быстрой проверки используют np.any и np.all.
np.any(mask) возвращает True, если в mask есть хотя бы один True — то есть хотя бы один NaN в исходном массиве:
print(np.any(np.isnan(arr))) # True
np.all(mask) возвращает True только если все элементы mask равны True — то есть весь массив состоит из NaN:
print(np.all(np.isnan(arr))) # False — есть и обычные числа
all_nan = np.array([np.nan, np.nan, np.nan])
print(np.all(np.isnan(all_nan))) # True
Оба вызова принимают булев массив и возвращают скалярный bool — удобно для ветвления в коде:
if np.any(np.isnan(arr)):
print("Массив содержит пропуски")
Комбинация np.isnan + np.any — стандартный способ проверить массив перед агрегацией. Если np.any(np.isnan(arr)) вернул True, нельзя просто взять np.sum или np.mean и получить осмысленный результат — почему именно, разберём в следующем разделе.
Почему стандартные агрегаты возвращают NaN
В арифметике с плавающей точкой NaN обычно распространяется через вычисления: когда NaN участвует в арифметической операции, результат, как правило, тоже NaN. Именно так устроены стандартные агрегирующие функции NumPy — np.sum, np.mean, np.min, np.max. При редукции массива они обрабатывают элементы последовательно, и как только в цепочке встречается NaN, итоговый результат становится NaN:
import numpy as np
arr = np.array([2.0, np.nan, 5.0, 8.0])
print(np.sum(arr)) # nan
print(np.mean(arr)) # nan
print(np.min(arr)) # nan
print(np.max(arr)) # nan
Все четыре вызова возвращают nan, хотя в массиве три вполне конкретных числа — 2.0, 5.0, 8.0.
Почему так? NumPy не знает, чем заменить «отсутствующее» значение. Если просто проигнорировать NaN и посчитать сумму оставшихся чисел, результат будет неверным: 2.0 + 5.0 + 8.0 = 15.0, но это сумма трёх элементов, а не четырёх. Вернуть 15.0 без явного указания пользователя было бы молчаливым искажением данных. Поэтому стандартные агрегаты выбирают честное поведение: «в данных есть неизвестное число — весь результат неизвестен».
Это не баг и не недосмотр. Это осознанное решение, которое заставляет явно думать о пропусках, а не получать «правдоподобные» числа из «грязных» данных незаметно для себя.
На практике это означает: если np.any(np.isnan(arr)) вернул True, то вызовы np.sum, np.mean, np.min, np.max дадут 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-устойчивые агрегаты чтобы считать результат без ручной фильтрации массива.
Задача 1
Дан массив arr = np.array([2.0, np.nan, 4.0]). Что вернёт вызов np.sum(arr)?
Задача 2
Дан массив arr = np.array([np.nan, np.nan, np.nan]). Что вернёт np.any(np.isnan(arr))?
Задача 3
Двумерный массив с NaN: обнаружение пропусков и nan-устойчивые агрегаты по осям
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinдва целых числаRиC— число строк и столбцов матрицы. - Читает следующие
Rстрок; каждая содержит ровноCтокенов через пробел. Токен"nan"означает пропуск, остальные токены — вещественные числа. Создаёт двумерный массивmчерезnp.array(..., dtype=float)формы(R, C). - Вычисляет булеву маску пропусков
mask = np.isnan(m). Выводит одной строкой:any=<bool> all=<bool> count=<int>, где<bool>— результатnp.any(mask)иnp.all(mask)в видеTrue/False, а<int>— количество NaN, вычисленное черезnp.sum(mask). - Вычисляет
np.nansum(m, axis=0)— сумму по столбцам (игнорируя NaN). Выводит значения через пробел, округлённые до 2 знаков. - Вычисляет
np.nanmean(m, axis=1)— среднее по строкам (игнорируя NaN). Выводит значения через пробел, округлённые до 2 знаков. - Вычисляет
np.nanmin(m)иnp.nanmax(m)— глобальный минимум и максимум по всему массиву. Выводит их через пробел, округлённые до 2 знаков.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
R C
токен_00 токен_01 ... токен_0(C-1)
...
токен_(R-1)0 ... токен_(R-1)(C-1)
Токен nan (строчными буквами) обозначает пропуск. Остальные токены — числа, допустимы как целые, так и вещественные.
Формат вывода
Ровно четыре строки:
any=<bool> all=<bool> count=<int>
<nansum по столбцам через пробел>
<nanmean по строкам через пробел>
<nanmin> <nanmax>
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 4
Найди и исправь: обычные агрегаты вместо nan-устойчивых
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.
В редакторе есть сломанный код. Он пытается вычислить четыре агрегата одномерного массива с пропусками, но использует обычные функции np.sum, np.mean, np.min, np.max — из-за этого все результаты равны nan вместо реальных чисел.
Что нужно сделать
Найдите и исправьте все четыре вызова: замените каждую обычную агрегирующую функцию на соответствующую nan-устойчивую (np.nansum, np.nanmean, np.nanmin, np.nanmax).
Программа должна:
- Читать из
stdinодну строку с токенами через пробел. Токенnan— пропуск, остальные — вещественные числа. Создавать одномерный массивarrчерезnp.array(..., dtype=float). - Вычислять и выводить в
stdoutровно четыре строки:
sum: <значение>— nan-устойчивая сумма, округлённая до 2 знаков;mean: <значение>— nan-устойчивое среднее, округлённое до 2 знаков;min: <значение>— nan-устойчивый минимум, округлённый до 2 знаков;max: <значение>— nan-устойчивый максимум, округлённый до 2 знаков.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
токен_0 токен_1 ... токен_n
Токен nan (строчными буквами) обозначает пропуск. Остальные токены — вещественные числа. Гарантируется, что в массиве есть хотя бы одно не-NaN значение.
Формат вывода
Ровно четыре строки:
sum: <значение>
mean: <значение>
min: <значение>
max: <значение>
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 5
Подсчёт NaN, не-NaN и проверка: nansum / count == nanmean
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinодну строку с токенами через пробел. Токенnan(строчными) означает пропуск, остальные — вещественные числа. Создаёт одномерный массивarrчерезnp.array(..., dtype=float). - Вычисляет и выводит число NaN-элементов:
np.sum(np.isnan(arr))— целое число. - Вычисляет и выводит число не-NaN-элементов:
np.sum(~np.isnan(arr))— целое число. - Вычисляет
s = np.nansum(arr)иm = np.nanmean(arr). Выводит оба значения, округлённые до 4 знаков, через пробел. - Проверяет, совпадает ли
round(s / np.sum(~np.isnan(arr)), 4)сround(m, 4). ВыводитTrueилиFalse.
Гарантируется, что в массиве есть хотя бы одно не-NaN значение.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
токен_0 токен_1 ... токен_n
Токен nan (строчными буквами) — пропуск. Остальные токены — вещественные числа. Гарантируется хотя бы одно не-NaN значение.
Формат вывода
Ровно четыре строки:
<число NaN>
<число не-NaN>
<nansum> <nanmean>
<True или False>
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 6
Маска пропусков: np.isnan, np.any, np.all и подсчёт NaN в одномерном массиве
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinодну строку с токенами через пробел. Токенnan(строчными буквами) означает пропуск, остальные токены — вещественные числа. Создаёт одномерный массивarrчерезnp.array(..., dtype=float). - Вычисляет булеву маску
mask = np.isnan(arr). Выводит маску как строку целых чисел0/1через пробел (True → 1, False → 0), затем еёshapeиdtypeв одну строку через пробел. - Выводит строку
any=<bool> all=<bool>, где<bool>— результатnp.any(mask)иnp.all(mask)в видеTrue/False. - Выводит число NaN-элементов, вычисленное через
np.sum(mask), в виде целого числа. - Выводит число не-NaN-элементов, вычисленное через
np.sum(~mask), в виде целого числа.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
токен_0 токен_1 ... токен_n
Токен nan (строчными буквами) — пропуск. Остальные токены — вещественные числа. Гарантируется, что в массиве есть хотя бы одно не-NaN значение и хотя бы один NaN.
Формат вывода
Ровно четыре строки:
<маска 0/1 через пробел> <shape> <dtype>
any=<bool> all=<bool>
<число NaN>
<число не-NaN>
Где <shape> — строковое представление вида (n,), <dtype> — строка bool.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задачи к уроку
- NumPy nanmean, nansum и isnan: среднее без пропусков
Как посчитать NaN и среднее по заполненным значениям NumPy? Пример isnan, nansum и nanmean, затем задача с проверкой кода.
- np.isnan, np.any и np.all: задача на пропуски в массиве
Найдите NaN в массиве, посчитайте пропуски и проверьте условия через np.any и np.all. Задача с онлайн-проверкой.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
