суббота, 1 августа 2020 г.

Шпаргалка по визуализации данных в Python с помощью Plotly

Plotly — библиотека для визуализации данных, состоящая из нескольких частей:

  • Front-End на JS
  • Back-End на Python (за основу взята библиотека Seaborn)
  • Back-End на R

В этой простыне все примеры разобраны от совсем простых к более сложным, так что разработчикам с опытом будет скучно. Так же эта «шпаргалка» не заменит на 100% примеры из документации.





Извиняюсь за замыленные gif'ки это происходит при конвертации из видео, записанного с экрана.

Jupyter Notebook со всеми примерами из статьи:


Документация

Так же на базе plotly и веб-сервера Flask существует специальная библиотека для создания дашбордов Dash.

  • Plotly — бесплатная библиотека, которую вы можете использовать в коммерческих целях
  • Plotly работает offline
  • Plotly позволяет строить интерактивные визуализации

Т.е. с помощью Plotly можно как изучать какие-то данные «на лету» (не перестраивая график в matplotlib, изменяя масштаб, включая/выключая какие-то данные), так и построить полноценный интерактивный отчёт (дашборд).

Для начала необходимо установить библиотеку, т.к. она не входит ни в стандартный пакет, ни в Anaconda. Для этого рекомендуется использовать pip:

pip install plotly

Если вы используете Jupyter Notebook, то можно использовать мэджик "!", поставив данный символ перед командой:

!pip install plotly

Перед началом работы необходимо импортировать модуль. В разных частях шпаргалки для разных задач нам понадобятся как основной модуль, так и один из его подмодулей, поэтому полный набор инструкций импорта у нас.

Так же нам понадобятся библиотеки Pandas и Numpy для работы с сырыми данными

Код импорта функций

Линейный график


Начнём с простой задачи построения графика по точкам.

Используем функцию f(x)=x2

Сперва поступим совсем просто и «в лоб»:

  • Создадим график с помощью функции scatter из подмодуля plotly.express (внутрь передадим 2 списка точек: координаты X и Y)
  • Тут же «покажем» его с помозью метода show()

Обратите внимание — график интерактивный, если навести на него курсор, то можно его приближать и удалять, выделять участки, по наведению курсора на точку получать подробную информацию, возвращать картинку в исходное положение, а при необходимости «скриншотить» и сохранять как файл.

Всё это делается с помощью JS в вашем браузере. А значит, при желании вы можете этим управлять уже после построения фигуры (но мы этого делать пожалуй не будем, т.к. JS != Python)

Код



Более читабельно и правильно записать тот же в код в следующем виде:

fig = px.scatter(x=x, y=f(x))
fig.show()

  • Создаём фигуру
  • Рисуем график
  • Показываем фигуру

2 строчки и готовый результат. Т.к. мы используем Express. Быстро и просто.

Но маловато гибкости, поэтому мы практически сразу переходим к более продвинутому уровню — сразу создадим фигуру и нанесём на неё объекты.

Так же сразу выведем фигуру для показа с помощью метода show().

В отличие от Matplotlib отдельные объекты осей не создаются, хотя мы с ними ещё столкнёмся, когда захотим построить несколько графиков вместе

fig = go.Figure()
#Здесь будет код
fig.show()


Как видим, пока пусто.

Чтобы добавить что на график нам понадобится метод фигуры add_trace.

fig.add_trace(ТУТ_ТО_ЧТО_ХОТИМ_ПЕРЕДАТЬ_ДЛЯ_ОТОБРАЖЕНИЯ_И_ГДЕ)

Но ЧТО мы хотим нарисовать? График по точкам. График мы уже рисовали с помощью Scatter в Экспрессе, у Объектов есть свой Scatter, давайте глянем что он делает:

go.Scatter(x=x, y=f(x))



А теперь объединим:

fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x)))
fig.show()



Как видим, отличия не только в коде, но и в результате — получилась гладкая кривая.

Кроме того, такой способ позволит нам нанести на график столько кривых, сколько мы хотим:

Код


Погодите, что это такое? Справа появилась ещё и легенда!

Впрочем, логично, пока график был один, зачем нам легенда?

Но магия Plotly тут не заканчивается. Нажмите на любую из подписей в легенде и соответствующий график исчезнет, а надпись станет более бледной. Вернуть их позволит повторный клик.

Подписи графиков

Добавим атрибут name, в который передадим строку с именем графика, которое мы хотим отображать в легенде.

Plotly поддерживает LATEX в подписях (аналогично matplotlib через использование $$ с обеих сторон).

Код


К сожалению, это имеет свои ограничения, как можно заметить подсказка при наведении на график отображается в «сыром» виде, а не в LATEX.

Победить это можно, если использовать HTML разметку в подписях. В данном примере я буду использовать тег sup. Так же заметьте, что шрифт для LATEX и HTML отличается начертанием.

Код



С увеличением длины подписи графика, легенда начала наезжать на график. Мне это не нравится, поэтому перенесём легенду вниз.

Для этого применим к фигуре метод update_layout, у которого нас интересует атрибут legend_orientation fig.update_layout(legend_orientation="h")
Код


Хорошо, но слишком большая часть рабочего пространства ноутбука не используется. Особенно это заметно сверху — большой отступ сверху до поля графика.

По умолчанию поля графика имеют отступ 20 пикселей. Мы можем задать свои значения отступам с помощью update_layout, у которого есть атрибут margin, принимающий словарь из отступов:

  • l — отступ слева
  • r — отступ справа
  • t — отступ сверху
  • b — отступ снизу

