Вспомогательный урок к курсу "Собери своего арбитражного бота". В занятии 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 один раз передает весь блок скомпилированному коду.
Твое упражнение
- Запусти весь код и сравни вывод с уроком.
- Сделай вектор из 5 своих цен и посчитай отношения шагов одной строкой. Первое проверь вручную.
- Собери таблицу 3 на 3. Выведи ее второй столбец и последнюю строку.
- Прибавь к своей таблице
np.array([[10], [20], [30]]). Сначала предскажи результат на бумаге, потом проверь. - Увеличь размер
xдо 10 миллионов. Как изменятся два времени?
Что дальше
Теперь можно читать занятие 13. Его прием это ровно шаг 4 выше, только с таблицей курсов обмена.