In [1]:
import plotly.express as px df = px.data.tips() fig = px.scatter(df, x="total_bill", y="tip", facet_col="sex", width=800, height=400) fig.update_layout( margin=dict(l=20, r=20, t=20, b=20), paper_bgcolor="LightSteelBlue", ) fig.show()
Adjusting graph size with Dash¶
Dash is the best way to build analytical apps in Python using Plotly figures. To run the app below, run pip install dash
, click «Download» to get the code and run python app.py
.
Get started with the official Dash docs and learn how to effortlessly style & deploy apps like this with Dash Enterprise.
Sign up for Dash Club → Free cheat sheets plus updates from Chris Parmer and Adam Schroeder delivered to your inbox every two months. Includes tips and tricks, community apps, and deep dives into the Dash architecture.
Join now.
Adjusting Height, Width, & Margins With Graph Objects¶
Graph objects are the low-level building blocks of figures which you can use instead of Plotly Express for greater control.
In [3]:
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Scatter( x=[0, 1, 2, 3, 4, 5, 6, 7, 8], y=[0, 1, 2, 3, 4, 5, 6, 7, 8] )) fig.update_layout( autosize=False, width=500, height=500, margin=dict( l=50, r=50, b=100, t=100, pad=4 ), paper_bgcolor="LightSteelBlue", ) fig.show()
Automatically Adjust Margins¶
Set automargin to True
and Plotly will automatically increase the margin size to prevent ticklabels from being cut off or overlapping with axis titles.
In [4]:
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Bar( x=["Apples", "Oranges", "Watermelon", "Pears"], y=[3, 2, 1, 4] )) fig.update_layout( autosize=False, width=500, height=500, yaxis=dict( title_text="Y-axis Title", ticktext=["Very long label", "long label", "3", "label"], tickvals=[1, 2, 3, 4], tickmode="array", titlefont=dict(size=30), ) ) fig.update_yaxes(automargin=True) fig.show()
Automatically Adjust Specific Margins¶
New in 5.10
You can also set automargin
for specific sides of the figure. Here, we set automargin
on the left
and top
of the figure.
In [5]:
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Bar( x=["Apples", "Oranges", "Watermelon", "Pears"], y=[3, 2, 1, 4] )) fig.update_layout( autosize=False, width=500, height=500, yaxis=dict( title_text="Y-axis Title", ticktext=["Very long label", "long label", "3", "label"], tickvals=[1, 2, 3, 4], tickmode="array", titlefont=dict(size=30), ) ) fig.update_yaxes(automargin='left+top') fig.show()
Setting a Minimum Plot Size with Automargins¶
New in 5.11
To set a minimum width and height for a plot to be after automargin is applied, use minreducedwidth
and minreducedheight
. Here we set both to 250
.
In [6]:
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Bar( x=["Apples", "Oranges", "Watermelon", "Pears"], y=[3, 2, 1, 4] )) fig.update_layout( autosize=False, minreducedwidth=250, minreducedheight=250, width=450, height=450, yaxis=dict( title_text="Y-axis Title", ticktext=["Label", "Very long label", "Other label", "Very very long label"], tickvals=[1, 2, 3, 4], tickmode="array", titlefont=dict(size=30), ) ) fig.show()
What About Dash?¶
Dash is an open-source framework for building analytical applications, with no Javascript required, and it is tightly integrated with the Plotly graphing library.
Learn about how to install Dash at https://dash.plot.ly/installation.
Everywhere in this page that you see fig.show()
, you can display the same figure in a Dash application by passing it to the figure
argument of the Graph
component from the built-in dash_core_components
package like this:
import plotly.graph_objects as go # or plotly.express as px fig = go.Figure() # or any Plotly Express function e.g. px.bar(...) # fig.add_trace( ... ) # fig.update_layout( ... ) import dash import dash_core_components as dcc import dash_html_components as html app = dash.Dash() app.layout = html.Div([ dcc.Graph(figure=fig) ]) app.run_server(debug=True, use_reloader=False) # Turn off reloader if inside Jupyter
jupyter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Adjusting Height, Width, & Margins with Plotly Express
Plotly Express is the easy-to-use, high-level interface to Plotly, which operates on a variety of types of data and produces easy-to-style figures.
import plotly.express as px df = px.data.tips() fig = px.scatter(df, x="total_bill", y="tip", facet_col="sex", width=800, height=400) fig.update_layout( margin=dict(l=20, r=20, t=20, b=20), paper_bgcolor="LightSteelBlue", ) fig.show()
Adjusting graph size with Dash
Dash is the best way to build analytical apps in Python using Plotly figures. To run the app below, run pip install dash
, click «Download» to get the code and run python app.py
.
Get started with the official Dash docs and learn how to effortlessly style & deploy apps like this with Dash Enterprise.
from IPython.display import IFrame snippet_url = 'https://python-docs-dash-snippets.herokuapp.com/python-docs-dash-snippets/' IFrame(snippet_url + 'setting-graph-size', width='100%', height=1200)
Sign up for Dash Club → Free cheat sheets plus updates from Chris Parmer and Adam Schroeder delivered to your inbox every two months. Includes tips and tricks, community apps, and deep dives into the Dash architecture.
Join now.
Adjusting Height, Width, & Margins With Graph Objects
Graph objects are the low-level building blocks of figures which you can use instead of Plotly Express for greater control.
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Scatter( x=[0, 1, 2, 3, 4, 5, 6, 7, 8], y=[0, 1, 2, 3, 4, 5, 6, 7, 8] )) fig.update_layout( autosize=False, width=500, height=500, margin=dict( l=50, r=50, b=100, t=100, pad=4 ), paper_bgcolor="LightSteelBlue", ) fig.show()
Automatically Adjust Margins
Set automargin to True
and Plotly will automatically increase the margin size to prevent ticklabels from being cut off or overlapping with axis titles.
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Bar( x=["Apples", "Oranges", "Watermelon", "Pears"], y=[3, 2, 1, 4] )) fig.update_layout( autosize=False, width=500, height=500, yaxis=dict( title_text="Y-axis Title", ticktext=["Very long label", "long label", "3", "label"], tickvals=[1, 2, 3, 4], tickmode="array", titlefont=dict(size=30), ) ) fig.update_yaxes(automargin=True) fig.show()
Automatically Adjust Specific Margins
New in 5.10
You can also set automargin
for specific sides of the figure. Here, we set automargin
on the left
and top
of the figure.
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Bar( x=["Apples", "Oranges", "Watermelon", "Pears"], y=[3, 2, 1, 4] )) fig.update_layout( autosize=False, width=500, height=500, yaxis=dict( title_text="Y-axis Title", ticktext=["Very long label", "long label", "3", "label"], tickvals=[1, 2, 3, 4], tickmode="array", titlefont=dict(size=30), ) ) fig.update_yaxes(automargin='left+top') fig.show()
Setting a Minimum Plot Size with Automargins
New in 5.11
To set a minimum width and height for a plot to be after automargin is applied, use minreducedwidth
and minreducedheight
. Here we set both to 250
.
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Bar( x=["Apples", "Oranges", "Watermelon", "Pears"], y=[3, 2, 1, 4] )) fig.update_layout( autosize=False, minreducedwidth=250, minreducedheight=250, width=450, height=450, yaxis=dict( title_text="Y-axis Title", ticktext=["Label", "Very long label", "Other label", "Very very long label"], tickvals=[1, 2, 3, 4], tickmode="array", titlefont=dict(size=30), ) ) fig.show()
Reference
See https://plotly.com/python/reference/layout/ for more information and chart attribute options!
Библиотека Python предоставляет нам интерактивную библиотеку Plotly с открытым исходным кодом, которая может поддерживать более 40 уникальных типов диаграмм, охватывающих широкий список статистических, финансовых, географических, научных и трехмерных вариантов использования.
Чтобы установить сюжет, выполните следующую команду в терминале:
pip3 install plotly.express
После того, как мы установили библиотеку, теперь мы можем изменить размер фигуры, манипулируя размером графика и размерами полей. Это также может быть достигнуто с использованием различных методов для различных вариантов использования.
Содержание
- Изменение высоты, ширины и полей с помощью Plotly Express
- Измените высоту, ширину и поля с помощью графических объектов в графике
- Автоматизация изменений полей в графике
Изменение высоты, ширины и полей с помощью Plotly Express
Теперь мы попробуем изменить размер фигуры, изменив ширину и высоту с помощью Plotly Express. Его можно использовать для построения тех же интерактивных диаграмм с помощью нескольких строк кода.
Давайте посмотрим на пример, где мы сначала импортировали библиотеку plotly.express. Затем мы будем использовать метод data.wind() для создания фрейма данных, а затем построим диаграмму рассеяния для фрейма данных.
Python
import
plotly.express as plotly
dataFrame
=
plotly.data.wind()
myFigure
=
plotly.scatter(dataFrame, x
=
"direction"
, y
=
"frequency"
,
width
=
400
, height
=
400
)
myFigure.show()
Выход:
Давайте рассмотрим пример, в котором мы можем обновить поля графика, используя размеры полей: правое верхнее, левое и нижнее (l, r, t, b). И эти параметры поля были переданы методу update_layout(), который вызывается для объекта фигуры. Применив это, мы, наконец, построили графические объекты в области диаграммы, чтобы обеспечить лучшую визуализацию.
Python3
import
plotly.express as plotly
dataframe
=
plotly.data.tips()
myFigure
=
plotly.scatter(dataframe, x
=
"total_bill"
,
y
=
"tip"
, facet_col
=
"sex"
,
width
=
600
, height
=
400
)
myFigure.update_layout(
margin
=
dict
(l
=
30
, r
=
30
, t
=
30
, b
=
20
),
paper_bgcolor
=
"LightSteelBlue"
,
)
myFigure.show()
Выход:
Измените высоту, ширину и поля с помощью графических объектов в графике
Теперь мы будем использовать графические объекты вместо сюжетного экспресса, чтобы изменить высоту, ширину и поля. Чтобы иметь больший контроль над элементами диаграммы, можно использовать графические объекты вместо графического выражения. Графические объекты считаются строительными блоками фигур на низком уровне.
Python3
import
plotly.graph_objects as plotly
import
plotly.express as px
myFigure
=
plotly.Figure()
dataFrame
=
px.data.wind()
myFigure.add_trace(plotly.Scatter(
x
=
dataFrame[
"direction"
],
y
=
dataFrame[
"frequency"
]
))
myFigure.show()
Выход:
В этом примере для установки области диаграммы мы использовали высоту и ширину. Параметры поля используются для установки объектов графика. Обратите внимание, что на этот раз все эти аргументы использовались внутри функции update_layout() объекта фигуры.
Python3
import
plotly.graph_objects as plotly
import
plotly.express as px
myFigure
=
plotly.Figure()
dataFrame
=
px.data.wind()
myFigure.add_trace(plotly.Scatter(
x
=
dataFrame[
"direction"
],
y
=
dataFrame[
"frequency"
]
))
myFigure.update_layout(
title
=
"Wind Frequencies"
,
xaxis_title
=
"Direction"
,
yaxis_title
=
"Frequency"
,
autosize
=
False
,
width
=
400
,
height
=
500
,
margin
=
dict
(l
=
30
,r
=
30
,b
=
30
,
t
=
30
,pad
=
3
),
paper_bgcolor
=
"LightSteelBlue"
,
)
myFigure.show()
Выход:
Автоматизация изменений полей в графике
Теперь попробуем автоматизировать изменение полей в plotly.
Python3
import
plotly.graph_objects as plotly
import
plotly.express as px
myFigure
=
plotly.Figure()
myFigure.add_trace(plotly.Bar(
x
=
[
"Moranp"
,
"Turquoise"
,
"Cornflower"
,
"Raddles"
],
y
=
[
6
,
4
,
5
,
11
]
))
myFigure.update_layout(
autosize
=
False
,
width
=
700
,
height
=
700
,
yaxis
=
dict
(
title_text
=
"Heights"
,
ticktext
=
[
"little"
,
"very long title"
,
"long"
,
"short title"
],
tickvals
=
[
2
,
4
,
6
,
8
],
tickmode
=
"array"
,
titlefont
=
dict
(size
=
20
),
)
)
myFigure.update_yaxes(automargin
=
False
)
myFigure.show()
Выход:
Как вы можете видеть выше, без использования манипулирования полями, метки деления оси Y перекрываются с заголовком оси Y.
Чтобы избежать этого перекрытия, мы должны установить «automargin» как True. Таким образом, он автоматически отрегулирует размер поля. А аргумент automargin будет препятствовать перекрытию меток делений заголовками осей.
Python3
myFigure.update_yaxes(automargin
=
True
)
myFigure.show()
Выход:
Method 1: Specifying figsize
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(24, 12))
ax = plt.axes()
plt.sca(ax)
plt.scatter(x =[1,2,3,4,5],y=[1,2,3,4,5])
Note that the size gets applied in inches.
sca set the current axes.
Method 2: Using set_size_inches
import matplotlib.pyplot as plt
plt.scatter(x =[1,2,3,4,5],y=[1,2,3,4,5])
fig = plt.gcf()
fig.set_size_inches(24, 12)
gcf stands for get current figure
Method 3: Using rcParams
This can be used when you want to avoid using the figure environment. Please note that the rc settings are global to the matplotlib package and making changes here will affect all plots (future ones as well), until you restart the kernel.
import matplotlib.pyplot as plt
plt.rcParams["figure.figsize"] = (24,12)
plt.scatter(x =[1,2,3,4,5],y=[1,2,3,4,5])
Here as well, the sizes are in inches.
Seaborn
Method 1: Using matplotlib axes
import matplotlib.pyplot as plt
import seaborn as sns
fig = plt.figure(figsize=(24, 12))
ax = plt.axes()
sns.scatterplot(x =[1,2,3,4,5],y=[1,2,3,4,5], ax=ax)
This works for axes-level functions in Seaborn.
Method 2: Using rc
import seaborn as sns
sns.set(rc={'figure.figsize':(24,12)})
sns.scatterplot(x =[1,2,3,4,5],y=[1,2,3,4,5])
Again, this is a global setting and will affect all future plots, unless you restart the kernel.
Method 3: Using height and aspect ratio
This works for figure-level functions in seaborn.
import pandas as pd
import seaborn as sns
df = pd.read_csv('myfile.csv')
sns.pairplot(df,height = 12, aspect = 24/12)
Plotly
Method 1: Function Arguments in Plotly Express
import plotly.express as px
fig = px.scatter(x =[1,2,3,4,5],y=[1,2,3,4,5], width=2400, height=1200)
fig.show()
Please note that instead of inches, the width and height are specified in pixels here.
Method 2: Using update_layout with Plotly Express
import plotly.express as px
fig = px.scatter(x =[1,2,3,4,5],y=[1,2,3,4,5])
fig.update_layout(
width=2400,
height=1200
)
fig.show()
Method 3: Using update_layout with Plotly Graph Objects
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(
x=[1,2,3,4,5],
y=[1,2,3,4,5]
))
fig.update_layout(
width=2400,
height=1200)
fig.show()
If you are interested in data science and visualization using Python, you may find this course by Jose Portilla on Udemy to be very helpful. It has been the foundation course in Python for me and several of my colleagues.
You are also encouraged to check out further posts on Python on iotespresso.com.
In this article, we will discuss how we can change the figure size in Plotly in Python. Let’s firstly take about Plotly in Python.
Plotly
Python library provides us with an interactive open-source Plotly library that can support over 40 unique chart types that cover a wide list of statistical, financial, geographic, scientific, and 3-dimensional use cases.
To install the plotly, run the below command in the terminal:
pip3 install plotly.express
Once we installed the library, we can now change the figure size by manipulating the graph size and the margin dimensions. It can also be achieved using different methods for different use cases.
Changing Height, Width, & Margins with Plotly Express
We will now try to change the figure size by modifying the width and heights using Plotly express. One may use it to plot the same interactive charts with few lines of code.
Let’s see an example, where we have first imported the plotly.express library. Then we will use data.wind() method we are creating a data frame, then scatterplot the dataframe.
Python
import
plotly.express as plotly
dataFrame
=
plotly.data.wind()
myFigure
=
plotly.scatter(dataFrame, x
=
"direction"
, y
=
"frequency"
,
width
=
400
, height
=
400
)
myFigure.show()
Output:
Let’s see an example where we can update the margins of the graph using margin dimensions, right top, left, and bottom (l, r, t, b). And these margin parameters have been passed to the update_layout() method that is called on the figure object. By applying this, we have finally constructed the graph objects in the chart area to give a better visualization.
Python3
import
plotly.express as plotly
dataframe
=
plotly.data.tips()
myFigure
=
plotly.scatter(dataframe, x
=
"total_bill"
,
y
=
"tip"
, facet_col
=
"sex"
,
width
=
600
, height
=
400
)
myFigure.update_layout(
margin
=
dict
(l
=
30
, r
=
30
, t
=
30
, b
=
20
),
paper_bgcolor
=
"LightSteelBlue"
,
)
myFigure.show()
Output:
Change Height, Width, & Margins with Graph Objects in plotly
Now we will use graph objects instead of plotly express in order to change the height, width, and margins. To have greater control over chart elements one may use graph objects instead of plotly express. Graph objects are considered building blocks of figures at the low level.
Python3
import
plotly.graph_objects as plotly
import
plotly.express as px
myFigure
=
plotly.Figure()
dataFrame
=
px.data.wind()
myFigure.add_trace(plotly.Scatter(
x
=
dataFrame[
"direction"
],
y
=
dataFrame[
"frequency"
]
))
myFigure.show()
Output:
In this example, in order to set the chart area we have used height and width. The margin parameters are used to set the graph objects. Note that this time all these arguments have been used inside the update_layout() function of the figure object.
Python3
import
plotly.graph_objects as plotly
import
plotly.express as px
myFigure
=
plotly.Figure()
dataFrame
=
px.data.wind()
myFigure.add_trace(plotly.Scatter(
x
=
dataFrame[
"direction"
],
y
=
dataFrame[
"frequency"
]
))
myFigure.update_layout(
title
=
"Wind Frequencies"
,
xaxis_title
=
"Direction"
,
yaxis_title
=
"Frequency"
,
autosize
=
False
,
width
=
400
,
height
=
500
,
margin
=
dict
(l
=
30
,r
=
30
,b
=
30
,
t
=
30
,pad
=
3
),
paper_bgcolor
=
"LightSteelBlue"
,
)
myFigure.show()
Output:
Automating margin changes in plotly
Now we will try to automate margin changes in plotly.
Python3
import
plotly.graph_objects as plotly
import
plotly.express as px
myFigure
=
plotly.Figure()
myFigure.add_trace(plotly.Bar(
x
=
[
"Moranp"
,
"Turquoise"
,
"Cornflower"
,
"Raddles"
],
y
=
[
6
,
4
,
5
,
11
]
))
myFigure.update_layout(
autosize
=
False
,
width
=
700
,
height
=
700
,
yaxis
=
dict
(
title_text
=
"Heights"
,
ticktext
=
[
"little"
,
"very long title"
,
"long"
,
"short title"
],
tickvals
=
[
2
,
4
,
6
,
8
],
tickmode
=
"array"
,
titlefont
=
dict
(size
=
20
),
)
)
myFigure.update_yaxes(automargin
=
False
)
myFigure.show()
Output:
As you can see above without using margin manipulation, the y-axis tick labels can overlap with the y-axis title.
To avoid this overlapping, we are required to set “automargin” as True. By doing so, it will automatically adjust the margin size. And the automargin argument would resist tick labels from overlapping with the axis titles.
Python3
myFigure.update_yaxes(automargin
=
True
)
myFigure.show()
Output:
To change the figure size in plotly.py, we can manipulate the graph size and the margin dimensions. We can achieve the required change in the graph through different methods for different use cases.
Change Height, Width, & Margins with Plotly Express in Python
import plotly.express as px df = px.data.wind() fig = px.scatter(df, x="direction", y="frequency", width=800, height=400) fig.show()
Here we have used Plotly Express to illustrate an example. Plotly Express is the high-level interface to Plotly operating on various data types. It produces the same interactive charts with minimal codes.
import plotly.express as px df = px.data.tips() fig = px.scatter(df, x="total_bill", y="tip", facet_col="sex", width=800, height=400) fig.update_layout( margin=dict(l=20, r=20, t=20, b=20), paper_bgcolor="LightSteelBlue", ) fig.show()
Here we have used width and height as function arguments in the scatter chart method of Plotly Express. We have used these arguments to set the dimension of the chart area. To update the margins of the graph we have used margin dimensions, left, right top, and bottom (l, r, t, b) in dictionary format. We have passed the margin parameters as an argument to the update_layout() method called on the fig object. By doing this, we have adjusted the graph objects in the chart area to render a better visualization.
Change Height, Width, & Margins with Graph Objects in plotly
import plotly.graph_objects as go fig = go.Figure() df = px.data.wind() fig.add_trace(go.Scatter( x= df["direction"], y= df["frequency"] )) fig.show()
For greater control over the chart elements, we can use Graph objects instead of Plotly Express. Graph objects are the building blocks of figures at the low level.
import plotly.graph_objects as go fig = go.Figure() df = px.data.wind() fig.add_trace(go.Scatter( x= df["direction"], y= df["frequency"] )) fig.update_layout( title="Wind Frequencies", xaxis_title="Direction", yaxis_title="Frequency", autosize=False, width=600, height=400, margin=dict( l=50, r=50, b=50, t=50, pad=4 ), paper_bgcolor="LightSteelBlue", ) fig.show()
Here again, we have used width and height to set the chart area, and margin arguments to adjust the graph objects. But this time we have used all these inside the update_layout() function of the fig object. And we have got the desired output.
Automatically changing Margins in plotly
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Bar( x=["Coral", "Turquoise", "Cornflowerblue", "Khaki"], y=[6, 4, 2, 8] )) fig.update_layout( autosize=False, width=600, height=500, yaxis=dict( title_text="Y-axis Title", ticktext=["Very long title", "long title", "5", "short title"], tickvals=[2, 4, 6, 8], tickmode="array", titlefont=dict(size=20), ) ) fig.update_yaxes(automargin=False) fig.show()
import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Bar( x=["Coral", "Turquoise", "Cornflowerblue", "Khaki"], y=[6, 4, 2, 8] )) fig.update_layout( autosize=False, width=600, height=500, yaxis=dict( title_text="Y-axis Title", ticktext=["Very long title", "long title", "5", "short title"], tickvals=[2, 4, 6, 8], tickmode="array", titlefont=dict(size=20), ) ) fig.update_yaxes(automargin=True) fig.show()
In this case, we have also updated the y-axis elements of the graph to make the y-axis tick labels a bit lengthier. Without margin manipulation, the y-axis tick labels would overlap with the y-axis title. To avoid this overlapping, we have set automargin to True which will automatically adjust the margin size. And automargin parameter will avoid tick labels from overlapping with the axis titles.
Read More: Seaborn Multiple Line Plot in Python
Plotly — библиотека для визуализации данных, состоящая из нескольких частей:
- Front-End на JS
- Back-End на Python (за основу взята библиотека Seaborn)
- Back-End на R
В этой простыне все примеры разобраны от совсем простых к более сложным, так что разработчикам с опытом будет скучно. Так же эта «шпаргалка» не заменит на 100% примеры из документации.
Извиняюсь за замыленные gif’ки это происходит при конвертации из видео, записанного с экрана.
Jupyter Notebook со всеми примерами из статьи:
- github
- colab
Документация
Так же на базе plotly и веб-сервера Flask существует специальная библиотека для создания дашбордов Dash.
- Plotly — бесплатная библиотека, которую вы можете использовать в коммерческих целях
- Plotly работает offline
- Plotly позволяет строить интерактивные визуализации
Т.е. с помощью Plotly можно как изучать какие-то данные «на лету» (не перестраивая график в matplotlib, изменяя масштаб, включая/выключая какие-то данные), так и построить полноценный интерактивный отчёт (дашборд).
Для начала необходимо установить библиотеку, т.к. она не входит ни в стандартный пакет, ни в 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)=x2
Сперва поступим совсем просто и «в лоб»:
- Создадим график с помощью функции 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()
- Создаём фигуру
- Рисуем график
- Показываем фигуру
2 строчки и готовый результат. Т.к. мы используем Express. Быстро и просто.
Но маловато гибкости, поэтому мы практически сразу переходим к более продвинутому уровню — сразу создадим фигуру и нанесём на неё объекты.
Так же сразу выведем фигуру для показа с помощью метода show().
В отличие от Matplotlib отдельные объекты осей не создаются, хотя мы с ними ещё столкнёмся, когда захотим построить несколько графиков вместе
fig = go.Figure()
#Здесь будет код
fig.show()
Как видим, пока пусто.
Чтобы добавить что на график нам понадобится метод фигуры add_trace.
fig.add_trace(ТУТ_ТО_ЧТО_ХОТИМ_ПЕРЕДАТЬ_ДЛЯ_ОТОБРАЖЕНИЯ_И_ГДЕ)
Но ЧТО мы хотим нарисовать? График по точкам. График мы уже рисовали с помощью Scatter в Экспрессе, у Объектов есть свой Scatter, давайте глянем что он делает:
go.Scatter(x=x, y=f(x))
А теперь объединим:
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x)))
fig.show()
Как видим, отличия не только в коде, но и в результате — получилась гладкая кривая.
Кроме того, такой способ позволит нам нанести на график столько кривых, сколько мы хотим:
Код
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)=x<sup>2</sup>'))
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)=x<sup>2</sup>'))
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 — отступ снизу
Зададим везде нулевые отступы fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
update_layout можно применять последовательно несколько раз, либо можно передать все аргументы в одну функцию (мы сделаем именно так)
Код
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x<sup>2</sup>'))
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)=x<sup>2</sup>'))
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)=x<sup>2</sup>'))
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)=x<sup>2</sup>'))
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)=x<sup>2</sup>'))
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 для форматирования, а имена переменных берём в фигурные скобки и выделяем %, например, %{x}
Код
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Кстати, маркерами можно управлять для конкретной кривой и явно.
Для этого используется аргумент marker, который принимает на вход словарь. Подробный пример.
А мы лишь ограничимся баловством:
Код
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Наверное всё же не следует смешивать вместе тригонометрические и арифметические функции. Давайте отобразим их на разных, соседних графиках.
Для этого нам потребуется создать фигуру с несколькими осями.
Фигура с несколькими графиками создаётся с помощью подмодуля make_subplots.
Необходимо указать количество:
- row — строк
- col — столбцов
А при построении графика передать «координаты» графика в этой «матрице» (сперва строка, потом столбец)
fig = make_subplots(rows=1, cols=2, specs=[[{'type':'domain'}, {'type':'domain'}]])
Код
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)=x<sup>2</sup>'), 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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'), 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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'), 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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'), 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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'), 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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
А что если мы хотим выделить одному из графиков больше места, чем другим, например, 2 строки или наоборот, 2 столбца?
В matplotlib мы использовали бы несколько фигур, либо оси с заданными размерами, здесь у нас есть другой инструмент. Мы можем сказать каким-то осям объединиться вдоль колонок или вдоль строк.
Для этого нам потребуется написать спецификацию на фигуру (для начала очень простую).
Спецификация — это список (если точнее, то даже матрица из списков), каждый объект внутри которого — словарь, описывающий одни из осей.
Если каких-то осей нет (например, если их место занимают растянувшиеся соседи, то вместо словаря передаётся None.
Давайте сделаем матрицу 2х2 и объединим вместе левые графики, получив одни высокие вертикальные оси. Для этого первому графику передадим атрибут «rowspan» равный 2, а его нижнего соседа уничтожим (None):
specs=[
[{"rowspan": 2}, {}],
[None, {}]
]
Код
fig = make_subplots(rows=2, cols=2,
specs=[[{"rowspan": 2}, {}], [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)=x<sup>2</sup>'), 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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Как видим, в вертикальный график идеально вписался тангенс, который отныне не невидим.
Для объединения используется:
- rowspan — по вертикали
- colspan — по горизонтали
Код, объединяющий ячейки в другом направлении
fig = make_subplots(rows=2, cols=2,
specs=[[{"colspan": 2}, 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)=x<sup>2</sup>'), 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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Больше примеров использования make_subplots
Последний вариант получился слишком узким по вертикали.
Высоту легко увеличить в помощью атрибута height у метода update_layout.
Размеры фигуры регулируются 2 атрибутами:
- width — ширина (в пикселях)
- height — высота (в пикселях)
Но следует помнить, если вы встраиваете фигуры plotly куда-то (а это логично, если вы делаете дашборд, например), то фигура занимает всё отведённое пространство по ширине, поэтому не изменять ширину в plotly будет не лучшей идеей. Лучше использовать стили в разметке.
Код
fig = make_subplots(rows=2, cols=2,
specs=[[{"colspan": 2}, 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)=x<sup>2</sup>'), 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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>',
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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
В предыдущем примере цветовая шкала не очень похожа на тепловую карту.
На самом деле цвета на шкале можно изменить, для этого служит атрибут colorscale, в который передаётся имя палитры.
Код
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>',
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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>',
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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Анимация
Можно ли ещё уплотнить информацию на графике? Да, можно, если использовать «четвёртое измерение» — время. Это так же может быть полезно и само по себе для оживления вашего графика.
Вернёмся на пару шагов назад. Мы будем анимировать график построения параболы. Для этого нам понадобятся:
- Начальное состояние
- Кнопки (анимация не должна начинаться сама по себе, поэтому для начала мы создадим простую кнопку, её запускающую, а постепенно перейдём к временной шкале)
- Фреймы (или кадры) — промежуточные состояния
1. Начальное состояние
Это то, что будет на графике до начала анимации. В нашем случае это будет стартовая точка.
Уберём практически всё лишнее из предыдущих шагов
Шаг 1 — код полуфабрикат
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
2. Кнопка
Код минимальной работоспособной кнопки выглядит так:
"updatemenus": [{"type": "buttons",
"buttons": [{"label": "Your Label",
"method": "animate",
"args": [See Below]}]}]
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)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
3. Фреймы
Это список «кадров» из которых состоит наша анимация.
Каждый фрейм должен содержать внутри себя целиком готовый график, который просто будет отображаться на нашей фигуре, как в декорациях.
Фрейм создаётся с помощью go.Frame()
График передаётся внутрь фрейма в аргумент data.
Таким образом, если мы хотим построить последовательность графиков (от 1 точки до целой фигуры), нам надо просто пройти в цикле:
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i], y=f(x[:i]))]))
После этого фреймы необходимо передать в фигуру. У каждой фигуры есть атрибут frames, который мы и будем использовать:
fig.frames = frames
Шаг 3 — Ёлочка гори, в смысле код, запускающий анимацию
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>'))
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="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>'),
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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Естественно, если добавить на графики (как на начальный, так и те, что во фреймах) маркеры с указанием цвета, цветовой шкалы и размера, то анимация будет более сложного графика.
Код
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>',
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="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Заметим, что код простейшей кнопки, которая запускает воспроизведение видео выглядит так:
dict(label="Play", method="animate", args=[None])
или
dict(label="", method="animate", args=[None])
Если мы хотим добавить кнопку «пауза» (в отличие от стандартной паузы повторное нажатие не будет вызывать воспроизведение, для начала воспроизведения придётся нажат Play), код усложнится:
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])
Правда, если добавить 2 такие кнопки, то вы заметите, что кнопка play, нажатая после паузы, в итоге начинает воспроизведение с начала. Это не совсем интуитивное поведение, поэтому ей следует добавить ещё 1 аргумент:
dict(label="", method="animate", args=[None, {"fromcurrent": True}])
Теперь полный набор из 2 наших кнопок будет выглядеть так:
buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])]
Код с 2 кнопками: запуска и остановки анимации
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>',
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, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
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)=x<sup>2</sup>',
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, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Слайдер
Слайдер по принципу работы похож на анимацию, но есть серьёзное отличие.
Слайдер — это элемент навигации, полоска по которой скользит ползунок, который управляет состоянием графиков на фигуре.
Т.е. если фреймы в анимации меняются один за другим, то в случае использования слайдера все графики одновременно есть. Но большая часть из них невидима. И при перемещении ползунка мы просто какие-то скрываем, а другие наоборот показываем (и перестраиваем оси, конечно).
1. Создаём список графиков. Важно, что один из графиков (например, 1й) должен быть видимым.
Для установления видимости/невидимости используется аргумент visible:
- Видимый график —
go.Scatter(visible=True, x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')
- Невидимый график —
go.Scatter(visible=False, x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')
Все графики рисуем на фигуре — для этого удобно использовать аргумент data фигуры, чтобы передать их все списком:
fig = go.Figure(data=trace_list)
2. Создаём список «шагов» слайдера.
Шаг имеет определённый синтаксис. По сути он описывает состояние (какие графики видимы, какие нет) и метод перехода к нему.
Шаг должен описывать состояние всех графиков.
Минимальный синтаксис 1 шага:
dict(
method = 'restyle',
args = #СПИСОК СОСТОЯНИЙ ВСЕХ ГРАФИКОВ
)
Состояние видимости/невидимости задаётся парой ‘visible’ и списка логических значений (какие графики ‘visible’, а какие нет). Поэтому для КАЖДОГО шага мы создадим список False, а потом поменяем нужное значение на True, чтобы показать какой-то конкретный график.
Шаги нужно собрать в список (т.е. это будет список словарей).
Наконец все шаги надо добавить в фигуру:
fig.layout.sliders = sliders
Минимально рабочий код (внимательно изучите его, т.к. там есть несколько тонких моментов):
Код
num_steps = len(x)
trace_list = [go.Scatter(visible=True, x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>')]
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)=x<sup>2</sup>'))
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, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
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, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
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, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
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, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
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="f(x)=x<sup>2</sup>"), 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, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
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="f(x)=x<sup>2</sup>"), 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 = {"prefix": "Шаг №", "font": {"size": 20}},
len = 0.9,
x = 0.1,
pad = {"b": 10, "t": 50},
steps = steps,
)]
fig.update_layout(title="Строим синус и косинус по шагам",
xaxis_title="Ось X",
yaxis_title="Ось Y",
updatemenus=[dict(direction="left",
pad = {"r": 10, "t": 80},
x = 0.1,
xanchor = "right",
y = 0,
yanchor = "top",
showactive=False,
type="buttons",
buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
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)=x<sup>2</sup>'),
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)=x<sup>2</sup>'),
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 = {"prefix": "Графиков отображается: ", "font": {"size": 20}},
len = 0.9,
x = 0.1,
pad = {"b": 10, "t": 50},
steps = steps,
)]
fig.update_layout(title="Выводим графики по очереди",
xaxis_title="Ось X",
yaxis_title="Ось Y",
updatemenus=[dict(direction="left",
pad = {"r": 10, "t": 80},
x = 0.1,
xanchor = "right",
y = 0,
yanchor = "top",
showactive=False,
type="buttons",
buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
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 легко поместить на локальный сервер и поменять ссылки)
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
2. Блок, где будет выводиться график
<div id="plotly_graph"></div>
Важно понимать, что Plotly по умолчанию не фиксирует размеры графика, поэтому вы можете применить к контейнеру свои стили, сверстав его такого размера и в том месте, где это необходимо вам. Plotly постарается занять всё свободное место по ширине.
Важно использовать действительно уникальный id — именно по нему plotly будет находить место на странице, куда надо встроить график.
3. JS переменная, содержащая наш JSON, описывающий график. Мы сформировали её и сложили в файл целиком. Вы можете выводить её непосредственно в коде страницы.
<script src="example.JSON"></script>
4. Вызов JS функции plotly, которая построит график.
- первым передаётся id контейнера, в котором выводить график
- вторым передаётся переменная, содержащая JSON Plotly объекта
<script>Plotly.plot('plotly_graph',graphs,{});</script>
А теперь испробуйте код на каком-нибудь своём графике!
Круговые диаграммы
Для полноты картины рассмотрим несколько других способов визуализации данных, кроме линейных графиков. Начнём с круговых диаграмм
Для нашего эксперимента «подбросим» 100 раз пару игральных кубиков (костей) и запишем суммы выпавших очков.
Код
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% радиуса круга
Мы создадим список из нулей, такой же длинны, что массив значений. А потом один элемент увеличим до 0.2.
Код
Обратите внимание, мы не используем метод 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% вырезать, ничего не оставить
Таким образом, значение 0.9 превратит круговую диаграмму в кольцевую.
Код
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='Суммы очков<br>при броске<br>2 игральных костей', x=0.5, y=0.5, font_size=20, showarrow=False)])
fig.show()
Естественно обычный способы оформления визуализаций, показанные для графиков, тут тоже работают:
- title
- title_x
- margin
- legend_orientation
Код
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='Суммы очков<br>при броске<br>2 игральных костей', x=0.5, y=0.5, font_size=20, showarrow=False)])
fig.show()
Что, если вы хотим детализовать картинку?
Sunburst или диаграмма «солнечные лучи»
Нам на помощь приходит диаграмма «солнечные лучи» — иерархическая диаграмма на основе круговой. По сути это набор кольцевых диаграмм, нанизанных друг на друга, причём сегменты следующего уровня находятся в пределах границ сегментов своего «родителя» на предыдущем.
Например, получить 8 очков с помощью 2 игральных костей можно несколькими способами:
- 2 + 6
- 3 + 5
- 4 + 4
Для построения диаграммы нам потребуется go.Sunburst и 4 основных аргумента:
- values — значения, задающие долю от круга на диаграмме
- branchvalues=«total» — такое значение указывает, что значения родительского элемента являются суммой значений потомков. Это необходимо для того, чтобы составить полный круг на каждом уровне.
- labels — список подписей, которые отображаются на диаграмме
- parents — список подписей родителей, для построения иерархии. Для элементов 0 уровня (без родителей) родителем указывается пустая строка.
Для начала обойдёмся 2 уровнями (все события и суммы)
Код
# 1-й уровень, центр диаграммы
labels = ["Всего событий: " + str(sum(sum_counts))]
parents = [""]
values = [sum(sum_counts)]
# 2-й уровень, "лепестки" диаграммы
second_level_dict = {x:'Событий: ' + str(sum_counts[x]) + '<br>Σ = ' + 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 = {x:'Событий: ' + str(sum_counts[x]) + '<br>Σ = ' + 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={'Сумма':'Value'}, 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={«size»:0.1}
- Вариант 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 аргумента:
- x — подписи
- y — величины
Код
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="Что будет, если мы будем подсчитывать в испытаниях<br>на какую сумму в первой попытке из 100 бросков было больше успехов?",
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="Что будет, если мы будем подсчитывать в испытаниях<br>на какую сумму в первой попытке из 100 бросков было больше успехов?",
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="Что будет, если мы будем подсчитывать в испытаниях<br>на какую сумму в первой попытке из 100 бросков было больше успехов?",
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 (долгота)
Так же нам понадобится подключить OSM карту, т.к. Scattermapbox может работать с разными видами карт:
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
Этот объект/словарь мы передаём в качестве значения аргумента center словаря внутрь mapbox:
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 = {"prefix": "День №", "font": {"size": 20}},
len = 0.9,
x = 0.1,
pad = {"b": 10, "t": 50},
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 = {"r": 10, "t": 80},
x = 0.1,
xanchor = "right",
y = 0,
yanchor = "top",
showactive=False,
type="buttons",
buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
fig.layout.sliders = sliders
fig.frames = frames
fig.show()
Безусловно мы разобрали далеко не все виды графиков Plotly. Однако, данного базового набора примеров должно быть достаточно чтобы понять принцип по которому все они работают.
С примерами других визуализаций можно ознакомиться тут — plotly.com/python (обратите внимание, что для каждой категории приведены далеко не все примеры, больше примеров всегда доступно по ссылке «More …»
Plotly — библиотека для визуализации данных, состоящая из нескольких частей:
- Front-End на JS
- Back-End на Python (за основу взята библиотека Seaborn)
- Back-End на R
В этой простыне все примеры разобраны от совсем простых к более сложным, так что разработчикам с опытом будет скучно. Так же эта «шпаргалка» не заменит на 100% примеры из документации.
Извиняюсь за замыленные gif’ки это происходит при конвертации из видео, записанного с экрана.
Jupyter Notebook со всеми примерами из статьи:
- github
- colab
Документация
Так же на базе plotly и веб-сервера Flask существует специальная библиотека для создания дашбордов Dash.
- Plotly — бесплатная библиотека, которую вы можете использовать в коммерческих целях
- Plotly работает offline
- Plotly позволяет строить интерактивные визуализации
Т.е. с помощью Plotly можно как изучать какие-то данные «на лету» (не перестраивая график в matplotlib, изменяя масштаб, включая/выключая какие-то данные), так и построить полноценный интерактивный отчёт (дашборд).
Для начала необходимо установить библиотеку, т.к. она не входит ни в стандартный пакет, ни в Anaconda. Для этого рекомендуется использовать pip:
pip install plotly
Если вы используете Jupyter Notebook, то можно использовать мэджик «!», поставив данный символ перед командой:
!pip install plotly
Перед началом работы необходимо импортировать модуль. В разных частях шпаргалки для разных задач нам понадобятся как основной модуль, так и один из его подмодулей, поэтому полный набор инструкций импорта у нас.
Так же нам понадобятся библиотеки Pandas и Numpy для работы с сырыми данными
Код импорта функций
Линейный график
Начнём с простой задачи построения графика по точкам.
Используем функцию f(x)=x2
Сперва поступим совсем просто и «в лоб»:
- Создадим график с помощью функции scatter из подмодуля plotly.express (внутрь передадим 2 списка точек: координаты X и Y)
- Тут же «покажем» его с помозью метода show()
Обратите внимание — график интерактивный, если навести на него курсор, то можно его приближать и удалять, выделять участки, по наведению курсора на точку получать подробную информацию, возвращать картинку в исходное положение, а при необходимости «скриншотить» и сохранять как файл.
Всё это делается с помощью JS в вашем браузере. А значит, при желании вы можете этим управлять уже после построения фигуры (но мы этого делать пожалуй не будем, т.к. JS != Python)
Код
Более читабельно и правильно записать тот же в код в следующем виде:
fig = px.scatter(x=x, y=f(x))
fig.show()
- Создаём фигуру
- Рисуем график
- Показываем фигуру
2 строчки и готовый результат. Т.к. мы используем Express. Быстро и просто.
Но маловато гибкости, поэтому мы практически сразу переходим к более продвинутому уровню — сразу создадим фигуру и нанесём на неё объекты.
Так же сразу выведем фигуру для показа с помощью метода show().
В отличие от Matplotlib отдельные объекты осей не создаются, хотя мы с ними ещё столкнёмся, когда захотим построить несколько графиков вместе
fig = go.Figure()
#Здесь будет код
fig.show()
Как видим, пока пусто.
Чтобы добавить что на график нам понадобится метод фигуры add_trace.
fig.add_trace(ТУТ_ТО_ЧТО_ХОТИМ_ПЕРЕДАТЬ_ДЛЯ_ОТОБРАЖЕНИЯ_И_ГДЕ)
Но ЧТО мы хотим нарисовать? График по точкам. График мы уже рисовали с помощью Scatter в Экспрессе, у Объектов есть свой Scatter, давайте глянем что он делает:
go.Scatter(x=x, y=f(x))
А теперь объединим:
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x)))
fig.show()
Как видим, отличия не только в коде, но и в результате — получилась гладкая кривая.
Кроме того, такой способ позволит нам нанести на график столько кривых, сколько мы хотим:
Код
Погодите, что это такое? Справа появилась ещё и легенда!
Впрочем, логично, пока график был один, зачем нам легенда?
Но магия Plotly тут не заканчивается. Нажмите на любую из подписей в легенде и соответствующий график исчезнет, а надпись станет более бледной. Вернуть их позволит повторный клик.
Подписи графиков
Добавим атрибут name, в который передадим строку с именем графика, которое мы хотим отображать в легенде.
Plotly поддерживает LATEX в подписях (аналогично matplotlib через использование $$ с обеих сторон).
Код
К сожалению, это имеет свои ограничения, как можно заметить подсказка при наведении на график отображается в «сыром» виде, а не в LATEX.
Победить это можно, если использовать HTML разметку в подписях. В данном примере я буду использовать тег sup. Так же заметьте, что шрифт для LATEX и HTML отличается начертанием.
Код
С увеличением длины подписи графика, легенда начала наезжать на график. Мне это не нравится, поэтому перенесём легенду вниз.
Для этого применим к фигуре метод update_layout, у которого нас интересует атрибут legend_orientation fig.update_layout(legend_orientation="h")
Код
Хорошо, но слишком большая часть рабочего пространства ноутбука не используется. Особенно это заметно сверху — большой отступ сверху до поля графика.
По умолчанию поля графика имеют отступ 20 пикселей. Мы можем задать свои значения отступам с помощью update_layout, у которого есть атрибут margin, принимающий словарь из отступов:
- l — отступ слева
- r — отступ справа
- t — отступ сверху
- b — отступ снизу
Зададим везде нулевые отступы fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
update_layout можно применять последовательно несколько раз, либо можно передать все аргументы в одну функцию (мы сделаем именно так)
Код
Поскольку подписи в легенде короткие, мне не нравится, что они расположены слева. Я бы предпочёл выровнять их по центру.
Для этого можно использовать у update_layout атрибут legend, куда передать словарь с координатами для сдвига (сдвиг может быть и по вертикали, но мы используем только горизонталь).
Сдвиг задаётся в долях от ширины всей фигуры, но важно помнить, что сдвигается левый край легенды. Т.е. если мы укажем 0.5 (50% ширины), то надпись будет на самом деле чуть сдвинута вправо.
Т.к. реальная ширина зависит от особенностей вашего экрана, браузера, шрифтов и т.п., то этот параметр часто приходится подгонять. Лично у меня для этого примера неплохо работает 0.43.
Чтобы не шаманить с шириной, можно легенду относительно точки сдвига с помощью аргумента xanchor.
В итоге для легенды мы получим такой словарь:
legend=dict(x=.5, xanchor="center")
Код целиком
Стоит сразу задать подписи к осям и графику в целом. Для этого нам вновь понадобится update_layout, у которого добавится 3 новых аргумента:
title="Plot Title",
xaxis_title="x Axis Title",
yaxis_title="y Axis Title",
Следует заметить, что сдвиги, которые мы задали ранее могут негавтивно сказаться на читаемости подписей (так заголовок графика вообще вытесняется из области видимости, поэтому я увеличу отступ сверху с 0 до 30 пикселей
Код
Вернёмся к самим графикам, и вспомним, что они состоят из точек. Выделим их с помощью атрибута mode у самих объектов Scatter.
Используем разные варианты выделения для демонстрации:
Код
Теперь особенно заметно, что LATEX в функции g(x)=x отображается некорректно при наведении курсора мыши на точки.
Давайте скроем эту информацию.
Зададим для всех графиков с помощью метода update_traces поведение при наведении. Это регулирует атрибут hoverinfo, в который передаётся маска из имён атрибутов, например, «x+y» — это только информация о значениях аргумента и функции:
Код
Как-то недостаточно наглядно, не находите?
Давайте разрешим использовать информацию из всех аргументов и сами зададим шаблон подсказки.
- hoverinfo=«all»
- в hovertemplate передаём строку, используем HTML для форматирования, а имена переменных берём в фигурные скобки и выделяем %, например, %{x}
Код
А что если мы хотим сравнить информацию на 2 кривых в точках, например, с одинаковых аргументом?
Т.к. это касается всей фигуры, нам нужен update_layout и его аргумент hovermode.
Код
Кстати, маркерами можно управлять для конкретной кривой и явно.
Для этого используется аргумент marker, который принимает на вход словарь. Подробный пример.
А мы лишь ограничимся баловством:
Код
Кажется теперь на графике плохо видно ту часть, где кривые пересекаются (вероятно наиболее интересную для нас).
Для этого у нас есть методы фигуры:
- update_yaxes — ось Y (вертикаль)
- update_xaxes — ось X (горизонталь)
С их помощью зададим интервалы отображения для осей (это только начальное положение, ничто не мешает нам сменить масштаб в процессе взаимодействия с графиком).
Код
Хорошо, но правильно было бы нанести осевые линии.
Для этого у тех же функций есть 3 атрибута:
- zeroline — выводить или нет осевую линию
- zerolinewidth — задаёт толщину осевой (в пикселях)
- zerolinecolor — задаёт цвет осевой (строка, можно указать название цвета, можно его код, как принято в HTML-разметке)
Код
Давайте добавим больше разных функций на наш график, но сделаем так, чтобы по умолчанию их не было видно.
Для этого у объекта Scatter есть специальный атрибут:
visible='legendonly'
Т.к. мы центрировали легенду относительно точки сдвига, то нам не пришлось менять величину сдвига с увеличением числа подписей.
Код
Наверное всё же не следует смешивать вместе тригонометрические и арифметические функции. Давайте отобразим их на разных, соседних графиках.
Для этого нам потребуется создать фигуру с несколькими осями.
Фигура с несколькими графиками создаётся с помощью подмодуля make_subplots.
Необходимо указать количество:
- row — строк
- col — столбцов
А при построении графика передать «координаты» графика в этой «матрице» (сперва строка, потом столбец)
fig = make_subplots(rows=1, cols=2, specs=[[{'type':'domain'}, {'type':'domain'}]])
Код
Заметили, наши изменения осей применились к обоим графикам?
Естественно, если у метода, изменяющего оси указать аргументы:
- row — координата строки
- col — координата столбца
то можно изменить ось только на конкретном графике:
Код
А вот если бездумно использовать title, xaxis_title и yaxis_title для update_layout, то может выйти казус — подписи применятся только к 1 графику:
Код, приводящий к казусу
Поэтому заголовки графиков можно задать, при создании фигуры, передав в аргумент subplot_titles кортеж/список с названиями.
Подписи осей под графиками можно поменять с помощью методов фигуры:
- fig.update_xaxes
- fig.update_yaxes
Передавая в них номер строки и колонки (т.е. «координаты изменяемого графика»)
Код, подписывающий все графики независимо
И конечно, если необходимо сделать так, чтобы один график был больше, а другой меньше, то для этого используется аргументы
- column_widths — задаёт отношения ширины графиков (в одной строке)
- row_heights — задаёт отношения высот графиков (в одном столбце)
Каждый из этих параметров принимает список чисел, которых должно быть столько, сколько графиков в строке/столбце. Отношения чисел задают отношения ширин или высот.
Рассмотрим на примере ширин. Сделаем левый график вдвое шире правого, т.е. зададим соотношение 2:1.
Код
А что если мы хотим выделить одному из графиков больше места, чем другим, например, 2 строки или наоборот, 2 столбца?
В matplotlib мы использовали бы несколько фигур, либо оси с заданными размерами, здесь у нас есть другой инструмент. Мы можем сказать каким-то осям объединиться вдоль колонок или вдоль строк.
Для этого нам потребуется написать спецификацию на фигуру (для начала очень простую).
Спецификация — это список (если точнее, то даже матрица из списков), каждый объект внутри которого — словарь, описывающий одни из осей.
Если каких-то осей нет (например, если их место занимают растянувшиеся соседи, то вместо словаря передаётся None.
Давайте сделаем матрицу 2х2 и объединим вместе левые графики, получив одни высокие вертикальные оси. Для этого первому графику передадим атрибут «rowspan» равный 2, а его нижнего соседа уничтожим (None):
specs=[
[{"rowspan": 2}, {}],
[None, {}]
]
Код
Как видим, в вертикальный график идеально вписался тангенс, который отныне не невидим.
Для объединения используется:
- rowspan — по вертикали
- colspan — по горизонтали
Код, объединяющий ячейки в другом направлении
Больше примеров использования make_subplots
Последний вариант получился слишком узким по вертикали.
Высоту легко увеличить в помощью атрибута height у метода update_layout.
Размеры фигуры регулируются 2 атрибутами:
- width — ширина (в пикселях)
- height — высота (в пикселях)
Но следует помнить, если вы встраиваете фигуры plotly куда-то (а это логично, если вы делаете дашборд, например), то фигура занимает всё отведённое пространство по ширине, поэтому не изменять ширину в plotly будет не лучшей идеей. Лучше использовать стили в разметке.
Код
Увеличиваем плотность информации
Тепловая карта
Вернёмся к 1 графику, но постараемся уместить на нём больше информации, используя цветовую кодировку (что-то вроде тепловой карты — чем выше значение некой величины, тем «теплее» цвет).
Для этого у объекта go.Scatter используем уже знакомый атрибут marker (напомним, он принимает словарь). Передаём следующие атрибуты в словарь:
- color — список значений по которым будут выбираться цвета. Элементов списка должно быть столько же, сколько и точек.
- colorbar — словарь, описывающий индикационную полосу цветов справа от графика. Принимает на вход словарь. Нас интересует пока только 1 значение словаря — title — заголовок полосы.
Код
В предыдущем примере цветовая шкала не очень похожа на тепловую карту.
На самом деле цвета на шкале можно изменить, для этого служит атрибут colorscale, в который передаётся имя палитры.
Код
Можно ли добавить больше информации? Конечно можно, но тут возникают хитрости.
Для ещё одного измерения можно использовать размер маркеров.
Важно. Размер — задаётся в пикселях, т.е. величина не отрицательная (в отличие от цвета), поэтому мы будем использовать модуль одной из функций.
так же, величины меньше 2 пикселей обычно плохо видны на экране, поэтому для размера мы добавим множитель.
Размеры задаётся атрибутом size того же словаря внутри marker. Этот атрибут принимает 1 значение (число), либо список (чисел).
Код
Анимация
Можно ли ещё уплотнить информацию на графике? Да, можно, если использовать «четвёртое измерение» — время. Это так же может быть полезно и само по себе для оживленя вашего графика.
Вернёмся на пару шагов назад. Мы будем анимировать график построения параболы. Для этого нам понадобятся:
- Начальное состояние
- Кнопки (анимация не должна начинаться сама по себе, поэтому для начала мы создадим простую кнопку, её запускающую, а постепенно перейдём к временной шкале)
- Фреймы (или кадры) — промежуточные состояния
1. Начальное состояние
Это то, что будет на графике до начала анимации. В нашем случае это будет стартовая точка.
Уберём практически всё лишнее из предыдущих шагов
Шаг 1 — код полуфабрикат
2. Кнопка
Код минимальной работоспособной кнопки выглядит так:
"updatemenus": [{"type": "buttons",
"buttons": [{"label": "Your Label",
"method": "animate",
"args": [See Below]}]}]
updatemenus — это один из элементов слоя, т.е. layout фигуры, а значит, мы добавим кнопку с помощью метода update_layout.
Пока она не будет ничего делать, т.к. у нас нечего анимировать.
Шаг 2 — код всё ещё полуфабрикат
3. Фреймы
Это список «кадров» из которых состоит наша анимация.
Каждый фрейм должен содержать внутри себя целиком готовый график, который просто будет отображаться на нашей фигуре, как в декорациях.
Фрейм создаётся с помощью go.Frame()
График передаётся внутрь фрейма в аргумент data.
Таким образом, если мы хотим построить последовательность графиков (от 1 точки до целой фигуры), нам надо просто пройти в цикле:
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i], y=f(x[:i]))]))
После этого фреймы необходимо передать в фигуру. У каждой фигуры есть атрибут frames, который мы и будем использовать:
fig.frames = frames
Шаг 3 — Ёлочка гори, в смысле код, запускающий анимацию
Другой способ задать начальное состояние, слой (с кнопками) и фреймы — сразу передать всё в объект go.Figure:
- data — атрибут для графика с начальным состоянием
- layout — описание «декораций» включая кнопки
- frames — фреймы (кадры) анимации
Код
Естественно, если добавить на графики (как на начальный, так и те, что во фреймах) маркеры с указанием цвета, цветовой шкалы и размера, то анимация будет более сложного графика.
Код
Заметим, что код простейшей кнопки, которая запускает воспроизведение видео выглядит так:
dict(label="Play", method="animate", args=[None])
или
dict(label="", method="animate", args=[None])
Если мы хотим добавить кнопку «пауза» (в отличие от стандартной паузы повторное нажатие не будет вызывать воспроизведение, для начала воспроизведения придётся нажат Play), код усложнится:
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])
Правда, если добавить 2 такие кнопки, то вы заметите, что кнопка play, нажатая после паузы, в итоге начинает воспроизведение с начала. Это не совсем интуитивное поведение, поэтому ей следует добавить ещё 1 аргумент:
dict(label="", method="animate", args=[None, {"fromcurrent": True}])
Теперь полный набор из 2 наших кнопок будет выглядеть так:
buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])]
Код с 2 кнопками: запуска и остановки анимации
Иногда полезно перенести кнопки в другоме место. Рассмотрим некоторые из атрибутов, которые с этим помогут:
- direction — направление расположения кнопок (по умолчанию сверху-вниз, но если указать «left», то будет слева-направо)
- x, y — положение (в долях от фигуры)
- xanchor, yanchor — как выравнивать кнопки. У нас была раньше проблема с выравниванием легенд, тут та же история. Если хотим выровнять по центру, то x=0.5 и xanchor=«center» помогут.
Код с кнопками внизу
Слайдер
Слайдер по принципу работы похож на анимацию, но есть серьёзное отличие.
Слайдер — это элемент навигации, полоска по которой скользит ползунок, который управляет состоянием графиков на фигуре.
Т.е. если фреймы в анимации меняются один за другим, то в случае использования слайдера все графики одновременно есть. Но большая часть из них невидима. И при перемещении ползунка мы просто какие-то скрываем, а другие наоборот показываем (и перестраиваем оси, конечно).
1. Создаём список графиков. Важно, что один из графиков (например, 1й) должен быть видимым.
Для установления видимости/невидимости используется аргумент visible:
- Видимый график —
go.Scatter(visible=True, x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')
- Невидимый график —
go.Scatter(visible=False, x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x2')
Все графики рисуем на фигуре — для этого удобно использовать аргумент data фигуры, чтобы передать их все списком:
fig = go.Figure(data=trace_list)
2. Создаём список «шагов» слайдера.
Шаг имеет определённый синтаксис. По сути он описывает состояние (какие графики видимы, какие нет) и метод перехода к нему.
Шаг должен описывать состояние всех графиков.
Минимальный синтаксис 1 шага:
dict(
method = 'restyle',
args = #СПИСОК СОСТОЯНИЙ ВСЕХ ГРАФИКОВ
)
Состояние видимости/невидимости задаётся парой ‘visible’ и списка логических значений (какие графики ‘visible’, а какие нет). Поэтому для КАЖДОГО шага мы создадим список False, а потом поменяем нужное значение на True, чтобы показать какой-то конкретный график.
Шаги нужно собрать в список (т.е. это будет список словарей).
Наконец все шаги надо добавить в фигуру:
fig.layout.sliders = sliders
Минимально рабочий код (внимательно изучите его, т.к. там есть несколько тонких моментов):
Код
Если мы хотим добавить не 1 график, а 2, то добавить их придётся парой везде:
- в первоначальное активное состояние
- все неактивные (скрытые)
- парами генерировать состояние шагов
- парами активировать видимые на шаге графики
Поменяем изучаемую фигуру на синус и косинус (чтобы следить за тем как они развиваются):
Код
Просто добавим кнопки из предыдущей части. Они ничего не будут делать, т.к. мы ещё никакой анимации не добавили. Просто «прицелимся».
Код слайдера с нерабочими кнопками анимации
Если просто добавить фреймы с анимацией так, как мы это делали раньше, то кнопки анимации конечно заработают. Но только на 1 состоянии слайдера.
Код искалеченного слайдера
ОК, значит нам придётся сделать «финт ушами» и отказаться от невидимых графиков, а привязать слайдер непосредственно к фреймам анимации (Почему мы так не сделали сразу? Чтобы был альтернативный способ, подходящий непосредственно для слайдера без изысков)
- Закомментируем всё, что касается trace_list — списка наших «невидимых» графиков.
- Вынесем созданием 2 видимых графиков в первоначальное создание фигуры
- Добавим параметр name каждому фрейму
- Переделаем шаблон шага:
- Добавим label, совпадающий с именем соответствующего фрейма
- Сменим метод на «animate»
- Все аргументы заменим на 1 список из 1 единственной строки, совпадающей с именем фрейма
- Уберём «проявление» невидимых графиков на определённых позициях слайдера, т.к. теперь слайдер будет привязан к фреймам
Чтобы все изменения были хорошо заметны, я закомментировал убираемые строки
Код относительно работающего слайдера с кнопками анимации
Единственный момент, который слегка раздражает — когда происходит «воспроизведение», то ползунок слайдера не ползёт.
Это легко исправить с помощью аргумента showactive для меню. Так же его выключение снимет состояние «нажато» с кнопок Play/Pause.
Код слайдера, который уже нормально анимирован
Конечно добавим данные тепловой карты и размер маркера для увеличения плотности информации
Заметьте, что colorbar мы добавили всего 1 раз, однако, в него пришлось внести некоторые правки — мы сдвинули его по вертикали слегка вниз, т.к. теперь в правой колонке у нас есть легенда.
Код
Осталось немного облагородить панель слайдера.
Добавим подписи к графику и осям, увеличим и оформим подпись текущего значения слайдера (в других обстоятельствах он стал бы временной шкалой), сместим кнопки анимации влевой, а слайдер чуть сожмём, чтобы освободить им место.
- Аргумент currentvalue — задаёт форматирование подписи к текущему шагу, включая префикс, положение на слайде, шрифт
- Аргументы x, y, xanchor, yanchor, pad — задают положение и отступы для слайдера и их синтаксис аналогичен таковому у кнопок
Код
Возникает вопрос зачем же мы делали вариант с невидимыми графиками, если они не пригодились?
На самом деле они нужны, в том числе для анимации. Дело в том, что если вы хотите на разных слайдах анимации показывать разное количество графиков, то вам надо в самом начале на этапе создания фигуры добавить столько графиков, сколько их может отображаться максимально. Они все должны быть невидимыми.
Я специально задам 2 переменные:
- graphs_invisible — содержит как невидимый корректный график, так и пустой объект графика вообще без указания видимости
- graphs_visible — содержит корректные видимые графики, которые надо показывать по очереди
В первоначальном состоянии мы отобразим невидимые графики или пустышки, а в каждом фрейме будем комбинировать видимые и невидимые, чтобы их количество было постоянным.
Код
Философский вопрос — зачем мы так мучаемся, если есть plotly.express?
Документация plotly по анимации plotly.com/python/animations начинается с феерического примера:
Код, создающий довольно сложную анимацию данных с помощью Plotly Express
Действительно, функции Экспресса принимают на вход датафреймы (да, обычные из Pandas), вам лишь нужно указать колонки по которым производится агрегация данных. И можно сразу строить и тепловые карты, и анимации очень небольшим количеством кода, как в этом примере:
Ответ и прост и сложен:
- Стандартные примеры Экспресса могут не удовлетворить ваших потребностей, нужно что-то чуть более сложное и хитрое, например, совместить график и гистограмму. Вручную вы получаете больше гибкости.
- Понять как сгруппировать и агрегировать данные в датафрейме для такой визуализации порой сложнее, чем просто построить набор картинок для фреймов анимации/слайдера.
Показ plotly графиков на сайте
Plotly — это не только библиотека для Python, но ещё и JS, это значит, что любые графики, которые вы строите в jupyter notebook, вы можете показывать и на сайте (если он на Python, либо если вы заранее выгрузите всё необходимое).
В рамках этого урока мы не будем рассматривать полный цикл разработки дашборда или веб приложения, просто рассмотрим небольшой пример:
import json
#Здесь вы создаёте свой график в фигуре fig
graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
В graphJSON окажется приличный JSON. Не будем вдаваться в его особенности, хотя легко заметить, что его структура соответствует нашему объекту фигуры, со всеми графиками, фреймами, подложками, кнопками и т.п.
Давайте добавим немного по краям и сложим этот код в файл:
with open('example.JSON', 'w') as file:
file.write('var graphs = {};'.format(graphJSON))
А теперь откроем страницу example.html, когда в одном каталоге с ней есть наш файл example.JSON (Для корректной работы необходимо подключение к интернет, т.к. некоторые стили и JS подтягиваются со сторонних сайтов).
Удивительно, не правда ли?
При этом, если посмотреть структуру, то она очень проста и содержит всего 4 важных объекта (их порядок на странице важен):
1. Подключение JS библиотеки Plotly (в примере это делается онлайн из CDN, но если вы делаете локальный продукт, например приватный дашборд для работы внутри организации, JS легко поместить на локальный сервер и поменять ссылки)
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
2. Блок, где будет выводиться график
<div id="plotly_graph"></div>
Важно понимать, что Plotly по умолчанию не фиксирует размеры графика, поэтому вы можете применить к контейнеру свои стили, сверстав его такого размера и в том месте, где это необходимо вам. Plotly постарается занять всё свободное место по ширине.
Важно использовать действительно уникальный id — именно по нему plotly будет находить место на странице, куда надо встроить график.
3. JS переменная, содержащая наш JSON, описывающий график. Мы сформировали её и сложили в файл целиком. Вы можете выводить её непосредственно в коде страницы.
<script src="example.JSON"></script>
4. Вызов JS функции plotly, которая построит график.
- первым передаётся id контейнера, в котором выводить график
- вторым передаётся переменная, содержащая JSON Plotly объекта
<script>Plotly.plot('plotly_graph',graphs,{});</script>
А теперь испробуйте код на каком-нибудь своём графике!
Круговые диаграммы
Для полноты картины рассмотрим несколько других способов визуализации данных, кроме линейных графиков. Начнём с круговых диаграмм
Для нашего эксперимента «подбросим» 100 раз пару игральных кубиков (костей) и запишем суммы выпавших очков.
Код
Для того чтобы создать круговую диаграмму используем go.Pie, который добавляем так же, как мы добавляли график на созданную фигуру.
Используем 2 основных атрибута:
- values — размер сектора диаграммы, в нашем случае прямо пропорционален количеству той или иной суммы
- labels — подпись сектора, в нашем случае значение суммы. Если не передать подпись, то в качестве подписи будет взят индекс значения из списка values
Код
Сразу бросается в глаза то, что хотя мы передали массив, упорядоченный по индексам, но при построении он был пересортирован по значениям.
Это легко исправить с помощью аргумента sort = False
Код
Так же при желании мы можем «выдвинуть» один или несколько секторов.
Для этого используем аргумент pull, который принимаем список чисел. Каждое число — доля, на которую надо выдвинуть сектор из круга:
- 0 — не выдвигать
- 1 — 100% радиуса круга
Мы создадим список из нулей, такой же длинны, что массив значений. А потом один элемент увеличим до 0.2.
Код
Если вам не нравятся классические круговые диаграммы «пирожки», то легко превратить их в «пончики», вырезав сердцевину. Для этого используем аргумент hole, в который передаём число (долю радиуса, которую надо удалить):
- 0 — не вырезать ничего
- 1 — 100% вырезать, ничего не оставить
Таким образом, значение 0.9 превратит круговую диаграмму в кольцевую.
Код
Кстати, образовавшаяся «дырка от бублика» — идеальное место для подписи, которую можно сделать с помощью атрибута annotations слоя.
Не забываем, что аннотаций может быть много, поэтому annotations принимаем список словарей.
Текст аннотации поддерживает HTML разметку (чем мы воспользуемся, задав абсурдно длинный текст, не помещающийся в 1 строку)
Код
Естественно обычный способы оформления визуализаций, показанные для графиков, тут тоже работают:
- title
- title_x
- margin
- legend_orientation
Код
Что, если вы хотим детализовать картинку?
Sunburst или диаграмма «солнечные лучи»
Нам на помощь приходит диаграмма «солнечные лучи» — иерархическая диаграмма на основе круговой. По сути это набор кольцевых диаграмм, нанизанных друг на друга, причём сегменты следующего уровня находятся в пределах границ сегментов своего «родителя» на предыдущем.
Например, получить 8 очков с помощью 2 игральных костей можно несколькими способами:
- 2 + 6
- 3 + 5
- 4 + 4
Для построения диаграммы нам потребуется go.Sunburst и 4 основных аргумента:
- values — значения, задающие долю от круга на диаграмме
- branchvalues=«total» — такое значение указывает, что значения родительского элемента являются суммой значений потомков. Это необходимо для того, чтобы составить полный круг на каждом уровне.
- labels — список подписей, которые отображаются на диаграмме
- parents — список подписей родителей, для построения иерархии. Для элементов 0 уровня (без родителей) родителем указывается пустая строка.
Для начала обойдёмся 2 уровнями (все события и суммы)
Код
А теперь добавим группировку по парам исходов игральных костей и вычисление для таких пар «родителей».
Конечно, если кости идентичны, то 6+2 и 2+6 — это идентичные исходы, как и пара 3+5 и 5+3, но в рамках следующего примера мы будем считать их разными, просто чтобы не добавлять лишнего кода.
Так же уменьшим отступы, т.к. подписи получаются уж очень мелкими.
Код
Гистограммы
Естественно не круговыми диаграммами едиными, иногда нужны и обычные столбчатые.
Простейшая гистограмма строится с помощью go.Histogram. В качестве единственного аргумента в x передаём список значений, которые участвуют в выборке (Plotly самостоятельно сгруппирует их в столбцы и вычислит высоту), в нашем случае это колонка с суммами.
Код
Если по какой-то причине нужно построить не вертикальную, а горизонтальную гистограмму, то меняем x на y:
Код
А что, если у нас 2 или 3 набора данных и мы хотим их сравнить? Сгенерируем ещё 1100 бросков пар кубиков и просто добавим на фигуру 2 гистограммы:
Код
Все 3 выборки подчиняются одному и тому же распределению, и очевидно, но количество событий сильно отличается, поэтому на нашей гистограмме некоторые столбцы сильно больше других.
Картинку надо «нормализовать». Для этого служит аргумент histnorm.
Код
Как и предыдущие виды визуализаций, гистограммы могут иметь оформление:
- подпись графика, подписи осей
- ориентация и положение легенды.
- отступы
Код
Другой интересны режим оформления — barmode=’overlay’ — он позволяет рисовать столбцы гистограммы одни поверх других.
Имеет смысл использовать его одновременно с аргументом opacity самих гистограмм — он задаёт прозрачность гистограммы (от 0 до 100%).
Однако, большое количество гистограмм в таком случае тяжело визуально интерпретировать, поэтому мы скроем одну.
Код
Если мы говорим о вероятности, то имеет так же смысл построить и накопительную гистограмму. Например, вероятности выпадения не менее чем X очков на сумме из 2 игральных костей.
Для этого используется аргумент гистограммы cumulative_enabled=True
Код
Так же весьма полезно то, что на одной фигуре можно совмещать график, построенный по точкам (go.Scatter) и гистограмму (go.Histogram).
Для демонстрации такого применения, давайте сгенерируем 1000 событий из другого распределения — нормального. Для него легко построить теоретическую кривую. Мы возьмём для этого готовые функции из модуля scipy:
- scipy.stats.norm.rvs — для генерации событий
- scipy.stats.norm.pdf — для получения теоретический функции распределения
Код
Этот пример так же демонстрирует как происходит объединение в столбцы, если величина не дискретная.
В данном случае каждый столбец тем выше, чем больше значений попало в интервал, соответствующий ширине этого столбца.
В свою очередь это означает, что при необходимости мы можем регулировать количество столбцов и их ширину (это 2 взаимосвязанных параметра).
- Вариант 1 — задав ширину столбца — xbins={«size»:0.1}
- Вариант 2 — задав количество столбцов — nbinsx=200
Код
Другие столбчатые диаграммы — Bar Charts
Столбчатые диаграммы можно сформировать и своими силами, если сгруппировать данные и вычислить высоты столбцов.
Далее, используя класс go.Bar передаём названия столбцов и их величины в 2 аргумента:
- x — подписи
- y — величины
Код
Важно!
Как и круговая диаграмма, такая столбчатая в отличие от ранее изученных гистограмм не построит столбец для того, чего нет!
Например, если мы сделаем только 10 бросков по 2 кости, то среди них не может выпасть всех возможных случаев. А значит, они не отобразятся на диаграмме:
Код дырявой диаграммы без крайнего столбца
При необходимости выведения ВСЕХ, даже нулевых столбцов, их следует сформировать самостоятельно.
Код
Создадим парную гистограмму для 2 наборов по 100 бросков, в оба набора добавив на всякий случай колонки с нулями, если их нет.
В зависимости от генерации начальных данных в каких-то местах должна быть только 1 колонка, либо не будет колонок вообще.
Код
А вот если мы хотим вывести и 3й набор испытаний (1000 бросков), то придётся самостоятельно нормализовать данные, т.к. у go.Bar в отличие от гистограмм нет аргумента вроде histnorm.
Иначе вновь получим странную картинку, когда столбцы третьей серии сильно больше остальных и их нельзя сравнить.
Код
Зато есть у такой диаграммы и серьёзное преимущество — высота столбцов не обязана быть положительной. Это можно использовать, например, для построения диаграммы прибылей и убытков по периодам. Просто вычтем из одного набора значений другой, получим список чисел (некоторые отрицательные, некоторые положительные, а некоторые 0) и используем его для построения.
Так же это отличный повод вспомнить, что для столбчатых диаграмм работают все способы оформления, которые мы отработали на графиках. В данном случае логично выделить нулевой уровень красным цветом.
Код
А ещё можно вывести подписи прямо поверх столбцов. Для этого пригодится пара аргументов:
- text — сюда передаём список тех значений, которые надо вывести (можно заранее сформировать произвольные строки)
- textposition — способ вывода текста:
- ‘auto’ — Plotly самостоятельно пример решение
- ‘outside’ — снаружи, в случае столбчатой диаграммы это будет над столбцом с положительной высотой и под столбцом с отрицательной высотой
- ‘inside’ внутри прямоугольника (если высота прямоугольника = 0, то надпись не будет видно)
- и т.д.
Код
А что, если мы хотим вывести не вертикальные столбцы, а горизонтальные полоски?
Это легко сделать надо только:
- Добавить аргумент orientation=’h’
- Поменять местами x и y в данных и подписях (а так же везде, где мы задаём подписи осей, осевые линии и т.п.)
И у нас получился вот такая «диаграмма торнадо» (так же этот способ подходит, например, если подписи столбцов слишком длинные и их неудобно читать снизу)
Код
Так же при отображении гистограмм 2 наборов данных есть полезный аргумент для слоя — barmode=’stack’.
Он позволяет объединять столбцы в общие колонки. Это полезно, если мы представляем наши данные, как единую серию экспериментов, т.е. мы бросили 100 раз кубики, потом ещё 100 раз и хотим узнать что вышло в итоге, сколько всё-таки каких исходов.
Код
Ящики с усами (Box Plots)
А что, если требуется более сложный и информативный инструмент? Примером может служить диаграмма размаха или «ящик с усами» (ссылка)
Для примера создадим набор 100 событий с бросками набора других игральных костей. На этот раз 3 4-гранных кости (3d4). Это могло бы быть сравнением 2 игровых мечей с уроном 2d6 и 3d4, однако, любому очевидно, что второй эффективнее (разброс 2-12 против разброса 3-12). Вся ли это информация, которую можно «вытащить» из этих данных?
Конечно нет, ведь у них будут отличаться и меры центральной тенденции (медианы или средние).
Для построения ящиков с усами мы используем класс go.Box. Данные (весь массив «сумм») передаём в единственный аргумент — y.
Код
Не совсем понятно кто есть кто.
Ну или относительно понятно
Однако, как и для других фигур, тут можно задать подписи.
Код
Иногда вертикальные ящики не очень наглядны (либо сложно прочитать подписи снизу), тогда их можно положить «на бок» так же, как мы делали с обычными столбчатыми диаграммами:
Код
Иногда полезно для каждого ящика с усами так же отобразить облако точек, формирующий распределение. Это легко сделать с помощью аргумента boxpoints=’all’
Код
Географические карты
Plotly поддерживает великое множество разных видов визуализаций, охватить все из которых в одном обзоре довольно трудно (и бессмысленно, т.к. общие принципы будут схожи с ранее показанными)
Полезно будет в завершении лишь показать один из наиболее красивых на мой взгляд «графиков» — Scattermapbox — геокарты.
Для этого возьмём CSV с 1117 населёнными пунктами РФ и их координатами (файл создан на основе github.com/hflabs/city/blob/master/city.csv) — ‘https://raw.githubusercontent.com/hflabs/city/master/city.csv.
Воспользуемся классом go.Scattermapbox и 2 атрибутами:
- lat (широта)
- lon (долгота)
Так же нам понадобится подключить OSM карту, т.к. Scattermapbox может работать с разными видами карт:
fig.update_layout(mapbox_style="open-street-map")
Код
Как-то криво, правда? Давайте сдвинем центр карты так, чтобы он пришёлся на столицу нашей родины (вернее столицу родины автора этих строк, т.к. у читателя родина может быть иной).
Для этого нам понадобится объект go.layout.mapbox.Center или обычный словарь с 2 аргументами:
- lat
- lon
Этот объект/словарь мы передаём в качестве значения аргумента center словаря внутрь mapbox:
fig.update_layout(
mapbox=dict(
center=dict(
lat=...,
lon=...
)
)
)
Код
Неплохо, но масштаб мелковат (по сути сейчас отображается карта мира на которой 1/6 часть суши занимает далеко не всё полезное место).
Без ущерба для полезной информации можно слегка приблизить картинку.
Для этого используем аргумент zoom=2
Код
Увы, на карту попало слишком много Европы без данных и слишком мало отечественного дальнего востока, так что в данном случае центрироваться возможно стоит по геометрическому центру страны (вычислим его весьма «приблизительно»).
Код
Давайте добавим подписи городов. Для этого используем аргумент text.
Код
Вспомним, как мы увеличивали плотность информации для обычных графиков. Тут так же можно задать размер маркера, например, от населения.
Следует учесть 2 момента:
- Данные замусорены. Население некоторых городов имеет вид 96[3]. Поэтому колонка с население не численная и нам нужна функция, которая этот мусор обнулит, либо приведёт к какому-то читаемому виду.
- Размер маркера задаётся в пикселях. И 15 миллионов пикселей — слишком большой диаметр. Потому разумно придумать формулу, например, логарифм.
Код
Добавим цветовую кодировку. Для этого используем данные о годе основания. Т.к. не для всех городов он точно известен (для некоторых указан век, для некоторых римскими, а не арабскими цифрами), то мы так же вынуждены будем написать свою функцию для обработки годов, но для простоты все проблемные случаи мы будем возвращать None и потом просто удалим все такие города.
Если возвращать, например, np.NaN, то при построении тепловой карты эти значения будут считаться эквивалентными 0 и мы будем считать такие населённые пункты одними из самых старых в стране)
Код
А что если мы хотим нанести линию? Без проблем!
Возьмём и добавим новый график на имеющуюся картинку, который будет содержать только 2 точки: Москву и Санкт-Петербург.
Нам понадобится новый атрибут mode = «lines» (у него доступны и другие значения, например «markers+lines»), но мы уже вывели метку города, так что не хотим её дублировать.
Не будем выводить никакой информации при наведении на этот новый график, чтобы она не перебивала эффект наведения на основные точки. hoverinfo=’skip’
Код
Ох, кажется полоска тепловой карты наложилась на легенду. Более того, теперь легенда выводится раздельная для точек-городов и линии между Москвой и Санкт-Петербургом.
- Переключим легенду в горизонтальный режим legend_orientation=«h» (в настройках слоя)
- «сгруппируем» легенды вместе. Для этого у каждого графика группы добавим аргумент legendgroup=«group» (можно использовать любые строки, лишь бы они были одинаковые у членов одной группы).
Код
Отлично, теперь они включаются и выключаются вместе. Давайте уберём из легенды «лишний» элемент (линию городов) showlegend=False
А так же подпишем легенду для городов.
Код
Давайте добавим чуть более осмысленные линии на карту. Для этого воспользуемся маршрутом поезда №002М «Россия» Москва-Владивосток (сайт РЖД)
Я заранее подготовил отдельный файл с городами, на маршруте, разбитом по дням. Это примерная разбивка, т.к. расписание меняется, так что не используйте мою таблицу для оценка когда вы приедете к любимой тёще в гости. Некоторые станции поезда не имеют аналога в нашей оригинальной таблице городов, поэтому они пропущена. Некоторые города указаны 2 раза, т.к. они являются конечной точкой одного дневного перегона и начальной другого дневного перегона.
Наш маршрут будет соединять города, а не вокзалы, так же он не будет совпадать с реальной железной дорогой. Это просто визуализация маршрута, а не инструмент навигации!
Код
Если мы хотим анимировать процесс появления маршрута по дням, то нам придётся использовать тот же приём, что и ранее с появлением нескольких графиков — заранее вывести все графики или их заглушки невидимыми, а потом на каждом фрейме и шаге слайдера делать их видимыми.
Код
Безусловно мы разобрали далеко не все виды графиков Plotly. Однако, данного базового набора примеров должно быть достаточно чтобы понять принцип по которому все они работают.
С примерами других визуализаций можно ознакомиться тут — plotly.com/python (обратите внимание, что для каждой категории приведены далеко не все примеры, больше примеров всегда доступно по ссылке «More …»
Это мой первый набег на Plotly. Мне нравится простота использования по сравнению с matplotlib и боке. Однако я застрял на некоторых основных вопросах о том, как украсить свой сюжет. Во-первых, это код ниже (он полностью функционален, просто скопируйте и вставьте!):
import plotly.express as px
from plotly.subplots import make_subplots
import plotly as py
import pandas as pd
from plotly import tools
d = {'Mkt_cd': ['Mkt1','Mkt2','Mkt3','Mkt4','Mkt5','Mkt1','Mkt2','Mkt3','Mkt4','Mkt5'],
'Category': ['Apple','Orange','Grape','Mango','Orange','Mango','Apple','Grape','Apple','Orange'],
'CategoryKey': ['Mkt1Apple','Mkt2Orange','Mkt3Grape','Mkt4Mango','Mkt5Orange','Mkt1Mango','Mkt2Apple','Mkt3Grape','Mkt4Apple','Mkt5Orange'],
'Current': [15,9,20,10,20,8,10,21,18,14],
'Goal': [50,35,21,44,20,24,14,29,28,19]
}
dataset = pd.DataFrame(d)
grouped = dataset.groupby('Category', as_index=False).sum()
data = grouped.to_dict(orient='list')
v_cat = grouped['Category'].tolist()
v_current = grouped['Current']
v_goal = grouped['Goal']
fig1 = px.bar(dataset, x = v_current, y = v_cat, orientation = 'h',
color_discrete_sequence = ["#ff0000"],height=10)
fig2 = px.bar(dataset, x = v_goal, y = v_cat, orientation = 'h',height=15)
trace1 = fig1['data'][0]
trace2 = fig2['data'][0]
fig = make_subplots(rows = 1, cols = 1, shared_xaxes=True, shared_yaxes=True)
fig.add_trace(trace2, 1, 1)
fig.add_trace(trace1, 1, 1)
fig.update_layout(barmode = 'overlay')
fig.show()
Вот результат:
Вопрос 1: как мне уменьшить ширину v_current (показанную красной полосой)? Как и в случае, он должен быть меньше по высоте, так как это горизонтальная полоса. Я добавил высоту 10 для trace1 и 15 для trace2, но они по-прежнему отображаются на той же высоте.
Вопрос 2: Есть ли способ сделать так, чтобы v_goal (показанная синей полосой) показывала только ее правый край, а не заполненную полосу? Что-то вроде этого:
Если вы заметили, я также добавил строку под каждой категорией. Есть ли быстрый способ добавить это? Не нарушение сделки, а просто бонус. Еще я пытаюсь добавить анимацию и т. Д., Но это как-нибудь в другой раз!
Заранее благодарим за ответ!
2 ответа
Лучший ответ
Запуск plotly.express
вернет объект plotly.graph_objs._figure.Figure
. То же самое касается plotly.graph_objects
запуска go.Figure()
вместе, например, с go.Bar()
. Итак, после построения фигуры с помощью plotly express вы можете добавлять линии или трассы через ссылки непосредственно на фигуру, например:
fig['data'][0].width = 0.4
Это именно то, что вам нужно, чтобы установить ширину ваших полос. И вы можете легко использовать это в сочетании с plotly express:
Код 1
fig = px.bar(grouped, y='Category', x = ['Current'],
orientation = 'h', barmode='overlay', opacity = 1,
color_discrete_sequence = px.colors.qualitative.Plotly[1:])
fig['data'][0].width = 0.4
Сюжет 1
Чтобы получить полосы или формы для обозначения целевых уровней, вы можете использовать подход, описанный DerekO, или вы можете использовать:
for i, g in enumerate(grouped.Goal):
fig.add_shape(type="rect",
x0=g+1, y0=grouped.Category[i], x1=g, y1=grouped.Category[i],
line=dict(color='#636EFA', width = 28))
Полный код:
import plotly.express as px
from plotly.subplots import make_subplots
import plotly as py
import pandas as pd
from plotly import tools
d = {'Mkt_cd': ['Mkt1','Mkt2','Mkt3','Mkt4','Mkt5','Mkt1','Mkt2','Mkt3','Mkt4','Mkt5'],
'Category': ['Apple','Orange','Grape','Mango','Orange','Mango','Apple','Grape','Apple','Orange'],
'CategoryKey': ['Mkt1Apple','Mkt2Orange','Mkt3Grape','Mkt4Mango','Mkt5Orange','Mkt1Mango','Mkt2Apple','Mkt3Grape','Mkt4Apple','Mkt5Orange'],
'Current': [15,9,20,10,20,8,10,21,18,14],
'Goal': [50,35,21,44,20,24,14,29,28,19]
}
dataset = pd.DataFrame(d)
grouped = dataset.groupby('Category', as_index=False).sum()
fig = px.bar(grouped, y='Category', x = ['Current'],
orientation = 'h', barmode='overlay', opacity = 1,
color_discrete_sequence = px.colors.qualitative.Plotly[1:])
fig['data'][0].width = 0.4
fig['data'][0].marker.line.width = 0
for i, g in enumerate(grouped.Goal):
fig.add_shape(type="rect",
x0=g+1, y0=grouped.Category[i], x1=g, y1=grouped.Category[i],
line=dict(color='#636EFA', width = 28))
f = fig.full_figure_for_development(warn=False)
fig.show()
2
vestland
13 Мар 2021 в 20:53
Вы можете использовать Plotly Express
, а затем получить прямой доступ к объекту-фигуре, как описано в @vestland, но лично я предпочитаю использовать graph_objects
для внесения всех изменений в одном месте.
Я также отмечу, что, поскольку вы складываете столбцы на одном графике, вам не нужны подзаголовки. Вы можете создать graph_object
с помощью fig = go.Figure()
и добавить трассировки, чтобы получить сложенные столбцы, аналогично тому, что вы уже сделали.
Что касается вопроса 1, если вы используете go.Bar()
, вы можете передать параметр ширины. Однако это единицы измерения оси положения, и поскольку ось Y категориальна, ширина = 1 заполнит всю категорию, поэтому я выбрал ширину = 0,25 для красной полосы и ширину = 0,3 (немного больше) для синюю полосу, поскольку, похоже, это было вашим намерением.
Что касается вопроса 2, единственное, что приходит на ум, — это взлом. Разделите полосы на две части (одна с высотой = исходная высота — 1) и установите ее непрозрачность на 0, чтобы она была прозрачной. Затем поместите пики высотой 1 поверх прозрачных планок.
Если вы не хотите, чтобы следы отображались в легенде, вы можете установить это индивидуально для каждой полосы, передав showlegend=False
в fig.add_trace
, или полностью скрыть легенду, передав showlegend=False
в метод fig.update_layout
.
import plotly.express as px
import plotly.graph_objects as go
# from plotly.subplots import make_subplots
import plotly as py
import pandas as pd
from plotly import tools
d = {'Mkt_cd': ['Mkt1','Mkt2','Mkt3','Mkt4','Mkt5','Mkt1','Mkt2','Mkt3','Mkt4','Mkt5'],
'Category': ['Apple','Orange','Grape','Mango','Orange','Mango','Apple','Grape','Apple','Orange'],
'CategoryKey': ['Mkt1Apple','Mkt2Orange','Mkt3Grape','Mkt4Mango','Mkt5Orange','Mkt1Mango','Mkt2Apple','Mkt3Grape','Mkt4Apple','Mkt5Orange'],
'Current': [15,9,20,10,20,8,10,21,18,14],
'Goal': [50,35,21,44,20,24,14,29,28,19]
}
dataset = pd.DataFrame(d)
grouped = dataset.groupby('Category', as_index=False).sum()
data = grouped.to_dict(orient='list')
v_cat = grouped['Category'].tolist()
v_current = grouped['Current']
v_goal = grouped['Goal']
fig = go.Figure()
## you have a categorical plot and the units for width are in position axis units
## therefore width = 1 will take up the entire allotted space
## a width value of less than 1 will be the fraction of the allotted space
fig.add_trace(go.Bar(
x=v_current,
y=v_cat,
marker_color="#ff0000",
orientation='h',
width=0.25
))
## you can show the right edge of the bar by splitting it into two bars
## with the majority of the bar being transparent (opacity set to 0)
fig.add_trace(go.Bar(
x=v_goal-1,
y=v_cat,
marker_color="#ffffff",
opacity=0,
orientation='h',
width=0.30,
))
fig.add_trace(go.Bar(
x=[1]*len(v_cat),
y=v_cat,
marker_color="#1f77b4",
orientation='h',
width=0.30,
))
fig.update_layout(barmode='relative')
fig.show()
2
Derek O
13 Мар 2021 в 20:50