Зададим везде нулевые отступы fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))

update_layout можно применять последовательно несколько раз, либо можно передать все аргументы в одну функцию (мы сделаем именно так)

Код

Поскольку подписи в легенде короткие, мне не нравится, что они расположены слева. Я бы предпочёл выровнять их по центру.

Для этого можно использовать у update_layout атрибут legend, куда передать словарь с координатами для сдвига (сдвиг может быть и по вертикали, но мы используем только горизонталь).

Сдвиг задаётся в долях от ширины всей фигуры, но важно помнить, что сдвигается левый край легенды. Т.е. если мы укажем 0.5 (50% ширины), то надпись будет на самом деле чуть сдвинута вправо.

Т.к. реальная ширина зависит от особенностей вашего экрана, браузера, шрифтов и т.п., то этот параметр часто приходится подгонять. Лично у меня для этого примера неплохо работает 0.43.

Чтобы не шаманить с шириной, можно легенду относительно точки сдвига с помощью аргумента xanchor.

В итоге для легенды мы получим такой словарь:

legend=dict(x=.5, xanchor="center")

Код целиком

Стоит сразу задать подписи к осям и графику в целом. Для этого нам вновь понадобится update_layout, у которого добавится 3 новых аргумента:

title="Plot Title",
xaxis_title="x Axis Title",
yaxis_title="y Axis Title",

