Векторизация в 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 элементов цикл может быть даже быстрее за счёт меньших накладных расходов.

См. также

Примечание

Лицензия и источники

Техническое описание адаптировано из официальной документации NumPy (https://numpy.org/doc/stable/), доступной под BSD-3-Clause License. Перевод на русский, примеры и пояснения — © AlashEd Wiki.