Урок курса
Векторная арифметика и where/clip
NumPy и pandas: практический тренажёрВ предыдущем уроке мы научились строить булевы маски и комбинировать их через &, | и ~. Теперь займёмся тем, ради чего маски особенно полезны: будем не просто отбирать элементы, а преобразовывать данные — ограничивать диапазоны, подставлять значения по условию и заменять циклы одной строкой кода.
np.clip — ограничение значений диапазоном
np.clip(a, a_min, a_max) решает простую, но часто встречающуюся задачу: убедиться, что все значения массива лежат в допустимом диапазоне. Всё, что ниже a_min, заменяется на a_min; всё, что выше a_max, — на a_max; значения внутри диапазона остаются без изменений.
Функция возвращает новый массив той же формы — исходный a не трогается.
import numpy as np
a = np.array([-2, 0, 3, 7])
result = np.clip(a, 0, 5)
print(result) # [0 0 3 5]
print(a) # [-2 0 3 7] — исходный не изменился
Здесь -2 подтягивается до нижней границы 0, 7 обрезается до 5, а 0 и 3 попадают в диапазон и остаются как есть.
Границы передаются скалярами — это стандартный случай. Передача массивов в качестве границ (поэлементные границы) существует, но в этом уроке не рассматривается.
Когда np.clip удобен на практике: нормализация оценок в диапазон [0, 100], ограничение физической величины (например, скорости или температуры) рабочим диапазоном датчика, подготовка входных данных перед логарифмированием — чтобы не получить log(0) или log от отрицательного числа.
np.where — поэлементный условный выбор
np.where(condition, x, y) — это векторная замена конструкции if/else, применённой к каждому элементу массива. Функция проходит по всем позициям: где condition истинно, берёт значение из x; где ложно — из y. Результат — новый массив той же формы.
В этом уроке x и y — либо скаляры, либо массивы ровно той же формы, что condition. Это важная оговорка: broadcasting здесь не рассматривается, и если формы не совпадают, NumPy либо сделает что-то неожиданное, либо выбросит ошибку.
Простейший случай — заменить все отрицательные значения нулём:
import numpy as np
a = np.array([-3, 0, 7, -1, 4])
result = np.where(a > 0, a, 0)
print(result) # [0 0 7 0 4]
Здесь condition — это a > 0, булев массив вида [False, False, True, False, True]. Там, где True, берётся соответствующий элемент a; там, где False, подставляется скаляр 0.
Исходный массив a не изменяется — np.where всегда возвращает новый объект.
Условие можно собрать из нескольких проверок с помощью & и | — тех же операторов, что используются для булевых масок при индексации. Синтаксис остаётся прежним: каждое условие в скобках, между ними & или |:
a = np.array([1, 6, 3, 8, 2])
out = np.where((a >= 3) & (a <= 7), a * 10, -1)
print(out) # [-1 60 30 -1 -1]
Здесь элементы от 3 до 7 умножаются на 10, все остальные заменяются на -1. Значение 8 не попадает в диапазон [3, 7], поэтому получает -1; значение 1 и 2 — тоже.
Обратите внимание: x и y вычисляются полностью до того, как применяется условие. Если написать np.where(a != 0, 1 / a, 0), деление 1 / a вычислится для всех элементов, включая нули — и RuntimeWarning появится, даже если нули никогда не попадут в результат. Это не баг np.where, а следствие того, как Python вычисляет аргументы функции.
Поэлементные арифметические операции над массивами
Когда вы пишете a + b для двух массивов NumPy одинаковой формы, сложение происходит не как «сложить два объекта», а как «сложить каждый элемент a с соответствующим элементом b». Так работают все стандартные операторы: +, -, *, /, //, **, %.
import numpy as np
a = np.array([10, 20, 30, 40])
b = np.array([3, 4, 5, 6])
print(a + b) # [13 24 35 46]
print(a - b) # [ 7 16 25 34]
print(a * b) # [ 30 80 150 240]
print(a / b) # [3.333... 5.0 6.0 6.666...]
print(a // b) # [3 5 6 6]
print(a ** 2) # [ 100 400 900 1600]
print(a % b) # [1 0 0 4]
Несколько моментов, на которые стоит обратить внимание.
/ выполняет true division. Для обычных целочисленных массивов (как int64 в примере выше) результат — вещественный массив float64. Это отличает / от //: np.array([10]) / np.array([3]) даёт [3.333...], а не [3]. Для вещественных и комплексных dtype конкретный тип результата определяется правилами promotion — например, float32 / float32 остаётся float32.
// — floor division, то есть частное округляется вниз до ближайшего целого. Для положительных чисел это совпадает с обычным усечением, но для отрицательных — нет: np.array([-7]) // np.array([2]) даст [-4], а не [-3].
Исходные массивы не изменяются. Любая операция возвращает новый массив — a и b остаются нетронутыми.
Тип результата определяется dtype операндов по правилам promotion. Два int64-массива при сложении, вычитании или умножении дадут int64; если один из операндов float64 — результат будет float64. NumPy выбирает тип автоматически, и явное приведение типов нужно редко.
Операции со скаляром работают так же — скаляр применяется к каждому элементу:
print(a * 2) # [20 40 60 80]
print(a + 100) # [110 120 130 140]
Это именно то, что делает NumPy быстрее Python-цикла: вместо последовательного обхода элементов вся операция выполняется на уровне скомпилированного кода за один вызов.
Замена условного Python-цикла векторной операцией
Когда нужно применить одно и то же действие к каждому элементу массива, первый импульс — написать цикл. Это работает, но в NumPy есть прямые замены, которые делают то же самое быстрее и в одну строку.
Самый простой случай — поэлементная арифметика. Цикл вида:
result = np.empty_like(a)
for i in range(len(a)):
result[i] = a[i] + b[i]
заменяется на:
result = a + b
Результат идентичен, но NumPy выполняет операцию на уровне скомпилированного кода — без Python-overhead на каждой итерации.
Сложнее, когда в цикле есть ветвление:
result = np.empty_like(a)
for i in range(len(a)):
result[i] = a[i] if a[i] > 0 else 0
Здесь алгоритм перевода в векторный вид состоит из трёх шагов.
- Определить условие и записать его как булеву маску или выражение:
a > 0. - Определить, что подставляется при
Trueи приFalse: приTrue—a[i](то есть сам элемент), приFalse—0. - Вызвать
np.where(condition, val_if_true, val_if_false).
Итого:
import numpy as np
a = np.array([-3, 0, 7, -1, 4])
result = np.where(a > 0, a, 0)
print(result) # [0 0 7 0 4]
Логика та же, что в цикле, только условие, значение при True и значение при False передаются как три аргумента функции.
Если условие составное — его собирают до вызова np.where или прямо внутри него. Например, «заменить на -1 всё, что выходит за диапазон [3, 7]»:
a = np.array([1, 6, 3, 8, 2])
mask = (a >= 3) & (a <= 7)
result = np.where(mask, a, -1)
print(result) # [-1 6 3 -1 -1]
Предварительно выделять маску в отдельную переменную необязательно — её можно передать прямо в np.where. Отдельная переменная полезна, если условие сложное и его удобнее читать и отлаживать по частям.
Практический критерий: если тело цикла можно описать как «взять одно из двух значений в зависимости от условия», это np.where. Если тело цикла — арифметическая операция без ветвления, это просто поэлементная операция над массивами.
Булевы маски как условие в np.where
В предыдущем уроке сравнительные операции вроде a > 0 или a == 5 возвращали булев массив, который использовался для индексации: a[a > 0] отбирал только подходящие элементы. Здесь тот же булев массив передаётся как аргумент condition в np.where — механика построения маски не меняется, меняется только то, что с ней делают дальше.
Операторы &, |, ~ для комбинирования масок тоже остаются прежними. Единственное отличие от индексации: в np.where маска не отфильтровывает элементы, а управляет выбором между двумя значениями — x при True и y при False.
import numpy as np
a = np.array([2, 8, -1, 5, -3])
# Та же маска, что при индексации:
mask = (a > 0) & (a < 7)
print(a[mask]) # [2 5] — индексация отбирает элементы
print(np.where(mask, a, 0)) # [2 0 0 5 0] — where выбирает из двух значений
Оба вызова используют одну и ту же mask. В первом случае результат короче исходного массива — возвращаются только совпавшие элементы. Во втором случае форма сохраняется: позиции, где условие ложно, получают 0 вместо исходного значения.
Это разделение полезно помнить при выборе инструмента: если нужно сократить массив — булева индексация; если нужно трансформировать массив с сохранением формы — np.where.
Задача 1
Дан массив a = np.array([1, 4, 9, 16]). Какой результат вернёт выражение a ** 0.5?
Задача 2
Дан массив v = np.array([-3, 0, 5, -1, 8]). Какой массив вернёт вызов np.where(v > 0, v, -v)?
Задача 3
Дан массив arr = np.array([0, 5, 12, 3, 8, 20]). Запишите одно выражение на Python с np.clip, которое ограничивает все значения диапазоном от 4 до 10 включительно. Запишите ответ в виде np.clip(...).
Задача 4
Нормализация датчика: clip, where и арифметика за один проход
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая обрабатывает сигнал датчика:
- Читает из
stdinодну строку с целыми числами, разделёнными пробелами — это сырые показания датчика. Создаёт одномерный массивrawчерезnp.array(..., dtype=int). - Читает вторую строку с четырьмя целыми числами:
lo hi scale offset— параметры обработки. - Шаг 1. Ограничивает
rawдиапазоном[lo, hi]черезnp.clipи сохраняет вclipped. Выводитclippedв одну строку (значения через пробел). - Шаг 2. Создаёт массив
scaledчерез поэлементную арифметику:(clipped - lo) * scale + offset. Выводитscaledв одну строку. - Шаг 3. Создаёт массив
flagsчерезnp.where: там, гдеraw > hi, ставит1; там, гдеraw < lo, ставит-1; все остальные позиции получают0. Реализуйте это двумя последовательными вызовамиnp.where(сначала обработайте верхний выброс, затем нижний). Выводитflagsв одну строку. - Выводит исходный массив
rawв одну строку — чтобы убедиться, что он не изменился.
Каждый из четырёх выводов — отдельная строка, значения разделены пробелами, без лишних пробелов в начале и конце.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
- Строка 1: целые числа через пробел — элементы массива
raw(от 2 до 10 элементов). - Строка 2: четыре целых числа через пробел:
lo hi scale offset.
Формат вывода
Ровно четыре строки:
- Элементы
clippedчерез пробел. - Элементы
scaledчерез пробел (целые числа). - Элементы
flagsчерез пробел. - Элементы
rawчерез пробел.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 5
Найди и исправь: ошибки в поэлементной арифметике и floor division
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.
В редакторе есть сломанный код. Он пытается выполнить четыре поэлементные операции над двумя массивами, но в каждой из четырёх операций допущена ошибка: либо перепутан оператор, либо использован Python-цикл вместо векторной операции, либо перепутаны операнды.
Что нужно сделать
Найдите и исправьте все четыре ошибки так, чтобы программа:
- Читала из
stdinодну строку с целыми числами через пробел — элементы массиваa, затем вторую строку с таким же числом целых чисел — элементы массиваb. Создавалаaиbчерезnp.array(..., dtype=int). - Вычисляла и выводила ровно четыре строки:
- Строка 1: элементы
a + bчерез пробел. - Строка 2: элементы
a * bчерез пробел. - Строка 3: элементы
a // bчерез пробел (floor division — результат целочисленный, округление вниз). - Строка 4: элементы
a % bчерез пробел (остаток от деления).
В каждой из четырёх строк исходного кода есть ровно одна ошибка.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
- Строка 1: целые числа через пробел — элементы массива
a(от 3 до 8 элементов). - Строка 2: целые числа через пробел — элементы массива
b(то же количество, все ненулевые).
Формат вывода
Ровно четыре строки: a+b, a*b, a//b, a%b — каждый результат через пробел.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 6
Пять строк вывода: степень, остаток, where, clip и арифметика
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinодну строку с целыми числами через пробел — элементы массиваa(от 3 до 7 элементов, все положительные, не менее 2). - Читает вторую строку с тремя целыми числами:
p lo hi— показатель степени, нижняя и верхняя границы. - Вычисляет и выводит ровно пять строк в следующем порядке:
- Строка 1: элементы
a ** pчерез пробел. - Строка 2: элементы
a % pчерез пробел. - Строка 3: элементы результата
np.where(a >= lo, "big", "small")через пробел. - Строка 4: элементы результата
np.where(a % 2 == 0, 1, 0)через пробел. - Строка 5: элементы
np.clip(a, lo, hi)через пробел.
Каждая строка — значения через пробел, без лишних пробелов в начале и в конце.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
- Строка 1: целые числа через пробел — элементы массива
a(от 3 до 7 элементов, все ≥ 2). - Строка 2: три целых числа через пробел:
p lo hi(2 ≤ p ≤ 4, lo < hi, оба входят в диапазон значенийa).
Формат вывода
Ровно пять строк: a**p, a%p, результат where со строками, результат where с 0/1, clip(a, lo, hi) — каждый через пробел.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 7
Аудит насыщения: подсчёт выбросов и clip за один проход
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая анализирует насыщение целочисленного массива:
- Читает из
stdinстроку с целыми числами через пробел — элементы массиваdata. Создаётdata = np.array([...], dtype=int). - Читает вторую строку с двумя целыми числами
loиhi— границы допустимого диапазона (lo < hi). - Вычисляет три счётчика с помощью булевой выборки и
len:
n_below— количество элементов строго меньшеlo;n_inside— количество элементов, удовлетворяющих(data >= lo) & (data <= hi);n_above— количество элементов строго большеhi.
- Выводит первую строку:
below=<n_below> inside=<n_inside> above=<n_above>. - Вычисляет
clipped = np.clip(data, lo, hi)и выводит элементыclippedчерез пробел. - Выводит элементы исходного массива
dataчерез пробел — убедитесь, что он не изменился.
Итого ровно три строки вывода.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
- Строка 1: целые числа через пробел — элементы массива
data(от 3 до 10 элементов). - Строка 2: два целых числа через пробел:
lo hi.
Формат вывода
Ровно три строки:
below=<n_below> inside=<n_inside> above=<n_above>— счётчики насыщения.- Элементы
clippedчерез пробел. - Элементы исходного
dataчерез пробел.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задачи к уроку
- np.where и np.clip в NumPy: задача без циклов
Чем различаются np.where и np.clip? Короткий пример и упражнение на поэлементные вычисления NumPy с бесплатной проверкой кода.
- np.clip в NumPy: ограничение значений — пример и задача
Как работает np.clip: короткий пример ограничения массива и задача на подсчёт значений вне диапазона. Бесплатная проверка кода в браузере.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