Следует заметить, что сдвиги, которые мы задали ранее могут негавтивно сказаться на читаемости подписей (так заголовок графика вообще вытесняется из области видимости, поэтому я увеличу отступ сверху с 0 до 30 пикселей

Код



Вернёмся к самим графикам, и вспомним, что они состоят из точек. Выделим их с помощью атрибута mode у самих объектов Scatter.

Используем разные варианты выделения для демонстрации:

Код



Теперь особенно заметно, что LATEX в функции g(x)=x отображается некорректно при наведении курсора мыши на точки.

Давайте скроем эту информацию.

Зададим для всех графиков с помощью метода update_traces поведение при наведении. Это регулирует атрибут hoverinfo, в который передаётся маска из имён атрибутов, например, «x+y» — это только информация о значениях аргумента и функции:

Код


Как-то недостаточно наглядно, не находите?

Давайте разрешим использовать информацию из всех аргументов и сами зададим шаблон подсказки.

  • hoverinfo=«all»
  • в hovertemplate передаём строку, используем HTML для форматирования, а имена переменных берём в фигурные скобки и выделяем %, например, %{x}

Код



А что если мы хотим сравнить информацию на 2 кривых в точках, например, с одинаковых аргументом?

Т.к. это касается всей фигуры, нам нужен update_layout и его аргумент hovermode.

Код


Кстати, маркерами можно управлять для конкретной кривой и явно.

Для этого используется аргумент marker, который принимает на вход словарь. Подробный пример.

А мы лишь ограничимся баловством:

Код



Кажется теперь на графике плохо видно ту часть, где кривые пересекаются (вероятно наиболее интересную для нас).

Для этого у нас есть методы фигуры:

  • update_yaxes — ось Y (вертикаль)
  • update_xaxes — ось X (горизонталь)

С их помощью зададим интервалы отображения для осей (это только начальное положение, ничто не мешает нам сменить масштаб в процессе взаимодействия с графиком).

Код



Хорошо, но правильно было бы нанести осевые линии.

Для этого у тех же функций есть 3 атрибута:

  • zeroline — выводить или нет осевую линию
  • zerolinewidth — задаёт толщину осевой (в пикселях)
  • zerolinecolor — задаёт цвет осевой (строка, можно указать название цвета, можно его код, как принято в HTML-разметке)

Код



Давайте добавим больше разных функций на наш график, но сделаем так, чтобы по умолчанию их не было видно.

Для этого у объекта Scatter есть специальный атрибут:

visible='legendonly'

Т.к. мы центрировали легенду относительно точки сдвига, то нам не пришлось менять величину сдвига с увеличением числа подписей.

Код



Наверное всё же не следует смешивать вместе тригонометрические и арифметические функции. Давайте отобразим их на разных, соседних графиках.

Для этого нам потребуется создать фигуру с несколькими осями.

Фигура с несколькими графиками создаётся с помощью подмодуля make_subplots.

Необходимо указать количество:

  • row — строк
  • col — столбцов

А при построении графика передать «координаты» графика в этой «матрице» (сперва строка, потом столбец)

fig = make_subplots(rows=1, cols=2, specs=[[{'type':'domain'}, {'type':'domain'}]])

Код



Заметили, наши изменения осей применились к обоим графикам?

Естественно, если у метода, изменяющего оси указать аргументы:

  • row — координата строки
  • col — координата столбца

то можно изменить ось только на конкретном графике:

Код


А вот если бездумно использовать title, xaxis_title и yaxis_title для update_layout, то может выйти казус — подписи применятся только к 1 графику:

Код, приводящий к казусу



Поэтому заголовки графиков можно задать, при создании фигуры, передав в аргумент subplot_titles кортеж/список с названиями.

Подписи осей под графиками можно поменять с помощью методов фигуры:

  • fig.update_xaxes
  • fig.update_yaxes

Передавая в них номер строки и колонки (т.е. «координаты изменяемого графика»)

Код, подписывающий все графики независимо



И конечно, если необходимо сделать так, чтобы один график был больше, а другой меньше, то для этого используется аргументы

  • column_widths — задаёт отношения ширины графиков (в одной строке)
  • row_heights — задаёт отношения высот графиков (в одном столбце)

Каждый из этих параметров принимает список чисел, которых должно быть столько, сколько графиков в строке/столбце. Отношения чисел задают отношения ширин или высот.

Рассмотрим на примере ширин. Сделаем левый график вдвое шире правого, т.е. зададим соотношение 2:1.

Код



А что если мы хотим выделить одному из графиков больше места, чем другим, например, 2 строки или наоборот, 2 столбца?

В matplotlib мы использовали бы несколько фигур, либо оси с заданными размерами, здесь у нас есть другой инструмент. Мы можем сказать каким-то осям объединиться вдоль колонок или вдоль строк.

Для этого нам потребуется написать спецификацию на фигуру (для начала очень простую).

Спецификация — это список (если точнее, то даже матрица из списков), каждый объект внутри которого — словарь, описывающий одни из осей.

Если каких-то осей нет (например, если их место занимают растянувшиеся соседи, то вместо словаря передаётся None.

Давайте сделаем матрицу 2х2 и объединим вместе левые графики, получив одни высокие вертикальные оси. Для этого первому графику передадим атрибут «rowspan» равный 2, а его нижнего соседа уничтожим (None):

specs=[
[{"rowspan": 2}, {}],
[None, {}]
]


Код



Как видим, в вертикальный график идеально вписался тангенс, который отныне не невидим.

Для объединения используется:

  • rowspan — по вертикали
  • colspan — по горизонтали

Код, объединяющий ячейки в другом направлении



Больше примеров использования make_subplots

Последний вариант получился слишком узким по вертикали.

Высоту легко увеличить в помощью атрибута height у метода update_layout.

Размеры фигуры регулируются 2 атрибутами:

  • width — ширина (в пикселях)
  • height — высота (в пикселях)

Но следует помнить, если вы встраиваете фигуры plotly куда-то (а это логично, если вы делаете дашборд, например), то фигура занимает всё отведённое пространство по ширине, поэтому не изменять ширину в plotly будет не лучшей идеей. Лучше использовать стили в разметке.

Код



Увеличиваем плотность информации


Тепловая карта


Вернёмся к 1 графику, но постараемся уместить на нём больше информации, используя цветовую кодировку (что-то вроде тепловой карты — чем выше значение некой величины, тем «теплее» цвет).

Для этого у объекта go.Scatter используем уже знакомый атрибут marker (напомним, он принимает словарь). Передаём следующие атрибуты в словарь:

  • color — список значений по которым будут выбираться цвета. Элементов списка должно быть столько же, сколько и точек.
  • colorbar — словарь, описывающий индикационную полосу цветов справа от графика. Принимает на вход словарь. Нас интересует пока только 1 значение словаря — title — заголовок полосы.

Код



В предыдущем примере цветовая шкала не очень похожа на тепловую карту.

На самом деле цвета на шкале можно изменить, для этого служит атрибут colorscale, в который передаётся имя палитры.

Код



Можно ли добавить больше информации? Конечно можно, но тут возникают хитрости.

Для ещё одного измерения можно использовать размер маркеров.

Важно. Размер — задаётся в пикселях, т.е. величина не отрицательная (в отличие от цвета), поэтому мы будем использовать модуль одной из функций.

так же, величины меньше 2 пикселей обычно плохо видны на экране, поэтому для размера мы добавим множитель.

Размеры задаётся атрибутом size того же словаря внутри marker. Этот атрибут принимает 1 значение (число), либо список (чисел).

Код



Анимация


Можно ли ещё уплотнить информацию на графике? Да, можно, если использовать «четвёртое измерение» — время. Это так же может быть полезно и само по себе для оживленя вашего графика.

Вернёмся на пару шагов назад. Мы будем анимировать график построения параболы. Для этого нам понадобятся:

  1. Начальное состояние
  2. Кнопки (анимация не должна начинаться сама по себе, поэтому для начала мы создадим простую кнопку, её запускающую, а постепенно перейдём к временной шкале)
  3. Фреймы (или кадры) — промежуточные состояния

1. Начальное состояние

Это то, что будет на графике до начала анимации. В нашем случае это будет стартовая точка.

Уберём практически всё лишнее из предыдущих шагов

Шаг 1 - код полуфабрикат

2. Кнопка

Код минимальной работоспособной кнопки выглядит так:

"updatemenus": [{"type": "buttons",
                 "buttons": [{"label": "Your Label",
                              "method": "animate",
                              "args": [See Below]}]}]

updatemenus — это один из элементов слоя, т.е. layout фигуры, а значит, мы добавим кнопку с помощью метода update_layout.

Пока она не будет ничего делать, т.к. у нас нечего анимировать.

Шаг 2 - код всё ещё полуфабрикат

3. Фреймы

Это список «кадров» из которых состоит наша анимация.

Каждый фрейм должен содержать внутри себя целиком готовый график, который просто будет отображаться на нашей фигуре, как в декорациях.

Фрейм создаётся с помощью go.Frame()

График передаётся внутрь фрейма в аргумент data.

Таким образом, если мы хотим построить последовательность графиков (от 1 точки до целой фигуры), нам надо просто пройти в цикле:

frames=[]
for i in range(1, len(x)):
    frames.append(go.Frame(data=[go.Scatter(x=x[:i], y=f(x[:i]))]))

После этого фреймы необходимо передать в фигуру. У каждой фигуры есть атрибут frames, который мы и будем использовать:

fig.frames = frames

Шаг 3 - Ёлочка гори, в смысле код, запускающий анимацию



Другой способ задать начальное состояние, слой (с кнопками) и фреймы — сразу передать всё в объект go.Figure:

  • data — атрибут для графика с начальным состоянием
  • layout — описание «декораций» включая кнопки
  • frames — фреймы (кадры) анимации

Код

Естественно, если добавить на графики (как на начальный, так и те, что во фреймах) маркеры с указанием цвета, цветовой шкалы и размера, то анимация будет более сложного графика.

Код

Заметим, что код простейшей кнопки, которая запускает воспроизведение видео выглядит так:

dict(label="Play", method="animate", args=[None])

или

dict(label="", method="animate", args=[None])

Если мы хотим добавить кнопку «пауза» (в отличие от стандартной паузы повторное нажатие не будет вызывать воспроизведение, для начала воспроизведения придётся нажат Play), код усложнится:

dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
                                                 "mode": "immediate",
                                                 "transition": {"duration": 0}}])

