Позднее Ctrl + ↑

Дашборды умерли

Время чтения текста – 12 минут

Перевод статьи «Dashboards are Dead»

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

Hello Dashboard, my old friend

На старте карьеры я работала в крупной технологической компании. Компания только-только приобрела свой первый инструмент для создания дашбордов, и наша команда отвечала за захватывающий переход от устаревших spreadsheets и отчетов SSRS к новым ярким дашбордам.

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

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

  1. Как? У вас ещё нет дашборда?! Неожиданно повсюду появились дашборды. Инженеру нужны данные для специального анализа? Вот дашборд. У вице-президента будет презентация на следующей неделе и ему нужны диаграммы? Она получает дашборд. А что происходит дальше? О нём просто забывают. Такой шаблонный подход истощал время, ресурсы и мотивацию нашей команды. Это уникальное деморализующее чувство — наблюдать, как ещё один из ваших дашбордов забросили быстрее, чем профиль MySpace в 2008 году.
  2. Смерть от 1000 фильтров. После того, как новый дашборд заработал, нас сразу же заваливали запросами на новые представления, фильтры, поля, страницы (напомните мне рассказать вам о том, как я увидела 67-страничный дашборд). Было ясно: дашборды не отвечали на все вопросы, что было либо неудачей на этапе разработки, либо неспособностью инструментов дать ответы, в которых нуждались люди. Что ещё хуже, мы выяснили, что люди использовали все эти фильтры, чтобы экспортировать данные в Excel и уже там работать с ними 🤦‍♀️
  3. Не мой дашборд. Постепенно шумиха вокруг дашбордов начала сходить на нет, люди начали пренебрегать ими и откровенно игнорировать их. Многие видели в них угрозу для своей работы, и если они встречали неожиданные цифры, то списывали всё на «плохие данные». У нас на работе были серьёзные проблемы с доверием между людьми, и дашборды только усугубляли положение. В конце концов, мы ведь не могли отправлять другим наши SQL-запросы для получения данных: люди бы просто не смогли не только прочитать их, но даже понять ту сложную схему, по которой они работают. И тем более мы не могли отправлять другим командам необработанные данные. Итак, у нас была просто огромная, наболевшая, серьезная проблема с доверием.

Реальный пример: что это за странная красная точка на карте?

Для примера давайте рассмотрим дашборд, который стал широко популярен во время пандемии — панель мониторинга коронавируса университета Джона Хопкинса.

Дашборд привлекателен визуально. Красный и чёрный вызывают чувство строгости и важности с первого взгляда. По мере того, как взгляд останавливается на странице, мы сталкиваемся с числами, точками разного размера и графиками, которые почти всегда направлены вправо-вверх. У нас осталось ощущение, что всё плохо, и, кажется, становится ещё хуже. Этот дашборд был создан с целью получения данных доступным и интересным способом. Возможно, он даже был разработан, чтобы ответить на несколько ключевых вопросов: «Сколько новых случаев было сегодня в моей стране? А в моём регионе?». Безусловно, это намного лучше, чем если бы они просто разместили таблицу или ссылку для скачивания.

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

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

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

Данные в портретном режиме

Блокноты с данными, такие как Jupyter, стали очень популярными за последние несколько лет в области Data Science. Их технологическая направленность оказалась лучше традиционных скриптовых инструментов для Data Analysis и Data Science. Это не только полезно для аналитика, выполняющего работу, но также помогает начальнику, коллеге, другу, который вынужден этим пользоваться.

По сути, блокноты обеспечивают:

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

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

Нам просто нужен способ вынести эту идею за пределы Data Science и сделать блокнот таким же доступным, как и дашборды.

Блокноты в массы

В Count мы настолько верим в преимущества блокнотов, что создали платформу для анализа данных на их основе. Народ, больше никаких дашбордов!

Чтобы использовать их за пределами Data Science, нам пришлось создать собственную версию, но фундаментальные принципы всё ещё применимы с некоторыми дополнительными преимуществами...

