2025, Oct 02 09:16
Пакетные обновления Plotly в Jupyter: ускоряем многорядную анимацию в VSCode
Как ускорить Plotly в Jupyter (VSCode): избегаем покадровых обновлений трейсов и используем fig.batch_update() для быстрых анимаций с несколькими рядами.
Обновление нескольких рядов (traces) Plotly в реальном времени в ноутбуке Jupyter, запущенном в VSCode, может казаться мучительно медленным, если отправлять изменения по одному ряду. Ниже — краткое руководство: сначала воспроизводим узкое место, затем устраняем его, группируя обновления так, чтобы интерфейс получал одно объединённое событие на шаг времени.
Как воспроизвести торможение
В примере ниже генерируется несколько десятков временных рядов, и анимация пытается строиться за счёт поштучного обновления каждого трейса. Сначала запустите первый блок, дождитесь появления рисунка, затем выполните второй блок для обновлений.
import plotly.graph_objects as go
import numpy as np
import random
perT = 20
t_steps_full = np.arange(1, 101, 2)
jitter = 0.25
decay_const = 60
series_count = 32
series_ids = np.arange(0, series_count, 1)
t_axis = np.arange(1, 51, 2)
y_store = [np.array([np.sin(2*np.pi*tv/perT)*np.exp(-tv/decay_const) + random.random()*jitter for tv in t_axis]) for _ in series_ids]
fig_widget = go.FigureWidget()
for idx, _sid in enumerate(series_ids):
    fig_widget.add_trace(go.Scatter(x=t_axis, y=y_store[idx][0:1], mode='lines+markers', name='lines'))
fig_widget.show()for step_idx, t_point in enumerate(t_axis):
    for s in range(len(series_ids)):
        fig_widget.data[s].x = t_axis[:step_idx]
        fig_widget.data[s].y = y_store[s][0:step_idx]При большом числе трейсов этот цикл работает медленно — на итерацию уходит несколько секунд. Если сократить количество трейсов, всё заметно ускоряется, что указывает на проблему именно в покадровых обновлениях каждого ряда по отдельности.
Что на самом деле бьёт по производительности
Каждое присваивание атрибутов трейсу превращается в отдельное обновление, отправляемое из Python на фронтенд. При множестве трейсов такие операции накапливают накладные расходы. В среде ноутбука эффект усиливается, потому что каждое изменение проходит через канал обмена Jupyter. В итоге появляется заметная задержка, растущая вместе с числом трейсов.
Быстрее: пакетные обновления
Вместо того чтобы трогать каждый трейс по отдельности, предварительно соберите новые данные для всех рядов и примените их за одну пакетную операцию. В Plotly для этого есть fig.batch_update(), который объединяет изменения и отправляет их в Plotly.js одним событием.
import plotly.graph_objects as go
import numpy as np
import random
import time
perT = 20
t_steps_full = np.arange(1, 101, 2)
jitter = 0.25
decay_const = 60
series_count = 32
series_ids = np.arange(0, series_count, 1)
t_axis = np.arange(1, 51, 2)
y_store = [np.array([np.sin(2*np.pi*tt/perT)*np.exp(-tt/decay_const) + random.random()*jitter for tt in t_axis]) for _ in series_ids]
fig_widget = go.FigureWidget()
for s_idx, _ in enumerate(series_ids):
    fig_widget.add_trace(go.Scatter(x=[], y=[], mode='lines+markers', name=f'Trace {s_idx+1}'))
fig_widget.show()
for j in range(len(t_axis)):
    updates = [{'x': t_axis[:j+1], 'y': y_store[i][:j+1]} for i in range(series_count)]
    with fig_widget.batch_update():
        for k in range(series_count):
            fig_widget.data[k].x = updates[k]['x']
            fig_widget.data[k].y = updates[k]['y']
    time.sleep(0.01)Такой подход устраняет «болтовню» по каждому трейсу и просит Plotly.js применить все изменения разом. На практике многорядные анимации становятся ощутимо отзывчивее.
Сопутствующие наблюдения из практики
Если всё нужно запускать в одной ячейке, вместо fig.show() используйте display(fig), а затем сделайте короткую паузу примерно на секунду — так последовательность сможет корректно отрисоваться и обновиться. Ещё один пример на Plotly приведён здесь: https://stackoverflow.com/a/78590766/8508004; у него есть особенность — иногда требуется запустить код дважды и нажать кнопку воспроизведения. Кроме того, подобная конфигурация при определённых сценариях может выдавать IOPub message rate exceeded; альтернативный пример для одной ячейки упомянут здесь: https://stackoverflow.com/a/66923695/8508004. Если вы не привязаны к Plotly, можно посмотреть подборку анимированных примеров Matplotlib: https://github.com/fomightez/animated_matplotlib-binder.
Почему это важно
Интерактивные визуализации в ноутбуках зависят от того, насколько эффективно передаются изменения состояния в браузер. Как только масштаб выходит за рамки одного‑двух трейсов, разница между десятками микрообновлений и одним пакетным обновлением на кадр превращается в разницу между плавным графиком и прерывистым.
Итоги
Заранее создавайте все трейсы с пустыми данными, на каждом кадре собирайте массивы для каждого ряда и передавайте их через fig.batch_update(). Если нужен единый блок кода, используйте display(fig) с небольшой задержкой, чтобы фигура успела инициализироваться. И если производительность всё ещё хромает, помните: чем меньше трейсов, тем ниже накладные расходы — именно это и наблюдается при уменьшении ntraces.