Урок курса
Продвинутая индексация: булевы маски и целочисленные массивы индексов
NumPy и pandas: практический тренажёрВ прошлом уроке мы выбирали элементы, строки и столбцы через целочисленный индекс и срезы — то есть всегда явно указывали позиции. Сейчас подход меняется: вместо «возьми третий элемент» мы будем говорить «возьми все элементы, которые больше нуля». Для этого нужен новый инструмент — булева маска.
Булева маска: создание сравнительными операциями
Когда вы пишете arr > 3, NumPy не возвращает один True или False — он проходит по каждому элементу массива и проверяет условие отдельно. Результат — новый массив того же shape, но с dtype bool.
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
mask = arr > 3
print(mask) # [False False False True True]
print(mask.shape) # (5,)
print(mask.dtype) # bool
Это работает со всеми шестью операторами сравнения: ==, !=, <, >, <=, >=. Каждый из них применяется поэлементно и возвращает булев массив.
print(arr == 3) # [False False True False False]
print(arr != 3) # [ True True False True True]
print(arr <= 2) # [ True True False False False]
Для двумерного массива механика та же — shape маски совпадает с shape исходника:
matrix = np.array([[1, 5], [3, 7]])
print(matrix >= 5)
# [[False True]
# [False True]]
print((matrix >= 5).shape) # (2, 2)
Маска — это полноценный ndarray. Её можно сохранить в переменную, проверить shape и dtype, передать дальше. На этом шаге мы только строим маску: какие позиции удовлетворяют условию. Что с ней делать дальше — отбирать элементы или изменять их — разберём в следующих разделах.
Комбинирование булевых масок через &, | и ~
Одна маска описывает одно условие. Но на практике условия почти всегда составные: «значение больше 10 и меньше 50», «отрицательное или нулевое», «всё, кроме выбросов». Для этого маски комбинируют поэлементными логическими операторами.
Три оператора:
&— истина только если оба условия выполнены одновременно;|— истина если выполнено хотя бы одно из условий;~— инверсия: True становится False и наоборот.
Все три работают поэлементно, как обычные арифметические операторы NumPy, и возвращают булеву маску той же формы.
Главное синтаксическое требование: каждое подусловие должно быть в круглых скобках. Без них Python разбирает выражение не так, как вы ожидаете, и получается ошибка или неверный результат.
import numpy as np
arr = np.array([5, 12, 3, 47, 28, 9])
# Оба условия должны быть истинны одновременно
mask_and = (arr > 5) & (arr < 30)
print(mask_and) # [False True False False True True]
# Хотя бы одно из условий
mask_or = (arr < 5) | (arr > 40)
print(mask_or) # [False False True True False False]
# Инверсия маски
mask_base = arr >= 10
print(~mask_base) # [ True False True False False True]
Обратите внимание: mask_and имеет тот же shape, что и arr — (6,). Комбинирование не меняет форму, только значения внутри.
Операторы and, or, not здесь не работают — они рассчитаны на одиночные булевы значения Python, а не на массивы. NumPy специально перегружает &, |, ~ для поэлементной логики.
Можно строить цепочки из нескольких условий, главное — не забывать скобки вокруг каждого:
# Три условия подряд
mask_chain = (arr > 3) & (arr < 30) & (arr != 9)
print(mask_chain) # [True True False False True False]
На этом этапе мы только строим составную маску и убеждаемся в её форме и содержимом. Как применить её для выбора элементов — следующий шаг.
Отбор элементов: булева маска и целочисленные индексы
Маска готова — теперь её можно использовать как индекс. Синтаксис arr[mask] говорит NumPy: «верни мне те элементы arr, где маска равна True».
import numpy as np
arr = np.array([10, 20, 30, 40, 50])
mask = arr >= 30
print(mask) # [False False True True True]
result = arr[mask]
print(result) # [30 40 50]
print(result.shape) # (3,)
Когда маска имеет ту же форму, что и массив (mask.shape == arr.shape), результат arr[mask] — всегда одномерный массив формы (k,), где k — количество True в маске. Это верно и для двумерного случая:
matrix = np.array([[1, 8, 3], [6, 2, 9]])
mask2d = matrix > 5
print(mask2d)
# [[False True False]
# [ True False True]]
print(matrix[mask2d]) # [8 6 9]
print(matrix[mask2d].shape) # (3,)
NumPy «разворачивает» матрицу и собирает подходящие элементы в плоский список — структура измерений не сохраняется. Важно: это правило (k,) действует именно тогда, когда маска полностью совпадает по форме с массивом. Если маска применяется только по одной оси, поведение иное — но такие варианты в этом уроке не рассматриваются.
Скомбинированные маски работают точно так же:
print(arr[(arr > 15) & (arr < 45)]) # [20 30 40]
print(arr[~mask]) # [10 20]
Целочисленная индексация — второй способ продвинутой индексации. Вместо булевой маски передаётся массив (или список) целых чисел — позиций, которые нужно извлечь:
idx = np.array([4, 0, 2])
print(arr[idx]) # [50 10 30]
print(arr[idx].shape) # (3,)
Для одномерного arr форма результата совпадает с формой массива индексов idx. Если idx имеет shape (3,), результат тоже (3,). Для многомерных массивов неиндексированные оси добавляются к форме результата по другим правилам — это отдельная тема, которая здесь не обобщается.
Два важных свойства целочисленной индексации, которых нет у обычных срезов:
- индексы можно задавать в произвольном порядке — порядок элементов в результате совпадает с порядком в
idx; - индексы можно повторять — один и тот же элемент попадёт в результат столько раз, сколько его индекс встречается в
idx.
idx_repeat = np.array([0, 0, 3, 1])
print(arr[idx_repeat]) # [10 10 40 20]
Сравним оба способа рядом:
| Способ | Синтаксис | Shape результата (в рамках этого урока) |
|---|---|---|
| Булева маска | arr[mask] | (k,) — при mask.shape == arr.shape |
| Целочисленный массив | arr[idx] | совпадает с idx.shape — для одномерного arr |
Оба способа работают для чтения данных — создают новый массив, не меняя исходный arr. Как использовать ту же конструкцию для записи — разберём в следующем разделе.
Изменение элементов присваиванием по маске и по индексам
До этого момента arr[mask] и arr[idx] использовались для чтения: мы получали новый массив, а исходный arr оставался нетронутым. Но те же конструкции работают и в левой части присваивания — тогда NumPy не создаёт копию, а изменяет элементы прямо в исходном массиве.
import numpy as np
arr = np.array([10, 20, 30, 40, 50])
mask = arr >= 30
arr[mask] = 0
print(arr) # [10 20 0 0 0]
После выполнения arr[mask] = 0 массив изменился на месте: все три позиции, где mask был True, теперь содержат 0. Никакой новый массив не возвращается — операция молча мутирует arr.
То же самое работает с целочисленными индексами:
arr = np.array([10, 20, 30, 40, 50])
idx = np.array([0, 2])
arr[idx] = 99
print(arr) # [99 20 99 40 50]
Вместо скаляра справа можно поставить массив. NumPy требует не буквального равенства длин, а совместимости формы правой части с формой выбранных позиций по правилам broadcasting. Скаляр и массив формы (1,) совместимы с любым числом выбранных позиций — они рассылаются по всем. Массив с точным числом элементов — тоже частный и самый распространённый случай:
arr = np.array([10, 20, 30, 40, 50])
mask = arr >= 30 # три True
# Точное совпадение: три значения на три позиции
arr[mask] = np.array([300, 400, 500])
print(arr) # [ 10 20 300 400 500]
# Broadcasting: одно значение на все выбранные позиции
arr[mask] = np.array([0]) # shape (1,) — работает так же, как скаляр
print(arr) # [ 10 20 0 0 0]
arr = np.array([10, 20, 30, 40, 50])
idx = np.array([1, 3]) # два индекса
arr[idx] = np.array([200, 400])
print(arr) # [ 10 200 30 400 50]
Ошибку вызывает именно несовместимая форма — например, три выбранных позиции и массив из двух значений, которые нельзя разослать:
arr = np.array([10, 20, 30, 40, 50])
mask = arr >= 30 # три True
# Несовместимая форма — NumPy бросит ValueError
arr[mask] = np.array([1, 2]) # shape (2,) нельзя разослать на (3,)
Один практический момент с целочисленной индексацией: не используйте повторяющиеся индексы при присваивании. NumPy не гарантирует порядок, в котором обрабатываются повторяющиеся позиции, поэтому итоговое значение в таких позициях непредсказуемо и может различаться в зависимости от версии и платформы. Для предсказуемого результата убедитесь, что все индексы в idx уникальны. У булевой маски этой проблемы нет по определению: каждая позиция массива либо True, либо False — она не может встретиться дважды.
Ключевое, что нужно держать в голове: arr[mask] = value и arr[idx] = value не возвращают ничего. Они меняют arr непосредственно, и если нужна копия с изменениями — её нужно сделать явно до операции.
Попробуйте решить
Дан массив arr = np.array([4, 7, 2, 9, 1]). Вычисляется маска mask = arr > 3. Чему равен shape результата arr[mask]?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