Правда, если добавить 2 такие кнопки, то вы заметите, что кнопка play, нажатая после паузы, в итоге начинает воспроизведение с начала. Это не совсем интуитивное поведение, поэтому ей следует добавить ещё 1 аргумент:

dict(label="", method="animate", args=[None, {"fromcurrent": True}])

Теперь полный набор из 2 наших кнопок будет выглядеть так:

buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
         dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
                                                           "mode": "immediate",
                                                           "transition": {"duration": 0}}])])]

Код с 2 кнопками: запуска и остановки анимации

Иногда полезно перенести кнопки в другоме место. Рассмотрим некоторые из атрибутов, которые с этим помогут:

  • direction — направление расположения кнопок (по умолчанию сверху-вниз, но если указать «left», то будет слева-направо)
  • x, y — положение (в долях от фигуры)
  • xanchor, yanchor — как выравнивать кнопки. У нас была раньше проблема с выравниванием легенд, тут та же история. Если хотим выровнять по центру, то x=0.5 и xanchor=«center» помогут.

Код с кнопками внизу

Слайдер


Слайдер по принципу работы похож на анимацию, но есть серьёзное отличие.

Слайдер — это элемент навигации, полоска по которой скользит ползунок, который управляет состоянием графиков на фигуре.

Т.е. если фреймы в анимации меняются один за другим, то в случае использования слайдера все графики одновременно есть. Но большая часть из них невидима. И при перемещении ползунка мы просто какие-то скрываем, а другие наоборот показываем (и перестраиваем оси, конечно).

1. Создаём список графиков. Важно, что один из графиков (например, 1й) должен быть видимым.

