Подготовка: векторы, матрицы и broadcasting

Короткий урок с нуля для курса по арбитражу. Список чисел как вектор, таблица как матрица, как numpy выполняет одну операцию сразу над всеми числами и что такое broadcasting. Код можно запустить, вывод приведен.

Вспомогательный урок к курсу "Собери своего арбитражного бота". В занятии 13 миллионы валютных кругов проверяются одной строкой numpy. Эта страница объясняет слова и сам прием на примере четырех цен.

Прямой путь: цикл

Есть четыре цены монеты, и нужно узнать, как изменился каждый шаг: текущая цена, деленная на предыдущую. Первая версия, которую пишут все, это цикл:

prices = [100.0, 101.5, 99.8, 102.0]
ret = [prices[i+1] / prices[i] for i in range(len(prices) - 1)]
print([round(r, 4) for r in ret])
[1.015, 0.9833, 1.022]

Это работает. Но именно из-за этого сканер становится медленным: Python идет по списку по одному элементу. На четырех ценах никто не заметит. На миллионе заметит.

Вектор: список, с которым работаешь как с одним объектом

Вектор это список чисел. Библиотека numpy хранит его одним куском памяти и дает делать арифметику над всем списком одним выражением:

import numpy as np
p = np.array(prices)
print(np.round(p[1:] / p[:-1], 4))
print(p.mean(), p.max(), p.argmax())
[1.015  0.9833 1.022 ]
100.825 102.0 3

p[1:] значит "все цены, кроме первой", p[:-1] значит "все, кроме последней". Деление одного на другое делит пары на одинаковых позициях: 101.5/100, 99.8/101.5, 102/99.8. Ответ тот же, что у цикла, но без цикла. mean это среднее, max наибольшее значение, argmax позиция наибольшего значения (индекс 3, цена 102).

Матрица: таблица чисел

Матрица это таблица со строками и столбцами. Таблица курсов обмена между валютами это матрица: в строке i, столбце j лежит, сколько валюты j получаешь за единицу i.

M = np.array([[1, 2, 3],
              [4, 5, 6]])
print(M.shape, M[1, 2], M[:, 0], M[0, :])
print(M * 10)
(2, 3) 6 [1 4] [1 2 3]
[[10 20 30]
 [40 50 60]]

Прочитаем. shape (2, 3) значит 2 строки и 3 столбца. M[1, 2] это строка 1, столбец 2 (счет с нуля), то есть 6. M[:, 0] весь первый столбец, M[0, :] вся первая строка. M * 10 умножает каждую клетку.

Broadcasting: маленькая таблица растягивается до большой

Что будет, если сложить две вещи разной формы? numpy растягивает меньшую, копируя ее, пока формы не совпадут. Это и называется broadcasting.

print(M + np.array([100, 200, 300]))
print(M + np.array([[1], [2]]))
[[101 202 303]
 [104 205 306]]
[[2 3 4]
 [6 7 8]]

В первой строке ряд из трех чисел копируется вниз на каждую строку M. Во второй столбец [[1], [2]] копируется вправо: первая строка получает +1, вторая +2. В занятии 13 то же растягивание перемножает три взгляда на одну таблицу и дает куб всех валютных троек за один шаг.

Насколько быстрее

import time
x = np.arange(1_000_000, dtype=float)
t = time.perf_counter(); s = 0.0
for v in x: s += v * 2
t1 = time.perf_counter() - t
t = time.perf_counter(); s2 = (x * 2).sum(); t2 = time.perf_counter() - t
print(s == s2, "loop %.3f s vs numpy %.4f s" % (t1, t2))
True loop 0.111 s vs numpy 0.0011 s

Ответ тот же, а скорость примерно в 100 раз выше на этой машине (у тебя числа будут другие). Цикл тратит время на учет каждого элемента, а numpy один раз передает весь блок скомпилированному коду.

Твое упражнение

  1. Запусти весь код и сравни вывод с уроком.
  2. Сделай вектор из 5 своих цен и посчитай отношения шагов одной строкой. Первое проверь вручную.
  3. Собери таблицу 3 на 3. Выведи ее второй столбец и последнюю строку.
  4. Прибавь к своей таблице np.array([[10], [20], [30]]). Сначала предскажи результат на бумаге, потом проверь.
  5. Увеличь размер x до 10 миллионов. Как изменятся два времени?

Что дальше

Теперь можно читать занятие 13. Его прием это ровно шаг 4 выше, только с таблицей курсов обмена.