Создан для любого уровня опыта

  1. Нет необходимости учить всех в вашей команде Python или SQL, поскольку запросы можно создавать по принципу drag-and-drop, используя «составной запрос» SQL или написания запроса с нуля.
  2. Стройте графики и диаграммы одним щелчком мыши, без сложных пакетов визуализации или программного обеспечения
  3. Автоматическое объединение таблиц и результатов запроса, нет необходимости писать сложные объединения или пытаться объяснить схему

Collaboration-enabled

  1. Делитесь блокнотами с товарищем по команде, всей командой или тем, у кого есть ссылка
  2. Добавляйте комментарии и выноски, чтобы сделать документ действительно общим

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

  1. Аналитики используют блокноты вместо SQL-скриптов для создания нескольких базовых таблиц, которые используют другие команды. Эти блокноты доступны для просмотра всем, что решает проблему доверия в команде
  2. Команда по работе с данными создаёт несколько базовых отчётов. Эти отчёты полны комментариев, которые помогут читателю лучше понять, как интерпретировать числа и какие соображения следует принять
  3. Затем пользователи делают fork этих дата-блокнотов или создают свои собственные. Они делятся этими блокнотами с Data Team, чтобы они могли помочь им, а затем и с другими подразделениями компании

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

Пишем клиент для нового API nalog.ru

Время чтения текста – 6 минут

Ранее в блоге мы рассказывали, как благодаря открытому API можно собирать данные от ФНС по нашим чекам из магазинов, обращаясь к приложению налоговой. С прошлой недели способ стал нерабочим: ФНС обновили методы приложения. Сегодня напишем свой клиент для nalog.ru, который проходит авторизацию и отправляет чеки на проверку.

Авторизация

Прежде чем начать пользоваться приложением, наш профиль необходимо авторизовать. В отличии от предыдущей версии, текущая требует прохождение капчи для авторизации по мобильному телефону — такой способ нам не подходит. Проще всего будет входить в профиль по ИНН и паролю от личного кабинета налогоплательщика. Для хранения этих данных создадим файл credentials.env. Переменную CLIENT_SECRET зададим согласно коду: она отвечает за авторизацию приложения. А ИНН и пароль подставляем свои.

INN = 
PASSWORD = 
CLIENT_SECRET=IyvrAbKt9h/8p6a7QPh8gpkXYQ4=

Теперь создадим файл nalog_python.py, в котором будет описан клиент. Библиотека dotenv используется для загрузки нашего логина и пароля из .env файла.

import os
import json
import requests
from dotenv import load_dotenv

Опишем класс NalogRuPython, и начнём с глобальных переменных класса. Здесь перечисляем headers, необходимые для отправки запроса.

class NalogRuPython:
    HOST = 'irkkt-mobile.nalog.ru:8888'
    DEVICE_OS = 'iOS'
    CLIENT_VERSION = '2.9.0'
    DEVICE_ID = '7C82010F-16CC-446B-8F66-FC4080C66521'
    ACCEPT = '*/*'
    USER_AGENT = 'billchecker/2.9.0 (iPhone; iOS 13.6; Scale/2.00)'
    ACCEPT_LANGUAGE = 'ru-RU;q=1, en-US;q=0.9'

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

def __init__(self):
        load_dotenv()
        self.__session_id = None
        self.set_session_id()

Метод set_session_id проводит авторизацию пользователя по ИНН и паролю от личного кабинета налогоплательщика и ничего не возвращает, только задаёт значение переменной __session_id. Отправляем по указанному в глобальных переменных HOST запрос с нашими данными от аккаунта и получаем идентификатор сессии в ответе.