Для установления видимости/невидимости используется аргумент visible:

  • Видимый график — go.Scatter(visible=True, x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')
  • Невидимый график — go.Scatter(visible=False, x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')

Все графики рисуем на фигуре — для этого удобно использовать аргумент data фигуры, чтобы передать их все списком:

fig = go.Figure(data=trace_list)

2. Создаём список «шагов» слайдера.

Шаг имеет определённый синтаксис. По сути он описывает состояние (какие графики видимы, какие нет) и метод перехода к нему.

Шаг должен описывать состояние всех графиков.

Минимальный синтаксис 1 шага:

dict(
method = 'restyle',
args = #СПИСОК СОСТОЯНИЙ ВСЕХ ГРАФИКОВ
)


Состояние видимости/невидимости задаётся парой 'visible' и списка логических значений (какие графики 'visible', а какие нет). Поэтому для КАЖДОГО шага мы создадим список False, а потом поменяем нужное значение на True, чтобы показать какой-то конкретный график.

Шаги нужно собрать в список (т.е. это будет список словарей).

Наконец все шаги надо добавить в фигуру:

fig.layout.sliders = sliders

Минимально рабочий код (внимательно изучите его, т.к. там есть несколько тонких моментов):

Код



Если мы хотим добавить не 1 график, а 2, то добавить их придётся парой везде:

  • в первоначальное активное состояние
  • все неактивные (скрытые)
  • парами генерировать состояние шагов
  • парами активировать видимые на шаге графики

Поменяем изучаемую фигуру на синус и косинус (чтобы следить за тем как они развиваются):

Код



Просто добавим кнопки из предыдущей части. Они ничего не будут делать, т.к. мы ещё никакой анимации не добавили. Просто «прицелимся».

Код слайдера с нерабочими кнопками анимации

Если просто добавить фреймы с анимацией так, как мы это делали раньше, то кнопки анимации конечно заработают. Но только на 1 состоянии слайдера.

Код искалеченного слайдера

ОК, значит нам придётся сделать «финт ушами» и отказаться от невидимых графиков, а привязать слайдер непосредственно к фреймам анимации (Почему мы так не сделали сразу? Чтобы был альтернативный способ, подходящий непосредственно для слайдера без изысков)

  1. Закомментируем всё, что касается trace_list — списка наших «невидимых» графиков.
  2. Вынесем созданием 2 видимых графиков в первоначальное создание фигуры
  3. Добавим параметр name каждому фрейму
  4. Переделаем шаблон шага:
    • Добавим label, совпадающий с именем соответствующего фрейма
    • Сменим метод на «animate»
    • Все аргументы заменим на 1 список из 1 единственной строки, совпадающей с именем фрейма
  5. Уберём «проявление» невидимых графиков на определённых позициях слайдера, т.к. теперь слайдер будет привязан к фреймам

Чтобы все изменения были хорошо заметны, я закомментировал убираемые строки

Код относительно работающего слайдера с кнопками анимации

Единственный момент, который слегка раздражает — когда происходит «воспроизведение», то ползунок слайдера не ползёт.

Это легко исправить с помощью аргумента showactive для меню. Так же его выключение снимет состояние «нажато» с кнопок Play/Pause.

Код слайдера, который уже нормально анимирован

Конечно добавим данные тепловой карты и размер маркера для увеличения плотности информации

Заметьте, что colorbar мы добавили всего 1 раз, однако, в него пришлось внести некоторые правки — мы сдвинули его по вертикали слегка вниз, т.к. теперь в правой колонке у нас есть легенда.

Код



Осталось немного облагородить панель слайдера.

Добавим подписи к графику и осям, увеличим и оформим подпись текущего значения слайдера (в других обстоятельствах он стал бы временной шкалой), сместим кнопки анимации влевой, а слайдер чуть сожмём, чтобы освободить им место.

  • Аргумент currentvalue — задаёт форматирование подписи к текущему шагу, включая префикс, положение на слайде, шрифт
  • Аргументы x, y, xanchor, yanchor, pad — задают положение и отступы для слайдера и их синтаксис аналогичен таковому у кнопок

Код



Возникает вопрос зачем же мы делали вариант с невидимыми графиками, если они не пригодились?

На самом деле они нужны, в том числе для анимации. Дело в том, что если вы хотите на разных слайдах анимации показывать разное количество графиков, то вам надо в самом начале на этапе создания фигуры добавить столько графиков, сколько их может отображаться максимально. Они все должны быть невидимыми.

Я специально задам 2 переменные:

  • graphs_invisible — содержит как невидимый корректный график, так и пустой объект графика вообще без указания видимости
  • graphs_visible — содержит корректные видимые графики, которые надо показывать по очереди

В первоначальном состоянии мы отобразим невидимые графики или пустышки, а в каждом фрейме будем комбинировать видимые и невидимые, чтобы их количество было постоянным.

Код



Философский вопрос — зачем мы так мучаемся, если есть plotly.express?


Документация plotly по анимации plotly.com/python/animations начинается с феерического примера:

Код, создающий довольно сложную анимацию данных с помощью Plotly Express

Действительно, функции Экспресса принимают на вход датафреймы (да, обычные из Pandas), вам лишь нужно указать колонки по которым производится агрегация данных. И можно сразу строить и тепловые карты, и анимации очень небольшим количеством кода, как в этом примере:



Ответ и прост и сложен:

  1. Стандартные примеры Экспресса могут не удовлетворить ваших потребностей, нужно что-то чуть более сложное и хитрое, например, совместить график и гистограмму. Вручную вы получаете больше гибкости.
  2. Понять как сгруппировать и агрегировать данные в датафрейме для такой визуализации порой сложнее, чем просто построить набор картинок для фреймов анимации/слайдера.

Показ plotly графиков на сайте


Plotly — это не только библиотека для Python, но ещё и JS, это значит, что любые графики, которые вы строите в jupyter notebook, вы можете показывать и на сайте (если он на Python, либо если вы заранее выгрузите всё необходимое).

В рамках этого урока мы не будем рассматривать полный цикл разработки дашборда или веб приложения, просто рассмотрим небольшой пример:

import json
#Здесь вы создаёте свой график в фигуре fig
graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)

В graphJSON окажется приличный JSON. Не будем вдаваться в его особенности, хотя легко заметить, что его структура соответствует нашему объекту фигуры, со всеми графиками, фреймами, подложками, кнопками и т.п.

Давайте добавим немного по краям и сложим этот код в файл:

with open('example.JSON', 'w') as file:
    file.write('var graphs = {};'.format(graphJSON))

А теперь откроем страницу example.html, когда в одном каталоге с ней есть наш файл example.JSON (Для корректной работы необходимо подключение к интернет, т.к. некоторые стили и JS подтягиваются со сторонних сайтов).

Удивительно, не правда ли?

При этом, если посмотреть структуру, то она очень проста и содержит всего 4 важных объекта (их порядок на странице важен):

1. Подключение JS библиотеки Plotly (в примере это делается онлайн из CDN, но если вы делаете локальный продукт, например приватный дашборд для работы внутри организации, JS легко поместить на локальный сервер и поменять ссылки)

<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>

2. Блок, где будет выводиться график

<div id="plotly_graph"></div>

Важно понимать, что Plotly по умолчанию не фиксирует размеры графика, поэтому вы можете применить к контейнеру свои стили, сверстав его такого размера и в том месте, где это необходимо вам. Plotly постарается занять всё свободное место по ширине.

Важно использовать действительно уникальный id — именно по нему plotly будет находить место на странице, куда надо встроить график.

3. JS переменная, содержащая наш JSON, описывающий график. Мы сформировали её и сложили в файл целиком. Вы можете выводить её непосредственно в коде страницы.

<script src="example.JSON"></script>

4. Вызов JS функции plotly, которая построит график.

  • первым передаётся id контейнера, в котором выводить график
  • вторым передаётся переменная, содержащая JSON Plotly объекта

<script>Plotly.plot('plotly_graph',graphs,{});</script>

А теперь испробуйте код на каком-нибудь своём графике!

Круговые диаграммы


Для полноты картины рассмотрим несколько других способов визуализации данных, кроме линейных графиков. Начнём с круговых диаграмм

Для нашего эксперимента «подбросим» 100 раз пару игральных кубиков (костей) и запишем суммы выпавших очков.

Код

Для того чтобы создать круговую диаграмму используем go.Pie, который добавляем так же, как мы добавляли график на созданную фигуру.

Используем 2 основных атрибута:

  • values — размер сектора диаграммы, в нашем случае прямо пропорционален количеству той или иной суммы
  • labels — подпись сектора, в нашем случае значение суммы. Если не передать подпись, то в качестве подписи будет взят индекс значения из списка values

Код



Сразу бросается в глаза то, что хотя мы передали массив, упорядоченный по индексам, но при построении он был пересортирован по значениям.

Это легко исправить с помощью аргумента sort = False

Код



Так же при желании мы можем «выдвинуть» один или несколько секторов.

Для этого используем аргумент pull, который принимаем список чисел. Каждое число — доля, на которую надо выдвинуть сектор из круга:

  • 0 — не выдвигать
  • 1 — 100% радиуса круга

Мы создадим список из нулей, такой же длинны, что массив значений. А потом один элемент увеличим до 0.2.

Код



Если вам не нравятся классические круговые диаграммы «пирожки», то легко превратить их в «пончики», вырезав сердцевину. Для этого используем аргумент hole, в который передаём число (долю радиуса, которую надо удалить):

  • 0 — не вырезать ничего
  • 1 — 100% вырезать, ничего не оставить

Таким образом, значение 0.9 превратит круговую диаграмму в кольцевую.

Код



Кстати, образовавшаяся «дырка от бублика» — идеальное место для подписи, которую можно сделать с помощью атрибута annotations слоя.

Не забываем, что аннотаций может быть много, поэтому annotations принимаем список словарей.

Текст аннотации поддерживает HTML разметку (чем мы воспользуемся, задав абсурдно длинный текст, не помещающийся в 1 строку)

Код



Естественно обычный способы оформления визуализаций, показанные для графиков, тут тоже работают:

  • title
  • title_x
  • margin
  • legend_orientation

Код



Что, если вы хотим детализовать картинку?

Sunburst или диаграмма «солнечные лучи»


Нам на помощь приходит диаграмма «солнечные лучи» — иерархическая диаграмма на основе круговой. По сути это набор кольцевых диаграмм, нанизанных друг на друга, причём сегменты следующего уровня находятся в пределах границ сегментов своего «родителя» на предыдущем.

Например, получить 8 очков с помощью 2 игральных костей можно несколькими способами:

  • 2 + 6
  • 3 + 5
  • 4 + 4

Для построения диаграммы нам потребуется go.Sunburst и 4 основных аргумента:

  • values — значения, задающие долю от круга на диаграмме
  • branchvalues=«total» — такое значение указывает, что значения родительского элемента являются суммой значений потомков. Это необходимо для того, чтобы составить полный круг на каждом уровне.
  • labels — список подписей, которые отображаются на диаграмме
  • parents — список подписей родителей, для построения иерархии. Для элементов 0 уровня (без родителей) родителем указывается пустая строка.

Для начала обойдёмся 2 уровнями (все события и суммы)

Код



А теперь добавим группировку по парам исходов игральных костей и вычисление для таких пар «родителей».

Конечно, если кости идентичны, то 6+2 и 2+6 — это идентичные исходы, как и пара 3+5 и 5+3, но в рамках следующего примера мы будем считать их разными, просто чтобы не добавлять лишнего кода.

Так же уменьшим отступы, т.к. подписи получаются уж очень мелкими.

Код



Гистограммы


Естественно не круговыми диаграммами едиными, иногда нужны и обычные столбчатые.

Простейшая гистограмма строится с помощью go.Histogram. В качестве единственного аргумента в x передаём список значений, которые участвуют в выборке (Plotly самостоятельно сгруппирует их в столбцы и вычислит высоту), в нашем случае это колонка с суммами.

Код



Если по какой-то причине нужно построить не вертикальную, а горизонтальную гистограмму, то меняем x на y:

Код



А что, если у нас 2 или 3 набора данных и мы хотим их сравнить? Сгенерируем ещё 1100 бросков пар кубиков и просто добавим на фигуру 2 гистограммы:

Код



Все 3 выборки подчиняются одному и тому же распределению, и очевидно, но количество событий сильно отличается, поэтому на нашей гистограмме некоторые столбцы сильно больше других.

Картинку надо «нормализовать». Для этого служит аргумент histnorm.

Код



Как и предыдущие виды визуализаций, гистограммы могут иметь оформление:

  • подпись графика, подписи осей
  • ориентация и положение легенды.
  • отступы

Код



Другой интересны режим оформления — barmode='overlay' — он позволяет рисовать столбцы гистограммы одни поверх других.

Имеет смысл использовать его одновременно с аргументом opacity самих гистограмм — он задаёт прозрачность гистограммы (от 0 до 100%).

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

Код



Если мы говорим о вероятности, то имеет так же смысл построить и накопительную гистограмму. Например, вероятности выпадения не менее чем X очков на сумме из 2 игральных костей.

Для этого используется аргумент гистограммы cumulative_enabled=True

Код



Так же весьма полезно то, что на одной фигуре можно совмещать график, построенный по точкам (go.Scatter) и гистограмму (go.Histogram).

Для демонстрации такого применения, давайте сгенерируем 1000 событий из другого распределения — нормального. Для него легко построить теоретическую кривую. Мы возьмём для этого готовые функции из модуля scipy:

  • scipy.stats.norm.rvs — для генерации событий
  • scipy.stats.norm.pdf — для получения теоретический функции распределения

Код



Этот пример так же демонстрирует как происходит объединение в столбцы, если величина не дискретная.

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

В свою очередь это означает, что при необходимости мы можем регулировать количество столбцов и их ширину (это 2 взаимосвязанных параметра).

  • Вариант 1 — задав ширину столбца — xbins={«size»:0.1}
  • Вариант 2 — задав количество столбцов — nbinsx=200

Код



Другие столбчатые диаграммы — Bar Charts


Столбчатые диаграммы можно сформировать и своими силами, если сгруппировать данные и вычислить высоты столбцов.

Далее, используя класс go.Bar передаём названия столбцов и их величины в 2 аргумента:

  • x — подписи
  • y — величины

Код


Важно!

Как и круговая диаграмма, такая столбчатая в отличие от ранее изученных гистограмм не построит столбец для того, чего нет!

Например, если мы сделаем только 10 бросков по 2 кости, то среди них не может выпасть всех возможных случаев. А значит, они не отобразятся на диаграмме:

Код дырявой диаграммы без крайнего столбца


При необходимости выведения ВСЕХ, даже нулевых столбцов, их следует сформировать самостоятельно.

Код



Создадим парную гистограмму для 2 наборов по 100 бросков, в оба набора добавив на всякий случай колонки с нулями, если их нет.

В зависимости от генерации начальных данных в каких-то местах должна быть только 1 колонка, либо не будет колонок вообще.

Код



А вот если мы хотим вывести и 3й набор испытаний (1000 бросков), то придётся самостоятельно нормализовать данные, т.к. у go.Bar в отличие от гистограмм нет аргумента вроде histnorm.

Иначе вновь получим странную картинку, когда столбцы третьей серии сильно больше остальных и их нельзя сравнить.

Код



Зато есть у такой диаграммы и серьёзное преимущество — высота столбцов не обязана быть положительной. Это можно использовать, например, для построения диаграммы прибылей и убытков по периодам. Просто вычтем из одного набора значений другой, получим список чисел (некоторые отрицательные, некоторые положительные, а некоторые 0) и используем его для построения.

Так же это отличный повод вспомнить, что для столбчатых диаграмм работают все способы оформления, которые мы отработали на графиках. В данном случае логично выделить нулевой уровень красным цветом.

Код



А ещё можно вывести подписи прямо поверх столбцов. Для этого пригодится пара аргументов:

  • text — сюда передаём список тех значений, которые надо вывести (можно заранее сформировать произвольные строки)
  • textposition — способ вывода текста:
    • 'auto' — Plotly самостоятельно пример решение
    • 'outside' — снаружи, в случае столбчатой диаграммы это будет над столбцом с положительной высотой и под столбцом с отрицательной высотой
    • 'inside' внутри прямоугольника (если высота прямоугольника = 0, то надпись не будет видно)
    • и т.д.


Код



А что, если мы хотим вывести не вертикальные столбцы, а горизонтальные полоски?

Это легко сделать надо только:

  • Добавить аргумент orientation='h'
  • Поменять местами x и y в данных и подписях (а так же везде, где мы задаём подписи осей, осевые линии и т.п.)

И у нас получился вот такая "диаграмма торнадо" (так же этот способ подходит, например, если подписи столбцов слишком длинные и их неудобно читать снизу)

Код



Так же при отображении гистограмм 2 наборов данных есть полезный аргумент для слоя — barmode='stack'.

Он позволяет объединять столбцы в общие колонки. Это полезно, если мы представляем наши данные, как единую серию экспериментов, т.е. мы бросили 100 раз кубики, потом ещё 100 раз и хотим узнать что вышло в итоге, сколько всё-таки каких исходов.

Код



Ящики с усами (Box Plots)


А что, если требуется более сложный и информативный инструмент? Примером может служить диаграмма размаха или «ящик с усами» (ссылка)

Для примера создадим набор 100 событий с бросками набора других игральных костей. На этот раз 3 4-гранных кости (3d4). Это могло бы быть сравнением 2 игровых мечей с уроном 2d6 и 3d4, однако, любому очевидно, что второй эффективнее (разброс 2-12 против разброса 3-12). Вся ли это информация, которую можно «вытащить» из этих данных?

Конечно нет, ведь у них будут отличаться и меры центральной тенденции (медианы или средние).

Для построения ящиков с усами мы используем класс go.Box. Данные (весь массив «сумм») передаём в единственный аргумент — y.

Код


Не совсем понятно кто есть кто.

Ну или относительно понятно

Однако, как и для других фигур, тут можно задать подписи.

Код



Иногда вертикальные ящики не очень наглядны (либо сложно прочитать подписи снизу), тогда их можно положить «на бок» так же, как мы делали с обычными столбчатыми диаграммами:

Код



Иногда полезно для каждого ящика с усами так же отобразить облако точек, формирующий распределение. Это легко сделать с помощью аргумента boxpoints='all'

Код



Географические карты


Plotly поддерживает великое множество разных видов визуализаций, охватить все из которых в одном обзоре довольно трудно (и бессмысленно, т.к. общие принципы будут схожи с ранее показанными)

Полезно будет в завершении лишь показать один из наиболее красивых на мой взгляд «графиков» — Scattermapbox — геокарты.

Для этого возьмём CSV с 1117 населёнными пунктами РФ и их координатами (файл создан на основе github.com/hflabs/city/blob/master/city.csv) — 'https://raw.githubusercontent.com/hflabs/city/master/city.csv.

Воспользуемся классом go.Scattermapbox и 2 атрибутами:

  • lat (широта)
  • lon (долгота)

Так же нам понадобится подключить OSM карту, т.к. Scattermapbox может работать с разными видами карт:

fig.update_layout(mapbox_style="open-street-map")

Код



Как-то криво, правда? Давайте сдвинем центр карты так, чтобы он пришёлся на столицу нашей родины (вернее столицу родины автора этих строк, т.к. у читателя родина может быть иной).

Для этого нам понадобится объект go.layout.mapbox.Center или обычный словарь с 2 аргументами:

  • lat
  • lon

Этот объект/словарь мы передаём в качестве значения аргумента center словаря внутрь mapbox:

fig.update_layout(
    mapbox=dict(
        center=dict(
            lat=...,
            lon=...
        )
    )
)

Код



Неплохо, но масштаб мелковат (по сути сейчас отображается карта мира на которой 1/6 часть суши занимает далеко не всё полезное место).

Без ущерба для полезной информации можно слегка приблизить картинку.

Для этого используем аргумент zoom=2

Код



Увы, на карту попало слишком много Европы без данных и слишком мало отечественного дальнего востока, так что в данном случае центрироваться возможно стоит по геометрическому центру страны (вычислим его весьма «приблизительно»).

Код



Давайте добавим подписи городов. Для этого используем аргумент text.

Код



Вспомним, как мы увеличивали плотность информации для обычных графиков. Тут так же можно задать размер маркера, например, от населения.

Следует учесть 2 момента:

  • Данные замусорены. Население некоторых городов имеет вид 96[3]. Поэтому колонка с население не численная и нам нужна функция, которая этот мусор обнулит, либо приведёт к какому-то читаемому виду.
  • Размер маркера задаётся в пикселях. И 15 миллионов пикселей — слишком большой диаметр. Потому разумно придумать формулу, например, логарифм.

Код



Добавим цветовую кодировку. Для этого используем данные о годе основания. Т.к. не для всех городов он точно известен (для некоторых указан век, для некоторых римскими, а не арабскими цифрами), то мы так же вынуждены будем написать свою функцию для обработки годов, но для простоты все проблемные случаи мы будем возвращать None и потом просто удалим все такие города.

Если возвращать, например, np.NaN, то при построении тепловой карты эти значения будут считаться эквивалентными 0 и мы будем считать такие населённые пункты одними из самых старых в стране)

