Выбор и фильтрация: where, select, clip и маски
Урок собирает практический арсенал для условной обработки массивов: where, select, clip и маски в реальных сценариях.
np.clip ограничивает значения массива заданным диапазоном: всё ниже минимума становится минимумом, всё выше максимума — максимумом.
np.where: условный выбор между двумя вариантами
Мы уже встречали np.where как векторный if/else. Закрепим: np.where(условие, x, y) возвращает массив, где на позициях с True берётся x, с False — y. И x, и y могут быть скалярами или массивами (с broadcasting). Это рабочая лошадка условной обработки.
import numpy as np
a = np.array([-3, 5, -1, 8, -7, 2])
print(np.where(a > 0, a, 0)) # отрицательные -> 0 (ReLU)
print(np.where(a > 0, 'плюс', 'минус')) # метки по знаку
print(np.where(a % 2 == 0, a * 10, a)) # чётные умножить на 10
Вывод:
[0 5 0 8 0 2] ['минус' 'плюс' 'минус' 'плюс' 'минус' 'плюс'] [-3 5 -1 80 -7 20]
Логика прозрачна — это поэлементный выбор. На чистом Python:
a = [-3, 5, -1, 8, -7, 2]
result = [a[i] if a[i] > 0 else 0 for i in range(len(a))]
print(result)
Вывод:
[0, 5, 0, 8, 0, 2]
where, маска или select: как выбирать
Три инструмента условной обработки перекрываются по возможностям, и стоит понимать, когда какой естественнее. np.where(cond, x, y) хорош, когда нужно преобразовать каждый элемент, сохранив форму массива: «отрицательные замени нулём, остальные оставь». Результат той же формы, что вход. Булева маска a[cond] нужна, когда нужно отобрать подмножество, изменив размер: «оставь только положительные». Это разные намерения — преобразовать против отфильтровать. np.select — это where для случая, когда веток больше двух: вместо нагромождения вложенных where вы перечисляете список условий и значений. А присваивание по маске a[cond] = value — когда нужно изменить часть массива на месте, не создавая новый. Простой ориентир: одно условие, два исхода, сохранить форму — where; одно условие, отобрать элементы — маска; много веток — select; изменить на месте — присваивание по маске. Когда вы чётко формулируете, что именно делаете с данными, нужный инструмент выбирается сам, и код получается ясным.
np.select: много условий сразу
Когда веток больше двух, вложенные where становятся нечитаемыми. np.select(условия, значения, default) решает это: принимает список условий и список соответствующих значений, применяя первое подходящее условие для каждого элемента. Идеально для категоризации.
import numpy as np
scores = np.array([95, 82, 67, 50, 73, 40])
conditions = [scores >= 90, scores >= 70, scores >= 60]
grades = ['A', 'B', 'C']
print(np.select(conditions, grades, default='F'))
Вывод:
['A' 'B' 'C' 'F' 'B' 'F']
Порядок условий важен: select берёт первое выполнившееся. Поэтому условия идут от строгого к мягкому (сначала ≥90, потом ≥70 и т. д.). Воспроизведём логику на Python — это цепочка if/elif:
def grade(s):
if s >= 90: return 'A'
if s >= 70: return 'B'
if s >= 60: return 'C'
return 'F'
scores = [95, 82, 67, 50, 73, 40]
print([grade(s) for s in scores])
Вывод:
['A', 'B', 'C', 'F', 'B', 'F']
np.where с одним аргументом: получить позиции
У np.where есть второе лицо. С тремя аргументами np.where(cond, x, y) — это выбор значений. А с одним аргументом np.where(cond) возвращает индексы элементов, где условие истинно. Это разные задачи, и важно их не путать: «какое значение поставить» против «где находятся подходящие элементы». Форма «с одним аргументом» полезна, когда вам нужны именно позиции — например, найти, на каких индексах произошёл всплеск сигнала, или получить координаты ненулевых элементов матрицы (для 2D np.where вернёт пару массивов: строки и столбцы). Результат можно сразу использовать для индексации: idx = np.where(a > threshold); a[idx] = 0. Но чаще для простого условия удобнее прямая булева маска a[a > threshold] = 0 — она короче и не требует промежуточных индексов. К np.where(cond) прибегают, когда позиции нужны сами по себе, а не только для немедленной фильтрации. Держите в голове обе формы и выбирайте по тому, что вам нужно на выходе — значения или координаты.
np.clip: ограничение диапазона
np.clip(a, lo, hi) «зажимает» значения в диапазон [lo, hi]: всё меньше lo становится lo, всё больше hi — hi, остальное не меняется. Это частая операция: ограничение яркости пикселей в [0, 255], отсечение выбросов, нормализация значений в допустимый диапазон.
import numpy as np
a = np.array([-5, 0, 50, 120, 200, 300])
print(np.clip(a, 0, 255)) # зажать в диапазон яркости пикселя
print(np.clip(a, 0, 100)) # ограничить сверху сотней
Вывод:
[ 0 0 50 120 200 255] [ 0 0 50 100 100 100]
Замена ручному циклу с двумя условиями. На Python это:
def clip(a, lo, hi):
return [lo if x < lo else hi if x > hi else x for x in a]
print(clip([-5, 0, 50, 120, 200, 300], 0, 255))
Вывод:
[0, 0, 50, 120, 200, 255]
Практический совет: всегда проверяйте порядок границ в clip и осмысленность диапазона. np.clip(a, lo, hi) с lo > hi даст вырожденный результат, где все значения схлопнутся в одно. А если нужно ограничить только сверху или только снизу, передавайте None для ненужной границы: np.clip(a, 0, None) зажмёт только снизу нулём, не трогая верх. Это удобнее, чем городить отдельное условие.
Фильтрация масками: извлечь подходящее
В отличие от where (который сохраняет форму, заменяя значения), фильтрация маской извлекает только подходящие элементы, меняя размер. Это разные задачи: «преобразовать на месте» против «отобрать подмножество». Комбинируя маски, строят сложные фильтры.
import numpy as np
data = np.array([12, 45, 7, 88, 23, 56, 9, 34])
# Отобрать значения в диапазоне [10, 50)
selected = data[(data >= 10) & (data < 50)]
print(selected)
print("Сколько прошло фильтр:", selected.size)
print("Их среднее:", selected.mean())
Вывод:
[12 45 23 34] Сколько прошло фильтр: 4 Их среднее: 28.5
Фильтрация по нескольким условиям и параллельным массивам
На практике фильтрация редко идёт по одному простому условию — обычно нужно отобрать данные, удовлетворяющие комбинации требований, и часто синхронно по нескольким связанным массивам. Здесь маски раскрывают всю силу. Вы строите составную маску логическими операторами — mask = (age >= 18) & (age < 65) & (income > 0) — и применяете её сразу ко всем параллельным массивам данных: names[mask], age[mask], income[mask] вернут согласованные подмножества, где отобраны одни и те же позиции. Это гарантирует, что строки данных остаются «склеенными»: вы не перепутаете имя одного человека с возрастом другого. Такой стиль — вычислить одну маску по условиям и применить её ко всем колонкам — это и есть способ фильтровать «таблицы», представленные параллельными массивами. Он напрямую предвосхищает работу с pandas, где маска применяется к строкам DataFrame целиком. Маски можно сохранять, инвертировать (~mask даст отвергнутые строки), комбинировать с масками по другим полям. Освоив этот приём, вы решаете сложные запросы к данным («все совершеннолетние клиенты с положительным доходом из этих регионов») декларативно, одним выражением, без единого цикла — быстро и без ошибок ручного перебора.
Замена значений по условию: where vs маска
Есть два стиля заменить значения по условию. Первый — np.where, создаёт новый массив. Второй — присваивание по маске, меняет на месте. Выбор зависит от того, нужен ли исходный массив дальше.
import numpy as np
a = np.array([1, -2, 3, -4, 5])
# Стиль 1: новый массив через where
b = np.where(a < 0, 0, a)
# Стиль 2: на месте через маску
c = a.copy()
c[c < 0] = 0
print(b)
print(c)
Вывод:
[1 0 3 0 5] [1 0 3 0 5]
Комбинирование инструментов в конвейер
Сила этих инструментов раскрывается, когда их соединяют в цепочки обработки. Реальная подготовка данных редко исчерпывается одной операцией — обычно это последовательность: ограничить выбросы, заменить пропуски, отфильтровать невалидные строки, перекодировать категории. Например, типичный конвейер очистки: сначала np.clip зажимает аномально большие значения в разумный диапазон, затем np.where заменяет отрицательные на ноль, потом маска отбирает строки без пропусков. Каждый шаг — векторная операция без циклов, и вместе они образуют читаемый, быстрый конвейер. Ключ к хорошему коду здесь — давать промежуточным результатам осмысленные имена и идти от грубой очистки к тонкой. Когда вы свободно владеете where, select, clip и масками, задачи очистки и преобразования данных, которые на чистом Python заняли бы десятки строк с вложенными циклами и условиями, решаются несколькими ясными выражениями. Это и есть тот стиль работы с данными, ради которого изучают NumPy, и он напрямую переносится в pandas, где те же приёмы применяются к колонкам таблиц.
Шпаргалка по выбору инструмента
| Задача | Инструмент |
| если/иначе, сохранить форму | np.where(cond, x, y) |
| много веток / категории | np.select(conds, vals, default) |
| зажать в диапазон | np.clip(a, lo, hi) |
| отобрать подмножество | маска a[mask] |
| заменить на месте | a[mask] = value |
| найти позиции | np.where(cond) |
Подводные камни
- Порядок условий в select. Берётся первое подходящее; идите от строгого к мягкому, иначе результат неверен.
- Путать where и фильтр маской. where сохраняет форму (заменяет), маска
a[mask]меняет размер (отбирает). - clip с перепутанными границами.
clip(a, hi, lo)с lo>hi даст бессмыслицу; следите за порядком. - Скобки в составных масках.
(a>=10) & (a<50)— обязательны скобки вокруг каждого условия.
Лучшие практики
- Для бинарного условия —
np.where; для многих категорий —np.select. - Ограничение диапазона делайте через
np.clip, а не парой условий. - Чётко различайте «преобразовать значения» (where/маска на месте) и «отобрать подмножество» (a[mask]).
- В
selectрасполагайте условия от самого строгого к самому общему.
Условная обработка — это повседневная реальность работы с данными: реальные данные всегда требуют очистки, ограничения, перекодирования, отбора. Инструменты этого урока покрывают почти все такие задачи и, что важно, делают это векторно — без циклов, быстро и читаемо. Освоив их, вы решаете типичные операции предобработки несколькими ясными выражениями вместо громоздких циклов с условиями.
Итог
np.where(cond, x, y)— векторный if/else, сохраняет форму.np.selectобрабатывает много условий, беря первое подходящее (порядок важен).np.clipзажимает значения в диапазон одним вызовом.- Фильтрация маской
a[mask]отбирает подмножество, меняя размер, — это не то же, что where.