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