Большое руководство по визуализации. Визуализация данных с помощью веб-фреймворка Dash.
Dash является довольно интересным Веб-фреймворком для визуализации данных и имеет в семе довольно много полезных функций в сочетании с простотой их применения.
Сам Dash это некий коллаб HTML, React.Js, Flask и CSS и предоставляет python классы для всех своих визуальных компонентов.
В качестве демонстративного датасета я возьму датасет diamonds с сайта kaggle (https://www.kaggle.com/shivam2503/diamonds)
Если описывать полностью все функции, которые предоставляет dash, уйдет довольно много времени, исходя из этого, предлагаю в качестве простого примера визуализировать более камерную задачу. Допустим, вывести гистограмму количества драгоценных камней в зависимости от нескольких факторов: качество огранки, уровень чистоты и цвет. При этом выбор нужной гистограммы происходит непосредственно в веб интерфейсе.
Привожу пример сэмпла датасета
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.express as px
import pandas as pd
from dash.dependencies import Input, Output
Для начала отметим что работу в dash можно разделить на две части внешнюю(layout) то есть создание визуальной составляющей дашборда, и внутреннюю(callbacks) в которых мы описываем логику взаимодействия с объектами на дашборде.
В начале инициализируем класс dash, а также подгружаем dataset
app = dash.Dash(__name__)
df = pd.read_csv('C://Users//User//Desktop//Учеба//diam.csv',sep = ';')
Далее создадим заголовок.
Для более красивого отображения, я использовал эти стили (https://github.com/matyushkin/lessons/blob/master/visualization/dash/avocado_analytics/apps/app_2/assets/style.css)
Определяем компонент html.Div а также вносим в него заголовок(html.h1) и абзац(html.P)
app.layout = html.Div(children=[
html.Div(
children=[
html.H1(children='Analysis diamonds dataset', className= 'header-title'),
html.P(children='maybe this demo will be useful to someone (:', className= 'header-description')и
], className= 'header')])
ClassName является ссылкой на css файл
Который можно подгрузить внутрь проекта создав для него папку assets.
Вид структуры должен быть подобный
И запустим наше приложение
if __name__ == '__main__':
app.run_server(debug=True)
Заголовок готов, теперь можно приступить к созданию объекта dropdown
Это будет меню для выбора необходимой гистограммы.
Данный элемент в коде будет выглядеть вот так
html.Div([
dcc.Dropdown(
id='demo_drop',
options=[
{'label': 'Огранка', 'value': 'cut'},
{'label': 'Ясность(чистота)', 'value': 'clarity'},
{'label': 'Цвет', 'value': 'color'}
],
value='cut', className="dropdown"
)])
В данном случае id означает уникальный идентификатор объекта он нам понадобится, когда будем описывать логику. Как можно понять из кода функционал данного объекта можно описать через словарь. Атрибут value означает дефолтное выбранное значение.
Далее добавляем графический объект
dcc.Graph(id='output_graph')],className="card")
Пока что он пуст, но это только пока.
Далее нам нужно сделать так что бы наше приложение реагировало на запрос пользователя и для этого нужно использовать функции обратного вызова. В них и будет описана связь между объектами Dropdown и graph.
@app.callback(
Output(component_id='output_graph', component_property='figure'),
[ Input(component_id='demo_drop', component_property='value')]
)
В данном случае в input у нас заносится уникальный идентификатор dropdown объекта и его значения, которые мы описывали в словаре атрибута options.
А output возвращает гистограмму и ее значения.
Остается только прописать логику для обработки информации.
Что бы понять сколько было драгоценных камней для каждого уровня огранки, нужно сгруппировать данные по уровням огранки и вычислить количество строк внутри группы.
Для pandas это будет выглядеть вот так
h = df.groupby(['cut'], as_index=False, sort=False)['carat'].count()
Теперь вплетаем данное вычисление в функцию
def update_output(value):
if value == 'cut':
h = df.groupby(['cut'], as_index=False, sort=False)['carat'].count()
elif value == 'clarity':
h = df.groupby(['clarity'], as_index=False, sort=False)['carat'].count()
elif value == 'color':
h = df.groupby(['color'], as_index=False, sort=False)['carat'].count()
fig = px.bar(h, x=value, y="carat", labels = {"carat" : "Count"} )
return fig
То есть если описывать, то каждая вариация выбора пользователя связана со значением, которое используется в алгоритме.
А переменная fig создает гистограмму
Таким образом у нас выходит такого вида интерфейс с выбором параметра группировки
Итоговый код
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.express as px
import pandas as pd
from dash.dependencies import Input, Output
app = dash.Dash(__name__)
df = pd.read_csv('C://Users//User//Desktop//Учеба//diam.csv',sep = ';')
app.layout = html.Div(children=[
html.Div(
children=[
html.H1(children='Analysis diamonds dataset', className= 'header-title'),
html.P(children='maybe this demo will be useful to someone (:', className= 'header-description')
], className= 'header'),
#html.Label('Количество'),
html.Div([
dcc.Dropdown(
id='demo_drop',
options=[
{'label': 'Огранка', 'value': 'cut'},
{'label': 'Ясность(чистота)', 'value': 'clarity'},
{'label': 'Цвет', 'value': 'color'}
],
value='cut', className="dropdown"
), dcc.Graph(id='output_graph')],className="card")
])
@app.callback(
Output(component_id='output_graph', component_property='figure'),
[Input(component_id='demo_drop', component_property='value')]
)
def update_output(value):
if value == 'cut':
h = df.groupby(['cut'], as_index=False, sort=False)['carat'].count()
elif value == 'clarity':
h = df.groupby(['clarity'], as_index=False, sort=False)['carat'].count()
elif value == 'color':
h = df.groupby(['color'], as_index=False, sort=False)['carat'].count()
fig = px.bar(h, x=value, y="carat", labels = {"carat" : "Count"} )
return fig
if __name__ == '__main__':
app.run_server(debug=True)
как создать многостраничный дэшборд, используя sidebar в качестве навигационного элемента, и наполнить страницы разного рода контентом.
Начнем с необходимых библиотек.
Далее читаем необходимые нам данные, для удобства я не буду изменять применяемый датасет и продолжу работу с данными из первой части.
df1 = pd.read_csv('C://*.csv',sep = ';')
Я решил использовать наиболее доступную тему оформления – bootstrap.
Собственно сам bootstrap – это набор инструментов для Веб приложений, который содержит довольно большое количество компонентов оформления для веб интерфейса, подключаем его при инициализации класса.
Уделим немного времени стилям, чтобы все выглядело аккуратно
По логике страница делится на две части: под sidebar и под контент.
Это и будут два наших начальных элемента. Также нам нужен элемент location, dcc.location – скрытый элемент, с помощью которого мы сможем получить доступ к различным частям url адреса, на который загружено приложение. И начнем с него.
app.layout = html.Div([
dcc.Location(id="url"),
Далее создаем сам sidebar
h2 будет заголовком sidebar’a. Для того чтобы было красиво, проведем под ним горизонтальную черту
html.H2("Map", className="display-3", style={'color': 'white'}),
html.Hr(style={'color': 'white'})
Далее перейдем к созданию макета навигационных элементов, допустим у нас будет три страницы.
Первый аргумент dbc.NavLink это имя которое будет высвечиваться на элементе, второй – ссылка, а третий аргумент означает, что элемент автоматически установит активное свойство, если имя пути совпадает со ссылкой, более подробно об этом можно почитать в документации.
Свойство vertical как можно догадаться означает ориентацию навигационных элементов
Свойство pills означает отображение активности
pills = True
pills = False
И также добавляем строку для отображения контента
html.Div(id="page-content", children=[], style=CONTSTYLE)
В итоге у нас получается примерно такая картина
Теперь нам необходимо чтобы наш дашборд отвечал нашим запросам (Оживить sidebar). Для этого напишем функцию обратного вызова(callback).
Его идея заключается в том, чтобы она определяла на какой мы ссылке и в зависимости от этого выводил необходимый нам контент.
И далее, нам необходима функция, которая будет переключать контент в зависимости от указанной нами ссылки.
По логике, это простое ветвление через elif. Далее нужно подготовить место для контента.
Фундамент создан, осталось наполнить страницы контентом.
Для большей наглядности для наполнения первой страницы я буду использовать контент предыдущей части статьи.
Тк это отдельная страница, начнем с создания макета
Как можно увидеть в предыдущей части статьи я использовал элемент dropdown для выбора необходимых данных выборки. Элементу dropdown для работы также необходима функция обратного вызова, в данной ситуации, она будет описана после функции навигации по контенту, так как ей необходим контент который уже существует.
То, что получаем на выходе.
Для заполнения второй страницы контентом, я решил использовать элемент scatter, чтобы показать зависимость цены камня от карата.
Структура второй страницы будет состоять из заголовка и двух графиков.
На первом графике мы сможем увидеть информацию о зависимости цены от карата на полной выборке.
Как можно заметить, чем больше камень, тем дороже он стоит.
На втором графике с помощью атрибута facet_col
Создадим отдельные графики для каждой цветности, в итоге получается так
Собственно, наполнение контентом готово, и теперь можно вернуться к описанию функции обратного вызова для элемента dropdown. Как я уже говорил, описываем ее после функции навигации по контенту.
и запускаем
Полный код выложен здесь https://github.com/LionYK/dash_brd
Теперь я расскажу о трех опциональных, но довольно полезных инструментах фреймворка dash, которые сделают ваш dashbord показательным и интерактивным.
Первый инструмент, которому я хочу уделить внимание это – sunburst диаграмма.Диаграмма в виде солнечных лучей, показывает зависимости дочерних элементов от основных и прекрасно подходит для детального рассмотрения каждого уровня иерархии. В Дэш также является довольно интерактивной и кастомизируемой.
Начнем с кода. Диаграмма такого вида довольно удобно интегрируется в уже созданный в предыдущей части код.
Создаем новую страницу в Навигаторе:
dbc.Nav(
[
dbc.NavLink("Analysis", href="/page1", active="exact"),
dbc.NavLink("The effect", href="/page2", active="exact"),
dbc.NavLink("Sunburst", href="/page3", active="exact"),
dbc.NavLink("3D SCATTER", href="/page4", active="exact"),
dbc.NavLink("Thanks", href="/page5", active="exact"),
],
vertical=True,pills=True),
И прописываем ее параметры в функции отображения контента.
dcc.Graph(id='graph3',
figure =px.sunburst(
data_frame=df1,
path=["cut", "color", "clarity"],
color="cut",
color_discrete_sequence=px.colors.qualitative.Pastel,
maxdepth=-1,
width=1200,
height=1200)
,className="card")
]
И в итоге у нас выходит, представленная ниже диаграмма, в которой на верхнем уровне идет качество, на втором цвет и третьем- частота.
Да, данных довольно много, и на общем фоне они превращаются в кашу, но каждый элемент в зависимости от его иерархии, можно рассмотреть более внимательно, кликнув на него.
Здесь выделяем основную ветвь.
Здесь побочную.
Настало время рассказать о коде, который создает эту диаграмму. Более подробное описание вместе с дополнительными атрибутами можно найти в документации библиотеки.
path=["cut", "color", "clarity"]
В атрибуте path обозначаем Иерархию между источником и лучам, то есть, как видно на скриншотах сначала качество(cut
), затем цвет(color
), потом чистота(clarity
)
В атрибуте color обозначаем, на каком уровне иерархии будет меняться цвет.
Атрибут maxdepth=-1
отвечает за количество отображаемых уровней. -1 означает, что отображаются все доступные уровни.
color_discrete_sequence=px.colors.qualitative.Pastel
отвечает за цветовую схему.
Следующий инструмент в нашем списке это scatter_3d
.В предыдущей части поста я использовал 2д вариант этого инструмента. Здесь можно увидеть зависимость ширины высоты и стоимости камня. При этом можно выставить определенное значение карат и необходимое качество обработки.
Это делается не так уж и сложно.
dcc.Graph(id='graph3',
figure=px.scatter_3d(df1, x='depth', y='table', z='price',
color='cut',animation_frame='carat',height=800))
Выставляем необходимые значения на оси, обозначаем необходимый столбец для смены цвета, и в animation_frame
вписываем величину, в рамках которой должен измениться график.
Обычно в данном случае атрибут animation_frame
больше подходит для временной шкалы, но за неимением таких данных и для наглядности я решил поставить вес камня.
Все выходит довольно интерактивно. В шкале справа можно выбрать необходимый уровень обработки, в шкале снизу можно установить необходимый вес камня. Да и сам график можно вращать в разные стороны.
Также можно запустить график в формате анимации, чтобы увидеть, как изменяются указанные на графике значения в динамике.
И последней вещью, о которой я хотел бы рассказать, это возможность вставлять lottie изображения.
Lottie является библиотекой для Веб разработки, отображающая After Effects анимацию в режиме реального времени. Разгружая приложения или сайты (так как анимации в gif или mp4 формате либо довольно тяжёлые, либо трудны для форматирования) и давая возможность использовать анимированное изображение также легко, как и обычные статические картинки. Спасибо инженерам airbnb за это изобретение.
Библиотека задействует анимации в виде json файлов путем кодировки через Bodymovin. (плагин after effects)
Но для того чтобы использовать данную возможность, нужно добавить в список наших библиотек еще один модуль.
import dash_extensions as de
После долгих раздумий и мук выбора, со всеми вытекающими из этого биологическими процессами, мой выбор пал на вот этот милый алмазик.
Давайте инкрустируем его в dashборд.
Для этого возьмем в профиле картинки Lottie Animation URL и создадим для него переменную url.
url = 'https://assets4.lottiefiles.com/packages/lf20_w4cfkn2i.json'
Далее нам необходима переменная с настройками отображения изображения.
options = dict(loop=True,autoplay=True,renderSettings=dict(perserveAspectRatio='xMidYMid slice'))
Loop – отвечает за цикличное воспроизведение, Autoplay – за автоматический запуск, а RendererSettings отвечает за настройку изображения, perserveAspectRatio='xMidYMid slice'
отвечает за то, как должен объект с заданным соотношением сторон вписываться в область с другим соотношением сторон, подробнее об этом вот здесь.
Создаем переменную options и url, остается вписать изображение в контент страницы:
elif pathname == "/page5":
return [
html.Div(
children=[
html.H1(children='Thank you for your attention! :)', className='header-title'),
], className='header'),
html.Div(de.Lottie(options=options,width="25%",height="25%",url=url))
Мне показалось, это изображение довольно неплохо впишется в общую канву)
В данном фреймворке присутствует еще много интересных инструментов, которые подходят под те или иные задачи.
Надеюсь, инструменты, о которых я рассказал помогут вам сделать ваш Дэшборд разнообразным, доступным и приятным для восприятия.
изучаю bokeh. есть ли у вас опыт с bokeh? чем и как лучше/хуже bokeh по сравнению с dash?