Векторизация в NumPy
Векторизация — это техника, при которой вместо явных циклов for мы работаем с массивами целиком. NumPy выполняет такие операции в скомпилированном C-коде, и они получаются в десятки, а иногда и в сотни раз быстрее Python-цикла.
Если вы пишете код вида for i in range(len(a)): result[i] = a[i] * 2 — почти наверняка это можно заменить на result = a * 2. Это и есть векторизация.
Зачем нужно
Скорость. Главная причина. NumPy-операции не тратят время на интерпретатор Python.
Читаемость.
mean = data.mean()короче и понятнее, чем цикл с накоплением суммы.Меньше ошибок. Нет шансов промахнуться с индексом или выйти за границы.
Параллелизм. Многие ufunc используют SIMD-инструкции процессора автоматически.
Пример 1. Цикл vs векторизация
import numpy as np
import time
n = 1_000_000
a = np.arange(n, dtype=float)
# Способ 1: Python-цикл
start = time.perf_counter()
result = np.empty(n)
for i in range(n):
result[i] = a[i] * 2 + 1
t_loop = time.perf_counter() - start
# Способ 2: векторизация
start = time.perf_counter()
result_vec = a * 2 + 1
t_vec = time.perf_counter() - start
print(f"loop: {t_loop:.3f} s")
print(f"vec : {t_vec:.4f} s")
print(f"speedup: {t_loop / t_vec:.0f}x")
loop: 0.215 s
vec : 0.0028 s
speedup: 77x
Точные цифры зависят от машины, но порядок ускорения — десятки-сотни раз.
Пример 2. Перевод АЦП в напряжение
Сравним «наивный» и векторизованный подходы.
import numpy as np
adc = np.array([0, 256, 512, 768, 1023])
# Цикл
volts_loop = []
for v in adc:
volts_loop.append(v * 5.0 / 1023.0)
# Векторизация
volts_vec = adc * 5.0 / 1023.0
print(np.round(volts_vec, 3))
[0. 1.251 2.503 3.754 5. ]
Пример 3. Условия через маски
Вместо if-else внутри цикла — булевы маски.
import numpy as np
temps = np.array([18.0, 22.5, 25.1, 30.2, 19.7, 35.0])
# Без векторизации:
# alerts = []
# for t in temps:
# if t > 28: alerts.append(t)
alerts = temps[temps > 28]
print(alerts)
# Заменить «горячие» значения на 28 (clip)
safe = np.where(temps > 28, 28, temps)
print(safe)
[30.2 35. ]
[18. 22.5 25.1 28. 19.7 28. ]
Пример 4. Скользящее среднее без цикла
С помощью np.cumsum можно посчитать скользящее среднее за O(n).
import numpy as np
data = np.array([10, 12, 11, 13, 15, 14, 16, 18, 17, 19], dtype=float)
window = 3
cs = np.cumsum(data)
cs[window:] = cs[window:] - cs[:-window]
moving_avg = cs[window - 1:] / window
print(np.round(moving_avg, 2))
[11. 12. 13. 14. 15. 16. 17. 18. ]
Пример 5. Подсчёт пересечений нуля
Задача: посчитать, сколько раз сигнал поменял знак — без цикла.
import numpy as np
signal = np.array([1, 2, -1, -3, 0, 4, 5, -2, -1, 3])
signs = np.sign(signal)
crossings = np.sum(np.diff(signs) != 0)
print(crossings)
4
np.diff находит разности соседних элементов, сравнение с нулём — маска, сумма True — количество.
Пример 6. Broadcasting вместо вложенного цикла
Посчитать попарные расстояния между точками без двойного цикла.
import numpy as np
points = np.array([[0, 0], [1, 0], [0, 1], [3, 4]])
# points[:, None, :] — форма (4, 1, 2)
# points[None, :, :] — форма (1, 4, 2)
diff = points[:, None, :] - points[None, :, :]
dist = np.sqrt((diff ** 2).sum(axis=2))
print(np.round(dist, 2))
[[0. 1. 1. 5. ]
[1. 0. 1.41 4.47]
[1. 1.41 0. 4.24]
[5. 4.47 4.24 0. ]]
Это классический пример: NumPy «раздвигает» массивы по новым осям и вычитает их за один проход.
Пример 7. Фильтрация и присваивание
import numpy as np
rng = np.random.default_rng(0)
readings = rng.normal(25, 2, size=12)
# Заменить выбросы (>3 sigma от среднего) на среднее
mean = readings.mean()
std = readings.std()
outliers = np.abs(readings - mean) > 2 * std
readings[outliers] = mean
print(np.round(readings, 2))
print("outliers:", outliers.sum())
[25.66 24.39 27.4 26.34 24.65 21.96 25.21 24.96 25.34 25.4 23.32 25.69]
outliers: 0
Пример 8. np.vectorize — удобство, не скорость
Иногда функция трудно переписать массивно. np.vectorize оборачивает Python-функцию, чтобы её можно было передавать массив. Это удобно, но не ускоряет код — внутри всё равно цикл.
import numpy as np
def classify(temp):
if temp < 20:
return 'cold'
elif temp < 28:
return 'ok'
return 'hot'
vec = np.vectorize(classify)
temps = np.array([15, 22, 30, 19, 25])
print(vec(temps))
['cold' 'ok' 'hot' 'cold' 'ok']
Если нужна скорость — переписывайте через маски и np.where, а не через np.vectorize.
Подводные камни
np.vectorize не векторизация. Несмотря на название, это просто красивая обёртка над циклом. Для скорости — ufunc и маски.
Память. Векторные операции часто создают временные массивы. Для очень больших данных используйте
out=или+=(in-place).Broadcasting не «бесплатен». Промежуточный массив 1000×1000×3 для попарных операций может занять много памяти.
Python-объекты в массиве. Если
dtype=object, все операции снова идут через интерпретатор — векторизация теряется.Не везде есть выигрыш. Для массивов из 5–10 элементов цикл может быть даже быстрее за счёт меньших накладных расходов.
См. также
Универсальные функции (ufunc) — ufunc и broadcasting
Создание массивов NumPy — создание массивов
Модуль numpy.random — генерация данных для тестов
Примечание
Лицензия и источники
Техническое описание адаптировано из официальной документации NumPy (https://numpy.org/doc/stable/), доступной под BSD-3-Clause License. Перевод на русский, примеры и пояснения — © AlashEd Wiki.