def set_session_id(self) -> None:
        if os.getenv('CLIENT_SECRET') is None:
            raise ValueError('OS environments not content "CLIENT_SECRET"')
        if os.getenv('INN') is None:
            raise ValueError('OS environments not content "INN"')
        if os.getenv('PASSWORD') is None:
            raise ValueError('OS environments not content "PASSWORD"')

        url = f'https://{self.HOST}/v2/mobile/users/lkfl/auth'
        payload = {
            'inn': os.getenv('INN'),
            'client_secret': os.getenv('CLIENT_SECRET'),
            'password': os.getenv('PASSWORD')
        }
        headers = {
            'Host': self.HOST,
            'Accept': self.ACCEPT,
            'Device-OS': self.DEVICE_OS,
            'Device-Id': self.DEVICE_ID,
            'clientVersion': self.CLIENT_VERSION,
            'Accept-Language': self.ACCEPT_LANGUAGE,
            'User-Agent': self.USER_AGENT,
        }

        resp = requests.post(url, json=payload, headers=headers)
        self.__session_id = resp.json()['sessionId']

Получение идентификатора чека

Следующий шаг — получение ticket_id. Прежде чем отправить сам чек, необходимо получить его идентификатор для проверки. Опишем метод _get_ticket_id, который принимает строку с расшифрованным QR-кодом чека, отправляет соответствующий запрос на сервер и возвращает идентификатор для этой строки. В headers помимо указания глобальных переменных появился также __session_id, который требует метод /v2/ticket.

def _get_ticket_id(self, qr: str) -> str:
        url = f'https://{self.HOST}/v2/ticket'
        payload = {'qr': qr}
        headers = {
            'Host': self.HOST,
            'Accept': self.ACCEPT,
            'Device-OS': self.DEVICE_OS,
            'Device-Id': self.DEVICE_ID,
            'clientVersion': self.CLIENT_VERSION,
            'Accept-Language': self.ACCEPT_LANGUAGE,
            'sessionId': self.__session_id,
            'User-Agent': self.USER_AGENT,
        }
        resp = requests.post(url, json=payload, headers=headers)
        return resp.json()["id"]

Расшифровка чека

Последний шаг — проверка чека. Формируем по ticket_id запрос к серверу и получаем подробную расшифровку чека в ответе. На этом клиент полностью описан и готов к работе.

def get_ticket(self, qr: str) -> dict:
        ticket_id = self._get_ticket_id(qr)
        url = f'https://{self.HOST}/v2/tickets/{ticket_id}'
        headers = {
            'Host': self.HOST,
            'sessionId': self.__session_id,
            'Device-OS': self.DEVICE_OS,
            'clientVersion': self.CLIENT_VERSION,
            'Device-Id': self.DEVICE_ID,
            'Accept': self.ACCEPT,
            'User-Agent': self.USER_AGENT,
            'Accept-Language': self.ACCEPT_LANGUAGE,
        }
        resp = requests.get(url, headers=headers)
        return resp.json()

Наконец, для удобства опишем пример работы клиента. Создадим экземпляр класса NalogRuPython, зададим для примера строку QR-кода и получим расшифрованный ticket, который затем напечатаем на экране.

if __name__ == '__main__':
    client = NalogRuPython()
    qr_code = "t=20200727T1117&s=4850.00&fn=9287440300634471&i=13571&fp=3730902192&n=1"
    ticket = client.get_ticket(qr_code)
    print(json.dumps(ticket, indent=4))

Клиент можно использовать и в своих скриптах: для этого нужно импортировать класс, создать экземпляр и, как и в примере, вызвать метод get_ticket.

from nalog_python import NalogRuPython

client = NalogRuPython()
qr_code = "t=20200727T1117&s=4850.00&fn=9287440300634471&i=13571&fp=3730902192&n=1"
ticket = client.get_ticket(qr_code)

Полный код проекта на GitHub

Создаём дашборд на Bootstrap с нуля (Часть 1)

Время чтения текста – 15 минут

В прошлых материалах мы познакомились с фреймворком Plotly Dash, научились строить scatter plots и визуализировать данные на карте. Сегодня мы попробуем объединить имеющиеся части в одном веб-приложении и расскажем как можно создать полноценный дашборд используя сетчатую структуру Bootstrap.
В этом нам поможет dash-bootstrap-components, эта библиотека позволяет строить дашборды по принципу “plug-and-play”, добавлять любые компоненты Bootstrap и стилизовать их используя грид-дизайн.

Подготовка макета

