Урок курса

Векторная арифметика и 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

Здесь алгоритм перевода в векторный вид состоит из трёх шагов.

  1. Определить условие и записать его как булеву маску или выражение: a > 0.
  2. Определить, что подставляется при True и при False: при True — a[i] (то есть сам элемент), при False — 0.
  3. Вызвать 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. Никаких заранее созданных переменных нет — вы читаете всё сами.

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

Напишите программу, которая обрабатывает сигнал датчика:

  1. Читает из stdin одну строку с целыми числами, разделёнными пробелами — это сырые показания датчика. Создаёт одномерный массив raw через np.array(..., dtype=int).
  2. Читает вторую строку с четырьмя целыми числами: lo hi scale offset — параметры обработки.
  3. Шаг 1. Ограничивает raw диапазоном [lo, hi] через np.clip и сохраняет в clipped. Выводит clipped в одну строку (значения через пробел).
  4. Шаг 2. Создаёт массив scaled через поэлементную арифметику: (clipped - lo) * scale + offset. Выводит scaled в одну строку.
  5. Шаг 3. Создаёт массив flags через np.where: там, где raw > hi, ставит 1; там, где raw < lo, ставит -1; все остальные позиции получают 0. Реализуйте это двумя последовательными вызовами np.where (сначала обработайте верхний выброс, затем нижний). Выводит flags в одну строку.
  6. Выводит исходный массив raw в одну строку — чтобы убедиться, что он не изменился.

Каждый из четырёх выводов — отдельная строка, значения разделены пробелами, без лишних пробелов в начале и конце.

Ввод и вывод

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

Формат ввода

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

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

Ровно четыре строки:

  1. Элементы clipped через пробел.
  2. Элементы scaled через пробел (целые числа).
  3. Элементы flags через пробел.
  4. Элементы raw через пробел.
РешениеPython

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

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

Задача 5

Найди и исправь: ошибки в поэлементной арифметике и floor division

Что уже дано

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

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

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

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

  1. Читала из stdin одну строку с целыми числами через пробел — элементы массива a, затем вторую строку с таким же числом целых чисел — элементы массива b. Создавала a и b через np.array(..., dtype=int).
  2. Вычисляла и выводила ровно четыре строки:
  • Строка 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 — каждый результат через пробел.

РешениеPython

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

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

Задача 6

Пять строк вывода: степень, остаток, where, clip и арифметика

Что уже дано

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

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

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

  1. Читает из stdin одну строку с целыми числами через пробел — элементы массива a (от 3 до 7 элементов, все положительные, не менее 2).
  2. Читает вторую строку с тремя целыми числами: p lo hi — показатель степени, нижняя и верхняя границы.
  3. Вычисляет и выводит ровно пять строк в следующем порядке:
  • Строка 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) — каждый через пробел.

РешениеPython

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

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

Задача 7

Аудит насыщения: подсчёт выбросов и clip за один проход

Что уже дано

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

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

Напишите программу, которая анализирует насыщение целочисленного массива:

  1. Читает из stdin строку с целыми числами через пробел — элементы массива data. Создаёт data = np.array([...], dtype=int).
  2. Читает вторую строку с двумя целыми числами lo и hi — границы допустимого диапазона (lo < hi).
  3. Вычисляет три счётчика с помощью булевой выборки и len:
  • n_below — количество элементов строго меньше lo;
  • n_inside — количество элементов, удовлетворяющих (data >= lo) & (data <= hi);
  • n_above — количество элементов строго больше hi.
  1. Выводит первую строку: below=<n_below> inside=<n_inside> above=<n_above>.
  2. Вычисляет clipped = np.clip(data, lo, hi) и выводит элементы clipped через пробел.
  3. Выводит элементы исходного массива data через пробел — убедитесь, что он не изменился.

Итого ровно три строки вывода.

Ввод и вывод

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

Формат ввода

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

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

Ровно три строки:

  1. below=<n_below> inside=<n_inside> above=<n_above> — счётчики насыщения.
  2. Элементы clipped через пробел.
  3. Элементы исходного data через пробел.
РешениеPython

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

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

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

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

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

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

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