Код



А что если мы хотим нанести линию? Без проблем!

Возьмём и добавим новый график на имеющуюся картинку, который будет содержать только 2 точки: Москву и Санкт-Петербург.

Нам понадобится новый атрибут mode = «lines» (у него доступны и другие значения, например «markers+lines»), но мы уже вывели метку города, так что не хотим её дублировать.

Не будем выводить никакой информации при наведении на этот новый график, чтобы она не перебивала эффект наведения на основные точки. hoverinfo='skip'

Код



Ох, кажется полоска тепловой карты наложилась на легенду. Более того, теперь легенда выводится раздельная для точек-городов и линии между Москвой и Санкт-Петербургом.

  1. Переключим легенду в горизонтальный режим legend_orientation=«h» (в настройках слоя)
  2. «сгруппируем» легенды вместе. Для этого у каждого графика группы добавим аргумент legendgroup=«group» (можно использовать любые строки, лишь бы они были одинаковые у членов одной группы).

Код



Отлично, теперь они включаются и выключаются вместе. Давайте уберём из легенды «лишний» элемент (линию городов) showlegend=False

А так же подпишем легенду для городов.

Код



Давайте добавим чуть более осмысленные линии на карту. Для этого воспользуемся маршрутом поезда №002М «Россия» Москва-Владивосток (сайт РЖД)