Перед созданием любого приложения, не только дашборда нам просто необходим план, черновой макет, следуя которому, мы бы видели общую картину и могли быстро внести корректировки в структуру. Для создания макета дашборда мы использовали приложение draw.io, этот инструмент позволяет быстро создавать диаграммы, графики, блок-схемы, формы и т. д. Наш дашборд будет построен по следующему макету:

Как и сам дашборд, шапка будет оформлена в главных цветах Untappd — белом и золотом. Ниже расположится раздел с пивоварнями, состоящий из scatter plot и панели с настройками. А в самом низу будет карта, показывающая средний рейтинг напитка по регионам России.

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

- application.py
- assets/
    |-- typography.css
    |-- header.css
    |-- custom-script.js
    |-- image.png

Импортируем нужные библиотеки и инициализируем приложение:

import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
import pandas as pd
from get_ratio_scatter_plot import get_plot
from get_russian_map import get_map
from clickhouse_driver import Client
from dash.dependencies import Input, Output

standard_BS = dbc.themes.BOOTSTRAP
app = dash.Dash(__name__, external_stylesheets=[standard_BS])

Главные аргументы для app:
__name__ — для активации cтатических элементов из папки assets (картинки, CSS и JS файлы)
external_stylesheets — внешний CSS для стилизации дашборда, здесь мы используем стандартный BootstrapCDN, однако вы можете создать свой или воспользоваться одним из уже готовых стилей.

Включаем ещё несколько параметров для работы с локальными файлами и подключаемся к ClickHouse:

app.scripts.config.serve_locally = True
app.css.config.serve_locally = True

client = Client(host='ec2-3-16-148-63.us-east-2.compute.amazonaws.com',
                user='default',
                password='',
                port='9000',
                database='default')

Добавим палитру цветов:

colors = ['#ffcc00', 
          '#f5f2e8', 
          '#f8f3e3',
          '#ffffff', 
          ]

Верстка макета

Все элементы дашборда будут помещены в Bootstrap Контейнер, который находится в блоке <div>:

- app 
    |-- div
     |-- container
      |-- logo&header
     |-- container
      |-- div
       |-- controls&scatter
       |-- map
