Выбор и фильтрация: where, select, clip и маски

Урок собирает практический арсенал для условной обработки массивов: where, select, clip и маски в реальных сценариях.

np.clip ограничивает значения массива заданным диапазоном: всё ниже минимума становится минимумом, всё выше максимума — максимумом.

np.where: условный выбор между двумя вариантами

Мы уже встречали np.where как векторный if/else. Закрепим: np.where(условие, x, y) возвращает массив, где на позициях с True берётся x, с Falsey. И 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, всё больше hihi, остальное не меняется. Это частая операция: ограничение яркости пикселей в [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.
Проверьте себя
1. Чем np.where(cond, x, y) отличается от фильтрации маской a[mask]?
AЭто одно и то же
Bwhere работает только с числами, а маски — со строками
Cwhere сохраняет форму массива, заменяя значения по условию, а a[mask] извлекает подмножество, меняя размер
Da[mask] всегда быстрее where
2. Почему в np.select(conditions, values) важен порядок условий?
AПорядок не важен, select проверяет все условия сразу
Bselect берёт последнее подходящее условие
CПорядок влияет только на скорость
Dselect применяет первое выполнившееся условие, поэтому условия должны идти от строгого к мягкому, иначе категории определятся неверно
3. Что делает np.clip(a, 0, 255)?
AЗажимает значения в диапазон: всё меньше 0 становится 0, всё больше 255 становится 255, остальное без изменения
BОставляет только элементы в диапазоне [0, 255], удаляя остальные
CСортирует массив и берёт первые 255 элементов
DВозвращает индексы элементов вне диапазона