Я заранее подготовил отдельный файл с городами, на маршруте, разбитом по дням. Это примерная разбивка, т.к. расписание меняется, так что не используйте мою таблицу для оценка когда вы приедете к любимой тёще в гости. Некоторые станции поезда не имеют аналога в нашей оригинальной таблице городов, поэтому они пропущена. Некоторые города указаны 2 раза, т.к. они являются конечной точкой одного дневного перегона и начальной другого дневного перегона.

Наш маршрут будет соединять города, а не вокзалы, так же он не будет совпадать с реальной железной дорогой. Это просто визуализация маршрута, а не инструмент навигации!

Код



Если мы хотим анимировать процесс появления маршрута по дням, то нам придётся использовать тот же приём, что и ранее с появлением нескольких графиков — заранее вывести все графики или их заглушки невидимыми, а потом на каждом фрейме и шаге слайдера делать их видимыми.

Код



Безусловно мы разобрали далеко не все виды графиков Plotly. Однако, данного базового набора примеров должно быть достаточно чтобы понять принцип по которому все они работают.

С примерами других визуализаций можно ознакомиться тут — plotly.com/python (обратите внимание, что для каждой категории приведены далеко не все примеры, больше примеров всегда доступно по ссылке «More ...»

Источник

Комментариев нет:

Отправить комментарий