How do you update seaborn to latest version (v0.9)?
This is my first question 😐 I am currently running seaborn version 0.8.1 in Python3. How do I update to v0.9?
asked Sep 21, 2018 at 0:01
116 1 1 gold badge 1 1 silver badge 8 8 bronze badges
Sep 21, 2018 at 0:52
1 Answer 1
Many ways you could do it, the most succinct and straight-forward way may be:
pip install seaborn --upgrade
If that doesn’t give you the correct version, you can specify it explicitly:
pip install seaborn==0.9.0
answered Sep 21, 2018 at 0:41
chickity china chinese chicken chickity china chinese chicken
7,749 2 2 gold badges 20 20 silver badges 50 50 bronze badges
- python-3.x
- seaborn
-
The Overflow Blog
Linked
Related
Hot Network Questions
Subscribe to RSS
Question feed
To subscribe to this RSS feed, copy and paste this URL into your RSS reader.
Site design / logo © 2023 Stack Exchange Inc; user contributions licensed under CC BY-SA . rev 2023.10.27.43697
By clicking “Accept all cookies”, you agree Stack Exchange can store cookies on your device and disclose information in accordance with our Cookie Policy.
seaborn: statistical data visualization
Seaborn is a Python visualization library based on matplotlib. It provides a high-level interface for drawing attractive statistical graphics.
Documentation
Online documentation is available at seaborn.pydata.org.
The docs include a tutorial, example gallery, API reference, FAQ, and other useful information.
To build the documentation locally, please refer to doc/README.md .
Dependencies
Seaborn supports Python 3.8+.
Installation requires numpy, pandas, and matplotlib. Some advanced statistical functionality requires scipy and/or statsmodels.
Installation
The latest stable release (and required dependencies) can be installed from PyPI:
pip install seaborn
It is also possible to include optional statistical dependencies:
pip install seaborn[stats]
Seaborn can also be installed with conda:
conda install seaborn
Note that the main anaconda repository lags PyPI in adding new releases, but conda-forge ( -c conda-forge ) typically updates quickly.
Citing
A paper describing seaborn has been published in the Journal of Open Source Software. The paper provides an introduction to the key features of the library, and it can be used as a citation if seaborn proves integral to a scientific publication.
Testing
Testing seaborn requires installing additional dependencies; they can be installed with the dev extra (e.g., pip install .[dev] ).
To test the code, run make test in the source directory. This will exercise the unit tests (using pytest) and generate a coverage report.
Code style is enforced with flake8 using the settings in the setup.cfg file. Run make lint to check. Alternately, you can use pre-commit to automatically run lint checks on any files you are committing: just run pre-commit install to set it up, and then commit as usual going forward.
Development
Seaborn development takes place on Github: https://github.com/mwaskom/seaborn
Please submit bugs that you encounter to the issue tracker with a reproducible example demonstrating the problem. Questions about usage are more at home on StackOverflow, where there is a seaborn tag.
Announcing the release of seaborn 0.12
Today sees the 0.12 release of seaborn, a Python library for data visualization. This is a major release that introduces an entirely new API along with numerous enhancements to existing functionality. This post highlights some notable features; see the release notes for complete details.
Introducing the objects interface
The biggest news in this release is the debut of the seaborn.objects interface, an entirely new approach to specifying graphics in seaborn.
The objects interface is the product of several years of work designing and implementing an API that is more declarative, composable, and extensible.
Taking inspiration from Wilkinson’s grammar of graphics — and its implementation in libraries such as ggplot2 and vega-lite — the objects interface offers a collection of classes that can be flexibly combined to specify a wide range of statistical graphics.
Here is a simple example, showing how you would make a scatter plot with dots colored by a categorical variable:
Like seaborn’s existing functions, you can get a complete plot by specifying only a minimal amount of information: where the data is coming from and how it should be visualized.
But while the plotting functions achieve simplicity by constraining what you can do with them, the objects interface will let you accomplish a lot more.
The design aims to alleviate some pain points that have emerged over the past decade of seaborn’s development and use. Namely, it will support a more cohesive customization experience, allowing most operations to be expressed within a common interface and avoiding the need for users to consult the matplotlib documentation when polishing a plot.
As this is the first official release, there will no doubt be some rough edges, and some key features have yet to be implemented. But it is ready for broad use. To learn more, check out the new tutorial, which introduces key concepts and demonstrates various usage patterns.
Enhancements to existing functionality
Not interested in the new interface? That’s OK: the existing functions aren’t going anywhere. While the focus is on the completely new tools, there are also a number of enhancements and fixes to the old ones.
These enhancements include some long-desired features, like a much more flexible approach to specifying error bars (complete with a new tutorial). The categorical scatterplot functions (stripplot and swarmplot) have been refactored: their default behavior is now more consistent with the rest of the library, and they’re sporting some new features that make them more flexible and easier to use. And all plotting functions now allow you to pass the data source first, making it easier to pipe a pandas dataframe into a seaborn plot.
Finally, the documentation has been revamped using the wonderful PyData sphinx theme. Not only does it look better, it should be more accessible and easier to navigate. And there is a new FAQ page with answers to common questions and explanations of notable gotchas.
How to upgrade
When you’re ready to upgrade, the new version is just a pip install away:
pip install seaborn==0.12.0
I hope you find it useful!
Шпаргалка по визуализации данных в Python с помощью Plotly
Plotly — библиотека для визуализации данных, состоящая из нескольких частей:
- Front-End на JS
- Back-End на Python (за основу взята библиотека Seaborn)
- Back-End на R
Извиняюсь за замыленные gif’ки это происходит при конвертации из видео, записанного с экрана.
Jupyter Notebook со всеми примерами из статьи:
Так же на базе plotly и веб-сервера Flask существует специальная библиотека для создания дашбордов Dash.
- Plotly — бесплатная библиотека, которую вы можете использовать в коммерческих целях
- Plotly работает offline
- Plotly позволяет строить интерактивные визуализации
Для начала необходимо установить библиотеку, т.к. она не входит ни в стандартный пакет, ни в Anaconda. Для этого рекомендуется использовать pip:
pip install plotly
Если вы используете Jupyter Notebook, то можно использовать мэджик «!», поставив данный символ перед командой:
!pip install plotly
Перед началом работы необходимо импортировать модуль. В разных частях шпаргалки для разных задач нам понадобятся как основной модуль, так и один из его подмодулей, поэтому полный набор инструкций импорта у нас.
Так же нам понадобятся библиотеки Pandas и Numpy для работы с сырыми данными
Код импорта функций
import plotly
import plotly.graph_objs as go
import plotly.express as px
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd
Линейный график
Начнём с простой задачи построения графика по точкам.
Используем функцию f(x)=x 2
Сперва поступим совсем просто и «в лоб»:
- Создадим график с помощью функции scatter из подмодуля plotly.express (внутрь передадим 2 списка точек: координаты X и Y)
- Тут же «покажем» его с помозью метода show()
Всё это делается с помощью JS в вашем браузере. А значит, при желании вы можете этим управлять уже после построения фигуры (но мы этого делать пожалуй не будем, т.к. JS != Python)
x = np.arange(0, 5, 0.1) def f(x): return x**2 px.scatter(x=x, y=f(x)).show()
Более читабельно и правильно записать тот же в код в следующем виде:
fig = px.scatter(x=x, y=f(x)) fig.show()
- Создаём фигуру
- Рисуем график
- Показываем фигуру
Но маловато гибкости, поэтому мы практически сразу переходим к более продвинутому уровню — сразу создадим фигуру и нанесём на неё объекты.
Так же сразу выведем фигуру для показа с помощью метода 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()
Как видим, отличия не только в коде, но и в результате — получилась гладкая кривая.
Кроме того, такой способ позволит нам нанести на график столько кривых, сколько мы хотим:
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x))) fig.add_trace(go.Scatter(x=x, y=x)) fig.show()
Погодите, что это такое? Справа появилась ещё и легенда!
Впрочем, логично, пока график был один, зачем нам легенда?
Но магия Plotly тут не заканчивается. Нажмите на любую из подписей в легенде и соответствующий график исчезнет, а надпись станет более бледной. Вернуть их позволит повторный клик.
Подписи графиков
Добавим атрибут name, в который передадим строку с именем графика, которое мы хотим отображать в легенде.
Plotly поддерживает LATEX в подписях (аналогично matplotlib через использование $$ с обеих сторон).
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), name='$$f(x)=x^2$$')) fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$')) fig.show()
К сожалению, это имеет свои ограничения, как можно заметить подсказка при наведении на график отображается в «сыром» виде, а не в LATEX.
Победить это можно, если использовать HTML разметку в подписях. В данном примере я буду использовать тег sup. Так же заметьте, что шрифт для LATEX и HTML отличается начертанием.
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$')) fig.show()
С увеличением длины подписи графика, легенда начала наезжать на график. Мне это не нравится, поэтому перенесём легенду вниз.
Для этого применим к фигуре метод update_layout, у которого нас интересует атрибут legend_orientation fig.update_layout(legend_orientation=»h»)
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$')) fig.update_layout(legend_orientation="h") fig.show()
Хорошо, но слишком большая часть рабочего пространства ноутбука не используется. Особенно это заметно сверху — большой отступ сверху до поля графика.
По умолчанию поля графика имеют отступ 20 пикселей. Мы можем задать свои значения отступам с помощью update_layout, у которого есть атрибут margin, принимающий словарь из отступов:
- l — отступ слева
- r — отступ справа
- t — отступ сверху
- b — отступ снизу
update_layout можно применять последовательно несколько раз, либо можно передать все аргументы в одну функцию (мы сделаем именно так)
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$')) fig.update_layout(legend_orientation="h", margin=dict(l=0, r=0, t=0, b=0)) fig.show()
Поскольку подписи в легенде короткие, мне не нравится, что они расположены слева. Я бы предпочёл выровнять их по центру.
Для этого можно использовать у update_layout атрибут legend, куда передать словарь с координатами для сдвига (сдвиг может быть и по вертикали, но мы используем только горизонталь).
Сдвиг задаётся в долях от ширины всей фигуры, но важно помнить, что сдвигается левый край легенды. Т.е. если мы укажем 0.5 (50% ширины), то надпись будет на самом деле чуть сдвинута вправо.
Т.к. реальная ширина зависит от особенностей вашего экрана, браузера, шрифтов и т.п., то этот параметр часто приходится подгонять. Лично у меня для этого примера неплохо работает 0.43.
Чтобы не шаманить с шириной, можно легенду относительно точки сдвига с помощью аргумента xanchor.
В итоге для легенды мы получим такой словарь:
legend=dict(x=.5, xanchor="center")
Код целиком
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), margin=dict(l=0, r=0, t=0, b=0)) fig.show()
Стоит сразу задать подписи к осям и графику в целом. Для этого нам вновь понадобится update_layout, у которого добавится 3 новых аргумента:
title="Plot Title", xaxis_title="x Axis Title", yaxis_title="y Axis Title",
Следует заметить, что сдвиги, которые мы задали ранее могут негавтивно сказаться на читаемости подписей (так заголовок графика вообще вытесняется из области видимости, поэтому я увеличу отступ сверху с 0 до 30 пикселей
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), title="Plot Title", xaxis_title="x Axis Title", yaxis_title="y Axis Title", margin=dict(l=0, r=0, t=30, b=0)) fig.show()
Вернёмся к самим графикам, и вспомним, что они состоят из точек. Выделим их с помощью атрибута mode у самих объектов Scatter.
Используем разные варианты выделения для демонстрации:
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='$$g(x)=x$$')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), margin=dict(l=0, r=0, t=0, b=0)) fig.show()
Теперь особенно заметно, что LATEX в функции g(x)=x отображается некорректно при наведении курсора мыши на точки.
Давайте скроем эту информацию.
Зададим для всех графиков с помощью метода update_traces поведение при наведении. Это регулирует атрибут hoverinfo, в который передаётся маска из имён атрибутов, например, «x+y» — это только информация о значениях аргумента и функции:
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='$$g(x)=x$$')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="x+y") fig.show()
Как-то недостаточно наглядно, не находите?
Давайте разрешим использовать информацию из всех аргументов и сами зададим шаблон подсказки.
- hoverinfo=«all»
- в hovertemplate передаём строку, используем HTML для форматирования, а имена переменных берём в фигурные скобки и выделяем %, например, %
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='g(x)=x')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
А что если мы хотим сравнить информацию на 2 кривых в точках, например, с одинаковых аргументом?
Т.к. это касается всей фигуры, нам нужен update_layout и его аргумент hovermode.
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='g(x)=x')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Кстати, маркерами можно управлять для конкретной кривой и явно.
Для этого используется аргумент marker, который принимает на вход словарь. Подробный пример.
А мы лишь ограничимся баловством:
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3)))) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Кажется теперь на графике плохо видно ту часть, где кривые пересекаются (вероятно наиболее интересную для нас).
Для этого у нас есть методы фигуры:
- update_yaxes — ось Y (вертикаль)
- update_xaxes — ось X (горизонталь)
fig = go.Figure() fig.update_yaxes(range=[-0.5, 1.5]) fig.update_xaxes(range=[-0.5, 1.5]) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3)))) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Хорошо, но правильно было бы нанести осевые линии.
Для этого у тех же функций есть 3 атрибута:
- zeroline — выводить или нет осевую линию
- zerolinewidth — задаёт толщину осевой (в пикселях)
- zerolinecolor — задаёт цвет осевой (строка, можно указать название цвета, можно его код, как принято в HTML-разметке)
fig = go.Figure() fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink') fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000') fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3)))) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Давайте добавим больше разных функций на наш график, но сделаем так, чтобы по умолчанию их не было видно.
Для этого у объекта Scatter есть специальный атрибут:
visible='legendonly'
Т.к. мы центрировали легенду относительно точки сдвига, то нам не пришлось менять величину сдвига с увеличением числа подписей.
def h(x): return np.sin(x) def k(x): return np.cos(x) def m(x): return np.tan(x) fig = go.Figure() fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink') fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000') fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)')) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)')) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)')) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2')) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3)))) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Наверное всё же не следует смешивать вместе тригонометрические и арифметические функции. Давайте отобразим их на разных, соседних графиках.
Для этого нам потребуется создать фигуру с несколькими осями.
Фигура с несколькими графиками создаётся с помощью подмодуля make_subplots.
Необходимо указать количество:
- row — строк
- col — столбцов
fig = make_subplots(rows=1, cols=2, specs=[[, ]])
fig = make_subplots(rows=1, cols=2) fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink') fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000') fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2'), 1, 2) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Заметили, наши изменения осей применились к обоим графикам?
Естественно, если у метода, изменяющего оси указать аргументы:
- row — координата строки
- col — координата столбца
fig = make_subplots(rows=1, cols=2) fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2) fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2) fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2'), 1, 2) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
А вот если бездумно использовать title, xaxis_title и yaxis_title для update_layout, то может выйти казус — подписи применятся только к 1 графику:
Код, приводящий к казусу
fig = make_subplots(rows=1, cols=2) fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2) fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2) fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2'), 1, 2) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=30, b=0)) fig.update_layout(title="Plot Title", xaxis_title="x Axis Title", yaxis_title="y Axis Title") fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Поэтому заголовки графиков можно задать, при создании фигуры, передав в аргумент subplot_titles кортеж/список с названиями.
Подписи осей под графиками можно поменять с помощью методов фигуры:
- fig.update_xaxes
- fig.update_yaxes
Код, подписывающий все графики независимо
fig = make_subplots(rows=1, cols=2, subplot_titles=("Plot 1", "Plot 2")) fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2) fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2) fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2'), 1, 2) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=40, b=0)) fig.update_layout(title="Plot Title") fig.update_xaxes(title='Ось X графика 1', col=1, row=1) fig.update_xaxes(title='Ось X графика 2', col=2, row=1) fig.update_yaxes(title='Ось Y графика 1', col=1, row=1) fig.update_yaxes(title='Ось Y графика 2', col=2, row=1) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
И конечно, если необходимо сделать так, чтобы один график был больше, а другой меньше, то для этого используется аргументы
- column_widths — задаёт отношения ширины графиков (в одной строке)
- row_heights — задаёт отношения высот графиков (в одном столбце)
Рассмотрим на примере ширин. Сделаем левый график вдвое шире правого, т.е. зададим соотношение 2:1.
fig = make_subplots(rows=1, cols=2, column_widths=[2, 1]) fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2) fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2) fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2'), 1, 2) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
А что если мы хотим выделить одному из графиков больше места, чем другим, например, 2 строки или наоборот, 2 столбца?
В matplotlib мы использовали бы несколько фигур, либо оси с заданными размерами, здесь у нас есть другой инструмент. Мы можем сказать каким-то осям объединиться вдоль колонок или вдоль строк.
Для этого нам потребуется написать спецификацию на фигуру (для начала очень простую).
Спецификация — это список (если точнее, то даже матрица из списков), каждый объект внутри которого — словарь, описывающий одни из осей.
Если каких-то осей нет (например, если их место занимают растянувшиеся соседи, то вместо словаря передаётся None.
Давайте сделаем матрицу 2х2 и объединим вместе левые графики, получив одни высокие вертикальные оси. Для этого первому графику передадим атрибут «rowspan» равный 2, а его нижнего соседа уничтожим (None):
fig = make_subplots(rows=2, cols=2, specs=[[, <>], [None, <>]]) fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2) fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2) fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 2, 2) fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 2, 2) fig.add_trace(go.Scatter(x=x, y=m(x), name='m(x)=tg(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2'), 1, 2) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Как видим, в вертикальный график идеально вписался тангенс, который отныне не невидим.
Для объединения используется:
- rowspan — по вертикали
- colspan — по горизонтали
Код, объединяющий ячейки в другом направлении
fig = make_subplots(rows=2, cols=2, specs=[[, None], [<>, <>]]) fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2) fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2) fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 2, 2) fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 2, 2) fig.add_trace(go.Scatter(x=x, y=m(x), name='m(x)=tg(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2'), 2, 1) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 2, 1) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Последний вариант получился слишком узким по вертикали.
Высоту легко увеличить в помощью атрибута height у метода update_layout.
Размеры фигуры регулируются 2 атрибутами:
- width — ширина (в пикселях)
- height — высота (в пикселях)
fig = make_subplots(rows=2, cols=2, specs=[[, None], [<>, <>]]) fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2) fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2) fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 2, 2) fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 2, 2) fig.add_trace(go.Scatter(x=x, y=m(x), name='m(x)=tg(x)'), 1, 1) fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2'), 2, 1) fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x', marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 2, 1) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), hovermode="x", margin=dict(l=0, r=0, t=0, b=0), height=1000, width=600) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Увеличиваем плотность информации
Тепловая карта
Вернёмся к 1 графику, но постараемся уместить на нём больше информации, используя цветовую кодировку (что-то вроде тепловой карты — чем выше значение некой величины, тем «теплее» цвет).
Для этого у объекта go.Scatter используем уже знакомый атрибут marker (напомним, он принимает словарь). Передаём следующие атрибуты в словарь:
- color — список значений по которым будут выбираться цвета. Элементов списка должно быть столько же, сколько и точек.
- colorbar — словарь, описывающий индикационную полосу цветов справа от графика. Принимает на вход словарь. Нас интересует пока только 1 значение словаря — title — заголовок полосы.
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2', marker=dict(color=h(x), colorbar=dict(title="h(x)=sin(x)")) )) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)')) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
В предыдущем примере цветовая шкала не очень похожа на тепловую карту.
На самом деле цвета на шкале можно изменить, для этого служит атрибут colorscale, в который передаётся имя палитры.
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2', marker=dict(color=h(x), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno') )) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)')) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Можно ли добавить больше информации? Конечно можно, но тут возникают хитрости.
Для ещё одного измерения можно использовать размер маркеров.
Важно. Размер — задаётся в пикселях, т.е. величина не отрицательная (в отличие от цвета), поэтому мы будем использовать модуль одной из функций.
так же, величины меньше 2 пикселей обычно плохо видны на экране, поэтому для размера мы добавим множитель.
Размеры задаётся атрибутом size того же словаря внутри marker. Этот атрибут принимает 1 значение (число), либо список (чисел).
fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x2', marker=dict(color=h(x), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno', size=50*abs(h(x))) )) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)')) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=abs(h(x)), name='h_mod(x)=|sin(x)|')) fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Анимация
Можно ли ещё уплотнить информацию на графике? Да, можно, если использовать «четвёртое измерение» — время. Это так же может быть полезно и само по себе для оживления вашего графика.
Вернёмся на пару шагов назад. Мы будем анимировать график построения параболы. Для этого нам понадобятся:
- Начальное состояние
- Кнопки (анимация не должна начинаться сама по себе, поэтому для начала мы создадим простую кнопку, её запускающую, а постепенно перейдём к временной шкале)
- Фреймы (или кадры) — промежуточные состояния
Это то, что будет на графике до начала анимации. В нашем случае это будет стартовая точка.
Уберём практически всё лишнее из предыдущих шагов
Шаг 1 — код полуфабрикат
fig = go.Figure() fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Код минимальной работоспособной кнопки выглядит так:
"updatemenus": []>]
updatemenus — это один из элементов слоя, т.е. layout фигуры, а значит, мы добавим кнопку с помощью метода update_layout.
Пока она не будет ничего делать, т.к. у нас нечего анимировать.
Шаг 2 — код всё ещё полуфабрикат
fig = go.Figure() fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')) fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])], margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Это список «кадров» из которых состоит наша анимация.
Каждый фрейм должен содержать внутри себя целиком готовый график, который просто будет отображаться на нашей фигуре, как в декорациях.
Фрейм создаётся с помощью 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 — Ёлочка гори, в смысле код, запускающий анимацию
fig = go.Figure() fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')) frames=[] for i in range(1, len(x)): frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]))])) fig.frames = frames fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])], margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Другой способ задать начальное состояние, слой (с кнопками) и фреймы — сразу передать всё в объект go.Figure:
- data — атрибут для графика с начальным состоянием
- layout — описание «декораций» включая кнопки
- frames — фреймы (кадры) анимации
frames=[] for i in range(1, len(x)): frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]))])) fig = go.Figure(data=go.Scatter(x=[x[0]], y=[f(x[0])], mode='lines+markers', name='f(x)=x2'), frames=frames, layout=dict(legend_orientation="h", legend=dict(x=.5, xanchor="center"), updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])], margin=dict(l=0, r=0, t=0, b=0))) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Естественно, если добавить на графики (как на начальный, так и те, что во фреймах) маркеры с указанием цвета, цветовой шкалы и размера, то анимация будет более сложного графика.
fig = go.Figure() fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2', marker=dict(color=h(x[0]), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno', size=50*abs(h(x[0]))) )) frames=[] for i in range(1, len(x)): frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]), marker=dict(color=h(x[:i+1]), size=50*abs(h(x[:i+1]))))])) fig.frames = frames fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])], margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Заметим, что код простейшей кнопки, которая запускает воспроизведение видео выглядит так:
dict(label="Play", method="animate", args=[None])
dict(label="", method="animate", args=[None])
Если мы хотим добавить кнопку «пауза» (в отличие от стандартной паузы повторное нажатие не будет вызывать воспроизведение, для начала воспроизведения придётся нажат Play), код усложнится:
dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])
Правда, если добавить 2 такие кнопки, то вы заметите, что кнопка play, нажатая после паузы, в итоге начинает воспроизведение с начала. Это не совсем интуитивное поведение, поэтому ей следует добавить ещё 1 аргумент:
dict(label="", method="animate", args=[None, ])
Теперь полный набор из 2 наших кнопок будет выглядеть так:
buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])]
Код с 2 кнопками: запуска и остановки анимации
fig = go.Figure() fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2', marker=dict(color=h(x[0]), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno', size=50*abs(h(x[0]))) )) frames=[] for i in range(1, len(x)): frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]), marker=dict(color=h(x[:i+1]), size=50*abs(h(x[:i+1]))))])) fig.frames = frames fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), updatemenus=[dict(type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Иногда полезно перенести кнопки в другоме место. Рассмотрим некоторые из атрибутов, которые с этим помогут:
- direction — направление расположения кнопок (по умолчанию сверху-вниз, но если указать «left», то будет слева-направо)
- x, y — положение (в долях от фигуры)
- xanchor, yanchor — как выравнивать кнопки. У нас была раньше проблема с выравниванием легенд, тут та же история. Если хотим выровнять по центру, то x=0.5 и xanchor=«center» помогут.
Код с кнопками внизу
fig = go.Figure() fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2', marker=dict(color=h(x[0]), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno', size=50*abs(h(x[0]))) )) frames=[] for i in range(1, len(x)): frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]), marker=dict(color=h(x[:i+1]), size=50*abs(h(x[:i+1]))))])) fig.frames = frames fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), updatemenus=[dict(direction="left", x=0.5, xanchor="center", y=0, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], margin=dict(l=0, r=0, t=0, b=0)) fig.update_traces(hoverinfo="all", hovertemplate ) fig.show()
Слайдер
Слайдер по принципу работы похож на анимацию, но есть серьёзное отличие.
Слайдер — это элемент навигации, полоска по которой скользит ползунок, который управляет состоянием графиков на фигуре.
Т.е. если фреймы в анимации меняются один за другим, то в случае использования слайдера все графики одновременно есть. Но большая часть из них невидима. И при перемещении ползунка мы просто какие-то скрываем, а другие наоборот показываем (и перестраиваем оси, конечно).
1. Создаём список графиков. Важно, что один из графиков (например, 1й) должен быть видимым.
Для установления видимости/невидимости используется аргумент visible:
- Видимый график — go.Scatter(visible=True, x=[x[0]], y=[f(x)[0]], mode=’lines+markers’, name=’f(x)=x 2 ‘)
- Невидимый график — go.Scatter(visible=False, x=[x[0]], y=[f(x)[0]], mode=’lines+markers’, name=’f(x)=x 2 ‘)
2. Создаём список «шагов» слайдера.
Шаг имеет определённый синтаксис. По сути он описывает состояние (какие графики видимы, какие нет) и метод перехода к нему.
Шаг должен описывать состояние всех графиков.
Минимальный синтаксис 1 шага:
dict(
method = ‘restyle’,
args = #СПИСОК СОСТОЯНИЙ ВСЕХ ГРАФИКОВ
)
Состояние видимости/невидимости задаётся парой ‘visible’ и списка логических значений (какие графики ‘visible’, а какие нет). Поэтому для КАЖДОГО шага мы создадим список False, а потом поменяем нужное значение на True, чтобы показать какой-то конкретный график.
Шаги нужно собрать в список (т.е. это будет список словарей).
Наконец все шаги надо добавить в фигуру:
Минимально рабочий код (внимательно изучите его, т.к. там есть несколько тонких моментов):
num_steps = len(x) trace_list = [go.Scatter(visible=True, x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')] for i in range(1, len(x)): trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=f(x[:i+1]), mode='lines+markers', name='f(x)=x2')) fig = go.Figure(data=trace_list) steps = [] for i in range(num_steps): # Hide all traces step = dict( method = 'restyle', args = ['visible', [False] * len(fig.data)], ) # Enable trace we want to see step['args'][1][i] = True # Add step to step list steps.append(step) sliders = [dict( steps = steps, )] fig.layout.sliders = sliders fig.show()
Если мы хотим добавить не 1 график, а 2, то добавить их придётся парой везде:
- в первоначальное активное состояние
- все неактивные (скрытые)
- парами генерировать состояние шагов
- парами активировать видимые на шаге графики
num_steps = len(x) trace_list = [go.Scatter(visible=True, x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(visible=True, x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)')] # Так выглядел процесс добавления этих функций раньше: #fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1) #fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1) for i in range(1, len(x)): trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)')) trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)')) fig = go.Figure(data=trace_list) steps = [] for i in range(num_steps): step = dict( method = 'restyle', args = ['visible', [False] * len(fig.data)], ) step['args'][1][2*i] = True step['args'][1][2*i+1] = True steps.append(step) sliders = [dict( steps = steps, )] fig.layout.sliders = sliders fig.show()
Просто добавим кнопки из предыдущей части. Они ничего не будут делать, т.к. мы ещё никакой анимации не добавили. Просто «прицелимся».
Код слайдера с нерабочими кнопками анимации
num_steps = len(x) trace_list = [go.Scatter(visible=True, x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(visible=True, x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)')] for i in range(1, len(x)): trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)')) trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)')) fig = go.Figure(data=trace_list) steps = [] for i in range(num_steps): step = dict( method = 'restyle', args = ['visible', [False] * len(fig.data)], ) step['args'][1][2*i] = True step['args'][1][2*i+1] = True steps.append(step) sliders = [dict( steps = steps, )] fig.update_layout(updatemenus=[dict(direction="left", x=0.5, xanchor="center", y=0, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], ) fig.layout.sliders = sliders fig.show()
Если просто добавить фреймы с анимацией так, как мы это делали раньше, то кнопки анимации конечно заработают. Но только на 1 состоянии слайдера.
Код искалеченного слайдера
num_steps = len(x) trace_list = [go.Scatter(visible=True, x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(visible=True, x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)')] for i in range(1, len(x)): trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)')) trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)')) frames=[] for i in range(1, len(x)): frames.append(go.Frame(data=[go.Scatter(visible=True, x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(visible=True, x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)')])) fig = go.Figure(data=trace_list) steps = [] for i in range(num_steps): step = dict( method = 'restyle', args = ['visible', [False] * len(fig.data)], ) step['args'][1][2*i] = True step['args'][1][2*i+1] = True steps.append(step) sliders = [dict( steps = steps, )] fig.update_layout(updatemenus=[dict(direction="left", x=0.5, xanchor="center", y=0, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], ) fig.layout.sliders = sliders fig.frames = frames fig.show()
ОК, значит нам придётся сделать «финт ушами» и отказаться от невидимых графиков, а привязать слайдер непосредственно к фреймам анимации (Почему мы так не сделали сразу? Чтобы был альтернативный способ, подходящий непосредственно для слайдера без изысков)
- Закомментируем всё, что касается trace_list — списка наших «невидимых» графиков.
- Вынесем созданием 2 видимых графиков в первоначальное создание фигуры
- Добавим параметр name каждому фрейму
- Переделаем шаблон шага:
- Добавим label, совпадающий с именем соответствующего фрейма
- Сменим метод на «animate»
- Все аргументы заменим на 1 список из 1 единственной строки, совпадающей с именем фрейма
- Уберём «проявление» невидимых графиков на определённых позициях слайдера, т.к. теперь слайдер будет привязан к фреймам
Код относительно работающего слайдера с кнопками анимации
num_steps = len(x) fig = go.Figure(data=[go.Scatter(x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)')]) ''' trace_list = [go.Scatter(visible=True, x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(visible=True, x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)')] for i in range(1, len(x)): trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)')) trace_list.append(go.Scatter(visible=False, x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)')) ''' frames=[] for i in range(0, len(x)): frames.append(go.Frame(name=str(i), data=[go.Scatter(x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)')])) #fig = go.Figure(data=trace_list) steps = [] for i in range(num_steps): step = dict( #method = 'restyle', #args = ['visible', [False] * len(fig.data)], label = str(i), method = "animate", args = [[str(i)]] ) #step['args'][1][2*i] = True #step['args'][1][2*i+1] = True steps.append(step) sliders = [dict( steps = steps, )] fig.update_layout(updatemenus=[dict(direction="left", x=0.5, xanchor="center", y=0, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], ) fig.layout.sliders = sliders fig.frames = frames fig.show()
Единственный момент, который слегка раздражает — когда происходит «воспроизведение», то ползунок слайдера не ползёт.
Это легко исправить с помощью аргумента showactive для меню. Так же его выключение снимет состояние «нажато» с кнопок Play/Pause.
Код слайдера, который уже нормально анимирован
num_steps = len(x) fig = go.Figure(data=[go.Scatter(x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)')]) frames=[] for i in range(0, len(x)): frames.append(go.Frame(name=str(i), data=[go.Scatter(x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)'), go.Scatter(x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)')])) steps = [] for i in range(num_steps): step = dict( label = str(i), method = "animate", args = [[str(i)]] ) steps.append(step) sliders = [dict( steps = steps, )] fig.update_layout(updatemenus=[dict(direction="left", x=0.5, xanchor="center", y=0, showactive=False, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], ) fig.layout.sliders = sliders fig.frames = frames fig.show()
Конечно добавим данные тепловой карты и размер маркера для увеличения плотности информации
Заметьте, что colorbar мы добавили всего 1 раз, однако, в него пришлось внести некоторые правки — мы сдвинули его по вертикали слегка вниз, т.к. теперь в правой колонке у нас есть легенда.
num_steps = len(x) fig = go.Figure(data=[go.Scatter(x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)', marker=dict(color=[f(x[0])], colorbar=dict(yanchor='top', y=0.8, title ), colorscale='Inferno', size=[50*abs(h(x[0]))])), go.Scatter(x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)', marker=dict(color=[f(x[0])], colorscale='Inferno', size=[50*abs(k(x[0]))]))]) frames=[] for i in range(0, len(x)): frames.append(go.Frame(name=str(i), data=[go.Scatter(x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)', marker=dict(color=f(x[:i+1]), colorscale='Inferno', size=50*abs(h(x[:i+1])))), go.Scatter(x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)', marker=dict(color=f(x[:i+1]), colorscale='Inferno', size=50*abs(k(x[:i+1]))))])) steps = [] for i in range(num_steps): step = dict( label = str(i), method = "animate", args = [[str(i)]] ) steps.append(step) sliders = [dict( steps = steps, )] fig.update_layout(updatemenus=[dict(direction="left", x=0.5, xanchor="center", y=0, showactive=False, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], ) fig.layout.sliders = sliders fig.frames = frames fig.show()
Осталось немного облагородить панель слайдера.
Добавим подписи к графику и осям, увеличим и оформим подпись текущего значения слайдера (в других обстоятельствах он стал бы временной шкалой), сместим кнопки анимации влевой, а слайдер чуть сожмём, чтобы освободить им место.
- Аргумент currentvalue — задаёт форматирование подписи к текущему шагу, включая префикс, положение на слайде, шрифт
- Аргументы x, y, xanchor, yanchor, pad — задают положение и отступы для слайдера и их синтаксис аналогичен таковому у кнопок
num_steps = len(x) fig = go.Figure(data=[go.Scatter(x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)', marker=dict(color=[f(x[0])], colorbar=dict(yanchor='top', y=0.8, title ), colorscale='Inferno', size=[50*abs(h(x[0]))])), go.Scatter(x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)', marker=dict(color=[f(x[0])], colorscale='Inferno', size=[50*abs(k(x[0]))]))]) frames=[] for i in range(0, len(x)): frames.append(go.Frame(name=str(i), data=[go.Scatter(x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)', marker=dict(color=f(x[:i+1]), colorscale='Inferno', size=50*abs(h(x[:i+1])))), go.Scatter(x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)', marker=dict(color=f(x[:i+1]), colorscale='Inferno', size=50*abs(k(x[:i+1]))))])) steps = [] for i in range(num_steps): step = dict( label = str(i), method = "animate", args = [[str(i)]] ) steps.append(step) sliders = [dict( currentvalue = >, len = 0.9, x = 0.1, pad = , steps = steps, )] fig.update_layout(title="Строим синус и косинус по шагам", xaxis_title="Ось X", yaxis_title="Ось Y", updatemenus=[dict(direction="left", pad = , x = 0.1, xanchor = "right", y = 0, yanchor = "top", showactive=False, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], ) fig.layout.sliders = sliders fig.frames = frames fig.show()
Возникает вопрос зачем же мы делали вариант с невидимыми графиками, если они не пригодились?
На самом деле они нужны, в том числе для анимации. Дело в том, что если вы хотите на разных слайдах анимации показывать разное количество графиков, то вам надо в самом начале на этапе создания фигуры добавить столько графиков, сколько их может отображаться максимально. Они все должны быть невидимыми.
Я специально задам 2 переменные:
- graphs_invisible — содержит как невидимый корректный график, так и пустой объект графика вообще без указания видимости
- graphs_visible — содержит корректные видимые графики, которые надо показывать по очереди
graphs_invisible = [go.Scatter(visible = False, x=x, y=f(x), name='f(x)=x2'), go.Scatter(visible = False, x=x, y=x, name='g(x)=x'), go.Scatter(visible = False, x=x, y=h(x), name='h(x)=sin(x)'), go.Scatter(visible = False, x=x, y=k(x), name='k(x)=cos(x)')] graphs_visible = [go.Scatter(visible = True, x=x, y=f(x), name='f(x)=x2'), go.Scatter(visible = True, x=x, y=x, name='g(x)=x'), go.Scatter(visible = True, x=x, y=h(x), name='h(x)=sin(x)'), go.Scatter(visible = True, x=x, y=k(x), name='k(x)=cos(x)')] fig = go.Figure(data=graphs_invisible) frames=[] for i in range(len(graphs_visible)+1): frames.append(go.Frame(name=str(i), data=graphs_visible[:i]+graphs_invisible[i:])) steps = [] for i in range(len(graphs_visible)+1): step = dict( label = str(i), method = "animate", args = [[str(i)]] ) steps.append(step) sliders = [dict( currentvalue = >, len = 0.9, x = 0.1, pad = , steps = steps, )] fig.update_layout(title="Выводим графики по очереди", xaxis_title="Ось X", yaxis_title="Ось Y", updatemenus=[dict(direction="left", pad = , x = 0.1, xanchor = "right", y = 0, yanchor = "top", showactive=False, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], ) fig.layout.sliders = sliders fig.frames = frames fig.show()
Философский вопрос — зачем мы так мучаемся, если есть plotly.express?
Документация plotly по анимации plotly.com/python/animations начинается с феерического примера:
Код, создающий довольно сложную анимацию данных с помощью Plotly Express
import plotly.express as px df = px.data.gapminder() px.scatter(df, x="gdpPercap", y="lifeExp", animation_frame="year", animation_group="country", size="pop", color="continent", hover_name="country", log_x=True, size_max=55, range_x=[100,100000], range_y=[25,90])
Действительно, функции Экспресса принимают на вход датафреймы (да, обычные из Pandas), вам лишь нужно указать колонки по которым производится агрегация данных. И можно сразу строить и тепловые карты, и анимации очень небольшим количеством кода, как в этом примере:
Ответ и прост и сложен:
- Стандартные примеры Экспресса могут не удовлетворить ваших потребностей, нужно что-то чуть более сложное и хитрое, например, совместить график и гистограмму. Вручную вы получаете больше гибкости.
- Понять как сгруппировать и агрегировать данные в датафрейме для такой визуализации порой сложнее, чем просто построить набор картинок для фреймов анимации/слайдера.
Показ 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 легко поместить на локальный сервер и поменять ссылки)
2. Блок, где будет выводиться график
Важно понимать, что Plotly по умолчанию не фиксирует размеры графика, поэтому вы можете применить к контейнеру свои стили, сверстав его такого размера и в том месте, где это необходимо вам. Plotly постарается занять всё свободное место по ширине.
Важно использовать действительно уникальный id — именно по нему plotly будет находить место на странице, куда надо встроить график.
3. JS переменная, содержащая наш JSON, описывающий график. Мы сформировали её и сложили в файл целиком. Вы можете выводить её непосредственно в коде страницы.
4. Вызов JS функции plotly, которая построит график.
- первым передаётся id контейнера, в котором выводить график
- вторым передаётся переменная, содержащая JSON Plotly объекта
А теперь испробуйте код на каком-нибудь своём графике!
Круговые диаграммы
Для полноты картины рассмотрим несколько других способов визуализации данных, кроме линейных графиков. Начнём с круговых диаграмм
Для нашего эксперимента «подбросим» 100 раз пару игральных кубиков (костей) и запишем суммы выпавших очков.
dices = pd.DataFrame(np.random.randint(low=1, high=7, size=(100, 2)), columns=('Кость 1', 'Кость 2')) dices['Сумма'] = dices['Кость 1'] + dices['Кость 2'] # Первые 5 бросков игральных костей display(dices.head()) sum_counts = dices['Сумма'].value_counts().sort_index() # количество выпавших сумм display(sum_counts)
Для того чтобы создать круговую диаграмму используем go.Pie, который добавляем так же, как мы добавляли график на созданную фигуру.
Используем 2 основных атрибута:
- values — размер сектора диаграммы, в нашем случае прямо пропорционален количеству той или иной суммы
- labels — подпись сектора, в нашем случае значение суммы. Если не передать подпись, то в качестве подписи будет взят индекс значения из списка values
fig = go.Figure() fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index)) fig.show()
Сразу бросается в глаза то, что хотя мы передали массив, упорядоченный по индексам, но при построении он был пересортирован по значениям.
Это легко исправить с помощью аргумента sort = False
fig = go.Figure() fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, sort = False)) fig.show()
Так же при желании мы можем «выдвинуть» один или несколько секторов.
Для этого используем аргумент pull, который принимаем список чисел. Каждое число — доля, на которую надо выдвинуть сектор из круга:
- 0 — не выдвигать
- 1 — 100% радиуса круга
Обратите внимание, мы не используем метод idxmax Pandas, т.к. наш массив имеет индексы, соответствующие суммам. А определение какой сектор выдвигать на диаграмме происходит по индексу списка, к которому наш массив приводится.
fig = go.Figure() pull = [0]*len(sum_counts) pull[sum_counts.tolist().index(sum_counts.max())] = 0.2 fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, pull=pull)) fig.show()
Если вам не нравятся классические круговые диаграммы «пирожки», то легко превратить их в «пончики», вырезав сердцевину. Для этого используем аргумент hole, в который передаём число (долю радиуса, которую надо удалить):
- 0 — не вырезать ничего
- 1 — 100% вырезать, ничего не оставить
fig = go.Figure() pull = [0]*len(sum_counts) pull[sum_counts.tolist().index(sum_counts.max())] = 0.2 fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, pull=pull, hole=0.9)) fig.show()
Кстати, образовавшаяся «дырка от бублика» — идеальное место для подписи, которую можно сделать с помощью атрибута annotations слоя.
Не забываем, что аннотаций может быть много, поэтому annotations принимаем список словарей.
Текст аннотации поддерживает HTML разметку (чем мы воспользуемся, задав абсурдно длинный текст, не помещающийся в 1 строку)
fig = go.Figure() pull = [0]*len(sum_counts) pull[sum_counts.tolist().index(sum_counts.max())] = 0.2 fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, pull=pull, hole=0.9)) fig.update_layout( annotations=[dict(text='Суммы очков
при броске
2 игральных костей', x=0.5, y=0.5, font_size=20, showarrow=False)]) fig.show()
Естественно обычный способы оформления визуализаций, показанные для графиков, тут тоже работают:
fig = go.Figure() pull = [0]*len(sum_counts) pull[sum_counts.tolist().index(sum_counts.max())] = 0.2 fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, pull=pull, hole=0.9)) fig.update_layout( title="Пример кольцевой/круговой диаграммы", title_x = 0.5, margin=dict(l=0, r=0, t=30, b=0), legend_orientation="h", annotations=[dict(text='Суммы очков
при броске
2 игральных костей', x=0.5, y=0.5, font_size=20, showarrow=False)]) fig.show()
Что, если вы хотим детализовать картинку?
Sunburst или диаграмма «солнечные лучи»
Нам на помощь приходит диаграмма «солнечные лучи» — иерархическая диаграмма на основе круговой. По сути это набор кольцевых диаграмм, нанизанных друг на друга, причём сегменты следующего уровня находятся в пределах границ сегментов своего «родителя» на предыдущем.
Например, получить 8 очков с помощью 2 игральных костей можно несколькими способами:
- 2 + 6
- 3 + 5
- 4 + 4
- values — значения, задающие долю от круга на диаграмме
- branchvalues=«total» — такое значение указывает, что значения родительского элемента являются суммой значений потомков. Это необходимо для того, чтобы составить полный круг на каждом уровне.
- labels — список подписей, которые отображаются на диаграмме
- parents — список подписей родителей, для построения иерархии. Для элементов 0 уровня (без родителей) родителем указывается пустая строка.
# 1-й уровень, центр диаграммы labels = ["Всего событий: " + str(sum(sum_counts))] parents = [""] values = [sum(sum_counts)] # 2-й уровень, "лепестки" диаграммы second_level_dict = Σ = ' + str(x) for x in sum_counts.index> labels += map(lambda x: second_level_dict[x], sum_counts.index) parents += [labels[0]]*len(sum_counts) values += sum_counts.tolist() fig = go.Figure(go.Sunburst( labels = labels, parents = parents, values=values, branchvalues="total" )) #fig.update_layout(margin = dict(t=0, l=0, r=0, b=0)) fig.show()
А теперь добавим группировку по парам исходов игральных костей и вычисление для таких пар «родителей».
Конечно, если кости идентичны, то 6+2 и 2+6 — это идентичные исходы, как и пара 3+5 и 5+3, но в рамках следующего примера мы будем считать их разными, просто чтобы не добавлять лишнего кода.
Так же уменьшим отступы, т.к. подписи получаются уж очень мелкими.
# 1-й уровень, центр диаграммы labels = ["Всего событий: " + str(sum(sum_counts))] parents = [""] values = [sum(sum_counts)] # 2-й уровень, "промежуточный" second_level_dict = Σ = ' + str(x) for x in sum_counts.index> labels += map(lambda x: second_level_dict[x], sum_counts.index) parents += [labels[0]]*len(sum_counts) values += sum_counts.tolist() # Готовим DataFrame для 3 уровня (группируем ) third_level = dices.groupby(['Кость 1', 'Кость 2']).count().reset_index() third_level.rename(columns=, inplace=True) third_level['Сумма'] = third_level['Кость 1'] + third_level['Кость 2'] third_level['Label'] = third_level['Кость 1'].map(str) + ' + ' + third_level['Кость 2'].map(str) third_level['Parent'] = third_level['Сумма'].map(lambda x: second_level_dict[x]) # 3-й уровень, "лепестки" диаграммы values += third_level['Value'].tolist() parents += third_level['Parent'].tolist() labels += third_level['Label'].tolist() fig = go.Figure(go.Sunburst( labels = labels, parents = parents, values=values, branchvalues="total" )) fig.update_layout(margin = dict(t=0, l=0, r=0, b=0)) fig.show()
Гистограммы
Естественно не круговыми диаграммами едиными, иногда нужны и обычные столбчатые.
Простейшая гистограмма строится с помощью go.Histogram. В качестве единственного аргумента в x передаём список значений, которые участвуют в выборке (Plotly самостоятельно сгруппирует их в столбцы и вычислит высоту), в нашем случае это колонка с суммами.
fig = go.Figure(data=[go.Histogram(x=dices['Сумма'])]) fig.show()
Если по какой-то причине нужно построить не вертикальную, а горизонтальную гистограмму, то меняем x на y:
fig = go.Figure(data=[go.Histogram(y=dices['Сумма'])]) fig.show()
А что, если у нас 2 или 3 набора данных и мы хотим их сравнить? Сгенерируем ещё 1100 бросков пар кубиков и просто добавим на фигуру 2 гистограммы:
dices2 = pd.DataFrame(np.random.randint(low=1, high=7, size=(100, 2)), columns=('Кость 1', 'Кость 2')) dices2['Сумма'] = dices2['Кость 1'] + dices2['Кость 2'] dices3 = pd.DataFrame(np.random.randint(low=1, high=7, size=(1000, 2)), columns=('Кость 1', 'Кость 2')) dices3['Сумма'] = dices3['Кость 1'] + dices3['Кость 2'] fig = go.Figure() fig.add_trace(go.Histogram(x=dices['Сумма'])) fig.add_trace(go.Histogram(x=dices2['Сумма'])) fig.add_trace(go.Histogram(x=dices3['Сумма'])) fig.show()
Все 3 выборки подчиняются одному и тому же распределению, и очевидно, но количество событий сильно отличается, поэтому на нашей гистограмме некоторые столбцы сильно больше других.
Картинку надо «нормализовать». Для этого служит аргумент histnorm.
fig = go.Figure() fig.add_trace(go.Histogram(x=dices['Сумма'], histnorm='probability density')) fig.add_trace(go.Histogram(x=dices2['Сумма'], histnorm='probability density')) fig.add_trace(go.Histogram(x=dices3['Сумма'], histnorm='probability density')) fig.show()
Как и предыдущие виды визуализаций, гистограммы могут иметь оформление:
- подпись графика, подписи осей
- ориентация и положение легенды.
- отступы
fig = go.Figure() fig.add_trace(go.Histogram(x=dices['Сумма'], histnorm='probability density', name='100 бросков v.1')) fig.add_trace(go.Histogram(x=dices2['Сумма'], histnorm='probability density', name='100 бросков v.2')) fig.add_trace(go.Histogram(x=dices3['Сумма'], histnorm='probability density', name='1000 бросков')) fig.update_layout( title="Пример гистограммы на основе бросков пары игральных костей", title_x = 0.5, xaxis_title="сумма очков", yaxis_title="Плотность вероятности", legend=dict(x=.5, xanchor="center", orientation="h"), margin=dict(l=0, r=0, t=30, b=0)) fig.show()
Другой интересны режим оформления — barmode=’overlay’ — он позволяет рисовать столбцы гистограммы одни поверх других.
Имеет смысл использовать его одновременно с аргументом opacity самих гистограмм — он задаёт прозрачность гистограммы (от 0 до 100%).
Однако, большое количество гистограмм в таком случае тяжело визуально интерпретировать, поэтому мы скроем одну.
fig = go.Figure() fig.add_trace(go.Histogram(x=dices['Сумма'], histnorm='probability density', opacity=0.75, name='100 бросков v.1')) fig.add_trace(go.Histogram(x=dices2['Сумма'], histnorm='probability density', opacity=0.75, name='100 бросков v.2')) #fig.add_trace(go.Histogram(x=dices3['Сумма'], histnorm='probability density', opacity=0.75, name='1000 бросков')) fig.update_layout( title="Пример гистограммы на основе бросков пары игральных костей", title_x = 0.5, xaxis_title="сумма очков", yaxis_title="Плотность вероятности", legend=dict(x=.5, xanchor="center", orientation="h"), barmode='overlay', margin=dict(l=0, r=0, t=30, b=0)) fig.show()
Если мы говорим о вероятности, то имеет так же смысл построить и накопительную гистограмму. Например, вероятности выпадения не менее чем X очков на сумме из 2 игральных костей.
Для этого используется аргумент гистограммы cumulative_enabled=True
fig = go.Figure() fig.add_trace(go.Histogram(x=dices['Сумма'], histnorm='probability density', cumulative_enabled=True, name='100 бросков v.1')) fig.add_trace(go.Histogram(x=dices2['Сумма'], histnorm='probability density', cumulative_enabled=True, name='100 бросков v.2')) fig.add_trace(go.Histogram(x=dices3['Сумма'], histnorm='probability density', cumulative_enabled=True, name='1000 бросков')) fig.update_layout( title="Пример накопительной гистограммы на основе бросков пары игральных костей", title_x = 0.5, xaxis_title="сумма очков", yaxis_title="Вероятность", legend=dict(x=.5, xanchor="center", orientation="h"), margin=dict(l=0, r=0, t=30, b=0)) fig.show()
Так же весьма полезно то, что на одной фигуре можно совмещать график, построенный по точкам (go.Scatter) и гистограмму (go.Histogram).
Для демонстрации такого применения, давайте сгенерируем 1000 событий из другого распределения — нормального. Для него легко построить теоретическую кривую. Мы возьмём для этого готовые функции из модуля scipy:
- scipy.stats.norm.rvs — для генерации событий
- scipy.stats.norm.pdf — для получения теоретический функции распределения
В качестве начального и конечного значений аргумента (x) возьмём границы интервала в 3σ
from scipy.stats import norm r = norm.rvs(size=1000) x_norm = np.linspace(norm.ppf(0.01), norm.ppf(0.99), 100) fig = go.Figure() fig.add_trace(go.Histogram(x=r, histnorm='probability density', name='"Экспериментальные" данные')) fig.add_trace(go.Scatter(x=x_norm, y=norm.pdf(x_norm), name='Теоретическая форма нормального распределения')) fig.update_layout( title="Пример гистограммы на основе нормального распределения", title_x = 0.5, legend=dict(x=.5, xanchor="center", orientation="h"), margin=dict(l=0, r=0, t=30, b=0)) fig.show()
Этот пример так же демонстрирует как происходит объединение в столбцы, если величина не дискретная.
В данном случае каждый столбец тем выше, чем больше значений попало в интервал, соответствующий ширине этого столбца.
В свою очередь это означает, что при необходимости мы можем регулировать количество столбцов и их ширину (это 2 взаимосвязанных параметра).
- Вариант 1 — задав ширину столбца — xbins=
- Вариант 2 — задав количество столбцов — nbinsx=200
fig = go.Figure() fig.add_trace(go.Histogram(nbinsx=200, x=r, histnorm='probability density', name='"Экспериментальные" данные')) fig.add_trace(go.Scatter(x=x_norm, y=norm.pdf(x_norm), name='Теоретическая форма нормального распределения')) fig.update_layout( title="Пример гистограммы на основе нормального распределения", title_x = 0.5, legend=dict(x=.5, xanchor="center", orientation="h"), margin=dict(l=0, r=0, t=30, b=0)) fig.show()
Другие столбчатые диаграммы — Bar Charts
Столбчатые диаграммы можно сформировать и своими силами, если сгруппировать данные и вычислить высоты столбцов.
Далее, используя класс go.Bar передаём названия столбцов и их величины в 2 аргумента:
d_grouped = dices.groupby(['Сумма']).count() labels = d_grouped.index values = d_grouped['Кость 1'].values fig = go.Figure(data=[go.Bar(x = labels, y = values)]) fig.show()
Важно!
Как и круговая диаграмма, такая столбчатая в отличие от ранее изученных гистограмм не построит столбец для того, чего нет!
Например, если мы сделаем только 10 бросков по 2 кости, то среди них не может выпасть всех возможных случаев. А значит, они не отобразятся на диаграмме:
Код дырявой диаграммы без крайнего столбца
BAD_d_grouped = dices.head(10).groupby(['Сумма']).count() labels = BAD_d_grouped.index values = BAD_d_grouped['Кость 1'].values fig = go.Figure(data=[go.Bar(x = labels, y = values)]) fig.show()
При необходимости выведения ВСЕХ, даже нулевых столбцов, их следует сформировать самостоятельно.
labels = tuple(range(2,13)) BAD_d_grouped = dices.head(10).groupby(['Сумма']).count() clear = BAD_d_grouped['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values = clear[0] fig = go.Figure(data=[go.Bar(x = labels, y = values)]) fig.show()
Создадим парную гистограмму для 2 наборов по 100 бросков, в оба набора добавив на всякий случай колонки с нулями, если их нет.
В зависимости от генерации начальных данных в каких-то местах должна быть только 1 колонка, либо не будет колонок вообще.
labels = tuple(range(2,13)) d_grouped = dices.groupby(['Сумма']).count() clear = d_grouped['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values = clear[0] d_grouped2 = dices2.groupby(['Сумма']).count() clear2 = d_grouped2['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values2 = clear2[0] fig = go.Figure() fig.add_trace(go.Bar(x = labels, y = values)) fig.add_trace(go.Bar(x = labels, y = values2)) fig.show()
А вот если мы хотим вывести и 3й набор испытаний (1000 бросков), то придётся самостоятельно нормализовать данные, т.к. у go.Bar в отличие от гистограмм нет аргумента вроде histnorm.
Иначе вновь получим странную картинку, когда столбцы третьей серии сильно больше остальных и их нельзя сравнить.
labels = tuple(range(2,13)) d_grouped = dices.groupby(['Сумма']).count() clear = d_grouped['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values = clear[0] / clear[0].sum() d_grouped2 = dices2.groupby(['Сумма']).count() clear2 = d_grouped2['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values2 = clear2[0] / clear2[0].sum() d_grouped3 = dices3.groupby(['Сумма']).count() clear3 = d_grouped3['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values3 = clear3[0] / clear3[0].sum() fig = go.Figure() fig.add_trace(go.Bar(x = labels, y = values)) fig.add_trace(go.Bar(x = labels, y = values2)) fig.add_trace(go.Bar(x = labels, y = values3)) fig.show()
Зато есть у такой диаграммы и серьёзное преимущество — высота столбцов не обязана быть положительной. Это можно использовать, например, для построения диаграммы прибылей и убытков по периодам. Просто вычтем из одного набора значений другой, получим список чисел (некоторые отрицательные, некоторые положительные, а некоторые 0) и используем его для построения.
Так же это отличный повод вспомнить, что для столбчатых диаграмм работают все способы оформления, которые мы отработали на графиках. В данном случае логично выделить нулевой уровень красным цветом.
labels = tuple(range(2,13)) d_grouped = dices.groupby(['Сумма']).count() clear = d_grouped['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values = clear[0] d_grouped2 = dices2.groupby(['Сумма']).count() clear2 = d_grouped2['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values2 = clear2[0] fig = go.Figure() fig.add_trace(go.Bar(x = labels, y = values - values2)) fig.update_yaxes(zeroline=True, zerolinewidth=3, zerolinecolor='red') fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), title , xaxis_title="Сумма очков", yaxis_title="Разница в числе исходов", margin=dict(l=0, r=0, t=50, b=0)) fig.show()
А ещё можно вывести подписи прямо поверх столбцов. Для этого пригодится пара аргументов:
- text — сюда передаём список тех значений, которые надо вывести (можно заранее сформировать произвольные строки)
- textposition — способ вывода текста:
- ‘auto’ — Plotly самостоятельно пример решение
- ‘outside’ — снаружи, в случае столбчатой диаграммы это будет над столбцом с положительной высотой и под столбцом с отрицательной высотой
- ‘inside’ внутри прямоугольника (если высота прямоугольника = 0, то надпись не будет видно)
- и т.д.
labels = tuple(range(2,13)) d_grouped = dices.groupby(['Сумма']).count() clear = d_grouped['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values = clear[0] d_grouped2 = dices2.groupby(['Сумма']).count() clear2 = d_grouped2['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values2 = clear2[0] fig = go.Figure() fig.add_trace(go.Bar(x = labels, y = values - values2, text=values - values2, textposition='outside')) fig.update_yaxes(zeroline=True, zerolinewidth=3, zerolinecolor='red') fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), title , xaxis_title="Сумма очков", yaxis_title="Разница в числе исходов", margin=dict(l=0, r=0, t=50, b=0)) fig.show()
А что, если мы хотим вывести не вертикальные столбцы, а горизонтальные полоски?
Это легко сделать надо только:
- Добавить аргумент orientation=’h’
- Поменять местами x и y в данных и подписях (а так же везде, где мы задаём подписи осей, осевые линии и т.п.)
labels = tuple(range(2,13)) d_grouped = dices.groupby(['Сумма']).count() clear = d_grouped['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values = clear[0] d_grouped2 = dices2.groupby(['Сумма']).count() clear2 = d_grouped2['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values2 = clear2[0] fig = go.Figure() fig.add_trace(go.Bar(y = labels, x = values - values2, orientation='h', text=values - values2, textposition='outside')) fig.update_xaxes(zeroline=True, zerolinewidth=3, zerolinecolor='red') fig.update_layout(legend_orientation="h", legend=dict(x=.5, xanchor="center"), title , yaxis_title="Сумма очков", xaxis_title="Разница в числе исходов", margin=dict(l=0, r=0, t=50, b=0)) fig.show()
Так же при отображении гистограмм 2 наборов данных есть полезный аргумент для слоя — barmode=’stack’.
Он позволяет объединять столбцы в общие колонки. Это полезно, если мы представляем наши данные, как единую серию экспериментов, т.е. мы бросили 100 раз кубики, потом ещё 100 раз и хотим узнать что вышло в итоге, сколько всё-таки каких исходов.
labels = tuple(range(2,13)) d_grouped = dices.groupby(['Сумма']).count() clear = d_grouped['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values = clear[0] d_grouped2 = dices2.groupby(['Сумма']).count() clear2 = d_grouped2['Кость 1'].append(pd.DataFrame([0]*len(labels), index=labels)).reset_index().drop_duplicates('index').sort_values('index') values2 = clear2[0] fig = go.Figure() fig.add_trace(go.Bar(x = labels, y = values)) fig.add_trace(go.Bar(x = labels, y = values2)) fig.update_layout(barmode='stack') fig.show()
Ящики с усами (Box Plots)
А что, если требуется более сложный и информативный инструмент? Примером может служить диаграмма размаха или «ящик с усами» (ссылка)
Для примера создадим набор 100 событий с бросками набора других игральных костей. На этот раз 3 4-гранных кости (3d4). Это могло бы быть сравнением 2 игровых мечей с уроном 2d6 и 3d4, однако, любому очевидно, что второй эффективнее (разброс 2-12 против разброса 3-12). Вся ли это информация, которую можно «вытащить» из этих данных?
Конечно нет, ведь у них будут отличаться и меры центральной тенденции (медианы или средние).
Для построения ящиков с усами мы используем класс go.Box. Данные (весь массив «сумм») передаём в единственный аргумент — y.
dices4 = pd.DataFrame(np.random.randint(low=1, high=5, size=(100, 3)), columns=('Кость 1', 'Кость 2', 'Кость 4')) dices4['Сумма'] = dices4['Кость 1'] + dices4['Кость 2'] + dices4['Кость 4'] fig = go.Figure() fig.add_trace(go.Box(y=dices['Сумма'])) fig.add_trace(go.Box(y=dices4['Сумма'])) fig.show()
Не совсем понятно кто есть кто.
Ну или относительно понятно
Примечание. Т.к. мы используем random, то в вашем случае результат может получиться не такой, как у меня при тестовой генерации, однако забавно, что с первой же попытки во время подготовки этого материала я получил вот такую картинку:
Тут ясно, что «усы» левого ящика имеют размах 2-12, значит, это и есть 2d6. Но занятно, что хотя нижняя граница прямого «усы» выше левого, но и верхняя ниже! Это объясняется тем, что 100 событий не так уж и много, а выбросить сразу 3 четвёрки довольно сложно. И медианы у них на одном уровне. Выходит, наше первоначальное предположение о большей эффективности оружия с уроном 3d4 можно считать справедливым лишь по уровню 25% квартиля — он явно выше на правой картинке. Т.е. «ящик с усами» всё же дал нам довольно много легко считываемой и не совсем очевидной первоначально информации.
Однако, как и для других фигур, тут можно задать подписи.
fig = go.Figure() fig.add_trace(go.Box(y=dices['Сумма'], name='2d6')) fig.add_trace(go.Box(y=dices4['Сумма'], name='3d4')) fig.update_layout(title="Сравнение испытаний по 100 бросков игральных костей", xaxis_title="Вид испытаний", yaxis_title="Сумма очков") fig.show()
Иногда вертикальные ящики не очень наглядны (либо сложно прочитать подписи снизу), тогда их можно положить «на бок» так же, как мы делали с обычными столбчатыми диаграммами:
fig = go.Figure() fig.add_trace(go.Box(x=dices['Сумма'], name='2d6')) fig.add_trace(go.Box(x=dices4['Сумма'], name='3d4')) fig.update_layout(title="Сравнение испытаний по 100 бросков игральных костей", yaxis_title="Вид испытаний", xaxis_title="Сумма очков") fig.show()
Иногда полезно для каждого ящика с усами так же отобразить облако точек, формирующий распределение. Это легко сделать с помощью аргумента boxpoints=’all’
fig = go.Figure() fig.add_trace(go.Box(x=dices['Сумма'], name='2d6', boxpoints='all')) fig.add_trace(go.Box(x=dices4['Сумма'], name='3d4', boxpoints='all')) fig.update_layout(title="Сравнение испытаний по 100 бросков игральных костей", yaxis_title="Вид испытаний", xaxis_title="Cумма очков") fig.show()
Географические карты
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 (долгота)
fig.update_layout(mapbox_style="open-street-map")
cities = pd.read_csv('https://raw.githubusercontent.com/hflabs/city/master/city.csv') fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'])) fig.update_layout(mapbox_style="open-street-map") fig.show()
Как-то криво, правда? Давайте сдвинем центр карты так, чтобы он пришёлся на столицу нашей родины (вернее столицу родины автора этих строк, т.к. у читателя родина может быть иной).
Для этого нам понадобится объект go.layout.mapbox.Center или обычный словарь с 2 аргументами:
- lat
- lon
fig.update_layout( mapbox=dict( center=dict( lat=. lon=. ) ) )
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'])) capital = cities[cities['region']=='Москва'] map_center = go.layout.mapbox.Center(lat=capital['geo_lat'].values[0], lon=capital['geo_lon'].values[0]) # Аналог с помощью словаря #map_center = dict(lat=capital['geo_lat'].values[0], lon=capital['geo_lon'].values[0]) fig.update_layout(mapbox_style="open-street-map", mapbox=dict(center=map_center)) fig.show()
Неплохо, но масштаб мелковат (по сути сейчас отображается карта мира на которой 1/6 часть суши занимает далеко не всё полезное место).
Без ущерба для полезной информации можно слегка приблизить картинку.
Для этого используем аргумент zoom=2
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'])) capital = cities[cities['region']=='Москва'] map_center = go.layout.mapbox.Center(lat=capital['geo_lat'].values[0], lon=capital['geo_lon'].values[0]) fig.update_layout(mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
Увы, на карту попало слишком много Европы без данных и слишком мало отечественного дальнего востока, так что в данном случае центрироваться возможно стоит по геометрическому центру страны (вычислим его весьма «приблизительно»).
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'])) map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
Давайте добавим подписи городов. Для этого используем аргумент text.
Следует заметить, что для нескольких населённых пунктов (города федерального значения) почему-то не заполнено поле city, поэтому для них мы его вручную заполним из address. Не очень красиво, но главное, что не пустота.
cities.loc[cities['city'].isna(), 'city'] = cities.loc[cities['city'].isna(), 'address'] fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city'])) map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
Вспомним, как мы увеличивали плотность информации для обычных графиков. Тут так же можно задать размер маркера, например, от населения.
Следует учесть 2 момента:
- Данные замусорены. Население некоторых городов имеет вид 96[3]. Поэтому колонка с население не численная и нам нужна функция, которая этот мусор обнулит, либо приведёт к какому-то читаемому виду.
- Размер маркера задаётся в пикселях. И 15 миллионов пикселей — слишком большой диаметр. Потому разумно придумать формулу, например, логарифм.
def to_int_size(value): try: return np.log10(int(value)) except: return np.log10(int(value.split('[')[0])) fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city'], marker=dict(size=cities['population'].map(to_int_size)))) map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
Добавим цветовую кодировку. Для этого используем данные о годе основания. Т.к. не для всех городов он точно известен (для некоторых указан век, для некоторых римскими, а не арабскими цифрами), то мы так же вынуждены будем написать свою функцию для обработки годов, но для простоты все проблемные случаи мы будем возвращать None и потом просто удалим все такие города.
Если возвращать, например, np.NaN, то при построении тепловой карты эти значения будут считаться эквивалентными 0 и мы будем считать такие населённые пункты одними из самых старых в стране)
def to_int_year(value): try: return int(value) except: return None cities['foundation_year'] = cities['foundation_year'].map(to_int_year) cities = cities[['region', 'city', 'geo_lat', 'geo_lon', 'foundation_year', 'population']].dropna() fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city'], marker=dict(colorbar=dict(title="Год основания"), color=cities['foundation_year'], size=cities['population'].map(to_int_size)))) map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
А что если мы хотим нанести линию? Без проблем!
Возьмём и добавим новый график на имеющуюся картинку, который будет содержать только 2 точки: Москву и Санкт-Петербург.
Нам понадобится новый атрибут mode = «lines» (у него доступны и другие значения, например «markers+lines»), но мы уже вывели метку города, так что не хотим её дублировать.
Не будем выводить никакой информации при наведении на этот новый график, чтобы она не перебивала эффект наведения на основные точки. hoverinfo=’skip’
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city'], marker=dict(colorbar=dict(title="Год основания"), color=cities['foundation_year'].map(to_int_year), size=cities['population'].map(to_int_size)))) fig.add_trace(go.Scattermapbox(mode = "lines", hoverinfo='skip', lat=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lat'], lon=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lon'])) map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
Ох, кажется полоска тепловой карты наложилась на легенду. Более того, теперь легенда выводится раздельная для точек-городов и линии между Москвой и Санкт-Петербургом.
- Переключим легенду в горизонтальный режим legend_orientation=«h» (в настройках слоя)
- «сгруппируем» легенды вместе. Для этого у каждого графика группы добавим аргумент legendgroup=«group» (можно использовать любые строки, лишь бы они были одинаковые у членов одной группы).
fig = go.Figure(go.Scattermapbox(legendgroup="group", lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city'], marker=dict(colorbar=dict(title="Год основания"), color=cities['foundation_year'].map(to_int_year), size=cities['population'].map(to_int_size)))) fig.add_trace(go.Scattermapbox(legendgroup="group", mode = "lines", hoverinfo='skip', lat=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lat'], lon=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lon'])) map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(legend_orientation="h", mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
Отлично, теперь они включаются и выключаются вместе. Давайте уберём из легенды «лишний» элемент (линию городов) showlegend=False
А так же подпишем легенду для городов.
fig = go.Figure(go.Scattermapbox(legendgroup="group", name='Города России', lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city'], marker=dict(colorbar=dict(title="Год основания"), color=cities['foundation_year'].map(to_int_year), size=cities['population'].map(to_int_size)))) fig.add_trace(go.Scattermapbox(legendgroup="group", showlegend=False, mode = "lines", hoverinfo='skip', lat=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lat'], lon=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lon'])) map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(legend_orientation="h", mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
Давайте добавим чуть более осмысленные линии на карту. Для этого воспользуемся маршрутом поезда №002М «Россия» Москва-Владивосток (сайт РЖД)
Я заранее подготовил отдельный файл с городами, на маршруте, разбитом по дням. Это примерная разбивка, т.к. расписание меняется, так что не используйте мою таблицу для оценка когда вы приедете к любимой тёще в гости. Некоторые станции поезда не имеют аналога в нашей оригинальной таблице городов, поэтому они пропущена. Некоторые города указаны 2 раза, т.к. они являются конечной точкой одного дневного перегона и начальной другого дневного перегона.
Наш маршрут будет соединять города, а не вокзалы, так же он не будет совпадать с реальной железной дорогой. Это просто визуализация маршрута, а не инструмент навигации!
train_russia = pd.read_csv('https://gist.githubusercontent.com/lexnekr/2da07b5fc12b5be24068e4d68ed47ca5/raw/d6256765a3223282fbfec7e0b040cbfb21593fff/train_russia.scv') fig = go.Figure(go.Scattermapbox(legendgroup="group", name='Города России', lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city'], marker=dict(colorbar=dict(title="Год основания"), color=cities['foundation_year'].map(to_int_year), size=cities['population'].map(to_int_size)))) for df_for_today in train_russia.groupby(['day number']): fig.add_trace(go.Scattermapbox(name='День <>'.format(df_for_today[0]), mode = "lines", hoverinfo='skip', lat=df_for_today[1]['geo_lat'], lon=df_for_today[1]['geo_lon'])) map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(title='По России на поезде', legend_orientation="h", mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2)) fig.show()
Если мы хотим анимировать процесс появления маршрута по дням, то нам придётся использовать тот же приём, что и ранее с появлением нескольких графиков — заранее вывести все графики или их заглушки невидимыми, а потом на каждом фрейме и шаге слайдера делать их видимыми.
data = [go.Scattermapbox(legendgroup="group", name='Города России', lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city'], marker=dict(colorbar=dict(title="Год основания"), color=cities['foundation_year'].map(to_int_year), size=cities['population'].map(to_int_size)))] for df_for_today in train_russia.groupby(['day number']): data.append(go.Scattermapbox(visible=False, name='День <>'.format(df_for_today[0]), mode = "lines", hoverinfo='skip', lat=df_for_today[1]['geo_lat'], lon=df_for_today[1]['geo_lon'])) fig = go.Figure(data) frames=[] for i in range(len(data)+1): temp_frame = go.Frame(name=str(i), data=data) for j in range(1, i): temp_frame['data'][j]['visible']=True frames.append(temp_frame) steps = [] for i in range(len(data)): step = dict( label = str(i), method = "animate", args = [[str(i+1)]] ) steps.append(step) sliders = [dict( currentvalue = >, len = 0.9, x = 0.1, pad = , steps = steps, )] map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2, lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2) fig.update_layout(title='По России на поезде', legend_orientation="h", mapbox_style="open-street-map", mapbox=dict(center=map_center, zoom=2), updatemenus=[dict(direction="left", pad = , x = 0.1, xanchor = "right", y = 0, yanchor = "top", showactive=False, type="buttons", buttons=[dict(label="►", method="animate", args=[None, ]), dict(label="❚❚", method="animate", args=[[None], , "mode": "immediate", "transition": >])])], ) fig.layout.sliders = sliders fig.frames = frames fig.show()
Безусловно мы разобрали далеко не все виды графиков Plotly. Однако, данного базового набора примеров должно быть достаточно чтобы понять принцип по которому все они работают.
С примерами других визуализаций можно ознакомиться тут — plotly.com/python (обратите внимание, что для каждой категории приведены далеко не все примеры, больше примеров всегда доступно по ссылке «More . »