Сегодня дашборды используются повсеместно: от быстрых отчетов "на лету" до демонстрации возможностей AI.

Существует множество замечательных продуктов для создания дашбордов: PowerBI, Tableu, Apache SuperSet и много других со своими плюсами и минусами. Однако все они являются отдельными приложениями в которые нужно дополнительно загружать данные, что часто бывает неудобно, если нужно быстро проанализировать датасет. И, если мы говорим про аналитиков, как правило все используют Python для анализа и ad-hoc исследований.

Для Python существует open source фреймворк построения дашбордов: PLotly Dash. Но те, кто его использовал, знают, насколько сложным может быть создание симпатичного интерфейса, и как скучно писать функции обратного вызова для каждого графика и фильтра. С этим еще можно мириться, если вы делаете дашборд для постоянного длительного использования, но с ad-hoc аналитикой этот вариант не годится.

Осознав проблему, я решил написать обертку над Dash, позволяющую создавать полноценные дашборды в 10-15 строк и запускать их прямо в jupiter-notebook и при этом отказаться от необходимости писать callback-функции. Так родился DashExpress. Документация

Концепция

Библиотека DashExpress в основном предназначена для ускорения разработки приложений Dash, но так же содержит ряд оптимизаций для более быстрой работы.

DashExpress опирается на 4 крупных проекта:

  1. Plotly Dash - для web-части

  2. Pandas - для хранения данных

  3. Dash Mantine - для симпатичного интерфейса

  4. Dash Leaflet - для построения карт

Установка

Библиотека устанавливается стандартно, через pip:

pip install dash-express

Это также установит компоненты Plotly Dash, Pandas, Dash Mantine Components и Dash Leaflet.

Создание DashExpress-приложения

Вот код минимального приложения с одним графиком и тремя фильтрами:

import pandas as pd
import plotly.graph_objects as go
import dash_mantine_components as dmc

from dash_express import DashExpress, Page


# Получаем данные
get_df = lambda: pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv')

# Инициализируем приложение
app = DashExpress(logo='DashExpress')

# Создаем страницу дашборда
page = Page(
    app=app,                    # DashExpress app
    url_path='/',               # Путь страницы
    name='Overview',            # Название страницы
    get_df=get_df,              # Функция получения дашборда
    )

# Функция построения графика
def bar_func(df):
    pv = pd.pivot_table(df, index='continent', values='lifeExp').reset_index()
    fig = go.Figure([go.Bar(x=pv['continent'], y=pv['lifeExp'])])
    return fig

# Размечаем макет
page.layout = dmc.SimpleGrid(
    page.add_graph(h='calc(100vh - 138px)',render_func=bar_func)
    )

# По каким колонкам фильтруем
page.add_autofilter('continent', multi=True)
page.add_autofilter('country', multi=True)
page.add_autofilter('lifeExp', multi=True)

app.run()

Запущенное приложение будет выглядеть так:

Минимальное приложение
Минимальное приложение

Приложение так же можно запустить прямо в jupiter-notebook, только перед этим нужно установить дополнительный пакет:

pip install jupyter-dash
Jupiter
Jupiter

Теперь расскажу подробнее о некоторых шагах и начну с получения данных.

Получение таблицы с данными

В параметр get_df нужно передать функцию получения таблицы. По умолчанию результат функции DashExpress кэширует на 1 час.
Если вы исследуете данные в jupiter notebook и у вас уже есть таблица, которую не нужно обновлять, просто передайте ее через функцию:

get_df = lambda: your_df

Если у вас большой датасет используйте оптимизации на стороне pandas:

def get_df():
    # Загружаем только нужные колонки
    df =  pd.read_csv('titanic.csv', usecols=['survived', 'age', 'class', 'who', 'alone'])

    # Преобразуем к оптимальным форматам
    df.age = df.age.fillna(0)
    df = df.astype(
        {
            'survived': 'int8',
            'age': 'int8',
            'class': 'category',
            'who': 'category',
            'alone': 'bool'
        }
    )  
    return df

Разметка страницы

Разметьте страницу с помощью grid сетки, и вставьте графики, карты и KPI c помощью методов .add_graph, .add_map и .add_kpi. Рекомендую использовать компоненты dash mantine. (dmc.Grid & dmc.SimpleGrid)

page.layout = dmc.SimpleGrid(
    ...
    )

KPI cards

KPI карточки являются основной частью мониторинга эффективности бизнеса и отслеживания актуальной информации. Любая карта состоит из постоянной части (контейнера) и переменной части (показателя KPI).

Система рендеринга KPI основана на использовании класса KPI, который содержит контейнерное представление и логику расчета показателя. Простейшая реализация KPI с автоматической генерацией функции расчета представлена в классе FastKPI:

app.add_kpi(FastKPI('survived', agg_func=np.mean)

Графики Plotly

Библиотека построения графиков Plotly содержит более 50 типов диаграмм на выбор. Чтобы встроить их в приложение Dash Express, вам нужно ответить на 2 вопроса:

  1. Где находится график

  2. Как построить график

Ответ на первый вопрос закладывается при разработке макета, путем вызова метода page.add_graph(...) в расположении графика, простой пример:

dmc.SimpleGrid(
    [
        page.add_graph(render_func=bar_chart),
        page.add_graph(render_func=line_chart),
    ],
    cols=2
    )

На второй вопрос отвечает параметр render_func, представляющий собой функцию принимающую DataFrame, и возвращающую график plotly.

Карты Leaflet

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

dmc.SimpleGrid(
    [
        page.add_map(geojson_func=None),
    ],
    cols=1
    )

geojson_func - должен возвращать geojson для построения карты. Если вам не нужны какие-либо дополнительные преобразования, не указывайте этот параметр, DashExpress все сделает за вас.

def geojson_func(gdf):
    gdf = gdf[gdf.geometry.geom_type == 'Polygon']
    return gdf.__geo_interface__

Фильтрация

Последним действием является добавление фильтров, которое выполняется простым вызовом метода page.add_filter и указанием столбца фильтрации.

page.add_autofilter('continent', multi=True)
page.add_autofilter('country', multi=True)
page.add_autofilter('lifeExp', multi=True)

Заключение

DashExpress не только быстрое решение, но и гибкое: Вы можете полностью изменить внешний вид интерфейса переопределив класс BaseAppShell. Подробнее об этом расскажу в одной из следующих статей.

Помимо этого Dash Express заботится о повышении производительности вместо вас, вот способы, встроенные по умолчанию:

  1. Использование обратных вызовов на стороне клиента - Большинство обратных вызовов реализовано на стороне клиента, а не на сервере в Python.

  2. Частичные обновления свойств - Функции создания графиков автоматически преобразуются в Patch объекты, обновляя только те части свойства, которые вы хотите изменить.

  3. Кэширование - Dash Express использует библиотеку Flask-Caching, которая сохраняет результаты в базе данных с общей памятью, такой как Redis, или в виде файла в вашей файловой системе.

  4. Сериализация данных с помощью orson - Dash Express использует or json для ускорения сериализации в JSON и, в свою очередь, повышения производительности обратного вызова

Спасибо за прочтение!

Комментарии (13)


  1. Abobcum
    02.08.2023 17:12
    +1

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

    1. Упростить конструктор до пайплайнов.

    2. Добавить много разных примеров в документацию.


    1. stpnvkirill Автор
      02.08.2023 17:12

      Спасибо за предложения! Над конструктором подумаю, а документация в работе. Сейчас в плане еще примеры с полной кастомизацией интерфейса и разработкой собственных KPI карт.


  1. sixxio
    02.08.2023 17:12

    Вспоминая опыт работы с Dash, выглядит супер!
    Но возник вопрос, что думаете насчет Streamlit?


    1. stpnvkirill Автор
      02.08.2023 17:12

      Лично я streamlit не понял, какой-то он сильно деревянный. Dash имеет большое community и возможность делать пользовательские компоненты на основе react-библиотек. DashExpress, например, использует dash mantine и dash leafle. Эти компоненты разработало сообщество, а не plotly.


  1. pavich
    02.08.2023 17:12

    name='Owerview', # Название старницы


    1. stpnvkirill Автор
      02.08.2023 17:12

      Спасибо, поправил.


  1. VPryadchenko
    02.08.2023 17:12

    Супер! Скажите, а если нужно что-то посчитать налёту, можно это отобразить? Например, гистограмму распределения по какому-то срезу. Или только для статичных датафреймов годится?


    1. stpnvkirill Автор
      02.08.2023 17:12

      При применении фильтрации DashExpress вызывает функции построения графиков (которые были переданы через .add_graph) и просто передает им отфильтрованный dataframe. Поэтому, если гистограмму, которую вы хотите отобразить, впринципе можно построить по исходным данным, то и при фильтрации всё будет работать. Уточните, что вы подразумеваете под статичным датафреймом?


      1. VPryadchenko
        02.08.2023 17:12

        Константный, наверное, было бы правильнее сказать - загруженный единожды.

        Поэтому, если гистограмму, которую вы хотите отобразить, впринципе можно построить по исходным данным, то и при фильтрации всё будет работать.

        Понял. Не уверен, что сходу соображу, как, но, уверен, что можно.
        А если, допустим, мне нужно при определенных действиях пользователя (скажем, выборе строки таблицы), подтягивать какие-то доп. данные и уже по ним что-то строить (в dash как есть это можно, но рутинно) - Ваш фреймворк позволит?


        1. stpnvkirill Автор
          02.08.2023 17:12

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


          1. VPryadchenko
            02.08.2023 17:12

            Это как раз то, чего хотелось избежать :))
            Спасибо за ответ!


  1. A-V-tor
    02.08.2023 17:12

    @stpnvkirill Подскажи а с Flask дружит также как и Dash?

    from flask import Flask
    from dash import Dash

    server = Flask(name)

    external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
    app = Dash(
    name,
    server=server,
    url_base_pathname='/admin/analytics/',
    external_stylesheets=external_stylesheets,
    )


    1. stpnvkirill Автор
      02.08.2023 17:12

      Да, конечно! В DashExpress можно передать все те же параметры, что и в Dash.

      Если вы хотите встроить DashExpress в приложение Flask, рекомендую, помимо сервера Flask, передать еще экземпляр flask_caching.Cache, чтобы dashexpres не создавал отдельный экземпляр, а использовал глобальный кэш для всего приложения.