Урок курса

Продвинутая индексация: булевы маски и целочисленные массивы индексов

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 непосредственно, и если нужна копия с изменениями — её нужно сделать явно до операции.

Закрепите тему на задачах

Сначала сравните форму маски и индексируемого массива. Составные условия заключайте в скобки и отличайте поэлементные & и | от Python and и or.

NumPy: целочисленная индексация, присваивание и маска

Булева маска NumPy для двумерного массива: отбор и замена

Откройте условие и решите задачу перед чтением следующей темы. В задачах на код проверяется программа, в тестовых заданиях — выбранный ответ. Для последовательного обучения вернитесь в программу бесплатного курса.

Задача 1

Дан массив arr = np.array([4, 7, 2, 9, 1]). Вычисляется маска mask = arr > 3. Чему равен shape результата arr[mask]?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Дан arr = np.array([10, 20, 30, 40, 50]). Какое выражение с комбинированной маской вернёт массив [20, 30, 40]?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Комбинированные маски и присваивание: замена выбросов на -1

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin одну строку с целыми числами, разделёнными пробелами. Создаёт из них одномерный ndarray с именем arr через np.array с dtype=int.
  2. Читает вторую строку с двумя целыми числами lo и hi — границы «нормального» диапазона.
  3. Строит маску mask_normal для элементов arr, которые одновременно больше или равны lo и меньше или равны hi, используя оператор &.
  4. Выводит через пробел элементы arr[mask_normal], затем на следующей строке — их количество (атрибут size полученной выборки).
  5. Строит маску mask_out как инверсию mask_normal через ~.
  6. Присваивает arr[mask_out] = -1 (заменяет выбросы на -1 на месте).
  7. Выводит изменённый массив arr через пробел, затем на следующей строке — количество элементов, равных -1 (например, arr[arr == -1].size).

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Строка 1: целые числа через пробел — элементы массива (не менее двух).
Строка 2: два целых числа lo hi через пробел.

Формат вывода

Строка 1: элементы arr[mask_normal] через пробел.
Строка 2: количество нормальных элементов (одно целое).
Строка 3: изменённый arr через пробел.
Строка 4: количество элементов, равных -1 (одно целое).

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 4

Целочисленные индексы: отбор, присваивание и маска ненулевых

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin одну строку с целыми числами через пробел — элементы массива data. Создаёт data = np.array([...], dtype=int).
  2. Читает вторую строку с целыми числами через пробел — массив целочисленных индексов idx. Создаёт idx = np.array([...], dtype=int).
  3. Выводит элементы data[idx] через пробел, затем на следующей строке — shape результата в виде (k,).
  4. Выполняет data[idx] = 0 — присваивает ноль всем позициям из idx на месте.
  5. Строит булеву маску mask_nz = data != 0 для ненулевых элементов.
  6. Выводит элементы data[mask_nz] через пробел, затем на следующей строке — их количество (одно целое число, равное data[mask_nz].shape[0]).
  7. Выводит итоговый массив data через пробел.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Строка 1: целые числа через пробел — элементы массива data (не менее четырёх).
Строка 2: целые числа через пробел — индексы idx (все в диапазоне [0, len(data)); могут не повторяться).

Формат вывода

Строка 1: элементы data[idx] через пробел.
Строка 2: shape результата data[idx] в виде (k,).
Строка 3: ненулевые элементы data после присваивания через пробел.
Строка 4: количество ненулевых элементов (одно целое).
Строка 5: итоговый массив data через пробел.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Двумерный массив: маска через |, инверсия и присваивание строкой

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin два целых числа R и C — размер двумерного массива.
  2. Создаёт массив m через np.arange(R * C).reshape(R, C) с dtype=int.
  3. Строит маску mask_edge для элементов, которые меньше C (первая строка) или больше или равны R * C - C (последняя строка), используя оператор |. Выводит элементы m[mask_edge] через пробел, затем их shape.
  4. Строит инвертированную маску mask_inner = ~mask_edge для внутренних элементов. Выводит элементы m[mask_inner] через пробел, затем их shape.
  5. Выполняет m[mask_inner] = -1 — заменяет внутренние элементы на -1 на месте.
  6. Читает из stdin один целый индекс строки r_idx. Строит маску mask_row = m[r_idx] != -1 для элементов указанной строки, которые не равны -1. Выводит m[r_idx][mask_row] через пробел, затем m[r_idx][~mask_row] через пробел.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Строка 1: два целых числа R C через пробел (R ≥ 3, C ≥ 2, R * C ≥ 6).
Строка 2: одно целое число r_idx — индекс строки (0 ≤ r_idx < R).

Формат вывода

Строка 1: элементы m[mask_edge] через пробел.
Строка 2: shape результата m[mask_edge] в виде (k,).
Строка 3: элементы m[mask_inner] через пробел.
Строка 4: shape результата m[mask_inner] в виде (k,).
Строка 5: элементы m[r_idx], которые не равны -1, через пробел (может быть пустой строкой, если таких нет).
Строка 6: элементы m[r_idx], которые равны -1, через пробел (может быть пустой строкой, если таких нет).

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

Найди и исправь: ошибки в булевых масках и присваивании по маске

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.

В редакторе есть сломанный код. Он пытается выполнить три операции над одномерным массивом с использованием булевых масок, но в каждой из трёх операций допущена ошибка: либо неверный оператор комбинирования, либо забыта инверсия готовой маски, либо присваивание сделано не по маске, а по срезу.

Что нужно сделать

Найдите и исправьте все три ошибки так, чтобы программа:

  1. Читала из stdin одну строку с целыми числами через пробел — элементы массива arr с dtype=int.
  2. Читала вторую строку с двумя целыми числами lo и hi.
  3. Строила маску mask_in = (arr >= lo) & (arr <= hi) и выводила элементы arr[mask_in] через пробел, затем их количество.
  4. Строила маску mask_out = ~mask_in и выводила количество элементов arr[mask_out].
  5. Выполняла arr[mask_out] = -1 и выводила итоговый массив arr через пробел, затем количество элементов, равных -1, через маску arr == -1.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Строка 1: целые числа через пробел — элементы массива (не менее четырёх).
Строка 2: два целых числа lo hi через пробел.

Формат вывода

Строка 1: элементы arr[mask_in] через пробел.
Строка 2: количество элементов arr[mask_in] (одно целое).
Строка 3: количество элементов arr[mask_out] (одно целое).
Строка 4: итоговый массив arr через пробел (после присваивания).
Строка 5: количество элементов, равных -1 (одно целое).

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку