Булева индексация и маски: фильтрация данных в NumPy
Достаём из массива только нужное: булевы маски, операторы & | ~ вместо and/or, np.where в двух ролях, argmax и честная чистка выбросов в данных датчика.
Редакция Питоники
В логах датчика температуры за неделю — десять тысяч значений, и где-то среди них сбой: вместо 22 градусов прибор записал 87. Просматривать глазами — час. Писать цикл с if — скучно. В NumPy булева индексация решает задачу строкой, которая читается почти как человеческий вопрос: данные[данные > 25]. Это приём, которым вы будете пользоваться чаще любого другого в NumPy — и у которого больше всего сюрпризов: попробуйте склеить два условия через and, и NumPy ответит загадочной фразой про «The truth value of an array». Разберём всё по порядку — от простой маски до чистки выбросов.
Сравнение с числом даёт булеву маску
Начнём с фундамента. Любое сравнение массива с числом — >, <, >=, <=, ==, != — NumPy выполняет со всеми элементами сразу (та же векторизация, что и в арифметике массивов) и возвращает массив из True и False. Такой массив называют булевой маской: он той же формы, что и данные, и в каждой позиции отвечает на вопрос «подходит ли это значение?».
import numpy as np
temps = np.array([18.2, 21.5, 24.8, 19.9, 27.3, 16.4, 30.1])
hot = temps > 20 # сравнение по всем элементам сразу
print(hot)
print(hot.dtype)
[False True True False True False True] bool
Обратите внимание на dtype: bool — отдельный тип с двумя значениями. NumPy хранит его очень плотно, по одному байту на элемент, и обрабатывает в скомпилированном коде, поэтому маски на миллионах значений стоят копейки. Маска — обычный ndarray: у неё можно спросить shape, её можно напечатать, присвоить переменной и переиспользовать в нескольких фильтрах.
Булева индексация: data[mask] и фильтрация в одну строку
Магия начинается, когда маску подставляют в квадратные скобки вместо индекса. NumPy отдаёт те элементы, где маска равна True, — и только их. Читается почти по-русски: «температуры, где температура больше двадцати».
import numpy as np
temps = np.array([18.2, 21.5, 24.8, 19.9, 27.3, 16.4, 30.1])
print(temps[temps > 20]) # только тёплые дни
print(temps[(temps > 20) & (temps < 28)]) # тёплые, но не жаркие
[21.5 24.8 27.3 30.1] [21.5 24.8 27.3]
Маску можно и не сохранять в переменную — запись temps[temps > 20] вполне законна, хотя с именованной маской код читается лучше. Две детали результата. Первая: булева индексация всегда возвращает одномерный массив, сколько бы осей ни было у исходника, — вы выбрали россыпь подходящих элементов, строки и столбцы для них потеряли смысл. Вторая: это копия, а не вид. К чему приводит вторая деталь — отдельная ловушка ниже, а пока — самая знаменитая ошибка булевой индексации.
Комбинируем условия: &, | и отрицание ~
С операторами-каскадёрами условия собираются в фильтры любой сложности: «и», «или», «не» — и всё это поэлементно, в векторном стиле, без единого цикла.
import numpy as np
temps = np.array([18.2, 21.5, 24.8, 19.9, 27.3, 16.4, 30.1])
warm = (temps > 20) & (temps < 28)
print(temps[warm]) # и тепло, и не жарко
uncomfortable = (temps < 18) | (temps > 28)
print(temps[uncomfortable]) # либо холодно, либо жарко
mild = ~uncomfortable
print(temps[mild]) # всё остальное
[21.5 24.8 27.3] [16.4 30.1] [18.2 21.5 24.8 19.9 27.3]
Тильда ~ инвертирует маску целиком: «все, кто не попал в uncomfortable». Это удобнее, чем переписывать противоположное условие руками: одно движение — и у вас дополнение множества. К слову, numpy сравнение массивов поэлементно — это не только числа: == и != работают и между двумя массивами одинаковой формы, возвращая маску совпадений.
np.where: индексы и выбор из двух вариантов
Маски работают и на таблицах. Если из матрицы наблюдений нужны только строки, где первый столбец превысил порог, условие ставят на столбец, а маску — в первый индекс: table[table[:, 0] > 100] вернёт целиком подходящие строки. Сочетается это и с индексацией по двум осям из урока про срезы: table[mask, 1] достанет значения второго столбца только у выбранных строк. До знакомства с Pandas это самый честный аналог запроса «WHERE по колонке».
У np.where два лица — и это постоянный источник путаницы, пока не разложишь по полочкам. Первое лицо: передаёте только условие — получаете индексы всех подходящих элементов. Второе: передаёте условие и два варианта — np.where(условие, x, y) — и получаете новый массив, где на местах True стоит значение из x, на местах False — из y. Тернарный оператор «если-то-иначе» для целых массивов.
import numpy as np
temps = np.array([18.2, 21.5, 24.8, 19.9, 27.3, 16.4, 30.1])
print(np.where(temps > 25)) # где именно жарко
(array([4, 6]),)
Кортеж из одного массива — NumPy отвечает одинаково для любого числа осей: по одному массиву индексов на ось. Для двумерного массива кортеж будет из двух массивов, и пары координат укажут на каждый подходящий элемент. Второе лицо — выбор:
import numpy as np
prices = np.array([990, 1500, 799, 2400])
final = np.where(prices > 1000, prices * 0.9, prices) # скидка 10%
print(final)
labels = np.where(prices > 1000, "со скидкой", "обычная цена")
print(labels)
[ 990. 1350. 799. 2160.] ['обычная цена' 'со скидкой' 'обычная цена' 'со скидкой']
Заметьте: в первом случае цены после умножения на 0.9 стали дробными — тип всего массива поднялся до float64. Выражения вроде prices * 0.9 NumPy расширил на форму всего массива — за это отвечает broadcasting из пятого урока. np.where — швейцарский нож очистки данных: заменить выбросы, проставить метки, заглушить подозрительные значения.
Считаем и находим: sum маски, argmax, nonzero
Маски отлично играют роль счётчиков: в булевом массиве True считается единицей, False — нулём. Поэтому mask.sum() — готовый ответ на вопрос «сколько значений прошло фильтр». Парный инструмент — np.count_nonzero(mask), то же самое, но словом. А np.argmax отвечает на вопрос «где максимум»: возвращает индекс первого максимального элемента. Рядом живёт np.argmin.
import numpy as np
temps = np.array([18.2, 21.5, 24.8, 19.9, 27.3, 16.4, 30.1])
hot = temps > 20
print(hot.sum()) # сколько тёплых дней
print(np.count_nonzero(hot)) # то же самое
print(np.argmax(temps)) # индекс самого тёплого дня
print(temps[np.argmax(temps)])# и само значение
4 4 6 30.1
«Как найти индекс максимального элемента numpy» — вопрос-фаворит собеседований, и ответ — np.argmax(a). На двумерных массивах argmax принимает ось: argmax(axis=1) вернёт индекс максимума в каждой строке — мы пользовались этим в уроке про reshape и оси, когда искали лучшую неделю у каждого магазина. Если нужны все позиции подходящих элементов, а не только индексы True маски, — np.nonzero(mask), близнец np.where в режиме индексов.
mask.sum()— потому что True считается единицейnp.count_nonzero(mask)— то же самое, чуть быстрее на больших данныхlen(temps[hot])— самый расточительный способ: сначала копирует элементы, потом считает
any и all: есть ли хоть один, все ли
Иногда от проверки нужен не список подходящих элементов, а одно слово «да» или «нет». any спрашивает «есть ли хоть один True», all — «все ли True». Это классическая пара для проверок данных перед обработкой.
import numpy as np
temps = np.array([18.2, 21.5, 24.8, 19.9, 27.3, 16.4, 30.1])
print((temps > 35).any()) # была ли жара за 35?
print((temps > 15).all()) # все ли дни теплее 15?
False True
Кстати, теперь понятно, что советовала ошибка из первой ловушки: она предлагала свернуть массив одним числом через a.any() или a.all() — именно потому, что в контексте if и and Python ждёт один ответ, а не массив. Проверки вида np.isfinite(a).all() («нет ли бесконечностей») и (prices < 0).any() («есть ли отрицательные цены») — стандартный ритуал перед расчётами.
Кейс: чистим выбросы в данных датчика
Соберём всё в практический сценарий. Датчик записал семь значений, одно из которых — сбой. План: находим медиану, меряем, насколько значения отрываются от неё, всё, что оторвалось слишком далеко, помечаем маской выбросов, заменяем на пропуск и считаем чистое среднее.
import numpy as np
sensor = np.array([21.0, 22.5, 23.1, 87.4, 22.8, 21.6, 22.2])
median = np.median(sensor)
spread = np.median(np.abs(sensor - median)) # устойчивая мера разброса
outliers = np.abs(sensor - median) > 5 * spread
print(median)
print(sensor[outliers]) # что выбивается
clean = np.where(outliers, np.nan, sensor)
print(np.round(np.nanmean(clean), 1)) # среднее без выброса
22.5 [87.4] 22.2
Порог «пять медианных отклонений» — простая и живучая эвристика вместо хрупкого «больше трёх сигм»: один дикий выброс раздувает стандартное отклонение и маскирует сам себя, а медианы такие скалы не пугают. Замена через np.where(..., np.nan, sensor) превращает выброс в пропуск, и np.nanmean считает среднее, игнорируя пропуски. Ловля на этом месте: обычный clean.mean() после появления nan вернёт nan — для данных с пропусками нужны nan-варианты из урока про агрегирующие функции NumPy.
Отдельный вопрос — что делать с выбросом: выбросить или заменить. Выбрасывать безопасно, когда вы уверены в сбое прибора; заменять на медиану или пропуск — когда данные дорогие и терять строку жалко. В отчёте честно пишут оба числа: среднее с выбросом и без. В нашем примере это 31.5 против 22.2 — разница в сорок процентов, которую один сбойный замер вносит одним движением. Маска лишь подсвечивает подозрительное; решение всегда за человеком.
Шпаргалка по фильтрации
Все инструменты урока на одной таблице — держите под рукой при задачах:
| Задача | Код | Что вернёт |
|---|---|---|
| Отфильтровать по условию | a[a > 10] | копию с подходящими элементами |
| Несколько условий | a[(a > 10) & (a < 50)] | копию; условия в скобках |
| Отрицание маски | a[~mask] | дополнение: все неподходящие |
| Индексы подходящих | np.where(a > 10) | кортеж с массивами индексов |
| Выбор из двух вариантов | np.where(a > 10, 1, 0) | массив той же формы |
| Сколько подходит | mask.sum() | число True |
| Где максимум | np.argmax(a) | индекс первого максимума |
| Хоть один / все | mask.any(), mask.all() | одно значение True или False |
Что дальше
Маски — последний кирпич базового синтаксиса NumPy. Дальше — случайные числа и модуль random: сгенерируем данные, чтобы было что фильтровать, и разберёмся с seed для воспроизводимости экспериментов. Затем финальный проект с анализом температуры, где reshape, оси и маски соберутся в один отчёт. А в Pandas фильтрация устроена на тех же булевых масках, только выглядят они как df[df['цена'] > 100] — узнаете родную речь с первого взгляда.
Сначала предскажи ответ в голове — это главный навык программиста.
import numpy as np
a = np.array([1, 5, 10, 15, 20])
print(a[(a > 5) & (a < 20)])
import numpy as np
a = np.array([4, 1, 7, 3])
print(np.where(a > 3, a * 10, -1))
1. Что вернёт выражение a[a > 10], если a — массив из 20 чисел?
2. Как правильно записать маску «значения от 10 до 50 включительно»?
3. Что делает np.where(mask, x, y)?
4. Что вернёт np.argmax(np.array([3, 9, 2, 9]))?
5. Сколько элементов насчитает mask.sum() для маски [True, False, True, True]?
В массиве data — десять чисел. Постройте маску «значение больше 30 и меньше 80», выведите отфильтрованные значения и их количество.
Как отфильтровать массив NumPy по условию?
Подставьте сравнение в квадратные скобки: a[a > 10] вернёт копию массива только из элементов больше десяти. Несколько условий комбинируют операторами & (и), | (или), ~ (не), и каждое условие заключают в скобки: a[(a > 10) & (a < 50)]. Результат всегда одномерный массив-копия, независимо от формы исходника.
Почему в масках NumPy нельзя использовать and и or?
Python пытается вычислить «истинность всего массива сразу», а у массива из многих элементов единого ответа нет, поэтому падает ошибка ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all(). Для поэлементной логики используйте & (и), | (или) и ~ (не), оборачивая каждое сравнение в скобки.
Как найти индекс максимального элемента массива NumPy?
Функцией np.argmax(a) — она возвращает индекс первого максимального элемента; np.argmin(a) делает то же для минимума. Само значение — a[np.argmax(a)]. Для двумерных массивов укажите ось: np.argmax(a, axis=1) вернёт индекс максимума в каждой строке.
Как удалить выбросы из данных в NumPy?
Постройте маску выбросов по расстоянию от медианы: outliers = np.abs(a - np.median(a)) > 5 * np.median(np.abs(a - np.median(a))). Затем либо отфильтруйте a[~outliers], либо замените выбросы на np.nan через np.where(outliers, np.nan, a) и считайте статистику nan-функциями вроде np.nanmean. Медианный порог устойчивее «трёх сигм» к самим выбросам.
Как заменить значения в массиве NumPy по условию?
np.where(условие, x, y): там, где условие истинно, подставится значение из x, где ложно — из y, например np.where(prices > 1000, prices * 0.9, prices) даёт скидку 10%. Если нужно поменять сам исходный массив, присваивайте по маске: temps[temps > 25] = 0. А цепочка temps[temps > 25][0] = 0 не сработает: булева индексация возвращает копию, и правка уйдёт во временный массив.
Понравился урок? Сошлитесь на него
«Маска лишь подсвечивает подозрительное; решение всегда за человеком.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
NumPy · Урок 7
Многомерные массивы и оси: reshape, axis и работа с матрицами
Форма массива — это взгляд на данные: reshape и магия -1, оси axis на пальцах, транспонирование T, матричное умножение @ и склейка vstack/hstack.
NumPy · Урок 9
Случайные числа в NumPy: модуль random и генерация данных
Генератор default_rng: целые, дробные и нормальные случайные числа, choice с весами, shuffle, seed и воспроизводимость — плюс симуляции монетки, кубика и роста тысячи людей.
Pandas · Урок 3
Выборка данных в Pandas: loc, iloc и фильтрация строк по условию
loc и iloc, булева маска, isin и between — достаём из таблицы заказов именно те строки и столбцы, которые нужны, и не попадаемся на SettingWithCopyWarning.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
sqlite3 · Урок 4
WHERE и фильтрация в SQL: условия, LIKE, BETWEEN и IN
Главный фильтр SQL: сравнения и логика с AND, OR, NOT, шаблоны LIKE с % и _, диапазоны BETWEEN, списки IN — и классическая ловушка = NULL.
sql wherewhere sql
json · Урок 11
Фильтрация данных: найти нужное в JSON
Включение с условием для отбора, next с default для первого подходящего, any и all для вопросов ко всей коллекции — работаем с выгрузкой заказов.
python фильтрация json данныхpython next первый элемент по условию
NumPy · Урок 3
Индексация и срезы массивов NumPy: как достать любой элемент
Индексы с нуля и с конца, срезы с шагом, строки и столбцы двумерных массивов — и главный подводный камень NumPy: срез возвращает view, а не копию. С примерами, разбором ошибок и шпаргалкой.
индексация numpyсрезы массива python