app.layout = html.Div(
                    [
                        dbc.Container(

                                         < шапка >
                         
                        dbc.Container(       
                            html.Div(
                                [
                        
                                    < основной код >
                        
                                ],
                            ),
                            fluid=False, style={'max-width': '1300px'},
                        ),
                    ],
                    style={'background-color': colors[1], 'font-family': 'Proxima Nova Bold'},
                )

Здесь мы сразу задаем фиксированную ширину контейнера, цвет фона и стиль шрифта на странице, который берется из typography.css в папке assets. Стоит подробней остановится на первом элементе блока div, это и есть заголовок страницы, он включает в себя логотип Untappd:

logo = html.Img(src=app.get_asset_url('logo.png'),
                        style={'width': "128px", 'height': "128px",
                        }, className='inline-image')

и главный заголовок:

header = html.H3("Статистика российских пивоварен в Untappd", style={'text-transform': "uppercase"})

Чтобы расположить эти два элемента на одной строке мы воспользовались Bootstrap Формами:

logo_and_header = dbc.FormGroup(
        [
            logo,
            html.Div(
                [
                    header
                ],
                className="p-5"
            )
        ],
        className='form-row',
)

В блоке html.Div параметр ’p-5’ позволяет добавить отступы и вертикально выровнять заголовок, а ’form-row’ поставить logo и header в один ряд. На данном этапе шапка дашборда вылгядит следующим образом:

Нам осталось выровнять их по центру и добавить красок. Для этого создаем отдельный контейнер, который будет состоять из одного ряда. В параметре className указываем ’d-flex justify-content-center’, чтобы выровнять элементы контейнера по центру.

dbc.Container(
                    dbc.Row(
                        [
                            dbc.Col(
                                html.Div(
                                    logo_and_header,
                                ),
                            ),
                        ],
                        style={'max-height': '128px',
                               'color': 'white',
                       }

                    ),
                    className='d-flex justify-content-center',
                    style={'max-width': '100%',
                           'background-color': colors[0]},
                ),

На данном этапе шапка дашборда готова:

Далее в следующий Bootstrap Контейнер добавим первый подзаголовок:

dbc.Container(
                    html.Div(
                        [
                            html.Br(),
                            html.H5("Пивоварни", style={'text-align':'center', 'text-transform': 'uppercase'}),
                            html.Hr(), # разделительная линия

Сам дашборд будет состоять из Bootstrap Карт, это обеспечит структурированное расположение всех элементов, придаст каждому элементу четкие границы и сохранит white space. Давайте создадим один из элементов дашборда, панель с настройками:

slider_day_values = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
slider_top_breweries_values = [5, 25, 50, 75, 100, 125, 150, 175, 200]

controls = dbc.Card(
    [
       dbc.CardBody(
           [
               dbc.FormGroup(
                    [
                        dbc.Label("Временной период", style={'text-align': 'center', 'font-size': '100%', 'text-transform': 'uppercase'}),
                        dcc.Slider(
                            id='slider-day',
                            min=1,
                            max=100,
                            step=10,
                            value=100,
                            marks={i: i for i in slider_day_values}
                        ),
                    ], style={'text-align': 'center'}
               ),
               dbc.FormGroup(
                    [
                        dbc.Label("Количество пивоварен", style={'text-align': 'center', 'font-size': '100%', 'text-transform': 'uppercase'}),
                        dcc.Slider(
                            id='slider-top-breweries',
                            min=5,
                            max=200,
                            step=5,
                            value=200,
                            marks={i: i for i in slider_top_breweries_values}
                        ),
                    ], style={'text-align': 'center'}
               ),
           ],
       )
    ],
    style={'height': '32.7rem', 'background-color': colors[3]}
)

Панель включает два слайдера для управления scatter plot, они располагаются друг под другом в Bootstrap формах. Мы добавили слайдеры в один блок — dbc.CardBody. остальные эдементы будут добавлены по такому же принципу, это позволяет поставить одинаковые отступы со всех сторон . По умолчанию слайдеры оформлены в голубом цвете, для того чтобы изменить их стиль, воспользуйтесь файлом sliders.css, находящемся в папке assets.
Добавляем панель управления и scatter plot следующим блоком:

dbc.Row(
                [
                    dbc.Col(controls, width={"size": 4,
                                     "order": 'first',
                                             "offset": 0},
                     ),
                     dbc.Col(dbc.Card(
                                [
                                    dbc.CardBody(
                                        [
                                            html.H6("Отношение количества отзывов к средней оценке пивоварни",
                                                    className="card-title",
                                                    style={'text-transform': 'uppercase'}), 
                                            dcc.Graph(id='ratio-scatter-plot'),
                                        ],
                                    ),
                                ],
                                style={'background-color': colors[2], 'text-align':'center'}
                             ),
                     md=8),
                ],
                align="start",
                justify='center',
            ),
html.Br(),

И в конце страницы расположим карту:

html.H5("Заведения и регионы", style={'text-align':'center', 'text-transform': 'uppercase',}),
                            html.Hr(), #разделительная линия
                            dbc.Row(
                                [
                                    dbc.Col(
                                        dbc.Card(
                                            [
                                                dbc.CardBody(
                                                    [
                                                        html.H6("Средний рейтинг пива по регионам",
                                                                className="card-title",
                                                                style={'text-transform': 'uppercase'},
                                                        ),  
                                                        dcc.Graph(figure=get_map())
                                                    ],
                                                ),
                                            ],
                                        style={'background-color': colors[2], 'text-align': 'center'}
                                        ),
                                md=12),
                                ]
                            ),
                            html.Br(),

Callback-функции в Dash

Callback-функции позволяют сделать элементы дашборда интерактивными, если меняется входной элемент (Input), то и выход (Output) тоже изменится.

@app.callback(
    Output('ratio-scatter-plot', 'figure'),
    [Input('slider-day', 'value'),
     Input('slider-top-breweries', 'value'),
     ]
)
def get_scatter_plots(n_days=100, top_n=200):
    if n_days == 100 and top_n == 200:
        df = pd.read_csv('data/ratio_scatter_plot.csv')
        return get_plot(n_days, top_n, df)
    else:
        return get_plot(n_days, top_n)

Входные/ выходные (Input/Output) значения это, проще говоря, параметр value элемента с определенным id. Например, входное значение верхнего слайдера с id=’slider-day’, показывающего временной период по умолчанию 100. При изменении этого значения функция, обернутая в декоратор будет вызвана автоматически и output на графике обновится. Больше примеров представлено сайте plotly.
Важный момент, чтобы scatter plot при загрузке страницы отображал данные нам нужно сперва считать их из сохраненного датафрейма в папке data (указать начальное состояние), иначе scatter plot будет пустым. В дальнейшем при изменении параметров слайдера данные будут загружаться из ClickHouse таблиц.

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

application = app.server

if __name__ == '__main__':
    application.run(debug=True, port=8000)

Теперь осталось только загрузить его на AWS с помощью BeansTalk и наш дашборд на Bootstrap готов:

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

Полный код проекта на GitHub

Визуализация данных на российской карте библиотекой Plotly

Время чтения текста – 11 минут

Часто для визуализации данных подходит карта: например, когда нужно показать, как статистика ведёт себя в определённых городах, областях, регионах. В таких случаях каждый регион или другая административная единица кодируется: ее границы преобразуют в полигоны и мультиполигоны с координатами широты и долготы относительно карты мира. Для Америки и Европы не составит труда найти встроенное в библиотеку Plotly решение, но в случае с картой России такого реализованного решения сходу найти не удалось. Сегодня мы разметим готовый geojson файл с административными границами регионов России, напишем парсер последних данных по коронавирусу и визуализируем статистику на карте при помощи библиотеки Plotly.

from urllib.request import urlopen
import json
import requests
import pandas as pd
from selenium import webdriver
from bs4 import BeautifulSoup as bs
import plotly.graph_objects as go

Правим geojson

Скачаем открытый geojson с границами российских регионов, найденный по одной из первых ссылок в Google по запросу «russia geojson». В нём уже есть кое-какие данные: например, наименования регионов. Но этот geojson-файл пока ещё не подходит под требуемый формат Plotly: в нём не размечены идентификаторы регионов.

with urlopen('https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/russia.geojson') as response:
    counties = json.load(response)

Кроме разметки идентификаторов есть различия в наименовании регионов. Например, на сайте стопкоронавирус.рф, откуда мы будем брать свежие данные о заболевших, республика Башкортостан занесена как «Республика Башкортостан», а в нашем geojson-файле — просто «Башкортостан». Все эти различия необходимо устранить во избежание конфликтов. Кроме того, все первые буквы в названиях регионов должны начинаться с верхнего регистра.

regions_republic_1 = ['Бурятия', 'Тыва', 'Адыгея', 'Татарстан', 'Марий Эл',
                      'Чувашия', 'Северная Осетия – Алания', 'Алтай',
                      'Дагестан', 'Ингушетия', 'Башкортостан']
regions_republic_2 = ['Удмуртская республика', 'Кабардино-Балкарская республика',
                      'Карачаево-Черкесская республика', 'Чеченская республика']
for k in range(len(counties['features'])):
    counties['features'][k]['id'] = k
    if counties['features'][k]['properties']['name'] in regions_republic_1:
        counties['features'][k]['properties']['name'] = 'Республика ' + counties['features'][k]['properties']['name']
    elif counties['features'][k]['properties']['name'] == 'Ханты-Мансийский автономный округ - Югра':
        counties['features'][k]['properties']['name'] = 'Ханты-Мансийский АО'
    elif counties['features'][k]['properties']['name'] in regions_republic_2:
        counties['features'][k]['properties']['name'] = counties['features'][k]['properties']['name'].title()

Из получившегося geojson-файла сформируем DataFrame с регионами России: возьмём идентификаторы и наименования.

region_id_list = []
regions_list = []
for k in range(len(counties['features'])):
    region_id_list.append(counties['features'][k]['id'])
    regions_list.append(counties['features'][k]['properties']['name'])
df_regions = pd.DataFrame()
df_regions['region_id'] = region_id_list
df_regions['region_name'] = regions_list

Если сделаем всё правильно, получим такой DataFrame:

Собираем данные

Будем парсить эту таблицу:

Воспользуемся библиотекой Selenium. Перейдём на сайт и получим всю страницу, а затем преобразуем её в Soup для парсинга.

driver = webdriver.Chrome()
driver.get('https://стопкоронавирус.рф/information/')
source_data = driver.page_source
soup = bs(source_data, 'lxml')

На сайте наименования регионов находятся под тегом <th>, а свежие данные по регионам под тегом <td>. Для начала получим данные.

divs_data = soup.find_all('td')

Данные в divs_data выглядят следующим образом:

Вся информация идёт в одну строчку: и новые случаи, и активные, и все прочие. Тем не менее, заметно, что каждому региону соответствует пять значений: для Москвы это первые пять, для Московской области — вторые пять и так далее. Воспользуемся этим: в каждый из пяти списков будем класть значения согласно индексу. Если это первое значение, то оно войдёт в список выявленных случаев, если второе — в список новых случаев и так далее. После пяти индекс будет обнуляться.

count = 1
for td in divs_data:
    if count == 1:
        sick_list.append(int(td.text))
    elif count == 2:
        new_list.append(int(td.text))
    elif count == 3:
        cases_list.append(int(td.text))
    elif count == 4:
        healed_list.append(int(td.text))
    elif count == 5:
        died_list.append(int(td.text))
        count = 0
    count += 1

Следующим шагом соберём названия регионов из таблицы — они лежат под классом col-region. Из названий нужно убрать лишние двойные пробелы и символы переноса строки.

divs_region_names = soup.find_all('th', {'class':'col-region'})
region_names_list = []
for i in range(1, len(divs_region_names)):
    region_name = divs_region_names[i].text
    region_name = region_name.replace('\n', '').replace('  ', '')
    region_names_list.append(region_name)

Соберём DataFrame:

df = pd.DataFrame()
df['region_name'] = region_names_list
df['sick'] = sick_list
df['new'] = new_list
df['cases'] = cases_list
df['healed'] = healed_list
df['died'] = died_list

И посмотрим на Челябинскую область под десятым индексом — в конце наименования остался пробел! Этот пробел в конце строки может причинить много бед, ведь тогда название не будет соответствовать названию региона в geojson-файле. Уберём его — благо, все остальные наименования на сайте в порядке.

df.loc[10, 'region_name'] = df[df.region_name == 'Челябинская область '].region_name.item().strip(' ')

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

df = df.merge(df_regions, on='region_name')

Визуализация данных на карте Plotly

Создадим новую фигуру — она будет являться объектом Choroplethmapbox. В параметр geojson передаём переменную counties с geojson-файлом, в параметр locations вставляем идентификаторы регионов. Параметр z — значения, которые мы хотим визуализировать. Для примера возьмём количество новых случаев в каждом регионе — они лежат в колонке new таблицы. В text передаём названия регионов. Другой параметр — colorscale — нужен для цветового сопровождения данных. Он принимает списки со значениями от 0 до 1, которые являются позициями цветов в градиенте. Чем меньше заболевших, тем зеленее будет регион. С увеличением числа заболевших цвет переходит от желтого к красному. Параметр hovertemplate — шаблон панели, появляющейся при наведении на регион. С тултипом связан ещё один аргумент — customdata. Он принимает объединенные вдоль оси объекты, которые затем можно использовать в hovertemplate для отображения новых данных.

fig = go.Figure(go.Choroplethmapbox(geojson=counties,
                           locations=df['region_id'],
                           z=df['new'],
                           text=df['region_name'],
                           colorscale=[[0, 'rgb(34, 150, 79)'],
                                       [0.2, 'rgb(249, 247, 174)'],
                                       [0.8, 'rgb(253, 172, 99)'],
                                       [1, 'rgb(212, 50, 44)']],
                           colorbar_thickness=20,
                           customdata=np.stack([df['cases'], df['died'], df['sick'], df['healed']], axis=-1),
                           hovertemplate='<b>%{text}</b>'+ '<br>' +
                                         'Новых случаев: %{z}' + '<br>' +
                                         'Активных: %{customdata[0]}' + '<br>' +
                                         'Умерло: %{customdata[1]}' + '<br>' +
                                         'Всего случаев: %{customdata[2]}' + '<br>' +
                                         'Выздоровело: %{customdata[3]}' +
                                         '<extra></extra>',
                           hoverinfo='text, z'))

Теперь зададим стиль карты — возьмём готовую carto-positron, нейтральный и минималистичный шаблон, который не отвлекает от основных данных. Аргумент mapbox_zoom отвечает за приближение карты, а mapbox_center принимает координаты начального центра карты. Зададим marker_line_width равный нулю, чтобы убрать границы между регионами. После зададим всем отступам в margin значение 0, чтобы карта была визуально шире. Сразу после выведем фигуру методом show().

fig.update_layout(mapbox_style="carto-positron",
                  mapbox_zoom=1, mapbox_center = {"lat": 66, "lon": 94})
fig.update_traces(marker_line_width=0)
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
fig.show()

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

Полный код проекта на GitHub

Деплой дашборда на AWS Elastic Beanstalk

Время чтения текста – 7 минут

Если под рукой имеется машина на Amazon Web Services и стоит задача развернуть веб-приложение, можно воспользоваться сервисом Elastic Beanstalk от AWS: он позволяет развертывать приложения под другими сервисами от Amazon, включая EC2.

Готовим приложение

В материале «Делаем дашборд с параметром на Python» мы создали проект с двумя файлами: application.py — скрипт с генерацией локального дашборда и get_plots.py — скрипт, возвращающий scatter plot с пивоварнями Untappd из материала «Строим scatter plot по пивоварням Untappd». Немного подкорректируем файл application.py: чтобы приложение запускалось на Elastic Beanstalk, app.server в конце файла присвоим переменной application. Должно получиться вот так:

application = app.server

if __name__ == '__main__':
   application.run(debug=True, port=8080)

Перед тем, как развернуть приложение, нужно собрать его в архив. В архиве должны присутствовать все необходимые файлы, включая requirements.txt — перечень зависимостей приложения. В нём перечислены пакеты и версии, необходимые для запуска приложения. Чтобы его создать, достаточно в директории с проектом и окружением ввести команду pip freeze и отправить вывод в файл:

pip freeze > requirements.txt

Теперь соберём архив. В unix для архивации и сжатия предусмотрена встроенная утилита zip.

zip deploy_v0 application.py get_plots.py requirements.txt

Создаём приложение и окружение

Переходим на Elastic Beanstalk в раздел «Applications». Жмём на «Create a new application».

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

Сразу после нам покажут список окружений для приложения: изначально он пустой, поэтому нажимаем на «Create a new environment».

Так как мы работаем с веб-приложением, выбираем окружение веб-сервера:

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

Следом выбираем платформу веб-приложения. Наше написано на Python.

Теперь загружаем само приложение: так как код мы уже написали, выбираем «Upload your code» и прикрепляем файл с архивом. После жмём «Create environment».

Следом откроется окно с логами создания окружения. Пару минут придётся подождать.

Если все сделали правильно, увидим экран с галочкой и подписью «OK»: это означает, что наше приложение успешно загружено и доступно. Если захотим загрузить новую версию, достаточно пересобрать архив с файлами и загрузить его по кнопке «Upload and deploy».

По ссылке, представленной выше можем пройти на сайт, где лежит дашборд. При помощи тега <iframe> этот дашборд можно также встроить на другой сайт:

<iframe id="igraph" scrolling="no" style="border:none;"seamless="seamless" src="http://dashboardleftjoin-env.eba-qxzgfj64.us-east-2.elasticbeanstalk.com" height="1100" width="800"></iframe>

В итоге получим такой дашборд на сайте:

Полный код проекта на GitHub

Ранее Ctrl + ↓